diff --git a/build.gradle b/build.gradle index a598ec767..4e40ec5bd 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,7 @@ plugins { id "java-library" id "checkstyle" id "signing" - id "com.github.johnrengelman.shadow" version "4.0.4" + id "com.github.johnrengelman.shadow" version "5.2.0" id "maven-publish" id "de.marcphilipp.nexus-publish" version "0.3.0" id "io.codearte.nexus-staging" version "0.21.2" @@ -38,13 +38,13 @@ configurations.all { allprojects { group = 'com.launchdarkly' version = "${version}" + archivesBaseName = 'launchdarkly-java-server-sdk' sourceCompatibility = 1.7 targetCompatibility = 1.7 } ext { sdkBasePackage = "com.launchdarkly.client" - sdkBaseName = "launchdarkly-java-server-sdk" // List any packages here that should be included in OSGi imports for the SDK, if they cannot // be discovered by looking in our explicit dependencies. @@ -53,23 +53,34 @@ ext { ext.libraries = [:] +ext.versions = [ + "commonsCodec": "1.10", + "gson": "2.7", + "guava": "19.0", + "jodaTime": "2.9.3", + "okhttpEventsource": "1.11.0", + "slf4j": "1.7.21", + "snakeyaml": "1.19", + "jedis": "2.9.0" +] + // Add dependencies to "libraries.internal" that are not exposed in our public API. These // will be completely omitted from the "thin" jar, and will be embedded with shaded names // in the other two SDK jars. libraries.internal = [ - "commons-codec:commons-codec:1.10", - "com.google.guava:guava:19.0", - "joda-time:joda-time:2.9.3", - "com.launchdarkly:okhttp-eventsource:1.10.2", - "org.yaml:snakeyaml:1.19", - "redis.clients:jedis:2.9.0" + "commons-codec:commons-codec:${versions.commonsCodec}", + "com.google.guava:guava:${versions.guava}", + "joda-time:joda-time:${versions.jodaTime}", + "com.launchdarkly:okhttp-eventsource:${versions.okhttpEventsource}", + "org.yaml:snakeyaml:${versions.snakeyaml}", + "redis.clients:jedis:${versions.jedis}" ] // 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 = [ - "com.google.code.gson:gson:2.7", - "org.slf4j:slf4j-api:1.7.21" + "com.google.code.gson:gson:${versions.gson}", + "org.slf4j:slf4j-api:${versions.slf4j}" ] // Add dependencies to "libraries.test" that are used only in unit tests. @@ -84,8 +95,7 @@ libraries.test = [ dependencies { implementation libraries.internal - compileClasspath libraries.external - runtime libraries.internal, libraries.external + api libraries.external testImplementation libraries.test, libraries.internal, libraries.external // Unlike what the name might suggest, the "shadow" configuration specifies dependencies that @@ -93,8 +103,11 @@ dependencies { shadow libraries.external } -task wrapper(type: Wrapper) { - gradleVersion = '4.10.2' +configurations { + // We need to define "internal" as a custom configuration that contains the same things as + // "implementation", because "implementation" has special behavior in Gradle that prevents us + // from referencing it the way we do in shadeDependencies(). + internal.extendsFrom implementation } checkstyle { @@ -102,7 +115,6 @@ checkstyle { } jar { - baseName = sdkBaseName // thin classifier means that the non-shaded non-fat jar is still available // but is opt-in since users will have to specify it. classifier = 'thin' @@ -118,8 +130,6 @@ jar { // This builds the default uberjar that contains all of our dependencies except Gson and // SLF4j, in shaded form. The user is expected to provide Gson and SLF4j. shadowJar { - baseName = sdkBaseName - // No classifier means that the shaded jar becomes the default artifact classifier = '' @@ -141,12 +151,11 @@ shadowJar { // This builds the "-all"/"fat" jar, which is the same as the default uberjar except that // Gson and SLF4j are bundled and exposed (unshaded). task shadowJarAll(type: com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { - baseName = sdkBaseName classifier = 'all' group = "shadow" description = "Builds a Shaded fat jar including SLF4J" from(project.convention.getPlugin(JavaPluginConvention).sourceSets.main.output) - configurations = [project.configurations.runtime] + configurations = [project.configurations.runtimeClasspath] exclude('META-INF/INDEX.LIST', 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA') // doFirst causes the following steps to be run during Gradle's execution phase rather than the @@ -228,7 +237,7 @@ def shadeDependencies(jarTask) { def excludePackages = getAllSdkPackages() + configurations.shadow.collectMany { getPackagesInDependencyJar(it)} def topLevelPackages = - configurations.runtime.collectMany { + configurations.internal.collectMany { getPackagesInDependencyJar(it).collect { it.contains(".") ? it.substring(0, it.indexOf(".")) : it } }. unique() @@ -325,7 +334,6 @@ test { idea { module { downloadJavadoc = true - downloadSources = true } } @@ -367,7 +375,6 @@ publishing { shadow(MavenPublication) { publication -> project.shadow.component(publication) - artifactId = sdkBaseName artifact jar artifact sourcesJar artifact javadocJar @@ -422,7 +429,7 @@ tasks.withType(Sign) { // dependencies of the SDK, so they can be put on the classpath as needed during tests. task exportDependencies(type: Copy, dependsOn: compileJava) { into "packaging-test/temp/dependencies-all" - from configurations.runtime.resolvedConfiguration.resolvedArtifacts.collect { it.file } + from configurations.runtimeClasspath.resolvedConfiguration.resolvedArtifacts.collect { it.file } } gitPublish { diff --git a/gradle.properties b/gradle.properties index 58b55d1e8..df3a1cc55 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,8 @@ -version=4.12.1 +version=4.12.2 # The following empty ossrh properties are used by LaunchDarkly's internal integration testing framework # and should not be needed for typical development purposes (including by third-party developers). ossrhUsername= ossrhPassword= + +# See https://github.com/gradle/gradle/issues/11308 regarding the following property +systemProp.org.gradle.internal.publish.checksums.insecure=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index ed88a042a..29953ea14 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fb7ef980f..a2bf1313b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-bin.zip diff --git a/packaging-test/Makefile b/packaging-test/Makefile index e69d2cf40..ffc0bbe30 100644 --- a/packaging-test/Makefile +++ b/packaging-test/Makefile @@ -18,39 +18,31 @@ SDK_DEFAULT_JAR=$(SDK_JARS_DIR)/launchdarkly-java-server-sdk-$(SDK_VERSION).jar SDK_ALL_JAR=$(SDK_JARS_DIR)/launchdarkly-java-server-sdk-$(SDK_VERSION)-all.jar SDK_THIN_JAR=$(SDK_JARS_DIR)/launchdarkly-java-server-sdk-$(SDK_VERSION)-thin.jar -TEMP_DIR=$(BASE_DIR)/temp -TEMP_OUTPUT=$(TEMP_DIR)/test.out +export TEMP_DIR=$(BASE_DIR)/temp +export TEMP_OUTPUT=$(TEMP_DIR)/test.out # Build product of the project in ./test-app; can be run as either a regular app or an OSGi bundle TEST_APP_JAR=$(TEMP_DIR)/test-app.jar -# SLF4j implementation - we need to download this separately because it's not in the SDK dependencies -SLF4J_SIMPLE_JAR=$(TEMP_DIR)/test-slf4j-simple.jar -SLF4J_SIMPLE_JAR_URL=https://oss.sonatype.org/content/groups/public/org/slf4j/slf4j-simple/1.7.21/slf4j-simple-1.7.21.jar - # Felix OSGi container -FELIX_ARCHIVE=org.apache.felix.main.distribution-6.0.3.tar.gz -FELIX_ARCHIVE_URL=http://mirrors.ibiblio.org/apache//felix/$(FELIX_ARCHIVE) -FELIX_DIR=$(TEMP_DIR)/felix -FELIX_JAR=$(FELIX_DIR)/bin/felix.jar -TEMP_BUNDLE_DIR=$(TEMP_DIR)/bundles +export FELIX_DIR=$(TEMP_DIR)/felix +export FELIX_JAR=$(FELIX_DIR)/lib/felix.jar +export FELIX_BASE_BUNDLE_DIR=$(FELIX_DIR)/base-bundles +export TEMP_BUNDLE_DIR=$(FELIX_DIR)/app-bundles # Lists of jars to use as a classpath (for the non-OSGi runtime test) or to install as bundles (for # the OSGi test). Note that we're assuming that all of the SDK's dependencies have built-in support # for OSGi, which is currently true; if that weren't true, we would have to do something different # to put them on the system classpath in the OSGi test. -RUN_JARS_test-all-jar=$(TEST_APP_JAR) $(SDK_ALL_JAR) \ - $(SLF4J_SIMPLE_JAR) +RUN_JARS_test-all-jar=$(TEST_APP_JAR) $(SDK_ALL_JAR) RUN_JARS_test-default-jar=$(TEST_APP_JAR) $(SDK_DEFAULT_JAR) \ - $(shell ls $(TEMP_DIR)/dependencies-external/*.jar) \ - $(SLF4J_SIMPLE_JAR) + $(shell ls $(TEMP_DIR)/dependencies-external/*.jar) RUN_JARS_test-thin-jar=$(TEST_APP_JAR) $(SDK_THIN_JAR) \ $(shell ls $(TEMP_DIR)/dependencies-internal/*.jar) \ - $(shell ls $(TEMP_DIR)/dependencies-external/*.jar) \ - $(SLF4J_SIMPLE_JAR) + $(shell ls $(TEMP_DIR)/dependencies-external/*.jar) # The test-app displays this message on success -SUCCESS_MESSAGE="@@@ successfully created LD client @@@" +export SUCCESS_MESSAGE=@@@ successfully created LD client @@@ 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 @@ -73,21 +65,12 @@ clean: # SECONDEXPANSION is needed so we can use "$@" inside a variable in the prerequisite list of the test targets .SECONDEXPANSION: -test-all-jar test-default-jar test-thin-jar: $$(RUN_JARS_$$@) $(TEST_APP_JAR) $(FELIX_JAR) get-sdk-dependencies $$@-classes +test-all-jar test-default-jar test-thin-jar: $$@-classes get-sdk-dependencies $$(RUN_JARS_$$@) $(TEST_APP_JAR) $(FELIX_DIR) @$(call caption,$@) - @echo " non-OSGi runtime test" - @java -classpath $(shell echo "$(RUN_JARS_$@)" | sed -e 's/ /:/g') testapp.TestApp | tee $(TEMP_OUTPUT) - @grep $(SUCCESS_MESSAGE) $(TEMP_OUTPUT) >/dev/null + ./run-non-osgi-test.sh $(RUN_JARS_$@) # Can't currently run the OSGi test for the thin jar, because some of our dependencies aren't available as OSGi bundles. @if [ "$@" != "test-thin-jar" ]; then \ - echo ""; \ - echo " OSGi runtime test"; \ - rm -rf $(TEMP_BUNDLE_DIR); \ - mkdir -p $(TEMP_BUNDLE_DIR); \ - cp $(RUN_JARS_$@) $(FELIX_DIR)/bundle/*.jar $(TEMP_BUNDLE_DIR); \ - rm -rf $(FELIX_DIR)/felix-cache; \ - cd $(FELIX_DIR) && echo "sleep 3;exit 0" | java -jar $(FELIX_JAR) -b $(TEMP_BUNDLE_DIR) | tee $(TEMP_OUTPUT); \ - grep $(SUCCESS_MESSAGE) $(TEMP_OUTPUT) >/dev/null; \ + ./run-osgi-test.sh $(RUN_JARS_$@); \ fi test-all-jar-classes: $(SDK_ALL_JAR) $(TEMP_DIR) @@ -144,13 +127,21 @@ $(TEMP_DIR)/dependencies-internal: $(TEMP_DIR)/dependencies-all cp $(TEMP_DIR)/dependencies-all/*.jar $@ rm $@/gson*.jar $@/slf4j*.jar -$(SLF4J_SIMPLE_JAR): | $(TEMP_DIR) - curl -f -L $(SLF4J_SIMPLE_JAR_URL) >$@ - -$(FELIX_JAR): | $(TEMP_DIR) - curl -f -L $(FELIX_ARCHIVE_URL) >$(TEMP_DIR)/$(FELIX_ARCHIVE) - cd $(TEMP_DIR) && tar xfz $(FELIX_ARCHIVE) && rm $(FELIX_ARCHIVE) - cd $(TEMP_DIR) && mv `ls -d felix*` felix +$(FELIX_JAR): $(FELIX_DIR) + +$(FELIX_DIR): + mkdir -p $(FELIX_DIR) + mkdir -p $(FELIX_DIR)/lib + mkdir -p $(FELIX_BASE_BUNDLE_DIR) + cd test-app && ../../gradlew createOsgi + @# createOsgi is a target provided by the osgi-run Gradle plugin; it downloads the Felix container and + @# puts it in build/osgi along with related bundles and a config file. + cp -r test-app/build/osgi/conf $(FELIX_DIR) + echo "felix.shutdown.hook=false" >>$(FELIX_DIR)/conf/config.properties + @# setting felix.shutdown.hook to false allows our test app to use System.exit() + cp test-app/build/osgi/system-libs/org.apache.felix.main-*.jar $(FELIX_JAR) + cp test-app/build/osgi/bundle/* $(FELIX_BASE_BUNDLE_DIR) + cd $(FELIX_BASE_BUNDLE_DIR) && rm -f launchdarkly-*.jar gson-*.jar $(TEMP_DIR): [ -d $@ ] || mkdir -p $@ diff --git a/packaging-test/run-non-osgi-test.sh b/packaging-test/run-non-osgi-test.sh new file mode 100755 index 000000000..dcf9c24db --- /dev/null +++ b/packaging-test/run-non-osgi-test.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +echo "" +echo " non-OSGi runtime test" +java -classpath $(echo "$@" | sed -e 's/ /:/g') testapp.TestApp | tee ${TEMP_OUTPUT} +grep "${SUCCESS_MESSAGE}" ${TEMP_OUTPUT} >/dev/null diff --git a/packaging-test/run-osgi-test.sh b/packaging-test/run-osgi-test.sh new file mode 100755 index 000000000..62439fedf --- /dev/null +++ b/packaging-test/run-osgi-test.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +echo "" +echo " OSGi runtime test" +rm -rf ${TEMP_BUNDLE_DIR} +mkdir -p ${TEMP_BUNDLE_DIR} +cp $@ ${FELIX_BASE_BUNDLE_DIR}/* ${TEMP_BUNDLE_DIR} +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} + +grep "${SUCCESS_MESSAGE}" ${TEMP_OUTPUT} >/dev/null diff --git a/packaging-test/test-app/build.gradle b/packaging-test/test-app/build.gradle index 02ba7b08f..59d3fd936 100644 --- a/packaging-test/test-app/build.gradle +++ b/packaging-test/test-app/build.gradle @@ -1,5 +1,17 @@ -apply plugin: "java" -apply plugin: "osgi" + +buildscript { + repositories { + jcenter() + mavenCentral() + } +} + +plugins { + id "java" + id "java-library" + id "biz.aQute.bnd.builder" version "5.0.1" + id "com.athaydes.osgi-run" version "1.6.0" +} repositories { mavenCentral() @@ -8,32 +20,26 @@ repositories { allprojects { group = "com.launchdarkly" version = "1.0.0" + archivesBaseName = 'test-app-bundle' sourceCompatibility = 1.7 targetCompatibility = 1.7 } dependencies { // Note, the SDK build must have already been run before this, since we're using its product as a dependency - compileClasspath fileTree(dir: "../../build/libs", include: "launchdarkly-java-server-sdk-*-thin.jar") - compileClasspath "com.google.code.gson:gson:2.7" - compileClasspath "org.slf4j:slf4j-api:1.7.21" - compileClasspath "org.osgi:osgi_R4_core:1.0" + implementation fileTree(dir: "../../build/libs", include: "launchdarkly-java-server-sdk-*-thin.jar") + implementation "com.google.code.gson:gson:2.7" + implementation "org.slf4j:slf4j-api:1.7.22" + implementation "org.osgi:osgi_R4_core:1.0" + osgiRuntime "org.slf4j:slf4j-simple:1.7.22" } jar { - baseName = 'test-app-bundle' - manifest { - instruction 'Bundle-Activator', 'testapp.TestAppOsgiEntryPoint' - } -} - -task wrapper(type: Wrapper) { - gradleVersion = '4.10.2' + bnd( + 'Bundle-Activator': 'testapp.TestAppOsgiEntryPoint' + ) } -buildscript { - repositories { - jcenter() - mavenCentral() - } +runOsgi { + bundles = [ ] // we don't need a CLI or anything like that - just the SLF4j dependency shown above } diff --git a/packaging-test/test-app/settings.gradle b/packaging-test/test-app/settings.gradle new file mode 100644 index 000000000..e2a1182e2 --- /dev/null +++ b/packaging-test/test-app/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'test-app-bundle' diff --git a/packaging-test/test-app/src/main/java/testapp/TestAppOsgiEntryPoint.java b/packaging-test/test-app/src/main/java/testapp/TestAppOsgiEntryPoint.java index f1a9db3ad..ed42ccb1a 100644 --- a/packaging-test/test-app/src/main/java/testapp/TestAppOsgiEntryPoint.java +++ b/packaging-test/test-app/src/main/java/testapp/TestAppOsgiEntryPoint.java @@ -8,9 +8,10 @@ public void start(BundleContext context) throws Exception { System.out.println("@@@ starting test bundle @@@"); TestApp.main(new String[0]); + + System.exit(0); } public void stop(BundleContext context) throws Exception { - System.out.println("@@@ stopping test bundle @@@"); } } \ No newline at end of file diff --git a/src/main/java/com/launchdarkly/client/Components.java b/src/main/java/com/launchdarkly/client/Components.java index 3d7d94e5c..f446eb8e6 100644 --- a/src/main/java/com/launchdarkly/client/Components.java +++ b/src/main/java/com/launchdarkly/client/Components.java @@ -2,21 +2,28 @@ import com.launchdarkly.client.DiagnosticEvent.ConfigProperty; import com.launchdarkly.client.integrations.EventProcessorBuilder; +import com.launchdarkly.client.integrations.HttpConfigurationBuilder; import com.launchdarkly.client.integrations.PersistentDataStoreBuilder; import com.launchdarkly.client.integrations.PollingDataSourceBuilder; import com.launchdarkly.client.integrations.StreamingDataSourceBuilder; import com.launchdarkly.client.interfaces.DiagnosticDescription; +import com.launchdarkly.client.interfaces.HttpAuthentication; +import com.launchdarkly.client.interfaces.HttpConfiguration; import com.launchdarkly.client.interfaces.PersistentDataStoreFactory; import com.launchdarkly.client.utils.CachingStoreWrapper; import com.launchdarkly.client.utils.FeatureStoreCore; import com.launchdarkly.client.value.LDValue; import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; import java.net.URI; import java.util.concurrent.Future; import static com.google.common.util.concurrent.Futures.immediateFuture; +import okhttp3.Credentials; + /** * Provides configurable factories for the standard implementations of LaunchDarkly component interfaces. *
@@ -313,6 +320,55 @@ public static UpdateProcessorFactory nullUpdateProcessor() { return nullUpdateProcessorFactory; } + /** + * Returns a configurable factory for the SDK's networking configuration. + *
+ * Passing this to {@link LDConfig.Builder#http(com.launchdarkly.client.interfaces.HttpConfigurationFactory)} + * applies this configuration to all HTTP/HTTPS requests made by the SDK. + *
+ * LDConfig config = new LDConfig.Builder()
+ * .http(
+ * Components.httpConfiguration()
+ * .connectTimeoutMillis(3000)
+ * .proxyHostAndPort("my-proxy", 8080)
+ * )
+ * .build();
+ *
+ * + * These properties will override any equivalent deprecated properties that were set with {@code LDConfig.Builder}, + * such as {@link LDConfig.Builder#connectTimeout(int)}. However, setting {@link LDConfig.Builder#offline(boolean)} + * to {@code true} will supersede these settings and completely disable network requests. + * + * @return a factory object + * @since 4.13.0 + * @see LDConfig.Builder#http(com.launchdarkly.client.interfaces.HttpConfigurationFactory) + */ + public static HttpConfigurationBuilder httpConfiguration() { + return new HttpConfigurationBuilderImpl(); + } + + /** + * Configures HTTP basic authentication, for use with a proxy server. + *
+ * LDConfig config = new LDConfig.Builder()
+ * .http(
+ * Components.httpConfiguration()
+ * .proxyHostAndPort("my-proxy", 8080)
+ * .proxyAuthentication(Components.httpBasicAuthentication("username", "password"))
+ * )
+ * .build();
+ *
+ *
+ * @param username the username
+ * @param password the password
+ * @return the basic authentication strategy
+ * @since 4.13.0
+ * @see HttpConfigurationBuilder#proxyAuth(HttpAuthentication)
+ */
+ public static HttpAuthentication httpBasicAuthentication(String username, String password) {
+ return new HttpBasicAuthentication(username, password);
+ }
+
private static final class InMemoryFeatureStoreFactory implements FeatureStoreFactory, DiagnosticDescription {
@Override
public FeatureStore createFeatureStore() {
@@ -646,6 +702,36 @@ public LDValue describeConfiguration(LDConfig config) {
}
}
+ private static final class HttpConfigurationBuilderImpl extends HttpConfigurationBuilder {
+ @Override
+ public HttpConfiguration createHttpConfiguration() {
+ return new HttpConfigurationImpl(
+ connectTimeoutMillis,
+ proxyHost == null ? null : new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)),
+ proxyAuth,
+ socketTimeoutMillis,
+ sslSocketFactory,
+ trustManager,
+ wrapperName == null ? null : (wrapperVersion == null ? wrapperName : (wrapperName + "/" + wrapperVersion))
+ );
+ }
+ }
+
+ private static final class HttpBasicAuthentication implements HttpAuthentication {
+ private final String username;
+ private final String password;
+
+ HttpBasicAuthentication(String username, String password) {
+ this.username = username;
+ this.password = password;
+ }
+
+ @Override
+ public String provideAuthorization(Iterable* Note that this is an enum-like class hierarchy rather than an enum, because some of the - * possible reasons have their own properties. + * possible reasons have their own properties. However, directly referencing the subclasses is + * deprecated; in a future version only the {@link EvaluationReason} base class will be visible, + * and it has getter methods for all of the possible properties. * * @since 4.3.0 */ @@ -101,6 +103,60 @@ public Kind getKind() { return kind; } + + /** + * The index of the rule that was matched (0 for the first rule in the feature flag), + * if the {@code kind} is {@link Kind#RULE_MATCH}. Otherwise this returns -1. + * + * @return the rule index or -1 + */ + public int getRuleIndex() { + return -1; + } + + /** + * The unique identifier of the rule that was matched, if the {@code kind} is + * {@link Kind#RULE_MATCH}. Otherwise {@code null}. + *
+ * Unlike the rule index, this identifier will not change if other rules are added or deleted.
+ *
+ * @return the rule identifier or null
+ */
+ public String getRuleId() {
+ return null;
+ }
+
+ /**
+ * The key of the prerequisite flag that did not return the desired variation, if the
+ * {@code kind} is {@link Kind#PREREQUISITE_FAILED}. Otherwise {@code null}.
+ *
+ * @return the prerequisite flag key or null
+ */
+ public String getPrerequisiteKey() {
+ return null;
+ }
+
+ /**
+ * An enumeration value indicating the general category of error, if the
+ * {@code kind} is {@link Kind#PREREQUISITE_FAILED}. Otherwise {@code null}.
+ *
+ * @return the error kind or null
+ */
+ public ErrorKind getErrorKind() {
+ return null;
+ }
+
+ /**
+ * The exception that caused the error condition, if the {@code kind} is
+ * {@link EvaluationReason.Kind#ERROR} and the {@code errorKind} is {@link ErrorKind#EXCEPTION}.
+ * Otherwise {@code null}.
+ *
+ * @return the exception instance
+ * @since 4.11.0
+ */
+ public Exception getException() {
+ return null;
+ }
@Override
public String toString() {
@@ -113,7 +169,7 @@ protected EvaluationReason(Kind kind)
}
/**
- * Returns an instance of {@link Off}.
+ * Returns an instance whose {@code kind} is {@link Kind#OFF}.
* @return a reason object
*/
public static Off off() {
@@ -121,7 +177,7 @@ public static Off off() {
}
/**
- * Returns an instance of {@link TargetMatch}.
+ * Returns an instance whose {@code kind} is {@link Kind#TARGET_MATCH}.
* @return a reason object
*/
public static TargetMatch targetMatch() {
@@ -129,7 +185,7 @@ public static TargetMatch targetMatch() {
}
/**
- * Returns an instance of {@link RuleMatch}.
+ * Returns an instance whose {@code kind} is {@link Kind#RULE_MATCH}.
* @param ruleIndex the rule index
* @param ruleId the rule identifier
* @return a reason object
@@ -139,7 +195,7 @@ public static RuleMatch ruleMatch(int ruleIndex, String ruleId) {
}
/**
- * Returns an instance of {@link PrerequisiteFailed}.
+ * Returns an instance whose {@code kind} is {@link Kind#PREREQUISITE_FAILED}.
* @param prerequisiteKey the flag key of the prerequisite that failed
* @return a reason object
*/
@@ -148,7 +204,7 @@ public static PrerequisiteFailed prerequisiteFailed(String prerequisiteKey) {
}
/**
- * Returns an instance of {@link Fallthrough}.
+ * Returns an instance whose {@code kind} is {@link Kind#FALLTHROUGH}.
* @return a reason object
*/
public static Fallthrough fallthrough() {
@@ -156,7 +212,7 @@ public static Fallthrough fallthrough() {
}
/**
- * Returns an instance of {@link Error}.
+ * Returns an instance whose {@code kind} is {@link Kind#ERROR}.
* @param errorKind describes the type of error
* @return a reason object
*/
@@ -186,7 +242,10 @@ public static Error exception(Exception exception) {
* Subclass of {@link EvaluationReason} that indicates that the flag was off and therefore returned
* its configured off value.
* @since 4.3.0
+ * @deprecated This type will be removed in a future version. Use {@link #getKind()} instead and check
+ * for the {@link Kind#OFF} value.
*/
+ @Deprecated
public static class Off extends EvaluationReason {
private Off() {
super(Kind.OFF);
@@ -199,7 +258,10 @@ private Off() {
* Subclass of {@link EvaluationReason} that indicates that the user key was specifically targeted
* for this flag.
* @since 4.3.0
+ * @deprecated This type will be removed in a future version. Use {@link #getKind()} instead and check
+ * for the {@link Kind#TARGET_MATCH} value.
*/
+ @Deprecated
public static class TargetMatch extends EvaluationReason {
private TargetMatch()
{
@@ -212,7 +274,10 @@ private TargetMatch()
/**
* Subclass of {@link EvaluationReason} that indicates that the user matched one of the flag's rules.
* @since 4.3.0
+ * @deprecated This type will be removed in a future version. Use {@link #getKind()} instead and check
+ * for the {@link Kind#RULE_MATCH} value.
*/
+ @Deprecated
public static class RuleMatch extends EvaluationReason {
private final int ruleIndex;
private final String ruleId;
@@ -227,6 +292,7 @@ private RuleMatch(int ruleIndex, String ruleId) {
* The index of the rule that was matched (0 for the first rule in the feature flag).
* @return the rule index
*/
+ @Override
public int getRuleIndex() {
return ruleIndex;
}
@@ -235,6 +301,7 @@ public int getRuleIndex() {
* A unique string identifier for the matched rule, which will not change if other rules are added or deleted.
* @return the rule identifier
*/
+ @Override
public String getRuleId() {
return ruleId;
}
@@ -263,7 +330,10 @@ public String toString() {
* Subclass of {@link EvaluationReason} that indicates that the flag was considered off because it
* had at least one prerequisite flag that either was off or did not return the desired variation.
* @since 4.3.0
+ * @deprecated This type will be removed in a future version. Use {@link #getKind()} instead and check
+ * for the {@link Kind#PREREQUISITE_FAILED} value.
*/
+ @Deprecated
public static class PrerequisiteFailed extends EvaluationReason {
private final String prerequisiteKey;
@@ -276,6 +346,7 @@ private PrerequisiteFailed(String prerequisiteKey) {
* The key of the prerequisite flag that did not return the desired variation.
* @return the prerequisite flag key
*/
+ @Override
public String getPrerequisiteKey() {
return prerequisiteKey;
}
@@ -301,7 +372,10 @@ public String toString() {
* Subclass of {@link EvaluationReason} that indicates that the flag was on but the user did not
* match any targets or rules.
* @since 4.3.0
+ * @deprecated This type will be removed in a future version. Use {@link #getKind()} instead and check
+ * for the {@link Kind#FALLTHROUGH} value.
*/
+ @Deprecated
public static class Fallthrough extends EvaluationReason {
private Fallthrough()
{
@@ -314,7 +388,10 @@ private Fallthrough()
/**
* Subclass of {@link EvaluationReason} that indicates that the flag could not be evaluated.
* @since 4.3.0
+ * @deprecated This type will be removed in a future version. Use {@link #getKind()} instead and check
+ * for the {@link Kind#ERROR} value.
*/
+ @Deprecated
public static class Error extends EvaluationReason {
private final ErrorKind errorKind;
private transient final Exception exception;
@@ -333,6 +410,7 @@ private Error(ErrorKind errorKind, Exception exception) {
* An enumeration value indicating the general category of error.
* @return the error kind
*/
+ @Override
public ErrorKind getErrorKind() {
return errorKind;
}
diff --git a/src/main/java/com/launchdarkly/client/EventOutputFormatter.java b/src/main/java/com/launchdarkly/client/EventOutputFormatter.java
index 268226fed..03ba5a12c 100644
--- a/src/main/java/com/launchdarkly/client/EventOutputFormatter.java
+++ b/src/main/java/com/launchdarkly/client/EventOutputFormatter.java
@@ -206,22 +206,25 @@ private void writeEvaluationReason(String key, EvaluationReason er, JsonWriter j
jw.name("kind");
jw.value(er.getKind().name());
- if (er instanceof EvaluationReason.Error) {
- EvaluationReason.Error ere = (EvaluationReason.Error)er;
+ switch (er.getKind()) {
+ case ERROR:
jw.name("errorKind");
- jw.value(ere.getErrorKind().name());
- } else if (er instanceof EvaluationReason.PrerequisiteFailed) {
- EvaluationReason.PrerequisiteFailed erpf = (EvaluationReason.PrerequisiteFailed)er;
+ jw.value(er.getErrorKind().name());
+ break;
+ case PREREQUISITE_FAILED:
jw.name("prerequisiteKey");
- jw.value(erpf.getPrerequisiteKey());
- } else if (er instanceof EvaluationReason.RuleMatch) {
- EvaluationReason.RuleMatch errm = (EvaluationReason.RuleMatch)er;
+ jw.value(er.getPrerequisiteKey());
+ break;
+ case RULE_MATCH:
jw.name("ruleIndex");
- jw.value(errm.getRuleIndex());
- if (errm.getRuleId() != null) {
+ jw.value(er.getRuleIndex());
+ if (er.getRuleId() != null) {
jw.name("ruleId");
- jw.value(errm.getRuleId());
+ jw.value(er.getRuleId());
}
+ break;
+ default:
+ break;
}
jw.endObject();
diff --git a/src/main/java/com/launchdarkly/client/HttpConfiguration.java b/src/main/java/com/launchdarkly/client/HttpConfiguration.java
deleted file mode 100644
index 7ca4593c6..000000000
--- a/src/main/java/com/launchdarkly/client/HttpConfiguration.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package com.launchdarkly.client;
-
-import java.net.Proxy;
-import java.util.concurrent.TimeUnit;
-
-import javax.net.ssl.SSLSocketFactory;
-import javax.net.ssl.X509TrustManager;
-
-import okhttp3.Authenticator;
-
-// Used internally to encapsulate top-level HTTP configuration that applies to all components.
-final class HttpConfiguration {
- final int connectTimeout;
- final TimeUnit connectTimeoutUnit;
- final Proxy proxy;
- final Authenticator proxyAuthenticator;
- final int socketTimeout;
- final TimeUnit socketTimeoutUnit;
- final SSLSocketFactory sslSocketFactory;
- final X509TrustManager trustManager;
- final String wrapperName;
- final String wrapperVersion;
-
- HttpConfiguration(int connectTimeout, TimeUnit connectTimeoutUnit, Proxy proxy, Authenticator proxyAuthenticator,
- int socketTimeout, TimeUnit socketTimeoutUnit, SSLSocketFactory sslSocketFactory, X509TrustManager trustManager,
- String wrapperName, String wrapperVersion) {
- super();
- this.connectTimeout = connectTimeout;
- this.connectTimeoutUnit = connectTimeoutUnit;
- this.proxy = proxy;
- this.proxyAuthenticator = proxyAuthenticator;
- this.socketTimeout = socketTimeout;
- this.socketTimeoutUnit = socketTimeoutUnit;
- this.sslSocketFactory = sslSocketFactory;
- this.trustManager = trustManager;
- this.wrapperName = wrapperName;
- this.wrapperVersion = wrapperVersion;
- }
-}
diff --git a/src/main/java/com/launchdarkly/client/HttpConfigurationImpl.java b/src/main/java/com/launchdarkly/client/HttpConfigurationImpl.java
new file mode 100644
index 000000000..c1e9b3e7c
--- /dev/null
+++ b/src/main/java/com/launchdarkly/client/HttpConfigurationImpl.java
@@ -0,0 +1,66 @@
+package com.launchdarkly.client;
+
+import com.launchdarkly.client.interfaces.HttpAuthentication;
+import com.launchdarkly.client.interfaces.HttpConfiguration;
+
+import java.net.Proxy;
+
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509TrustManager;
+
+final class HttpConfigurationImpl implements HttpConfiguration {
+ final int connectTimeoutMillis;
+ final Proxy proxy;
+ final HttpAuthentication proxyAuth;
+ final int socketTimeoutMillis;
+ final SSLSocketFactory sslSocketFactory;
+ final X509TrustManager trustManager;
+ final String wrapper;
+
+ HttpConfigurationImpl(int connectTimeoutMillis, Proxy proxy, HttpAuthentication proxyAuth,
+ int socketTimeoutMillis, SSLSocketFactory sslSocketFactory, X509TrustManager trustManager,
+ String wrapper) {
+ this.connectTimeoutMillis = connectTimeoutMillis;
+ this.proxy = proxy;
+ this.proxyAuth = proxyAuth;
+ this.socketTimeoutMillis = socketTimeoutMillis;
+ this.sslSocketFactory = sslSocketFactory;
+ this.trustManager = trustManager;
+ this.wrapper = wrapper;
+ }
+
+ @Override
+ public int getConnectTimeoutMillis() {
+ return connectTimeoutMillis;
+ }
+
+ @Override
+ public Proxy getProxy() {
+ return proxy;
+ }
+
+ @Override
+ public HttpAuthentication getProxyAuthentication() {
+ return proxyAuth;
+ }
+
+ @Override
+ public int getSocketTimeoutMillis() {
+ return socketTimeoutMillis;
+ }
+
+ @Override
+ public SSLSocketFactory getSslSocketFactory() {
+ return sslSocketFactory;
+ }
+
+ @Override
+ public X509TrustManager getTrustManager() {
+ return trustManager;
+ }
+
+ @Override
+ public String getWrapperIdentifier() {
+ return wrapper;
+ }
+}
diff --git a/src/main/java/com/launchdarkly/client/JsonHelpers.java b/src/main/java/com/launchdarkly/client/JsonHelpers.java
index 97f4c95a5..7fb1c0095 100644
--- a/src/main/java/com/launchdarkly/client/JsonHelpers.java
+++ b/src/main/java/com/launchdarkly/client/JsonHelpers.java
@@ -2,20 +2,28 @@
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
+import com.launchdarkly.client.interfaces.SerializationException;
import java.io.IOException;
+import static com.launchdarkly.client.VersionedDataKind.FEATURES;
+import static com.launchdarkly.client.VersionedDataKind.SEGMENTS;
+
abstract class JsonHelpers {
private static final Gson gson = new Gson();
/**
* Returns a shared instance of Gson with default configuration. This should not be used for serializing
* event data, since it does not have any of the configurable behavior related to private attributes.
+ * Code in _unit tests_ should _not_ use this method, because the tests can be run from other projects
+ * in an environment where the classpath contains a shaded copy of Gson instead of regular Gson.
*/
static Gson gsonInstance() {
return gson;
@@ -29,7 +37,68 @@ static Gson gsonInstanceForEventsSerialization(EventsConfiguration config) {
.registerTypeAdapter(LDUser.class, new LDUser.UserAdapterWithPrivateAttributeBehavior(config))
.create();
}
+
+ /**
+ * Deserializes an object from JSON. We should use this helper method instead of directly calling
+ * gson.fromJson() to minimize reliance on details of the framework we're using, and to ensure that we
+ * consistently use our wrapper exception.
+ *
+ * @param json the serialized JSON string
+ * @param objectClass class of object to create
+ * @return the deserialized object
+ * @throws SerializationException if Gson throws an exception
+ */
+ static
+ * For built-in data model classes, our usual abstraction for deserializing from a string is inefficient in
+ * this case, because Gson has already parsed the original JSON and then we would have to convert the
+ * JsonElement back into a string and parse it again. So it's best to call Gson directly instead of going
+ * through our abstraction in that case, but it's also best to implement that special-casing just once here
+ * instead of scattered throughout the SDK.
+ *
+ * @param kind the data kind
+ * @param parsedJson the parsed JSON
+ * @return the deserialized item
+ */
+ static VersionedData deserializeFromParsedJson(VersionedDataKind> kind, JsonElement parsedJson) throws SerializationException {
+ VersionedData item;
+ try {
+ if (kind == FEATURES) {
+ item = gson.fromJson(parsedJson, FeatureFlag.class);
+ } else if (kind == SEGMENTS) {
+ item = gson.fromJson(parsedJson, Segment.class);
+ } else {
+ // This shouldn't happen since we only use this method internally with our predefined data kinds
+ throw new IllegalArgumentException("unknown data kind");
+ }
+ } catch (JsonParseException e) {
+ throw new SerializationException(e);
+ }
+ return item;
+ }
+
/**
* Implement this interface on any internal class that needs to do some kind of post-processing after
* being unmarshaled from JSON. You must also add the annotation {@code JsonAdapter(JsonHelpers.PostProcessingDeserializableTypeAdapterFactory)}
diff --git a/src/main/java/com/launchdarkly/client/LDClient.java b/src/main/java/com/launchdarkly/client/LDClient.java
index 7be875e39..cfe55e61d 100644
--- a/src/main/java/com/launchdarkly/client/LDClient.java
+++ b/src/main/java/com/launchdarkly/client/LDClient.java
@@ -65,6 +65,14 @@ public LDClient(String sdkKey, LDConfig config) {
this.config = new LDConfig(checkNotNull(config, "config must not be null"));
this.sdkKey = checkNotNull(sdkKey, "sdkKey must not be null");
+ if (config.httpConfig.getProxy() != null) {
+ if (config.httpConfig.getProxyAuthentication() != null) {
+ logger.info("Using proxy: {} with authentication.", config.httpConfig.getProxy());
+ } else {
+ logger.info("Using proxy: {} without authentication.", config.httpConfig.getProxy());
+ }
+ }
+
FeatureStore store;
if (this.config.deprecatedFeatureStore != null) {
store = this.config.deprecatedFeatureStore;
@@ -465,7 +473,8 @@ private static String getClientVersion() {
String value = attr.getValue("Implementation-Version");
return value;
} catch (IOException e) {
- logger.warn("Unable to determine LaunchDarkly client library version", e);
+ logger.warn("Unable to determine LaunchDarkly client library version: {}", e.toString());
+ logger.debug(e.toString(), e);
return "Unknown";
}
}
diff --git a/src/main/java/com/launchdarkly/client/LDConfig.java b/src/main/java/com/launchdarkly/client/LDConfig.java
index adba16f8d..2a1be1b35 100644
--- a/src/main/java/com/launchdarkly/client/LDConfig.java
+++ b/src/main/java/com/launchdarkly/client/LDConfig.java
@@ -2,39 +2,25 @@
import com.google.common.collect.ImmutableSet;
import com.launchdarkly.client.integrations.EventProcessorBuilder;
+import com.launchdarkly.client.integrations.HttpConfigurationBuilder;
import com.launchdarkly.client.integrations.PollingDataSourceBuilder;
import com.launchdarkly.client.integrations.StreamingDataSourceBuilder;
+import com.launchdarkly.client.interfaces.HttpConfiguration;
+import com.launchdarkly.client.interfaces.HttpConfigurationFactory;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.net.InetSocketAddress;
-import java.net.Proxy;
import java.net.URI;
-import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.X509TrustManager;
-import okhttp3.Authenticator;
-import okhttp3.Credentials;
-import okhttp3.Request;
-import okhttp3.Response;
-import okhttp3.Route;
-
/**
* This class exposes advanced configuration options for the {@link LDClient}. Instances of this class must be constructed with a {@link com.launchdarkly.client.LDConfig.Builder}.
*/
public final class LDConfig {
- private static final Logger logger = LoggerFactory.getLogger(LDConfig.class);
-
static final URI DEFAULT_BASE_URI = URI.create("https://app.launchdarkly.com");
static final URI DEFAULT_EVENTS_URI = URI.create("https://events.launchdarkly.com");
static final URI DEFAULT_STREAM_URI = URI.create("https://stream.launchdarkly.com");
private static final int DEFAULT_CAPACITY = 10000;
- private static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 2000;
- private static final int DEFAULT_SOCKET_TIMEOUT_MILLIS = 10000;
private static final int DEFAULT_FLUSH_INTERVAL_SECONDS = 5;
private static final long MIN_POLLING_INTERVAL_MILLIS = PollingDataSourceBuilder.DEFAULT_POLL_INTERVAL_MILLIS;
private static final long DEFAULT_START_WAIT_MILLIS = 5000L;
@@ -78,20 +64,20 @@ protected LDConfig(Builder builder) {
this.offline = builder.offline;
this.startWaitMillis = builder.startWaitMillis;
- Proxy proxy = builder.proxy();
- Authenticator proxyAuthenticator = builder.proxyAuthenticator();
- if (proxy != null) {
- if (proxyAuthenticator != null) {
- logger.info("Using proxy: " + proxy + " with authentication.");
- } else {
- logger.info("Using proxy: " + proxy + " without authentication.");
- }
+ if (builder.httpConfigFactory != null) {
+ this.httpConfig = builder.httpConfigFactory.createHttpConfiguration();
+ } else {
+ this.httpConfig = Components.httpConfiguration()
+ .connectTimeoutMillis(builder.connectTimeoutMillis)
+ .proxyHostAndPort(builder.proxyPort == -1 ? null : builder.proxyHost, builder.proxyPort)
+ .proxyAuth(builder.proxyUsername == null || builder.proxyPassword == null ? null :
+ Components.httpBasicAuthentication(builder.proxyUsername, builder.proxyPassword))
+ .socketTimeoutMillis(builder.socketTimeoutMillis)
+ .sslSocketFactory(builder.sslSocketFactory, builder.trustManager)
+ .wrapper(builder.wrapperName, builder.wrapperVersion)
+ .createHttpConfiguration();
}
- this.httpConfig = new HttpConfiguration(builder.connectTimeout, builder.connectTimeoutUnit,
- proxy, proxyAuthenticator, builder.socketTimeout, builder.socketTimeoutUnit,
- builder.sslSocketFactory, builder.trustManager, builder.wrapperName, builder.wrapperVersion);
-
this.deprecatedAllAttributesPrivate = builder.allAttributesPrivate;
this.deprecatedBaseURI = builder.baseURI;
this.deprecatedCapacity = builder.capacity;
@@ -156,10 +142,9 @@ public static class Builder {
private URI baseURI = DEFAULT_BASE_URI;
private URI eventsURI = DEFAULT_EVENTS_URI;
private URI streamURI = DEFAULT_STREAM_URI;
- private int connectTimeout = DEFAULT_CONNECT_TIMEOUT_MILLIS;
- private TimeUnit connectTimeoutUnit = TimeUnit.MILLISECONDS;
- private int socketTimeout = DEFAULT_SOCKET_TIMEOUT_MILLIS;
- private TimeUnit socketTimeoutUnit = TimeUnit.MILLISECONDS;
+ private HttpConfigurationFactory httpConfigFactory = null;
+ private int connectTimeoutMillis = HttpConfigurationBuilder.DEFAULT_CONNECT_TIMEOUT_MILLIS;
+ private int socketTimeoutMillis = HttpConfigurationBuilder.DEFAULT_SOCKET_TIMEOUT_MILLIS;
private boolean diagnosticOptOut = false;
private int capacity = DEFAULT_CAPACITY;
private int flushIntervalSeconds = DEFAULT_FLUSH_INTERVAL_SECONDS;
@@ -307,6 +292,7 @@ public Builder events(EventProcessorFactory factory) {
* @since 4.0.0
* @deprecated Use {@link #events(EventProcessorFactory)}.
*/
+ @Deprecated
public Builder eventProcessorFactory(EventProcessorFactory factory) {
this.eventProcessorFactory = factory;
return this;
@@ -366,63 +352,72 @@ public Builder stream(boolean stream) {
}
/**
- * Set the connection timeout in seconds for the configuration. This is the time allowed for the underlying HTTP client to connect
- * to the LaunchDarkly server. The default is 2 seconds.
- * Both this method and {@link #connectTimeoutMillis(int) connectTimeoutMillis} affect the same property internally. Both this method and {@link #socketTimeoutMillis(int) socketTimeoutMillis} affect the same property internally. Both this method and {@link #connectTimeout(int) connectTimeoutMillis} affect the same property internally. Both this method and {@link #socketTimeout(int) socketTimeoutMillis} affect the same property internally.
* If neither {@link #proxyHost(String)} nor {@link #proxyPort(int)} are specified,
* a proxy will not be used, and {@link LDClient} will connect to LaunchDarkly directly.
@@ -457,56 +453,66 @@ public Builder capacity(int capacity) {
*
* @param host the proxy hostname
* @return the builder
+ * @deprecated Use {@link Components#httpConfiguration()} with {@link HttpConfigurationBuilder#proxyHostAndPort(String, int)}.
*/
+ @Deprecated
public Builder proxyHost(String host) {
this.proxyHost = host;
return this;
}
/**
- * Set the port to use for an HTTP proxy for making connections to LaunchDarkly. This is required for proxied HTTP connections.
+ * Deprecated method for specifying the port of an HTTP proxy.
*
* @param port the proxy port
* @return the builder
+ * @deprecated Use {@link Components#httpConfiguration()} with {@link HttpConfigurationBuilder#proxyHostAndPort(String, int)}.
*/
+ @Deprecated
public Builder proxyPort(int port) {
this.proxyPort = port;
return this;
}
/**
- * Sets the username for the optional HTTP proxy. Only used when {@link LDConfig.Builder#proxyPassword(String)}
- * is also called.
+ * Deprecated method for specifying HTTP proxy authorization credentials.
*
* @param username the proxy username
* @return the builder
+ * @deprecated Use {@link Components#httpConfiguration()} with {@link HttpConfigurationBuilder#proxyAuth(com.launchdarkly.client.interfaces.HttpAuthentication)}
+ * and {@link Components#httpBasicAuthentication(String, String)}.
*/
+ @Deprecated
public Builder proxyUsername(String username) {
this.proxyUsername = username;
return this;
}
/**
- * Sets the password for the optional HTTP proxy. Only used when {@link LDConfig.Builder#proxyUsername(String)}
- * is also called.
+ * Deprecated method for specifying HTTP proxy authorization credentials.
*
* @param password the proxy password
* @return the builder
+ * @deprecated Use {@link Components#httpConfiguration()} with {@link HttpConfigurationBuilder#proxyAuth(com.launchdarkly.client.interfaces.HttpAuthentication)}
+ * and {@link Components#httpBasicAuthentication(String, String)}.
*/
+ @Deprecated
public Builder proxyPassword(String password) {
this.proxyPassword = password;
return this;
}
/**
- * Sets the {@link SSLSocketFactory} used to secure HTTPS connections to LaunchDarkly.
+ * Deprecated method for specifying a custom SSL socket factory and certificate trust manager.
*
* @param sslSocketFactory the SSL socket factory
* @param trustManager the trust manager
* @return the builder
*
* @since 4.7.0
+ * @deprecated Use {@link Components#httpConfiguration()} with {@link HttpConfigurationBuilder#sslSocketFactory(SSLSocketFactory, X509TrustManager)}.
*/
+ @Deprecated
public Builder sslSocketFactory(SSLSocketFactory sslSocketFactory, X509TrustManager trustManager) {
this.sslSocketFactory = sslSocketFactory;
this.trustManager = trustManager;
@@ -712,60 +718,33 @@ public Builder diagnosticOptOut(boolean diagnosticOptOut) {
}
/**
- * For use by wrapper libraries to set an identifying name for the wrapper being used. This will be included in a
- * header during requests to the LaunchDarkly servers to allow recording metrics on the usage of
- * these wrapper libraries.
+ * Deprecated method of specifing a wrapper library identifier.
*
* @param wrapperName an identifying name for the wrapper library
* @return the builder
* @since 4.12.0
+ * @deprecated Use {@link Components#httpConfiguration()} with {@link HttpConfigurationBuilder#wrapper(String, String)}.
*/
+ @Deprecated
public Builder wrapperName(String wrapperName) {
this.wrapperName = wrapperName;
return this;
}
/**
- * For use by wrapper libraries to report the version of the library in use. If {@link #wrapperName(String)} is not
- * set, this field will be ignored. Otherwise the version string will be included in a header along
- * with the wrapperName during requests to the LaunchDarkly servers.
+ * Deprecated method of specifing a wrapper library identifier.
*
- * @param wrapperVersion Version string for the wrapper library
+ * @param wrapperVersion version string for the wrapper library
* @return the builder
* @since 4.12.0
+ * @deprecated Use {@link Components#httpConfiguration()} with {@link HttpConfigurationBuilder#wrapper(String, String)}.
*/
+ @Deprecated
public Builder wrapperVersion(String wrapperVersion) {
this.wrapperVersion = wrapperVersion;
return this;
}
- // returns null if none of the proxy bits were configured. Minimum required part: port.
- Proxy proxy() {
- if (this.proxyPort == -1) {
- return null;
- } else {
- return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort));
- }
- }
-
- Authenticator proxyAuthenticator() {
- if (this.proxyUsername != null && this.proxyPassword != null) {
- final String credential = Credentials.basic(proxyUsername, proxyPassword);
- return new Authenticator() {
- public Request authenticate(Route route, Response response) throws IOException {
- if (response.request().header("Proxy-Authorization") != null) {
- return null; // Give up, we've already failed to authenticate with the proxy.
- } else {
- return response.request().newBuilder()
- .header("Proxy-Authorization", credential)
- .build();
- }
- }
- };
- }
- return null;
- }
-
/**
* Builds the configured {@link com.launchdarkly.client.LDConfig} object.
*
diff --git a/src/main/java/com/launchdarkly/client/PollingProcessor.java b/src/main/java/com/launchdarkly/client/PollingProcessor.java
index 7e97aa3b8..df3bf609a 100644
--- a/src/main/java/com/launchdarkly/client/PollingProcessor.java
+++ b/src/main/java/com/launchdarkly/client/PollingProcessor.java
@@ -3,6 +3,7 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.SettableFuture;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.launchdarkly.client.interfaces.SerializationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -76,6 +77,8 @@ public void run() {
} catch (IOException e) {
logger.error("Encountered exception in LaunchDarkly client when retrieving update: {}", e.toString());
logger.debug(e.toString(), e);
+ } catch (SerializationException e) {
+ logger.error("Polling request received malformed data: {}", e.toString());
}
}
}, 0L, pollIntervalMillis, TimeUnit.MILLISECONDS);
diff --git a/src/main/java/com/launchdarkly/client/StreamProcessor.java b/src/main/java/com/launchdarkly/client/StreamProcessor.java
index 764421a69..9da59b497 100644
--- a/src/main/java/com/launchdarkly/client/StreamProcessor.java
+++ b/src/main/java/com/launchdarkly/client/StreamProcessor.java
@@ -2,8 +2,9 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.SettableFuture;
-import com.google.gson.Gson;
import com.google.gson.JsonElement;
+import com.launchdarkly.client.interfaces.HttpConfiguration;
+import com.launchdarkly.client.interfaces.SerializationException;
import com.launchdarkly.eventsource.ConnectionErrorHandler;
import com.launchdarkly.eventsource.EventHandler;
import com.launchdarkly.eventsource.EventSource;
@@ -15,6 +16,8 @@
import java.io.IOException;
import java.net.URI;
+import java.util.AbstractMap;
+import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -22,12 +25,28 @@
import static com.launchdarkly.client.Util.getHeadersBuilderFor;
import static com.launchdarkly.client.Util.httpErrorMessage;
import static com.launchdarkly.client.Util.isHttpErrorRecoverable;
-import static com.launchdarkly.client.VersionedDataKind.FEATURES;
import static com.launchdarkly.client.VersionedDataKind.SEGMENTS;
import okhttp3.Headers;
import okhttp3.OkHttpClient;
+/**
+ * Implementation of the streaming data source, not including the lower-level SSE implementation which is in
+ * okhttp-eventsource.
+ *
+ * Error handling works as follows:
+ * 1. If any event is malformed, we must assume the stream is broken and we may have missed updates. Restart it.
+ * 2. If we try to put updates into the data store and we get an error, we must assume something's wrong with the
+ * data store. We must assume that updates have been lost, so we'll restart the stream. (Starting in version 5.0,
+ * we will be able to do this in a smarter way and not restart the stream until the store is actually working
+ * again, but in 4.x we don't have the monitoring mechanism for this.)
+ * 3. If we receive an unrecoverable error like HTTP 401, we close the stream and don't retry. Any other HTTP
+ * error or network error causes a retry with backoff.
+ * 4. We set the Future returned by start() to tell the client initialization logic that initialization has either
+ * succeeded (we got an initial payload and successfully stored it) or permanently failed (we got a 401, etc.).
+ * Otherwise, the client initialization method may time out but we will still be retrying in the background, and
+ * if we succeed then the client can detect that we're initialized now by calling our Initialized method.
+ */
final class StreamProcessor implements UpdateProcessor {
private static final String PUT = "put";
private static final String PATCH = "patch";
@@ -48,6 +67,7 @@ final class StreamProcessor implements UpdateProcessor {
private volatile EventSource es;
private final AtomicBoolean initialized = new AtomicBoolean(false);
private volatile long esStarted = 0;
+ private volatile boolean lastStoreUpdateFailed = false;
ConnectionErrorHandler connectionErrorHandler = createDefaultConnectionErrorHandler(); // exposed for testing
@@ -123,77 +143,112 @@ public void onClosed() throws Exception {
}
@Override
- public void onMessage(String name, MessageEvent event) throws Exception {
- Gson gson = new Gson();
- switch (name) {
- case PUT: {
- recordStreamInit(false);
- esStarted = 0;
- PutData putData = gson.fromJson(event.getData(), PutData.class);
- store.init(DefaultFeatureRequestor.toVersionedDataMap(putData.data));
- if (!initialized.getAndSet(true)) {
- initFuture.set(null);
- logger.info("Initialized LaunchDarkly client.");
+ public void onMessage(String name, MessageEvent event) {
+ try {
+ switch (name) {
+ case PUT: {
+ recordStreamInit(false);
+ esStarted = 0;
+ PutData putData = parseStreamJson(PutData.class, event.getData());
+ try {
+ store.init(DefaultFeatureRequestor.toVersionedDataMap(putData.data));
+ } catch (Exception e) {
+ throw new StreamStoreException(e);
+ }
+ if (!initialized.getAndSet(true)) {
+ initFuture.set(null);
+ logger.info("Initialized LaunchDarkly client.");
+ }
+ break;
}
- break;
- }
- case PATCH: {
- PatchData data = gson.fromJson(event.getData(), PatchData.class);
- if (FEATURES.getKeyFromStreamApiPath(data.path) != null) {
- store.upsert(FEATURES, gson.fromJson(data.data, FeatureFlag.class));
- } else if (SEGMENTS.getKeyFromStreamApiPath(data.path) != null) {
- store.upsert(SEGMENTS, gson.fromJson(data.data, Segment.class));
+ case PATCH: {
+ PatchData data = parseStreamJson(PatchData.class, event.getData());
+ Map.Entry
+ * If you want to set non-default values for any of these properties, create a builder with
+ * {@link Components#httpConfiguration()}, change its properties with the methods of this class,
+ * and pass it to {@link com.launchdarkly.client.LDConfig.Builder#http(HttpConfigurationFactory)}:
+ *
+ * These properties will override any equivalent deprecated properties that were set with {@code LDConfig.Builder},
+ * such as {@link com.launchdarkly.client.LDConfig.Builder#connectTimeoutMillis(int)}.
+ *
+ * Note that this class is abstract; the actual implementation is created by calling {@link Components#httpConfiguration()}.
+ *
+ * @since 4.13.0
+ */
+public abstract class HttpConfigurationBuilder implements HttpConfigurationFactory {
+ /**
+ * The default value for {@link #connectTimeoutMillis(int)}.
+ */
+ public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 2000;
+
+ /**
+ * The default value for {@link #socketTimeoutMillis(int)}.
+ */
+ public static final int DEFAULT_SOCKET_TIMEOUT_MILLIS = 10000;
+
+ protected int connectTimeoutMillis = DEFAULT_CONNECT_TIMEOUT_MILLIS;
+ protected HttpAuthentication proxyAuth;
+ protected String proxyHost;
+ protected int proxyPort;
+ protected int socketTimeoutMillis = DEFAULT_SOCKET_TIMEOUT_MILLIS;
+ protected SSLSocketFactory sslSocketFactory;
+ protected X509TrustManager trustManager;
+ protected String wrapperName;
+ protected String wrapperVersion;
+
+ /**
+ * Sets the connection timeout. This is the time allowed for the SDK to make a socket connection to
+ * any of the LaunchDarkly services.
+ *
+ * The default is {@link #DEFAULT_CONNECT_TIMEOUT_MILLIS}.
+ *
+ * @param connectTimeoutMillis the connection timeout, in milliseconds
+ * @return the builder
+ */
+ public HttpConfigurationBuilder connectTimeoutMillis(int connectTimeoutMillis) {
+ this.connectTimeoutMillis = connectTimeoutMillis;
+ return this;
+ }
+
+ /**
+ * Sets an HTTP proxy for making connections to LaunchDarkly.
+ *
+ * @param host the proxy hostname
+ * @param port the proxy port
+ * @return the builder
+ */
+ public HttpConfigurationBuilder proxyHostAndPort(String host, int port) {
+ this.proxyHost = host;
+ this.proxyPort = port;
+ return this;
+ }
+
+ /**
+ * Sets an authentication strategy for use with an HTTP proxy. This has no effect unless a proxy
+ * was specified with {@link #proxyHostAndPort(String, int)}.
+ *
+ * @param strategy the authentication strategy
+ * @return the builder
+ */
+ public HttpConfigurationBuilder proxyAuth(HttpAuthentication strategy) {
+ this.proxyAuth = strategy;
+ return this;
+ }
+
+ /**
+ * Sets the socket timeout. This is the amount of time without receiving data on a connection that the
+ * SDK will tolerate before signaling an error. This does not apply to the streaming connection
+ * used by {@link com.launchdarkly.client.Components#streamingDataSource()}, which has its own
+ * non-configurable read timeout based on the expected behavior of the LaunchDarkly streaming service.
+ *
+ * The default is {@link #DEFAULT_SOCKET_TIMEOUT_MILLIS}.
+ *
+ * @param socketTimeoutMillis the socket timeout, in milliseconds
+ * @return the builder
+ */
+ public HttpConfigurationBuilder socketTimeoutMillis(int socketTimeoutMillis) {
+ this.socketTimeoutMillis = socketTimeoutMillis;
+ return this;
+ }
+
+ /**
+ * Specifies a custom security configuration for HTTPS connections to LaunchDarkly.
+ *
+ * This uses the standard Java interfaces for configuring secure socket connections and certificate
+ * verification.
+ *
+ * @param sslSocketFactory the SSL socket factory
+ * @param trustManager the trust manager
+ * @return the builder
+ */
+ public HttpConfigurationBuilder sslSocketFactory(SSLSocketFactory sslSocketFactory, X509TrustManager trustManager) {
+ this.sslSocketFactory = sslSocketFactory;
+ this.trustManager = trustManager;
+ return this;
+ }
+
+ /**
+ * For use by wrapper libraries to set an identifying name for the wrapper being used. This will be included in a
+ * header during requests to the LaunchDarkly servers to allow recording metrics on the usage of
+ * these wrapper libraries.
+ *
+ * @param wrapperName an identifying name for the wrapper library
+ * @param wrapperVersion version string for the wrapper library
+ * @return the builder
+ */
+ public HttpConfigurationBuilder wrapper(String wrapperName, String wrapperVersion) {
+ this.wrapperName = wrapperName;
+ this.wrapperVersion = wrapperVersion;
+ return this;
+ }
+}
diff --git a/src/main/java/com/launchdarkly/client/interfaces/HttpAuthentication.java b/src/main/java/com/launchdarkly/client/interfaces/HttpAuthentication.java
new file mode 100644
index 000000000..879a201ec
--- /dev/null
+++ b/src/main/java/com/launchdarkly/client/interfaces/HttpAuthentication.java
@@ -0,0 +1,52 @@
+package com.launchdarkly.client.interfaces;
+
+/**
+ * Represents a supported method of HTTP authentication, including proxy authentication.
+ *
+ * @since 4.13.0
+ */
+public interface HttpAuthentication {
+ /**
+ * Computes the {@code Authorization} or {@code Proxy-Authorization} header for an authentication challenge.
+ *
+ * @param challenges the authentication challenges provided by the server, if any (may be empty if this is
+ * pre-emptive authentication)
+ * @return the value for the authorization request header
+ */
+ String provideAuthorization(Iterable
+ * Use {@link HttpConfigurationBuilder} to construct an instance.
+ *
+ * @since 4.13.0
+ */
+public interface HttpConfiguration {
+ /**
+ * The connection timeout. This is the time allowed for the underlying HTTP client to connect
+ * to the LaunchDarkly server.
+ *
+ * @return the connection timeout, in milliseconds
+ */
+ int getConnectTimeoutMillis();
+
+ /**
+ * The proxy configuration, if any.
+ *
+ * @return a {@link Proxy} instance or null
+ */
+ Proxy getProxy();
+
+ /**
+ * The authentication method to use for a proxy, if any. Ignored if {@link #getProxy()} is null.
+ *
+ * @return an {@link HttpAuthentication} implementation or null
+ */
+ HttpAuthentication getProxyAuthentication();
+
+ /**
+ * The socket timeout. This is the amount of time without receiving data on a connection that the
+ * SDK will tolerate before signaling an error. This does not apply to the streaming connection
+ * used by {@link com.launchdarkly.client.Components#streamingDataSource()}, which has its own
+ * non-configurable read timeout based on the expected behavior of the LaunchDarkly streaming service.
+ *
+ * @return the socket timeout, in milliseconds
+ */
+ int getSocketTimeoutMillis();
+
+ /**
+ * The configured socket factory for secure connections.
+ *
+ * @return a SSLSocketFactory or null
+ */
+ SSLSocketFactory getSslSocketFactory();
+
+ /**
+ * The configured trust manager for secure connections, if custom certificate verification is needed.
+ *
+ * @return an X509TrustManager or null
+ */
+ X509TrustManager getTrustManager();
+
+ /**
+ * An optional identifier used by wrapper libraries to indicate what wrapper is being used.
+ *
+ * This allows LaunchDarkly to gather metrics on the usage of wrappers that are based on the Java SDK.
+ * It is part of {@link HttpConfiguration} because it is included in HTTP headers.
+ *
+ * @return a wrapper identifier string or null
+ */
+ String getWrapperIdentifier();
+}
diff --git a/src/main/java/com/launchdarkly/client/interfaces/HttpConfigurationFactory.java b/src/main/java/com/launchdarkly/client/interfaces/HttpConfigurationFactory.java
new file mode 100644
index 000000000..ade4a5d48
--- /dev/null
+++ b/src/main/java/com/launchdarkly/client/interfaces/HttpConfigurationFactory.java
@@ -0,0 +1,16 @@
+package com.launchdarkly.client.interfaces;
+
+/**
+ * Interface for a factory that creates an {@link HttpConfiguration}.
+ *
+ * @see com.launchdarkly.client.Components#httpConfiguration()
+ * @see com.launchdarkly.client.LDConfig.Builder#http(HttpConfigurationFactory)
+ * @since 4.13.0
+ */
+public interface HttpConfigurationFactory {
+ /**
+ * Creates the configuration object.
+ * @return an {@link HttpConfiguration}
+ */
+ public HttpConfiguration createHttpConfiguration();
+}
diff --git a/src/main/java/com/launchdarkly/client/interfaces/SerializationException.java b/src/main/java/com/launchdarkly/client/interfaces/SerializationException.java
new file mode 100644
index 000000000..0473c991f
--- /dev/null
+++ b/src/main/java/com/launchdarkly/client/interfaces/SerializationException.java
@@ -0,0 +1,23 @@
+package com.launchdarkly.client.interfaces;
+
+/**
+ * General exception class for all errors in serializing or deserializing JSON.
+ *
+ * The SDK uses this class to avoid depending on exception types from the underlying JSON framework
+ * that it uses (currently Gson).
+ *
+ * This is currently an unchecked exception, because adding checked exceptions to existing SDK
+ * interfaces would be a breaking change. In the future it will become a checked exception, to make
+ * error-handling requirements clearer. However, public SDK client methods will not throw this
+ * exception in any case; it is only relevant when implementing custom components.
+ */
+@SuppressWarnings("serial")
+public class SerializationException extends RuntimeException {
+ /**
+ * Creates an instance.
+ * @param cause the underlying exception
+ */
+ public SerializationException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/main/java/com/launchdarkly/client/utils/FeatureStoreHelpers.java b/src/main/java/com/launchdarkly/client/utils/FeatureStoreHelpers.java
index 6fefb8e62..e49cbb7c7 100644
--- a/src/main/java/com/launchdarkly/client/utils/FeatureStoreHelpers.java
+++ b/src/main/java/com/launchdarkly/client/utils/FeatureStoreHelpers.java
@@ -5,6 +5,7 @@
import com.launchdarkly.client.FeatureStore;
import com.launchdarkly.client.VersionedData;
import com.launchdarkly.client.VersionedDataKind;
+import com.launchdarkly.client.interfaces.SerializationException;
/**
* Helper methods that may be useful for implementing a {@link FeatureStore} or {@link FeatureStoreCore}.
@@ -48,7 +49,7 @@ public static String marshalJson(VersionedData item) {
* Thrown by {@link FeatureStoreHelpers#unmarshalJson(VersionedDataKind, String)} for a deserialization error.
*/
@SuppressWarnings("serial")
- public static class UnmarshalException extends RuntimeException {
+ public static class UnmarshalException extends SerializationException {
/**
* Constructs an instance.
* @param cause the underlying exception
diff --git a/src/test/java/com/launchdarkly/client/DefaultEventProcessorTest.java b/src/test/java/com/launchdarkly/client/DefaultEventProcessorTest.java
index e652c3f7b..3ec9aab0b 100644
--- a/src/test/java/com/launchdarkly/client/DefaultEventProcessorTest.java
+++ b/src/test/java/com/launchdarkly/client/DefaultEventProcessorTest.java
@@ -819,8 +819,7 @@ public void wrapperHeaderSentWhenSet() throws Exception {
try (MockWebServer server = makeStartedServer(eventsSuccessResponse())) {
LDConfig config = new LDConfig.Builder()
.diagnosticOptOut(true)
- .wrapperName("Scala")
- .wrapperVersion("0.1.0")
+ .http(Components.httpConfiguration().wrapper("Scala", "0.1.0"))
.build();
try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(server), config)) {
Event e = EventFactory.DEFAULT.newIdentifyEvent(user);
@@ -832,24 +831,6 @@ public void wrapperHeaderSentWhenSet() throws Exception {
}
}
- @Test
- public void wrapperHeaderSentWithoutVersion() throws Exception {
- try (MockWebServer server = makeStartedServer(eventsSuccessResponse())) {
- LDConfig config = new LDConfig.Builder()
- .diagnosticOptOut(true)
- .wrapperName("Scala")
- .build();
-
- try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(server), config)) {
- Event e = EventFactory.DEFAULT.newIdentifyEvent(user);
- ep.sendEvent(e);
- }
-
- RecordedRequest req = server.takeRequest();
- assertThat(req.getHeader("X-LaunchDarkly-Wrapper"), equalTo("Scala"));
- }
- }
-
@Test
public void http400ErrorIsRecoverable() throws Exception {
testRecoverableHttpError(400);
@@ -907,7 +888,8 @@ public void httpClientCanUseCustomTlsConfig() throws Exception {
try (TestHttpUtil.ServerWithCert serverWithCert = httpsServerWithSelfSignedCert(eventsSuccessResponse())) {
EventProcessorBuilder ec = sendEvents().baseURI(serverWithCert.uri());
LDConfig config = new LDConfig.Builder()
- .sslSocketFactory(serverWithCert.socketFactory, serverWithCert.trustManager) // allows us to trust the self-signed cert
+ .http(Components.httpConfiguration().sslSocketFactory(serverWithCert.socketFactory, serverWithCert.trustManager))
+ // allows us to trust the self-signed cert
.build();
try (DefaultEventProcessor ep = makeEventProcessor(ec, config)) {
diff --git a/src/test/java/com/launchdarkly/client/DiagnosticEventTest.java b/src/test/java/com/launchdarkly/client/DiagnosticEventTest.java
index 20fe513ef..81f847bf0 100644
--- a/src/test/java/com/launchdarkly/client/DiagnosticEventTest.java
+++ b/src/test/java/com/launchdarkly/client/DiagnosticEventTest.java
@@ -89,21 +89,12 @@ public void testDefaultDiagnosticConfiguration() {
@Test
public void testCustomDiagnosticConfigurationGeneralProperties() {
LDConfig ldConfig = new LDConfig.Builder()
- .connectTimeout(5)
- .socketTimeout(20)
.startWaitMillis(10_000)
- .proxyPort(1234)
- .proxyUsername("username")
- .proxyPassword("password")
.build();
LDValue diagnosticJson = DiagnosticEvent.Init.getConfigurationData(ldConfig);
LDValue expected = expectedDefaultProperties()
- .put("connectTimeoutMillis", 5_000)
- .put("socketTimeoutMillis", 20_000)
.put("startWaitMillis", 10_000)
- .put("usingProxy", true)
- .put("usingProxyAuthenticator", true)
.build();
assertEquals(expected, diagnosticJson);
@@ -209,6 +200,29 @@ public void testCustomDiagnosticConfigurationForOffline() {
assertEquals(expected, diagnosticJson);
}
+ @Test
+ public void testCustomDiagnosticConfigurationHttpProperties() {
+ LDConfig ldConfig = new LDConfig.Builder()
+ .http(
+ Components.httpConfiguration()
+ .connectTimeoutMillis(5_000)
+ .socketTimeoutMillis(20_000)
+ .proxyHostAndPort("localhost", 1234)
+ .proxyAuth(Components.httpBasicAuthentication("username", "password"))
+ )
+ .build();
+
+ LDValue diagnosticJson = DiagnosticEvent.Init.getConfigurationData(ldConfig);
+ LDValue expected = expectedDefaultProperties()
+ .put("connectTimeoutMillis", 5_000)
+ .put("socketTimeoutMillis", 20_000)
+ .put("usingProxy", true)
+ .put("usingProxyAuthenticator", true)
+ .build();
+
+ assertEquals(expected, diagnosticJson);
+ }
+
@SuppressWarnings("deprecation")
@Test
public void testCustomDiagnosticConfigurationDeprecatedPropertiesForStreaming() {
@@ -263,4 +277,27 @@ public void testCustomDiagnosticConfigurationDeprecatedPropertyForDaemonMode() {
assertEquals(expected, diagnosticJson);
}
+
+ @SuppressWarnings("deprecation")
+ @Test
+ public void testCustomDiagnosticConfigurationDeprecatedHttpProperties() {
+ LDConfig ldConfig = new LDConfig.Builder()
+ .connectTimeout(5)
+ .socketTimeout(20)
+ .proxyPort(1234)
+ .proxyUsername("username")
+ .proxyPassword("password")
+ .build();
+
+ LDValue diagnosticJson = DiagnosticEvent.Init.getConfigurationData(ldConfig);
+ LDValue expected = expectedDefaultProperties()
+ .put("connectTimeoutMillis", 5_000)
+ .put("socketTimeoutMillis", 20_000)
+ .put("usingProxy", true)
+ .put("usingProxyAuthenticator", true)
+ .build();
+
+ assertEquals(expected, diagnosticJson);
+ }
+
}
diff --git a/src/test/java/com/launchdarkly/client/DiagnosticSdkTest.java b/src/test/java/com/launchdarkly/client/DiagnosticSdkTest.java
index ccdd229e0..f0c01184f 100644
--- a/src/test/java/com/launchdarkly/client/DiagnosticSdkTest.java
+++ b/src/test/java/com/launchdarkly/client/DiagnosticSdkTest.java
@@ -8,8 +8,8 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
+@SuppressWarnings("javadoc")
public class DiagnosticSdkTest {
-
private static final Gson gson = new Gson();
@Test
@@ -24,8 +24,7 @@ public void defaultFieldValues() {
@Test
public void getsWrapperValuesFromConfig() {
LDConfig config = new LDConfig.Builder()
- .wrapperName("Scala")
- .wrapperVersion("0.1.0")
+ .http(Components.httpConfiguration().wrapper("Scala", "0.1.0"))
.build();
DiagnosticSdk diagnosticSdk = new DiagnosticSdk(config);
assertEquals("java-server-sdk", diagnosticSdk.name);
@@ -46,8 +45,7 @@ public void gsonSerializationNoWrapper() {
@Test
public void gsonSerializationWithWrapper() {
LDConfig config = new LDConfig.Builder()
- .wrapperName("Scala")
- .wrapperVersion("0.1.0")
+ .http(Components.httpConfiguration().wrapper("Scala", "0.1.0"))
.build();
DiagnosticSdk diagnosticSdk = new DiagnosticSdk(config);
JsonObject jsonObject = gson.toJsonTree(diagnosticSdk).getAsJsonObject();
diff --git a/src/test/java/com/launchdarkly/client/EvaluationReasonTest.java b/src/test/java/com/launchdarkly/client/EvaluationReasonTest.java
index 4745aaaca..f1b409294 100644
--- a/src/test/java/com/launchdarkly/client/EvaluationReasonTest.java
+++ b/src/test/java/com/launchdarkly/client/EvaluationReasonTest.java
@@ -6,12 +6,124 @@
import org.junit.Test;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
@SuppressWarnings("javadoc")
public class EvaluationReasonTest {
private static final Gson gson = new Gson();
+ @Test
+ public void offProperties() {
+ EvaluationReason reason = EvaluationReason.off();
+ assertEquals(EvaluationReason.Kind.OFF, reason.getKind());
+ assertEquals(-1, reason.getRuleIndex());
+ assertNull(reason.getRuleId());
+ assertNull(reason.getPrerequisiteKey());
+ assertNull(reason.getErrorKind());
+ assertNull(reason.getException());
+ }
+
+ @Test
+ public void fallthroughProperties() {
+ EvaluationReason reason = EvaluationReason.fallthrough();
+ assertEquals(EvaluationReason.Kind.FALLTHROUGH, reason.getKind());
+ assertEquals(-1, reason.getRuleIndex());
+ assertNull(reason.getRuleId());
+ assertNull(reason.getPrerequisiteKey());
+ assertNull(reason.getErrorKind());
+ assertNull(reason.getException());
+ }
+
+ @Test
+ public void targetMatchProperties() {
+ EvaluationReason reason = EvaluationReason.targetMatch();
+ assertEquals(EvaluationReason.Kind.TARGET_MATCH, reason.getKind());
+ assertEquals(-1, reason.getRuleIndex());
+ assertNull(reason.getRuleId());
+ assertNull(reason.getPrerequisiteKey());
+ assertNull(reason.getErrorKind());
+ assertNull(reason.getException());
+ }
+
+ @Test
+ public void ruleMatchProperties() {
+ EvaluationReason reason = EvaluationReason.ruleMatch(2, "id");
+ assertEquals(EvaluationReason.Kind.RULE_MATCH, reason.getKind());
+ assertEquals(2, reason.getRuleIndex());
+ assertEquals("id", reason.getRuleId());
+ assertNull(reason.getPrerequisiteKey());
+ assertNull(reason.getErrorKind());
+ assertNull(reason.getException());
+ }
+
+ @Test
+ public void prerequisiteFailedProperties() {
+ EvaluationReason reason = EvaluationReason.prerequisiteFailed("prereq-key");
+ assertEquals(EvaluationReason.Kind.PREREQUISITE_FAILED, reason.getKind());
+ assertEquals(-1, reason.getRuleIndex());
+ assertNull(reason.getRuleId());
+ assertEquals("prereq-key", reason.getPrerequisiteKey());
+ assertNull(reason.getErrorKind());
+ assertNull(reason.getException());
+ }
+
+ @Test
+ public void errorProperties() {
+ EvaluationReason reason = EvaluationReason.error(EvaluationReason.ErrorKind.CLIENT_NOT_READY);
+ assertEquals(EvaluationReason.Kind.ERROR, reason.getKind());
+ assertEquals(-1, reason.getRuleIndex());
+ assertNull(reason.getRuleId());
+ assertNull(reason.getPrerequisiteKey());
+ assertEquals(EvaluationReason.ErrorKind.CLIENT_NOT_READY, reason.getErrorKind());
+ assertNull(reason.getException());
+ }
+
+ @Test
+ public void exceptionErrorProperties() {
+ Exception ex = new Exception("sorry");
+ EvaluationReason reason = EvaluationReason.exception(ex);
+ assertEquals(EvaluationReason.Kind.ERROR, reason.getKind());
+ assertEquals(-1, reason.getRuleIndex());
+ assertNull(reason.getRuleId());
+ assertNull(reason.getPrerequisiteKey());
+ assertEquals(EvaluationReason.ErrorKind.EXCEPTION, reason.getErrorKind());
+ assertEquals(ex, reason.getException());
+ }
+
+ @SuppressWarnings("deprecation")
+ @Test
+ public void deprecatedSubclassProperties() {
+ EvaluationReason ro = EvaluationReason.off();
+ assertEquals(EvaluationReason.Off.class, ro.getClass());
+
+ EvaluationReason rf = EvaluationReason.fallthrough();
+ assertEquals(EvaluationReason.Fallthrough.class, rf.getClass());
+
+ EvaluationReason rtm = EvaluationReason.targetMatch();
+ assertEquals(EvaluationReason.TargetMatch.class, rtm.getClass());
+
+ EvaluationReason rrm = EvaluationReason.ruleMatch(2, "id");
+ assertEquals(EvaluationReason.RuleMatch.class, rrm.getClass());
+ assertEquals(2, ((EvaluationReason.RuleMatch)rrm).getRuleIndex());
+ assertEquals("id", ((EvaluationReason.RuleMatch)rrm).getRuleId());
+
+ EvaluationReason rpf = EvaluationReason.prerequisiteFailed("prereq-key");
+ assertEquals(EvaluationReason.PrerequisiteFailed.class, rpf.getClass());
+ assertEquals("prereq-key", ((EvaluationReason.PrerequisiteFailed)rpf).getPrerequisiteKey());
+
+ EvaluationReason re = EvaluationReason.error(EvaluationReason.ErrorKind.CLIENT_NOT_READY);
+ assertEquals(EvaluationReason.Error.class, re.getClass());
+ assertEquals(EvaluationReason.ErrorKind.CLIENT_NOT_READY, ((EvaluationReason.Error)re).getErrorKind());
+ assertNull(((EvaluationReason.Error)re).getException());
+
+ Exception ex = new Exception("sorry");
+ EvaluationReason ree = EvaluationReason.exception(ex);
+ assertEquals(EvaluationReason.Error.class, ree.getClass());
+ assertEquals(EvaluationReason.ErrorKind.EXCEPTION, ((EvaluationReason.Error)ree).getErrorKind());
+ assertEquals(ex, ((EvaluationReason.Error)ree).getException());
+ }
+
@Test
public void testOffReasonSerialization() {
EvaluationReason reason = EvaluationReason.off();
@@ -73,9 +185,9 @@ public void testErrorSerializationWithException() {
@Test
public void errorInstancesAreReused() {
for (EvaluationReason.ErrorKind errorKind: EvaluationReason.ErrorKind.values()) {
- EvaluationReason.Error r0 = EvaluationReason.error(errorKind);
+ EvaluationReason r0 = EvaluationReason.error(errorKind);
assertEquals(errorKind, r0.getErrorKind());
- EvaluationReason.Error r1 = EvaluationReason.error(errorKind);
+ EvaluationReason r1 = EvaluationReason.error(errorKind);
assertSame(r0, r1);
}
}
diff --git a/src/test/java/com/launchdarkly/client/FeatureFlagTest.java b/src/test/java/com/launchdarkly/client/FeatureFlagTest.java
index 71791785c..c8c072605 100644
--- a/src/test/java/com/launchdarkly/client/FeatureFlagTest.java
+++ b/src/test/java/com/launchdarkly/client/FeatureFlagTest.java
@@ -12,7 +12,7 @@
import java.util.Arrays;
import static com.launchdarkly.client.EvaluationDetail.fromValue;
-import static com.launchdarkly.client.JsonHelpers.gsonInstance;
+import static com.launchdarkly.client.TestUtil.TEST_GSON_INSTANCE;
import static com.launchdarkly.client.TestUtil.booleanFlagWithClauses;
import static com.launchdarkly.client.TestUtil.fallthroughVariation;
import static com.launchdarkly.client.VersionedDataKind.FEATURES;
@@ -534,17 +534,17 @@ public void testSegmentMatchClauseFallsThroughIfSegmentNotFound() throws Excepti
@Test
public void flagIsDeserializedWithAllProperties() {
String json = flagWithAllPropertiesJson().toJsonString();
- FeatureFlag flag0 = gsonInstance().fromJson(json, FeatureFlag.class);
+ FeatureFlag flag0 = TEST_GSON_INSTANCE.fromJson(json, FeatureFlag.class);
assertFlagHasAllProperties(flag0);
- FeatureFlag flag1 = gsonInstance().fromJson(gsonInstance().toJson(flag0), FeatureFlag.class);
+ FeatureFlag flag1 = TEST_GSON_INSTANCE.fromJson(TEST_GSON_INSTANCE.toJson(flag0), FeatureFlag.class);
assertFlagHasAllProperties(flag1);
}
@Test
public void flagIsDeserializedWithMinimalProperties() {
String json = LDValue.buildObject().put("key", "flag-key").put("version", 99).build().toJsonString();
- FeatureFlag flag = gsonInstance().fromJson(json, FeatureFlag.class);
+ FeatureFlag flag = TEST_GSON_INSTANCE.fromJson(json, FeatureFlag.class);
assertEquals("flag-key", flag.getKey());
assertEquals(99, flag.getVersion());
assertFalse(flag.isOn());
diff --git a/src/test/java/com/launchdarkly/client/FeatureRequestorTest.java b/src/test/java/com/launchdarkly/client/FeatureRequestorTest.java
index 6b8ad7646..dacb9b0a2 100644
--- a/src/test/java/com/launchdarkly/client/FeatureRequestorTest.java
+++ b/src/test/java/com/launchdarkly/client/FeatureRequestorTest.java
@@ -178,7 +178,8 @@ public void httpClientCanUseCustomTlsConfig() throws Exception {
try (TestHttpUtil.ServerWithCert serverWithCert = httpsServerWithSelfSignedCert(resp)) {
LDConfig config = new LDConfig.Builder()
- .sslSocketFactory(serverWithCert.socketFactory, serverWithCert.trustManager) // allows us to trust the self-signed cert
+ .http(Components.httpConfiguration().sslSocketFactory(serverWithCert.socketFactory, serverWithCert.trustManager))
+ // allows us to trust the self-signed cert
.build();
try (DefaultFeatureRequestor r = makeRequestor(serverWithCert.server, config)) {
@@ -194,8 +195,7 @@ public void httpClientCanUseProxyConfig() throws Exception {
try (MockWebServer server = makeStartedServer(jsonResponse(flag1Json))) {
HttpUrl serverUrl = server.url("/");
LDConfig config = new LDConfig.Builder()
- .proxyHost(serverUrl.host())
- .proxyPort(serverUrl.port())
+ .http(Components.httpConfiguration().proxyHostAndPort(serverUrl.host(), serverUrl.port()))
.build();
try (DefaultFeatureRequestor r = new DefaultFeatureRequestor(sdkKey, config.httpConfig, fakeBaseUri, true)) {
diff --git a/src/test/java/com/launchdarkly/client/LDClientEndToEndTest.java b/src/test/java/com/launchdarkly/client/LDClientEndToEndTest.java
index 22e897712..355090516 100644
--- a/src/test/java/com/launchdarkly/client/LDClientEndToEndTest.java
+++ b/src/test/java/com/launchdarkly/client/LDClientEndToEndTest.java
@@ -70,7 +70,8 @@ public void clientStartsInPollingModeWithSelfSignedCert() throws Exception {
LDConfig config = new LDConfig.Builder()
.dataSource(basePollingConfig(serverWithCert.server))
.events(noEvents())
- .sslSocketFactory(serverWithCert.socketFactory, serverWithCert.trustManager) // allows us to trust the self-signed cert
+ .http(Components.httpConfiguration().sslSocketFactory(serverWithCert.socketFactory, serverWithCert.trustManager))
+ // allows us to trust the self-signed cert
.build();
try (LDClient client = new LDClient(sdkKey, config)) {
@@ -126,7 +127,8 @@ public void clientStartsInStreamingModeWithSelfSignedCert() throws Exception {
LDConfig config = new LDConfig.Builder()
.dataSource(baseStreamingConfig(serverWithCert.server))
.events(noEvents())
- .sslSocketFactory(serverWithCert.socketFactory, serverWithCert.trustManager) // allows us to trust the self-signed cert
+ .http(Components.httpConfiguration().sslSocketFactory(serverWithCert.socketFactory, serverWithCert.trustManager))
+ // allows us to trust the self-signed cert
.build();
try (LDClient client = new LDClient(sdkKey, config)) {
diff --git a/src/test/java/com/launchdarkly/client/LDConfigTest.java b/src/test/java/com/launchdarkly/client/LDConfigTest.java
index 0f9d074f1..3e89c7a5a 100644
--- a/src/test/java/com/launchdarkly/client/LDConfigTest.java
+++ b/src/test/java/com/launchdarkly/client/LDConfigTest.java
@@ -1,128 +1,218 @@
package com.launchdarkly.client;
+import com.launchdarkly.client.integrations.HttpConfigurationBuilderTest;
+import com.launchdarkly.client.interfaces.HttpConfiguration;
+
import org.junit.Test;
import java.net.InetSocketAddress;
import java.net.Proxy;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509TrustManager;
+
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.assertSame;
import static org.junit.Assert.assertTrue;
@SuppressWarnings("javadoc")
public class LDConfigTest {
+ @SuppressWarnings("deprecation")
+ @Test
+ public void testMinimumPollingIntervalIsEnforcedProperly(){
+ LDConfig config = new LDConfig.Builder().pollingIntervalMillis(10L).build();
+ assertEquals(30000L, config.deprecatedPollingIntervalMillis);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Test
+ public void testPollingIntervalIsEnforcedProperly(){
+ LDConfig config = new LDConfig.Builder().pollingIntervalMillis(30001L).build();
+ assertEquals(30001L, config.deprecatedPollingIntervalMillis);
+ }
+
+ @Test
+ public void testSendEventsDefaultsToTrue() {
+ LDConfig config = new LDConfig.Builder().build();
+ assertEquals(true, config.deprecatedSendEvents);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Test
+ public void testSendEventsCanBeSetToFalse() {
+ LDConfig config = new LDConfig.Builder().sendEvents(false).build();
+ assertEquals(false, config.deprecatedSendEvents);
+ }
+
+ @Test
+ public void testDefaultDiagnosticOptOut() {
+ LDConfig config = new LDConfig.Builder().build();
+ assertFalse(config.diagnosticOptOut);
+ }
+
+ @Test
+ public void testDiagnosticOptOut() {
+ LDConfig config = new LDConfig.Builder().diagnosticOptOut(true).build();
+ assertTrue(config.diagnosticOptOut);
+ }
+
+ @Test
+ public void testWrapperNotConfigured() {
+ LDConfig config = new LDConfig.Builder().build();
+ assertNull(config.httpConfig.getWrapperIdentifier());
+ }
+
+ @Test
+ public void testWrapperNameOnly() {
+ LDConfig config = new LDConfig.Builder()
+ .http(
+ Components.httpConfiguration()
+ .wrapper("Scala", null)
+ )
+ .build();
+ assertEquals("Scala", config.httpConfig.getWrapperIdentifier());
+ }
+
@Test
- public void testNoProxyConfigured() {
+ public void testWrapperWithVersion() {
+ LDConfig config = new LDConfig.Builder()
+ .http(
+ Components.httpConfiguration()
+ .wrapper("Scala", "0.1.0")
+ )
+ .build();
+ assertEquals("Scala/0.1.0", config.httpConfig.getWrapperIdentifier());
+ }
+
+ @Test
+ public void testHttpDefaults() {
LDConfig config = new LDConfig.Builder().build();
- assertNull(config.httpConfig.proxy);
- assertNull(config.httpConfig.proxyAuthenticator);
+ HttpConfiguration hc = config.httpConfig;
+ HttpConfiguration defaults = Components.httpConfiguration().createHttpConfiguration();
+ assertEquals(defaults.getConnectTimeoutMillis(), hc.getConnectTimeoutMillis());
+ assertNull(hc.getProxy());
+ assertNull(hc.getProxyAuthentication());
+ assertEquals(defaults.getSocketTimeoutMillis(), hc.getSocketTimeoutMillis());
+ assertNull(hc.getSslSocketFactory());
+ assertNull(hc.getTrustManager());
+ assertNull(hc.getWrapperIdentifier());
}
+ @SuppressWarnings("deprecation")
@Test
- public void testOnlyProxyHostConfiguredIsNull() {
+ public void testDeprecatedHttpConnectTimeout() {
+ LDConfig config = new LDConfig.Builder().connectTimeoutMillis(999).build();
+ assertEquals(999, config.httpConfig.getConnectTimeoutMillis());
+ }
+
+ @SuppressWarnings("deprecation")
+ @Test
+ public void testDeprecatedHttpConnectTimeoutSeconds() {
+ LDConfig config = new LDConfig.Builder().connectTimeout(999).build();
+ assertEquals(999000, config.httpConfig.getConnectTimeoutMillis());
+ }
+
+ @SuppressWarnings("deprecation")
+ @Test
+ public void testDeprecatedHttpSocketTimeout() {
+ LDConfig config = new LDConfig.Builder().socketTimeoutMillis(999).build();
+ assertEquals(999, config.httpConfig.getSocketTimeoutMillis());
+ }
+
+ @SuppressWarnings("deprecation")
+ @Test
+ public void testDeprecatedHttpSocketTimeoutSeconds() {
+ LDConfig config = new LDConfig.Builder().socketTimeout(999).build();
+ assertEquals(999000, config.httpConfig.getSocketTimeoutMillis());
+ }
+
+ @SuppressWarnings("deprecation")
+ @Test
+ public void testDeprecatedHttpOnlyProxyHostConfiguredIsNull() {
LDConfig config = new LDConfig.Builder().proxyHost("bla").build();
- assertNull(config.httpConfig.proxy);
+ assertNull(config.httpConfig.getProxy());
}
+ @SuppressWarnings("deprecation")
@Test
- public void testOnlyProxyPortConfiguredHasPortAndDefaultHost() {
+ public void testDeprecatedHttpOnlyProxyPortConfiguredHasPortAndDefaultHost() {
LDConfig config = new LDConfig.Builder().proxyPort(1234).build();
- assertEquals(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("localhost", 1234)), config.httpConfig.proxy);
+ assertEquals(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("localhost", 1234)), config.httpConfig.getProxy());
}
+
+ @SuppressWarnings("deprecation")
@Test
- public void testProxy() {
+ public void testDeprecatedHttpProxy() {
LDConfig config = new LDConfig.Builder()
.proxyHost("localhost2")
.proxyPort(4444)
.build();
- assertEquals(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("localhost2", 4444)), config.httpConfig.proxy);
+ assertEquals(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("localhost2", 4444)), config.httpConfig.getProxy());
}
+ @SuppressWarnings("deprecation")
@Test
- public void testProxyAuth() {
+ public void testDeprecatedHttpProxyAuth() {
LDConfig config = new LDConfig.Builder()
.proxyHost("localhost2")
.proxyPort(4444)
- .proxyUsername("proxyUser")
- .proxyPassword("proxyPassword")
+ .proxyUsername("user")
+ .proxyPassword("pass")
.build();
- assertNotNull(config.httpConfig.proxy);
- assertNotNull(config.httpConfig.proxyAuthenticator);
+ assertNotNull(config.httpConfig.getProxy());
+ assertNotNull(config.httpConfig.getProxyAuthentication());
+ assertEquals("Basic dXNlcjpwYXNz", config.httpConfig.getProxyAuthentication().provideAuthorization(null));
}
+ @SuppressWarnings("deprecation")
@Test
- public void testProxyAuthPartialConfig() {
+ public void testDeprecatedHttpProxyAuthPartialConfig() {
LDConfig config = new LDConfig.Builder()
.proxyHost("localhost2")
.proxyPort(4444)
.proxyUsername("proxyUser")
.build();
- assertNotNull(config.httpConfig.proxy);
- assertNull(config.httpConfig.proxyAuthenticator);
+ assertNotNull(config.httpConfig.getProxy());
+ assertNull(config.httpConfig.getProxyAuthentication());
config = new LDConfig.Builder()
.proxyHost("localhost2")
.proxyPort(4444)
.proxyPassword("proxyPassword")
.build();
- assertNotNull(config.httpConfig.proxy);
- assertNull(config.httpConfig.proxyAuthenticator);
+ assertNotNull(config.httpConfig.getProxy());
+ assertNull(config.httpConfig.getProxyAuthentication());
}
@SuppressWarnings("deprecation")
@Test
- public void testMinimumPollingIntervalIsEnforcedProperly(){
- LDConfig config = new LDConfig.Builder().pollingIntervalMillis(10L).build();
- assertEquals(30000L, config.deprecatedPollingIntervalMillis);
+ public void testDeprecatedHttpSslOptions() {
+ SSLSocketFactory sf = new HttpConfigurationBuilderTest.StubSSLSocketFactory();
+ X509TrustManager tm = new HttpConfigurationBuilderTest.StubX509TrustManager();
+ LDConfig config = new LDConfig.Builder().sslSocketFactory(sf, tm).build();
+ assertSame(sf, config.httpConfig.getSslSocketFactory());
+ assertSame(tm, config.httpConfig.getTrustManager());
}
@SuppressWarnings("deprecation")
@Test
- public void testPollingIntervalIsEnforcedProperly(){
- LDConfig config = new LDConfig.Builder().pollingIntervalMillis(30001L).build();
- assertEquals(30001L, config.deprecatedPollingIntervalMillis);
- }
-
- @Test
- public void testSendEventsDefaultsToTrue() {
- LDConfig config = new LDConfig.Builder().build();
- assertEquals(true, config.deprecatedSendEvents);
- }
-
- @SuppressWarnings("deprecation")
- @Test
- public void testSendEventsCanBeSetToFalse() {
- LDConfig config = new LDConfig.Builder().sendEvents(false).build();
- assertEquals(false, config.deprecatedSendEvents);
- }
-
- @Test
- public void testDefaultDiagnosticOptOut() {
- LDConfig config = new LDConfig.Builder().build();
- assertFalse(config.diagnosticOptOut);
- }
-
- @Test
- public void testDiagnosticOptOut() {
- LDConfig config = new LDConfig.Builder().diagnosticOptOut(true).build();
- assertTrue(config.diagnosticOptOut);
+ public void testDeprecatedHttpWrapperNameOnly() {
+ LDConfig config = new LDConfig.Builder()
+ .wrapperName("Scala")
+ .build();
+ assertEquals("Scala", config.httpConfig.getWrapperIdentifier());
}
+ @SuppressWarnings("deprecation")
@Test
- public void testWrapperNotConfigured() {
- LDConfig config = new LDConfig.Builder().build();
- assertNull(config.httpConfig.wrapperName);
- assertNull(config.httpConfig.wrapperVersion);
- }
-
- @Test public void testWrapperConfigured() {
+ public void testDeprecatedHttpWrapperWithVersion() {
LDConfig config = new LDConfig.Builder()
.wrapperName("Scala")
.wrapperVersion("0.1.0")
.build();
- assertEquals("Scala", config.httpConfig.wrapperName);
- assertEquals("0.1.0", config.httpConfig.wrapperVersion);
+ assertEquals("Scala/0.1.0", config.httpConfig.getWrapperIdentifier());
}
}
\ No newline at end of file
diff --git a/src/test/java/com/launchdarkly/client/LDUserTest.java b/src/test/java/com/launchdarkly/client/LDUserTest.java
index c4c6adeb8..fd66966f6 100644
--- a/src/test/java/com/launchdarkly/client/LDUserTest.java
+++ b/src/test/java/com/launchdarkly/client/LDUserTest.java
@@ -18,8 +18,8 @@
import java.util.Map;
import java.util.Set;
-import static com.launchdarkly.client.JsonHelpers.gsonInstance;
import static com.launchdarkly.client.JsonHelpers.gsonInstanceForEventsSerialization;
+import static com.launchdarkly.client.TestUtil.TEST_GSON_INSTANCE;
import static com.launchdarkly.client.TestUtil.defaultEventsConfig;
import static com.launchdarkly.client.TestUtil.jbool;
import static com.launchdarkly.client.TestUtil.jdouble;
@@ -172,6 +172,7 @@ public void canSetAnonymous() {
assertEquals(true, user.getAnonymous().booleanValue());
}
+ @SuppressWarnings("deprecation")
@Test
public void canSetCountry() {
LDUser user = new LDUser.Builder("key").country(LDCountryCode.US).build();
@@ -216,6 +217,7 @@ public void invalidCountryNameDoesNotSetCountry() {
assertEquals(LDValue.ofNull(), user.getCountry());
}
+ @SuppressWarnings("deprecation")
@Test
public void canSetPrivateCountry() {
LDUser user = new LDUser.Builder("key").privateCountry(LDCountryCode.US).build();
@@ -297,8 +299,8 @@ public void canSetPrivateDeprecatedCustomJsonValue() {
@Test
public void testAllPropertiesInDefaultEncoding() {
for (Map.Entrylocalhost.
+ * Deprecated method for specifying an HTTP proxy.
+ *
+ * If this is not set, but {@link #proxyPort(int)} is specified, this will default to localhost.
*
+ *
+ * LDConfig config = new LDConfig.Builder()
+ * .http(
+ * Components.httpConfiguration()
+ * .connectTimeoutMillis(3000)
+ * .proxyHostAndPort("my-proxy", 8080)
+ * )
+ * .build();
+ *