1+ package com .optimizely .ab .config ;
2+
3+ import javax .annotation .Nonnull ;
4+ import javax .annotation .Nullable ;
5+ import java .util .*;
6+ import java .util .concurrent .ConcurrentHashMap ;
7+
8+ /**
9+ * HoldoutConfig manages collections of Holdout objects and their relationships to flags.
10+ */
11+ public class HoldoutConfig {
12+ private List <Holdout > allHoldouts ;
13+ private List <Holdout > global ;
14+ private Map <String , Holdout > holdoutIdMap ;
15+ private Map <String , List <Holdout >> flagHoldoutsMap ;
16+ private Map <String , List <Holdout >> includedHoldouts ;
17+ private Map <String , List <Holdout >> excludedHoldouts ;
18+
19+ /**
20+ * Initializes a new HoldoutConfig with an empty list of holdouts.
21+ */
22+ public HoldoutConfig () {
23+ this (Collections .emptyList ());
24+ }
25+
26+ /**
27+ * Initializes a new HoldoutConfig with the specified holdouts.
28+ *
29+ * @param allHoldouts The list of holdouts to manage
30+ */
31+ public HoldoutConfig (@ Nonnull List <Holdout > allHoldouts ) {
32+ this .allHoldouts = new ArrayList <>(allHoldouts );
33+ this .global = new ArrayList <>();
34+ this .holdoutIdMap = new HashMap <>();
35+ this .flagHoldoutsMap = new ConcurrentHashMap <>();
36+ this .includedHoldouts = new HashMap <>();
37+ this .excludedHoldouts = new HashMap <>();
38+ updateHoldoutMapping ();
39+ }
40+
41+ /**
42+ * Updates internal mappings of holdouts including the id map, global list,
43+ * and per-flag inclusion/exclusion maps.
44+ */
45+ public void updateHoldoutMapping () {
46+ holdoutIdMap .clear ();
47+ for (Holdout holdout : allHoldouts ) {
48+ holdoutIdMap .put (holdout .getId (), holdout );
49+ }
50+
51+ flagHoldoutsMap .clear ();
52+ global .clear ();
53+ includedHoldouts .clear ();
54+ excludedHoldouts .clear ();
55+
56+ for (Holdout holdout : allHoldouts ) {
57+ boolean hasIncludedFlags = !holdout .getIncludedFlags ().isEmpty ();
58+ boolean hasExcludedFlags = !holdout .getExcludedFlags ().isEmpty ();
59+
60+ if (!hasIncludedFlags && !hasExcludedFlags ) {
61+ // Global holdout (applies to all flags)
62+ global .add (holdout );
63+ } else if (hasIncludedFlags ) {
64+ // Holdout only applies to specific included flags
65+ for (String flagId : holdout .getIncludedFlags ()) {
66+ includedHoldouts .computeIfAbsent (flagId , k -> new ArrayList <>()).add (holdout );
67+ }
68+ } else {
69+ // Global holdout with specific exclusions
70+ global .add (holdout );
71+
72+ for (String flagId : holdout .getExcludedFlags ()) {
73+ excludedHoldouts .computeIfAbsent (flagId , k -> new ArrayList <>()).add (holdout );
74+ }
75+ }
76+ }
77+ }
78+
79+ /**
80+ * Returns the applicable holdouts for the given flag ID by combining global holdouts
81+ * (excluding any specified) and included holdouts, in that order.
82+ * Caches the result for future calls.
83+ *
84+ * @param id The flag identifier
85+ * @return A list of Holdout objects relevant to the given flag
86+ */
87+ public List <Holdout > getHoldoutForFlag (@ Nonnull String id ) {
88+ if (allHoldouts .isEmpty ()) {
89+ return Collections .emptyList ();
90+ }
91+
92+ // Check cache and return persistent holdouts
93+ if (flagHoldoutsMap .containsKey (id )) {
94+ return flagHoldoutsMap .get (id );
95+ }
96+
97+ // Prioritize global holdouts first
98+ List <Holdout > activeHoldouts = new ArrayList <>();
99+ List <Holdout > excluded = excludedHoldouts .getOrDefault (id , Collections .emptyList ());
100+
101+ if (!excluded .isEmpty ()) {
102+ for (Holdout holdout : global ) {
103+ if (!excluded .contains (holdout )) {
104+ activeHoldouts .add (holdout );
105+ }
106+ }
107+ } else {
108+ activeHoldouts .addAll (global );
109+ }
110+
111+ // Add included holdouts
112+ activeHoldouts .addAll (includedHoldouts .getOrDefault (id , Collections .emptyList ()));
113+
114+ // Cache the result
115+ flagHoldoutsMap .put (id , activeHoldouts );
116+
117+ return activeHoldouts ;
118+ }
119+
120+ /**
121+ * Get a Holdout object for an Id.
122+ *
123+ * @param id The holdout identifier
124+ * @return The Holdout object if found, null otherwise
125+ */
126+ @ Nullable
127+ public Holdout getHoldout (@ Nonnull String id ) {
128+ return holdoutIdMap .get (id );
129+ }
130+
131+ /**
132+ * Returns all holdouts managed by this config.
133+ *
134+ * @return An unmodifiable list of all holdouts
135+ */
136+ public List <Holdout > getAllHoldouts () {
137+ return Collections .unmodifiableList (allHoldouts );
138+ }
139+
140+ /**
141+ * Returns the global holdouts (those that apply to all flags by default).
142+ *
143+ * @return An unmodifiable list of global holdouts
144+ */
145+ public List <Holdout > getGlobal () {
146+ return Collections .unmodifiableList (global );
147+ }
148+
149+ }
0 commit comments