diff --git a/CHANGELOG.md b/CHANGELOG.md index 4caeda474..c2dd8e2d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,6 +87,12 @@ This is a major rewrite that introduces a cleaner API design, adds new features, - The Redis integration is no longer built into the main SDK library. See: https://github.com/launchdarkly/java-server-sdk-redis - The deprecated New Relic integration has been removed. +## [4.14.4] - 2020-09-28 +### Fixed: +- Restored compatibility with Java 7. A transitive dependency that required Java 8 had accidentally been included, and the CI build did not detect this because the tests were being run in Java 8 even though the compiler target was 7. CI builds now verify that the SDK really can run in Java 7. This fix is only for 4.x; the 5.x SDK still does not support Java 7. +- Bumped OkHttp version to 3.12.12 to avoid a crash on Java 8u252. +- Removed an obsolete comment that said the `trackMetric` method was not yet supported by the LaunchDarkly service; it is. + ## [4.14.3] - 2020-09-03 ### Fixed: - Bump SnakeYAML from 1.19 to 1.26 to address CVE-2017-18640. The SDK only parses YAML if the application has configured the SDK with a flag data file, so it's unlikely this CVE would affect SDK usage as it would require configuration and access to a local file. diff --git a/build.gradle b/build.gradle index 95a1d43f4..648dce998 100644 --- a/build.gradle +++ b/build.gradle @@ -71,7 +71,7 @@ ext.versions = [ "commonsCodec": "1.10", "gson": "2.7", "guava": "28.2-jre", - "jackson": "2.10.0", + "jackson": "2.11.2", "launchdarklyJavaSdkCommon": "1.0.0", "okhttp": "4.8.1", // specify this for the SDK build instead of relying on the transitive dependency from okhttp-eventsource "okhttpEventsource": "2.3.1", @@ -96,7 +96,9 @@ libraries.internal = [ // Add dependencies to "libraries.external" that are exposed in our public API, or that have // global state that must be shared between the SDK and the caller. libraries.external = [ - "org.slf4j:slf4j-api:${versions.slf4j}" + "org.slf4j:slf4j-api:${versions.slf4j}", + "com.fasterxml.jackson.core:jackson-core:${versions.jackson}", + "com.fasterxml.jackson.core:jackson-databind:${versions.jackson}" ] // Add dependencies to "libraries.test" that are used only in unit tests. @@ -169,6 +171,7 @@ shadowJar { dependencies { exclude(dependency('org.slf4j:.*:.*')) + exclude(dependency('com.fasterxml.jackson.core:.*:.*')) } // Kotlin metadata for shaded classes should not be included - it confuses IDEs @@ -198,12 +201,16 @@ task shadowJarAll(type: com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJ description = "Builds a Shaded fat jar including SLF4J" from(project.convention.getPlugin(JavaPluginConvention).sourceSets.main.output) configurations = [project.configurations.runtimeClasspath] - exclude('META-INF/INDEX.LIST', 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA') + exclude('META-INF/INDEX.LIST', 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA') exclude '**/*.kotlin_metadata' exclude '**/*.kotlin_module' exclude '**/*.kotlin_builtins' + dependencies { + exclude(dependency('com.fasterxml.jackson.core:.*:.*')) + } + // doFirst causes the following steps to be run during Gradle's execution phase rather than the // configuration phase; this is necessary because they access the build products doFirst { @@ -384,7 +391,9 @@ def addOsgiManifest(jarTask, List importConfigs, List/dev/null) + $(shell ls $(TEMP_DIR)/dependencies-external/gson*.jar 2>/dev/null) \ + $(shell ls $(TEMP_DIR)/dependencies-external/jackson*.jar 2>/dev/null) RUN_JARS_test-default-jar=$(TEST_APP_JAR) $(SDK_DEFAULT_JAR) \ $(shell ls $(TEMP_DIR)/dependencies-external/*.jar 2>/dev/null) RUN_JARS_test-thin-jar=$(TEST_APP_JAR) $(SDK_THIN_JAR) \ @@ -44,8 +45,8 @@ RUN_JARS_test-thin-jar=$(TEST_APP_JAR) $(SDK_THIN_JAR) \ $(shell ls $(TEMP_DIR)/dependencies-external/*.jar 2>/dev/null) classes_prepare=echo " checking $(1)..." && jar tf $(1) | grep '\.class$$' >$(TEMP_OUTPUT) -classes_should_contain=echo " should contain $(2)" && grep "^$(1)/[^/]*$$" $(TEMP_OUTPUT) >/dev/null -classes_should_not_contain=echo " should not contain $(2)" && ! grep "^$(1)/[^/]*$$" $(TEMP_OUTPUT) +classes_should_contain=echo " should contain $(2)" && grep "^$(1)/.*\.class$$" $(TEMP_OUTPUT) >/dev/null +classes_should_not_contain=echo " should not contain $(2)" && ! grep "^$(1)/.*\.class$$" $(TEMP_OUTPUT) verify_sdk_classes= \ $(call classes_should_contain,com/launchdarkly/sdk,com.launchdarkly.sdk) && \ @@ -79,6 +80,8 @@ test-all-jar-classes: $(SDK_ALL_JAR) $(TEMP_DIR) @$(call classes_should_not_contain,com/launchdarkly/shaded/com/launchdarkly/sdk,shaded SDK classes) @$(call classes_should_contain,com/launchdarkly/shaded/com/google/gson,shaded Gson) @$(call classes_should_not_contain,com/google/gson,unshaded Gson) + @$(call classes_should_not_contain,com/fasterxml/jackson,unshaded Jackson) + @$(call classes_should_not_contain,com/launchdarkly/shaded/com/fasterxml/jackson,shaded Jackson) @$(call classes_should_not_contain,com/launchdarkly/shaded/org/slf4j,shaded SLF4j) test-default-jar-classes: $(SDK_DEFAULT_JAR) $(TEMP_DIR) @@ -89,6 +92,8 @@ test-default-jar-classes: $(SDK_DEFAULT_JAR) $(TEMP_DIR) @$(call classes_should_contain,com/launchdarkly/shaded/com/google/gson,shaded Gson) @$(call classes_should_not_contain,com/launchdarkly/shaded/org/slf4j,shaded SLF4j) @$(call classes_should_not_contain,com/google/gson,unshaded Gson) + @$(call classes_should_not_contain,com/fasterxml/jackson,unshaded Jackson) + @$(call classes_should_not_contain,com/launchdarkly/shaded/com/fasterxml/jackson,shaded Jackson) @$(call classes_should_not_contain,org/slf4j,unshaded SLF4j) test-thin-jar-classes: $(SDK_THIN_JAR) $(TEMP_DIR) @@ -121,6 +126,7 @@ $(TEMP_DIR)/dependencies-external: $(TEMP_DIR)/dependencies-all [ -d $@ ] || mkdir -p $@ cp $(TEMP_DIR)/dependencies-all/slf4j*.jar $@ cp $(TEMP_DIR)/dependencies-all/gson*.jar $@ + cp $(TEMP_DIR)/dependencies-all/jackson*.jar $@ $(TEMP_DIR)/dependencies-internal: $(TEMP_DIR)/dependencies-all [ -d $@ ] || mkdir -p $@ diff --git a/packaging-test/run-non-osgi-test.sh b/packaging-test/run-non-osgi-test.sh index 49b8d953d..2c1f0cd4d 100755 --- a/packaging-test/run-non-osgi-test.sh +++ b/packaging-test/run-non-osgi-test.sh @@ -1,5 +1,7 @@ #!/bin/bash +set -e + function run_test() { rm -f ${TEMP_OUTPUT} touch ${TEMP_OUTPUT} @@ -8,29 +10,41 @@ function run_test() { grep "TestApp: PASS" ${TEMP_OUTPUT} >/dev/null } -echo "" -echo " non-OSGi runtime test - with Gson" -run_test $@ -grep "LDGson tests OK" ${TEMP_OUTPUT} >/dev/null || (echo "FAIL: should have run LDGson tests but did not" && exit 1) - # It does not make sense to test the "thin" jar without Gson. The SDK uses Gson internally # and can't work without it; in the default jar and the "all" jar, it has its own embedded # copy of Gson, but the "thin" jar does not include any third-party dependencies so you must # provide all of them including Gson. -thin_sdk_regex=".*launchdarkly-java-server-sdk-[^ ]*-thin\\.jar" +echo "" if [[ "$@" =~ $thin_sdk_regex ]]; then - exit 0 + echo " non-OSGi runtime test - without Jackson" + filtered_deps="" + json_jar_regex=".*jackson.*" + for dep in $@; do + if [[ ! "$dep" =~ $json_jar_regex ]]; then + filtered_deps="$filtered_deps $dep" + fi + done + run_test $filtered_deps + grep "skipping LDJackson tests" ${TEMP_OUTPUT} >/dev/null || \ + (echo "FAIL: should have skipped LDJackson tests but did not; test setup was incorrect" && exit 1) +else + echo " non-OSGi runtime test - without Gson or Jackson" + filtered_deps="" + json_jar_regex=".*gson.*|.*jackson.*" + for dep in $@; do + if [[ ! "$dep" =~ $json_jar_regex ]]; then + filtered_deps="$filtered_deps $dep" + fi + done + run_test $filtered_deps + grep "skipping LDGson tests" ${TEMP_OUTPUT} >/dev/null || \ + (echo "FAIL: should have skipped LDGson tests but did not; test setup was incorrect" && exit 1) + grep "skipping LDJackson tests" ${TEMP_OUTPUT} >/dev/null || \ + (echo "FAIL: should have skipped LDJackson tests but did not; test setup was incorrect" && exit 1) fi echo "" -echo " non-OSGi runtime test - without Gson" -deps_except_json="" -json_jar_regex=".*gson.*" -for dep in $@; do - if [[ ! "$dep" =~ $json_jar_regex ]]; then - deps_except_json="$deps_except_json $dep" - fi -done -run_test $deps_except_json -grep "skipping LDGson tests" ${TEMP_OUTPUT} >/dev/null || \ - (echo "FAIL: should have skipped LDGson tests but did not; test setup was incorrect" && exit 1) +echo " non-OSGi runtime test - with Gson and Jackson" +run_test $@ +grep "LDGson tests OK" ${TEMP_OUTPUT} >/dev/null || (echo "FAIL: should have run LDGson tests but did not" && exit 1) +grep "LDJackson tests OK" ${TEMP_OUTPUT} >/dev/null || (echo "FAIL: should have run LDJackson tests but did not" && exit 1) diff --git a/packaging-test/run-osgi-test.sh b/packaging-test/run-osgi-test.sh index df5f69739..22bc52459 100755 --- a/packaging-test/run-osgi-test.sh +++ b/packaging-test/run-osgi-test.sh @@ -1,32 +1,57 @@ #!/bin/bash +set -e + +# This script uses Felix to run the test application as an OSGi bundle, with or without +# additional bundles to support the optional Gson and Jackson integrations. We are +# verifying that the SDK itself works correctly as an OSGi bundle, and also that its +# imports of other bundles work correctly. +# +# This test is being run in CI using the lowest compatible JDK version. It may not work +# in higher JDK versions due to incompatibilities with the version of Felix we are using. + +JAR_DEPS="$@" + # We can't test the "thin" jar in OSGi, because some of our third-party dependencies # aren't available as OSGi bundles. That isn't a plausible use case anyway. thin_sdk_regex=".*launchdarkly-java-server-sdk-[^ ]*-thin\\.jar" -if [[ "$@" =~ $thin_sdk_regex ]]; then +if [[ "${JAR_DEPS}" =~ $thin_sdk_regex ]]; then exit 0 fi rm -rf ${TEMP_BUNDLE_DIR} mkdir -p ${TEMP_BUNDLE_DIR} +function copy_deps() { + if [ -n "${JAR_DEPS}" ]; then + cp ${JAR_DEPS} ${TEMP_BUNDLE_DIR} + fi + cp ${FELIX_BASE_BUNDLE_DIR}/* ${TEMP_BUNDLE_DIR} +} + function run_test() { rm -rf ${FELIX_DIR}/felix-cache rm -f ${TEMP_OUTPUT} touch ${TEMP_OUTPUT} - cd ${FELIX_DIR} && java -jar ${FELIX_JAR} -b ${TEMP_BUNDLE_DIR} | tee ${TEMP_OUTPUT} + cd ${FELIX_DIR} + java -jar ${FELIX_JAR} -b ${TEMP_BUNDLE_DIR} | tee ${TEMP_OUTPUT} grep "TestApp: PASS" ${TEMP_OUTPUT} >/dev/null } echo "" -echo " OSGi runtime test - with Gson" -cp $@ ${FELIX_BASE_BUNDLE_DIR}/* ${TEMP_BUNDLE_DIR} +echo " OSGi runtime test - without Gson or Jackson" +copy_deps +rm ${TEMP_BUNDLE_DIR}/*gson*.jar ${TEMP_BUNDLE_DIR}/*jackson*.jar +ls ${TEMP_BUNDLE_DIR} run_test -grep "LDGson tests OK" ${TEMP_OUTPUT} >/dev/null || (echo "FAIL: should have run LDGson tests but did not" && exit 1) +grep "skipping LDGson tests" ${TEMP_OUTPUT} >/dev/null || \ + (echo "FAIL: should have skipped LDGson tests but did not; test setup was incorrect" && exit 1) +grep "skipping LDJackson tests" ${TEMP_OUTPUT} >/dev/null || \ + (echo "FAIL: should have skipped LDJackson tests but did not; test setup was incorrect" && exit 1) echo "" -echo " OSGi runtime test - without Gson" -rm ${TEMP_BUNDLE_DIR}/*gson*.jar +echo " OSGi runtime test - with Gson and Jackson" +copy_deps run_test -grep "skipping LDGson tests" ${TEMP_OUTPUT} >/dev/null || \ - (echo "FAIL: should have skipped LDGson tests but did not; test setup was incorrect" && exit 1) +grep "LDGson tests OK" ${TEMP_OUTPUT} >/dev/null || (echo "FAIL: should have run LDGson tests but did not" && exit 1) +grep "LDJackson tests OK" ${TEMP_OUTPUT} >/dev/null || (echo "FAIL: should have run LDJackson tests but did not" && exit 1) diff --git a/packaging-test/test-app/build.gradle b/packaging-test/test-app/build.gradle index b00af000f..1d03d1d63 100644 --- a/packaging-test/test-app/build.gradle +++ b/packaging-test/test-app/build.gradle @@ -21,14 +21,21 @@ allprojects { group = "com.launchdarkly" version = "1.0.0" archivesBaseName = 'test-app-bundle' - sourceCompatibility = 1.7 - targetCompatibility = 1.7 + sourceCompatibility = 1.8 + targetCompatibility = 1.8 } +ext.versions = [ + "gson": "2.7", + "jackson": "2.10.0" +] + dependencies { // Note, the SDK build must have already been run before this, since we're using its product as a dependency implementation fileTree(dir: "../../build/libs", include: "launchdarkly-java-server-sdk-*-thin.jar") - implementation "com.google.code.gson:gson:2.7" + implementation "com.fasterxml.jackson.core:jackson-core:${versions.jackson}" + implementation "com.fasterxml.jackson.core:jackson-databind:${versions.jackson}" + implementation "com.google.code.gson:gson:${versions.gson}" implementation "org.slf4j:slf4j-api:1.7.22" implementation "org.osgi:osgi_R4_core:1.0" osgiRuntime "org.slf4j:slf4j-simple:1.7.22" @@ -47,7 +54,8 @@ jar { 'Import-Package': 'com.launchdarkly.sdk,com.launchdarkly.sdk.json' + ',com.launchdarkly.sdk.server,org.slf4j' + ',org.osgi.framework' + - ',com.google.gson;resolution:=optional' + ',com.google.gson;resolution:=optional' + + ',com.fasterxml.jackson.*;resolution:=optional' ) } diff --git a/packaging-test/test-app/src/main/java/testapp/TestApp.java b/packaging-test/test-app/src/main/java/testapp/TestApp.java index 034852cbe..a6d87be37 100644 --- a/packaging-test/test-app/src/main/java/testapp/TestApp.java +++ b/packaging-test/test-app/src/main/java/testapp/TestApp.java @@ -48,6 +48,14 @@ public static void main(String[] args) throws Exception { addError("unexpected error in LDGson tests", e); } + try { + Class.forName("testapp.TestAppJacksonTests"); // see TestAppJacksonTests for why we're loading it in this way + } catch (NoClassDefFoundError e) { + log("skipping LDJackson tests because Jackson is not in the classpath"); + } catch (RuntimeException e) { + addError("unexpected error in LDJackson tests", e); + } + if (errors.isEmpty()) { log("PASS"); } else { diff --git a/packaging-test/test-app/src/main/java/testapp/TestAppJacksonTests.java b/packaging-test/test-app/src/main/java/testapp/TestAppJacksonTests.java new file mode 100644 index 000000000..531f3b15b --- /dev/null +++ b/packaging-test/test-app/src/main/java/testapp/TestAppJacksonTests.java @@ -0,0 +1,43 @@ +package testapp; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.launchdarkly.sdk.*; +import com.launchdarkly.sdk.json.*; + +// This code is in its own class that is loaded dynamically because some of our test scenarios +// involve running TestApp without having Jackson in the classpath, to make sure the SDK does not +// *require* the presence of an external Jackson even though it can interoperate with one. + +public class TestAppJacksonTests { + // Use static block so simply loading this class causes the tests to execute + static { + // First try referencing Jackson, so we fail right away if it's not on the classpath + Class c = ObjectMapper.class; + try { + runJacksonTests(); + } catch (Exception e) { + // If we've even gotten to this static block, then Jackson itself *is* on the application's + // classpath, so this must be some other kind of classloading error that we do want to + // report. For instance, a NoClassDefFound error for Jackson at this point, if we're in + // OSGi, would mean that the SDK bundle is unable to see the external Jackson classes. + TestApp.addError("unexpected error in LDJackson tests", e); + } + } + + public static void runJacksonTests() throws Exception { + ObjectMapper jacksonMapper = new ObjectMapper(); + jacksonMapper.registerModule(LDJackson.module()); + + boolean ok = true; + for (JsonSerializationTestData.TestItem item: JsonSerializationTestData.TEST_ITEMS) { + String actualJson = jacksonMapper.writeValueAsString(item.objectToSerialize); + if (!JsonSerializationTestData.assertJsonEquals(item.expectedJson, actualJson, item.objectToSerialize)) { + ok = false; + } + } + + if (ok) { + TestApp.log("LDJackson tests OK"); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/launchdarkly/sdk/server/DefaultEventSender.java b/src/main/java/com/launchdarkly/sdk/server/DefaultEventSender.java index 03d821998..e61fb795d 100644 --- a/src/main/java/com/launchdarkly/sdk/server/DefaultEventSender.java +++ b/src/main/java/com/launchdarkly/sdk/server/DefaultEventSender.java @@ -16,6 +16,7 @@ import java.util.UUID; import static com.launchdarkly.sdk.server.Util.checkIfErrorIsRecoverableAndLog; +import static com.launchdarkly.sdk.server.Util.concatenateUriPath; import static com.launchdarkly.sdk.server.Util.configureHttpClientBuilder; import static com.launchdarkly.sdk.server.Util.describeDuration; import static com.launchdarkly.sdk.server.Util.getHeadersBuilderFor; @@ -91,7 +92,7 @@ public Result sendEventData(EventDataKind kind, String data, int eventCount, URI throw new IllegalArgumentException("kind"); // COVERAGE: unreachable code, those are the only enum values } - URI uri = eventsBaseUri.resolve(eventsBaseUri.getPath().endsWith("/") ? path : ("/" + path)); + URI uri = concatenateUriPath(eventsBaseUri, path); Headers headers = headersBuilder.build(); RequestBody body = RequestBody.create(data, JSON_CONTENT_TYPE); boolean mustShutDown = false; diff --git a/src/main/java/com/launchdarkly/sdk/server/DefaultFeatureRequestor.java b/src/main/java/com/launchdarkly/sdk/server/DefaultFeatureRequestor.java index 8aff2e66a..c3c7cc4bd 100644 --- a/src/main/java/com/launchdarkly/sdk/server/DefaultFeatureRequestor.java +++ b/src/main/java/com/launchdarkly/sdk/server/DefaultFeatureRequestor.java @@ -11,6 +11,7 @@ import java.nio.file.Files; import java.nio.file.Path; +import static com.launchdarkly.sdk.server.Util.concatenateUriPath; import static com.launchdarkly.sdk.server.Util.configureHttpClientBuilder; import static com.launchdarkly.sdk.server.Util.getHeadersBuilderFor; import static com.launchdarkly.sdk.server.Util.shutdownHttpClient; @@ -26,7 +27,7 @@ */ final class DefaultFeatureRequestor implements FeatureRequestor { private static final Logger logger = Loggers.DATA_SOURCE; - private static final String GET_LATEST_ALL_PATH = "/sdk/latest-all"; + private static final String GET_LATEST_ALL_PATH = "sdk/latest-all"; private static final long MAX_HTTP_CACHE_SIZE_BYTES = 10 * 1024 * 1024; // 10 MB @VisibleForTesting final URI baseUri; @@ -37,7 +38,7 @@ final class DefaultFeatureRequestor implements FeatureRequestor { DefaultFeatureRequestor(HttpConfiguration httpConfig, URI baseUri) { this.baseUri = baseUri; - this.pollingUri = baseUri.resolve(GET_LATEST_ALL_PATH); + this.pollingUri = concatenateUriPath(baseUri, GET_LATEST_ALL_PATH); OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder(); configureHttpClientBuilder(httpConfig, httpBuilder); diff --git a/src/main/java/com/launchdarkly/sdk/server/EventBroadcasterImpl.java b/src/main/java/com/launchdarkly/sdk/server/EventBroadcasterImpl.java index d59aa1e1a..b417dc180 100644 --- a/src/main/java/com/launchdarkly/sdk/server/EventBroadcasterImpl.java +++ b/src/main/java/com/launchdarkly/sdk/server/EventBroadcasterImpl.java @@ -90,7 +90,7 @@ void broadcast(EventT event) { try { broadcastAction.accept(l, event); } catch (Exception e) { - Loggers.MAIN.warn("Unexpected error from listener ({0}): {1}", l.getClass(), e.toString()); + Loggers.MAIN.warn("Unexpected error from listener ({}): {}", l.getClass(), e.toString()); Loggers.MAIN.debug(e.toString(), e); } }); diff --git a/src/main/java/com/launchdarkly/sdk/server/LDClient.java b/src/main/java/com/launchdarkly/sdk/server/LDClient.java index 4ce81ee1c..2f50d0a73 100644 --- a/src/main/java/com/launchdarkly/sdk/server/LDClient.java +++ b/src/main/java/com/launchdarkly/sdk/server/LDClient.java @@ -397,7 +397,7 @@ public boolean isFlagKnown(String featureKey) { return true; } } catch (Exception e) { - Loggers.MAIN.error("Encountered exception while calling isFlagKnown for feature flag \"{}\": {}", e.toString()); + Loggers.MAIN.error("Encountered exception while calling isFlagKnown for feature flag \"{}\": {}", featureKey, e.toString()); Loggers.MAIN.debug(e.toString(), e); } diff --git a/src/main/java/com/launchdarkly/sdk/server/PersistentDataStoreStatusManager.java b/src/main/java/com/launchdarkly/sdk/server/PersistentDataStoreStatusManager.java index ed79adad3..971089fe1 100644 --- a/src/main/java/com/launchdarkly/sdk/server/PersistentDataStoreStatusManager.java +++ b/src/main/java/com/launchdarkly/sdk/server/PersistentDataStoreStatusManager.java @@ -78,7 +78,8 @@ public void run() { updateAvailability(true); } } catch (Exception e) { - logger.error("Unexpected error from data store status function: {0}", e); + logger.error("Unexpected error from data store status function: {}", e.toString()); + logger.debug(e.toString(), e); } } }; diff --git a/src/main/java/com/launchdarkly/sdk/server/PersistentDataStoreWrapper.java b/src/main/java/com/launchdarkly/sdk/server/PersistentDataStoreWrapper.java index d42f787c6..0da3495a6 100644 --- a/src/main/java/com/launchdarkly/sdk/server/PersistentDataStoreWrapper.java +++ b/src/main/java/com/launchdarkly/sdk/server/PersistentDataStoreWrapper.java @@ -420,7 +420,8 @@ private boolean pollAvailabilityAfterOutage() { } else { // We failed to write the cached data to the underlying store. In this case, we should not // return to a recovered state, but just try this all again next time the poll task runs. - logger.error("Tried to write cached data to persistent store after a store outage, but failed: {}", e); + logger.error("Tried to write cached data to persistent store after a store outage, but failed: {}", e.toString()); + logger.debug(e.toString(), e); return false; } } diff --git a/src/main/java/com/launchdarkly/sdk/server/StreamProcessor.java b/src/main/java/com/launchdarkly/sdk/server/StreamProcessor.java index 7f5ccc53f..36c4e6cab 100644 --- a/src/main/java/com/launchdarkly/sdk/server/StreamProcessor.java +++ b/src/main/java/com/launchdarkly/sdk/server/StreamProcessor.java @@ -36,6 +36,7 @@ import static com.launchdarkly.sdk.server.DataModel.ALL_DATA_KINDS; import static com.launchdarkly.sdk.server.DataModel.SEGMENTS; import static com.launchdarkly.sdk.server.Util.checkIfErrorIsRecoverableAndLog; +import static com.launchdarkly.sdk.server.Util.concatenateUriPath; import static com.launchdarkly.sdk.server.Util.configureHttpClientBuilder; import static com.launchdarkly.sdk.server.Util.getHeadersBuilderFor; import static com.launchdarkly.sdk.server.Util.httpErrorDescription; @@ -68,6 +69,7 @@ * if we succeed then the client can detect that we're initialized now by calling our Initialized method. */ final class StreamProcessor implements DataSource { + private static final String STREAM_URI_PATH = "all"; private static final String PUT = "put"; private static final String PATCH = "patch"; private static final String DELETE = "delete"; @@ -202,7 +204,7 @@ public Future start() { EventHandler handler = new StreamEventHandler(initFuture); es = eventSourceCreator.createEventSource(new EventSourceParams(handler, - URI.create(streamUri.toASCIIString() + "/all"), + concatenateUriPath(streamUri, STREAM_URI_PATH), initialReconnectDelay, wrappedConnectionErrorHandler, headers, diff --git a/src/main/java/com/launchdarkly/sdk/server/Util.java b/src/main/java/com/launchdarkly/sdk/server/Util.java index 06864a4d6..3ffd243b6 100644 --- a/src/main/java/com/launchdarkly/sdk/server/Util.java +++ b/src/main/java/com/launchdarkly/sdk/server/Util.java @@ -6,6 +6,7 @@ import org.slf4j.Logger; import java.io.IOException; +import java.net.URI; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; @@ -176,5 +177,11 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc) { } }); } catch (IOException e) {} - } + } + + static URI concatenateUriPath(URI baseUri, String path) { + String uriStr = baseUri.toString(); + String addPath = path.startsWith("/") ? path.substring(1) : path; + return URI.create(uriStr + (uriStr.endsWith("/") ? "" : "/") + addPath); + } } diff --git a/src/main/java/com/launchdarkly/sdk/server/integrations/FileDataSourceImpl.java b/src/main/java/com/launchdarkly/sdk/server/integrations/FileDataSourceImpl.java index 3a6bb1f17..20b831cf4 100644 --- a/src/main/java/com/launchdarkly/sdk/server/integrations/FileDataSourceImpl.java +++ b/src/main/java/com/launchdarkly/sdk/server/integrations/FileDataSourceImpl.java @@ -70,7 +70,8 @@ final class FileDataSourceImpl implements DataSource { fw = FileWatcher.create(dataLoader.getSources()); } catch (IOException e) { // COVERAGE: there is no way to simulate this condition in a unit test - logger.error("Unable to watch files for auto-updating: " + e); + logger.error("Unable to watch files for auto-updating: {}", e.toString()); + logger.debug(e.toString(), e); fw = null; } } diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/LDClientInterface.java b/src/main/java/com/launchdarkly/sdk/server/interfaces/LDClientInterface.java index 4906359d2..0c5a48cb3 100644 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/LDClientInterface.java +++ b/src/main/java/com/launchdarkly/sdk/server/interfaces/LDClientInterface.java @@ -46,11 +46,6 @@ public interface LDClientInterface extends Closeable { /** * Tracks that a user performed an event, and provides an additional numeric value for custom metrics. - *

