diff --git a/.circleci/config.yml b/.circleci/config.yml index 5ba8048..631548b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,11 +7,6 @@ workflows: test: jobs: - build-linux - - test-linux: - name: Java 8 - Linux - OpenJDK - docker-image: cimg/openjdk:8.0 - requires: - - build-linux - test-linux: name: Java 11 - Linux - OpenJDK docker-image: cimg/openjdk:11.0 @@ -36,7 +31,7 @@ workflows: jobs: build-linux: docker: - - image: cimg/openjdk:8.0 + - image: cimg/openjdk:11.0 steps: - checkout - run: java -version @@ -81,7 +76,7 @@ jobs: packaging: docker: - - image: cimg/openjdk:8.0 + - image: cimg/openjdk:11.0 steps: - run: java -version - run: sudo apt-get install make -y -q diff --git a/README.md b/README.md index afed7b9..295f7cd 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,8 @@ Your project will need compatible versions of the LaunchDarkly Server-Side SDK f Example gradle dependencies: ```groovy -implementation group: 'com.launchdarkly', name: 'launchdarkly-java-server-sdk', version: '[6.0.0, 7.0.0)' -implementation 'dev.openfeature:sdk:[1.2.0,2.0.0)' +implementation group: 'com.launchdarkly', name: 'launchdarkly-java-server-sdk', version: '[7.1.0, 8.0.0)' +implementation 'dev.openfeature:sdk:[1.7.0,2.0.0)' ``` ### Installation @@ -54,8 +54,7 @@ import com.launchdarkly.openfeature.serverprovider.Provider; public class Main { public static void main(String[] args) { - LDClient ldClient = new LDClient("my-sdk-key"); - OpenFeatureAPI.getInstance().setProvider(new Provider(ldClient)); + OpenFeatureAPI.getInstance().setProvider(new Provider("my-sdk-key")); // Refer to OpenFeature documentation for getting a client and performing evaluations. } @@ -86,6 +85,16 @@ There are several other attributes which have special functionality within a sin - A key of `anonymous`. Must be a boolean value. [Equivalent to the 'anonymous' builder method in the SDK.](https://launchdarkly.github.io/java-server-sdk/com/launchdarkly/sdk/ContextBuilder.html#anonymous(boolean)) - A key of `name`. Must be a string. [Equivalent to the 'name' builder method in the SDK.](https://launchdarkly.github.io/java-server-sdk/com/launchdarkly/sdk/ContextBuilder.html#name(java.lang.String)) +### Initialization and Shutdown + +The LaunchDarkly supports Initialization and Shutdown using the OpenFeature API. The provider begins initialization as soon as it is constructed, and the underlying LaunchDarkly SDK will block execution based on the configured start wait time. If you wish to defer the blocking behavior, then you can use the `startWait` function when building the `LDConfig`. + +OpenFeature will report when the provider is ready, and additionally the `setProviderAndWait` function of the OpenFeature +API can be used to wait until the provider is ready, or it has encountered a permanent error. + +It the provider has been shutdown, because the OpenFeature API has been shutdown, or because the provider was no longer in use by the OpenFeature API, then the underlying LaunchDarkly SDK will be closed. +This is an important consideration if you are using the `getLdClient` method of the provider to access the underlying SDK instance. + ### Examples #### A single user context diff --git a/build.gradle b/build.gradle index 45da11f..3844789 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,8 @@ java { repositories { mavenLocal() mavenCentral() + // Before LaunchDarkly release artifacts get synced to Maven Central they are here along with snapshots: + maven { url "https://oss.sonatype.org/content/groups/public/" } } test { @@ -41,6 +43,15 @@ checkstyle { checkstyleTest.enabled = false } +task generateJava(type: Copy) { + // This updates Version.java + from 'src/templates/java' + into "src/main/java" + filter(org.apache.tools.ant.filters.ReplaceTokens, tokens: [VERSION: version.toString()]) +} + +compileJava.dependsOn 'generateJava' + publishing { publications { mavenJava(MavenPublication) { @@ -111,11 +122,19 @@ dependencies { // This dependency is used internally, and not exposed to consumers on their own compile classpath. implementation 'com.google.guava:guava:23.0' - implementation group: 'com.launchdarkly', name: 'launchdarkly-java-server-sdk', version: '[6.0.0, 7.0.0)' - implementation 'dev.openfeature:sdk:[1.2.0,2.0.0)' + implementation group: 'com.launchdarkly', name: 'launchdarkly-java-server-sdk', version: '[7.1.0, 8.0.0)' + + implementation 'dev.openfeature:sdk:[1.7.0,2.0.0)' // Use JUnit test framework - testImplementation 'junit:junit:4.12' + testImplementation(platform('org.junit:junit-bom:5.10.0')) + testImplementation('org.junit.jupiter:junit-jupiter') testImplementation "org.mockito:mockito-core:3.+" } +test { + useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed" + } +} diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationContextConverter.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationContextConverter.java index 4e913f8..68540d5 100644 --- a/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationContextConverter.java +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationContextConverter.java @@ -90,7 +90,7 @@ private String getTargetingKey(String targetingKey, Value keyAsValue) { targetingKey = !Objects.equals(targetingKey, "") ? targetingKey : keyAsValue.asString(); } - if (targetingKey == null || targetingKey.equals("")) { + if (targetingKey == null || targetingKey.isEmpty()) { logger.error("The EvaluationContext must contain either a 'targetingKey' or a 'key' and the type " + "must be a string."); } return targetingKey; diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java index 9d6cf53..df3af64 100644 --- a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java @@ -1,32 +1,36 @@ package com.launchdarkly.openfeature.serverprovider; -import com.launchdarkly.logging.LDLogAdapter; import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.EvaluationDetail; import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.server.Components; import com.launchdarkly.sdk.server.LDClient; +import com.launchdarkly.sdk.server.LDConfig; +import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider; import com.launchdarkly.sdk.server.interfaces.LDClientInterface; -import com.launchdarkly.sdk.server.subsystems.LoggingConfiguration; import dev.openfeature.sdk.*; +import java.io.IOException; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.concurrent.TimeoutException; + /** * An OpenFeature {@link FeatureProvider} which enables the use of the LaunchDarkly Server-Side SDK for Java * with OpenFeature. *
- *import dev.openfeature.sdk.OpenFeatureAPI;
- *import com.launchdarkly.sdk.server.LDClient;
+ * import dev.openfeature.sdk.OpenFeatureAPI;
*
- *public class Main {
+ * public class Main {
* public static void main(String[] args) {
- * LDClient ldClient = new LDClient("my-sdk-key");
- * OpenFeatureAPI.getInstance().setProvider(new Provider(ldClient));
+ * OpenFeatureAPI.getInstance().setProvider(new Provider("fake-key"));
*
* // Refer to OpenFeature documentation for getting a client and performing evaluations.
* }
- *}
+ * }
*
*/
-public class Provider implements FeatureProvider {
+public class Provider extends EventProvider {
private static final class ProviderMetaData implements Metadata {
@Override
public String getName() {
@@ -43,39 +47,38 @@ public String getName() {
private final LDClientInterface client;
+ private ProviderState state = ProviderState.NOT_READY;
+
/**
- * Create a provider with the given LaunchDarkly client and provider configuration.
- *
- * // Using the provider with a custom log level.
- * new Provider(ldclient, ProviderConfiguration
- * .builder()
- * .logging(Components.logging().level(LDLogLevel.INFO)
- * .build());
- *
+ * Create a provider with the specified SDK and default configuration.
+ * + * If you need to specify any configuration use {@link Provider#Provider(String, LDConfig)} instead. * - * @param client A {@link LDClient} instance. - * @param config Configuration for the provider. + * @param sdkKey the SDK key for your LaunchDarkly environment */ - public Provider(LDClientInterface client, ProviderConfiguration config) { - this.client = client; - LoggingConfiguration loggingConfig = config.getLoggingConfigurationFactory().build(null); - LDLogAdapter adapter = loggingConfig.getLogAdapter(); - logger = LDLogger.withAdapter(adapter, loggingConfig.getBaseLoggerName()); - - evaluationContextConverter = new EvaluationContextConverter(logger); - evaluationDetailConverter = new EvaluationDetailConverter(logger); - valueConverter = new ValueConverter(logger); + public Provider(String sdkKey) { + this(sdkKey, new LDConfig.Builder().build()); } /** - * Create a provider with the given LaunchDarkly client. - *
- * The provider will be created with default configuration.
+ * Crate a provider with the specified SDK key and configuration.
*
- * @param client A {@link LDClient} instance.
+ * @param sdkKey the SDK key for your LaunchDarkly environment
+ * @param config a client configuration object
*/
- public Provider(LDClientInterface client) {
- this(client, ProviderConfiguration.builder().build());
+ public Provider(String sdkKey, LDConfig config) {
+ this(new LDClient(sdkKey, LDConfig.Builder.fromConfig(config)
+ .wrapper(Components.wrapperInfo()
+ .wrapperName("open-feature-java-server")
+ .wrapperVersion(Version.SDK_VERSION)).build()));
+ }
+
+ Provider(LDClientInterface client) {
+ this.client = client;
+ logger = client.getLogger();
+ evaluationContextConverter = new EvaluationContextConverter(logger);
+ evaluationDetailConverter = new EvaluationDetailConverter(logger);
+ valueConverter = new ValueConverter(logger);
}
@Override
@@ -86,7 +89,7 @@ public Metadata getMetadata() {
@Override
public ProviderEvaluation
+ * This can be used to access LaunchDarkly features which are not available in OpenFeature.
+ *
+ * @return the launchdarkly client instance
+ */
+ public LDClientInterface getLdClient() {
+ return client;
+ }
}
diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/ProviderConfiguration.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/ProviderConfiguration.java
deleted file mode 100644
index b233bae..0000000
--- a/src/main/java/com/launchdarkly/openfeature/serverprovider/ProviderConfiguration.java
+++ /dev/null
@@ -1,79 +0,0 @@
-package com.launchdarkly.openfeature.serverprovider;
-
-import com.launchdarkly.logging.LDLogAdapter;
-import com.launchdarkly.sdk.server.Components;
-import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer;
-import com.launchdarkly.sdk.server.subsystems.LoggingConfiguration;
-
-/**
- * An immutable configuration for the provider. Must be created using a {@link ProviderConfigurationBuilder}.
- *
- * You will most often use {@link com.launchdarkly.openfeature.serverprovider.Provider} (the provider) and
- * {@link com.launchdarkly.openfeature.serverprovider.ProviderConfiguration} (configuration options for the provider).
- *
+ * You will most often use {@link com.launchdarkly.openfeature.serverprovider.Provider} (the provider).
*/
package com.launchdarkly.openfeature.serverprovider;
diff --git a/src/templates/java/com/launchdarkly/openfeature/serverprovider/Version.java b/src/templates/java/com/launchdarkly/openfeature/serverprovider/Version.java
new file mode 100644
index 0000000..c4145cc
--- /dev/null
+++ b/src/templates/java/com/launchdarkly/openfeature/serverprovider/Version.java
@@ -0,0 +1,8 @@
+package com.launchdarkly.openfeature.serverprovider;
+
+abstract class Version {
+ private Version() {}
+
+ // This constant is updated automatically by our Gradle script during a release, if the project version has changed
+ static final String SDK_VERSION = "@VERSION@";
+}
diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/ContextConverterTest.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/ContextConverterTest.java
index fe4d9f2..cebb687 100644
--- a/src/test/java/com/launchdarkly/openfeature/serverprovider/ContextConverterTest.java
+++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/ContextConverterTest.java
@@ -8,12 +8,12 @@
import dev.openfeature.sdk.ImmutableContext;
import dev.openfeature.sdk.ImmutableStructure;
import dev.openfeature.sdk.Value;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.HashMap;
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
public class ContextConverterTest {
TestLogger testLogger = new TestLogger();
diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverterTest.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverterTest.java
index 21c147a..8cb0c3b 100644
--- a/src/test/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverterTest.java
+++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverterTest.java
@@ -3,11 +3,11 @@
import com.launchdarkly.logging.LDLogger;
import com.launchdarkly.sdk.*;
import dev.openfeature.sdk.*;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
import java.util.List;
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
public class EvaluationDetailConverterTest {
private final Double EPSILON = 0.00001;
diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/EventsTest.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/EventsTest.java
new file mode 100644
index 0000000..be63d0f
--- /dev/null
+++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/EventsTest.java
@@ -0,0 +1,45 @@
+package com.launchdarkly.openfeature.serverprovider;
+
+import com.launchdarkly.sdk.LDValue;
+import com.launchdarkly.sdk.server.Components;
+import com.launchdarkly.sdk.server.LDConfig;
+import com.launchdarkly.sdk.server.integrations.TestData;
+import dev.openfeature.sdk.OpenFeatureAPI;
+import dev.openfeature.sdk.ProviderEvent;
+import org.junit.jupiter.api.Test;
+
+import java.util.concurrent.ArrayBlockingQueue;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Tests in this suite use a real client instance and the public constructor.
+ *
+ * Detailed provider tests use a mock client to test specific result and context conversions.
+ */
+public class EventsTest {
+ @Test
+ public void emitsFlagChangeEvents() throws InterruptedException {
+ var td = TestData.dataSource();
+ td.update(td.flag("flagA").valueForAll(LDValue.of("test")));
+
+ var provider = new Provider("fake-key", new LDConfig.Builder().dataSource(td)
+ .events(Components.noEvents()).build());
+
+ var changes = new ArrayBlockingQueue
+ * Detailed provider tests use a mock client to test specific result and context conversions.
+ */
+public class LifeCycleTest {
+ @Test
+ public void canCallThePublicConstructor() {
+ assertDoesNotThrow(() -> {
+ var provider = new Provider("fake-key", new LDConfig.Builder()
+ .offline(true).build());
+ });
+ }
+
+ @Test
+ public void canInitializeAnOfflineClient() {
+ assertDoesNotThrow(() -> {
+ var provider = new Provider("fake-key", new LDConfig.Builder()
+ .offline(true).build());
+ provider.initialize(new ImmutableContext("context-key"));
+ assertEquals(ProviderState.READY, provider.getState());
+ var ldClient = provider.getLdClient();
+ assertEquals(DataSourceStatusProvider.State.VALID, ldClient.getDataSourceStatusProvider().getStatus().getState());
+ });
+ }
+
+ @Test
+ public void canShutdownAnOfflineClient() {
+ assertDoesNotThrow(() -> {
+ var provider = new Provider("fake-key", new LDConfig.Builder()
+ .offline(true).build());
+ provider.initialize(new ImmutableContext("context-key"));
+ provider.shutdown();
+ // Currently this does not check the provider state as the OF spec doesn't yet have a terminal
+ // shutdown state.
+ var ldClient = provider.getLdClient();
+ assertEquals(DataSourceStatusProvider.State.OFF, ldClient.getDataSourceStatusProvider().getStatus().getState());
+ });
+ }
+
+ @Test
+ public void itEmitsReadyEvents() {
+ var provider = new Provider("fake-key", new LDConfig.Builder()
+ .offline(true).build());
+
+ var readyCount = new AtomicInteger();
+ var errorCount = new AtomicInteger();
+ var staleCount = new AtomicInteger();
+
+ OpenFeatureAPI.getInstance().on(ProviderEvent.PROVIDER_READY, (detail) -> {
+ readyCount.getAndIncrement();
+ });
+
+ OpenFeatureAPI.getInstance().on(ProviderEvent.PROVIDER_STALE, (detail) -> {
+ staleCount.getAndIncrement();
+ });
+
+ OpenFeatureAPI.getInstance().on(ProviderEvent.PROVIDER_ERROR, (detail) -> {
+ errorCount.getAndIncrement();
+ });
+
+ OpenFeatureAPI.getInstance().setProviderAndWait(provider);
+
+ OpenFeatureAPI.getInstance().shutdown();
+
+ assertEquals(1, readyCount.get());
+ assertEquals(0, staleCount.get());
+ assertEquals(0, errorCount.get());
+ }
+}
diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/ProviderConfigurationTests.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/ProviderConfigurationTests.java
deleted file mode 100644
index e23ac67..0000000
--- a/src/test/java/com/launchdarkly/openfeature/serverprovider/ProviderConfigurationTests.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package com.launchdarkly.openfeature.serverprovider;
-
-import com.launchdarkly.logging.LDLogLevel;
-import com.launchdarkly.logging.LDLogger;
-import com.launchdarkly.sdk.server.Components;
-import com.launchdarkly.sdk.server.subsystems.LoggingConfiguration;
-import org.junit.Test;
-
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
-
-public class ProviderConfigurationTests {
- @Test
- public void itCanBuildADefaultConfiguration() {
- ProviderConfiguration defaultConfig = ProviderConfiguration.builder().build();
- assertNotNull(defaultConfig.getLoggingConfigurationFactory());
- }
-
- @Test
- public void itCanBeUsedWithALogAdapter() {
- TestLogger logAdapter = new TestLogger();
- ProviderConfiguration withLogAdapter = ProviderConfiguration.builder()
- .logging(logAdapter).build();
-
- LoggingConfiguration loggingConfig = withLogAdapter.getLoggingConfigurationFactory()
- .build(null);
-
- LDLogger logger = LDLogger.withAdapter(loggingConfig.getLogAdapter(), "the-name");
- logger.error("this is the error");
-
- assertTrue(logAdapter
- .getChannel("the-name")
- .expectedMessageInLevel(LDLogLevel.ERROR, "this is the error"));
- }
-
- @Test
- public void itCanBeUsedWithALoggingComponentConfigurer() {
- TestLogger logAdapter = new TestLogger();
- ProviderConfiguration withConfigurer = ProviderConfiguration.builder()
- .logging(Components.logging().adapter(logAdapter)).build();
-
- LoggingConfiguration loggingConfig = withConfigurer.getLoggingConfigurationFactory()
- .build(null);
-
- LDLogger logger = LDLogger.withAdapter(loggingConfig.getLogAdapter(), "the-name");
- logger.error("this is the error");
-
- assertTrue(logAdapter
- .getChannel("the-name")
- .expectedMessageInLevel(LDLogLevel.ERROR, "this is the error"));
- }
-}
diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/ProviderTest.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/ProviderTest.java
index 7194764..757a4c5 100644
--- a/src/test/java/com/launchdarkly/openfeature/serverprovider/ProviderTest.java
+++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/ProviderTest.java
@@ -6,13 +6,18 @@
import com.launchdarkly.sdk.LDValue;
import com.launchdarkly.sdk.server.interfaces.LDClientInterface;
import dev.openfeature.sdk.*;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
public class ProviderTest {
LDClientInterface mockedLdClient = mock(LDClientInterface.class);
+
+ /**
+ * This test uses the package private constructor, which means that it does not set
+ * wrapper information.
+ */
Provider ldProvider = new Provider(mockedLdClient);
@Test
@@ -26,19 +31,19 @@ public void itCanDoABooleanEvaluation() {
EvaluationContext evaluationContext = new ImmutableContext("user-key");
when(mockedLdClient.boolVariationDetail("the-key", LDContext.create("user-key"), false))
- .thenReturn(EvaluationDetail.fromValue(true, 12, EvaluationReason.fallthrough()));
+ .thenReturn(EvaluationDetail.fromValue(true, 12, EvaluationReason.fallthrough()));
OpenFeatureAPI.getInstance().setProvider(ldProvider);
assertTrue(OpenFeatureAPI
- .getInstance()
- .getClient()
- .getBooleanValue("the-key", false, evaluationContext));
+ .getInstance()
+ .getClient()
+ .getBooleanValue("the-key", false, evaluationContext));
FlagEvaluationDetails
- */
-public final class ProviderConfiguration {
- ProviderConfigurationBuilder builder;
-
- private ProviderConfiguration(ProviderConfigurationBuilder builder) {
- this.builder = builder;
- }
-
- /**
- * A mutable object that uses the Builder pattern to specify properties for a {@link ProviderConfiguration} object.
- */
- public static final class ProviderConfigurationBuilder {
- private ComponentConfigurer
- * ProviderConfiguration.builder().build();
- *