Skip to content

Commit d5d9cd3

Browse files
fix: typed audience conditions non-string parsing (#232)
* fix typed audience conditions non-string parsing * add license header * make sure and check for null type in deserializer * remove check for null * fix jackson deserializer * cast typedAudience list to audience list
1 parent 3bd8c84 commit d5d9cd3

11 files changed

+131
-24
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
*
3+
* Copyright 2018, 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.audience;
18+
19+
import com.fasterxml.jackson.annotation.JsonCreator;
20+
import com.fasterxml.jackson.annotation.JsonProperty;
21+
22+
public class TypedAudience extends Audience {
23+
@JsonCreator
24+
public TypedAudience(@JsonProperty("id") String id,
25+
@JsonProperty("name") String name,
26+
@JsonProperty("conditions") Condition conditions) {
27+
super(id, name, conditions);
28+
}
29+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public Audience deserialize(JsonElement json, Type typeOfT, JsonDeserializationC
4646
String name = jsonObject.get("name").getAsString();
4747

4848
JsonElement conditionsElement = jsonObject.get("conditions");
49-
if (!conditionsElement.isJsonArray()) {
49+
if (!typeOfT.toString().contains("TypedAudience")) {
5050
conditionsElement = parser.parse(jsonObject.get("conditions").getAsString());
5151
}
5252
List<Object> rawObjectList = gson.fromJson(conditionsElement, List.class);

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

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package com.optimizely.ab.config.parser;
1818

1919
import com.fasterxml.jackson.core.JsonParser;
20-
import com.fasterxml.jackson.core.JsonProcessingException;
2120
import com.fasterxml.jackson.core.ObjectCodec;
2221
import com.fasterxml.jackson.databind.DeserializationContext;
2322
import com.fasterxml.jackson.databind.JsonDeserializer;
@@ -26,8 +25,6 @@
2625
import com.optimizely.ab.config.audience.*;
2726

2827
import java.io.IOException;
29-
import java.util.ArrayList;
30-
import java.util.List;
3128

3229
public class AudienceJacksonDeserializer extends JsonDeserializer<Audience> {
3330
private ObjectMapper objectMapper;
@@ -49,13 +46,11 @@ public Audience deserialize(JsonParser parser, DeserializationContext context) t
4946
String name = node.get("name").textValue();
5047

5148
JsonNode conditionsJson = node.get("conditions");
52-
if (conditionsJson.isTextual()) {
53-
conditionsJson = objectMapper.readTree(conditionsJson.textValue());
54-
}
49+
conditionsJson = objectMapper.readTree(conditionsJson.textValue());
50+
5551
Condition conditions = ConditionJacksonDeserializer.<UserAttribute>parseConditions(UserAttribute.class, objectMapper, conditionsJson);
5652

5753
return new Audience(id, name, conditions);
5854
}
5955

6056
}
61-

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.optimizely.ab.config.Group;
2424
import com.optimizely.ab.config.ProjectConfig;
2525
import com.optimizely.ab.config.audience.Audience;
26+
import com.optimizely.ab.config.audience.TypedAudience;
2627

2728
import javax.annotation.Nonnull;
2829

@@ -41,6 +42,7 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse
4142
}
4243
Gson gson = new GsonBuilder()
4344
.registerTypeAdapter(Audience.class, new AudienceGsonDeserializer())
45+
.registerTypeAdapter(TypedAudience.class, new AudienceGsonDeserializer())
4446
.registerTypeAdapter(Experiment.class, new ExperimentGsonDeserializer())
4547
.registerTypeAdapter(FeatureFlag.class, new FeatureFlagGsonDeserializer())
4648
.registerTypeAdapter(Group.class, new GroupGsonDeserializer())

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.optimizely.ab.config.ProjectConfig;
2222
import com.optimizely.ab.config.audience.Audience;
2323
import com.optimizely.ab.config.audience.Condition;
24+
import com.optimizely.ab.config.audience.TypedAudience;
2425

2526
import javax.annotation.Nonnull;
2627

@@ -55,6 +56,7 @@ public ProjectConfigModule() {
5556
super(NAME);
5657
addDeserializer(ProjectConfig.class, new ProjectConfigJacksonDeserializer());
5758
addDeserializer(Audience.class, new AudienceJacksonDeserializer(objectMapper));
59+
addDeserializer(TypedAudience.class, new TypedAudienceJacksonDeserializer(objectMapper));
5860
addDeserializer(Condition.class, new ConditionJacksonDeserializer(objectMapper));
5961
}
6062
}

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

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse
7676

7777
List<Audience> typedAudiences = null;
7878
if (rootObject.has("typedAudiences")) {
79-
typedAudiences = parseAudiences(rootObject.getJSONArray("typedAudiences"));
79+
typedAudiences = parseTypedAudiences(rootObject.getJSONArray("typedAudiences"));
8080
}
8181

8282
List<Group> groups = parseGroups(rootObject.getJSONArray("groups"));
@@ -289,10 +289,24 @@ private List<Audience> parseAudiences(JSONArray audienceJson) {
289289
String id = audienceObject.getString("id");
290290
String key = audienceObject.getString("name");
291291
Object conditionsObject = audienceObject.get("conditions");
292-
// audience.conditions will still be a string and is parsed here.
293-
// typedAudiences.conditions is not a string and is also parsed here.
294-
JSONArray conditionJson = (conditionsObject instanceof String) ?
295-
new JSONArray((String)conditionsObject) : (JSONArray)conditionsObject;
292+
JSONArray conditionJson = new JSONArray((String)conditionsObject);
293+
294+
Condition conditions = ConditionUtils.<UserAttribute>parseConditions(UserAttribute.class, conditionJson);
295+
audiences.add(new Audience(id, key, conditions));
296+
}
297+
298+
return audiences;
299+
}
300+
301+
private List<Audience> parseTypedAudiences(JSONArray audienceJson) {
302+
List<Audience> audiences = new ArrayList<Audience>(audienceJson.length());
303+
304+
for (Object obj : audienceJson) {
305+
JSONObject audienceObject = (JSONObject)obj;
306+
String id = audienceObject.getString("id");
307+
String key = audienceObject.getString("name");
308+
Object conditionsObject = audienceObject.get("conditions");
309+
JSONArray conditionJson = (JSONArray)conditionsObject;
296310

297311
Condition conditions = ConditionUtils.<UserAttribute>parseConditions(UserAttribute.class, conditionJson);
298312
audiences.add(new Audience(id, key, conditions));

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

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse
8080

8181
List<Audience> typedAudiences = null;
8282
if (rootObject.containsKey("typedAudiences")) {
83-
typedAudiences = parseAudiences((JSONArray)parser.parse(rootObject.get("typedAudiences").toString()));
83+
typedAudiences = parseTypedAudiences((JSONArray)parser.parse(rootObject.get("typedAudiences").toString()));
8484
}
8585

8686
List<Group> groups = parseGroups((JSONArray)rootObject.get("groups"));
@@ -303,10 +303,23 @@ private List<Audience> parseAudiences(JSONArray audienceJson) throws ParseExcept
303303
String id = (String)audienceObject.get("id");
304304
String key = (String)audienceObject.get("name");
305305
Object conditionObject = audienceObject.get("conditions");
306-
// typedAudiences.conditions is a array.
307-
// audience.conditions is a string.
308-
JSONArray conditionJson = conditionObject instanceof String ?
309-
(JSONArray)parser.parse((String)conditionObject) : (JSONArray)conditionObject;
306+
JSONArray conditionJson = (JSONArray)parser.parse((String)conditionObject);
307+
Condition conditions = ConditionUtils.<UserAttribute>parseConditions(UserAttribute.class, conditionJson);
308+
audiences.add(new Audience(id, key, conditions));
309+
}
310+
311+
return audiences;
312+
}
313+
314+
private List<Audience> parseTypedAudiences(JSONArray audienceJson) throws ParseException {
315+
List<Audience> audiences = new ArrayList<Audience>(audienceJson.size());
316+
317+
for (Object obj : audienceJson) {
318+
JSONObject audienceObject = (JSONObject)obj;
319+
String id = (String)audienceObject.get("id");
320+
String key = (String)audienceObject.get("name");
321+
Object conditionObject = audienceObject.get("conditions");
322+
JSONArray conditionJson = (JSONArray)conditionObject;
310323
Condition conditions = ConditionUtils.<UserAttribute>parseConditions(UserAttribute.class, conditionJson);
311324
audiences.add(new Audience(id, key, conditions));
312325
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import com.optimizely.ab.config.ProjectConfig;
3232
import com.optimizely.ab.config.Rollout;
3333
import com.optimizely.ab.config.audience.Audience;
34+
import com.optimizely.ab.config.audience.TypedAudience;
3435

3536
import java.lang.reflect.Type;
3637
import java.util.Collections;
@@ -58,6 +59,7 @@ public ProjectConfig deserialize(JsonElement json, Type typeOfT, JsonDeserializa
5859
Type attributesType = new TypeToken<List<Attribute>>() {}.getType();
5960
Type eventsType = new TypeToken<List<EventType>>() {}.getType();
6061
Type audienceType = new TypeToken<List<Audience>>() {}.getType();
62+
Type typedAudienceType = new TypeToken<List<TypedAudience>>() {}.getType();
6163

6264
List<Group> groups = context.deserialize(jsonObject.get("groups").getAsJsonArray(), groupsType);
6365
List<Experiment> experiments =
@@ -75,7 +77,7 @@ public ProjectConfig deserialize(JsonElement json, Type typeOfT, JsonDeserializa
7577

7678
List<Audience> typedAudiences = null;
7779
if (jsonObject.has("typedAudiences")) {
78-
typedAudiences = context.deserialize(jsonObject.get("typedAudiences").getAsJsonArray(), audienceType);
80+
typedAudiences = context.deserialize(jsonObject.get("typedAudiences").getAsJsonArray(), typedAudienceType);
7981
}
8082
boolean anonymizeIP = false;
8183
// live variables should be null if using V2

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@
2424
import com.optimizely.ab.config.*;
2525
import com.optimizely.ab.config.audience.Audience;
2626
import com.optimizely.ab.config.audience.Condition;
27+
import com.optimizely.ab.config.audience.TypedAudience;
2728

2829
import java.io.IOException;
30+
import java.util.ArrayList;
2931
import java.util.Collections;
3032
import java.util.List;
3133

@@ -51,9 +53,9 @@ public ProjectConfig deserialize(JsonParser parser, DeserializationContext conte
5153
audiences = JacksonHelpers.arrayNodeToList(node.get("audiences"), Audience.class, codec);
5254
}
5355

54-
List<Audience> typedAudiences = null;
56+
List<TypedAudience> typedAudiences = null;
5557
if (node.has("typedAudiences")) {
56-
typedAudiences = JacksonHelpers.arrayNodeToList(node.get("typedAudiences"), Audience.class, codec);
58+
typedAudiences = JacksonHelpers.arrayNodeToList(node.get("typedAudiences"), TypedAudience.class, codec);
5759
}
5860

5961
boolean anonymizeIP = false;
@@ -83,7 +85,7 @@ public ProjectConfig deserialize(JsonParser parser, DeserializationContext conte
8385
version,
8486
attributes,
8587
audiences,
86-
typedAudiences,
88+
(List<Audience>)(List<? extends Audience>)typedAudiences,
8789
events,
8890
experiments,
8991
featureFlags,
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.optimizely.ab.config.parser;
2+
3+
import com.fasterxml.jackson.core.JsonParser;
4+
import com.fasterxml.jackson.core.ObjectCodec;
5+
import com.fasterxml.jackson.databind.DeserializationContext;
6+
import com.fasterxml.jackson.databind.JsonDeserializer;
7+
import com.fasterxml.jackson.databind.JsonNode;
8+
import com.fasterxml.jackson.databind.ObjectMapper;
9+
import com.optimizely.ab.config.audience.Condition;
10+
import com.optimizely.ab.config.audience.TypedAudience;
11+
import com.optimizely.ab.config.audience.UserAttribute;
12+
13+
import java.io.IOException;
14+
15+
public class TypedAudienceJacksonDeserializer extends JsonDeserializer<TypedAudience> {
16+
private ObjectMapper objectMapper;
17+
18+
public TypedAudienceJacksonDeserializer() {
19+
this(new ObjectMapper());
20+
}
21+
22+
TypedAudienceJacksonDeserializer(ObjectMapper objectMapper) {
23+
this.objectMapper = objectMapper;
24+
}
25+
26+
@Override
27+
public TypedAudience deserialize(JsonParser parser, DeserializationContext context) throws IOException {
28+
ObjectCodec codec = parser.getCodec();
29+
JsonNode node = codec.readTree(parser);
30+
31+
String id = node.get("id").textValue();
32+
String name = node.get("name").textValue();
33+
34+
JsonNode conditionsJson = node.get("conditions");
35+
36+
Condition conditions = ConditionJacksonDeserializer.<UserAttribute>parseConditions(UserAttribute.class, objectMapper, conditionsJson);
37+
38+
return new TypedAudience(id, name, conditions);
39+
}
40+
41+
}
42+

core-api/src/test/java/com/optimizely/ab/config/parser/GsonConfigParserTest.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.google.gson.JsonArray;
2020
import com.google.gson.JsonElement;
2121
import com.google.gson.JsonObject;
22+
import com.google.gson.reflect.TypeToken;
2223
import com.optimizely.ab.config.ProjectConfig;
2324
import com.optimizely.ab.config.audience.Audience;
2425
import com.optimizely.ab.config.audience.Condition;
@@ -28,6 +29,9 @@
2829
import org.junit.Test;
2930
import org.junit.rules.ExpectedException;
3031

32+
import java.lang.reflect.Type;
33+
import java.util.List;
34+
3135
import static com.optimizely.ab.config.ProjectConfigTestUtils.validConfigJsonV2;
3236
import static com.optimizely.ab.config.ProjectConfigTestUtils.validConfigJsonV3;
3337
import static com.optimizely.ab.config.ProjectConfigTestUtils.validConfigJsonV4;
@@ -81,8 +85,9 @@ public void parseAudience() throws Exception {
8185
"[\"and\", [\"or\", [\"or\", {\"name\": \"doubleKey\", \"type\": \"custom_attribute\", \"match\":\"lt\", \"value\":100.0}]]]");
8286

8387
AudienceGsonDeserializer deserializer = new AudienceGsonDeserializer();
88+
Type audienceType = new TypeToken<List<Audience>>() {}.getType();
8489

85-
Audience audience = deserializer.deserialize(jsonObject, null, null);
90+
Audience audience = deserializer.deserialize(jsonObject, audienceType, null);
8691

8792
assertNotNull(audience);
8893
assertNotNull(audience.getConditions());
@@ -98,8 +103,9 @@ public void parseInvalidAudience() throws Exception {
98103
"[\"and\", [\"or\", [\"or\", \"123\"]]]");
99104

100105
AudienceGsonDeserializer deserializer = new AudienceGsonDeserializer();
106+
Type audienceType = new TypeToken<List<Audience>>() {}.getType();
101107

102-
Audience audience = deserializer.deserialize(jsonObject, null, null);
108+
Audience audience = deserializer.deserialize(jsonObject, audienceType, null);
103109

104110
assertNotNull(audience);
105111
assertNotNull(audience.getConditions());

0 commit comments

Comments
 (0)