From 032c270ac042f90bb9c94b765de7e0cf7e49ccd8 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Mon, 20 Apr 2020 16:07:24 -0700 Subject: [PATCH 01/25] add OptimizelyJSON --- .../ab/optimizelyjson/OptimizelyJSON.java | 134 +++++++++++ .../ab/optimizelyjson/OptimizelyJSONTest.java | 212 ++++++++++++++++++ 2 files changed, 346 insertions(+) create mode 100644 core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java create mode 100644 core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONTest.java diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java b/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java new file mode 100644 index 000000000..db2e489a1 --- /dev/null +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java @@ -0,0 +1,134 @@ +/** + * + * Copyright 2020, 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.optimizelyjson; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import com.optimizely.ab.Optimizely; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.IOException; +import java.util.Map; + +public class OptimizelyJSON { + @Nullable + private String payload; + @Nullable + private Map map; + + private static final Logger logger = LoggerFactory.getLogger(OptimizelyJSON.class); + + public OptimizelyJSON(@Nonnull String payload) { + this.payload = payload; + } + + public OptimizelyJSON(@Nonnull Map map) { + this.map = map; + } + + public String toString() { + if (payload == null) { + if (map == null) return null; + + try { + Gson gson = new Gson(); + payload = gson.toJson(map); + } catch (JsonSyntaxException e) { + logger.error("Provided dictionary could not be converted to string."); + } + } + + return payload; + } + + public Map toMap() { + if (map == null) { + if (payload == null) return null; + + try { + Gson gson = new Gson(); + map = gson.fromJson(payload, Map.class); + } catch (JsonSyntaxException e) { + logger.error("Provided string could not be converted to dictionary."); + } + } + + return map; + } + + public T getValue(@Nullable String jsonKey, Class clazz) { + Map subMap = toMap(); + + if (jsonKey == null || jsonKey.isEmpty()) { + return getValueInternal(subMap, clazz); + } + String[] keys = jsonKey.split("\\."); + + T result = null; + + for(int i=0; i) subMap.get(key); + + if (i == keys.length - 1) { + result = getValueInternal(subMap, clazz); + break; + } + } else { + if (i == keys.length - 1) { + result = getValueInternal(subMap.get(key), clazz); + } else { + logger.error("Value for JSON key ({}) not found.", jsonKey); + } + break; + } + } + + if (result == null) { + logger.error("Value for path could not be assigned to provided schema."); + } + return result; + } + + private T getValueInternal(@Nullable Object object, Class clazz) { + if (object == null) return null; + + if (clazz.isInstance(object)) return (T)object; // primitive (String, Boolean, Integer, Double) + + Gson gson = new Gson(); + + try { + String payload = gson.toJson(object); + return gson.fromJson(payload, clazz); + } catch (Exception e) { + // + } + + return null; + } + +} + diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONTest.java new file mode 100644 index 000000000..a717d98a7 --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONTest.java @@ -0,0 +1,212 @@ +package com.optimizely.ab.optimizelyjson; + +import com.google.gson.annotations.SerializedName; +import com.optimizely.ab.Optimizely; +import com.optimizely.ab.config.DatafileProjectConfig; +import com.optimizely.ab.config.PollingProjectConfigManagerTest; +import com.sun.org.apache.xpath.internal.operations.Bool; +import org.junit.Before; +import org.junit.Test; + +import java.util.*; + +import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validConfigJsonV4; +import static org.junit.Assert.*; + +public class OptimizelyJSONTest { + + private String orgJson; + private Map orgMap; + + @Before + public void setUp() throws Exception { + orgJson = + "{ " + + " \"k1\": \"v1\", " + + " \"k2\": true, " + + " \"k3\": { " + + " \"kk1\": 1.5, " + + " \"kk2\": { " + + " \"kkk1\": true, " + + " \"kkk2\": 3.0, " + + " \"kkk3\": \"vvv3\", " + + " \"kkk4\": [5.7, true, \"vvv4\"] " + + " } " + + " } " + + "} "; + + Map m3 = new HashMap(); + m3.put("kkk1", true); + m3.put("kkk2", 3.0); + m3.put("kkk3", "vvv3"); + m3.put("kkk4", new ArrayList(Arrays.asList(5.7, true, "vvv4"))); + + Map m2 = new HashMap(); + m2.put("kk1", 1.5); + m2.put("kk2", m3); + + Map m1 = new HashMap(); + m1.put("k1", "v1"); + m1.put("k2", true); + m1.put("k3", m2); + + orgMap = m1; + } + + public class MD1 { + String k1; + boolean k2; + MD2 k3; + } + + public class MD2 { + double kk1; + MD3 kk2; + } + + public class MD3 { + boolean kkk1; + int kkk2; + String kkk3; + Object[] kkk4; + } + + public class NotMatchingType { + String x99; + } + + private String compact(String str) { + return str.replaceAll("\\s", ""); + } + + @Test + public void testOptimizelyJSON() { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + Map map = oj1.toMap(); + + OptimizelyJSON oj2 = new OptimizelyJSON(map); + String data = oj2.toString(); + + assertEquals(compact(data), compact(orgJson)); + } + + @Test + public void testToStringFromString() { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + assertEquals(compact(oj1.toString()), compact(orgJson)); + } + + @Test + public void testToStringFromMap() { + OptimizelyJSON oj1 = new OptimizelyJSON(orgMap); + assertEquals(compact(oj1.toString()), compact(orgJson)); + } + + @Test + public void testToMapFromString() { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + assertEquals(oj1.toMap(), orgMap); + } + + @Test + public void testToMapFromMap() { + OptimizelyJSON oj1 = new OptimizelyJSON(orgMap); + assertEquals(oj1.toMap(), orgMap); + } + + + @Test + public void testGetValueNullKeyPath() { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + + MD1 md1 = oj1.getValue(null, MD1.class); + assertNotNull(md1); + assertEquals(md1.k1, "v1"); + assertEquals(md1.k2, true); + assertEquals(md1.k3.kk2.kkk1, true); + assertEquals(md1.k3.kk2.kkk4[0], 5.7); + assertEquals(md1.k3.kk2.kkk4[2], "vvv4"); + } + + @Test + public void testGetValueEmptyKeyPath() { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + + MD1 md1 = oj1.getValue("", MD1.class); + assertEquals(md1.k1, "v1"); + assertEquals(md1.k2, true); + assertEquals(md1.k3.kk2.kkk1, true); + } + + @Test + public void testGetValueWithKeyPathToMapWithLevel1() { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + + MD2 md2 = oj1.getValue("k3", MD2.class); + assertNotNull(md2); + assertEquals(md2.kk1, 1.5, 0.01); + assertEquals(md2.kk2.kkk1, true); + } + + @Test + public void testGetValueWithKeyPathToMapWithLevel2() { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + + MD3 md3 = oj1.getValue("k3.kk2", MD3.class); + assertNotNull(md3); + assertEquals(md3.kkk1, true); + } + + @Test + public void testGetValueWithKeyPathToBoolean() { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + + Boolean value = oj1.getValue("k3.kk2.kkk1", Boolean.class); + assertNotNull(value); + assertEquals(value, true); + } + + @Test + public void testGetValueWithKeyPathToDouble() { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + + Double value = oj1.getValue("k3.kk2.kkk2", Double.class); + assertNotNull(value); + assertEquals(value.doubleValue(), 3.0, 0.01); + } + + @Test + public void testGetValueWithKeyPathToString() { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + + String value = oj1.getValue("k3.kk2.kkk3", String.class); + assertNotNull(value); + assertEquals(value, "vvv3"); + } + + @Test + public void testGetValueWithInvalidKeyPath() { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + + Integer value = oj1.getValue("x9", Integer.class); + assertNull(value); + } + + @Test + public void testGetValueWithWrongType() { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + + Integer value = oj1.getValue("k3.kk2.kkk3", Integer.class); + assertNull(value); + } + + @Test + public void testGetValueWithNotMatchingType() { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + + NotMatchingType md = oj1.getValue(null, NotMatchingType.class); + assertNull(md.x99); + } +} + + From 99014d2cc3ad3ae34a308b76bee742f682575b4e Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 21 Apr 2020 11:30:40 -0700 Subject: [PATCH 02/25] clean up --- .../ab/optimizelyjson/OptimizelyJSON.java | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java b/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java index db2e489a1..36ec438dc 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java @@ -32,7 +32,7 @@ public class OptimizelyJSON { @Nullable private String payload; @Nullable - private Map map; + private Map map; private static final Logger logger = LoggerFactory.getLogger(OptimizelyJSON.class); @@ -59,7 +59,7 @@ public String toString() { return payload; } - public Map toMap() { + public Map toMap() { if (map == null) { if (payload == null) return null; @@ -75,14 +75,14 @@ public Map toMap() { } public T getValue(@Nullable String jsonKey, Class clazz) { - Map subMap = toMap(); + Map subMap = toMap(); + T result = null; if (jsonKey == null || jsonKey.isEmpty()) { return getValueInternal(subMap, clazz); } - String[] keys = jsonKey.split("\\."); - T result = null; + String[] keys = jsonKey.split("\\."); for(int i=0; i T getValue(@Nullable String jsonKey, Class clazz) { String key = keys[i]; if (key.isEmpty()) break; - if (subMap.get(key) instanceof Map) { - subMap = (Map) subMap.get(key); + if (i == keys.length - 1) { + result = getValueInternal(subMap.get(key), clazz); + break; + } - if (i == keys.length - 1) { - result = getValueInternal(subMap, clazz); - break; - } + if (subMap.get(key) instanceof Map) { + subMap = (Map) subMap.get(key); } else { - if (i == keys.length - 1) { - result = getValueInternal(subMap.get(key), clazz); - } else { - logger.error("Value for JSON key ({}) not found.", jsonKey); - } + logger.error("Value for JSON key ({}) not found.", jsonKey); break; } } @@ -118,9 +114,8 @@ private T getValueInternal(@Nullable Object object, Class clazz) { if (clazz.isInstance(object)) return (T)object; // primitive (String, Boolean, Integer, Double) - Gson gson = new Gson(); - try { + Gson gson = new Gson(); String payload = gson.toJson(object); return gson.fromJson(payload, clazz); } catch (Exception e) { From 9de25358e01a74b77e9ded33382686ef0433049d Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 22 Apr 2020 13:55:01 -0700 Subject: [PATCH 03/25] add doc comments --- .../ab/optimizelyjson/OptimizelyJSON.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java b/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java index 36ec438dc..9e2020156 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java @@ -28,6 +28,9 @@ import java.io.IOException; import java.util.Map; +/** + * OptimizelyJSON is an object for accessing values of JSON-type feature variables + */ public class OptimizelyJSON { @Nullable private String payload; @@ -44,6 +47,9 @@ public OptimizelyJSON(@Nonnull Map map) { this.map = map; } + /** + * Returns the string representation of json data + */ public String toString() { if (payload == null) { if (map == null) return null; @@ -59,6 +65,9 @@ public String toString() { return payload; } + /** + * Returns the Map representation of json data + */ public Map toMap() { if (map == null) { if (payload == null) return null; @@ -74,6 +83,21 @@ public Map toMap() { return map; } + /** + * Populates the schema passed by the user - it takes primitive types and complex struct type + *

+ * Example: + *

+     *  JSON data is {"k1":true, "k2":{"k2":"v2"}}
+     *
+     *  Set jsonKey to "k1" to access the true boolean value or set it to to "k1.k2" to access {"k2":"v2"}.
+     *  Set it to null to access the entire JSON data.
+     * 
+ * + * @param jsonKey The JSON key paths for the data to access + * @param clazz The user-defined class that the json data will be parsed to + * @return an instance of clazz type with the parsed data filled in (or null if parse fails) + */ public T getValue(@Nullable String jsonKey, Class clazz) { Map subMap = toMap(); T result = null; From 69144a0c7f9d77e9ffd2b235ec4593953de0a1e0 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 24 Apr 2020 14:26:53 -0700 Subject: [PATCH 04/25] add more tests --- .../ab/optimizelyjson/OptimizelyJSON.java | 2 +- .../ab/optimizelyjson/OptimizelyJSONTest.java | 43 ++++++++++++++++--- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java b/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java index 9e2020156..2a94003f4 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java @@ -106,7 +106,7 @@ public T getValue(@Nullable String jsonKey, Class clazz) { return getValueInternal(subMap, clazz); } - String[] keys = jsonKey.split("\\."); + String[] keys = jsonKey.split("\\.", -1); // -1 to keep trailing empty fields for(int i=0; i m2 = new HashMap(); - m2.put("kk1", 1.5); + m2.put("kk1", 1); m2.put("kk2", m3); Map m1 = new HashMap(); @@ -60,7 +60,7 @@ public class MD1 { } public class MD2 { - double kk1; + int kk1; MD3 kk2; } @@ -123,9 +123,15 @@ public void testGetValueNullKeyPath() { assertNotNull(md1); assertEquals(md1.k1, "v1"); assertEquals(md1.k2, true); + assertEquals(md1.k3.kk1, 1); assertEquals(md1.k3.kk2.kkk1, true); assertEquals(md1.k3.kk2.kkk4[0], 5.7); assertEquals(md1.k3.kk2.kkk4[2], "vvv4"); + + // verify previous getValue does not destroy the data + + Boolean value = oj1.getValue("k3.kk2.kkk1", Boolean.class); + assertEquals(value, true); } @Test @@ -135,7 +141,10 @@ public void testGetValueEmptyKeyPath() { MD1 md1 = oj1.getValue("", MD1.class); assertEquals(md1.k1, "v1"); assertEquals(md1.k2, true); + assertEquals(md1.k3.kk1, 1); assertEquals(md1.k3.kk2.kkk1, true); + assertEquals(md1.k3.kk2.kkk4[0], 5.7); + assertEquals(md1.k3.kk2.kkk4[2], "vvv4"); } @Test @@ -188,7 +197,31 @@ public void testGetValueWithKeyPathToString() { public void testGetValueWithInvalidKeyPath() { OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); - Integer value = oj1.getValue("x9", Integer.class); + String value = oj1.getValue("k3..kkk3", String.class); + assertNull(value); + } + + @Test + public void testGetValueWithInvalidKeyPath2() { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + + String value = oj1.getValue("k1.", String.class); + assertNull(value); + } + + @Test + public void testGetValueWithInvalidKeyPath3() { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + + String value = oj1.getValue("x9", String.class); + assertNull(value); + } + + @Test + public void testGetValueWithInvalidKeyPath4() { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + + String value = oj1.getValue("k3.x9", String.class); assertNull(value); } From f319fdc079350a65ae32c1c4389464c1865e94ee Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 24 Apr 2020 16:29:11 -0700 Subject: [PATCH 05/25] fix test --- .../com/optimizely/ab/optimizelyjson/OptimizelyJSONTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONTest.java index 8653dff60..61a30f1b4 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONTest.java @@ -37,12 +37,12 @@ public void setUp() throws Exception { Map m3 = new HashMap(); m3.put("kkk1", true); - m3.put("kkk2", 3.0); + m3.put("kkk2", 3.5); m3.put("kkk3", "vvv3"); m3.put("kkk4", new ArrayList(Arrays.asList(5.7, true, "vvv4"))); Map m2 = new HashMap(); - m2.put("kk1", 1); + m2.put("kk1", Integer.valueOf(1)); m2.put("kk2", m3); Map m1 = new HashMap(); From 096bcb9cfb3496a0a99b4ee02b1aa1f584a1f49c Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 28 Apr 2020 15:45:44 -0700 Subject: [PATCH 06/25] (wip) add multiple parser support to OptimizelyJSON --- .../ab/config/parser/ConfigParser.java | 10 ++ .../ab/config/parser/GsonConfigParser.java | 16 ++- .../ab/config/parser/JacksonConfigParser.java | 23 +++- .../ab/config/parser/JsonConfigParser.java | 63 +++++++++-- .../config/parser/JsonSimpleConfigParser.java | 14 ++- .../parser/UnsupportedOperationException.java | 14 +++ .../ab/optimizelyjson/OptimizelyJSON.java | 45 +++++--- .../com/optimizely/ab/optimizelyjson/MD1.java | 7 ++ .../com/optimizely/ab/optimizelyjson/MD2.java | 6 + .../com/optimizely/ab/optimizelyjson/MD3.java | 8 ++ .../ab/optimizelyjson/NotMatchingType.java | 5 + .../ab/optimizelyjson/OptimizelyJSONTest.java | 107 ++++++++---------- .../OptimizelyJSONWithGsonParserTest.java | 41 +++++++ .../OptimizelyJSONWithJacksonParserTest.java | 41 +++++++ .../OptimizelyJSONWithJsonParserTest.java | 31 +++++ ...ptimizelyJSONWithJsonSimpleParserTest.java | 35 ++++++ 16 files changed, 380 insertions(+), 86 deletions(-) create mode 100644 core-api/src/main/java/com/optimizely/ab/config/parser/UnsupportedOperationException.java create mode 100644 core-api/src/test/java/com/optimizely/ab/optimizelyjson/MD1.java create mode 100644 core-api/src/test/java/com/optimizely/ab/optimizelyjson/MD2.java create mode 100644 core-api/src/test/java/com/optimizely/ab/optimizelyjson/MD3.java create mode 100644 core-api/src/test/java/com/optimizely/ab/optimizelyjson/NotMatchingType.java create mode 100644 core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithGsonParserTest.java create mode 100644 core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJacksonParserTest.java create mode 100644 core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonParserTest.java create mode 100644 core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonSimpleParserTest.java diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/ConfigParser.java b/core-api/src/main/java/com/optimizely/ab/config/parser/ConfigParser.java index eb24b68f3..ca5ec01c2 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/ConfigParser.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/ConfigParser.java @@ -16,6 +16,10 @@ */ package com.optimizely.ab.config.parser; +import com.google.gson.JsonElement; +import com.google.gson.JsonIOException; +import com.google.gson.JsonSyntaxException; +import com.google.gson.stream.JsonWriter; import com.optimizely.ab.config.ProjectConfig; import javax.annotation.Nonnull; @@ -38,4 +42,10 @@ public interface ConfigParser { * @throws ConfigParseException when there's an issue parsing the provided project config */ ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParseException; + + /** + * OptimizelyJSON parsing + */ + String toJson(Object src) throws ConfigParseException; + T fromJson(String json, Class clazz) throws ConfigParseException, UnsupportedOperationException; } 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 d86f72140..2b33c697f 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,6 +18,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; import com.optimizely.ab.config.*; import com.optimizely.ab.config.audience.Audience; import com.optimizely.ab.config.audience.TypedAudience; @@ -27,7 +28,7 @@ /** * {@link Gson}-based config parser implementation. */ -final class GsonConfigParser implements ConfigParser { +final public class GsonConfigParser implements ConfigParser { @Override public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParseException { @@ -52,4 +53,17 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse throw new ConfigParseException("Unable to parse datafile: " + json, e); } } + + public String toJson(Object src) { + return new Gson().toJson(src); + } + + public T fromJson(String json, Class clazz) throws ConfigParseException { + try { + return new Gson().fromJson(json, clazz); + } catch (Exception e) { + throw new ConfigParseException("Unable to parse JSON string: " + json, e); + } + } + } diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/JacksonConfigParser.java b/core-api/src/main/java/com/optimizely/ab/config/parser/JacksonConfigParser.java index e5c5ca5c0..f64cabfd6 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/JacksonConfigParser.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/JacksonConfigParser.java @@ -16,6 +16,7 @@ */ package com.optimizely.ab.config.parser; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; import com.optimizely.ab.config.DatafileProjectConfig; @@ -25,11 +26,12 @@ import com.optimizely.ab.config.audience.TypedAudience; import javax.annotation.Nonnull; +import java.io.IOException; /** * {@code Jackson}-based config parser implementation. */ -final class JacksonConfigParser implements ConfigParser { +final public class JacksonConfigParser implements ConfigParser { private ObjectMapper objectMapper; public JacksonConfigParser() { @@ -61,4 +63,23 @@ public ProjectConfigModule() { addDeserializer(Condition.class, new ConditionJacksonDeserializer(objectMapper)); } } + + @Override + public String toJson(Object src) throws ConfigParseException { + try { + return objectMapper.writeValueAsString(src); + } catch (JsonProcessingException e) { + throw new ConfigParseException("Serialization failed", e); + } + } + + @Override + public T fromJson(String json, Class clazz) throws ConfigParseException { + try { + return objectMapper.readValue(json, clazz); + } catch (IOException e) { + throw new ConfigParseException("Unable to parse JSON string: " + json, e); + } + } + } 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 21198da06..fb79c4957 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 @@ -28,17 +28,12 @@ import org.json.JSONTokener; import javax.annotation.Nonnull; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; /** * {@code org.json}-based config parser implementation. */ -final class JsonConfigParser implements ConfigParser { +final public class JsonConfigParser implements ConfigParser { @Override public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParseException { @@ -385,4 +380,58 @@ private List parseRollouts(JSONArray rolloutsJson) { return rollouts; } + + @Override + public String toJson(Object src) { + JSONObject json = new JSONObject(src); + return json.toString(); + } + + @Override + public T fromJson(String json, Class clazz) throws UnsupportedOperationException { + if (Map.class.isAssignableFrom(clazz)) { + JSONObject obj = new JSONObject(json); + return (T)jsonObjectToMap(obj); + } + + // JsonSimple does not support parsing to user objects + + throw new UnsupportedOperationException("A proper JSON parser is not available. Use Gson or Jackson parser for this operation."); + } + + private Map jsonObjectToMap(JSONObject obj) { + Map map = new HashMap<>(); + + Iterator keys = obj.keys(); + while(keys.hasNext()) { + String key = keys.next(); + Object value = obj.get(key); + + if (value instanceof JSONArray) { + value = jsonArrayToList((JSONArray)value); + } else if (value instanceof JSONObject) { + value = jsonObjectToMap((JSONObject)value); + } + + map.put(key, value); + } + + return map; + } + + private List jsonArrayToList(JSONArray array) { + List list = new ArrayList<>(); + for(Object value : array) { + if (value instanceof JSONArray) { + value = jsonArrayToList((JSONArray)value); + } else if (value instanceof JSONObject) { + value = jsonObjectToMap((JSONObject)value); + } + + list.add(value); + } + + return list; + } + } 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 f800595e3..47a1b8b24 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 @@ -26,6 +26,7 @@ import com.optimizely.ab.internal.ConditionUtils; import org.json.simple.JSONArray; import org.json.simple.JSONObject; +import org.json.simple.JSONValue; import org.json.simple.parser.JSONParser; import org.json.simple.parser.ParseException; @@ -41,7 +42,7 @@ /** * {@code json-simple}-based config parser implementation. */ -final class JsonSimpleConfigParser implements ConfigParser { +final public class JsonSimpleConfigParser implements ConfigParser { @Override public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParseException { @@ -371,5 +372,16 @@ private List parseRollouts(JSONArray rolloutsJson) { return rollouts; } + + @Override + public String toJson(Object src) { + return JSONValue.toJSONString(src); + } + + @Override + public T fromJson(String json, Class clazz) throws UnsupportedOperationException { + throw new UnsupportedOperationException("A proper JSON parser is not available. Use Gson or Jackson parser for this operation."); + } + } diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/UnsupportedOperationException.java b/core-api/src/main/java/com/optimizely/ab/config/parser/UnsupportedOperationException.java new file mode 100644 index 000000000..0e1a2f09d --- /dev/null +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/UnsupportedOperationException.java @@ -0,0 +1,14 @@ +package com.optimizely.ab.config.parser; + +/** + * Wrapper around all types of JSON parser exceptions. + */ +public final class UnsupportedOperationException extends Exception { + public UnsupportedOperationException(String message) { + super(message); + } + + public UnsupportedOperationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java b/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java index 2a94003f4..0677980cc 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java @@ -16,16 +16,16 @@ */ package com.optimizely.ab.optimizelyjson; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonSyntaxException; import com.optimizely.ab.Optimizely; +import com.optimizely.ab.config.parser.ConfigParseException; +import com.optimizely.ab.config.parser.ConfigParser; +import com.optimizely.ab.config.parser.DefaultConfigParser; +import com.optimizely.ab.config.parser.UnsupportedOperationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import java.io.IOException; import java.util.Map; /** @@ -37,14 +37,26 @@ public class OptimizelyJSON { @Nullable private Map map; + private ConfigParser parser; + private static final Logger logger = LoggerFactory.getLogger(OptimizelyJSON.class); public OptimizelyJSON(@Nonnull String payload) { + this(payload, DefaultConfigParser.getInstance()); + } + + public OptimizelyJSON(@Nonnull String payload, ConfigParser parser) { this.payload = payload; + this.parser = parser; } public OptimizelyJSON(@Nonnull Map map) { + this(map, DefaultConfigParser.getInstance()); + } + + public OptimizelyJSON(@Nonnull Map map, ConfigParser parser) { this.map = map; + this.parser = parser; } /** @@ -55,10 +67,9 @@ public String toString() { if (map == null) return null; try { - Gson gson = new Gson(); - payload = gson.toJson(map); - } catch (JsonSyntaxException e) { - logger.error("Provided dictionary could not be converted to string."); + payload = parser.toJson(map); + } catch (ConfigParseException e) { + logger.error("Provided map could not be converted to a string."); } } @@ -73,10 +84,9 @@ public Map toMap() { if (payload == null) return null; try { - Gson gson = new Gson(); - map = gson.fromJson(payload, Map.class); - } catch (JsonSyntaxException e) { - logger.error("Provided string could not be converted to dictionary."); + map = parser.fromJson(payload, Map.class); + } catch (Exception e) { + logger.error("Provided string could not be converted to a dictionary."); } } @@ -88,9 +98,9 @@ public Map toMap() { *

* Example: *

-     *  JSON data is {"k1":true, "k2":{"k2":"v2"}}
+     *  JSON data is {"k1":true, "k2":{"k22":"v22"}}
      *
-     *  Set jsonKey to "k1" to access the true boolean value or set it to to "k1.k2" to access {"k2":"v2"}.
+     *  Set jsonKey to "k2" to access {"k22":"v22"} or set it to to "k2.k22" to access "v22".
      *  Set it to null to access the entire JSON data.
      * 
* @@ -139,11 +149,10 @@ private T getValueInternal(@Nullable Object object, Class clazz) { if (clazz.isInstance(object)) return (T)object; // primitive (String, Boolean, Integer, Double) try { - Gson gson = new Gson(); - String payload = gson.toJson(object); - return gson.fromJson(payload, clazz); + String payload = parser.toJson(object); + return parser.fromJson(payload, clazz); } catch (Exception e) { - // + logger.error("Map to Java Object failed.", e.toString()); } return null; diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/MD1.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/MD1.java new file mode 100644 index 000000000..0d144d738 --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/MD1.java @@ -0,0 +1,7 @@ +package com.optimizely.ab.optimizelyjson; + +public class MD1 { + public String k1; + public boolean k2; + public MD2 k3; +} diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/MD2.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/MD2.java new file mode 100644 index 000000000..a0471db05 --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/MD2.java @@ -0,0 +1,6 @@ +package com.optimizely.ab.optimizelyjson; + +public class MD2 { + public double kk1; + public MD3 kk2; +} diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/MD3.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/MD3.java new file mode 100644 index 000000000..9588a3a13 --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/MD3.java @@ -0,0 +1,8 @@ +package com.optimizely.ab.optimizelyjson; + +public class MD3 { + public boolean kkk1; + public double kkk2; + public String kkk3; + public Object[] kkk4; +} diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/NotMatchingType.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/NotMatchingType.java new file mode 100644 index 000000000..8d98f0841 --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/NotMatchingType.java @@ -0,0 +1,5 @@ +package com.optimizely.ab.optimizelyjson; + +public class NotMatchingType { + public String x99; +} diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONTest.java index 61a30f1b4..56c966769 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONTest.java @@ -1,9 +1,27 @@ +/** + * + * Copyright 2020, 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.optimizelyjson; import com.google.gson.annotations.SerializedName; import com.optimizely.ab.Optimizely; import com.optimizely.ab.config.DatafileProjectConfig; import com.optimizely.ab.config.PollingProjectConfigManagerTest; +import com.optimizely.ab.config.parser.ConfigParser; +import com.optimizely.ab.config.parser.DefaultConfigParser; import com.sun.org.apache.xpath.internal.operations.Bool; import org.junit.Before; import org.junit.Test; @@ -14,9 +32,8 @@ import static org.junit.Assert.*; public class OptimizelyJSONTest { - - private String orgJson; - private Map orgMap; + protected String orgJson; + protected Map orgMap; @Before public void setUp() throws Exception { @@ -25,7 +42,7 @@ public void setUp() throws Exception { " \"k1\": \"v1\", " + " \"k2\": true, " + " \"k3\": { " + - " \"kk1\": 1, " + + " \"kk1\": 1.0, " + " \"kk2\": { " + " \"kkk1\": true, " + " \"kkk2\": 3.5, " + @@ -42,7 +59,7 @@ public void setUp() throws Exception { m3.put("kkk4", new ArrayList(Arrays.asList(5.7, true, "vvv4"))); Map m2 = new HashMap(); - m2.put("kk1", Integer.valueOf(1)); + m2.put("kk1", 1.0); m2.put("kk2", m3); Map m1 = new HashMap(); @@ -53,38 +70,21 @@ public void setUp() throws Exception { orgMap = m1; } - public class MD1 { - String k1; - boolean k2; - MD2 k3; - } - - public class MD2 { - int kk1; - MD3 kk2; - } - public class MD3 { - boolean kkk1; - int kkk2; - String kkk3; - Object[] kkk4; - } - public class NotMatchingType { - String x99; - } - private String compact(String str) { + protected String compact(String str) { return str.replaceAll("\\s", ""); } + protected ConfigParser getParser() { return DefaultConfigParser.getInstance(); } + // Common tests for all parsers (GSON, Jackson, Json, JsonSimple) @Test public void testOptimizelyJSON() { - OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); Map map = oj1.toMap(); - OptimizelyJSON oj2 = new OptimizelyJSON(map); + OptimizelyJSON oj2 = new OptimizelyJSON(map, getParser()); String data = oj2.toString(); assertEquals(compact(data), compact(orgJson)); @@ -92,40 +92,39 @@ public void testOptimizelyJSON() { @Test public void testToStringFromString() { - OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); assertEquals(compact(oj1.toString()), compact(orgJson)); } @Test public void testToStringFromMap() { - OptimizelyJSON oj1 = new OptimizelyJSON(orgMap); + OptimizelyJSON oj1 = new OptimizelyJSON(orgMap, getParser()); assertEquals(compact(oj1.toString()), compact(orgJson)); } @Test public void testToMapFromString() { - OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); assertEquals(oj1.toMap(), orgMap); } @Test public void testToMapFromMap() { - OptimizelyJSON oj1 = new OptimizelyJSON(orgMap); + OptimizelyJSON oj1 = new OptimizelyJSON(orgMap, getParser()); assertEquals(oj1.toMap(), orgMap); } - @Test public void testGetValueNullKeyPath() { - OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); MD1 md1 = oj1.getValue(null, MD1.class); assertNotNull(md1); assertEquals(md1.k1, "v1"); assertEquals(md1.k2, true); - assertEquals(md1.k3.kk1, 1); + assertEquals(md1.k3.kk1, 1.0, 0.01); assertEquals(md1.k3.kk2.kkk1, true); - assertEquals(md1.k3.kk2.kkk4[0], 5.7); + assertEquals((Double)md1.k3.kk2.kkk4[0], 5.7, 0.01); assertEquals(md1.k3.kk2.kkk4[2], "vvv4"); // verify previous getValue does not destroy the data @@ -136,30 +135,30 @@ public void testGetValueNullKeyPath() { @Test public void testGetValueEmptyKeyPath() { - OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); MD1 md1 = oj1.getValue("", MD1.class); assertEquals(md1.k1, "v1"); assertEquals(md1.k2, true); - assertEquals(md1.k3.kk1, 1); + assertEquals(md1.k3.kk1, 1.0, 0.01); assertEquals(md1.k3.kk2.kkk1, true); - assertEquals(md1.k3.kk2.kkk4[0], 5.7); + assertEquals((Double) md1.k3.kk2.kkk4[0], 5.7, 0.01); assertEquals(md1.k3.kk2.kkk4[2], "vvv4"); } @Test public void testGetValueWithKeyPathToMapWithLevel1() { - OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); MD2 md2 = oj1.getValue("k3", MD2.class); assertNotNull(md2); - assertEquals(md2.kk1, 1.5, 0.01); + assertEquals(md2.kk1, 1.0, 0.01); assertEquals(md2.kk2.kkk1, true); } @Test public void testGetValueWithKeyPathToMapWithLevel2() { - OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); MD3 md3 = oj1.getValue("k3.kk2", MD3.class); assertNotNull(md3); @@ -168,7 +167,7 @@ public void testGetValueWithKeyPathToMapWithLevel2() { @Test public void testGetValueWithKeyPathToBoolean() { - OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); Boolean value = oj1.getValue("k3.kk2.kkk1", Boolean.class); assertNotNull(value); @@ -177,16 +176,16 @@ public void testGetValueWithKeyPathToBoolean() { @Test public void testGetValueWithKeyPathToDouble() { - OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); Double value = oj1.getValue("k3.kk2.kkk2", Double.class); assertNotNull(value); - assertEquals(value.doubleValue(), 3.0, 0.01); + assertEquals(value.doubleValue(), 3.5, 0.01); } @Test public void testGetValueWithKeyPathToString() { - OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); String value = oj1.getValue("k3.kk2.kkk3", String.class); assertNotNull(value); @@ -195,7 +194,7 @@ public void testGetValueWithKeyPathToString() { @Test public void testGetValueWithInvalidKeyPath() { - OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); String value = oj1.getValue("k3..kkk3", String.class); assertNull(value); @@ -203,7 +202,7 @@ public void testGetValueWithInvalidKeyPath() { @Test public void testGetValueWithInvalidKeyPath2() { - OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); String value = oj1.getValue("k1.", String.class); assertNull(value); @@ -211,7 +210,7 @@ public void testGetValueWithInvalidKeyPath2() { @Test public void testGetValueWithInvalidKeyPath3() { - OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); String value = oj1.getValue("x9", String.class); assertNull(value); @@ -219,7 +218,7 @@ public void testGetValueWithInvalidKeyPath3() { @Test public void testGetValueWithInvalidKeyPath4() { - OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); String value = oj1.getValue("k3.x9", String.class); assertNull(value); @@ -227,19 +226,11 @@ public void testGetValueWithInvalidKeyPath4() { @Test public void testGetValueWithWrongType() { - OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); Integer value = oj1.getValue("k3.kk2.kkk3", Integer.class); assertNull(value); } - @Test - public void testGetValueWithNotMatchingType() { - OptimizelyJSON oj1 = new OptimizelyJSON(orgJson); - - NotMatchingType md = oj1.getValue(null, NotMatchingType.class); - assertNull(md.x99); - } } - diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithGsonParserTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithGsonParserTest.java new file mode 100644 index 000000000..26fd19f31 --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithGsonParserTest.java @@ -0,0 +1,41 @@ +/** + * + * Copyright 2020, 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.optimizelyjson; + +import com.optimizely.ab.config.parser.ConfigParser; +import com.optimizely.ab.config.parser.GsonConfigParser; +import com.optimizely.ab.config.parser.JsonSimpleConfigParser; +import org.junit.Test; + +import static org.junit.Assert.assertNull; + +public class OptimizelyJSONWithGsonParserTest extends OptimizelyJSONTest { + @Override + protected ConfigParser getParser() { + return new GsonConfigParser(); + } + + // Tests for GSON only + + @Test + public void testGetValueWithNotMatchingType() { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); + + NotMatchingType md = oj1.getValue(null, NotMatchingType.class); + assertNull(md.x99); + } +} diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJacksonParserTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJacksonParserTest.java new file mode 100644 index 000000000..9be7e612f --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJacksonParserTest.java @@ -0,0 +1,41 @@ +/** + * + * Copyright 2020, 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.optimizelyjson; + +import com.optimizely.ab.config.parser.ConfigParser; +import com.optimizely.ab.config.parser.JacksonConfigParser; +import org.junit.Test; + +import static org.junit.Assert.assertNull; + +public class OptimizelyJSONWithJacksonParserTest extends OptimizelyJSONTest { + @Override + protected ConfigParser getParser() { + return new JacksonConfigParser(); + } + + // Tests for Jackson only + + @Test + public void testGetValueWithNotMatchingType() { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); + + // Jackson returns null object when variables not matching (while GSON returns an object with null variables + NotMatchingType md = oj1.getValue(null, NotMatchingType.class); + assertNull(md); + } +} diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonParserTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonParserTest.java new file mode 100644 index 000000000..9a29f2d63 --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonParserTest.java @@ -0,0 +1,31 @@ +/** + * + * Copyright 2020, 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.optimizelyjson; + +import com.optimizely.ab.config.parser.ConfigParser; +import com.optimizely.ab.config.parser.JsonConfigParser; +import com.optimizely.ab.config.parser.JsonSimpleConfigParser; + +public class OptimizelyJSONWithJsonParserTest extends OptimizelyJSONTest { + @Override + protected ConfigParser getParser() { + return new JsonConfigParser(); + } + + // Tests for Json only + +} diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonSimpleParserTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonSimpleParserTest.java new file mode 100644 index 000000000..f7692bfac --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonSimpleParserTest.java @@ -0,0 +1,35 @@ +/** + * + * Copyright 2020, 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.optimizelyjson; + +import com.optimizely.ab.config.parser.ConfigParser; +import com.optimizely.ab.config.parser.JsonSimpleConfigParser; +import org.junit.Test; + +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class OptimizelyJSONWithJsonSimpleParserTest extends OptimizelyJSONTest { + @Override + protected ConfigParser getParser() { + return new JsonSimpleConfigParser(); + } + + // Tests for JsonSimple only + +} From 5be5f3fb5928b3e55673f87fc7a8c8615cb92026 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 29 Apr 2020 11:04:39 -0700 Subject: [PATCH 07/25] split getvalue tests --- .../ab/config/parser/JsonConfigParser.java | 41 +----- .../config/parser/JsonSimpleConfigParser.java | 7 + ...NTest.java => OptimizelyJSONCoreTest.java} | 127 +----------------- .../OptimizelyJSONWithGsonParserTest.java | 1 - .../OptimizelyJSONWithJsonParserTest.java | 5 +- ...ptimizelyJSONWithJsonSimpleParserTest.java | 5 +- 6 files changed, 17 insertions(+), 169 deletions(-) rename core-api/src/test/java/com/optimizely/ab/optimizelyjson/{OptimizelyJSONTest.java => OptimizelyJSONCoreTest.java} (50%) 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 fb79c4957..1acf15481 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 @@ -383,7 +383,7 @@ private List parseRollouts(JSONArray rolloutsJson) { @Override public String toJson(Object src) { - JSONObject json = new JSONObject(src); + JSONObject json = (JSONObject)JsonHelpers.convertToJsonObject(src); return json.toString(); } @@ -391,47 +391,12 @@ public String toJson(Object src) { public T fromJson(String json, Class clazz) throws UnsupportedOperationException { if (Map.class.isAssignableFrom(clazz)) { JSONObject obj = new JSONObject(json); - return (T)jsonObjectToMap(obj); + return (T)JsonHelpers.jsonObjectToMap(obj); } - // JsonSimple does not support parsing to user objects + // org.json parser does not support parsing to user objects throw new UnsupportedOperationException("A proper JSON parser is not available. Use Gson or Jackson parser for this operation."); } - private Map jsonObjectToMap(JSONObject obj) { - Map map = new HashMap<>(); - - Iterator keys = obj.keys(); - while(keys.hasNext()) { - String key = keys.next(); - Object value = obj.get(key); - - if (value instanceof JSONArray) { - value = jsonArrayToList((JSONArray)value); - } else if (value instanceof JSONObject) { - value = jsonObjectToMap((JSONObject)value); - } - - map.put(key, value); - } - - return map; - } - - private List jsonArrayToList(JSONArray array) { - List list = new ArrayList<>(); - for(Object value : array) { - if (value instanceof JSONArray) { - value = jsonArrayToList((JSONArray)value); - } else if (value instanceof JSONObject) { - value = jsonObjectToMap((JSONObject)value); - } - - list.add(value); - } - - return list; - } - } 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 47a1b8b24..c7921c9b9 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 @@ -380,6 +380,13 @@ public String toJson(Object src) { @Override public T fromJson(String json, Class clazz) throws UnsupportedOperationException { + if (Map.class.isAssignableFrom(clazz)) { + org.json.JSONObject obj = new org.json.JSONObject(json); + return (T)JsonHelpers.jsonObjectToMap(obj); + } + + // org.json.simple does not support parsing to user objects + throw new UnsupportedOperationException("A proper JSON parser is not available. Use Gson or Jackson parser for this operation."); } diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONCoreTest.java similarity index 50% rename from core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONTest.java rename to core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONCoreTest.java index 56c966769..43997c722 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONCoreTest.java @@ -31,7 +31,7 @@ import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validConfigJsonV4; import static org.junit.Assert.*; -public class OptimizelyJSONTest { +public class OptimizelyJSONCoreTest { protected String orgJson; protected Map orgMap; @@ -42,7 +42,7 @@ public void setUp() throws Exception { " \"k1\": \"v1\", " + " \"k2\": true, " + " \"k3\": { " + - " \"kk1\": 1.0, " + + " \"kk1\": 1.2, " + " \"kk2\": { " + " \"kkk1\": true, " + " \"kkk2\": 3.5, " + @@ -59,7 +59,7 @@ public void setUp() throws Exception { m3.put("kkk4", new ArrayList(Arrays.asList(5.7, true, "vvv4"))); Map m2 = new HashMap(); - m2.put("kk1", 1.0); + m2.put("kk1", 1.2); m2.put("kk2", m3); Map m1 = new HashMap(); @@ -70,9 +70,6 @@ public void setUp() throws Exception { orgMap = m1; } - - - protected String compact(String str) { return str.replaceAll("\\s", ""); } @@ -114,123 +111,5 @@ public void testToMapFromMap() { assertEquals(oj1.toMap(), orgMap); } - @Test - public void testGetValueNullKeyPath() { - OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); - - MD1 md1 = oj1.getValue(null, MD1.class); - assertNotNull(md1); - assertEquals(md1.k1, "v1"); - assertEquals(md1.k2, true); - assertEquals(md1.k3.kk1, 1.0, 0.01); - assertEquals(md1.k3.kk2.kkk1, true); - assertEquals((Double)md1.k3.kk2.kkk4[0], 5.7, 0.01); - assertEquals(md1.k3.kk2.kkk4[2], "vvv4"); - - // verify previous getValue does not destroy the data - - Boolean value = oj1.getValue("k3.kk2.kkk1", Boolean.class); - assertEquals(value, true); - } - - @Test - public void testGetValueEmptyKeyPath() { - OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); - - MD1 md1 = oj1.getValue("", MD1.class); - assertEquals(md1.k1, "v1"); - assertEquals(md1.k2, true); - assertEquals(md1.k3.kk1, 1.0, 0.01); - assertEquals(md1.k3.kk2.kkk1, true); - assertEquals((Double) md1.k3.kk2.kkk4[0], 5.7, 0.01); - assertEquals(md1.k3.kk2.kkk4[2], "vvv4"); - } - - @Test - public void testGetValueWithKeyPathToMapWithLevel1() { - OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); - - MD2 md2 = oj1.getValue("k3", MD2.class); - assertNotNull(md2); - assertEquals(md2.kk1, 1.0, 0.01); - assertEquals(md2.kk2.kkk1, true); - } - - @Test - public void testGetValueWithKeyPathToMapWithLevel2() { - OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); - - MD3 md3 = oj1.getValue("k3.kk2", MD3.class); - assertNotNull(md3); - assertEquals(md3.kkk1, true); - } - - @Test - public void testGetValueWithKeyPathToBoolean() { - OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); - - Boolean value = oj1.getValue("k3.kk2.kkk1", Boolean.class); - assertNotNull(value); - assertEquals(value, true); - } - - @Test - public void testGetValueWithKeyPathToDouble() { - OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); - - Double value = oj1.getValue("k3.kk2.kkk2", Double.class); - assertNotNull(value); - assertEquals(value.doubleValue(), 3.5, 0.01); - } - - @Test - public void testGetValueWithKeyPathToString() { - OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); - - String value = oj1.getValue("k3.kk2.kkk3", String.class); - assertNotNull(value); - assertEquals(value, "vvv3"); - } - - @Test - public void testGetValueWithInvalidKeyPath() { - OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); - - String value = oj1.getValue("k3..kkk3", String.class); - assertNull(value); - } - - @Test - public void testGetValueWithInvalidKeyPath2() { - OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); - - String value = oj1.getValue("k1.", String.class); - assertNull(value); - } - - @Test - public void testGetValueWithInvalidKeyPath3() { - OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); - - String value = oj1.getValue("x9", String.class); - assertNull(value); - } - - @Test - public void testGetValueWithInvalidKeyPath4() { - OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); - - String value = oj1.getValue("k3.x9", String.class); - assertNull(value); - } - - @Test - public void testGetValueWithWrongType() { - OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); - - Integer value = oj1.getValue("k3.kk2.kkk3", Integer.class); - assertNull(value); - } - } diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithGsonParserTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithGsonParserTest.java index 26fd19f31..aeb0dc66a 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithGsonParserTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithGsonParserTest.java @@ -18,7 +18,6 @@ import com.optimizely.ab.config.parser.ConfigParser; import com.optimizely.ab.config.parser.GsonConfigParser; -import com.optimizely.ab.config.parser.JsonSimpleConfigParser; import org.junit.Test; import static org.junit.Assert.assertNull; diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonParserTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonParserTest.java index 9a29f2d63..ba80308c2 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonParserTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonParserTest.java @@ -18,9 +18,10 @@ import com.optimizely.ab.config.parser.ConfigParser; import com.optimizely.ab.config.parser.JsonConfigParser; -import com.optimizely.ab.config.parser.JsonSimpleConfigParser; -public class OptimizelyJSONWithJsonParserTest extends OptimizelyJSONTest { +import static org.junit.Assert.assertNull; + +public class OptimizelyJSONWithJsonParserTest extends OptimizelyJSONCoreTest { @Override protected ConfigParser getParser() { return new JsonConfigParser(); diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonSimpleParserTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonSimpleParserTest.java index f7692bfac..51e749618 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonSimpleParserTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonSimpleParserTest.java @@ -18,13 +18,10 @@ import com.optimizely.ab.config.parser.ConfigParser; import com.optimizely.ab.config.parser.JsonSimpleConfigParser; -import org.junit.Test; - -import java.util.Map; import static org.junit.Assert.assertEquals; -public class OptimizelyJSONWithJsonSimpleParserTest extends OptimizelyJSONTest { +public class OptimizelyJSONWithJsonSimpleParserTest extends OptimizelyJSONCoreTest { @Override protected ConfigParser getParser() { return new JsonSimpleConfigParser(); From 01970f5adf407168f0eaa6e31572ba4e565f54f2 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 29 Apr 2020 11:36:49 -0700 Subject: [PATCH 08/25] disable getValue for legacy parsers --- .../ab/config/parser/JsonConfigParser.java | 5 ++--- .../ab/config/parser/JsonSimpleConfigParser.java | 5 ++--- .../ab/optimizelyjson/OptimizelyJSON.java | 10 ++++++---- .../OptimizelyJSONWithGsonParserTest.java | 3 ++- .../OptimizelyJSONWithJacksonParserTest.java | 3 ++- .../OptimizelyJSONWithJsonParserTest.java | 16 +++++++++++++++- .../OptimizelyJSONWithJsonSimpleParserTest.java | 14 ++++++++++++++ 7 files changed, 43 insertions(+), 13 deletions(-) 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 1acf15481..cff854502 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 @@ -388,15 +388,14 @@ public String toJson(Object src) { } @Override - public T fromJson(String json, Class clazz) throws UnsupportedOperationException { + public T fromJson(String json, Class clazz) { if (Map.class.isAssignableFrom(clazz)) { JSONObject obj = new JSONObject(json); return (T)JsonHelpers.jsonObjectToMap(obj); } // org.json parser does not support parsing to user objects - - throw new UnsupportedOperationException("A proper JSON parser is not available. Use Gson or Jackson parser for this operation."); + return null; } } 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 c7921c9b9..d392642d3 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 @@ -379,15 +379,14 @@ public String toJson(Object src) { } @Override - public T fromJson(String json, Class clazz) throws UnsupportedOperationException { + public T fromJson(String json, Class clazz) { if (Map.class.isAssignableFrom(clazz)) { org.json.JSONObject obj = new org.json.JSONObject(json); return (T)JsonHelpers.jsonObjectToMap(obj); } // org.json.simple does not support parsing to user objects - - throw new UnsupportedOperationException("A proper JSON parser is not available. Use Gson or Jackson parser for this operation."); + return null; } } diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java b/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java index 0677980cc..b23f0beb9 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java @@ -17,9 +17,7 @@ package com.optimizely.ab.optimizelyjson; import com.optimizely.ab.Optimizely; -import com.optimizely.ab.config.parser.ConfigParseException; -import com.optimizely.ab.config.parser.ConfigParser; -import com.optimizely.ab.config.parser.DefaultConfigParser; +import com.optimizely.ab.config.parser.*; import com.optimizely.ab.config.parser.UnsupportedOperationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -108,7 +106,11 @@ public Map toMap() { * @param clazz The user-defined class that the json data will be parsed to * @return an instance of clazz type with the parsed data filled in (or null if parse fails) */ - public T getValue(@Nullable String jsonKey, Class clazz) { + public T getValue(@Nullable String jsonKey, Class clazz) throws UnsupportedOperationException { + if (!(parser instanceof GsonConfigParser || parser instanceof JacksonConfigParser)) { + throw new UnsupportedOperationException("A proper JSON parser is not available. Use Gson or Jackson parser for this operation."); + } + Map subMap = toMap(); T result = null; diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithGsonParserTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithGsonParserTest.java index aeb0dc66a..cc043de2d 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithGsonParserTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithGsonParserTest.java @@ -18,6 +18,7 @@ import com.optimizely.ab.config.parser.ConfigParser; import com.optimizely.ab.config.parser.GsonConfigParser; +import com.optimizely.ab.config.parser.UnsupportedOperationException; import org.junit.Test; import static org.junit.Assert.assertNull; @@ -31,7 +32,7 @@ protected ConfigParser getParser() { // Tests for GSON only @Test - public void testGetValueWithNotMatchingType() { + public void testGetValueWithNotMatchingType() throws UnsupportedOperationException { OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); NotMatchingType md = oj1.getValue(null, NotMatchingType.class); diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJacksonParserTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJacksonParserTest.java index 9be7e612f..2cba7928c 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJacksonParserTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJacksonParserTest.java @@ -18,6 +18,7 @@ import com.optimizely.ab.config.parser.ConfigParser; import com.optimizely.ab.config.parser.JacksonConfigParser; +import com.optimizely.ab.config.parser.UnsupportedOperationException; import org.junit.Test; import static org.junit.Assert.assertNull; @@ -31,7 +32,7 @@ protected ConfigParser getParser() { // Tests for Jackson only @Test - public void testGetValueWithNotMatchingType() { + public void testGetValueWithNotMatchingType() throws UnsupportedOperationException { OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); // Jackson returns null object when variables not matching (while GSON returns an object with null variables diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonParserTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonParserTest.java index ba80308c2..af2cb0259 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonParserTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonParserTest.java @@ -18,8 +18,10 @@ import com.optimizely.ab.config.parser.ConfigParser; import com.optimizely.ab.config.parser.JsonConfigParser; +import com.optimizely.ab.config.parser.UnsupportedOperationException; +import org.junit.Test; -import static org.junit.Assert.assertNull; +import static org.junit.Assert.*; public class OptimizelyJSONWithJsonParserTest extends OptimizelyJSONCoreTest { @Override @@ -29,4 +31,16 @@ protected ConfigParser getParser() { // Tests for Json only + @Test + public void testGetValueThrowsException() { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); + + try { + MD1 md1 = oj1.getValue(null, MD1.class); + fail("GetValue is not supported for or.json paraser"); + } catch (UnsupportedOperationException e) { + assertEquals(e.getMessage(), "A proper JSON parser is not available. Use Gson or Jackson parser for this operation."); + } + } + } diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonSimpleParserTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonSimpleParserTest.java index 51e749618..568224b0d 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonSimpleParserTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonSimpleParserTest.java @@ -18,8 +18,11 @@ import com.optimizely.ab.config.parser.ConfigParser; import com.optimizely.ab.config.parser.JsonSimpleConfigParser; +import com.optimizely.ab.config.parser.UnsupportedOperationException; +import org.junit.Test; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; public class OptimizelyJSONWithJsonSimpleParserTest extends OptimizelyJSONCoreTest { @Override @@ -29,4 +32,15 @@ protected ConfigParser getParser() { // Tests for JsonSimple only + @Test + public void testGetValueThrowsException() { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); + + try { + MD1 md1 = oj1.getValue(null, MD1.class); + fail("GetValue is not supported for or.json paraser"); + } catch (UnsupportedOperationException e) { + assertEquals(e.getMessage(), "A proper JSON parser is not available. Use Gson or Jackson parser for this operation."); + } + } } From 2401784985c4010b6f85b2e141f5bab2b35e92e5 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 29 Apr 2020 11:46:36 -0700 Subject: [PATCH 09/25] add legacy parser tests --- .../ab/config/parser/JsonHelpers.java | 81 ++++++++++ .../ab/optimizelyjson/OptimizelyJSONTest.java | 145 ++++++++++++++++++ .../OptimizelyJSONWithJsonParserTest.java | 2 +- ...ptimizelyJSONWithJsonSimpleParserTest.java | 2 +- 4 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 core-api/src/main/java/com/optimizely/ab/config/parser/JsonHelpers.java create mode 100644 core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONTest.java diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonHelpers.java b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonHelpers.java new file mode 100644 index 000000000..6b9a5f199 --- /dev/null +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonHelpers.java @@ -0,0 +1,81 @@ +/** + * + * Copyright 2016-2017, 2019, 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 org.json.JSONArray; +import org.json.JSONObject; + +import java.util.*; + +final class JsonHelpers { + + static Object convertToJsonObject(Object obj) { + if (obj instanceof Map) { + Map map = (Map)obj; + JSONObject jObj = new JSONObject(); + for (Object key : map.keySet()) { + jObj.put(key.toString(), convertToJsonObject(map.get(key))); + } + return jObj; + } else if (obj instanceof List) { + List list = (List)obj; + JSONArray jArray = new JSONArray(); + for (Object value : list) { + jArray.put(convertToJsonObject(value)); + } + return jArray; + } else { + return obj; + } + } + + static Map jsonObjectToMap(JSONObject jObj) { + Map map = new HashMap<>(); + + Iterator keys = jObj.keys(); + while(keys.hasNext()) { + String key = keys.next(); + Object value = jObj.get(key); + + if (value instanceof JSONArray) { + value = jsonArrayToList((JSONArray)value); + } else if (value instanceof JSONObject) { + value = jsonObjectToMap((JSONObject)value); + } + + map.put(key, value); + } + + return map; + } + + static List jsonArrayToList(JSONArray array) { + List list = new ArrayList<>(); + for(Object value : array) { + if (value instanceof JSONArray) { + value = jsonArrayToList((JSONArray)value); + } else if (value instanceof JSONObject) { + value = jsonObjectToMap((JSONObject)value); + } + + list.add(value); + } + + return list; + } + +} diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONTest.java new file mode 100644 index 000000000..945fa60fd --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONTest.java @@ -0,0 +1,145 @@ +/** + * + * Copyright 2020, 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.optimizelyjson; + +import com.optimizely.ab.config.parser.UnsupportedOperationException; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class OptimizelyJSONTest extends OptimizelyJSONCoreTest { + + @Test + public void testGetValueNullKeyPath() throws UnsupportedOperationException { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); + + MD1 md1 = oj1.getValue(null, MD1.class); + assertNotNull(md1); + assertEquals(md1.k1, "v1"); + assertEquals(md1.k2, true); + assertEquals(md1.k3.kk1, 1.2, 0.01); + assertEquals(md1.k3.kk2.kkk1, true); + assertEquals((Double)md1.k3.kk2.kkk4[0], 5.7, 0.01); + assertEquals(md1.k3.kk2.kkk4[2], "vvv4"); + + // verify previous getValue does not destroy the data + + Boolean value = oj1.getValue("k3.kk2.kkk1", Boolean.class); + assertEquals(value, true); + } + + @Test + public void testGetValueEmptyKeyPath() throws UnsupportedOperationException { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); + + MD1 md1 = oj1.getValue("", MD1.class); + assertEquals(md1.k1, "v1"); + assertEquals(md1.k2, true); + assertEquals(md1.k3.kk1, 1.2, 0.01); + assertEquals(md1.k3.kk2.kkk1, true); + assertEquals((Double) md1.k3.kk2.kkk4[0], 5.7, 0.01); + assertEquals(md1.k3.kk2.kkk4[2], "vvv4"); + } + + @Test + public void testGetValueWithKeyPathToMapWithLevel1() throws UnsupportedOperationException { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); + + MD2 md2 = oj1.getValue("k3", MD2.class); + assertNotNull(md2); + assertEquals(md2.kk1, 1.2, 0.01); + assertEquals(md2.kk2.kkk1, true); + } + + @Test + public void testGetValueWithKeyPathToMapWithLevel2() throws UnsupportedOperationException { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); + + MD3 md3 = oj1.getValue("k3.kk2", MD3.class); + assertNotNull(md3); + assertEquals(md3.kkk1, true); + } + + @Test + public void testGetValueWithKeyPathToBoolean() throws UnsupportedOperationException { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); + + Boolean value = oj1.getValue("k3.kk2.kkk1", Boolean.class); + assertNotNull(value); + assertEquals(value, true); + } + + @Test + public void testGetValueWithKeyPathToDouble() throws UnsupportedOperationException { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); + + Double value = oj1.getValue("k3.kk2.kkk2", Double.class); + assertNotNull(value); + assertEquals(value.doubleValue(), 3.5, 0.01); + } + + @Test + public void testGetValueWithKeyPathToString() throws UnsupportedOperationException { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); + + String value = oj1.getValue("k3.kk2.kkk3", String.class); + assertNotNull(value); + assertEquals(value, "vvv3"); + } + + @Test + public void testGetValueWithInvalidKeyPath() throws UnsupportedOperationException { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); + + String value = oj1.getValue("k3..kkk3", String.class); + assertNull(value); + } + + @Test + public void testGetValueWithInvalidKeyPath2() throws UnsupportedOperationException { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); + + String value = oj1.getValue("k1.", String.class); + assertNull(value); + } + + @Test + public void testGetValueWithInvalidKeyPath3() throws UnsupportedOperationException { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); + + String value = oj1.getValue("x9", String.class); + assertNull(value); + } + + @Test + public void testGetValueWithInvalidKeyPath4() throws UnsupportedOperationException { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); + + String value = oj1.getValue("k3.x9", String.class); + assertNull(value); + } + + @Test + public void testGetValueWithWrongType() throws UnsupportedOperationException { + OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); + + Integer value = oj1.getValue("k3.kk2.kkk3", Integer.class); + assertNull(value); + } + +} + diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonParserTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonParserTest.java index af2cb0259..9094c56fc 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonParserTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonParserTest.java @@ -36,7 +36,7 @@ public void testGetValueThrowsException() { OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); try { - MD1 md1 = oj1.getValue(null, MD1.class); + String str = oj1.getValue(null, String.class); fail("GetValue is not supported for or.json paraser"); } catch (UnsupportedOperationException e) { assertEquals(e.getMessage(), "A proper JSON parser is not available. Use Gson or Jackson parser for this operation."); diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonSimpleParserTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonSimpleParserTest.java index 568224b0d..40279a331 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonSimpleParserTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonSimpleParserTest.java @@ -37,7 +37,7 @@ public void testGetValueThrowsException() { OptimizelyJSON oj1 = new OptimizelyJSON(orgJson, getParser()); try { - MD1 md1 = oj1.getValue(null, MD1.class); + String str = oj1.getValue(null, String.class); fail("GetValue is not supported for or.json paraser"); } catch (UnsupportedOperationException e) { assertEquals(e.getMessage(), "A proper JSON parser is not available. Use Gson or Jackson parser for this operation."); From afdb8be4cdd0f60c3b5047930f8f229c62394abd Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 30 Apr 2020 11:27:08 -0700 Subject: [PATCH 10/25] fix tests for all parsers --- .../ab/config/parser/ConfigParser.java | 2 +- .../ab/config/parser/GsonConfigParser.java | 2 +- .../ab/config/parser/JacksonConfigParser.java | 4 +- .../ab/config/parser/JsonConfigParser.java | 4 +- .../config/parser/JsonSimpleConfigParser.java | 4 +- .../parser/UnsupportedOperationException.java | 3 - .../ab/optimizelyjson/OptimizelyJSON.java | 6 +- ...t.java => OptimizelyJSONExtendedTest.java} | 5 +- .../OptimizelyJSONWithGsonParserTest.java | 64 ++++++++++++++++++- .../OptimizelyJSONWithJacksonParserTest.java | 61 +++++++++++++++++- .../OptimizelyJSONWithJsonParserTest.java | 45 +++++++++++++ ...ptimizelyJSONWithJsonSimpleParserTest.java | 46 +++++++++++++ .../ab/optimizelyjson/{ => types}/MD1.java | 2 +- .../ab/optimizelyjson/{ => types}/MD2.java | 2 +- .../ab/optimizelyjson/{ => types}/MD3.java | 2 +- .../ab/optimizelyjson/types/MDN1.java | 7 ++ .../ab/optimizelyjson/types/MDN2.java | 6 ++ .../{ => types}/NotMatchingType.java | 2 +- 18 files changed, 246 insertions(+), 21 deletions(-) rename core-api/src/test/java/com/optimizely/ab/optimizelyjson/{OptimizelyJSONTest.java => OptimizelyJSONExtendedTest.java} (95%) rename core-api/src/test/java/com/optimizely/ab/optimizelyjson/{ => types}/MD1.java (64%) rename core-api/src/test/java/com/optimizely/ab/optimizelyjson/{ => types}/MD2.java (57%) rename core-api/src/test/java/com/optimizely/ab/optimizelyjson/{ => types}/MD3.java (71%) create mode 100644 core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MDN1.java create mode 100644 core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MDN2.java rename core-api/src/test/java/com/optimizely/ab/optimizelyjson/{ => types}/NotMatchingType.java (54%) diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/ConfigParser.java b/core-api/src/main/java/com/optimizely/ab/config/parser/ConfigParser.java index ca5ec01c2..11e1a6905 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/ConfigParser.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/ConfigParser.java @@ -47,5 +47,5 @@ public interface ConfigParser { * OptimizelyJSON parsing */ String toJson(Object src) throws ConfigParseException; - T fromJson(String json, Class clazz) throws ConfigParseException, UnsupportedOperationException; + T fromJson(String json, Class clazz) throws ConfigParseException; } 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 2b33c697f..93f0f4de9 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 @@ -62,7 +62,7 @@ public T fromJson(String json, Class clazz) throws ConfigParseException { try { return new Gson().fromJson(json, clazz); } catch (Exception e) { - throw new ConfigParseException("Unable to parse JSON string: " + json, e); + throw new ConfigParseException("Unable to parse JSON string: " + e.toString()); } } diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/JacksonConfigParser.java b/core-api/src/main/java/com/optimizely/ab/config/parser/JacksonConfigParser.java index f64cabfd6..88a762114 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/JacksonConfigParser.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/JacksonConfigParser.java @@ -69,7 +69,7 @@ public String toJson(Object src) throws ConfigParseException { try { return objectMapper.writeValueAsString(src); } catch (JsonProcessingException e) { - throw new ConfigParseException("Serialization failed", e); + throw new ConfigParseException("Serialization failed: " + e.toString()); } } @@ -78,7 +78,7 @@ public T fromJson(String json, Class clazz) throws ConfigParseException { try { return objectMapper.readValue(json, clazz); } catch (IOException e) { - throw new ConfigParseException("Unable to parse JSON string: " + json, e); + throw new ConfigParseException("Unable to parse JSON string: " + e.toString()); } } 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 cff854502..a2c3f16d1 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 @@ -388,14 +388,14 @@ public String toJson(Object src) { } @Override - public T fromJson(String json, Class clazz) { + public T fromJson(String json, Class clazz) throws ConfigParseException { if (Map.class.isAssignableFrom(clazz)) { JSONObject obj = new JSONObject(json); return (T)JsonHelpers.jsonObjectToMap(obj); } // org.json parser does not support parsing to user objects - return null; + throw new ConfigParseException("Parsing fails with a unsupported 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 d392642d3..152413a08 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 @@ -379,14 +379,14 @@ public String toJson(Object src) { } @Override - public T fromJson(String json, Class clazz) { + public T fromJson(String json, Class clazz) throws ConfigParseException { if (Map.class.isAssignableFrom(clazz)) { org.json.JSONObject obj = new org.json.JSONObject(json); return (T)JsonHelpers.jsonObjectToMap(obj); } // org.json.simple does not support parsing to user objects - return null; + throw new ConfigParseException("Parsing fails with a unsupported type"); } } diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/UnsupportedOperationException.java b/core-api/src/main/java/com/optimizely/ab/config/parser/UnsupportedOperationException.java index 0e1a2f09d..d4fb974d8 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/UnsupportedOperationException.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/UnsupportedOperationException.java @@ -1,8 +1,5 @@ package com.optimizely.ab.config.parser; -/** - * Wrapper around all types of JSON parser exceptions. - */ public final class UnsupportedOperationException extends Exception { public UnsupportedOperationException(String message) { super(message); diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java b/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java index b23f0beb9..808279d49 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java @@ -67,7 +67,7 @@ public String toString() { try { payload = parser.toJson(map); } catch (ConfigParseException e) { - logger.error("Provided map could not be converted to a string."); + logger.error("Provided map could not be converted to a string ({})", e.toString()); } } @@ -84,7 +84,7 @@ public Map toMap() { try { map = parser.fromJson(payload, Map.class); } catch (Exception e) { - logger.error("Provided string could not be converted to a dictionary."); + logger.error("Provided string could not be converted to a dictionary ({})", e.toString()); } } @@ -154,7 +154,7 @@ private T getValueInternal(@Nullable Object object, Class clazz) { String payload = parser.toJson(object); return parser.fromJson(payload, clazz); } catch (Exception e) { - logger.error("Map to Java Object failed.", e.toString()); + logger.error("Map to Java Object failed ({})", e.toString()); } return null; diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONExtendedTest.java similarity index 95% rename from core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONTest.java rename to core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONExtendedTest.java index 945fa60fd..a8429c945 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONExtendedTest.java @@ -17,11 +17,14 @@ package com.optimizely.ab.optimizelyjson; import com.optimizely.ab.config.parser.UnsupportedOperationException; +import com.optimizely.ab.optimizelyjson.types.MD1; +import com.optimizely.ab.optimizelyjson.types.MD2; +import com.optimizely.ab.optimizelyjson.types.MD3; import org.junit.Test; import static org.junit.Assert.*; -public class OptimizelyJSONTest extends OptimizelyJSONCoreTest { +public class OptimizelyJSONExtendedTest extends OptimizelyJSONCoreTest { @Test public void testGetValueNullKeyPath() throws UnsupportedOperationException { diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithGsonParserTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithGsonParserTest.java index cc043de2d..75e5d4e92 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithGsonParserTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithGsonParserTest.java @@ -19,11 +19,17 @@ import com.optimizely.ab.config.parser.ConfigParser; import com.optimizely.ab.config.parser.GsonConfigParser; import com.optimizely.ab.config.parser.UnsupportedOperationException; +import com.optimizely.ab.optimizelyjson.types.MDN1; +import com.optimizely.ab.optimizelyjson.types.NotMatchingType; import org.junit.Test; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; -public class OptimizelyJSONWithGsonParserTest extends OptimizelyJSONTest { +public class OptimizelyJSONWithGsonParserTest extends OptimizelyJSONExtendedTest { @Override protected ConfigParser getParser() { return new GsonConfigParser(); @@ -38,4 +44,60 @@ public void testGetValueWithNotMatchingType() throws UnsupportedOperationExcepti NotMatchingType md = oj1.getValue(null, NotMatchingType.class); assertNull(md.x99); } + + // Tests for integer/double processing + + @Test + public void testIntegerProcessing() throws UnsupportedOperationException { + + // GSON parser toMap() adds ".0" to all integers + + String json = "{\"k1\":1,\"k2\":2.5,\"k3\":{\"kk1\":3,\"kk2\":4.0}}"; + + Map m2 = new HashMap(); + m2.put("kk1", 3.0); + m2.put("kk2", 4.0); + + Map m1 = new HashMap(); + m1.put("k1", 1.0); + m1.put("k2", 2.5); + m1.put("k3", m2); + + OptimizelyJSON oj1 = new OptimizelyJSON(json, getParser()); + assertEquals(oj1.toMap(), m1); + } + + @Test + public void testIntegerProcessing2() throws UnsupportedOperationException { + + // GSON parser toString() keeps ".0" in double + + String json = "{\"k1\":1,\"k2\":2.5,\"k3\":{\"kk1\":3,\"kk2\":4.0}}"; + + Map m2 = new HashMap(); + m2.put("kk1", 3); + m2.put("kk2", 4.0); + + Map m1 = new HashMap(); + m1.put("k1", 1); + m1.put("k2", 2.5); + m1.put("k3", m2); + + OptimizelyJSON oj1 = new OptimizelyJSON(m1, getParser()); + assertEquals(compact(oj1.toString()), compact(json)); + } + + @Test + public void testIntegerProcessing3() throws UnsupportedOperationException { + String json = "{\"k1\":1,\"k2\":2.5,\"k3\":{\"kk1\":3,\"kk2\":4.0}}"; + + OptimizelyJSON oj1 = new OptimizelyJSON(json, getParser()); + MDN1 obj = oj1.getValue(null, MDN1.class); + + assertEquals(obj.k1, 1); + assertEquals(obj.k2, 2.5, 0.01); + assertEquals(obj.k3.kk1, 3); + assertEquals(obj.k3.kk2, 4.0, 0.01); + } + } diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJacksonParserTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJacksonParserTest.java index 2cba7928c..e56290394 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJacksonParserTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJacksonParserTest.java @@ -19,11 +19,17 @@ import com.optimizely.ab.config.parser.ConfigParser; import com.optimizely.ab.config.parser.JacksonConfigParser; import com.optimizely.ab.config.parser.UnsupportedOperationException; +import com.optimizely.ab.optimizelyjson.types.MDN1; +import com.optimizely.ab.optimizelyjson.types.NotMatchingType; import org.junit.Test; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; -public class OptimizelyJSONWithJacksonParserTest extends OptimizelyJSONTest { +public class OptimizelyJSONWithJacksonParserTest extends OptimizelyJSONExtendedTest { @Override protected ConfigParser getParser() { return new JacksonConfigParser(); @@ -39,4 +45,57 @@ public void testGetValueWithNotMatchingType() throws UnsupportedOperationExcepti NotMatchingType md = oj1.getValue(null, NotMatchingType.class); assertNull(md); } + + // Tests for integer/double processing + + @Test + public void testIntegerProcessing() throws UnsupportedOperationException { + + // Jackson parser toMap() keeps ".0" in double + + String json = "{\"k1\":1,\"k2\":2.5,\"k3\":{\"kk1\":3,\"kk2\":4.0}}"; + + Map m2 = new HashMap(); + m2.put("kk1", 3); + m2.put("kk2", 4.0); + + Map m1 = new HashMap(); + m1.put("k1", 1); + m1.put("k2", 2.5); + m1.put("k3", m2); + + OptimizelyJSON oj1 = new OptimizelyJSON(json, getParser()); + assertEquals(oj1.toMap(), m1); + } + + @Test + public void testIntegerProcessing2() throws UnsupportedOperationException { + + // Jackson parser toString() keeps ".0" in double + + String json = "{\"k1\":1,\"k2\":2.5,\"k3\":{\"kk1\":3,\"kk2\":4.0}}"; + + Map m2 = new HashMap(); + m2.put("kk1", 3); + m2.put("kk2", 4.0); + + Map m1 = new HashMap(); + m1.put("k1", 1); + m1.put("k2", 2.5); + m1.put("k3", m2); + + OptimizelyJSON oj1 = new OptimizelyJSON(m1, getParser()); + assertEquals(compact(oj1.toString()), compact(json)); + } + + @Test + public void testIntegerProcessing3() throws UnsupportedOperationException { + String json = "{\"k1\":1,\"k2\":2.5,\"k3\":{\"kk1\":3,\"kk2\":4.0}}"; + + OptimizelyJSON oj1 = new OptimizelyJSON(json, getParser()); + MDN1 obj = oj1.getValue(null, MDN1.class); + + assertEquals(obj.k1, 1); + } + } diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonParserTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonParserTest.java index 9094c56fc..f2205a596 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonParserTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonParserTest.java @@ -21,6 +21,9 @@ import com.optimizely.ab.config.parser.UnsupportedOperationException; import org.junit.Test; +import java.util.HashMap; +import java.util.Map; + import static org.junit.Assert.*; public class OptimizelyJSONWithJsonParserTest extends OptimizelyJSONCoreTest { @@ -43,4 +46,46 @@ public void testGetValueThrowsException() { } } + // Tests for integer/double processing + + @Test + public void testIntegerProcessing() throws UnsupportedOperationException { + + // org.json parser toMap() keeps ".0" in double + + String json = "{\"k1\":1,\"k2\":2.5,\"k3\":{\"kk1\":3,\"kk2\":4.0}}"; + + Map m2 = new HashMap(); + m2.put("kk1", 3); + m2.put("kk2", 4.0); + + Map m1 = new HashMap(); + m1.put("k1", 1); + m1.put("k2", 2.5); + m1.put("k3", m2); + + OptimizelyJSON oj1 = new OptimizelyJSON(json, getParser()); + assertEquals(oj1.toMap(), m1); + } + + @Test + public void testIntegerProcessing2() throws UnsupportedOperationException { + + // org.json parser toString() drops ".0" from double + + String json = "{\"k1\":1,\"k2\":2.5,\"k3\":{\"kk1\":3,\"kk2\":4}}"; + + Map m2 = new HashMap(); + m2.put("kk1", 3); + m2.put("kk2", 4.0); + + Map m1 = new HashMap(); + m1.put("k1", 1); + m1.put("k2", 2.5); + m1.put("k3", m2); + + OptimizelyJSON oj1 = new OptimizelyJSON(m1, getParser()); + assertEquals(compact(oj1.toString()), compact(json)); + } + } diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonSimpleParserTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonSimpleParserTest.java index 40279a331..645821ac6 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonSimpleParserTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonSimpleParserTest.java @@ -21,6 +21,9 @@ import com.optimizely.ab.config.parser.UnsupportedOperationException; import org.junit.Test; +import java.util.HashMap; +import java.util.Map; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; @@ -43,4 +46,47 @@ public void testGetValueThrowsException() { assertEquals(e.getMessage(), "A proper JSON parser is not available. Use Gson or Jackson parser for this operation."); } } + + // Tests for integer/double processing + + @Test + public void testIntegerProcessing() throws UnsupportedOperationException { + + // org.json.simple parser toMap() keeps ".0" in double + + String json = "{\"k1\":1,\"k2\":2.5,\"k3\":{\"kk1\":3,\"kk2\":4.0}}"; + + Map m2 = new HashMap(); + m2.put("kk1", 3); + m2.put("kk2", 4.0); + + Map m1 = new HashMap(); + m1.put("k1", 1); + m1.put("k2", 2.5); + m1.put("k3", m2); + + OptimizelyJSON oj1 = new OptimizelyJSON(json, getParser()); + assertEquals(oj1.toMap(), m1); + } + + @Test + public void testIntegerProcessing2() throws UnsupportedOperationException { + + // org.json.simple parser toString() keeps ".0" in double + + String json = "{\"k1\":1,\"k2\":2.5,\"k3\":{\"kk1\":3,\"kk2\":4.0}}"; + + Map m2 = new HashMap(); + m2.put("kk1", 3); + m2.put("kk2", 4.0); + + Map m1 = new HashMap(); + m1.put("k1", 1); + m1.put("k2", 2.5); + m1.put("k3", m2); + + OptimizelyJSON oj1 = new OptimizelyJSON(m1, getParser()); + assertEquals(compact(oj1.toString()), compact(json)); + } + } diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/MD1.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MD1.java similarity index 64% rename from core-api/src/test/java/com/optimizely/ab/optimizelyjson/MD1.java rename to core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MD1.java index 0d144d738..888a7548b 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/MD1.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MD1.java @@ -1,4 +1,4 @@ -package com.optimizely.ab.optimizelyjson; +package com.optimizely.ab.optimizelyjson.types; public class MD1 { public String k1; diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/MD2.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MD2.java similarity index 57% rename from core-api/src/test/java/com/optimizely/ab/optimizelyjson/MD2.java rename to core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MD2.java index a0471db05..c60148345 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/MD2.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MD2.java @@ -1,4 +1,4 @@ -package com.optimizely.ab.optimizelyjson; +package com.optimizely.ab.optimizelyjson.types; public class MD2 { public double kk1; diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/MD3.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MD3.java similarity index 71% rename from core-api/src/test/java/com/optimizely/ab/optimizelyjson/MD3.java rename to core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MD3.java index 9588a3a13..cfe36ff35 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/MD3.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MD3.java @@ -1,4 +1,4 @@ -package com.optimizely.ab.optimizelyjson; +package com.optimizely.ab.optimizelyjson.types; public class MD3 { public boolean kkk1; diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MDN1.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MDN1.java new file mode 100644 index 000000000..bf18e8f45 --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MDN1.java @@ -0,0 +1,7 @@ +package com.optimizely.ab.optimizelyjson.types; + +public class MDN1 { + public int k1; + public double k2; + public MDN2 k3; +} diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MDN2.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MDN2.java new file mode 100644 index 000000000..9fa6d91c7 --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MDN2.java @@ -0,0 +1,6 @@ +package com.optimizely.ab.optimizelyjson.types; + +public class MDN2 { + public int kk1; + public double kk2; +} diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/NotMatchingType.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/NotMatchingType.java similarity index 54% rename from core-api/src/test/java/com/optimizely/ab/optimizelyjson/NotMatchingType.java rename to core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/NotMatchingType.java index 8d98f0841..1e2cd367f 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/NotMatchingType.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/NotMatchingType.java @@ -1,4 +1,4 @@ -package com.optimizely.ab.optimizelyjson; +package com.optimizely.ab.optimizelyjson.types; public class NotMatchingType { public String x99; From 24ed0d68e1d58d6ba6def2647913462677d6ebf9 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 30 Apr 2020 12:09:07 -0700 Subject: [PATCH 11/25] cleanup --- .../java/com/optimizely/ab/config/parser/GsonConfigParser.java | 2 +- .../com/optimizely/ab/config/parser/JacksonConfigParser.java | 2 +- .../java/com/optimizely/ab/config/parser/JsonConfigParser.java | 2 +- .../main/java/com/optimizely/ab/config/parser/JsonHelpers.java | 2 +- .../com/optimizely/ab/config/parser/JsonSimpleConfigParser.java | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) 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 93f0f4de9..ca683cbc5 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 @@ -1,6 +1,6 @@ /** * - * Copyright 2016-2017, 2019, Optimizely and contributors + * Copyright 2016-2017, 2019-2020, 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. diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/JacksonConfigParser.java b/core-api/src/main/java/com/optimizely/ab/config/parser/JacksonConfigParser.java index 88a762114..59cc8639e 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/JacksonConfigParser.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/JacksonConfigParser.java @@ -1,6 +1,6 @@ /** * - * Copyright 2016-2018, Optimizely and contributors + * Copyright 2016-2018, 2020, 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. 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 a2c3f16d1..3ace07556 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 @@ -1,6 +1,6 @@ /** * - * Copyright 2016-2019, Optimizely and contributors + * Copyright 2016-2019, 2020, 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. diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonHelpers.java b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonHelpers.java index 6b9a5f199..ddede9787 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonHelpers.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonHelpers.java @@ -1,6 +1,6 @@ /** * - * Copyright 2016-2017, 2019, Optimizely and contributors + * Copyright 2020, 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. 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 152413a08..15d3c63f5 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 @@ -1,6 +1,6 @@ /** * - * Copyright 2016-2019, Optimizely and contributors + * Copyright 2016-2019, 2020, 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. From 5d4508568c6bf51a10fcdd125facc3ea818c977a Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 30 Apr 2020 15:08:39 -0700 Subject: [PATCH 12/25] fix findbugs issues --- .../optimizely/ab/config/parser/JsonHelpers.java | 6 +++--- .../parser/UnsupportedOperationException.java | 16 ++++++++++++++++ .../ab/optimizelyjson/OptimizelyJSON.java | 12 ++++-------- .../OptimizelyJSONWithJsonParserTest.java | 2 +- .../OptimizelyJSONWithJsonSimpleParserTest.java | 2 +- .../optimizely/ab/optimizelyjson/types/MD1.java | 16 ++++++++++++++++ .../optimizely/ab/optimizelyjson/types/MD2.java | 16 ++++++++++++++++ .../optimizely/ab/optimizelyjson/types/MD3.java | 16 ++++++++++++++++ .../optimizely/ab/optimizelyjson/types/MDN1.java | 16 ++++++++++++++++ .../optimizely/ab/optimizelyjson/types/MDN2.java | 16 ++++++++++++++++ .../ab/optimizelyjson/types/NotMatchingType.java | 16 ++++++++++++++++ 11 files changed, 121 insertions(+), 13 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonHelpers.java b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonHelpers.java index ddede9787..405c863c5 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonHelpers.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonHelpers.java @@ -25,10 +25,10 @@ final class JsonHelpers { static Object convertToJsonObject(Object obj) { if (obj instanceof Map) { - Map map = (Map)obj; + Map map = (Map)obj; JSONObject jObj = new JSONObject(); - for (Object key : map.keySet()) { - jObj.put(key.toString(), convertToJsonObject(map.get(key))); + for (Map.Entry entry : map.entrySet()) { + jObj.put(entry.getKey().toString(), convertToJsonObject(entry.getValue())); } return jObj; } else if (obj instanceof List) { diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/UnsupportedOperationException.java b/core-api/src/main/java/com/optimizely/ab/config/parser/UnsupportedOperationException.java index d4fb974d8..02980455c 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/UnsupportedOperationException.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/UnsupportedOperationException.java @@ -1,3 +1,19 @@ +/** + * + * Copyright 2020, 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; public final class UnsupportedOperationException extends Exception { diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java b/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java index 808279d49..7a9cf6afe 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java @@ -61,9 +61,7 @@ public OptimizelyJSON(@Nonnull Map map, ConfigParser parser) { * Returns the string representation of json data */ public String toString() { - if (payload == null) { - if (map == null) return null; - + if (payload == null && map != null) { try { payload = parser.toJson(map); } catch (ConfigParseException e) { @@ -71,16 +69,14 @@ public String toString() { } } - return payload; + return payload != null ? payload : ""; } /** - * Returns the Map representation of json data + * Returns the {@code Map} representation of json data */ public Map toMap() { - if (map == null) { - if (payload == null) return null; - + if (map == null && payload != null) { try { map = parser.fromJson(payload, Map.class); } catch (Exception e) { diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonParserTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonParserTest.java index f2205a596..b08beefa6 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonParserTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonParserTest.java @@ -40,7 +40,7 @@ public void testGetValueThrowsException() { try { String str = oj1.getValue(null, String.class); - fail("GetValue is not supported for or.json paraser"); + fail("GetValue is not supported for or.json paraser: " + str); } catch (UnsupportedOperationException e) { assertEquals(e.getMessage(), "A proper JSON parser is not available. Use Gson or Jackson parser for this operation."); } diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonSimpleParserTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonSimpleParserTest.java index 645821ac6..5632737ca 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonSimpleParserTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONWithJsonSimpleParserTest.java @@ -41,7 +41,7 @@ public void testGetValueThrowsException() { try { String str = oj1.getValue(null, String.class); - fail("GetValue is not supported for or.json paraser"); + fail("GetValue is not supported for or.json paraser: " + str); } catch (UnsupportedOperationException e) { assertEquals(e.getMessage(), "A proper JSON parser is not available. Use Gson or Jackson parser for this operation."); } diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MD1.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MD1.java index 888a7548b..57f9cb413 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MD1.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MD1.java @@ -1,3 +1,19 @@ +/** + * + * Copyright 2020, 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.optimizelyjson.types; public class MD1 { diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MD2.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MD2.java index c60148345..2830f36c7 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MD2.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MD2.java @@ -1,3 +1,19 @@ +/** + * + * Copyright 2020, 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.optimizelyjson.types; public class MD2 { diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MD3.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MD3.java index cfe36ff35..4244d8d85 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MD3.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MD3.java @@ -1,3 +1,19 @@ +/** + * + * Copyright 2020, 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.optimizelyjson.types; public class MD3 { diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MDN1.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MDN1.java index bf18e8f45..edf7a0a81 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MDN1.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MDN1.java @@ -1,3 +1,19 @@ +/** + * + * Copyright 2020, 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.optimizelyjson.types; public class MDN1 { diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MDN2.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MDN2.java index 9fa6d91c7..28f1de0eb 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MDN2.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/MDN2.java @@ -1,3 +1,19 @@ +/** + * + * Copyright 2020, 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.optimizelyjson.types; public class MDN2 { diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/NotMatchingType.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/NotMatchingType.java index 1e2cd367f..ddd5cc85a 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/NotMatchingType.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/types/NotMatchingType.java @@ -1,3 +1,19 @@ +/** + * + * Copyright 2020, 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.optimizelyjson.types; public class NotMatchingType { From 2af6f143312fdbbf283e1225e0ef3e9eceaf8fcb Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 30 Apr 2020 15:28:48 -0700 Subject: [PATCH 13/25] cleanup --- .../java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java b/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java index 7a9cf6afe..565eba4d6 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java @@ -60,6 +60,7 @@ public OptimizelyJSON(@Nonnull Map map, ConfigParser parser) { /** * Returns the string representation of json data */ + @Nonnull public String toString() { if (payload == null && map != null) { try { @@ -75,6 +76,7 @@ public String toString() { /** * Returns the {@code Map} representation of json data */ + @Nullable public Map toMap() { if (map == null && payload != null) { try { @@ -102,6 +104,7 @@ public Map toMap() { * @param clazz The user-defined class that the json data will be parsed to * @return an instance of clazz type with the parsed data filled in (or null if parse fails) */ + @Nullable public T getValue(@Nullable String jsonKey, Class clazz) throws UnsupportedOperationException { if (!(parser instanceof GsonConfigParser || parser instanceof JacksonConfigParser)) { throw new UnsupportedOperationException("A proper JSON parser is not available. Use Gson or Jackson parser for this operation."); From b5db42937ad00ad8c6a102820300d5f5e503133b Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 30 Apr 2020 17:35:25 -0700 Subject: [PATCH 14/25] fix test per review --- .../ab/optimizelyjson/OptimizelyJSONExtendedTest.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONExtendedTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONExtendedTest.java index a8429c945..acab2350b 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONExtendedTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyjson/OptimizelyJSONExtendedTest.java @@ -41,8 +41,13 @@ public void testGetValueNullKeyPath() throws UnsupportedOperationException { // verify previous getValue does not destroy the data - Boolean value = oj1.getValue("k3.kk2.kkk1", Boolean.class); - assertEquals(value, true); + MD1 newMd1 = oj1.getValue(null, MD1.class); + assertEquals(newMd1.k1, "v1"); + assertEquals(newMd1.k2, true); + assertEquals(newMd1.k3.kk1, 1.2, 0.01); + assertEquals(newMd1.k3.kk2.kkk1, true); + assertEquals((Double)newMd1.k3.kk2.kkk4[0], 5.7, 0.01); + assertEquals(newMd1.k3.kk2.kkk4[2], "vvv4"); } @Test From f6e5b0fe495c81889e1118dcd7591d40d363d0e5 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 24 Apr 2020 13:05:56 -0700 Subject: [PATCH 15/25] add JSON apis and notification support --- .../java/com/optimizely/ab/Optimizely.java | 141 ++++++++++++++++++ .../optimizely/ab/config/FeatureVariable.java | 1 + .../ab/notification/DecisionNotification.java | 39 +++-- .../ab/notification/NotificationCenter.java | 3 +- 4 files changed, 172 insertions(+), 12 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java index 39690a82e..97c1f61d5 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -31,6 +31,8 @@ import com.optimizely.ab.optimizelyconfig.OptimizelyConfig; import com.optimizely.ab.optimizelyconfig.OptimizelyConfigManager; import com.optimizely.ab.optimizelyconfig.OptimizelyConfigService; +import com.optimizely.ab.optimizelyjson.OptimizelyJSON; +import com.sun.org.apache.xpath.internal.operations.Variable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -601,6 +603,46 @@ public String getFeatureVariableString(@Nonnull String featureKey, FeatureVariable.STRING_TYPE); } + /** + * Get the JSON value of the specified variable in the feature. + * + * @param featureKey The unique key of the feature. + * @param variableKey The unique key of the variable. + * @param userId The ID of the user. + * @return An OptimizelyJSON instance for the JSON variable value. + * Null if the feature or variable could not be found. + */ + @Nullable + public OptimizelyJSON getFeatureVariableJSON(@Nonnull String featureKey, + @Nonnull String variableKey, + @Nonnull String userId) { + return getFeatureVariableJSON(featureKey, variableKey, userId, Collections.emptyMap()); + } + + /** + * Get the JSON value of the specified variable in the feature. + * + * @param featureKey The unique key of the feature. + * @param variableKey The unique key of the variable. + * @param userId The ID of the user. + * @param attributes The user's attributes. + * @return An OptimizelyJSON instance for the JSON variable value. + * Null if the feature or variable could not be found. + */ + @Nullable + public OptimizelyJSON getFeatureVariableJSON(@Nonnull String featureKey, + @Nonnull String variableKey, + @Nonnull String userId, + @Nonnull Map attributes) { + + return getFeatureVariableValueForType( + featureKey, + variableKey, + userId, + attributes, + FeatureVariable.JSON_TYPE); + } + @VisibleForTesting T getFeatureVariableValueForType(@Nonnull String featureKey, @Nonnull String variableKey, @@ -714,6 +756,8 @@ Object convertStringToType(String variableValue, String type) { "\" as Integer. " + exception.toString()); } break; + case FeatureVariable.JSON_TYPE: + return new OptimizelyJSON(variableValue); default: return variableValue; } @@ -722,6 +766,103 @@ Object convertStringToType(String variableValue, String type) { return null; } + /** + * Get the values of all variables in the feature. + * + * @param featureKey The unique key of the feature. + * @param userId The ID of the user. + * @return An OptimizelyJSON instance for all variable values. + * Null if the feature could not be found. + */ + @Nullable + public OptimizelyJSON getAllFeatureVariables(@Nonnull String featureKey, + @Nonnull String userId) { + return getAllFeatureVariables(featureKey, userId, Collections.emptyMap()); + } + + /** + * Get the values of all variables in the feature. + * + * @param featureKey The unique key of the feature. + * @param userId The ID of the user. + * @param attributes The user's attributes. + * @return An OptimizelyJSON instance for all variable values. + * Null if the feature could not be found. + */ + @Nullable + public OptimizelyJSON getAllFeatureVariables(@Nonnull String featureKey, + @Nonnull String userId, + @Nonnull Map attributes) { + + if (featureKey == null) { + logger.warn("The featureKey parameter must be nonnull."); + return null; + } else if (userId == null) { + logger.warn("The userId parameter must be nonnull."); + return null; + } + + ProjectConfig projectConfig = getProjectConfig(); + if (projectConfig == null) { + logger.error("Optimizely instance is not valid, failing getAllFeatureVariableValues call. type"); + return null; + } + + FeatureFlag featureFlag = projectConfig.getFeatureKeyMapping().get(featureKey); + if (featureFlag == null) { + logger.info("No feature flag was found for key \"{}\".", featureKey); + return null; + } + + Map copiedAttributes = copyAttributes(attributes); + FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, userId, copiedAttributes, projectConfig); + Boolean featureEnabled = false; + Variation variation = featureDecision.variation; + + if (variation != null) { + if (!variation.getFeatureEnabled()) { + logger.info("Feature \"{}\" for variation \"{}\" was not enabled. " + + "The default value is being returned.", featureKey, featureDecision.variation.getKey()); + } + + featureEnabled = variation.getFeatureEnabled(); + } else { + logger.info("User \"{}\" was not bucketed into any variation for feature flag \"{}\". " + + "The default values are being returned.", userId, featureKey); + } + + Map valuesMap = new HashMap(); + for (FeatureVariable variable : featureFlag.getVariables()) { + String value = variable.getDefaultValue(); + if (featureEnabled && variation != null) { + FeatureVariableUsageInstance instance = variation.getVariableIdToFeatureVariableUsageInstanceMap().get(variable.getId()); + if (instance != null) { + value = instance.getValue(); + } + } + + Object convertedValue = convertStringToType(value, variable.getType()); + if (convertedValue instanceof OptimizelyJSON) { + convertedValue = ((OptimizelyJSON) convertedValue).toMap(); + } + + valuesMap.put(variable.getKey(), value); + } + + DecisionNotification decisionNotification = DecisionNotification.newFeatureVariableDecisionNotificationBuilder() + .withUserId(userId) + .withAttributes(copiedAttributes) + .withFeatureKey(featureKey) + .withFeatureEnabled(featureEnabled) + .withVariableValues(valuesMap) + .withFeatureDecision(featureDecision) + .build(); + + notificationCenter.send(decisionNotification); + + return new OptimizelyJSON(valuesMap); + } + /** * Get the list of features that are enabled for the user. * TODO revisit this method. Calling this as-is can dramatically increase visitor impression counts. diff --git a/core-api/src/main/java/com/optimizely/ab/config/FeatureVariable.java b/core-api/src/main/java/com/optimizely/ab/config/FeatureVariable.java index 7d8657970..528e44f10 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/FeatureVariable.java +++ b/core-api/src/main/java/com/optimizely/ab/config/FeatureVariable.java @@ -65,6 +65,7 @@ public static VariableStatus fromString(String variableStatusString) { public static final String INTEGER_TYPE = "integer"; public static final String DOUBLE_TYPE = "double"; public static final String BOOLEAN_TYPE = "boolean"; + public static final String JSON_TYPE = "json"; private final String id; private final String key; diff --git a/core-api/src/main/java/com/optimizely/ab/notification/DecisionNotification.java b/core-api/src/main/java/com/optimizely/ab/notification/DecisionNotification.java index 741af7bd2..15d916c75 100644 --- a/core-api/src/main/java/com/optimizely/ab/notification/DecisionNotification.java +++ b/core-api/src/main/java/com/optimizely/ab/notification/DecisionNotification.java @@ -239,13 +239,16 @@ public static class FeatureVariableDecisionNotificationBuilder { public static final String VARIABLE_KEY = "variableKey"; public static final String VARIABLE_TYPE = "variableType"; public static final String VARIABLE_VALUE = "variableValue"; + public static final String VARIABLE_VALUES = "variableValues"; + private NotificationCenter.DecisionNotificationType notificationType; private String featureKey; private Boolean featureEnabled; private FeatureDecision featureDecision; private String variableKey; private String variableType; private Object variableValue; + private Object variableValues; private String userId; private Map attributes; private Map decisionInfo; @@ -293,6 +296,11 @@ public FeatureVariableDecisionNotificationBuilder withVariableValue(Object varia return this; } + public FeatureVariableDecisionNotificationBuilder withVariableValues(Object variableValues) { + this.variableValues = variableValues; + return this; + } + public DecisionNotification build() { if (featureKey == null) { throw new OptimizelyRuntimeException("featureKey not set"); @@ -302,20 +310,29 @@ public DecisionNotification build() { throw new OptimizelyRuntimeException("featureEnabled not set"); } - if (variableKey == null) { - throw new OptimizelyRuntimeException("variableKey not set"); - } - - if (variableType == null) { - throw new OptimizelyRuntimeException("variableType not set"); - } + notificationType = (variableValues != null) ? NotificationCenter.DecisionNotificationType.ALL_FEATURE_VARIABLES : + NotificationCenter.DecisionNotificationType.FEATURE_VARIABLE; decisionInfo = new HashMap<>(); decisionInfo.put(FEATURE_KEY, featureKey); decisionInfo.put(FEATURE_ENABLED, featureEnabled); - decisionInfo.put(VARIABLE_KEY, variableKey); - decisionInfo.put(VARIABLE_TYPE, variableType.toString()); - decisionInfo.put(VARIABLE_VALUE, variableValue); + + if (notificationType == NotificationCenter.DecisionNotificationType.FEATURE_VARIABLE) { + if (variableKey == null) { + throw new OptimizelyRuntimeException("variableKey not set"); + } + + if (variableType == null) { + throw new OptimizelyRuntimeException("variableType not set"); + } + + decisionInfo.put(VARIABLE_KEY, variableKey); + decisionInfo.put(VARIABLE_TYPE, variableType.toString()); + decisionInfo.put(VARIABLE_VALUE, variableValue); + } else if (notificationType == NotificationCenter.DecisionNotificationType.ALL_FEATURE_VARIABLES) { + decisionInfo.put(VARIABLE_VALUES, variableValues); + } + SourceInfo sourceInfo = new RolloutSourceInfo(); if (featureDecision != null && FeatureDecision.DecisionSource.FEATURE_TEST.equals(featureDecision.decisionSource)) { @@ -327,7 +344,7 @@ public DecisionNotification build() { decisionInfo.put(SOURCE_INFO, sourceInfo.get()); return new DecisionNotification( - NotificationCenter.DecisionNotificationType.FEATURE_VARIABLE.toString(), + notificationType.toString(), userId, attributes, decisionInfo); diff --git a/core-api/src/main/java/com/optimizely/ab/notification/NotificationCenter.java b/core-api/src/main/java/com/optimizely/ab/notification/NotificationCenter.java index 8b5fc933e..4b0b3e406 100644 --- a/core-api/src/main/java/com/optimizely/ab/notification/NotificationCenter.java +++ b/core-api/src/main/java/com/optimizely/ab/notification/NotificationCenter.java @@ -54,7 +54,8 @@ public enum DecisionNotificationType { AB_TEST("ab-test"), FEATURE("feature"), FEATURE_TEST("feature-test"), - FEATURE_VARIABLE("feature-variable"); + FEATURE_VARIABLE("feature-variable"), + ALL_FEATURE_VARIABLES("all-feature-variables"); private final String key; From 5bc87aabba9236a7d5285180182a2c4c18b8b7ec Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 1 May 2020 14:45:34 -0700 Subject: [PATCH 16/25] add json-api tests --- .../ab/notification/DecisionNotification.java | 12 +- .../com/optimizely/ab/OptimizelyTest.java | 228 ++++++++++++++++++ 2 files changed, 234 insertions(+), 6 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/notification/DecisionNotification.java b/core-api/src/main/java/com/optimizely/ab/notification/DecisionNotification.java index 15d916c75..0a7ea6e3c 100644 --- a/core-api/src/main/java/com/optimizely/ab/notification/DecisionNotification.java +++ b/core-api/src/main/java/com/optimizely/ab/notification/DecisionNotification.java @@ -19,7 +19,6 @@ import com.optimizely.ab.OptimizelyRuntimeException; import com.optimizely.ab.bucketing.FeatureDecision; -import com.optimizely.ab.config.FeatureVariable; import com.optimizely.ab.config.Variation; import javax.annotation.Nonnull; @@ -310,14 +309,17 @@ public DecisionNotification build() { throw new OptimizelyRuntimeException("featureEnabled not set"); } - notificationType = (variableValues != null) ? NotificationCenter.DecisionNotificationType.ALL_FEATURE_VARIABLES : - NotificationCenter.DecisionNotificationType.FEATURE_VARIABLE; decisionInfo = new HashMap<>(); decisionInfo.put(FEATURE_KEY, featureKey); decisionInfo.put(FEATURE_ENABLED, featureEnabled); - if (notificationType == NotificationCenter.DecisionNotificationType.FEATURE_VARIABLE) { + if (variableValues != null) { + notificationType = NotificationCenter.DecisionNotificationType.ALL_FEATURE_VARIABLES; + decisionInfo.put(VARIABLE_VALUES, variableValues); + } else { + notificationType = NotificationCenter.DecisionNotificationType.FEATURE_VARIABLE; + if (variableKey == null) { throw new OptimizelyRuntimeException("variableKey not set"); } @@ -329,8 +331,6 @@ public DecisionNotification build() { decisionInfo.put(VARIABLE_KEY, variableKey); decisionInfo.put(VARIABLE_TYPE, variableType.toString()); decisionInfo.put(VARIABLE_VALUE, variableValue); - } else if (notificationType == NotificationCenter.DecisionNotificationType.ALL_FEATURE_VARIABLES) { - decisionInfo.put(VARIABLE_VALUES, variableValues); } SourceInfo sourceInfo = new RolloutSourceInfo(); diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java index c8c5ddb53..446aaf64b 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java @@ -2215,6 +2215,96 @@ public void getFeatureVariableDoubleWithListenerUserInExperimentFeatureOn() thro assertTrue(optimizely.notificationCenter.removeNotificationListener(notificationId)); } + /** + * Verify that the {@link Optimizely#getAllFeatureVariables(String, String, Map)} + * notification listener of getAllFeatureVariables is called when feature is in experiment and feature is true + */ + @Test + public void getAllFeatureVariablesWithListenerUserInExperimentFeatureOn() throws Exception { + assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())); + isListenerCalled = false; + final String validFeatureKey = FEATURE_MULTI_VARIATE_FEATURE_KEY; + String expectedString = "{\"first_letter\":\"F\",\"rest_of_name\":\"red\",\"json_patched\":{\"k1\":\"v1\",\"k2\":3.5,\"k3\":true,\"k4\":{\"kk1\":\"vv1\",\"kk2\":false}}}"; + + Optimizely optimizely = optimizelyBuilder.build(); + + final Map testUserAttributes = new HashMap<>(); + testUserAttributes.put(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE); + + final Map testSourceInfo = new HashMap<>(); + testSourceInfo.put(EXPERIMENT_KEY, "multivariate_experiment"); + testSourceInfo.put(VARIATION_KEY, "Fred"); + + final Map testDecisionInfoMap = new HashMap<>(); + testDecisionInfoMap.put(FEATURE_KEY, validFeatureKey); + testDecisionInfoMap.put(FEATURE_ENABLED, true); + testDecisionInfoMap.put(VARIABLE_VALUES, expectedString); + testDecisionInfoMap.put(SOURCE, FeatureDecision.DecisionSource.FEATURE_TEST.toString()); + testDecisionInfoMap.put(SOURCE_INFO, testSourceInfo); + + int notificationId = optimizely.addDecisionNotificationHandler( + getDecisionListener(NotificationCenter.DecisionNotificationType.FEATURE_VARIABLES.toString(), + testUserId, + testUserAttributes, + testDecisionInfoMap)); + + assertEquals(optimizely.getAllFeatureVariables( + validFeatureKey, + testUserId, + Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE)).toString(), + expectedString); + + // Verify that listener being called + assertTrue(isListenerCalled); + assertTrue(optimizely.notificationCenter.removeNotificationListener(notificationId)); + } + + /** + * Verify that the {@link Optimizely#getAllFeatureVariables(String, String, Map)} + * notification listener of getAllFeatureVariables is called when feature is in experiment and feature enabled is false + * than default value will get returned and passing null attribute will send empty map instead of null + */ + @SuppressFBWarnings("NP_NONNULL_PARAM_VIOLATION") + @Test + public void getAllFeatureVariablesWithListenerUserInExperimentFeatureOff() { + assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())); + isListenerCalled = false; + final String validFeatureKey = FEATURE_MULTI_VARIATE_FEATURE_KEY; + String expectedString = "{\"first_letter\":\"H\",\"rest_of_name\":\"arry\",\"json_patched\":{\"k1\":\"v1\",\"k2\":3.5,\"k3\":true,\"k4\":{\"kk1\":\"vv1\",\"kk2\":false}}}"; + String userID = "Gred"; + + Optimizely optimizely = optimizelyBuilder.build(); + + final Map testUserAttributes = new HashMap<>(); + + final Map testSourceInfo = new HashMap<>(); + testSourceInfo.put(EXPERIMENT_KEY, "multivariate_experiment"); + testSourceInfo.put(VARIATION_KEY, "Gred"); + + final Map testDecisionInfoMap = new HashMap<>(); + testDecisionInfoMap.put(FEATURE_KEY, validFeatureKey); + testDecisionInfoMap.put(FEATURE_ENABLED, false); + testDecisionInfoMap.put(VARIABLE_VALUES, expectedString); + testDecisionInfoMap.put(SOURCE, FeatureDecision.DecisionSource.FEATURE_TEST.toString()); + testDecisionInfoMap.put(SOURCE_INFO, testSourceInfo); + + int notificationId = optimizely.addDecisionNotificationHandler( + getDecisionListener(NotificationCenter.DecisionNotificationType.FEATURE_VARIABLES.toString(), + userID, + testUserAttributes, + testDecisionInfoMap)); + + assertEquals(optimizely.getAllFeatureVariables( + validFeatureKey, + userID, + null).toString(), + expectedString); + + // Verify that listener being called + assertTrue(isListenerCalled); + assertTrue(optimizely.notificationCenter.removeNotificationListener(notificationId)); + } + /** * Verify that the {@link Optimizely#activate(String, String, Map)} call * correctly builds an endpoint url and request params @@ -4046,6 +4136,144 @@ public void getFeatureVariableIntegerCatchesExceptionFromParsing() throws Except assertNull(spyOptimizely.getFeatureVariableInteger(featureKey, variableKey, genericUserId)); } + /** + * Verify that the {@link Optimizely#getFeatureVariableJSON(String, String, String, Map)} + * is called when feature is in experiment and feature enabled is true + * returns variable value + */ + @Test + public void getFeatureVariableJSONUserInExperimentFeatureOn() throws Exception { + assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())); + + final String validFeatureKey = FEATURE_MULTI_VARIATE_FEATURE_KEY; + String validVariableKey = VARIABLE_JSON_PATCHED_TYPE_KEY; + String expectedString = "{\"k1\":\"v1\",\"k2\":3.5,\"k3\":true,\"k4\":{\"kk1\":\"vv1\",\"kk2\":false}}"; + + Optimizely optimizely = optimizelyBuilder.build(); + + OptimizelyJSON json = optimizely.getFeatureVariableJSON( + validFeatureKey, + validVariableKey, + testUserId, + Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE)); + + assertEquals(json.toString(), expectedString); + + assertEquals(json.toMap().get("k1"), "v1"); + assertEquals(json.toMap().get("k2"), 3.5); + assertEquals(json.toMap().get("k3"), true); + assertEquals(json.toMap().get("k4").get("kk1"), "vv11"); + assertEquals(json.toMap().get("k4").get("kk2"), false); + + assertEquals(json.getValue("k1", String.class), "v1"); + assertEquals(json.getValue("k1.k4", Boolean.class), "v1"); + } + + /** + * Verify that the {@link Optimizely#getFeatureVariableJSON(String, String, String, Map)} + * is called when feature is in experiment and feature enabled is false + * than default value will gets returned + */ + @SuppressFBWarnings("NP_NONNULL_PARAM_VIOLATION") + @Test + public void getFeatureVariableJSONUserInExperimentFeatureOff() { + assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())); + + final String validFeatureKey = FEATURE_MULTI_VARIATE_FEATURE_KEY; + String validVariableKey = VARIABLE_JSON_PATCHED_TYPE_KEY; + String expectedString = "{\"k1\":\"v1\",\"k2\":3.5,\"k3\":true,\"k4\":{\"kk1\":\"vv1\",\"kk2\":false}}"; + String userID = "Gred"; + + Optimizely optimizely = optimizelyBuilder.build(); + + OptimizelyJSON json = optimizely.getFeatureVariableJSON( + validFeatureKey, + validVariableKey, + userID, + null); + + assertEquals(json.toString(), expectedString); + + assertEquals(json.toMap().get("k1"), "v1"); + assertEquals(json.toMap().get("k2"), 3.5); + assertEquals(json.toMap().get("k3"), true); + assertEquals(json.toMap().get("k4").get("kk1"), "vv11"); + assertEquals(json.toMap().get("k4").get("kk2"), false); + + assertEquals(json.getValue("k1", String.class), "v1"); + assertEquals(json.getValue("k1.k4", Boolean.class), "v1"); + } + + /** + * Verify that the {@link Optimizely#getFeatureVariableJSON(String, String, String, Map)} + * is called when feature is in experiment and feature enabled is true + * returns variable value + */ + @Test + public void getAllFeatureVariablesUserInExperimentFeatureOn() throws Exception { + assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())); + + final String validFeatureKey = FEATURE_MULTI_VARIATE_FEATURE_KEY; + String expectedString = "{\"first_letter\":\"F\",\"rest_of_name\":\"red\",\"json_patched\":{\"k1\":\"v1\",\"k2\":3.5,\"k3\":true,\"k4\":{\"kk1\":\"vv1\",\"kk2\":false}}}"; + + Optimizely optimizely = optimizelyBuilder.build(); + + OptimizelyJSON json = optimizely.getAllFeatureVariables( + validFeatureKey, + testUserId, + Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE)); + + assertEquals(json.toString(), expectedString); + + assertEquals(json.toMap().get("first_letter"), "F"); + assertEquals(json.toMap().get("rest_of_name"), "red"); + assertEquals(json.toMap().get("json_patched").get("k1"), "v1"); + assertEquals(json.toMap().get("json_patched").get("k2"), 3.5); + assertEquals(json.toMap().get("json_patched").get("k3"), true); + assertEquals(json.toMap().get("json_patched").get("k4").get("kk1"), "vv11"); + assertEquals(json.toMap().get("json_patched").get("k4").get("kk2"), false); + + assertEquals(json.getValue("first_letter", String.class), "F"); + assertEquals(json.getValue("json_patched.k1", String.class), "v1"); + assertEquals(json.getValue("json_patched.k1.k4", Boolean.class), "v1"); + } + + /** + * Verify that the {@link Optimizely#getFeatureVariableJSON(String, String, String, Map)} + * is called when feature is in experiment and feature enabled is false + * than default value will gets returned + */ + @SuppressFBWarnings("NP_NONNULL_PARAM_VIOLATION") + @Test + public void getAllFeatureVariablesUserInExperimentFeatureOff() { + assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())); + + final String validFeatureKey = FEATURE_MULTI_VARIATE_FEATURE_KEY; + String expectedString = "{\"first_letter\":\"H\",\"rest_of_name\":\"arry\",\"json_patched\":{\"k1\":\"v1\",\"k2\":3.5,\"k3\":true,\"k4\":{\"kk1\":\"vv1\",\"kk2\":false}}}"; + String userID = "Gred"; + + Optimizely optimizely = optimizelyBuilder.build(); + + OptimizelyJSON json = optimizely.getAllFeatureVariables( + validFeatureKey, + userID, + null); + + assertEquals(json.toString(), expectedString); + + assertEquals(json.toMap().get("first_letter"), "H"); + assertEquals(json.toMap().get("rest_of_name"), "arry"); + assertEquals(json.toMap().get("json_patched").get("k1"), "v1"); + assertEquals(json.toMap().get("json_patched").get("k2"), 3.5); + assertEquals(json.toMap().get("json_patched").get("k3"), true); + assertEquals(json.toMap().get("json_patched").get("k4").get("kk1"), "vv11"); + assertEquals(json.toMap().get("json_patched").get("k4").get("kk2"), false); + + assertEquals(json.getValue("first_letter", String.class), "H"); + assertEquals(json.getValue("json_patched.k1", String.class), "v1"); + assertEquals(json.getValue("json_patched.k1.k4", Boolean.class), "v1"); + } + /** * Verify that {@link Optimizely#getVariation(String, String)} returns a variation when given an experiment * with no audiences and no user attributes and verify that listener is getting called. From effd75d096d056e983847e1f15294d7a4e5be65d Mon Sep 17 00:00:00 2001 From: Jae Kim <45045038+jaeopt@users.noreply.github.com> Date: Fri, 1 May 2020 14:30:56 -0700 Subject: [PATCH 17/25] feat: add "json" type to FeatureVariable (#372) --- .../optimizely/ab/config/FeatureVariable.java | 11 +++- .../ab/config/parser/JsonConfigParser.java | 6 +- .../config/parser/JsonSimpleConfigParser.java | 3 +- .../ab/config/ValidProjectConfigV4.java | 66 ++++++++++++++++--- .../config/parser/GsonConfigParserTest.java | 42 ++++++++++++ .../parser/JacksonConfigParserTest.java | 41 ++++++++++++ .../config/parser/JsonConfigParserTest.java | 42 ++++++++++++ .../parser/JsonSimpleConfigParserTest.java | 44 +++++++++++++ .../OptimizelyConfigServiceTest.java | 6 +- .../config/valid-project-config-v4.json | 31 ++++++++- 10 files changed, 276 insertions(+), 16 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/config/FeatureVariable.java b/core-api/src/main/java/com/optimizely/ab/config/FeatureVariable.java index 528e44f10..92d082a08 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/FeatureVariable.java +++ b/core-api/src/main/java/com/optimizely/ab/config/FeatureVariable.java @@ -72,6 +72,8 @@ public static VariableStatus fromString(String variableStatusString) { private final String defaultValue; private final String type; @Nullable + private final String subType; // this is for backward-compatibility (json type) + @Nullable private final VariableStatus status; @JsonCreator @@ -79,12 +81,14 @@ public FeatureVariable(@JsonProperty("id") String id, @JsonProperty("key") String key, @JsonProperty("defaultValue") String defaultValue, @JsonProperty("status") VariableStatus status, - @JsonProperty("type") String type) { + @JsonProperty("type") String type, + @JsonProperty("subType") String subType) { this.id = id; this.key = key; this.defaultValue = defaultValue; this.status = status; this.type = type; + this.subType = subType; } @Nullable @@ -105,6 +109,7 @@ public String getDefaultValue() { } public String getType() { + if (type.equals(STRING_TYPE) && subType != null && subType.equals(JSON_TYPE)) return JSON_TYPE; return type; } @@ -115,6 +120,7 @@ public String toString() { ", key='" + key + '\'' + ", defaultValue='" + defaultValue + '\'' + ", type=" + type + + ", subType=" + subType + ", status=" + status + '}'; } @@ -139,7 +145,8 @@ public int hashCode() { result = 31 * result + key.hashCode(); result = 31 * result + defaultValue.hashCode(); result = 31 * result + type.hashCode(); - result = 31 * result + status.hashCode(); + result = 31 * result + (subType != null ? subType.hashCode() : 0); + result = 31 * result + (status != null ? status.hashCode() : 0); return result; } } 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 3ace07556..de1ea2920 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 @@ -340,12 +340,16 @@ private List parseFeatureVariables(JSONArray featureVariablesJs String key = FeatureVariableObject.getString("key"); String defaultValue = FeatureVariableObject.getString("defaultValue"); String type = FeatureVariableObject.getString("type"); + String subType = null; + if (FeatureVariableObject.has("subType")) { + subType = FeatureVariableObject.getString("subType"); + } FeatureVariable.VariableStatus status = null; if (FeatureVariableObject.has("status")) { status = FeatureVariable.VariableStatus.fromString(FeatureVariableObject.getString("status")); } - featureVariables.add(new FeatureVariable(id, key, defaultValue, status, type)); + featureVariables.add(new FeatureVariable(id, key, defaultValue, status, type, subType)); } return featureVariables; 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 15d3c63f5..b9cb17f7a 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 @@ -336,9 +336,10 @@ private List parseFeatureVariables(JSONArray featureVariablesJs String key = (String) featureVariableObject.get("key"); String defaultValue = (String) featureVariableObject.get("defaultValue"); String type = (String) featureVariableObject.get("type"); + String subType = (String) featureVariableObject.get("subType"); VariableStatus status = VariableStatus.fromString((String) featureVariableObject.get("status")); - featureVariables.add(new FeatureVariable(id, key, defaultValue, status, type)); + featureVariables.add(new FeatureVariable(id, key, defaultValue, status, type, subType)); } return featureVariables; 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 dd79294b8..1c71667f8 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 @@ -247,7 +247,8 @@ public class ValidProjectConfigV4 { VARIABLE_DOUBLE_VARIABLE_KEY, VARIABLE_DOUBLE_DEFAULT_VALUE, null, - FeatureVariable.DOUBLE_TYPE + FeatureVariable.DOUBLE_TYPE, + null ); private static final String FEATURE_SINGLE_VARIABLE_INTEGER_ID = "3281420120"; public static final String FEATURE_SINGLE_VARIABLE_INTEGER_KEY = "integer_single_variable_feature"; @@ -259,7 +260,8 @@ public class ValidProjectConfigV4 { VARIABLE_INTEGER_VARIABLE_KEY, VARIABLE_INTEGER_DEFAULT_VALUE, null, - FeatureVariable.INTEGER_TYPE + FeatureVariable.INTEGER_TYPE, + null ); private static final String FEATURE_SINGLE_VARIABLE_BOOLEAN_ID = "2591051011"; public static final String FEATURE_SINGLE_VARIABLE_BOOLEAN_KEY = "boolean_single_variable_feature"; @@ -271,7 +273,8 @@ public class ValidProjectConfigV4 { VARIABLE_BOOLEAN_VARIABLE_KEY, VARIABLE_BOOLEAN_VARIABLE_DEFAULT_VALUE, null, - FeatureVariable.BOOLEAN_TYPE + FeatureVariable.BOOLEAN_TYPE, + null ); private static final FeatureFlag FEATURE_FLAG_SINGLE_VARIABLE_BOOLEAN = new FeatureFlag( FEATURE_SINGLE_VARIABLE_BOOLEAN_ID, @@ -292,7 +295,8 @@ public class ValidProjectConfigV4 { VARIABLE_STRING_VARIABLE_KEY, VARIABLE_STRING_VARIABLE_DEFAULT_VALUE, null, - FeatureVariable.STRING_TYPE + FeatureVariable.STRING_TYPE, + null ); private static final String ROLLOUT_1_ID = "1058508303"; private static final String ROLLOUT_1_EVERYONE_ELSE_EXPERIMENT_ID = "1785077004"; @@ -388,7 +392,8 @@ public class ValidProjectConfigV4 { VARIABLE_FIRST_LETTER_KEY, VARIABLE_FIRST_LETTER_DEFAULT_VALUE, null, - FeatureVariable.STRING_TYPE + FeatureVariable.STRING_TYPE, + null ); private static final String VARIABLE_REST_OF_NAME_ID = "4052219963"; private static final String VARIABLE_REST_OF_NAME_KEY = "rest_of_name"; @@ -398,9 +403,32 @@ public class ValidProjectConfigV4 { VARIABLE_REST_OF_NAME_KEY, VARIABLE_REST_OF_NAME_DEFAULT_VALUE, null, - FeatureVariable.STRING_TYPE + FeatureVariable.STRING_TYPE, + null + ); + private static final String VARIABLE_JSON_PATCHED_TYPE_ID = "4111661000"; + private static final String VARIABLE_JSON_PATCHED_TYPE_KEY = "json_patched"; + private static final String VARIABLE_JSON_PATCHED_TYPE_DEFAULT_VALUE = "{\"k1\":\"v1\",\"k2\":3.5,\"k3\":true,\"k4\":{\"kk1\":\"vv1\",\"kk2\":false}}"; + private static final FeatureVariable VARIABLE_JSON_PATCHED_TYPE_VARIABLE = new FeatureVariable( + VARIABLE_JSON_PATCHED_TYPE_ID, + VARIABLE_JSON_PATCHED_TYPE_KEY, + VARIABLE_JSON_PATCHED_TYPE_DEFAULT_VALUE, + null, + FeatureVariable.STRING_TYPE, + FeatureVariable.JSON_TYPE + ); + private static final String VARIABLE_JSON_NATIVE_TYPE_ID = "4111661001"; + private static final String VARIABLE_JSON_NATIVE_TYPE_KEY = "json_native"; + private static final String VARIABLE_JSON_NATIVE_TYPE_DEFAULT_VALUE = "{\"k1\":\"v1\",\"k2\":3.5,\"k3\":true,\"k4\":{\"kk1\":\"vv1\",\"kk2\":false}}"; + private static final FeatureVariable VARIABLE_JSON_NATIVE_TYPE_VARIABLE = new FeatureVariable( + VARIABLE_JSON_NATIVE_TYPE_ID, + VARIABLE_JSON_NATIVE_TYPE_KEY, + VARIABLE_JSON_NATIVE_TYPE_DEFAULT_VALUE, + null, + FeatureVariable.JSON_TYPE, + null ); - private static final String VARIABLE_FUTURE_TYPE_ID = "4111661234"; + private static final String VARIABLE_FUTURE_TYPE_ID = "4111661002"; private static final String VARIABLE_FUTURE_TYPE_KEY = "future_variable"; private static final String VARIABLE_FUTURE_TYPE_DEFAULT_VALUE = "future_value"; private static final FeatureVariable VARIABLE_FUTURE_TYPE_VARIABLE = new FeatureVariable( @@ -408,7 +436,8 @@ public class ValidProjectConfigV4 { VARIABLE_FUTURE_TYPE_KEY, VARIABLE_FUTURE_TYPE_DEFAULT_VALUE, null, - "future_type" + "future_type", + null ); private static final String FEATURE_MUTEX_GROUP_FEATURE_ID = "3263342226"; public static final String FEATURE_MUTEX_GROUP_FEATURE_KEY = "mutex_group_feature"; @@ -420,7 +449,8 @@ public class ValidProjectConfigV4 { VARIABLE_CORRELATING_VARIATION_NAME_KEY, VARIABLE_CORRELATING_VARIATION_NAME_DEFAULT_VALUE, null, - FeatureVariable.STRING_TYPE + FeatureVariable.STRING_TYPE, + null ); // group IDs @@ -733,6 +763,10 @@ public class ValidProjectConfigV4 { new FeatureVariableUsageInstance( VARIABLE_REST_OF_NAME_ID, "red" + ), + new FeatureVariableUsageInstance( + VARIABLE_JSON_PATCHED_TYPE_ID, + "{\"k1\":\"s1\",\"k2\":103.5,\"k3\":false,\"k4\":{\"kk1\":\"ss1\",\"kk2\":true}}" ) ) ); @@ -750,6 +784,10 @@ public class ValidProjectConfigV4 { new FeatureVariableUsageInstance( VARIABLE_REST_OF_NAME_ID, "eorge" + ), + new FeatureVariableUsageInstance( + VARIABLE_JSON_PATCHED_TYPE_ID, + "{\"k1\":\"s2\",\"k2\":203.5,\"k3\":true,\"k4\":{\"kk1\":\"ss2\",\"kk2\":true}}" ) ) ); @@ -768,6 +806,10 @@ public class ValidProjectConfigV4 { new FeatureVariableUsageInstance( VARIABLE_REST_OF_NAME_ID, "red" + ), + new FeatureVariableUsageInstance( + VARIABLE_JSON_PATCHED_TYPE_ID, + "{\"k1\":\"s3\",\"k2\":303.5,\"k3\":true,\"k4\":{\"kk1\":\"ss3\",\"kk2\":false}}" ) ) ); @@ -785,6 +827,10 @@ public class ValidProjectConfigV4 { new FeatureVariableUsageInstance( VARIABLE_REST_OF_NAME_ID, "eorge" + ), + new FeatureVariableUsageInstance( + VARIABLE_JSON_PATCHED_TYPE_ID, + "{\"k1\":\"s4\",\"k2\":403.5,\"k3\":false,\"k4\":{\"kk1\":\"ss4\",\"kk2\":true}}" ) ) ); @@ -1262,6 +1308,8 @@ public class ValidProjectConfigV4 { DatafileProjectConfigTestUtils.createListOfObjects( VARIABLE_FIRST_LETTER_VARIABLE, VARIABLE_REST_OF_NAME_VARIABLE, + VARIABLE_JSON_PATCHED_TYPE_VARIABLE, + VARIABLE_JSON_NATIVE_TYPE_VARIABLE, VARIABLE_FUTURE_TYPE_VARIABLE ) ); 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 c6d5807bc..de2ac643a 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 @@ -21,6 +21,8 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.reflect.TypeToken; +import com.optimizely.ab.config.FeatureFlag; +import com.optimizely.ab.config.FeatureVariable; import com.optimizely.ab.config.ProjectConfig; import com.optimizely.ab.config.audience.Audience; import com.optimizely.ab.config.audience.Condition; @@ -42,6 +44,7 @@ import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV3; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV4; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.verifyProjectConfig; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; /** @@ -92,6 +95,45 @@ public void parseNullFeatureEnabledProjectConfigV4() throws Exception { } + @Test + public void parseFeatureVariablesWithJsonPatched() throws Exception { + JsonSimpleConfigParser parser = new JsonSimpleConfigParser(); + ProjectConfig actual = parser.parseProjectConfig(validConfigJsonV4()); + + // "string" type + "json" subType + + FeatureFlag featureFlag = actual.getFeatureKeyMapping().get("multi_variate_feature"); + FeatureVariable variable = featureFlag.getVariableKeyToFeatureVariableMap().get("json_patched"); + + assertEquals(variable.getType(), "json"); + } + + @Test + public void parseFeatureVariablesWithJsonNative() throws Exception { + JsonSimpleConfigParser parser = new JsonSimpleConfigParser(); + ProjectConfig actual = parser.parseProjectConfig(validConfigJsonV4()); + + // native "json" type + + FeatureFlag featureFlag = actual.getFeatureKeyMapping().get("multi_variate_feature"); + FeatureVariable variable = featureFlag.getVariableKeyToFeatureVariableMap().get("json_native"); + + assertEquals(variable.getType(), "json"); + } + + @Test + public void parseFeatureVariablesWithFutureType() throws Exception { + JsonSimpleConfigParser parser = new JsonSimpleConfigParser(); + ProjectConfig actual = parser.parseProjectConfig(validConfigJsonV4()); + + // unknown type + + FeatureFlag featureFlag = actual.getFeatureKeyMapping().get("multi_variate_feature"); + FeatureVariable variable = featureFlag.getVariableKeyToFeatureVariableMap().get("future_variable"); + + assertEquals(variable.getType(), "future_type"); + } + @Test public void parseAudience() throws Exception { JsonObject jsonObject = new JsonObject(); diff --git a/core-api/src/test/java/com/optimizely/ab/config/parser/JacksonConfigParserTest.java b/core-api/src/test/java/com/optimizely/ab/config/parser/JacksonConfigParserTest.java index bca3ebb9a..61c44e730 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/parser/JacksonConfigParserTest.java +++ b/core-api/src/test/java/com/optimizely/ab/config/parser/JacksonConfigParserTest.java @@ -18,6 +18,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; +import com.optimizely.ab.config.FeatureFlag; +import com.optimizely.ab.config.FeatureVariable; import com.optimizely.ab.config.ProjectConfig; import com.optimizely.ab.config.audience.Audience; import com.optimizely.ab.config.audience.Condition; @@ -36,6 +38,7 @@ import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV3; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV4; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.verifyProjectConfig; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; /** @@ -86,6 +89,44 @@ public void parseNullFeatureEnabledProjectConfigV4() throws Exception { } + @Test + public void parseFeatureVariablesWithJsonPatched() throws Exception { + JsonSimpleConfigParser parser = new JsonSimpleConfigParser(); + ProjectConfig actual = parser.parseProjectConfig(validConfigJsonV4()); + + // "string" type + "json" subType + + FeatureFlag featureFlag = actual.getFeatureKeyMapping().get("multi_variate_feature"); + FeatureVariable variable = featureFlag.getVariableKeyToFeatureVariableMap().get("json_patched"); + + assertEquals(variable.getType(), "json"); + } + + @Test + public void parseFeatureVariablesWithJsonNative() throws Exception { + JsonSimpleConfigParser parser = new JsonSimpleConfigParser(); + ProjectConfig actual = parser.parseProjectConfig(validConfigJsonV4()); + + // native "json" type + + FeatureFlag featureFlag = actual.getFeatureKeyMapping().get("multi_variate_feature"); + FeatureVariable variable = featureFlag.getVariableKeyToFeatureVariableMap().get("json_native"); + + assertEquals(variable.getType(), "json"); + } + + @Test + public void parseFeatureVariablesWithFutureType() throws Exception { + JsonSimpleConfigParser parser = new JsonSimpleConfigParser(); + ProjectConfig actual = parser.parseProjectConfig(validConfigJsonV4()); + + // unknown type + + FeatureFlag featureFlag = actual.getFeatureKeyMapping().get("multi_variate_feature"); + FeatureVariable variable = featureFlag.getVariableKeyToFeatureVariableMap().get("future_variable"); + + assertEquals(variable.getType(), "future_type"); + } @Test public void parseAudience() throws Exception { diff --git a/core-api/src/test/java/com/optimizely/ab/config/parser/JsonConfigParserTest.java b/core-api/src/test/java/com/optimizely/ab/config/parser/JsonConfigParserTest.java index 9e2b52e8e..27995b88f 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/parser/JsonConfigParserTest.java +++ b/core-api/src/test/java/com/optimizely/ab/config/parser/JsonConfigParserTest.java @@ -16,6 +16,8 @@ */ package com.optimizely.ab.config.parser; +import com.optimizely.ab.config.FeatureFlag; +import com.optimizely.ab.config.FeatureVariable; import com.optimizely.ab.config.ProjectConfig; import com.optimizely.ab.config.audience.AudienceIdCondition; @@ -38,6 +40,7 @@ import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV3; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV4; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.verifyProjectConfig; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; /** @@ -88,6 +91,45 @@ public void parseNullFeatureEnabledProjectConfigV4() throws Exception { } + @Test + public void parseFeatureVariablesWithJsonPatched() throws Exception { + JsonSimpleConfigParser parser = new JsonSimpleConfigParser(); + ProjectConfig actual = parser.parseProjectConfig(validConfigJsonV4()); + + // "string" type + "json" subType + + FeatureFlag featureFlag = actual.getFeatureKeyMapping().get("multi_variate_feature"); + FeatureVariable variable = featureFlag.getVariableKeyToFeatureVariableMap().get("json_patched"); + + assertEquals(variable.getType(), "json"); + } + + @Test + public void parseFeatureVariablesWithJsonNative() throws Exception { + JsonSimpleConfigParser parser = new JsonSimpleConfigParser(); + ProjectConfig actual = parser.parseProjectConfig(validConfigJsonV4()); + + // native "json" type + + FeatureFlag featureFlag = actual.getFeatureKeyMapping().get("multi_variate_feature"); + FeatureVariable variable = featureFlag.getVariableKeyToFeatureVariableMap().get("json_native"); + + assertEquals(variable.getType(), "json"); + } + + @Test + public void parseFeatureVariablesWithFutureType() throws Exception { + JsonSimpleConfigParser parser = new JsonSimpleConfigParser(); + ProjectConfig actual = parser.parseProjectConfig(validConfigJsonV4()); + + // unknown type + + FeatureFlag featureFlag = actual.getFeatureKeyMapping().get("multi_variate_feature"); + FeatureVariable variable = featureFlag.getVariableKeyToFeatureVariableMap().get("future_variable"); + + assertEquals(variable.getType(), "future_type"); + } + @Test public void parseAudience() throws Exception { JSONObject jsonObject = new JSONObject(); diff --git a/core-api/src/test/java/com/optimizely/ab/config/parser/JsonSimpleConfigParserTest.java b/core-api/src/test/java/com/optimizely/ab/config/parser/JsonSimpleConfigParserTest.java index d95b52500..e22192291 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/parser/JsonSimpleConfigParserTest.java +++ b/core-api/src/test/java/com/optimizely/ab/config/parser/JsonSimpleConfigParserTest.java @@ -16,6 +16,8 @@ */ package com.optimizely.ab.config.parser; +import com.optimizely.ab.config.FeatureFlag; +import com.optimizely.ab.config.FeatureVariable; import com.optimizely.ab.config.ProjectConfig; import com.optimizely.ab.config.audience.AudienceIdCondition; @@ -30,6 +32,8 @@ import org.junit.Test; import org.junit.rules.ExpectedException; +import java.util.List; + import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.nullFeatureEnabledConfigJsonV4; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validConfigJsonV2; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validConfigJsonV4; @@ -38,6 +42,7 @@ import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV3; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV4; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.verifyProjectConfig; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; /** @@ -88,6 +93,45 @@ public void parseNullFeatureEnabledProjectConfigV4() throws Exception { } + @Test + public void parseFeatureVariablesWithJsonPatched() throws Exception { + JsonSimpleConfigParser parser = new JsonSimpleConfigParser(); + ProjectConfig actual = parser.parseProjectConfig(validConfigJsonV4()); + + // "string" type + "json" subType + + FeatureFlag featureFlag = actual.getFeatureKeyMapping().get("multi_variate_feature"); + FeatureVariable variable = featureFlag.getVariableKeyToFeatureVariableMap().get("json_patched"); + + assertEquals(variable.getType(), "json"); + } + + @Test + public void parseFeatureVariablesWithJsonNative() throws Exception { + JsonSimpleConfigParser parser = new JsonSimpleConfigParser(); + ProjectConfig actual = parser.parseProjectConfig(validConfigJsonV4()); + + // native "json" type + + FeatureFlag featureFlag = actual.getFeatureKeyMapping().get("multi_variate_feature"); + FeatureVariable variable = featureFlag.getVariableKeyToFeatureVariableMap().get("json_native"); + + assertEquals(variable.getType(), "json"); + } + + @Test + public void parseFeatureVariablesWithFutureType() throws Exception { + JsonSimpleConfigParser parser = new JsonSimpleConfigParser(); + ProjectConfig actual = parser.parseProjectConfig(validConfigJsonV4()); + + // unknown type + + FeatureFlag featureFlag = actual.getFeatureKeyMapping().get("multi_variate_feature"); + FeatureVariable variable = featureFlag.getVariableKeyToFeatureVariableMap().get("future_variable"); + + assertEquals(variable.getType(), "future_type"); + } + @Test public void parseAudience() throws Exception { JSONObject jsonObject = new JSONObject(); diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyConfigServiceTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyConfigServiceTest.java index 44808c93b..94058776b 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyConfigServiceTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyConfigServiceTest.java @@ -287,14 +287,16 @@ private ProjectConfig generateOptimizelyConfig() { "first_letter", "H", FeatureVariable.VariableStatus.ACTIVE, - FeatureVariable.STRING_TYPE + FeatureVariable.STRING_TYPE, + null ), new FeatureVariable( "4052219963", "rest_of_name", "arry", FeatureVariable.VariableStatus.ACTIVE, - FeatureVariable.STRING_TYPE + FeatureVariable.STRING_TYPE, + null ) ) ) 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 293b26cc7..455a5e8bf 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 @@ -271,6 +271,10 @@ { "id": "4052219963", "value": "red" + }, + { + "id": "4111661000", + "value": "{\"k1\":\"s1\",\"k2\":103.5,\"k3\":false,\"k4\":{\"kk1\":\"ss1\",\"kk2\":true}}" } ] }, @@ -286,6 +290,10 @@ { "id": "4052219963", "value": "eorge" + }, + { + "id": "4111661000", + "value": "{\"k1\":\"s2\",\"k2\":203.5,\"k3\":true,\"k4\":{\"kk1\":\"ss2\",\"kk2\":true}}" } ] }, @@ -301,6 +309,10 @@ { "id": "4052219963", "value": "red" + }, + { + "id": "4111661000", + "value": "{\"k1\":\"s3\",\"k2\":303.5,\"k3\":true,\"k4\":{\"kk1\":\"ss3\",\"kk2\":false}}" } ] }, @@ -316,6 +328,10 @@ { "id": "4052219963", "value": "eorge" + }, + { + "id": "4111661000", + "value": "{\"k1\":\"s4\",\"k2\":403.5,\"k3\":false,\"k4\":{\"kk1\":\"ss4\",\"kk2\":true}}" } ] } @@ -695,7 +711,20 @@ "defaultValue": "arry" }, { - "id": "4111661234", + "id": "4111661000", + "key": "json_patched", + "type": "string", + "subType": "json", + "defaultValue": "{\"k1\":\"v1\",\"k2\":3.5,\"k3\":true,\"k4\":{\"kk1\":\"vv1\",\"kk2\":false}}" + }, + { + "id": "4111661001", + "key": "json_native", + "type": "json", + "defaultValue": "{\"k1\":\"v1\",\"k2\":3.5,\"k3\":true,\"k4\":{\"kk1\":\"vv1\",\"kk2\":false}}" + }, + { + "id": "4111661002", "key": "future_variable", "type": "future_type", "defaultValue": "future_value" From 1e45e027b4cf6ee4668918ce3b4bc4fc8ac6cf5e Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 1 May 2020 14:52:42 -0700 Subject: [PATCH 18/25] fix error --- core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java index 446aaf64b..4253e70d8 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java @@ -2243,7 +2243,7 @@ public void getAllFeatureVariablesWithListenerUserInExperimentFeatureOn() throws testDecisionInfoMap.put(SOURCE_INFO, testSourceInfo); int notificationId = optimizely.addDecisionNotificationHandler( - getDecisionListener(NotificationCenter.DecisionNotificationType.FEATURE_VARIABLES.toString(), + getDecisionListener(NotificationCenter.DecisionNotificationType.ALL_FEATURE_VARIABLES.toString(), testUserId, testUserAttributes, testDecisionInfoMap)); @@ -2289,7 +2289,7 @@ public void getAllFeatureVariablesWithListenerUserInExperimentFeatureOff() { testDecisionInfoMap.put(SOURCE_INFO, testSourceInfo); int notificationId = optimizely.addDecisionNotificationHandler( - getDecisionListener(NotificationCenter.DecisionNotificationType.FEATURE_VARIABLES.toString(), + getDecisionListener(NotificationCenter.DecisionNotificationType.ALL_FEATURE_VARIABLES.toString(), userID, testUserAttributes, testDecisionInfoMap)); From a7da2d5805bfe97a1abd94472e534ef0b49b476f Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 1 May 2020 15:45:27 -0700 Subject: [PATCH 19/25] fix tests --- .../com/optimizely/ab/OptimizelyTest.java | 66 +++++++++---------- .../ab/config/ValidProjectConfigV4.java | 12 ++-- 2 files changed, 36 insertions(+), 42 deletions(-) diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java index 4253e70d8..75fa2a421 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java @@ -28,9 +28,10 @@ import com.optimizely.ab.event.EventProcessor; import com.optimizely.ab.event.LogEvent; import com.optimizely.ab.event.internal.UserEventFactory; -import com.optimizely.ab.internal.LogbackVerifier; import com.optimizely.ab.internal.ControlAttribute; +import com.optimizely.ab.internal.LogbackVerifier; import com.optimizely.ab.notification.*; +import com.optimizely.ab.optimizelyjson.OptimizelyJSON; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.junit.Before; import org.junit.Rule; @@ -43,14 +44,8 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; -import java.io.Closeable; import java.io.IOException; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.function.Function; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.*; @@ -63,10 +58,7 @@ import static junit.framework.TestCase.assertTrue; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; +import static org.junit.Assert.*; import static org.junit.Assume.assumeTrue; import static org.mockito.Matchers.*; import static org.mockito.Mockito.*; @@ -4147,7 +4139,7 @@ public void getFeatureVariableJSONUserInExperimentFeatureOn() throws Exception { final String validFeatureKey = FEATURE_MULTI_VARIATE_FEATURE_KEY; String validVariableKey = VARIABLE_JSON_PATCHED_TYPE_KEY; - String expectedString = "{\"k1\":\"v1\",\"k2\":3.5,\"k3\":true,\"k4\":{\"kk1\":\"vv1\",\"kk2\":false}}"; + String expectedString = "{\"k1\":\"s1\",\"k2\":103.5,\"k3\":false,\"k4\":{\"kk1\":\"ss1\",\"kk2\":true}}"; Optimizely optimizely = optimizelyBuilder.build(); @@ -4159,14 +4151,14 @@ public void getFeatureVariableJSONUserInExperimentFeatureOn() throws Exception { assertEquals(json.toString(), expectedString); - assertEquals(json.toMap().get("k1"), "v1"); - assertEquals(json.toMap().get("k2"), 3.5); - assertEquals(json.toMap().get("k3"), true); - assertEquals(json.toMap().get("k4").get("kk1"), "vv11"); - assertEquals(json.toMap().get("k4").get("kk2"), false); + assertEquals(json.toMap().get("k1"), "s1"); + assertEquals(json.toMap().get("k2"), 103.5); + assertEquals(json.toMap().get("k3"), false); + assertEquals(((Map)json.toMap().get("k4")).get("kk1"), "ss1"); + assertEquals(((Map)json.toMap().get("k4")).get("kk2"), true); - assertEquals(json.getValue("k1", String.class), "v1"); - assertEquals(json.getValue("k1.k4", Boolean.class), "v1"); + assertEquals(json.getValue("k1", String.class), "s1"); + assertEquals(json.getValue("k4.kk2", Boolean.class), true); } /** @@ -4176,7 +4168,7 @@ public void getFeatureVariableJSONUserInExperimentFeatureOn() throws Exception { */ @SuppressFBWarnings("NP_NONNULL_PARAM_VIOLATION") @Test - public void getFeatureVariableJSONUserInExperimentFeatureOff() { + public void getFeatureVariableJSONUserInExperimentFeatureOff() throws Exception { assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())); final String validFeatureKey = FEATURE_MULTI_VARIATE_FEATURE_KEY; @@ -4197,11 +4189,11 @@ public void getFeatureVariableJSONUserInExperimentFeatureOff() { assertEquals(json.toMap().get("k1"), "v1"); assertEquals(json.toMap().get("k2"), 3.5); assertEquals(json.toMap().get("k3"), true); - assertEquals(json.toMap().get("k4").get("kk1"), "vv11"); - assertEquals(json.toMap().get("k4").get("kk2"), false); + assertEquals(((Map)json.toMap().get("k4")).get("kk1"), "vv1"); + assertEquals(((Map)json.toMap().get("k4")).get("kk2"), false); assertEquals(json.getValue("k1", String.class), "v1"); - assertEquals(json.getValue("k1.k4", Boolean.class), "v1"); + assertEquals(json.getValue("k4.kk2", Boolean.class), false); } /** @@ -4227,11 +4219,12 @@ public void getAllFeatureVariablesUserInExperimentFeatureOn() throws Exception { assertEquals(json.toMap().get("first_letter"), "F"); assertEquals(json.toMap().get("rest_of_name"), "red"); - assertEquals(json.toMap().get("json_patched").get("k1"), "v1"); - assertEquals(json.toMap().get("json_patched").get("k2"), 3.5); - assertEquals(json.toMap().get("json_patched").get("k3"), true); - assertEquals(json.toMap().get("json_patched").get("k4").get("kk1"), "vv11"); - assertEquals(json.toMap().get("json_patched").get("k4").get("kk2"), false); + Map subMap = (Map)json.toMap().get("json_patched"); + assertEquals(subMap.get("k1"), "v1"); + assertEquals(subMap.get("k2"), 3.5); + assertEquals(subMap.get("k3"), true); + assertEquals(((Map)subMap.get("k4")).get("kk1"), "vv11"); + assertEquals(((Map)subMap.get("k4")).get("kk2"), false); assertEquals(json.getValue("first_letter", String.class), "F"); assertEquals(json.getValue("json_patched.k1", String.class), "v1"); @@ -4245,7 +4238,7 @@ public void getAllFeatureVariablesUserInExperimentFeatureOn() throws Exception { */ @SuppressFBWarnings("NP_NONNULL_PARAM_VIOLATION") @Test - public void getAllFeatureVariablesUserInExperimentFeatureOff() { + public void getAllFeatureVariablesUserInExperimentFeatureOff() throws Exception { assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())); final String validFeatureKey = FEATURE_MULTI_VARIATE_FEATURE_KEY; @@ -4263,15 +4256,16 @@ public void getAllFeatureVariablesUserInExperimentFeatureOff() { assertEquals(json.toMap().get("first_letter"), "H"); assertEquals(json.toMap().get("rest_of_name"), "arry"); - assertEquals(json.toMap().get("json_patched").get("k1"), "v1"); - assertEquals(json.toMap().get("json_patched").get("k2"), 3.5); - assertEquals(json.toMap().get("json_patched").get("k3"), true); - assertEquals(json.toMap().get("json_patched").get("k4").get("kk1"), "vv11"); - assertEquals(json.toMap().get("json_patched").get("k4").get("kk2"), false); + Map subMap = (Map)json.toMap().get("json_patched"); + assertEquals(subMap.get("k1"), "v1"); + assertEquals(subMap.get("k2"), 3.5); + assertEquals(subMap.get("k3"), true); + assertEquals(((Map)subMap.get("k4")).get("kk1"), "vv11"); + assertEquals(((Map)subMap.get("k4")).get("kk2"), false); assertEquals(json.getValue("first_letter", String.class), "H"); assertEquals(json.getValue("json_patched.k1", String.class), "v1"); - assertEquals(json.getValue("json_patched.k1.k4", Boolean.class), "v1"); + assertEquals(json.getValue("json_patched.k4.kk2", Boolean.class), false); } /** 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 1c71667f8..7d711660f 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 @@ -407,8 +407,8 @@ public class ValidProjectConfigV4 { null ); private static final String VARIABLE_JSON_PATCHED_TYPE_ID = "4111661000"; - private static final String VARIABLE_JSON_PATCHED_TYPE_KEY = "json_patched"; - private static final String VARIABLE_JSON_PATCHED_TYPE_DEFAULT_VALUE = "{\"k1\":\"v1\",\"k2\":3.5,\"k3\":true,\"k4\":{\"kk1\":\"vv1\",\"kk2\":false}}"; + public static final String VARIABLE_JSON_PATCHED_TYPE_KEY = "json_patched"; + public static final String VARIABLE_JSON_PATCHED_TYPE_DEFAULT_VALUE = "{\"k1\":\"v1\",\"k2\":3.5,\"k3\":true,\"k4\":{\"kk1\":\"vv1\",\"kk2\":false}}"; private static final FeatureVariable VARIABLE_JSON_PATCHED_TYPE_VARIABLE = new FeatureVariable( VARIABLE_JSON_PATCHED_TYPE_ID, VARIABLE_JSON_PATCHED_TYPE_KEY, @@ -418,8 +418,8 @@ public class ValidProjectConfigV4 { FeatureVariable.JSON_TYPE ); private static final String VARIABLE_JSON_NATIVE_TYPE_ID = "4111661001"; - private static final String VARIABLE_JSON_NATIVE_TYPE_KEY = "json_native"; - private static final String VARIABLE_JSON_NATIVE_TYPE_DEFAULT_VALUE = "{\"k1\":\"v1\",\"k2\":3.5,\"k3\":true,\"k4\":{\"kk1\":\"vv1\",\"kk2\":false}}"; + public static final String VARIABLE_JSON_NATIVE_TYPE_KEY = "json_native"; + public static final String VARIABLE_JSON_NATIVE_TYPE_DEFAULT_VALUE = "{\"k1\":\"v1\",\"k2\":3.5,\"k3\":true,\"k4\":{\"kk1\":\"vv1\",\"kk2\":false}}"; private static final FeatureVariable VARIABLE_JSON_NATIVE_TYPE_VARIABLE = new FeatureVariable( VARIABLE_JSON_NATIVE_TYPE_ID, VARIABLE_JSON_NATIVE_TYPE_KEY, @@ -429,8 +429,8 @@ public class ValidProjectConfigV4 { null ); private static final String VARIABLE_FUTURE_TYPE_ID = "4111661002"; - private static final String VARIABLE_FUTURE_TYPE_KEY = "future_variable"; - private static final String VARIABLE_FUTURE_TYPE_DEFAULT_VALUE = "future_value"; + public static final String VARIABLE_FUTURE_TYPE_KEY = "future_variable"; + public static final String VARIABLE_FUTURE_TYPE_DEFAULT_VALUE = "future_value"; private static final FeatureVariable VARIABLE_FUTURE_TYPE_VARIABLE = new FeatureVariable( VARIABLE_FUTURE_TYPE_ID, VARIABLE_FUTURE_TYPE_KEY, From 87ac9563576b797ea3d372746201fc618be6827b Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 1 May 2020 15:56:29 -0700 Subject: [PATCH 20/25] fix error for getAllFeatureVariables --- .../main/java/com/optimizely/ab/Optimizely.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java index 97c1f61d5..77f2357f1 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -25,14 +25,16 @@ import com.optimizely.ab.error.ErrorHandler; import com.optimizely.ab.error.NoOpErrorHandler; import com.optimizely.ab.event.*; -import com.optimizely.ab.event.internal.*; +import com.optimizely.ab.event.internal.ClientEngineInfo; +import com.optimizely.ab.event.internal.EventFactory; +import com.optimizely.ab.event.internal.UserEvent; +import com.optimizely.ab.event.internal.UserEventFactory; import com.optimizely.ab.event.internal.payload.EventBatch; import com.optimizely.ab.notification.*; import com.optimizely.ab.optimizelyconfig.OptimizelyConfig; import com.optimizely.ab.optimizelyconfig.OptimizelyConfigManager; import com.optimizely.ab.optimizelyconfig.OptimizelyConfigService; import com.optimizely.ab.optimizelyjson.OptimizelyJSON; -import com.sun.org.apache.xpath.internal.operations.Variable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,11 +42,7 @@ import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; import java.io.Closeable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import static com.optimizely.ab.internal.SafetyUtils.tryClose; @@ -846,7 +844,7 @@ public OptimizelyJSON getAllFeatureVariables(@Nonnull String featureKey, convertedValue = ((OptimizelyJSON) convertedValue).toMap(); } - valuesMap.put(variable.getKey(), value); + valuesMap.put(variable.getKey(), convertedValue); } DecisionNotification decisionNotification = DecisionNotification.newFeatureVariableDecisionNotificationBuilder() From 734ec13e6346100e9ded9824821cfc263a7f66c4 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 1 May 2020 18:07:09 -0700 Subject: [PATCH 21/25] fix tests for json apis --- .../com/optimizely/ab/OptimizelyTest.java | 48 +++++++++++-------- .../ab/config/ValidProjectConfigV4.java | 15 +++++- .../config/parser/GsonConfigParserTest.java | 4 +- .../parser/JacksonConfigParserTest.java | 4 +- .../config/parser/JsonConfigParserTest.java | 4 +- .../parser/JsonSimpleConfigParserTest.java | 16 ++----- .../config/valid-project-config-v4.json | 10 +++- 7 files changed, 61 insertions(+), 40 deletions(-) diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java index 75fa2a421..7e39ce36d 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java @@ -17,6 +17,8 @@ import ch.qos.logback.classic.Level; import com.google.common.collect.ImmutableMap; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; import com.optimizely.ab.bucketing.Bucketer; import com.optimizely.ab.bucketing.DecisionService; import com.optimizely.ab.bucketing.FeatureDecision; @@ -2240,11 +2242,11 @@ public void getAllFeatureVariablesWithListenerUserInExperimentFeatureOn() throws testUserAttributes, testDecisionInfoMap)); - assertEquals(optimizely.getAllFeatureVariables( + String jsonString = optimizely.getAllFeatureVariables( validFeatureKey, testUserId, - Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE)).toString(), - expectedString); + Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE)).toString(); + assertTrue(compareJsonStrings(jsonString, expectedString)); // Verify that listener being called assertTrue(isListenerCalled); @@ -2286,11 +2288,11 @@ public void getAllFeatureVariablesWithListenerUserInExperimentFeatureOff() { testUserAttributes, testDecisionInfoMap)); - assertEquals(optimizely.getAllFeatureVariables( + String jsonString = optimizely.getAllFeatureVariables( validFeatureKey, userID, - null).toString(), - expectedString); + null).toString(); + assertTrue(compareJsonStrings(jsonString, expectedString)); // Verify that listener being called assertTrue(isListenerCalled); @@ -4149,7 +4151,7 @@ public void getFeatureVariableJSONUserInExperimentFeatureOn() throws Exception { testUserId, Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE)); - assertEquals(json.toString(), expectedString); + assertTrue(compareJsonStrings(json.toString(), expectedString)); assertEquals(json.toMap().get("k1"), "s1"); assertEquals(json.toMap().get("k2"), 103.5); @@ -4184,7 +4186,7 @@ public void getFeatureVariableJSONUserInExperimentFeatureOff() throws Exception userID, null); - assertEquals(json.toString(), expectedString); + assertTrue(compareJsonStrings(json.toString(), expectedString)); assertEquals(json.toMap().get("k1"), "v1"); assertEquals(json.toMap().get("k2"), 3.5); @@ -4206,7 +4208,7 @@ public void getAllFeatureVariablesUserInExperimentFeatureOn() throws Exception { assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())); final String validFeatureKey = FEATURE_MULTI_VARIATE_FEATURE_KEY; - String expectedString = "{\"first_letter\":\"F\",\"rest_of_name\":\"red\",\"json_patched\":{\"k1\":\"v1\",\"k2\":3.5,\"k3\":true,\"k4\":{\"kk1\":\"vv1\",\"kk2\":false}}}"; + String expectedString = "{\"first_letter\":\"F\",\"rest_of_name\":\"red\",\"json_patched\":{\"k1\":\"s1\",\"k2\":103.5,\"k3\":false,\"k4\":{\"kk1\":\"ss1\",\"kk2\":true}}}"; Optimizely optimizely = optimizelyBuilder.build(); @@ -4215,20 +4217,20 @@ public void getAllFeatureVariablesUserInExperimentFeatureOn() throws Exception { testUserId, Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE)); - assertEquals(json.toString(), expectedString); + assertTrue(compareJsonStrings(json.toString(), expectedString)); assertEquals(json.toMap().get("first_letter"), "F"); assertEquals(json.toMap().get("rest_of_name"), "red"); Map subMap = (Map)json.toMap().get("json_patched"); - assertEquals(subMap.get("k1"), "v1"); - assertEquals(subMap.get("k2"), 3.5); - assertEquals(subMap.get("k3"), true); - assertEquals(((Map)subMap.get("k4")).get("kk1"), "vv11"); - assertEquals(((Map)subMap.get("k4")).get("kk2"), false); + assertEquals(subMap.get("k1"), "s1"); + assertEquals(subMap.get("k2"), 103.5); + assertEquals(subMap.get("k3"), false); + assertEquals(((Map)subMap.get("k4")).get("kk1"), "ss1"); + assertEquals(((Map)subMap.get("k4")).get("kk2"), true); assertEquals(json.getValue("first_letter", String.class), "F"); - assertEquals(json.getValue("json_patched.k1", String.class), "v1"); - assertEquals(json.getValue("json_patched.k1.k4", Boolean.class), "v1"); + assertEquals(json.getValue("json_patched.k1", String.class), "s1"); + assertEquals(json.getValue("json_patched.k4.kk2", Boolean.class), true); } /** @@ -4252,7 +4254,7 @@ public void getAllFeatureVariablesUserInExperimentFeatureOff() throws Exception userID, null); - assertEquals(json.toString(), expectedString); + assertTrue(compareJsonStrings(json.toString(), expectedString)); assertEquals(json.toMap().get("first_letter"), "H"); assertEquals(json.toMap().get("rest_of_name"), "arry"); @@ -4260,7 +4262,7 @@ public void getAllFeatureVariablesUserInExperimentFeatureOff() throws Exception assertEquals(subMap.get("k1"), "v1"); assertEquals(subMap.get("k2"), 3.5); assertEquals(subMap.get("k3"), true); - assertEquals(((Map)subMap.get("k4")).get("kk1"), "vv11"); + assertEquals(((Map)subMap.get("k4")).get("kk1"), "vv1"); assertEquals(((Map)subMap.get("k4")).get("kk2"), false); assertEquals(json.getValue("first_letter", String.class), "H"); @@ -4383,6 +4385,14 @@ private EventType createUnknownEventType() { return new EventType("8765", "unknown_event_type", experimentIds); } + private boolean compareJsonStrings(String str1, String str2) { + JsonParser parser = new JsonParser(); + + JsonElement j1 = parser.parse(str1); + JsonElement j2 = parser.parse(str2); + return j1.equals(j2); + } + /* Invalid Experiment */ @Test @SuppressFBWarnings("NP") 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 7d711660f..f50a2780e 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 @@ -417,6 +417,9 @@ public class ValidProjectConfigV4 { FeatureVariable.STRING_TYPE, FeatureVariable.JSON_TYPE ); + + private static final String FEATURE_MULTI_VARIATE_FUTURE_FEATURE_ID = "3263342227"; + public static final String FEATURE_MULTI_VARIATE_FUTURE_FEATURE_KEY = "multi_variate_future_feature"; private static final String VARIABLE_JSON_NATIVE_TYPE_ID = "4111661001"; public static final String VARIABLE_JSON_NATIVE_TYPE_KEY = "json_native"; public static final String VARIABLE_JSON_NATIVE_TYPE_DEFAULT_VALUE = "{\"k1\":\"v1\",\"k2\":3.5,\"k3\":true,\"k4\":{\"kk1\":\"vv1\",\"kk2\":false}}"; @@ -439,6 +442,7 @@ public class ValidProjectConfigV4 { "future_type", null ); + private static final String FEATURE_MUTEX_GROUP_FEATURE_ID = "3263342226"; public static final String FEATURE_MUTEX_GROUP_FEATURE_KEY = "mutex_group_feature"; private static final String VARIABLE_CORRELATING_VARIATION_NAME_ID = "2059187672"; @@ -1308,7 +1312,15 @@ public class ValidProjectConfigV4 { DatafileProjectConfigTestUtils.createListOfObjects( VARIABLE_FIRST_LETTER_VARIABLE, VARIABLE_REST_OF_NAME_VARIABLE, - VARIABLE_JSON_PATCHED_TYPE_VARIABLE, + VARIABLE_JSON_PATCHED_TYPE_VARIABLE + ) + ); + public static final FeatureFlag FEATURE_FLAG_MULTI_VARIATE_FUTURE_FEATURE = new FeatureFlag( + FEATURE_MULTI_VARIATE_FUTURE_FEATURE_ID, + FEATURE_MULTI_VARIATE_FUTURE_FEATURE_KEY, + ROLLOUT_2_ID, + Collections.singletonList(EXPERIMENT_MULTIVARIATE_EXPERIMENT_ID), + DatafileProjectConfigTestUtils.createListOfObjects( VARIABLE_JSON_NATIVE_TYPE_VARIABLE, VARIABLE_FUTURE_TYPE_VARIABLE ) @@ -1401,6 +1413,7 @@ public static ProjectConfig generateValidProjectConfigV4() { featureFlags.add(FEATURE_FLAG_SINGLE_VARIABLE_BOOLEAN); featureFlags.add(FEATURE_FLAG_SINGLE_VARIABLE_STRING); featureFlags.add(FEATURE_FLAG_MULTI_VARIATE_FEATURE); + featureFlags.add(FEATURE_FLAG_MULTI_VARIATE_FUTURE_FEATURE); featureFlags.add(FEATURE_FLAG_MUTEX_GROUP_FEATURE); List groups = new ArrayList(); 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 de2ac643a..e426d0cbd 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 @@ -115,7 +115,7 @@ public void parseFeatureVariablesWithJsonNative() throws Exception { // native "json" type - FeatureFlag featureFlag = actual.getFeatureKeyMapping().get("multi_variate_feature"); + FeatureFlag featureFlag = actual.getFeatureKeyMapping().get("multi_variate_future_feature"); FeatureVariable variable = featureFlag.getVariableKeyToFeatureVariableMap().get("json_native"); assertEquals(variable.getType(), "json"); @@ -128,7 +128,7 @@ public void parseFeatureVariablesWithFutureType() throws Exception { // unknown type - FeatureFlag featureFlag = actual.getFeatureKeyMapping().get("multi_variate_feature"); + FeatureFlag featureFlag = actual.getFeatureKeyMapping().get("multi_variate_future_feature"); FeatureVariable variable = featureFlag.getVariableKeyToFeatureVariableMap().get("future_variable"); assertEquals(variable.getType(), "future_type"); diff --git a/core-api/src/test/java/com/optimizely/ab/config/parser/JacksonConfigParserTest.java b/core-api/src/test/java/com/optimizely/ab/config/parser/JacksonConfigParserTest.java index 61c44e730..7371ab0c2 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/parser/JacksonConfigParserTest.java +++ b/core-api/src/test/java/com/optimizely/ab/config/parser/JacksonConfigParserTest.java @@ -109,7 +109,7 @@ public void parseFeatureVariablesWithJsonNative() throws Exception { // native "json" type - FeatureFlag featureFlag = actual.getFeatureKeyMapping().get("multi_variate_feature"); + FeatureFlag featureFlag = actual.getFeatureKeyMapping().get("multi_variate_future_feature"); FeatureVariable variable = featureFlag.getVariableKeyToFeatureVariableMap().get("json_native"); assertEquals(variable.getType(), "json"); @@ -122,7 +122,7 @@ public void parseFeatureVariablesWithFutureType() throws Exception { // unknown type - FeatureFlag featureFlag = actual.getFeatureKeyMapping().get("multi_variate_feature"); + FeatureFlag featureFlag = actual.getFeatureKeyMapping().get("multi_variate_future_feature"); FeatureVariable variable = featureFlag.getVariableKeyToFeatureVariableMap().get("future_variable"); assertEquals(variable.getType(), "future_type"); diff --git a/core-api/src/test/java/com/optimizely/ab/config/parser/JsonConfigParserTest.java b/core-api/src/test/java/com/optimizely/ab/config/parser/JsonConfigParserTest.java index 27995b88f..9b3fd2262 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/parser/JsonConfigParserTest.java +++ b/core-api/src/test/java/com/optimizely/ab/config/parser/JsonConfigParserTest.java @@ -111,7 +111,7 @@ public void parseFeatureVariablesWithJsonNative() throws Exception { // native "json" type - FeatureFlag featureFlag = actual.getFeatureKeyMapping().get("multi_variate_feature"); + FeatureFlag featureFlag = actual.getFeatureKeyMapping().get("multi_variate_future_feature"); FeatureVariable variable = featureFlag.getVariableKeyToFeatureVariableMap().get("json_native"); assertEquals(variable.getType(), "json"); @@ -124,7 +124,7 @@ public void parseFeatureVariablesWithFutureType() throws Exception { // unknown type - FeatureFlag featureFlag = actual.getFeatureKeyMapping().get("multi_variate_feature"); + FeatureFlag featureFlag = actual.getFeatureKeyMapping().get("multi_variate_future_feature"); FeatureVariable variable = featureFlag.getVariableKeyToFeatureVariableMap().get("future_variable"); assertEquals(variable.getType(), "future_type"); diff --git a/core-api/src/test/java/com/optimizely/ab/config/parser/JsonSimpleConfigParserTest.java b/core-api/src/test/java/com/optimizely/ab/config/parser/JsonSimpleConfigParserTest.java index e22192291..f869740c8 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/parser/JsonSimpleConfigParserTest.java +++ b/core-api/src/test/java/com/optimizely/ab/config/parser/JsonSimpleConfigParserTest.java @@ -19,7 +19,6 @@ import com.optimizely.ab.config.FeatureFlag; import com.optimizely.ab.config.FeatureVariable; import com.optimizely.ab.config.ProjectConfig; - import com.optimizely.ab.config.audience.AudienceIdCondition; import com.optimizely.ab.config.audience.Condition; import com.optimizely.ab.config.audience.UserAttribute; @@ -32,16 +31,7 @@ import org.junit.Test; import org.junit.rules.ExpectedException; -import java.util.List; - -import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.nullFeatureEnabledConfigJsonV4; -import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validConfigJsonV2; -import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validConfigJsonV4; -import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV2; -import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validConfigJsonV3; -import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV3; -import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV4; -import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.verifyProjectConfig; +import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.*; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -113,7 +103,7 @@ public void parseFeatureVariablesWithJsonNative() throws Exception { // native "json" type - FeatureFlag featureFlag = actual.getFeatureKeyMapping().get("multi_variate_feature"); + FeatureFlag featureFlag = actual.getFeatureKeyMapping().get("multi_variate_future_feature"); FeatureVariable variable = featureFlag.getVariableKeyToFeatureVariableMap().get("json_native"); assertEquals(variable.getType(), "json"); @@ -126,7 +116,7 @@ public void parseFeatureVariablesWithFutureType() throws Exception { // unknown type - FeatureFlag featureFlag = actual.getFeatureKeyMapping().get("multi_variate_feature"); + FeatureFlag featureFlag = actual.getFeatureKeyMapping().get("multi_variate_future_feature"); FeatureVariable variable = featureFlag.getVariableKeyToFeatureVariableMap().get("future_variable"); assertEquals(variable.getType(), "future_type"); 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 455a5e8bf..42e965967 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 @@ -716,7 +716,15 @@ "type": "string", "subType": "json", "defaultValue": "{\"k1\":\"v1\",\"k2\":3.5,\"k3\":true,\"k4\":{\"kk1\":\"vv1\",\"kk2\":false}}" - }, + } + ] + }, + { + "id": "3263342227", + "key": "multi_variate_future_feature", + "rolloutId": "813411034", + "experimentIds": ["3262035800"], + "variables": [ { "id": "4111661001", "key": "json_native", From 81375d679113632e3d63b81a33f9518b5f5fc6a1 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Mon, 4 May 2020 11:23:56 -0700 Subject: [PATCH 22/25] fix notification error --- .../java/com/optimizely/ab/Optimizely.java | 6 +- .../com/optimizely/ab/OptimizelyTest.java | 112 +++++++++++++++++- 2 files changed, 114 insertions(+), 4 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java index 77f2357f1..205fed40b 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -711,6 +711,10 @@ T getFeatureVariableValueForType(@Nonnull String featureKey, } Object convertedValue = convertStringToType(variableValue, variableType); + Object notificationValue = convertedValue; + if (convertedValue instanceof OptimizelyJSON) { + notificationValue = ((OptimizelyJSON) convertedValue).toMap(); + } DecisionNotification decisionNotification = DecisionNotification.newFeatureVariableDecisionNotificationBuilder() .withUserId(userId) @@ -719,7 +723,7 @@ T getFeatureVariableValueForType(@Nonnull String featureKey, .withFeatureEnabled(featureEnabled) .withVariableKey(variableKey) .withVariableType(variableType) - .withVariableValue(convertedValue) + .withVariableValue(notificationValue) .withFeatureDecision(featureDecision) .build(); diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java index 7e39ce36d..392dff26c 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java @@ -17,6 +17,7 @@ import ch.qos.logback.classic.Level; import com.google.common.collect.ImmutableMap; +import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonParser; import com.optimizely.ab.bucketing.Bucketer; @@ -2209,6 +2210,107 @@ public void getFeatureVariableDoubleWithListenerUserInExperimentFeatureOn() thro assertTrue(optimizely.notificationCenter.removeNotificationListener(notificationId)); } + /** + * Verify that the {@link Optimizely#getFeatureVariableJSON(String, String, String, Map)} + * notification listener of getFeatureVariableString is called when feature is in experiment and feature is true + */ + @Test + public void getFeatureVariableJSONWithListenerUserInExperimentFeatureOn() throws Exception { + assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())); + isListenerCalled = false; + final String validFeatureKey = FEATURE_MULTI_VARIATE_FEATURE_KEY; + String validVariableKey = VARIABLE_JSON_PATCHED_TYPE_KEY; + String expectedString = "{\"k1\":\"s1\",\"k2\":103.5,\"k3\":false,\"k4\":{\"kk1\":\"ss1\",\"kk2\":true}}"; + + Optimizely optimizely = optimizelyBuilder.build(); + + final Map testUserAttributes = new HashMap<>(); + testUserAttributes.put(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE); + + final Map testSourceInfo = new HashMap<>(); + testSourceInfo.put(EXPERIMENT_KEY, "multivariate_experiment"); + testSourceInfo.put(VARIATION_KEY, "Fred"); + + final Map testDecisionInfoMap = new HashMap<>(); + testDecisionInfoMap.put(FEATURE_KEY, validFeatureKey); + testDecisionInfoMap.put(FEATURE_ENABLED, true); + testDecisionInfoMap.put(VARIABLE_KEY, validVariableKey); + testDecisionInfoMap.put(VARIABLE_TYPE, FeatureVariable.JSON_TYPE); + testDecisionInfoMap.put(VARIABLE_VALUE, parseJsonString(expectedString)); + testDecisionInfoMap.put(SOURCE, FeatureDecision.DecisionSource.FEATURE_TEST.toString()); + testDecisionInfoMap.put(SOURCE_INFO, testSourceInfo); + + int notificationId = optimizely.addDecisionNotificationHandler( + getDecisionListener(NotificationCenter.DecisionNotificationType.FEATURE_VARIABLE.toString(), + testUserId, + testUserAttributes, + testDecisionInfoMap)); + + OptimizelyJSON json = optimizely.getFeatureVariableJSON( + validFeatureKey, + validVariableKey, + testUserId, + Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE)); + + assertTrue(compareJsonStrings(json.toString(), expectedString)); + + // Verify that listener being called + assertTrue(isListenerCalled); + assertTrue(optimizely.notificationCenter.removeNotificationListener(notificationId)); + } + + /** + * Verify that the {@link Optimizely#getFeatureVariableJSON(String, String, String, Map)} + * notification listener of getFeatureVariableString is called when feature is in experiment and feature enabled is false + * than default value will get returned and passing null attribute will send empty map instead of null + */ + @SuppressFBWarnings("NP_NONNULL_PARAM_VIOLATION") + @Test + public void getFeatureVariableJSONWithListenerUserInExperimentFeatureOff() { + assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())); + isListenerCalled = false; + final String validFeatureKey = FEATURE_MULTI_VARIATE_FEATURE_KEY; + String validVariableKey = VARIABLE_JSON_PATCHED_TYPE_KEY; + String expectedString = "{\"k1\":\"v1\",\"k2\":3.5,\"k3\":true,\"k4\":{\"kk1\":\"vv1\",\"kk2\":false}}"; + + String userID = "Gred"; + + Optimizely optimizely = optimizelyBuilder.build(); + + final Map testUserAttributes = new HashMap<>(); + + final Map testSourceInfo = new HashMap<>(); + testSourceInfo.put(EXPERIMENT_KEY, "multivariate_experiment"); + testSourceInfo.put(VARIATION_KEY, "Gred"); + + final Map testDecisionInfoMap = new HashMap<>(); + testDecisionInfoMap.put(FEATURE_KEY, validFeatureKey); + testDecisionInfoMap.put(FEATURE_ENABLED, false); + testDecisionInfoMap.put(VARIABLE_KEY, validVariableKey); + testDecisionInfoMap.put(VARIABLE_TYPE, FeatureVariable.JSON_TYPE); + testDecisionInfoMap.put(VARIABLE_VALUE, parseJsonString(expectedString)); + testDecisionInfoMap.put(SOURCE, FeatureDecision.DecisionSource.FEATURE_TEST.toString()); + testDecisionInfoMap.put(SOURCE_INFO, testSourceInfo); + + int notificationId = optimizely.addDecisionNotificationHandler( + getDecisionListener(NotificationCenter.DecisionNotificationType.FEATURE_VARIABLE.toString(), + userID, + testUserAttributes, + testDecisionInfoMap)); + + OptimizelyJSON json = optimizely.getFeatureVariableJSON( + validFeatureKey, + validVariableKey, + userID, + null); + + assertTrue(compareJsonStrings(json.toString(), expectedString)); + + // Verify that listener being called + assertTrue(isListenerCalled); + assertTrue(optimizely.notificationCenter.removeNotificationListener(notificationId)); + } + /** * Verify that the {@link Optimizely#getAllFeatureVariables(String, String, Map)} * notification listener of getAllFeatureVariables is called when feature is in experiment and feature is true @@ -2218,7 +2320,7 @@ public void getAllFeatureVariablesWithListenerUserInExperimentFeatureOn() throws assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())); isListenerCalled = false; final String validFeatureKey = FEATURE_MULTI_VARIATE_FEATURE_KEY; - String expectedString = "{\"first_letter\":\"F\",\"rest_of_name\":\"red\",\"json_patched\":{\"k1\":\"v1\",\"k2\":3.5,\"k3\":true,\"k4\":{\"kk1\":\"vv1\",\"kk2\":false}}}"; + String expectedString = "{\"first_letter\":\"F\",\"rest_of_name\":\"red\",\"json_patched\":{\"k1\":\"s1\",\"k2\":103.5,\"k3\":false,\"k4\":{\"kk1\":\"ss1\",\"kk2\":true}}}"; Optimizely optimizely = optimizelyBuilder.build(); @@ -2232,7 +2334,7 @@ public void getAllFeatureVariablesWithListenerUserInExperimentFeatureOn() throws final Map testDecisionInfoMap = new HashMap<>(); testDecisionInfoMap.put(FEATURE_KEY, validFeatureKey); testDecisionInfoMap.put(FEATURE_ENABLED, true); - testDecisionInfoMap.put(VARIABLE_VALUES, expectedString); + testDecisionInfoMap.put(VARIABLE_VALUES, parseJsonString(expectedString)); testDecisionInfoMap.put(SOURCE, FeatureDecision.DecisionSource.FEATURE_TEST.toString()); testDecisionInfoMap.put(SOURCE_INFO, testSourceInfo); @@ -2278,7 +2380,7 @@ public void getAllFeatureVariablesWithListenerUserInExperimentFeatureOff() { final Map testDecisionInfoMap = new HashMap<>(); testDecisionInfoMap.put(FEATURE_KEY, validFeatureKey); testDecisionInfoMap.put(FEATURE_ENABLED, false); - testDecisionInfoMap.put(VARIABLE_VALUES, expectedString); + testDecisionInfoMap.put(VARIABLE_VALUES, parseJsonString(expectedString)); testDecisionInfoMap.put(SOURCE, FeatureDecision.DecisionSource.FEATURE_TEST.toString()); testDecisionInfoMap.put(SOURCE_INFO, testSourceInfo); @@ -4393,6 +4495,10 @@ private boolean compareJsonStrings(String str1, String str2) { return j1.equals(j2); } + private Map parseJsonString(String str) { + return new Gson().fromJson(str, Map.class); + } + /* Invalid Experiment */ @Test @SuppressFBWarnings("NP") From 1bddf80e5e5b59ac96610c12b351fb48682dafdf Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 5 May 2020 14:05:42 -0700 Subject: [PATCH 23/25] cleanup --- core-api/src/main/java/com/optimizely/ab/Optimizely.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java index 205fed40b..e7f614f2a 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -836,7 +836,7 @@ public OptimizelyJSON getAllFeatureVariables(@Nonnull String featureKey, Map valuesMap = new HashMap(); for (FeatureVariable variable : featureFlag.getVariables()) { String value = variable.getDefaultValue(); - if (featureEnabled && variation != null) { + if (featureEnabled) { FeatureVariableUsageInstance instance = variation.getVariableIdToFeatureVariableUsageInstanceMap().get(variable.getId()); if (instance != null) { value = instance.getValue(); From 8c45ed2e02b46f7a99a9b8277613e508f3140160 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 5 May 2020 14:47:09 -0700 Subject: [PATCH 24/25] add tests for coverage --- .../parser/UnsupportedOperationException.java | 27 ------------------- .../com/optimizely/ab/OptimizelyTest.java | 27 +++++++++++++++++-- 2 files changed, 25 insertions(+), 29 deletions(-) delete mode 100644 core-api/src/main/java/com/optimizely/ab/config/parser/UnsupportedOperationException.java diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/UnsupportedOperationException.java b/core-api/src/main/java/com/optimizely/ab/config/parser/UnsupportedOperationException.java deleted file mode 100644 index 02980455c..000000000 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/UnsupportedOperationException.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * - * Copyright 2020, 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; - -public final class UnsupportedOperationException extends Exception { - public UnsupportedOperationException(String message) { - super(message); - } - - public UnsupportedOperationException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java index 392dff26c..f4c67df82 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java @@ -4301,7 +4301,7 @@ public void getFeatureVariableJSONUserInExperimentFeatureOff() throws Exception } /** - * Verify that the {@link Optimizely#getFeatureVariableJSON(String, String, String, Map)} + * Verify that the {@link Optimizely#getAllFeatureVariables(String,String, Map)} * is called when feature is in experiment and feature enabled is true * returns variable value */ @@ -4336,7 +4336,7 @@ public void getAllFeatureVariablesUserInExperimentFeatureOn() throws Exception { } /** - * Verify that the {@link Optimizely#getFeatureVariableJSON(String, String, String, Map)} + * Verify that the {@link Optimizely#getAllFeatureVariables(String, String, Map)} * is called when feature is in experiment and feature enabled is false * than default value will gets returned */ @@ -4372,6 +4372,29 @@ public void getAllFeatureVariablesUserInExperimentFeatureOff() throws Exception assertEquals(json.getValue("json_patched.k4.kk2", Boolean.class), false); } + /** + * Verify {@link Optimizely#getAllFeatureVariables(String,String, Map)} with invalid parameters + */ + @SuppressFBWarnings("NP_NONNULL_PARAM_VIOLATION") + @Test + public void getAllFeatureVariablesWithInvalidParameters() throws Exception { + Optimizely optimizely = optimizelyBuilder.build(); + + OptimizelyJSON value; + value = optimizely.getAllFeatureVariables(null, testUserId); + assertNull(value); + + value = optimizely.getAllFeatureVariables(FEATURE_MULTI_VARIATE_FEATURE_KEY, null); + assertNull(value); + + value = optimizely.getAllFeatureVariables("invalid-feature-flag", testUserId); + assertNull(value); + + Optimizely optimizelyInvalid = Optimizely.builder(invalidProjectConfigV5(), mockEventHandler).build(); + value = optimizelyInvalid.getAllFeatureVariables(FEATURE_MULTI_VARIATE_FEATURE_KEY, testUserId); + assertNull(value); + } + /** * Verify that {@link Optimizely#getVariation(String, String)} returns a variation when given an experiment * with no audiences and no user attributes and verify that listener is getting called. From 5f2a23ed71f58f9f61101a92bf289e866322ba03 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 7 May 2020 11:44:46 -0700 Subject: [PATCH 25/25] cleanup --- .../java/com/optimizely/ab/config/parser/ConfigParser.java | 4 ---- .../com/optimizely/ab/config/parser/GsonConfigParser.java | 1 - 2 files changed, 5 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/ConfigParser.java b/core-api/src/main/java/com/optimizely/ab/config/parser/ConfigParser.java index 34fcb9c2c..c8fe74b08 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/ConfigParser.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/ConfigParser.java @@ -16,10 +16,6 @@ */ package com.optimizely.ab.config.parser; -import com.google.gson.JsonElement; -import com.google.gson.JsonIOException; -import com.google.gson.JsonSyntaxException; -import com.google.gson.stream.JsonWriter; import com.optimizely.ab.config.ProjectConfig; import javax.annotation.Nonnull; 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 012b45d32..972d76431 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,7 +18,6 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import com.google.gson.JsonSyntaxException; import com.optimizely.ab.config.*; import com.optimizely.ab.config.audience.Audience; import com.optimizely.ab.config.audience.TypedAudience;