3434import com .optimizely .ab .optimizelyconfig .OptimizelyConfig ;
3535import com .optimizely .ab .optimizelyconfig .OptimizelyConfigManager ;
3636import com .optimizely .ab .optimizelyconfig .OptimizelyConfigService ;
37+ import com .optimizely .ab .optimizelydecision .DecisionMessage ;
38+ import com .optimizely .ab .optimizelydecision .DecisionReasons ;
39+ import com .optimizely .ab .optimizelydecision .DefaultDecisionReasons ;
40+ import com .optimizely .ab .optimizelydecision .OptimizelyDecideOption ;
41+ import com .optimizely .ab .optimizelydecision .OptimizelyDecision ;
3742import com .optimizely .ab .optimizelyjson .OptimizelyJSON ;
3843import org .slf4j .Logger ;
3944import org .slf4j .LoggerFactory ;
@@ -76,7 +81,6 @@ public class Optimizely implements AutoCloseable {
7681
7782 private static final Logger logger = LoggerFactory .getLogger (Optimizely .class );
7883
79- @ VisibleForTesting
8084 final DecisionService decisionService ;
8185 @ VisibleForTesting
8286 @ Deprecated
@@ -86,6 +90,8 @@ public class Optimizely implements AutoCloseable {
8690 @ VisibleForTesting
8791 final ErrorHandler errorHandler ;
8892
93+ public final List <OptimizelyDecideOption > defaultDecideOptions ;
94+
8995 private final ProjectConfigManager projectConfigManager ;
9096
9197 @ Nullable
@@ -104,7 +110,8 @@ private Optimizely(@Nonnull EventHandler eventHandler,
104110 @ Nullable UserProfileService userProfileService ,
105111 @ Nonnull ProjectConfigManager projectConfigManager ,
106112 @ Nullable OptimizelyConfigManager optimizelyConfigManager ,
107- @ Nonnull NotificationCenter notificationCenter
113+ @ Nonnull NotificationCenter notificationCenter ,
114+ @ Nonnull List <OptimizelyDecideOption > defaultDecideOptions
108115 ) {
109116 this .eventHandler = eventHandler ;
110117 this .eventProcessor = eventProcessor ;
@@ -114,6 +121,7 @@ private Optimizely(@Nonnull EventHandler eventHandler,
114121 this .projectConfigManager = projectConfigManager ;
115122 this .optimizelyConfigManager = optimizelyConfigManager ;
116123 this .notificationCenter = notificationCenter ;
124+ this .defaultDecideOptions = defaultDecideOptions ;
117125 }
118126
119127 /**
@@ -779,7 +787,6 @@ <T> T getFeatureVariableValueForType(@Nonnull String featureKey,
779787 }
780788
781789 // Helper method which takes type and variable value and convert it to object to use in Listener DecisionInfo object variable value
782- @ VisibleForTesting
783790 Object convertStringToType (String variableValue , String type ) {
784791 if (variableValue != null ) {
785792 switch (type ) {
@@ -1129,6 +1136,202 @@ public OptimizelyConfig getOptimizelyConfig() {
11291136 return new OptimizelyConfigService (projectConfig ).getConfig ();
11301137 }
11311138
1139+ //============ decide ============//
1140+
1141+ /**
1142+ * Create a context of the user for which decision APIs will be called.
1143+ *
1144+ * A user context will be created successfully even when the SDK is not fully configured yet.
1145+ *
1146+ * @param userId The user ID to be used for bucketing.
1147+ * @param attributes: A map of attribute names to current user attribute values.
1148+ * @return An OptimizelyUserContext associated with this OptimizelyClient.
1149+ */
1150+ public OptimizelyUserContext createUserContext (@ Nonnull String userId ,
1151+ @ Nonnull Map <String , Object > attributes ) {
1152+ if (userId == null ) {
1153+ logger .warn ("The userId parameter must be nonnull." );
1154+ return null ;
1155+ }
1156+
1157+ return new OptimizelyUserContext (this , userId , attributes );
1158+ }
1159+
1160+ public OptimizelyUserContext createUserContext (@ Nonnull String userId ) {
1161+ return new OptimizelyUserContext (this , userId );
1162+ }
1163+
1164+ OptimizelyDecision decide (@ Nonnull OptimizelyUserContext user ,
1165+ @ Nonnull String key ,
1166+ @ Nonnull List <OptimizelyDecideOption > options ) {
1167+
1168+ ProjectConfig projectConfig = getProjectConfig ();
1169+ if (projectConfig == null ) {
1170+ return OptimizelyDecision .newErrorDecision (key , user , DecisionMessage .SDK_NOT_READY .reason ());
1171+ }
1172+
1173+ FeatureFlag flag = projectConfig .getFeatureKeyMapping ().get (key );
1174+ if (flag == null ) {
1175+ return OptimizelyDecision .newErrorDecision (key , user , DecisionMessage .FLAG_KEY_INVALID .reason (key ));
1176+ }
1177+
1178+ String userId = user .getUserId ();
1179+ Map <String , Object > attributes = user .getAttributes ();
1180+ Boolean decisionEventDispatched = false ;
1181+ List <OptimizelyDecideOption > allOptions = getAllOptions (options );
1182+ DecisionReasons decisionReasons = DefaultDecisionReasons .newInstance (allOptions );
1183+
1184+ Map <String , ?> copiedAttributes = new HashMap <>(attributes );
1185+ FeatureDecision flagDecision = decisionService .getVariationForFeature (
1186+ flag ,
1187+ userId ,
1188+ copiedAttributes ,
1189+ projectConfig ,
1190+ allOptions ,
1191+ decisionReasons );
1192+
1193+ Boolean flagEnabled = false ;
1194+ if (flagDecision .variation != null ) {
1195+ if (flagDecision .variation .getFeatureEnabled ()) {
1196+ flagEnabled = true ;
1197+ }
1198+ }
1199+ logger .info ("Feature \" {}\" is enabled for user \" {}\" ? {}" , key , userId , flagEnabled );
1200+
1201+ Map <String , Object > variableMap = new HashMap <>();
1202+ if (!allOptions .contains (OptimizelyDecideOption .EXCLUDE_VARIABLES )) {
1203+ variableMap = getDecisionVariableMap (
1204+ flag ,
1205+ flagDecision .variation ,
1206+ flagEnabled ,
1207+ decisionReasons );
1208+ }
1209+ OptimizelyJSON optimizelyJSON = new OptimizelyJSON (variableMap );
1210+
1211+ FeatureDecision .DecisionSource decisionSource = FeatureDecision .DecisionSource .ROLLOUT ;
1212+ if (flagDecision .decisionSource != null ) {
1213+ decisionSource = flagDecision .decisionSource ;
1214+ }
1215+
1216+ List <String > reasonsToReport = decisionReasons .toReport ();
1217+ String variationKey = flagDecision .variation != null ? flagDecision .variation .getKey () : null ;
1218+ // TODO: add ruleKey values when available later. use a copy of experimentKey until then.
1219+ // add to event metadata as well (currently set to experimentKey)
1220+ String ruleKey = flagDecision .experiment != null ? flagDecision .experiment .getKey () : null ;
1221+
1222+ if (!allOptions .contains (OptimizelyDecideOption .DISABLE_DECISION_EVENT )) {
1223+ sendImpression (
1224+ projectConfig ,
1225+ flagDecision .experiment ,
1226+ userId ,
1227+ copiedAttributes ,
1228+ flagDecision .variation ,
1229+ key ,
1230+ decisionSource .toString (),
1231+ flagEnabled );
1232+ decisionEventDispatched = true ;
1233+ }
1234+
1235+ DecisionNotification decisionNotification = DecisionNotification .newFlagDecisionNotificationBuilder ()
1236+ .withUserId (userId )
1237+ .withAttributes (copiedAttributes )
1238+ .withFlagKey (key )
1239+ .withEnabled (flagEnabled )
1240+ .withVariables (variableMap )
1241+ .withVariationKey (variationKey )
1242+ .withRuleKey (ruleKey )
1243+ .withReasons (reasonsToReport )
1244+ .withDecisionEventDispatched (decisionEventDispatched )
1245+ .build ();
1246+ notificationCenter .send (decisionNotification );
1247+
1248+ return new OptimizelyDecision (
1249+ variationKey ,
1250+ flagEnabled ,
1251+ optimizelyJSON ,
1252+ ruleKey ,
1253+ key ,
1254+ user ,
1255+ reasonsToReport );
1256+ }
1257+
1258+ Map <String , OptimizelyDecision > decideForKeys (@ Nonnull OptimizelyUserContext user ,
1259+ @ Nonnull List <String > keys ,
1260+ @ Nonnull List <OptimizelyDecideOption > options ) {
1261+ Map <String , OptimizelyDecision > decisionMap = new HashMap <>();
1262+
1263+ ProjectConfig projectConfig = getProjectConfig ();
1264+ if (projectConfig == null ) {
1265+ logger .error ("Optimizely instance is not valid, failing isFeatureEnabled call." );
1266+ return decisionMap ;
1267+ }
1268+
1269+ if (keys .isEmpty ()) return decisionMap ;
1270+
1271+ List <OptimizelyDecideOption > allOptions = getAllOptions (options );
1272+
1273+ for (String key : keys ) {
1274+ OptimizelyDecision decision = decide (user , key , options );
1275+ if (!allOptions .contains (OptimizelyDecideOption .ENABLED_FLAGS_ONLY ) || decision .getEnabled ()) {
1276+ decisionMap .put (key , decision );
1277+ }
1278+ }
1279+
1280+ return decisionMap ;
1281+ }
1282+
1283+ Map <String , OptimizelyDecision > decideAll (@ Nonnull OptimizelyUserContext user ,
1284+ @ Nonnull List <OptimizelyDecideOption > options ) {
1285+ Map <String , OptimizelyDecision > decisionMap = new HashMap <>();
1286+
1287+ ProjectConfig projectConfig = getProjectConfig ();
1288+ if (projectConfig == null ) {
1289+ logger .error ("Optimizely instance is not valid, failing isFeatureEnabled call." );
1290+ return decisionMap ;
1291+ }
1292+
1293+ List <FeatureFlag > allFlags = projectConfig .getFeatureFlags ();
1294+ List <String > allFlagKeys = new ArrayList <>();
1295+ for (int i = 0 ; i < allFlags .size (); i ++) allFlagKeys .add (allFlags .get (i ).getKey ());
1296+
1297+ return decideForKeys (user , allFlagKeys , options );
1298+ }
1299+
1300+ private List <OptimizelyDecideOption > getAllOptions (List <OptimizelyDecideOption > options ) {
1301+ List <OptimizelyDecideOption > copiedOptions = new ArrayList (defaultDecideOptions );
1302+ if (options != null ) {
1303+ copiedOptions .addAll (options );
1304+ }
1305+ return copiedOptions ;
1306+ }
1307+
1308+ private Map <String , Object > getDecisionVariableMap (@ Nonnull FeatureFlag flag ,
1309+ @ Nonnull Variation variation ,
1310+ @ Nonnull Boolean featureEnabled ,
1311+ @ Nonnull DecisionReasons decisionReasons ) {
1312+ Map <String , Object > valuesMap = new HashMap <String , Object >();
1313+ for (FeatureVariable variable : flag .getVariables ()) {
1314+ String value = variable .getDefaultValue ();
1315+ if (featureEnabled ) {
1316+ FeatureVariableUsageInstance instance = variation .getVariableIdToFeatureVariableUsageInstanceMap ().get (variable .getId ());
1317+ if (instance != null ) {
1318+ value = instance .getValue ();
1319+ }
1320+ }
1321+
1322+ Object convertedValue = convertStringToType (value , variable .getType ());
1323+ if (convertedValue == null ) {
1324+ decisionReasons .addError (DecisionMessage .VARIABLE_VALUE_INVALID .reason (variable .getKey ()));
1325+ } else if (convertedValue instanceof OptimizelyJSON ) {
1326+ convertedValue = ((OptimizelyJSON ) convertedValue ).toMap ();
1327+ }
1328+
1329+ valuesMap .put (variable .getKey (), convertedValue );
1330+ }
1331+
1332+ return valuesMap ;
1333+ }
1334+
11321335 /**
11331336 * Helper method which makes separate copy of attributesMap variable and returns it
11341337 *
@@ -1233,6 +1436,7 @@ public static class Builder {
12331436 private OptimizelyConfigManager optimizelyConfigManager ;
12341437 private UserProfileService userProfileService ;
12351438 private NotificationCenter notificationCenter ;
1439+ private List <OptimizelyDecideOption > defaultDecideOptions ;
12361440
12371441 // For backwards compatibility
12381442 private AtomicProjectConfigManager fallbackConfigManager = new AtomicProjectConfigManager ();
@@ -1304,6 +1508,11 @@ public Builder withDatafile(String datafile) {
13041508 return this ;
13051509 }
13061510
1511+ public Builder withDefaultDecideOptions (List <OptimizelyDecideOption > defaultDecideOtions ) {
1512+ this .defaultDecideOptions = defaultDecideOtions ;
1513+ return this ;
1514+ }
1515+
13071516 // Helper functions for making testing easier
13081517 protected Builder withBucketing (Bucketer bucketer ) {
13091518 this .bucketer = bucketer ;
@@ -1372,7 +1581,13 @@ public Optimizely build() {
13721581 eventProcessor = new ForwardingEventProcessor (eventHandler , notificationCenter );
13731582 }
13741583
1375- return new Optimizely (eventHandler , eventProcessor , errorHandler , decisionService , userProfileService , projectConfigManager , optimizelyConfigManager , notificationCenter );
1584+ if (defaultDecideOptions != null ) {
1585+ defaultDecideOptions = Collections .unmodifiableList (defaultDecideOptions );
1586+ } else {
1587+ defaultDecideOptions = Collections .emptyList ();
1588+ }
1589+
1590+ return new Optimizely (eventHandler , eventProcessor , errorHandler , decisionService , userProfileService , projectConfigManager , optimizelyConfigManager , notificationCenter , defaultDecideOptions );
13761591 }
13771592 }
13781593}
0 commit comments