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 51f5dadbd..bbaec926e 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -725,27 +725,6 @@ public boolean setForcedVariation(@Nonnull String experimentKey, return projectConfig; } - /** - * @return a {@link ProjectConfig} instance given a json string - */ - private static ProjectConfig getProjectConfig(String datafile) throws ConfigParseException { - if (datafile == null) { - throw new ConfigParseException("Unable to parse null datafile."); - } - if (datafile.length() == 0) { - throw new ConfigParseException("Unable to parse empty datafile."); - } - - ProjectConfig projectConfig = DefaultConfigParser.getInstance().parseProjectConfig(datafile); - - if (projectConfig.getVersion().equals("1")) { - throw new ConfigParseException("This version of the Java SDK does not support version 1 datafiles. " + - "Please use a version 2 or 3 datafile with this SDK."); - } - - return projectConfig; - } - @Nullable public UserProfileService getUserProfileService() { return userProfileService; @@ -891,7 +870,9 @@ protected Builder withConfig(ProjectConfig projectConfig) { public Optimizely build() throws ConfigParseException { if (projectConfig == null) { - projectConfig = Optimizely.getProjectConfig(datafile); + projectConfig = new ProjectConfig.Builder() + .withDatafile(datafile) + .build(); } if (bucketer == null) { diff --git a/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java b/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java index 8a81af72f..086f2d6b5 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java +++ b/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java @@ -21,10 +21,11 @@ import com.optimizely.ab.UnknownExperimentException; import com.optimizely.ab.config.audience.Audience; import com.optimizely.ab.config.audience.Condition; +import com.optimizely.ab.config.parser.ConfigParseException; +import com.optimizely.ab.config.parser.DefaultConfigParser; import com.optimizely.ab.error.ErrorHandler; import com.optimizely.ab.error.NoOpErrorHandler; import com.optimizely.ab.error.RaiseExceptionErrorHandler; -import com.optimizely.ab.internal.ControlAttribute; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,11 +33,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** @@ -65,6 +62,12 @@ public String toString() { } } + private static final List supportedVersions = Arrays.asList( + Version.V2.version, + Version.V3.version, + Version.V4.version + ); + // logger private static final Logger logger = LoggerFactory.getLogger(ProjectConfig.class); @@ -599,4 +602,33 @@ public String toString() { ", variationIdToExperimentMapping=" + variationIdToExperimentMapping + '}'; } + + public static class Builder { + private String datafile; + + public Builder withDatafile(String datafile) { + this.datafile = datafile; + return this; + } + + /** + * @return a {@link ProjectConfig} instance given a JSON string datafile + */ + public ProjectConfig build() throws ConfigParseException{ + if (datafile == null) { + throw new ConfigParseException("Unable to parse null datafile."); + } + if (datafile.isEmpty()) { + throw new ConfigParseException("Unable to parse empty datafile."); + } + + ProjectConfig projectConfig = DefaultConfigParser.getInstance().parseProjectConfig(datafile); + + if (!supportedVersions.contains(projectConfig.getVersion())) { + throw new ConfigParseException("This version of the Java SDK does not support the given datafile version: " + projectConfig.getVersion()); + } + + return projectConfig; + } + } } diff --git a/core-api/src/test/java/com/optimizely/ab/config/ProjectConfigBuilderTest.java b/core-api/src/test/java/com/optimizely/ab/config/ProjectConfigBuilderTest.java new file mode 100644 index 000000000..b1d849981 --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/config/ProjectConfigBuilderTest.java @@ -0,0 +1,53 @@ +package com.optimizely.ab.config; + +import com.optimizely.ab.config.parser.ConfigParseException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static com.optimizely.ab.config.ProjectConfigTestUtils.invalidProjectConfigV5; +import static com.optimizely.ab.config.ProjectConfigTestUtils.validConfigJsonV4; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +/** + * Tests for {@link com.optimizely.ab.config.ProjectConfig.Builder}. + */ +public class ProjectConfigBuilderTest { + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void withNullDatafile() throws Exception { + thrown.expect(ConfigParseException.class); + new ProjectConfig.Builder() + .withDatafile(null) + .build(); + } + + @Test + public void withEmptyDatafile() throws Exception { + thrown.expect(ConfigParseException.class); + new ProjectConfig.Builder() + .withDatafile("") + .build(); + } + + @Test + public void withValidDatafile() throws Exception { + ProjectConfig projectConfig = new ProjectConfig.Builder() + .withDatafile(validConfigJsonV4()) + .build(); + assertNotNull(projectConfig); + assertEquals("4", projectConfig.getVersion()); + } + + @Test + public void withUnsupportedDatafile() throws Exception { + thrown.expect(ConfigParseException.class); + new ProjectConfig.Builder() + .withDatafile(invalidProjectConfigV5()) + .build(); + } +} diff --git a/core-api/src/test/java/com/optimizely/ab/config/ProjectConfigTestUtils.java b/core-api/src/test/java/com/optimizely/ab/config/ProjectConfigTestUtils.java index c072d79ee..a731830ac 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/ProjectConfigTestUtils.java +++ b/core-api/src/test/java/com/optimizely/ab/config/ProjectConfigTestUtils.java @@ -438,6 +438,13 @@ public static ProjectConfig validProjectConfigV4() { return VALID_PROJECT_CONFIG_V4; } + /** + * @return the expected {@link ProjectConfig} for the json produced by {@link #invalidProjectConfigV5()} + */ + public static String invalidProjectConfigV5() throws IOException { + return Resources.toString(Resources.getResource("config/invalid-project-config-v5.json"), Charsets.UTF_8); + } + /** * Asserts that the provided project configs are equivalent. */ diff --git a/core-api/src/test/resources/config/invalid-project-config-v5.json b/core-api/src/test/resources/config/invalid-project-config-v5.json new file mode 100644 index 000000000..a3cc668f9 --- /dev/null +++ b/core-api/src/test/resources/config/invalid-project-config-v5.json @@ -0,0 +1,129 @@ +{ + "accountId": "789", + "projectId": "1234", + "version": "5", + "revision": "42", + "experiments": [ + { + "id": "223", + "key": "etag1", + "status": "Running", + "layerId": "1", + "percentageIncluded": 9000, + "audienceIds": [], + "variations": [{ + "id": "276", + "key": "vtag1", + "variables": [] + }, { + "id": "277", + "key": "vtag2", + "variables": [] + }], + "forcedVariations": { + "testUser1": "vtag1", + "testUser2": "vtag2" + }, + "trafficAllocation": [{ + "entityId": "276", + "endOfRange": 3500 + }, { + "entityId": "277", + "endOfRange": 9000 + }] + }, + { + "id": "118", + "key": "etag2", + "status": "Not started", + "layerId": "2", + "audienceIds": [], + "variations": [{ + "id": "278", + "key": "vtag3", + "variables": [] + }, { + "id": "279", + "key": "vtag4", + "variables": [] + }], + "forcedVariations": {}, + "trafficAllocation": [{ + "entityId": "278", + "endOfRange": 4500 + }, { + "entityId": "279", + "endOfRange": 9000 + }] + }, + { + "id": "119", + "key": "etag3", + "status": "Launched", + "layerId": "3", + "audienceIds": [], + "variations": [{ + "id": "280", + "key": "vtag5" + }, { + "id": "281", + "key": "vtag6" + }], + "forcedVariations": {}, + "trafficAllocation": [{ + "entityId": "280", + "endOfRange": 5000 + }, { + "entityId": "281", + "endOfRange": 10000 + }] + } + ], + "groups": [], + "audiences": [], + "attributes": [ + { + "id": "134", + "key": "browser_type" + } + ], + "events": [ + { + "id": "971", + "key": "clicked_cart", + "experimentIds": [ + "223" + ] + }, + { + "id": "098", + "key": "Total Revenue", + "experimentIds": [ + "223" + ] + }, + { + "id": "099", + "key": "clicked_purchase", + "experimentIds": [ + "118", + "223" + ] + }, + { + "id": "100", + "key": "launched_exp_event", + "experimentIds": [ + "119" + ] + }, + { + "id": "101", + "key": "event_with_launched_and_running_experiments", + "experimentIds": [ + "119", + "223" + ] + } + ] +} \ No newline at end of file