diff --git a/core-api/src/main/java/com/optimizely/ab/config/FeatureFlag.java b/core-api/src/main/java/com/optimizely/ab/config/FeatureFlag.java index 66c8b2675..915da05c5 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/FeatureFlag.java +++ b/core-api/src/main/java/com/optimizely/ab/config/FeatureFlag.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; +import java.util.Map; /** * Represents a FeatureFlag definition at the project level @@ -33,6 +34,7 @@ public class FeatureFlag implements IdKeyMapped{ private final String layerId; private final List experimentIds; private final List variables; + private final Map variableKeyToLiveVariableMap; @JsonCreator public FeatureFlag(@JsonProperty("id") String id, @@ -45,6 +47,7 @@ public FeatureFlag(@JsonProperty("id") String id, this.layerId = layerId; this.experimentIds = experimentIds; this.variables = variables; + this.variableKeyToLiveVariableMap = ProjectConfigUtils.generateNameMapping(variables); } public String getId() { @@ -67,6 +70,10 @@ public List getVariables() { return variables; } + public Map getVariableKeyToLiveVariableMap() { + return variableKeyToLiveVariableMap; + } + @Override public String toString() { return "FeatureFlag{" + @@ -75,6 +82,33 @@ public String toString() { ", layerId='" + layerId + '\'' + ", experimentIds=" + experimentIds + ", variables=" + variables + + ", variableKeyToLiveVariableMap=" + variableKeyToLiveVariableMap + '}'; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FeatureFlag that = (FeatureFlag) o; + + if (!id.equals(that.id)) return false; + if (!key.equals(that.key)) return false; + if (!layerId.equals(that.layerId)) return false; + if (!experimentIds.equals(that.experimentIds)) return false; + if (!variables.equals(that.variables)) return false; + return variableKeyToLiveVariableMap.equals(that.variableKeyToLiveVariableMap); + } + + @Override + public int hashCode() { + int result = id.hashCode(); + result = 31 * result + key.hashCode(); + result = 31 * result + layerId.hashCode(); + result = 31 * result + experimentIds.hashCode(); + result = 31 * result + variables.hashCode(); + result = 31 * result + variableKeyToLiveVariableMap.hashCode(); + return result; + } } diff --git a/core-api/src/main/java/com/optimizely/ab/config/LiveVariable.java b/core-api/src/main/java/com/optimizely/ab/config/LiveVariable.java index 74f656b6c..4ae910301 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/LiveVariable.java +++ b/core-api/src/main/java/com/optimizely/ab/config/LiveVariable.java @@ -22,6 +22,8 @@ import com.fasterxml.jackson.annotation.JsonValue; import com.google.gson.annotations.SerializedName; +import javax.annotation.Nullable; + /** * Represents a live variable definition at the project level */ @@ -100,7 +102,7 @@ public static VariableType fromString(String variableTypeString) { private final String key; private final String defaultValue; private final VariableType type; - private final VariableStatus status; + @Nullable private final VariableStatus status; @JsonCreator public LiveVariable(@JsonProperty("id") String id, @@ -111,16 +113,11 @@ public LiveVariable(@JsonProperty("id") String id, this.id = id; this.key = key; this.defaultValue = defaultValue; - if (status == null) { - this.status = VariableStatus.ACTIVE; - } - else { - this.status = status; - } + this.status = status; this.type = type; } - public VariableStatus getStatus() { + public @Nullable VariableStatus getStatus() { return status; } @@ -150,4 +147,28 @@ public String toString() { ", status=" + status + '}'; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + LiveVariable variable = (LiveVariable) o; + + if (!id.equals(variable.id)) return false; + if (!key.equals(variable.key)) return false; + if (!defaultValue.equals(variable.defaultValue)) return false; + if (type != variable.type) return false; + return status == variable.status; + } + + @Override + public int hashCode() { + int result = id.hashCode(); + result = 31 * result + key.hashCode(); + result = 31 * result + defaultValue.hashCode(); + result = 31 * result + type.hashCode(); + result = 31 * result + status.hashCode(); + return result; + } } diff --git a/core-api/src/main/java/com/optimizely/ab/config/LiveVariableUsageInstance.java b/core-api/src/main/java/com/optimizely/ab/config/LiveVariableUsageInstance.java index 05378b808..79cf05620 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/LiveVariableUsageInstance.java +++ b/core-api/src/main/java/com/optimizely/ab/config/LiveVariableUsageInstance.java @@ -24,7 +24,7 @@ * Represents the value of a live variable for a variation */ @JsonIgnoreProperties(ignoreUnknown = true) -public class LiveVariableUsageInstance { +public class LiveVariableUsageInstance implements IdMapped { private final String id; private final String value; diff --git a/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java b/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java index fac0bbdce..4e9ad423b 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java +++ b/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java @@ -224,6 +224,10 @@ public List getExperimentsForEventKey(String eventKey) { return Collections.emptyList(); } + public List getFeatureFlags() { + return featureFlags; + } + public List getAttributes() { return attributes; } diff --git a/core-api/src/main/java/com/optimizely/ab/config/Variation.java b/core-api/src/main/java/com/optimizely/ab/config/Variation.java index 0991a0a5e..02db51eab 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/Variation.java +++ b/core-api/src/main/java/com/optimizely/ab/config/Variation.java @@ -23,7 +23,9 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.Collections; import java.util.List; +import java.util.Map; /** * Represents the Optimizely Variation configuration. @@ -36,6 +38,7 @@ public class Variation implements IdKeyMapped { private final String id; private final String key; private final List liveVariableUsageInstances; + private final Map variableIdToLiveVariableUsageInstanceMap; public Variation(String id, String key) { this(id, key, null); @@ -47,7 +50,13 @@ public Variation(@JsonProperty("id") String id, @JsonProperty("variables") List liveVariableUsageInstances) { this.id = id; this.key = key; - this.liveVariableUsageInstances = liveVariableUsageInstances; + if (liveVariableUsageInstances == null) { + this.liveVariableUsageInstances = Collections.emptyList(); + } + else { + this.liveVariableUsageInstances = liveVariableUsageInstances; + } + this.variableIdToLiveVariableUsageInstanceMap = ProjectConfigUtils.generateIdMapping(this.liveVariableUsageInstances); } public @Nonnull String getId() { @@ -62,6 +71,10 @@ public Variation(@JsonProperty("id") String id, return liveVariableUsageInstances; } + public Map getVariableIdToLiveVariableUsageInstanceMap() { + return variableIdToLiveVariableUsageInstanceMap; + } + public boolean is(String otherKey) { return key.equals(otherKey); } diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/FeatureFlagGsonDeserializer.java b/core-api/src/main/java/com/optimizely/ab/config/parser/FeatureFlagGsonDeserializer.java new file mode 100644 index 000000000..e26623a8b --- /dev/null +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/FeatureFlagGsonDeserializer.java @@ -0,0 +1,36 @@ +/** + * + * Copyright 2017, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.optimizely.ab.config.parser; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.optimizely.ab.config.FeatureFlag; + +import java.lang.reflect.Type; + +public class FeatureFlagGsonDeserializer implements JsonDeserializer { + @Override + public FeatureFlag deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + + JsonObject jsonObject = json.getAsJsonObject(); + return GsonHelpers.parseFeatureFlag(jsonObject, context); + } +} diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/GsonConfigParser.java b/core-api/src/main/java/com/optimizely/ab/config/parser/GsonConfigParser.java index b87c0a16a..e20146520 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/GsonConfigParser.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/GsonConfigParser.java @@ -18,11 +18,11 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; - import com.optimizely.ab.config.Experiment; +import com.optimizely.ab.config.FeatureFlag; import com.optimizely.ab.config.Group; -import com.optimizely.ab.config.audience.Audience; import com.optimizely.ab.config.ProjectConfig; +import com.optimizely.ab.config.audience.Audience; import javax.annotation.Nonnull; @@ -40,11 +40,12 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse throw new ConfigParseException("Unable to parse empty json."); } Gson gson = new GsonBuilder() - .registerTypeAdapter(ProjectConfig.class, new ProjectConfigGsonDeserializer()) - .registerTypeAdapter(Audience.class, new AudienceGsonDeserializer()) - .registerTypeAdapter(Group.class, new GroupGsonDeserializer()) - .registerTypeAdapter(Experiment.class, new ExperimentGsonDeserializer()) - .create(); + .registerTypeAdapter(Audience.class, new AudienceGsonDeserializer()) + .registerTypeAdapter(Experiment.class, new ExperimentGsonDeserializer()) + .registerTypeAdapter(FeatureFlag.class, new FeatureFlagGsonDeserializer()) + .registerTypeAdapter(Group.class, new GroupGsonDeserializer()) + .registerTypeAdapter(ProjectConfig.class, new ProjectConfigGsonDeserializer()) + .create(); try { return gson.fromJson(json, ProjectConfig.class); diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/GsonHelpers.java b/core-api/src/main/java/com/optimizely/ab/config/parser/GsonHelpers.java index 7ebdb02d2..fc75a6437 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/GsonHelpers.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/GsonHelpers.java @@ -20,23 +20,30 @@ import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; import com.google.gson.reflect.TypeToken; - +import com.optimizely.ab.bucketing.DecisionService; import com.optimizely.ab.config.Experiment; import com.optimizely.ab.config.Experiment.ExperimentStatus; +import com.optimizely.ab.config.FeatureFlag; +import com.optimizely.ab.config.LiveVariable; import com.optimizely.ab.config.LiveVariableUsageInstance; import com.optimizely.ab.config.TrafficAllocation; import com.optimizely.ab.config.Variation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.lang.reflect.Type; import java.util.ArrayList; -import java.util.List; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; final class GsonHelpers { + private static final Logger logger = LoggerFactory.getLogger(DecisionService.class); + private static List parseVariations(JsonArray variationJson, JsonDeserializationContext context) { List variations = new ArrayList(variationJson.size()); for (Object obj : variationJson) { @@ -114,4 +121,35 @@ static Experiment parseExperiment(JsonObject experimentJson, String groupId, Jso static Experiment parseExperiment(JsonObject experimentJson, JsonDeserializationContext context) { return parseExperiment(experimentJson, "", context); } + + static FeatureFlag parseFeatureFlag(JsonObject featureFlagJson, JsonDeserializationContext context) { + String id = featureFlagJson.get("id").getAsString(); + String key = featureFlagJson.get("key").getAsString(); + String layerId = featureFlagJson.get("layerId").getAsString(); + + JsonArray experimentIdsJson = featureFlagJson.getAsJsonArray("experimentIds"); + List experimentIds = new ArrayList(); + for (JsonElement experimentIdObj : experimentIdsJson) { + experimentIds.add(experimentIdObj.getAsString()); + } + + List liveVariables = new ArrayList(); + try { + Type liveVariableType = new TypeToken>() {}.getType(); + liveVariables = context.deserialize(featureFlagJson.getAsJsonArray("variables"), + liveVariableType); + } + catch (JsonParseException exception) { + logger.warn("Unable to parse variables for feature \"" + key + + "\". JsonParseException: " + exception); + } + + return new FeatureFlag( + id, + key, + layerId, + experimentIds, + liveVariables + ); + } } diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java index 8a37865d8..697b500dc 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java @@ -17,35 +17,34 @@ package com.optimizely.ab.config.parser; import com.optimizely.ab.config.Attribute; -import com.optimizely.ab.config.audience.AndCondition; -import com.optimizely.ab.config.audience.Audience; -import com.optimizely.ab.config.audience.Condition; -import com.optimizely.ab.config.audience.NotCondition; -import com.optimizely.ab.config.audience.OrCondition; -import com.optimizely.ab.config.audience.UserAttribute; import com.optimizely.ab.config.EventType; import com.optimizely.ab.config.Experiment; import com.optimizely.ab.config.Experiment.ExperimentStatus; +import com.optimizely.ab.config.FeatureFlag; import com.optimizely.ab.config.Group; import com.optimizely.ab.config.LiveVariable; -import com.optimizely.ab.config.LiveVariableUsageInstance; import com.optimizely.ab.config.LiveVariable.VariableStatus; import com.optimizely.ab.config.LiveVariable.VariableType; +import com.optimizely.ab.config.LiveVariableUsageInstance; import com.optimizely.ab.config.ProjectConfig; import com.optimizely.ab.config.TrafficAllocation; import com.optimizely.ab.config.Variation; - +import com.optimizely.ab.config.audience.AndCondition; +import com.optimizely.ab.config.audience.Audience; +import com.optimizely.ab.config.audience.Condition; +import com.optimizely.ab.config.audience.NotCondition; +import com.optimizely.ab.config.audience.OrCondition; +import com.optimizely.ab.config.audience.UserAttribute; import org.json.JSONArray; import org.json.JSONObject; +import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; -import javax.annotation.Nonnull; - /** * {@code org.json}-based config parser implementation. */ @@ -60,6 +59,7 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse String projectId = rootObject.getString("projectId"); String revision = rootObject.getString("revision"); String version = rootObject.getString("version"); + int datafileVersion = Integer.parseInt(version); List experiments = parseExperiments(rootObject.getJSONArray("experiments")); @@ -72,14 +72,31 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse boolean anonymizeIP = false; List liveVariables = null; - if (version.equals(ProjectConfig.Version.V3.toString())) { + if (datafileVersion >= Integer.parseInt(ProjectConfig.Version.V3.toString())) { liveVariables = parseLiveVariables(rootObject.getJSONArray("variables")); anonymizeIP = rootObject.getBoolean("anonymizeIP"); } - return new ProjectConfig(accountId, projectId, version, revision, groups, experiments, attributes, events, - audiences, anonymizeIP, liveVariables); + List featureFlags = null; + if (datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())) { + featureFlags = parseFeatureFlags(rootObject.getJSONArray("featureFlags")); + } + + return new ProjectConfig( + accountId, + anonymizeIP, + projectId, + revision, + version, + attributes, + audiences, + events, + experiments, + featureFlags, + groups, + liveVariables + ); } catch (Exception e) { throw new ConfigParseException("Unable to parse datafile: " + json, e); } @@ -123,6 +140,41 @@ private List parseExperiments(JSONArray experimentJson, String group return experiments; } + private List parseExperimentIds(JSONArray experimentIdsJson) { + ArrayList experimentIds = new ArrayList(experimentIdsJson.length()); + + for (Object experimentIdObj : experimentIdsJson) { + experimentIds.add((String) experimentIdObj); + } + + return experimentIds; + } + + private List parseFeatureFlags(JSONArray featureFlagJson) { + List featureFlags = new ArrayList(featureFlagJson.length()); + + for (Object obj : featureFlagJson) { + JSONObject featureFlagObject = (JSONObject) obj; + String id = featureFlagObject.getString("id"); + String key = featureFlagObject.getString("key"); + String layerId = featureFlagObject.getString("layerId"); + + List experimentIds = parseExperimentIds(featureFlagObject.getJSONArray("experimentIds")); + + List variables = parseLiveVariables(featureFlagObject.getJSONArray("variables")); + + featureFlags.add(new FeatureFlag( + id, + key, + layerId, + experimentIds, + variables + )); + } + + return featureFlags; + } + private List parseVariations(JSONArray variationJson) { List variations = new ArrayList(variationJson.length()); @@ -187,12 +239,7 @@ private List parseEvents(JSONArray eventJson) { for (Object obj : eventJson) { JSONObject eventObject = (JSONObject)obj; - JSONArray experimentIdsJson = eventObject.getJSONArray("experimentIds"); - List experimentIds = new ArrayList(experimentIdsJson.length()); - - for (Object experimentIdObj : experimentIdsJson) { - experimentIds.add((String)experimentIdObj); - } + List experimentIds = parseExperimentIds(eventObject.getJSONArray("experimentIds")); String id = eventObject.getString("id"); String key = eventObject.getString("key"); @@ -273,7 +320,10 @@ private List parseLiveVariables(JSONArray liveVariablesJson) { String key = liveVariableObject.getString("key"); String defaultValue = liveVariableObject.getString("defaultValue"); VariableType type = VariableType.fromString(liveVariableObject.getString("type")); - VariableStatus status = VariableStatus.fromString(liveVariableObject.getString("status")); + VariableStatus status = null; + if (liveVariableObject.has("status")) { + status = VariableStatus.fromString(liveVariableObject.getString("status")); + } liveVariables.add(new LiveVariable(id, key, defaultValue, status, type)); } diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java index 2c05e0c25..2c37e9abb 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java @@ -20,6 +20,7 @@ import com.optimizely.ab.config.EventType; import com.optimizely.ab.config.Experiment; import com.optimizely.ab.config.Experiment.ExperimentStatus; +import com.optimizely.ab.config.FeatureFlag; import com.optimizely.ab.config.Group; import com.optimizely.ab.config.LiveVariable; import com.optimizely.ab.config.LiveVariable.VariableStatus; @@ -60,6 +61,7 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse String projectId = (String)rootObject.get("projectId"); String revision = (String)rootObject.get("revision"); String version = (String)rootObject.get("version"); + int datafileVersion = Integer.parseInt(version); List experiments = parseExperiments((JSONArray)rootObject.get("experiments")); @@ -72,14 +74,31 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse boolean anonymizeIP = false; List liveVariables = null; - if (version.equals(ProjectConfig.Version.V3.toString())) { + if (datafileVersion >= Integer.parseInt(ProjectConfig.Version.V3.toString())) { liveVariables = parseLiveVariables((JSONArray)rootObject.get("variables")); anonymizeIP = (Boolean)rootObject.get("anonymizeIP"); } - return new ProjectConfig(accountId, projectId, version, revision, groups, experiments, attributes, events, - audiences, anonymizeIP, liveVariables); + List featureFlags = null; + if (datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())) { + featureFlags = parseFeatureFlags((JSONArray) rootObject.get("featureFlags")); + } + + return new ProjectConfig( + accountId, + anonymizeIP, + projectId, + revision, + version, + attributes, + audiences, + events, + experiments, + featureFlags, + groups, + liveVariables + ); } catch (Exception e) { throw new ConfigParseException("Unable to parse datafile: " + json, e); } @@ -125,6 +144,42 @@ private List parseExperiments(JSONArray experimentJson, String group return experiments; } + private List parseExperimentIds(JSONArray experimentIdsJsonArray) { + List experimentIds = new ArrayList(experimentIdsJsonArray.size()); + + for (Object experimentIdObj : experimentIdsJsonArray) { + experimentIds.add((String)experimentIdObj); + } + + return experimentIds; + } + + private List parseFeatureFlags(JSONArray featureFlagJson) { + List featureFlags = new ArrayList(featureFlagJson.size()); + + for (Object obj : featureFlagJson) { + JSONObject featureFlagObject = (JSONObject)obj; + String id = (String)featureFlagObject.get("id"); + String key = (String)featureFlagObject.get("key"); + String layerId = (String)featureFlagObject.get("layerId"); + + JSONArray experimentIdsJsonArray = (JSONArray)featureFlagObject.get("experimentIds"); + List experimentIds = parseExperimentIds(experimentIdsJsonArray); + + List liveVariables = parseLiveVariables((JSONArray) featureFlagObject.get("variables")); + + featureFlags.add(new FeatureFlag( + id, + key, + layerId, + experimentIds, + liveVariables + )); + } + + return featureFlags; + } + private List parseVariations(JSONArray variationJson) { List variations = new ArrayList(variationJson.size()); @@ -189,11 +244,7 @@ private List parseEvents(JSONArray eventJson) { for (Object obj : eventJson) { JSONObject eventObject = (JSONObject)obj; JSONArray experimentIdsJson = (JSONArray)eventObject.get("experimentIds"); - List experimentIds = new ArrayList(experimentIdsJson.size()); - - for (Object experimentIdObj : experimentIdsJson) { - experimentIds.add((String)experimentIdObj); - } + List experimentIds = parseExperimentIds(experimentIdsJson); String id = (String)eventObject.get("id"); String key = (String)eventObject.get("key"); diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/ProjectConfigGsonDeserializer.java b/core-api/src/main/java/com/optimizely/ab/config/parser/ProjectConfigGsonDeserializer.java index cb0b172c2..3f4df5210 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/ProjectConfigGsonDeserializer.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/ProjectConfigGsonDeserializer.java @@ -25,6 +25,7 @@ import com.optimizely.ab.config.Attribute; import com.optimizely.ab.config.EventType; import com.optimizely.ab.config.Experiment; +import com.optimizely.ab.config.FeatureFlag; import com.optimizely.ab.config.Group; import com.optimizely.ab.config.LiveVariable; import com.optimizely.ab.config.ProjectConfig; @@ -47,6 +48,7 @@ public ProjectConfig deserialize(JsonElement json, Type typeOfT, JsonDeserializa String projectId = jsonObject.get("projectId").getAsString(); String revision = jsonObject.get("revision").getAsString(); String version = jsonObject.get("version").getAsString(); + int datafileVersion = Integer.parseInt(version); // generic list type tokens Type groupsType = new TypeToken>() {}.getType(); @@ -70,14 +72,32 @@ public ProjectConfig deserialize(JsonElement json, Type typeOfT, JsonDeserializa boolean anonymizeIP = false; // live variables should be null if using V2 List liveVariables = null; - if (version.equals(ProjectConfig.Version.V3.toString())) { + if (datafileVersion >= Integer.parseInt(ProjectConfig.Version.V3.toString())) { Type liveVariablesType = new TypeToken>() {}.getType(); liveVariables = context.deserialize(jsonObject.getAsJsonArray("variables"), liveVariablesType); anonymizeIP = jsonObject.get("anonymizeIP").getAsBoolean(); } - return new ProjectConfig(accountId, projectId, version, revision, groups, experiments, attributes, events, - audiences, anonymizeIP, liveVariables); + List featureFlags = null; + if (datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())) { + Type featureFlagsType = new TypeToken>() {}.getType(); + featureFlags = context.deserialize(jsonObject.getAsJsonArray("featureFlags"), featureFlagsType); + } + + return new ProjectConfig( + accountId, + anonymizeIP, + projectId, + revision, + version, + attributes, + audiences, + events, + experiments, + featureFlags, + groups, + liveVariables + ); } } diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/ProjectConfigJacksonDeserializer.java b/core-api/src/main/java/com/optimizely/ab/config/parser/ProjectConfigJacksonDeserializer.java index e8722086a..04503c150 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/ProjectConfigJacksonDeserializer.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/ProjectConfigJacksonDeserializer.java @@ -23,14 +23,14 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; - import com.optimizely.ab.config.Attribute; -import com.optimizely.ab.config.audience.Audience; import com.optimizely.ab.config.EventType; import com.optimizely.ab.config.Experiment; +import com.optimizely.ab.config.FeatureFlag; import com.optimizely.ab.config.Group; import com.optimizely.ab.config.LiveVariable; import com.optimizely.ab.config.ProjectConfig; +import com.optimizely.ab.config.audience.Audience; import java.io.IOException; import java.util.List; @@ -51,6 +51,7 @@ public ProjectConfig deserialize(JsonParser parser, DeserializationContext conte String projectId = node.get("projectId").textValue(); String revision = node.get("revision").textValue(); String version = node.get("version").textValue(); + int datafileVersion = Integer.parseInt(version); List groups = mapper.readValue(node.get("groups").toString(), new TypeReference>() {}); List experiments = mapper.readValue(node.get("experiments").toString(), @@ -66,13 +67,31 @@ public ProjectConfig deserialize(JsonParser parser, DeserializationContext conte boolean anonymizeIP = false; List liveVariables = null; - if (version.equals(ProjectConfig.Version.V3.toString())) { + if (datafileVersion >= Integer.parseInt(ProjectConfig.Version.V3.toString())) { liveVariables = mapper.readValue(node.get("variables").toString(), new TypeReference>() {}); anonymizeIP = node.get("anonymizeIP").asBoolean(); } - return new ProjectConfig(accountId, projectId, version, revision, groups, experiments, attributes, events, - audiences, anonymizeIP, liveVariables); + List featureFlags = null; + if (datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())) { + featureFlags = mapper.readValue(node.get("featureFlags").toString(), + new TypeReference>() {}); + } + + return new ProjectConfig( + accountId, + anonymizeIP, + projectId, + revision, + version, + attributes, + audiences, + events, + experiments, + featureFlags, + groups, + liveVariables + ); } } \ No newline at end of file diff --git a/core-api/src/test/java/com/optimizely/ab/config/ProjectConfigTestUtils.java b/core-api/src/test/java/com/optimizely/ab/config/ProjectConfigTestUtils.java index fecc6b2d4..fa4a43a25 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/ProjectConfigTestUtils.java +++ b/core-api/src/test/java/com/optimizely/ab/config/ProjectConfigTestUtils.java @@ -450,12 +450,13 @@ public static void verifyProjectConfig(@CheckForNull ProjectConfig actual, @Nonn assertThat(actual.getVersion(), is(expected.getVersion())); assertThat(actual.getRevision(), is(expected.getRevision())); - verifyGroups(actual.getGroups(), expected.getGroups()); - verifyExperiments(actual.getExperiments(), expected.getExperiments()); verifyAttributes(actual.getAttributes(), expected.getAttributes()); - verifyEvents(actual.getEventTypes(), expected.getEventTypes()); verifyAudiences(actual.getAudiences(), expected.getAudiences()); + verifyEvents(actual.getEventTypes(), expected.getEventTypes()); + verifyExperiments(actual.getExperiments(), expected.getExperiments()); + verifyFeatureFlags(actual.getFeatureFlags(), expected.getFeatureFlags()); verifyLiveVariables(actual.getLiveVariables(), expected.getLiveVariables()); + verifyGroups(actual.getGroups(), expected.getGroups()); } /** @@ -482,6 +483,16 @@ private static void verifyExperiments(List actual, List } } + private static void verifyFeatureFlags(List actual, List expected) { + assertEquals(expected.size(), actual.size()); + for (int i = 0; i < actual.size(); i ++) { + FeatureFlag actualFeatureFlag = actual.get(i); + FeatureFlag expectedFeatureFlag = expected.get(i); + + assertEquals(expectedFeatureFlag, actualFeatureFlag); + } + } + /** * Asserts that the provided variation configs are equivalent. */ diff --git a/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java b/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java index 1607e3d99..e6693cdc3 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java +++ b/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java @@ -150,7 +150,7 @@ public class ValidProjectConfigV4 { ) ); private static final String FEATURE_MULTI_VARIATE_FEATURE_ID = "3263342226"; - private static final String FEATURE_MULTI_VARIATE_FEATURE_KEY = "multi_variate_feature"; + public static final String FEATURE_MULTI_VARIATE_FEATURE_KEY = "multi_variate_feature"; private static final String VARIABLE_FIRST_LETTER_ID = "675244127"; private static final String VARIABLE_FIRST_LETTER_KEY = "first_letter"; private static final String VARIABLE_FIRST_LETTER_DEFAULT_VALUE = "H"; @@ -608,7 +608,7 @@ public static ProjectConfig generateValidProjectConfigV4() { experiments, featureFlags, groups, - null + Collections.emptyList() ); } } diff --git a/core-api/src/test/java/com/optimizely/ab/config/parser/GsonConfigParserTest.java b/core-api/src/test/java/com/optimizely/ab/config/parser/GsonConfigParserTest.java index c170befb9..3c5cc947e 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/parser/GsonConfigParserTest.java +++ b/core-api/src/test/java/com/optimizely/ab/config/parser/GsonConfigParserTest.java @@ -57,7 +57,7 @@ public void parseProjectConfigV3() throws Exception { } @Test - public void parseProjectCOnfigV4() throws Exception { + public void parseProjectConfigV4() throws Exception { GsonConfigParser parser = new GsonConfigParser(); ProjectConfig actual = parser.parseProjectConfig(validConfigJsonV4()); ProjectConfig expected = validProjectConfigV4(); diff --git a/core-api/src/test/java/com/optimizely/ab/internal/LogbackVerifier.java b/core-api/src/test/java/com/optimizely/ab/internal/LogbackVerifier.java index 58d2dd2f5..3ce4f39a7 100644 --- a/core-api/src/test/java/com/optimizely/ab/internal/LogbackVerifier.java +++ b/core-api/src/test/java/com/optimizely/ab/internal/LogbackVerifier.java @@ -11,12 +11,14 @@ import org.mockito.ArgumentMatcher; import org.mockito.Mock; import org.mockito.Mockito; +import org.mockito.verification.VerificationMode; import org.slf4j.LoggerFactory; import java.util.LinkedList; import java.util.List; import static org.mockito.Matchers.argThat; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; @@ -51,11 +53,22 @@ public void expectMessage(Level level) { } public void expectMessage(Level level, String msg) { - expectMessage(level, msg, null); + expectMessage(level, msg, (Class) null); } public void expectMessage(Level level, String msg, Class throwableClass) { - expectedEvents.add(new ExpectedLogEvent(level, msg, throwableClass)); + expectMessage(level, msg, null, times(1)); + } + + public void expectMessage(Level level, String msg, VerificationMode times) { + expectMessage(level, msg, null, times); + } + + public void expectMessage(Level level, + String msg, + Class throwableClass, + VerificationMode times) { + expectedEvents.add(new ExpectedLogEvent(level, msg, throwableClass, times)); } private void before() { @@ -66,7 +79,7 @@ private void before() { private void verify() throws Throwable { for (final ExpectedLogEvent expectedEvent : expectedEvents) { - Mockito.verify(appender).doAppend(argThat(new ArgumentMatcher() { + Mockito.verify(appender, expectedEvent.times).doAppend(argThat(new ArgumentMatcher() { @Override public boolean matches(final Object argument) { return expectedEvent.matches((ILoggingEvent) argument); @@ -83,11 +96,16 @@ private final static class ExpectedLogEvent { private final String message; private final Level level; private final Class throwableClass; + private final VerificationMode times; - private ExpectedLogEvent(Level level, String message, Class throwableClass) { + private ExpectedLogEvent(Level level, + String message, + Class throwableClass, + VerificationMode times) { this.message = message; this.level = level; this.throwableClass = throwableClass; + this.times = times; } private boolean matches(ILoggingEvent actual) { diff --git a/core-api/src/test/resources/config/valid-project-config-v4.json b/core-api/src/test/resources/config/valid-project-config-v4.json index 0a74c6eb8..624d8a538 100644 --- a/core-api/src/test/resources/config/valid-project-config-v4.json +++ b/core-api/src/test/resources/config/valid-project-config-v4.json @@ -387,5 +387,5 @@ ] } ], - "liveVariables": [] + "variables": [] }