Skip to content

Commit aa84c7d

Browse files
[FSSDK-11134] Update: enable project config to track CMAB properties (#577)
* Cmab datafile parsed * Add CMAB configuration and parsing tests with cmab datafile * Add copyright notice to CmabTest and CmabParsingTest files * Refactor cmab parsing logic to simplify null check in JsonConfigParser
1 parent bc39669 commit aa84c7d

File tree

9 files changed

+843
-17
lines changed

9 files changed

+843
-17
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
*
3+
* Copyright 2025 Optimizely and contributors
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package com.optimizely.ab.config;
18+
19+
import java.util.List;
20+
import java.util.Objects;
21+
22+
import com.fasterxml.jackson.annotation.JsonCreator;
23+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
24+
import com.fasterxml.jackson.annotation.JsonProperty;
25+
/**
26+
* Represents the Optimizely Traffic Allocation configuration.
27+
*
28+
* @see <a href="http://developers.optimizely.com/server/reference/index.html#json">Project JSON</a>
29+
*/
30+
@JsonIgnoreProperties(ignoreUnknown = true)
31+
public class Cmab {
32+
33+
private final List<String> attributeIds;
34+
private final int trafficAllocation;
35+
36+
@JsonCreator
37+
public Cmab(@JsonProperty("attributeIds") List<String> attributeIds,
38+
@JsonProperty("trafficAllocation") int trafficAllocation) {
39+
this.attributeIds = attributeIds;
40+
this.trafficAllocation = trafficAllocation;
41+
}
42+
43+
public List<String> getAttributeIds() {
44+
return attributeIds;
45+
}
46+
47+
public int getTrafficAllocation() {
48+
return trafficAllocation;
49+
}
50+
51+
@Override
52+
public boolean equals(Object obj) {
53+
if (this == obj) return true;
54+
if (obj == null || getClass() != obj.getClass()) return false;
55+
Cmab cmab = (Cmab) obj;
56+
return trafficAllocation == cmab.trafficAllocation &&
57+
Objects.equals(attributeIds, cmab.attributeIds);
58+
}
59+
60+
@Override
61+
public int hashCode() {
62+
return Objects.hash(attributeIds, trafficAllocation);
63+
}
64+
65+
@Override
66+
public String toString() {
67+
return "Cmab{" +
68+
"attributeIds=" + attributeIds +
69+
", trafficAllocation=" + trafficAllocation +
70+
'}';
71+
}
72+
}

core-api/src/main/java/com/optimizely/ab/config/Experiment.java

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public class Experiment implements ExperimentCore {
4141
private final String status;
4242
private final String layerId;
4343
private final String groupId;
44+
private final Cmab cmab;
4445

4546
private final List<String> audienceIds;
4647
private final Condition<AudienceIdCondition> audienceConditions;
@@ -71,7 +72,25 @@ public String toString() {
7172

7273
@VisibleForTesting
7374
public Experiment(String id, String key, String layerId) {
74-
this(id, key, null, layerId, Collections.emptyList(), null, Collections.emptyList(), Collections.emptyMap(), Collections.emptyList(), "");
75+
this(id, key, null, layerId, Collections.emptyList(), null, Collections.emptyList(), Collections.emptyMap(), Collections.emptyList(), "", null);
76+
}
77+
78+
@VisibleForTesting
79+
public Experiment(String id, String key, String status, String layerId,
80+
List<String> audienceIds, Condition audienceConditions,
81+
List<Variation> variations, Map<String, String> userIdToVariationKeyMap,
82+
List<TrafficAllocation> trafficAllocation, String groupId) {
83+
this(id, key, status, layerId, audienceIds, audienceConditions, variations,
84+
userIdToVariationKeyMap, trafficAllocation, groupId, null); // Default cmab=null
85+
}
86+
87+
@VisibleForTesting
88+
public Experiment(String id, String key, String status, String layerId,
89+
List<String> audienceIds, Condition audienceConditions,
90+
List<Variation> variations, Map<String, String> userIdToVariationKeyMap,
91+
List<TrafficAllocation> trafficAllocation) {
92+
this(id, key, status, layerId, audienceIds, audienceConditions, variations,
93+
userIdToVariationKeyMap, trafficAllocation, "", null); // Default groupId="" and cmab=null
7594
}
7695

7796
@JsonCreator
@@ -83,8 +102,9 @@ public Experiment(@JsonProperty("id") String id,
83102
@JsonProperty("audienceConditions") Condition audienceConditions,
84103
@JsonProperty("variations") List<Variation> variations,
85104
@JsonProperty("forcedVariations") Map<String, String> userIdToVariationKeyMap,
86-
@JsonProperty("trafficAllocation") List<TrafficAllocation> trafficAllocation) {
87-
this(id, key, status, layerId, audienceIds, audienceConditions, variations, userIdToVariationKeyMap, trafficAllocation, "");
105+
@JsonProperty("trafficAllocation") List<TrafficAllocation> trafficAllocation,
106+
@JsonProperty("cmab") Cmab cmab) {
107+
this(id, key, status, layerId, audienceIds, audienceConditions, variations, userIdToVariationKeyMap, trafficAllocation, "", cmab);
88108
}
89109

90110
public Experiment(@Nonnull String id,
@@ -96,7 +116,8 @@ public Experiment(@Nonnull String id,
96116
@Nonnull List<Variation> variations,
97117
@Nonnull Map<String, String> userIdToVariationKeyMap,
98118
@Nonnull List<TrafficAllocation> trafficAllocation,
99-
@Nonnull String groupId) {
119+
@Nonnull String groupId,
120+
@Nullable Cmab cmab) {
100121
this.id = id;
101122
this.key = key;
102123
this.status = status == null ? ExperimentStatus.NOT_STARTED.toString() : status;
@@ -109,6 +130,7 @@ public Experiment(@Nonnull String id,
109130
this.userIdToVariationKeyMap = userIdToVariationKeyMap;
110131
this.variationKeyToVariationMap = ProjectConfigUtils.generateNameMapping(variations);
111132
this.variationIdToVariationMap = ProjectConfigUtils.generateIdMapping(variations);
133+
this.cmab = cmab;
112134
}
113135

114136
public String getId() {
@@ -159,6 +181,10 @@ public String getGroupId() {
159181
return groupId;
160182
}
161183

184+
public Cmab getCmab() {
185+
return cmab;
186+
}
187+
162188
public boolean isActive() {
163189
return status.equals(ExperimentStatus.RUNNING.toString()) ||
164190
status.equals(ExperimentStatus.LAUNCHED.toString());
@@ -185,6 +211,7 @@ public String toString() {
185211
", variationKeyToVariationMap=" + variationKeyToVariationMap +
186212
", userIdToVariationKeyMap=" + userIdToVariationKeyMap +
187213
", trafficAllocation=" + trafficAllocation +
214+
", cmab=" + cmab +
188215
'}';
189216
}
190217
}

core-api/src/main/java/com/optimizely/ab/config/Group.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ public Group(@JsonProperty("id") String id,
6262
experiment.getVariations(),
6363
experiment.getUserIdToVariationKeyMap(),
6464
experiment.getTrafficAllocation(),
65-
id
65+
id,
66+
experiment.getCmab()
6667
);
6768
}
6869
this.experiments.add(experiment);

core-api/src/main/java/com/optimizely/ab/config/parser/GsonHelpers.java

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,8 @@
2424
import com.google.gson.JsonParseException;
2525
import com.google.gson.reflect.TypeToken;
2626
import com.optimizely.ab.bucketing.DecisionService;
27-
import com.optimizely.ab.config.Experiment;
28-
import com.optimizely.ab.config.Holdout;
27+
import com.optimizely.ab.config.*;
2928
import com.optimizely.ab.config.Experiment.ExperimentStatus;
30-
import com.optimizely.ab.config.Holdout.HoldoutStatus;
31-
import com.optimizely.ab.config.FeatureFlag;
32-
import com.optimizely.ab.config.FeatureVariable;
33-
import com.optimizely.ab.config.FeatureVariableUsageInstance;
34-
import com.optimizely.ab.config.TrafficAllocation;
35-
import com.optimizely.ab.config.Variation;
3629
import com.optimizely.ab.config.audience.AudienceIdCondition;
3730
import com.optimizely.ab.config.audience.Condition;
3831
import com.optimizely.ab.internal.ConditionUtils;
@@ -120,6 +113,27 @@ static Condition parseAudienceConditions(JsonObject experimentJson) {
120113

121114
}
122115

116+
static Cmab parseCmab(JsonObject cmabJson, JsonDeserializationContext context) {
117+
if (cmabJson == null) {
118+
return null;
119+
}
120+
121+
JsonArray attributeIdsJson = cmabJson.getAsJsonArray("attributeIds");
122+
List<String> attributeIds = new ArrayList<>();
123+
if (attributeIdsJson != null) {
124+
for (JsonElement attributeIdElement : attributeIdsJson) {
125+
attributeIds.add(attributeIdElement.getAsString());
126+
}
127+
}
128+
129+
int trafficAllocation = 0;
130+
if (cmabJson.has("trafficAllocation")) {
131+
trafficAllocation = cmabJson.get("trafficAllocation").getAsInt();
132+
}
133+
134+
return new Cmab(attributeIds, trafficAllocation);
135+
}
136+
123137
static Experiment parseExperiment(JsonObject experimentJson, String groupId, JsonDeserializationContext context) {
124138
String id = experimentJson.get("id").getAsString();
125139
String key = experimentJson.get("key").getAsString();
@@ -145,8 +159,17 @@ static Experiment parseExperiment(JsonObject experimentJson, String groupId, Jso
145159
List<TrafficAllocation> trafficAllocations =
146160
parseTrafficAllocation(experimentJson.getAsJsonArray("trafficAllocation"));
147161

162+
Cmab cmab = null;
163+
if (experimentJson.has("cmab")) {
164+
JsonElement cmabElement = experimentJson.get("cmab");
165+
if (!cmabElement.isJsonNull()) {
166+
JsonObject cmabJson = cmabElement.getAsJsonObject();
167+
cmab = parseCmab(cmabJson, context);
168+
}
169+
}
170+
148171
return new Experiment(id, key, status, layerId, audienceIds, conditions, variations, userIdToVariationKeyMap,
149-
trafficAllocations, groupId);
172+
trafficAllocations, groupId, cmab);
150173
}
151174

152175
static Experiment parseExperiment(JsonObject experimentJson, JsonDeserializationContext context) {

core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,14 @@ private List<Experiment> parseExperiments(JSONArray experimentJson, String group
173173
List<TrafficAllocation> trafficAllocations =
174174
parseTrafficAllocation(experimentObject.getJSONArray("trafficAllocation"));
175175

176+
Cmab cmab = null;
177+
if (experimentObject.has("cmab")) {
178+
JSONObject cmabObject = experimentObject.optJSONObject("cmab");
179+
cmab = parseCmab(cmabObject);
180+
}
181+
176182
experiments.add(new Experiment(id, key, status, layerId, audienceIds, conditions, variations, userIdToVariationKeyMap,
177-
trafficAllocations, groupId));
183+
trafficAllocations, groupId, cmab));
178184
}
179185

180186
return experiments;
@@ -332,6 +338,23 @@ private List<TrafficAllocation> parseTrafficAllocation(JSONArray trafficAllocati
332338
return trafficAllocation;
333339
}
334340

341+
private Cmab parseCmab(JSONObject cmabObject) {
342+
if (cmabObject == null) {
343+
return null;
344+
}
345+
346+
JSONArray attributeIdsJson = cmabObject.optJSONArray("attributeIds");
347+
List<String> attributeIds = new ArrayList<String>();
348+
if (attributeIdsJson != null) {
349+
for (int i = 0; i < attributeIdsJson.length(); i++) {
350+
attributeIds.add(attributeIdsJson.getString(i));
351+
}
352+
}
353+
354+
int trafficAllocation = cmabObject.optInt("trafficAllocation", 0);
355+
return new Cmab(attributeIds, trafficAllocation);
356+
}
357+
335358
private List<Attribute> parseAttributes(JSONArray attributeJson) {
336359
List<Attribute> attributes = new ArrayList<Attribute>(attributeJson.length());
337360

core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,17 @@ private List<Experiment> parseExperiments(JSONArray experimentJson, String group
180180
List<TrafficAllocation> trafficAllocations =
181181
parseTrafficAllocation((JSONArray) experimentObject.get("trafficAllocation"));
182182

183-
experiments.add(new Experiment(id, key, status, layerId, audienceIds, conditions, variations, userIdToVariationKeyMap,
184-
trafficAllocations, groupId));
183+
// Add cmab parsing
184+
Cmab cmab = null;
185+
if (experimentObject.containsKey("cmab")) {
186+
JSONObject cmabObject = (JSONObject) experimentObject.get("cmab");
187+
if (cmabObject != null) {
188+
cmab = parseCmab(cmabObject);
189+
}
190+
}
191+
192+
experiments.add(new Experiment(id, key, status, layerId, audienceIds, conditions, variations,
193+
userIdToVariationKeyMap, trafficAllocations, groupId, cmab));
185194
}
186195

187196
return experiments;
@@ -465,6 +474,26 @@ private List<Integration> parseIntegrations(JSONArray integrationsJson) {
465474
return integrations;
466475
}
467476

477+
private Cmab parseCmab(JSONObject cmabObject) {
478+
if (cmabObject == null) {
479+
return null;
480+
}
481+
482+
JSONArray attributeIdsJson = (JSONArray) cmabObject.get("attributeIds");
483+
List<String> attributeIds = new ArrayList<>();
484+
if (attributeIdsJson != null) {
485+
for (Object idObj : attributeIdsJson) {
486+
attributeIds.add((String) idObj);
487+
}
488+
}
489+
490+
Object trafficAllocationObj = cmabObject.get("trafficAllocation");
491+
int trafficAllocation = trafficAllocationObj != null ?
492+
((Long) trafficAllocationObj).intValue() : 0;
493+
494+
return new Cmab(attributeIds, trafficAllocation);
495+
}
496+
468497
@Override
469498
public String toJson(Object src) {
470499
return JSONValue.toJSONString(src);

0 commit comments

Comments
 (0)