- * As of this version’s release date, the LaunchDarkly service does not support the {@code metricValue} - * parameter. As a result, calling this overload of {@code track} will not yet produce any different - * behavior from calling {@link #trackData(String, LDUser, LDValue)} without a {@code metricValue}. - * Refer to the SDK reference guide for the latest status. * * @param eventName the name of the event * @param user the user that performed the event diff --git a/src/test/java/com/launchdarkly/sdk/server/DefaultEventSenderTest.java b/src/test/java/com/launchdarkly/sdk/server/DefaultEventSenderTest.java index 1e8d3401c..b806997a4 100644 --- a/src/test/java/com/launchdarkly/sdk/server/DefaultEventSenderTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/DefaultEventSenderTest.java @@ -301,7 +301,7 @@ public void httpClientCanUseCustomTlsConfig() throws Exception { } @Test - public void baseUriDoesNotNeedToEndInSlash() throws Exception { + public void baseUriDoesNotNeedTrailingSlash() throws Exception { try (MockWebServer server = makeStartedServer(eventsSuccessResponse())) { try (EventSender es = makeEventSender()) { URI uriWithoutSlash = URI.create(server.url("/").toString().replaceAll("/$", "")); @@ -318,6 +318,24 @@ public void baseUriDoesNotNeedToEndInSlash() throws Exception { } } + @Test + public void baseUriCanHaveContextPath() throws Exception { + try (MockWebServer server = makeStartedServer(eventsSuccessResponse())) { + try (EventSender es = makeEventSender()) { + URI baseUri = URI.create(server.url("/context/path").toString()); + EventSender.Result result = es.sendEventData(ANALYTICS, FAKE_DATA, 1, baseUri); + + assertTrue(result.isSuccess()); + assertFalse(result.isMustShutDown()); + } + + RecordedRequest req = server.takeRequest(); + assertEquals("/context/path/bulk", req.getPath()); + assertEquals("application/json; charset=utf-8", req.getHeader("content-type")); + assertEquals(FAKE_DATA, req.getBody().readUtf8()); + } + } + @Test public void nothingIsSentForNullData() throws Exception { try (MockWebServer server = makeStartedServer(eventsSuccessResponse())) { diff --git a/src/test/java/com/launchdarkly/sdk/server/DefaultFeatureRequestorTest.java b/src/test/java/com/launchdarkly/sdk/server/DefaultFeatureRequestorTest.java index ed98993c0..1158ca4eb 100644 --- a/src/test/java/com/launchdarkly/sdk/server/DefaultFeatureRequestorTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/DefaultFeatureRequestorTest.java @@ -44,7 +44,7 @@ private DefaultFeatureRequestor makeRequestor(MockWebServer server) { } private DefaultFeatureRequestor makeRequestor(MockWebServer server, LDConfig config) { - URI uri = server.url("").uri(); + URI uri = server.url("/").uri(); return new DefaultFeatureRequestor(makeHttpConfig(config), uri); } @@ -187,6 +187,42 @@ public void httpClientCanUseProxyConfig() throws Exception { } } + @Test + public void baseUriDoesNotNeedTrailingSlash() throws Exception { + MockResponse resp = jsonResponse(allDataJson); + + try (MockWebServer server = makeStartedServer(resp)) { + URI uri = server.url("").uri(); + try (DefaultFeatureRequestor r = new DefaultFeatureRequestor(makeHttpConfig(LDConfig.DEFAULT), uri)) { + FeatureRequestor.AllData data = r.getAllData(true); + + RecordedRequest req = server.takeRequest(); + assertEquals("/sdk/latest-all", req.getPath()); + verifyHeaders(req); + + verifyExpectedData(data); + } + } + } + + @Test + public void baseUriCanHaveContextPath() throws Exception { + MockResponse resp = jsonResponse(allDataJson); + + try (MockWebServer server = makeStartedServer(resp)) { + URI uri = server.url("/context/path").uri(); + try (DefaultFeatureRequestor r = new DefaultFeatureRequestor(makeHttpConfig(LDConfig.DEFAULT), uri)) { + FeatureRequestor.AllData data = r.getAllData(true); + + RecordedRequest req = server.takeRequest(); + assertEquals("/context/path/sdk/latest-all", req.getPath()); + verifyHeaders(req); + + verifyExpectedData(data); + } + } + } + private void verifyHeaders(RecordedRequest req) { HttpConfiguration httpConfig = clientContext(sdkKey, LDConfig.DEFAULT).getHttp(); for (Map.Entry kv: httpConfig.getDefaultHeaders()) { diff --git a/src/test/java/com/launchdarkly/sdk/server/StreamProcessorTest.java b/src/test/java/com/launchdarkly/sdk/server/StreamProcessorTest.java index 338ca289a..c258501c2 100644 --- a/src/test/java/com/launchdarkly/sdk/server/StreamProcessorTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/StreamProcessorTest.java @@ -72,7 +72,8 @@ public class StreamProcessorTest extends EasyMockSupport { private static final String SDK_KEY = "sdk_key"; - private static final URI STREAM_URI = URI.create("http://stream.test.com"); + private static final URI STREAM_URI = URI.create("http://stream.test.com/"); + private static final URI STREAM_URI_WITHOUT_SLASH = URI.create("http://stream.test.com"); private static final String FEATURE1_KEY = "feature1"; private static final int FEATURE1_VERSION = 11; private static final DataModel.FeatureFlag FEATURE = flagBuilder(FEATURE1_KEY).version(FEATURE1_VERSION).build(); @@ -124,7 +125,21 @@ public void builderCanSpecifyConfiguration() throws Exception { @Test public void streamUriHasCorrectEndpoint() { createStreamProcessor(STREAM_URI).start(); - assertEquals(URI.create(STREAM_URI.toString() + "/all"), + assertEquals(URI.create(STREAM_URI.toString() + "all"), + mockEventSourceCreator.getNextReceivedParams().streamUri); + } + + @Test + public void streamBaseUriDoesNotNeedTrailingSlash() { + createStreamProcessor(STREAM_URI_WITHOUT_SLASH).start(); + assertEquals(URI.create(STREAM_URI_WITHOUT_SLASH.toString() + "/all"), + mockEventSourceCreator.getNextReceivedParams().streamUri); + } + + @Test + public void streamBaseUriCanHaveContextPath() { + createStreamProcessor(URI.create(STREAM_URI.toString() + "/context/path")).start(); + assertEquals(URI.create(STREAM_URI.toString() + "/context/path/all"), mockEventSourceCreator.getNextReceivedParams().streamUri); }