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 ;
26+ import javax .annotation .Nullable ;
2327import javax .annotation .concurrent .Immutable ;
2428import java .util .ArrayList ;
2529import java .util .Collections ;
2630import java .util .List ;
2731import java .util .Map ;
32+ import java .util .concurrent .ConcurrentHashMap ;
2833
2934/**
3035 * Represents the Optimizely Project configuration.
@@ -52,6 +57,10 @@ public String toString() {
5257 }
5358 }
5459
60+ // logger
61+ private static final Logger logger = LoggerFactory .getLogger (ProjectConfig .class );
62+
63+ // ProjectConfig properties
5564 private final String accountId ;
5665 private final String projectId ;
5766 private final String revision ;
@@ -75,6 +84,15 @@ public String toString() {
7584 private final Map <String , List <Experiment >> liveVariableIdToExperimentsMapping ;
7685 private final Map <String , Map <String , LiveVariableUsageInstance >> variationToLiveVariableUsageInstanceMapping ;
7786
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
7896 public ProjectConfig (String accountId , String projectId , String version , String revision , List <Group > groups ,
7997 List <Experiment > experiments , List <Attribute > attributes , List <EventType > eventType ,
8098 List <Audience > audiences ) {
@@ -236,6 +254,136 @@ public Map<String, Map<String, LiveVariableUsageInstance>> getVariationToLiveVar
236254 return variationToLiveVariableUsageInstanceMapping ;
237255 }
238256
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+
239387 @ Override
240388 public String toString () {
241389 return "ProjectConfig{" +
@@ -259,6 +407,7 @@ public String toString() {
259407 ", groupIdMapping=" + groupIdMapping +
260408 ", liveVariableIdToExperimentsMapping=" + liveVariableIdToExperimentsMapping +
261409 ", variationToLiveVariableUsageInstanceMapping=" + variationToLiveVariableUsageInstanceMapping +
410+ ", forcedVariationMapping=" + forcedVariationMapping +
262411 '}' ;
263412 }
264413}
0 commit comments