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 ;
23
26
import javax .annotation .Nullable ;
24
27
import javax .annotation .concurrent .Immutable ;
25
28
import java .util .ArrayList ;
26
29
import java .util .Collections ;
27
30
import java .util .HashMap ;
28
31
import java .util .List ;
29
32
import java .util .Map ;
33
+ import java .util .concurrent .ConcurrentHashMap ;
30
34
31
35
/**
32
36
* Represents the Optimizely Project configuration.
@@ -54,6 +58,10 @@ public String toString() {
54
58
}
55
59
}
56
60
61
+ // logger
62
+ private static final Logger logger = LoggerFactory .getLogger (ProjectConfig .class );
63
+
64
+ // ProjectConfig properties
57
65
private final String accountId ;
58
66
private final String projectId ;
59
67
private final String revision ;
@@ -85,6 +93,14 @@ public String toString() {
85
93
private final Map <String , Map <String , LiveVariableUsageInstance >> variationToLiveVariableUsageInstanceMapping ;
86
94
private final Map <String , Experiment > variationIdToExperimentMapping ;
87
95
96
+ /**
97
+ * Forced variations supersede any other mappings. They are transient and are not persistent or part of
98
+ * the actual datafile. This contains all the forced variations
99
+ * set by the user by calling {@link ProjectConfig#setForcedVariation(String, String, String)} (it is not the same as the
100
+ * whitelisting forcedVariations data structure in the Experiments class).
101
+ */
102
+ private transient ConcurrentHashMap <String , ConcurrentHashMap <String , String >> forcedVariationMapping = new ConcurrentHashMap <String , ConcurrentHashMap <String , String >>();
103
+
88
104
// v2 constructor
89
105
public ProjectConfig (String accountId , String projectId , String version , String revision , List <Group > groups ,
90
106
List <Experiment > experiments , List <Attribute > attributes , List <EventType > eventType ,
@@ -318,6 +334,136 @@ public Map<String, FeatureFlag> getFeatureKeyMapping() {
318
334
return featureKeyMapping ;
319
335
}
320
336
337
+ public ConcurrentHashMap <String , ConcurrentHashMap <String , String >> getForcedVariationMapping () { return forcedVariationMapping ; }
338
+
339
+ /**
340
+ * Force a user into a variation for a given experiment.
341
+ * The forced variation value does not persist across application launches.
342
+ * If the experiment key is not in the project file, this call fails and returns false.
343
+ *
344
+ * @param experimentKey The key for the experiment.
345
+ * @param userId The user ID to be used for bucketing.
346
+ * @param variationKey The variation key to force the user into. If the variation key is null
347
+ * then the forcedVariation for that experiment is removed.
348
+ *
349
+ * @return boolean A boolean value that indicates if the set completed successfully.
350
+ */
351
+ public boolean setForcedVariation (@ Nonnull String experimentKey ,
352
+ @ Nonnull String userId ,
353
+ @ Nullable String variationKey ) {
354
+
355
+ // if the experiment is not a valid experiment key, don't set it.
356
+ Experiment experiment = getExperimentKeyMapping ().get (experimentKey );
357
+ if (experiment == null ){
358
+ logger .error ("Experiment {} does not exist in ProjectConfig for project {}" , experimentKey , projectId );
359
+ return false ;
360
+ }
361
+
362
+ Variation variation = null ;
363
+
364
+ // keep in mind that you can pass in a variationKey that is null if you want to
365
+ // remove the variation.
366
+ if (variationKey != null ) {
367
+ variation = experiment .getVariationKeyToVariationMap ().get (variationKey );
368
+ // if the variation is not part of the experiment, return false.
369
+ if (variation == null ) {
370
+ logger .error ("Variation {} does not exist for experiment {}" , variationKey , experimentKey );
371
+ return false ;
372
+ }
373
+ }
374
+
375
+ // if the user id is invalid, return false.
376
+ if (userId == null || userId .trim ().isEmpty ()) {
377
+ logger .error ("User ID is invalid" );
378
+ return false ;
379
+ }
380
+
381
+ ConcurrentHashMap <String , String > experimentToVariation ;
382
+ if (!forcedVariationMapping .containsKey (userId )) {
383
+ forcedVariationMapping .putIfAbsent (userId , new ConcurrentHashMap <String , String >());
384
+ }
385
+ experimentToVariation = forcedVariationMapping .get (userId );
386
+
387
+ boolean retVal = true ;
388
+ // if it is null remove the variation if it exists.
389
+ if (variationKey == null ) {
390
+ String removedVariationId = experimentToVariation .remove (experiment .getId ());
391
+ if (removedVariationId != null ) {
392
+ Variation removedVariation = experiment .getVariationIdToVariationMap ().get (removedVariationId );
393
+ if (removedVariation != null ) {
394
+ logger .debug ("Variation mapped to experiment \" %s\" has been removed for user \" %s\" " , experiment .getKey (), userId );
395
+ }
396
+ else {
397
+ logger .debug ("Removed forced variation that did not exist in experiment" );
398
+ }
399
+ }
400
+ else {
401
+ logger .debug ("No variation for experiment {}" , experimentKey );
402
+ retVal = false ;
403
+ }
404
+ }
405
+ else {
406
+ String previous = experimentToVariation .put (experiment .getId (), variation .getId ());
407
+ logger .debug ("Set variation \" %s\" for experiment \" %s\" and user \" %s\" in the forced variation map." ,
408
+ variation .getKey (), experiment .getKey (), userId );
409
+ if (previous != null ) {
410
+ Variation previousVariation = experiment .getVariationIdToVariationMap ().get (previous );
411
+ if (previousVariation != null ) {
412
+ logger .debug ("forced variation {} replaced forced variation {} in forced variation map." ,
413
+ variation .getKey (), previousVariation .getKey ());
414
+ }
415
+ }
416
+ }
417
+
418
+ return retVal ;
419
+ }
420
+
421
+ /**
422
+ * Gets the forced variation for a given user and experiment.
423
+ *
424
+ * @param experimentKey The key for the experiment.
425
+ * @param userId The user ID to be used for bucketing.
426
+ *
427
+ * @return The variation the user was bucketed into. This value can be null if the
428
+ * forced variation fails.
429
+ */
430
+ public @ Nullable Variation getForcedVariation (@ Nonnull String experimentKey ,
431
+ @ Nonnull String userId ) {
432
+
433
+ // if the user id is invalid, return false.
434
+ if (userId == null || userId .trim ().isEmpty ()) {
435
+ logger .error ("User ID is invalid" );
436
+ return null ;
437
+ }
438
+
439
+ if (experimentKey == null || experimentKey .isEmpty ()) {
440
+ logger .error ("experiment key is invalid" );
441
+ return null ;
442
+ }
443
+
444
+ Map <String , String > experimentToVariation = getForcedVariationMapping ().get (userId );
445
+ if (experimentToVariation != null ) {
446
+ Experiment experiment = getExperimentKeyMapping ().get (experimentKey );
447
+ if (experiment == null ) {
448
+ logger .debug ("No experiment \" %s\" mapped to user \" %s\" in the forced variation map " , experimentKey , userId );
449
+ return null ;
450
+ }
451
+ String variationId = experimentToVariation .get (experiment .getId ());
452
+ if (variationId != null ) {
453
+ Variation variation = experiment .getVariationIdToVariationMap ().get (variationId );
454
+ if (variation != null ) {
455
+ logger .debug ("Variation \" %s\" is mapped to experiment \" %s\" and user \" %s\" in the forced variation map" ,
456
+ variation .getKey (), experimentKey , userId );
457
+ return variation ;
458
+ }
459
+ }
460
+ else {
461
+ logger .debug ("No variation for experiment \" %s\" mapped to user \" %s\" in the forced variation map " , experimentKey , userId );
462
+ }
463
+ }
464
+ return null ;
465
+ }
466
+
321
467
@ Override
322
468
public String toString () {
323
469
return "ProjectConfig{" +
@@ -344,6 +490,7 @@ public String toString() {
344
490
", groupIdMapping=" + groupIdMapping +
345
491
", liveVariableIdToExperimentsMapping=" + liveVariableIdToExperimentsMapping +
346
492
", variationToLiveVariableUsageInstanceMapping=" + variationToLiveVariableUsageInstanceMapping +
493
+ ", forcedVariationMapping=" + forcedVariationMapping +
347
494
", variationIdToExperimentMapping=" + variationIdToExperimentMapping +
348
495
'}' ;
349
496
}
0 commit comments