19
19
import com .fasterxml .jackson .annotation .JsonIgnoreProperties ;
20
20
import com .optimizely .ab .config .audience .Audience ;
21
21
import com .optimizely .ab .config .audience .Condition ;
22
+ import org .slf4j .Logger ;
23
+ import org .slf4j .LoggerFactory ;
22
24
25
+ import javax .annotation .Nonnull ;
26
+ import javax .annotation .Nullable ;
23
27
import javax .annotation .concurrent .Immutable ;
24
28
import java .util .ArrayList ;
25
29
import java .util .Collections ;
26
30
import java .util .List ;
27
31
import java .util .Map ;
32
+ import java .util .concurrent .ConcurrentHashMap ;
28
33
29
34
/**
30
35
* Represents the Optimizely Project configuration.
@@ -52,6 +57,10 @@ public String toString() {
52
57
}
53
58
}
54
59
60
+ // logger
61
+ private static final Logger logger = LoggerFactory .getLogger (ProjectConfig .class );
62
+
63
+ // ProjectConfig properties
55
64
private final String accountId ;
56
65
private final String projectId ;
57
66
private final String revision ;
@@ -75,6 +84,15 @@ public String toString() {
75
84
private final Map <String , List <Experiment >> liveVariableIdToExperimentsMapping ;
76
85
private final Map <String , Map <String , LiveVariableUsageInstance >> variationToLiveVariableUsageInstanceMapping ;
77
86
87
+ /**
88
+ * Forced variations supersede any other mappings. They are transient and are not persistent or part of
89
+ * the actual datafile. This contains all the forced variations
90
+ * set by the user by calling {@link ProjectConfig#setForcedVariation(String, String, String)} (it is not the same as the
91
+ * whitelisting forcedVariations data structure in the Experiments class).
92
+ */
93
+ private transient ConcurrentHashMap <String , ConcurrentHashMap <String , String >> forcedVariationMapping = new ConcurrentHashMap <String , ConcurrentHashMap <String , String >>();
94
+
95
+ // v2 constructor
78
96
public ProjectConfig (String accountId , String projectId , String version , String revision , List <Group > groups ,
79
97
List <Experiment > experiments , List <Attribute > attributes , List <EventType > eventType ,
80
98
List <Audience > audiences ) {
@@ -236,6 +254,136 @@ public Map<String, Map<String, LiveVariableUsageInstance>> getVariationToLiveVar
236
254
return variationToLiveVariableUsageInstanceMapping ;
237
255
}
238
256
257
+ public ConcurrentHashMap <String , ConcurrentHashMap <String , String >> getForcedVariationMapping () { return forcedVariationMapping ; }
258
+
259
+ /**
260
+ * Force a user into a variation for a given experiment.
261
+ * The forced variation value does not persist across application launches.
262
+ * If the experiment key is not in the project file, this call fails and returns false.
263
+ *
264
+ * @param experimentKey The key for the experiment.
265
+ * @param userId The user ID to be used for bucketing.
266
+ * @param variationKey The variation key to force the user into. If the variation key is null
267
+ * then the forcedVariation for that experiment is removed.
268
+ *
269
+ * @return boolean A boolean value that indicates if the set completed successfully.
270
+ */
271
+ public boolean setForcedVariation (@ Nonnull String experimentKey ,
272
+ @ Nonnull String userId ,
273
+ @ Nullable String variationKey ) {
274
+
275
+ // if the experiment is not a valid experiment key, don't set it.
276
+ Experiment experiment = getExperimentKeyMapping ().get (experimentKey );
277
+ if (experiment == null ){
278
+ logger .error ("Experiment {} does not exist in ProjectConfig for project {}" , experimentKey , projectId );
279
+ return false ;
280
+ }
281
+
282
+ Variation variation = null ;
283
+
284
+ // keep in mind that you can pass in a variationKey that is null if you want to
285
+ // remove the variation.
286
+ if (variationKey != null ) {
287
+ variation = experiment .getVariationKeyToVariationMap ().get (variationKey );
288
+ // if the variation is not part of the experiment, return false.
289
+ if (variation == null ) {
290
+ logger .error ("Variation {} does not exist for experiment {}" , variationKey , experimentKey );
291
+ return false ;
292
+ }
293
+ }
294
+
295
+ // if the user id is invalid, return false.
296
+ if (userId == null || userId .trim ().isEmpty ()) {
297
+ logger .error ("User ID is invalid" );
298
+ return false ;
299
+ }
300
+
301
+ ConcurrentHashMap <String , String > experimentToVariation ;
302
+ if (!forcedVariationMapping .containsKey (userId )) {
303
+ forcedVariationMapping .putIfAbsent (userId , new ConcurrentHashMap <String , String >());
304
+ }
305
+ experimentToVariation = forcedVariationMapping .get (userId );
306
+
307
+ boolean retVal = true ;
308
+ // if it is null remove the variation if it exists.
309
+ if (variationKey == null ) {
310
+ String removedVariationId = experimentToVariation .remove (experiment .getId ());
311
+ if (removedVariationId != null ) {
312
+ Variation removedVariation = experiment .getVariationIdToVariationMap ().get (removedVariationId );
313
+ if (removedVariation != null ) {
314
+ logger .debug ("Variation mapped to experiment \" %s\" has been removed for user \" %s\" " , experiment .getKey (), userId );
315
+ }
316
+ else {
317
+ logger .debug ("Removed forced variation that did not exist in experiment" );
318
+ }
319
+ }
320
+ else {
321
+ logger .debug ("No variation for experiment {}" , experimentKey );
322
+ retVal = false ;
323
+ }
324
+ }
325
+ else {
326
+ String previous = experimentToVariation .put (experiment .getId (), variation .getId ());
327
+ logger .debug ("Set variation \" %s\" for experiment \" %s\" and user \" %s\" in the forced variation map." ,
328
+ variation .getKey (), experiment .getKey (), userId );
329
+ if (previous != null ) {
330
+ Variation previousVariation = experiment .getVariationIdToVariationMap ().get (previous );
331
+ if (previousVariation != null ) {
332
+ logger .debug ("forced variation {} replaced forced variation {} in forced variation map." ,
333
+ variation .getKey (), previousVariation .getKey ());
334
+ }
335
+ }
336
+ }
337
+
338
+ return retVal ;
339
+ }
340
+
341
+ /**
342
+ * Gets the forced variation for a given user and experiment.
343
+ *
344
+ * @param experimentKey The key for the experiment.
345
+ * @param userId The user ID to be used for bucketing.
346
+ *
347
+ * @return The variation the user was bucketed into. This value can be null if the
348
+ * forced variation fails.
349
+ */
350
+ public @ Nullable Variation getForcedVariation (@ Nonnull String experimentKey ,
351
+ @ Nonnull String userId ) {
352
+
353
+ // if the user id is invalid, return false.
354
+ if (userId == null || userId .trim ().isEmpty ()) {
355
+ logger .error ("User ID is invalid" );
356
+ return null ;
357
+ }
358
+
359
+ if (experimentKey == null || experimentKey .isEmpty ()) {
360
+ logger .error ("experiment key is invalid" );
361
+ return null ;
362
+ }
363
+
364
+ Map <String , String > experimentToVariation = getForcedVariationMapping ().get (userId );
365
+ if (experimentToVariation != null ) {
366
+ Experiment experiment = getExperimentKeyMapping ().get (experimentKey );
367
+ if (experiment == null ) {
368
+ logger .debug ("No experiment \" %s\" mapped to user \" %s\" in the forced variation map " , experimentKey , userId );
369
+ return null ;
370
+ }
371
+ String variationId = experimentToVariation .get (experiment .getId ());
372
+ if (variationId != null ) {
373
+ Variation variation = experiment .getVariationIdToVariationMap ().get (variationId );
374
+ if (variation != null ) {
375
+ logger .debug ("Variation \" %s\" is mapped to experiment \" %s\" and user \" %s\" in the forced variation map" ,
376
+ variation .getKey (), experimentKey , userId );
377
+ return variation ;
378
+ }
379
+ }
380
+ else {
381
+ logger .debug ("No variation for experiment \" %s\" mapped to user \" %s\" in the forced variation map " , experimentKey , userId );
382
+ }
383
+ }
384
+ return null ;
385
+ }
386
+
239
387
@ Override
240
388
public String toString () {
241
389
return "ProjectConfig{" +
@@ -259,6 +407,7 @@ public String toString() {
259
407
", groupIdMapping=" + groupIdMapping +
260
408
", liveVariableIdToExperimentsMapping=" + liveVariableIdToExperimentsMapping +
261
409
", variationToLiveVariableUsageInstanceMapping=" + variationToLiveVariableUsageInstanceMapping +
410
+ ", forcedVariationMapping=" + forcedVariationMapping +
262
411
'}' ;
263
412
}
264
413
}
0 commit comments