diff --git a/core-api/src/main/java/com/optimizely/ab/internal/JsonParserProvider.java b/core-api/src/main/java/com/optimizely/ab/internal/JsonParserProvider.java
new file mode 100644
index 000000000..6f6de6516
--- /dev/null
+++ b/core-api/src/main/java/com/optimizely/ab/internal/JsonParserProvider.java
@@ -0,0 +1,74 @@
+/**
+ * Copyright 2022, Optimizely Inc. 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.internal;
+
+import com.optimizely.ab.config.parser.MissingJsonParserException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public enum JsonParserProvider {
+ GSON_CONFIG_PARSER("com.google.gson.Gson"),
+ JACKSON_CONFIG_PARSER("com.fasterxml.jackson.databind.ObjectMapper" ),
+ JSON_CONFIG_PARSER("org.json.JSONObject"),
+ JSON_SIMPLE_CONFIG_PARSER("org.json.simple.JSONObject");
+
+ private static final Logger logger = LoggerFactory.getLogger(JsonParserProvider.class);
+
+ private final String className;
+
+ JsonParserProvider(String className) {
+ this.className = className;
+ }
+
+ private boolean isAvailable() {
+ try {
+ Class.forName(className);
+ return true;
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ }
+
+ public static JsonParserProvider getDefaultParser() {
+ String defaultParserName = PropertyUtils.get("default_parser");
+
+ if (defaultParserName != null) {
+ try {
+ JsonParserProvider parser = JsonParserProvider.valueOf(defaultParserName);
+ if (parser.isAvailable()) {
+ logger.debug("using json parser: {}, based on override config", parser.className);
+ return parser;
+ }
+
+ logger.warn("configured parser {} is not available in the classpath", defaultParserName);
+ } catch (IllegalArgumentException e) {
+ logger.warn("configured parser {} is not a valid value", defaultParserName);
+ }
+ }
+
+ for (JsonParserProvider parser: JsonParserProvider.values()) {
+ if (!parser.isAvailable()) {
+ continue;
+ }
+
+ logger.info("using json parser: {}", parser.className);
+ return parser;
+ }
+
+ throw new MissingJsonParserException("unable to locate a JSON parser. "
+ + "Please see for more information");
+ }
+}
diff --git a/core-api/src/main/java/com/optimizely/ab/odp/ODPApiManager.java b/core-api/src/main/java/com/optimizely/ab/odp/ODPApiManager.java
new file mode 100644
index 000000000..ed40e37fd
--- /dev/null
+++ b/core-api/src/main/java/com/optimizely/ab/odp/ODPApiManager.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright 2022, Optimizely Inc. 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.odp;
+
+import java.util.List;
+
+public interface ODPApiManager {
+ String fetchQualifiedSegments(String apiKey, String apiEndpoint, String userKey, String userValue, List segmentsToCheck);
+}
diff --git a/core-api/src/main/java/com/optimizely/ab/odp/parser/ResponseJsonParser.java b/core-api/src/main/java/com/optimizely/ab/odp/parser/ResponseJsonParser.java
new file mode 100644
index 000000000..d494a78d0
--- /dev/null
+++ b/core-api/src/main/java/com/optimizely/ab/odp/parser/ResponseJsonParser.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright 2022, Optimizely Inc. 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.odp.parser;
+
+import java.util.List;
+
+public interface ResponseJsonParser {
+ public List parseQualifiedSegments(String responseJson);
+}
diff --git a/core-api/src/main/java/com/optimizely/ab/odp/parser/ResponseJsonParserFactory.java b/core-api/src/main/java/com/optimizely/ab/odp/parser/ResponseJsonParserFactory.java
new file mode 100644
index 000000000..7762cef0e
--- /dev/null
+++ b/core-api/src/main/java/com/optimizely/ab/odp/parser/ResponseJsonParserFactory.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright 2022, Optimizely Inc. 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.odp.parser;
+
+import com.optimizely.ab.internal.JsonParserProvider;
+import com.optimizely.ab.odp.parser.impl.GsonParser;
+import com.optimizely.ab.odp.parser.impl.JacksonParser;
+import com.optimizely.ab.odp.parser.impl.JsonParser;
+import com.optimizely.ab.odp.parser.impl.JsonSimpleParser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ResponseJsonParserFactory {
+ private static final Logger logger = LoggerFactory.getLogger(ResponseJsonParserFactory.class);
+
+ public static ResponseJsonParser getParser() {
+ JsonParserProvider parserProvider = JsonParserProvider.getDefaultParser();
+ ResponseJsonParser jsonParser = null;
+ switch (parserProvider) {
+ case GSON_CONFIG_PARSER:
+ jsonParser = new GsonParser();
+ break;
+ case JACKSON_CONFIG_PARSER:
+ jsonParser = new JacksonParser();
+ break;
+ case JSON_CONFIG_PARSER:
+ jsonParser = new JsonParser();
+ break;
+ case JSON_SIMPLE_CONFIG_PARSER:
+ jsonParser = new JsonSimpleParser();
+ break;
+ }
+ logger.info("Using " + parserProvider.toString() + " parser");
+ return jsonParser;
+ }
+}
diff --git a/core-api/src/main/java/com/optimizely/ab/odp/parser/impl/GsonParser.java b/core-api/src/main/java/com/optimizely/ab/odp/parser/impl/GsonParser.java
new file mode 100644
index 000000000..b27d65078
--- /dev/null
+++ b/core-api/src/main/java/com/optimizely/ab/odp/parser/impl/GsonParser.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright 2022, Optimizely Inc. 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.odp.parser.impl;
+
+import com.google.gson.*;
+import com.google.gson.JsonParser;
+import com.optimizely.ab.odp.parser.ResponseJsonParser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class GsonParser implements ResponseJsonParser {
+ private static final Logger logger = LoggerFactory.getLogger(GsonParser.class);
+
+ @Override
+ public List parseQualifiedSegments(String responseJson) {
+ List parsedSegments = new ArrayList<>();
+ try {
+ JsonObject root = JsonParser.parseString(responseJson).getAsJsonObject();
+
+ if (root.has("errors")) {
+ JsonArray errors = root.getAsJsonArray("errors");
+ StringBuilder logMessage = new StringBuilder();
+ for (int i = 0; i < errors.size(); i++) {
+ if (i > 0) {
+ logMessage.append(", ");
+ }
+ logMessage.append(errors.get(i).getAsJsonObject().get("message").getAsString());
+ }
+ logger.error(logMessage.toString());
+ return null;
+ }
+
+ JsonArray edges = root.getAsJsonObject("data").getAsJsonObject("customer").getAsJsonObject("audiences").getAsJsonArray("edges");
+ for (int i = 0; i < edges.size(); i++) {
+ JsonObject node = edges.get(i).getAsJsonObject().getAsJsonObject("node");
+ if (node.has("state") && node.get("state").getAsString().equals("qualified")) {
+ parsedSegments.add(node.get("name").getAsString());
+ }
+ }
+ return parsedSegments;
+ } catch (JsonSyntaxException e) {
+ logger.error("Error parsing qualified segments from response", e);
+ return null;
+ }
+ }
+}
diff --git a/core-api/src/main/java/com/optimizely/ab/odp/parser/impl/JacksonParser.java b/core-api/src/main/java/com/optimizely/ab/odp/parser/impl/JacksonParser.java
new file mode 100644
index 000000000..f1a38eca7
--- /dev/null
+++ b/core-api/src/main/java/com/optimizely/ab/odp/parser/impl/JacksonParser.java
@@ -0,0 +1,66 @@
+/**
+ * Copyright 2022, Optimizely Inc. 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.odp.parser.impl;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.optimizely.ab.odp.parser.ResponseJsonParser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+
+import java.util.List;
+
+public class JacksonParser implements ResponseJsonParser {
+ private static final Logger logger = LoggerFactory.getLogger(JacksonParser.class);
+
+ @Override
+ public List parseQualifiedSegments(String responseJson) {
+ ObjectMapper objectMapper = new ObjectMapper();
+ List parsedSegments = new ArrayList<>();
+ JsonNode root;
+ try {
+ root = objectMapper.readTree(responseJson);
+
+ if (root.has("errors")) {
+ JsonNode errors = root.path("errors");
+ StringBuilder logMessage = new StringBuilder();
+ for (int i = 0; i < errors.size(); i++) {
+ if (i > 0) {
+ logMessage.append(", ");
+ }
+ logMessage.append(errors.get(i).path("message"));
+ }
+ logger.error(logMessage.toString());
+ return null;
+ }
+
+ JsonNode edges = root.path("data").path("customer").path("audiences").path("edges");
+ for (JsonNode edgeNode : edges) {
+ String state = edgeNode.path("node").path("state").asText();
+ if (state.equals("qualified")) {
+ parsedSegments.add(edgeNode.path("node").path("name").asText());
+ }
+ }
+ return parsedSegments;
+ } catch (JsonProcessingException e) {
+ logger.error("Error parsing qualified segments from response", e);
+ return null;
+ }
+ }
+}
diff --git a/core-api/src/main/java/com/optimizely/ab/odp/parser/impl/JsonParser.java b/core-api/src/main/java/com/optimizely/ab/odp/parser/impl/JsonParser.java
new file mode 100644
index 000000000..fcae748a4
--- /dev/null
+++ b/core-api/src/main/java/com/optimizely/ab/odp/parser/impl/JsonParser.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright 2022, Optimizely Inc. 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.odp.parser.impl;
+
+import com.optimizely.ab.odp.parser.ResponseJsonParser;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class JsonParser implements ResponseJsonParser {
+ private static final Logger logger = LoggerFactory.getLogger(JsonParser.class);
+
+ @Override
+ public List parseQualifiedSegments(String responseJson) {
+ List parsedSegments = new ArrayList<>();
+ try {
+ JSONObject root = new JSONObject(responseJson);
+
+ if (root.has("errors")) {
+ JSONArray errors = root.getJSONArray("errors");
+ StringBuilder logMessage = new StringBuilder();
+ for (int i = 0; i < errors.length(); i++) {
+ if (i > 0) {
+ logMessage.append(", ");
+ }
+ logMessage.append(errors.getJSONObject(i).getString("message"));
+ }
+ logger.error(logMessage.toString());
+ return null;
+ }
+
+ JSONArray edges = root.getJSONObject("data").getJSONObject("customer").getJSONObject("audiences").getJSONArray("edges");
+ for (int i = 0; i < edges.length(); i++) {
+ JSONObject node = edges.getJSONObject(i).getJSONObject("node");
+ if (node.has("state") && node.getString("state").equals("qualified")) {
+ parsedSegments.add(node.getString("name"));
+ }
+ }
+ return parsedSegments;
+ } catch (JSONException e) {
+ logger.error("Error parsing qualified segments from response", e);
+ return null;
+ }
+ }
+}
diff --git a/core-api/src/main/java/com/optimizely/ab/odp/parser/impl/JsonSimpleParser.java b/core-api/src/main/java/com/optimizely/ab/odp/parser/impl/JsonSimpleParser.java
new file mode 100644
index 000000000..1bee81b0a
--- /dev/null
+++ b/core-api/src/main/java/com/optimizely/ab/odp/parser/impl/JsonSimpleParser.java
@@ -0,0 +1,66 @@
+/**
+ * Copyright 2022, Optimizely Inc. 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.odp.parser.impl;
+
+import com.optimizely.ab.odp.parser.ResponseJsonParser;
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.parser.JSONParser;
+import org.json.simple.parser.ParseException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class JsonSimpleParser implements ResponseJsonParser {
+ private static final Logger logger = LoggerFactory.getLogger(JsonSimpleParser.class);
+
+ @Override
+ public List parseQualifiedSegments(String responseJson) {
+ List parsedSegments = new ArrayList<>();
+ JSONParser parser = new JSONParser();
+ JSONObject root = null;
+ try {
+ root = (JSONObject) parser.parse(responseJson);
+
+ if (root.containsKey("errors")) {
+ JSONArray errors = (JSONArray) root.get("errors");
+ StringBuilder logMessage = new StringBuilder();
+ for (int i = 0; i < errors.size(); i++) {
+ if (i > 0) {
+ logMessage.append(", ");
+ }
+ logMessage.append((String)((JSONObject) errors.get(i)).get("message"));
+ }
+ logger.error(logMessage.toString());
+ return null;
+ }
+
+ JSONArray edges = (JSONArray)((JSONObject)((JSONObject)(((JSONObject) root.get("data"))).get("customer")).get("audiences")).get("edges");
+ for (int i = 0; i < edges.size(); i++) {
+ JSONObject node = (JSONObject) ((JSONObject) edges.get(i)).get("node");
+ if (node.containsKey("state") && (node.get("state")).equals("qualified")) {
+ parsedSegments.add((String) node.get("name"));
+ }
+ }
+ return parsedSegments;
+ } catch (ParseException | NullPointerException e) {
+ logger.error("Error parsing qualified segments from response", e);
+ return null;
+ }
+ }
+}
diff --git a/core-api/src/test/java/com/optimizely/ab/internal/JsonParserProviderTest.java b/core-api/src/test/java/com/optimizely/ab/internal/JsonParserProviderTest.java
new file mode 100644
index 000000000..a65e9b6f5
--- /dev/null
+++ b/core-api/src/test/java/com/optimizely/ab/internal/JsonParserProviderTest.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright 2022, Optimizely Inc. 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.internal;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class JsonParserProviderTest {
+ @Before
+ @After
+ public void clearParserSystemProperty() {
+ PropertyUtils.clear("default_parser");
+ }
+
+ @Test
+ public void getGsonParserProviderWhenNoDefaultIsSet() {
+ assertEquals(JsonParserProvider.GSON_CONFIG_PARSER, JsonParserProvider.getDefaultParser());
+ }
+
+ @Test
+ public void getCorrectParserProviderWhenValidDefaultIsProvided() {
+ PropertyUtils.set("default_parser", "JSON_CONFIG_PARSER");
+ assertEquals(JsonParserProvider.JSON_CONFIG_PARSER, JsonParserProvider.getDefaultParser());
+ }
+
+ @Test
+ public void getGsonParserWhenProvidedDefaultParserDoesNotExist() {
+ PropertyUtils.set("default_parser", "GARBAGE_VALUE");
+ assertEquals(JsonParserProvider.GSON_CONFIG_PARSER, JsonParserProvider.getDefaultParser());
+ }
+}
diff --git a/core-api/src/test/java/com/optimizely/ab/odp/parser/ResponseJsonParserFactoryTest.java b/core-api/src/test/java/com/optimizely/ab/odp/parser/ResponseJsonParserFactoryTest.java
new file mode 100644
index 000000000..f5d8e8c89
--- /dev/null
+++ b/core-api/src/test/java/com/optimizely/ab/odp/parser/ResponseJsonParserFactoryTest.java
@@ -0,0 +1,51 @@
+/**
+ * Copyright 2022, Optimizely Inc. 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.odp.parser;
+
+import com.optimizely.ab.internal.JsonParserProvider;
+import com.optimizely.ab.internal.PropertyUtils;
+import com.optimizely.ab.odp.parser.impl.GsonParser;
+import com.optimizely.ab.odp.parser.impl.JsonParser;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class ResponseJsonParserFactoryTest {
+ @Before
+ @After
+ public void clearParserSystemProperty() {
+ PropertyUtils.clear("default_parser");
+ }
+
+ @Test
+ public void getGsonParserWhenNoDefaultIsSet() {
+ assertEquals(GsonParser.class, ResponseJsonParserFactory.getParser().getClass());
+ }
+
+ @Test
+ public void getCorrectParserWhenValidDefaultIsProvided() {
+ PropertyUtils.set("default_parser", "JSON_CONFIG_PARSER");
+ assertEquals(JsonParser.class, ResponseJsonParserFactory.getParser().getClass());
+ }
+
+ @Test
+ public void getGsonParserWhenGivenDefaultParserDoesNotExist() {
+ PropertyUtils.set("default_parser", "GARBAGE_VALUE");
+ assertEquals(GsonParser.class, ResponseJsonParserFactory.getParser().getClass());
+ }
+}
diff --git a/core-api/src/test/java/com/optimizely/ab/odp/parser/ResponseJsonParserTest.java b/core-api/src/test/java/com/optimizely/ab/odp/parser/ResponseJsonParserTest.java
new file mode 100644
index 000000000..1acd5cf15
--- /dev/null
+++ b/core-api/src/test/java/com/optimizely/ab/odp/parser/ResponseJsonParserTest.java
@@ -0,0 +1,92 @@
+/**
+ * Copyright 2022, Optimizely Inc. 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.odp.parser;
+
+import ch.qos.logback.classic.Level;
+import com.optimizely.ab.internal.LogbackVerifier;
+import static junit.framework.TestCase.assertEquals;
+
+import com.optimizely.ab.odp.parser.impl.*;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(Parameterized.class)
+public class ResponseJsonParserTest {
+ private final ResponseJsonParser jsonParser;
+
+ @Rule
+ public LogbackVerifier logbackVerifier = new LogbackVerifier();
+
+ public ResponseJsonParserTest(ResponseJsonParser jsonParser) {
+ super();
+ this.jsonParser = jsonParser;
+ }
+
+ @Parameterized.Parameters
+ public static List input() {
+ return Arrays.asList(new GsonParser(), new JsonParser(), new JsonSimpleParser(), new JacksonParser());
+ }
+
+ @Test
+ public void returnSegmentsListWhenResponseIsCorrect() {
+ String responseToParse = "{\"data\":{\"customer\":{\"audiences\":{\"edges\":[{\"node\":{\"name\":\"has_email\",\"state\":\"qualified\"}},{\"node\":{\"name\":\"has_email_opted_in\",\"state\":\"qualified\"}}]}}}}";
+ List parsedSegments = jsonParser.parseQualifiedSegments(responseToParse);
+ assertEquals(Arrays.asList("has_email", "has_email_opted_in"), parsedSegments);
+ }
+
+ @Test
+ public void excludeSegmentsWhenStateNotQualified() {
+ String responseToParse = "{\"data\":{\"customer\":{\"audiences\":{\"edges\":[{\"node\":{\"name\":\"has_email\",\"state\":\"qualified\"}},{\"node\":{\"name\":\"has_email_opted_in\",\"state\":\"not_qualified\"}}]}}}}";
+ List parsedSegments = jsonParser.parseQualifiedSegments(responseToParse);
+ assertEquals(Arrays.asList("has_email"), parsedSegments);
+ }
+
+ @Test
+ public void returnEmptyListWhenResponseHasEmptyArray() {
+ String responseToParse = "{\"data\":{\"customer\":{\"audiences\":{\"edges\":[]}}}}";
+ List parsedSegments = jsonParser.parseQualifiedSegments(responseToParse);
+ assertEquals(Arrays.asList(), parsedSegments);
+ }
+
+ @Test
+ public void returnNullWhenJsonFormatIsValidButUnexpectedData() {
+ String responseToParse = "{\"data\"\"consumer\":{\"randomKey\":{\"edges\":[{\"node\":{\"name\":\"has_email\",\"state\":\"qualified\"}},{\"node\":{\"name\":\"has_email_opted_in\",\"state\":\"qualified\"}}]}}}}";
+ List parsedSegments = jsonParser.parseQualifiedSegments(responseToParse);
+ logbackVerifier.expectMessage(Level.ERROR, "Error parsing qualified segments from response");
+ assertEquals(null, parsedSegments);
+ }
+
+ @Test
+ public void returnNullWhenJsonIsMalformed() {
+ String responseToParse = "{\"data\"\"customer\":{\"audiences\":{\"edges\":[{\"node\":{\"name\":\"has_email\",\"state\":\"qualified\"}},{\"node\":{\"name\":\"has_email_opted_in\",\"state\":\"qualified\"}}]}}}}";
+ List parsedSegments = jsonParser.parseQualifiedSegments(responseToParse);
+ logbackVerifier.expectMessage(Level.ERROR, "Error parsing qualified segments from response");
+ assertEquals(null, parsedSegments);
+ }
+
+ @Test
+ public void returnNullAndLogCorrectErrorWhenErrorResponseIsReturned() {
+ String responseToParse = "{\"errors\":[{\"message\":\"Exception while fetching data (/customer) : java.lang.RuntimeException: could not resolve _fs_user_id = wrong_id\",\"locations\":[{\"line\":2,\"column\":3}],\"path\":[\"customer\"],\"extensions\":{\"classification\":\"InvalidIdentifierException\"}}],\"data\":{\"customer\":null}}";
+ List parsedSegments = jsonParser.parseQualifiedSegments(responseToParse);
+ logbackVerifier.expectMessage(Level.ERROR, "Exception while fetching data (/customer) : java.lang.RuntimeException: could not resolve _fs_user_id = wrong_id");
+ assertEquals(null, parsedSegments);
+ }
+}
diff --git a/core-httpclient-impl/src/main/java/com/optimizely/ab/odp/DefaultODPApiManager.java b/core-httpclient-impl/src/main/java/com/optimizely/ab/odp/DefaultODPApiManager.java
new file mode 100644
index 000000000..f0703dcb0
--- /dev/null
+++ b/core-httpclient-impl/src/main/java/com/optimizely/ab/odp/DefaultODPApiManager.java
@@ -0,0 +1,179 @@
+/**
+ * Copyright 2022, Optimizely Inc. 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.odp;
+
+import com.optimizely.ab.OptimizelyHttpClient;
+import com.optimizely.ab.annotations.VisibleForTesting;
+import org.apache.http.HttpStatus;
+import org.apache.http.StatusLine;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.util.EntityUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.List;
+
+public class DefaultODPApiManager implements ODPApiManager {
+ private static final Logger logger = LoggerFactory.getLogger(DefaultODPApiManager.class);
+
+ private final OptimizelyHttpClient httpClient;
+
+ public DefaultODPApiManager() {
+ this(OptimizelyHttpClient.builder().build());
+ }
+
+ @VisibleForTesting
+ DefaultODPApiManager(OptimizelyHttpClient httpClient) {
+ this.httpClient = httpClient;
+ }
+
+ @VisibleForTesting
+ String getSegmentsStringForRequest(List segmentsList) {
+ StringBuilder segmentsString = new StringBuilder();
+ for (int i = 0; i < segmentsList.size(); i++) {
+ if (i > 0) {
+ segmentsString.append(", ");
+ }
+ segmentsString.append("\\\"").append(segmentsList.get(i)).append("\\\"");
+ }
+ return segmentsString.toString();
+ }
+
+ // ODP GraphQL API
+ // - https://api.zaius.com/v3/graphql
+ // - test ODP public API key = "W4WzcEs-ABgXorzY7h1LCQ"
+ /*
+
+ [GraphQL Request]
+
+ // fetch info with fs_user_id for ["has_email", "has_email_opted_in", "push_on_sale"] segments
+ curl -i -H 'Content-Type: application/json' -H 'x-api-key: W4WzcEs-ABgXorzY7h1LCQ' -X POST -d '{"query":"query {customer(fs_user_id: \"tester-101\") {audiences(subset:[\"has_email\",\"has_email_opted_in\",\"push_on_sale\"]) {edges {node {name state}}}}}"}' https://api.zaius.com/v3/graphql
+ // fetch info with vuid for ["has_email", "has_email_opted_in", "push_on_sale"] segments
+ curl -i -H 'Content-Type: application/json' -H 'x-api-key: W4WzcEs-ABgXorzY7h1LCQ' -X POST -d '{"query":"query {customer(vuid: \"d66a9d81923d4d2f99d8f64338976322\") {audiences(subset:[\"has_email\",\"has_email_opted_in\",\"push_on_sale\"]) {edges {node {name state}}}}}"}' https://api.zaius.com/v3/graphql
+ query MyQuery {
+ customer(vuid: "d66a9d81923d4d2f99d8f64338976322") {
+ audiences(subset:["has_email","has_email_opted_in","push_on_sale"]) {
+ edges {
+ node {
+ name
+ state
+ }
+ }
+ }
+ }
+ }
+ [GraphQL Response]
+
+ {
+ "data": {
+ "customer": {
+ "audiences": {
+ "edges": [
+ {
+ "node": {
+ "name": "has_email",
+ "state": "qualified",
+ }
+ },
+ {
+ "node": {
+ "name": "has_email_opted_in",
+ "state": "qualified",
+ }
+ },
+ ...
+ ]
+ }
+ }
+ }
+ }
+
+ [GraphQL Error Response]
+ {
+ "errors": [
+ {
+ "message": "Exception while fetching data (/customer) : java.lang.RuntimeException: could not resolve _fs_user_id = asdsdaddddd",
+ "locations": [
+ {
+ "line": 2,
+ "column": 3
+ }
+ ],
+ "path": [
+ "customer"
+ ],
+ "extensions": {
+ "classification": "InvalidIdentifierException"
+ }
+ }
+ ],
+ "data": {
+ "customer": null
+ }
+ }
+ */
+ @Override
+ public String fetchQualifiedSegments(String apiKey, String apiEndpoint, String userKey, String userValue, List segmentsToCheck) {
+ HttpPost request = new HttpPost(apiEndpoint);
+ String segmentsString = getSegmentsStringForRequest(segmentsToCheck);
+ String requestPayload = String.format("{\"query\": \"query {customer(%s: \\\"%s\\\") {audiences(subset: [%s]) {edges {node {name state}}}}}\"}", userKey, userValue, segmentsString);
+ try {
+ request.setEntity(new StringEntity(requestPayload));
+ } catch (UnsupportedEncodingException e) {
+ logger.warn("Error encoding request payload", e);
+ }
+ request.setHeader("x-api-key", apiKey);
+ request.setHeader("content-type", "application/json");
+
+ CloseableHttpResponse response = null;
+ try {
+ response = httpClient.execute(request);
+ } catch (IOException e) {
+ logger.error("Error retrieving response from ODP service", e);
+ return null;
+ }
+
+ if (response.getStatusLine().getStatusCode() >= 400) {
+ StatusLine statusLine = response.getStatusLine();
+ logger.error(String.format("Unexpected response from ODP server, Response code: %d, %s", statusLine.getStatusCode(), statusLine.getReasonPhrase()));
+ closeHttpResponse(response);
+ return null;
+ }
+
+ try {
+ return EntityUtils.toString(response.getEntity());
+ } catch (IOException e) {
+ logger.error("Error converting ODP segments response to string", e);
+ } finally {
+ closeHttpResponse(response);
+ }
+ return null;
+ }
+
+ private static void closeHttpResponse(CloseableHttpResponse response) {
+ if (response != null) {
+ try {
+ response.close();
+ } catch (IOException e) {
+ logger.warn(e.getLocalizedMessage());
+ }
+ }
+ }
+}
diff --git a/core-httpclient-impl/src/test/java/com/optimizely/ab/internal/LogbackVerifier.java b/core-httpclient-impl/src/test/java/com/optimizely/ab/internal/LogbackVerifier.java
new file mode 100644
index 000000000..25154e97d
--- /dev/null
+++ b/core-httpclient-impl/src/test/java/com/optimizely/ab/internal/LogbackVerifier.java
@@ -0,0 +1,168 @@
+/**
+ * Copyright 2022, Optimizely Inc. 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.internal;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.classic.spi.IThrowableProxy;
+import ch.qos.logback.core.AppenderBase;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+import org.slf4j.LoggerFactory;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+
+import static org.junit.Assert.fail;
+
+/**
+ * TODO As a usability improvement we should require expected messages be added after the message are expected to be
+ * logged. This will allow us to map the failure immediately back to the test line number as opposed to the async
+ * validation now that happens at the end of each individual test.
+ *
+ * From http://techblog.kenshoo.com/2013/08/junit-rule-for-verifying-logback-logging.html
+ */
+public class LogbackVerifier implements TestRule {
+
+ private List expectedEvents = new LinkedList();
+
+ private CaptureAppender appender;
+
+ @Override
+ public Statement apply(final Statement base, Description description) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ before();
+ try {
+ base.evaluate();
+ verify();
+ } finally {
+ after();
+ }
+ }
+ };
+ }
+
+ public void expectMessage(Level level) {
+ expectMessage(level, "");
+ }
+
+ public void expectMessage(Level level, String msg) {
+ expectMessage(level, msg, (Class extends Throwable>) null);
+ }
+
+ public void expectMessage(Level level, String msg, Class extends Throwable> throwableClass) {
+ expectMessage(level, msg, null, 1);
+ }
+
+ public void expectMessage(Level level, String msg, int times) {
+ expectMessage(level, msg, null, times);
+ }
+
+ public void expectMessage(Level level,
+ String msg,
+ Class extends Throwable> throwableClass,
+ int times) {
+ for (int i = 0; i < times; i++) {
+ expectedEvents.add(new ExpectedLogEvent(level, msg, throwableClass));
+ }
+ }
+
+ private void before() {
+ appender = new CaptureAppender();
+ appender.setName("MOCK");
+ appender.start();
+ ((Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME)).addAppender(appender);
+ }
+
+ private void verify() throws Throwable {
+ ListIterator actualIterator = appender.getEvents().listIterator();
+
+ for (final ExpectedLogEvent expectedEvent : expectedEvents) {
+ boolean found = false;
+ while (actualIterator.hasNext()) {
+ ILoggingEvent actual = actualIterator.next();
+
+ if (expectedEvent.matches(actual)) {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ fail(expectedEvent.toString());
+ }
+ }
+ }
+
+ private void after() {
+ ((Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME)).detachAppender(appender);
+ }
+
+ private static class CaptureAppender extends AppenderBase {
+
+ List actualLoggingEvent = new LinkedList<>();
+
+ @Override
+ protected void append(ILoggingEvent eventObject) {
+ actualLoggingEvent.add(eventObject);
+ }
+
+ public List getEvents() {
+ return actualLoggingEvent;
+ }
+ }
+
+ private final static class ExpectedLogEvent {
+ private final String message;
+ private final Level level;
+ private final Class extends Throwable> throwableClass;
+
+ private ExpectedLogEvent(Level level,
+ String message,
+ Class extends Throwable> throwableClass) {
+ this.message = message;
+ this.level = level;
+ this.throwableClass = throwableClass;
+ }
+
+ private boolean matches(ILoggingEvent actual) {
+ boolean match = actual.getFormattedMessage().contains(message);
+ match &= actual.getLevel().equals(level);
+ match &= matchThrowables(actual);
+ return match;
+ }
+
+ private boolean matchThrowables(ILoggingEvent actual) {
+ IThrowableProxy eventProxy = actual.getThrowableProxy();
+ return throwableClass == null || eventProxy != null && throwableClass.getName().equals(eventProxy.getClassName());
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("ExpectedLogEvent{");
+ sb.append("level=").append(level);
+ sb.append(", message='").append(message).append('\'');
+ sb.append(", throwableClass=").append(throwableClass);
+ sb.append('}');
+ return sb.toString();
+ }
+ }
+}
diff --git a/core-httpclient-impl/src/test/java/com/optimizely/ab/odp/DefaultODPApiManagerTest.java b/core-httpclient-impl/src/test/java/com/optimizely/ab/odp/DefaultODPApiManagerTest.java
new file mode 100644
index 000000000..c81af2dce
--- /dev/null
+++ b/core-httpclient-impl/src/test/java/com/optimizely/ab/odp/DefaultODPApiManagerTest.java
@@ -0,0 +1,116 @@
+/**
+ * Copyright 2022, Optimizely Inc. 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.odp;
+
+import ch.qos.logback.classic.Level;
+import com.optimizely.ab.OptimizelyHttpClient;
+import com.optimizely.ab.internal.LogbackVerifier;
+import org.apache.http.StatusLine;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.util.EntityUtils;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import static org.junit.Assert.*;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.*;
+
+public class DefaultODPApiManagerTest {
+ private static final String validResponse = "{\"data\":{\"customer\":{\"audiences\":{\"edges\":[{\"node\":{\"name\":\"has_email\",\"state\":\"qualified\"}},{\"node\":{\"name\":\"has_email_opted_in\",\"state\":\"qualified\"}}]}}}}";
+
+ @Rule
+ public LogbackVerifier logbackVerifier = new LogbackVerifier();
+
+ OptimizelyHttpClient mockHttpClient;
+
+ @Before
+ public void setUp() throws Exception {
+ setupHttpClient(200);
+ }
+
+ private void setupHttpClient(int statusCode) throws Exception {
+ mockHttpClient = mock(OptimizelyHttpClient.class);
+ CloseableHttpResponse httpResponse = mock(CloseableHttpResponse.class);
+ StatusLine statusLine = mock(StatusLine.class);
+
+ when(statusLine.getStatusCode()).thenReturn(statusCode);
+ when(httpResponse.getStatusLine()).thenReturn(statusLine);
+ when(httpResponse.getEntity()).thenReturn(new StringEntity(validResponse));
+
+ when(mockHttpClient.execute(any(HttpPost.class)))
+ .thenReturn(httpResponse);
+ }
+
+ @Test
+ public void generateCorrectSegmentsStringWhenListHasOneItem() {
+ DefaultODPApiManager apiManager = new DefaultODPApiManager();
+ String expected = "\\\"only_segment\\\"";
+ String actual = apiManager.getSegmentsStringForRequest(Arrays.asList("only_segment"));
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void generateCorrectSegmentsStringWhenListHasMultipleItems() {
+ DefaultODPApiManager apiManager = new DefaultODPApiManager();
+ String expected = "\\\"segment_1\\\", \\\"segment_2\\\", \\\"segment_3\\\"";
+ String actual = apiManager.getSegmentsStringForRequest(Arrays.asList("segment_1", "segment_2", "segment_3"));
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void generateEmptyStringWhenGivenListIsEmpty() {
+ DefaultODPApiManager apiManager = new DefaultODPApiManager();
+ String actual = apiManager.getSegmentsStringForRequest(new ArrayList<>());
+ assertEquals("", actual);
+ }
+
+ @Test
+ public void generateCorrectRequestBody() throws Exception {
+ ODPApiManager apiManager = new DefaultODPApiManager(mockHttpClient);
+ apiManager.fetchQualifiedSegments("key", "endPoint", "fs_user_id", "test_user", Arrays.asList("segment_1", "segment_2"));
+ verify(mockHttpClient, times(1)).execute(any(HttpPost.class));
+
+ String expectedResponse = "{\"query\": \"query {customer(fs_user_id: \\\"test_user\\\") {audiences(subset: [\\\"segment_1\\\", \\\"segment_2\\\"]) {edges {node {name state}}}}}\"}";
+ ArgumentCaptor request = ArgumentCaptor.forClass(HttpPost.class);
+ verify(mockHttpClient).execute(request.capture());
+ assertEquals(expectedResponse, EntityUtils.toString(request.getValue().getEntity()));
+ }
+
+ @Test
+ public void returnResponseStringWhenStatusIs200() throws Exception {
+ ODPApiManager apiManager = new DefaultODPApiManager(mockHttpClient);
+ String responseString = apiManager.fetchQualifiedSegments("key", "endPoint", "fs_user_id", "test_user", Arrays.asList("segment_1", "segment_2"));
+ verify(mockHttpClient, times(1)).execute(any(HttpPost.class));
+ assertEquals(validResponse, responseString);
+ }
+
+ @Test
+ public void returnNullWhenStatusIsNot200AndLogError() throws Exception {
+ setupHttpClient(500);
+ ODPApiManager apiManager = new DefaultODPApiManager(mockHttpClient);
+ String responseString = apiManager.fetchQualifiedSegments("key", "endPoint", "fs_user_id", "test_user", Arrays.asList("segment_1", "segment_2"));
+ verify(mockHttpClient, times(1)).execute(any(HttpPost.class));
+ logbackVerifier.expectMessage(Level.ERROR, "Unexpected response from ODP server, Response code: 500, null");
+ assertEquals(null, responseString);
+ }
+}