1919import com .fasterxml .jackson .annotation .JsonIgnoreProperties ;
2020import com .optimizely .ab .config .audience .Audience ;
2121import com .optimizely .ab .config .audience .Condition ;
22+ import org .slf4j .Logger ;
23+ import org .slf4j .LoggerFactory ;
2224
25+ import javax .annotation .Nonnull ;
2326import javax .annotation .Nullable ;
2427import javax .annotation .concurrent .Immutable ;
2528import java .util .ArrayList ;
2629import java .util .Collections ;
2730import java .util .HashMap ;
2831import java .util .List ;
2932import java .util .Map ;
33+ import java .util .concurrent .ConcurrentHashMap ;
3034
3135/**
3236 * Represents the Optimizely Project configuration.
@@ -54,6 +58,10 @@ public String toString() {
5458 }
5559 }
5660
61+ // logger
62+ private static final Logger logger = LoggerFactory .getLogger (ProjectConfig .class );
63+
64+ // ProjectConfig properties
5765 private final String accountId ;
5866 private final String projectId ;
5967 private final String revision ;
@@ -85,6 +93,14 @@ public String toString() {
8593 private final Map <String , Map <String , LiveVariableUsageInstance >> variationToLiveVariableUsageInstanceMapping ;
8694 private final Map <String , Experiment > variationIdToExperimentMapping ;
8795
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+
88104 // v2 constructor
89105 public ProjectConfig (String accountId , String projectId , String version , String revision , List <Group > groups ,
90106 List <Experiment > experiments , List <Attribute > attributes , List <EventType > eventType ,
@@ -318,6 +334,136 @@ public Map<String, FeatureFlag> getFeatureKeyMapping() {
318334 return featureKeyMapping ;
319335 }
320336
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+
321467 @ Override
322468 public String toString () {
323469 return "ProjectConfig{" +
@@ -344,6 +490,7 @@ public String toString() {
344490 ", groupIdMapping=" + groupIdMapping +
345491 ", liveVariableIdToExperimentsMapping=" + liveVariableIdToExperimentsMapping +
346492 ", variationToLiveVariableUsageInstanceMapping=" + variationToLiveVariableUsageInstanceMapping +
493+ ", forcedVariationMapping=" + forcedVariationMapping +
347494 ", variationIdToExperimentMapping=" + variationIdToExperimentMapping +
348495 '}' ;
349496 }
0 commit comments