11package com .launchdarkly .openfeature .serverprovider ;
22
3- import com .launchdarkly .logging .LDLogAdapter ;
43import com .launchdarkly .logging .LDLogger ;
54import com .launchdarkly .sdk .EvaluationDetail ;
65import com .launchdarkly .sdk .LDValue ;
6+ import com .launchdarkly .sdk .server .Components ;
77import com .launchdarkly .sdk .server .LDClient ;
8+ import com .launchdarkly .sdk .server .LDConfig ;
9+ import com .launchdarkly .sdk .server .interfaces .DataSourceStatusProvider ;
810import com .launchdarkly .sdk .server .interfaces .LDClientInterface ;
9- import com .launchdarkly .sdk .server .subsystems .LoggingConfiguration ;
1011import dev .openfeature .sdk .*;
1112
13+ import java .io .IOException ;
14+ import java .time .temporal .ChronoUnit ;
15+ import java .util .Collections ;
16+ import java .util .concurrent .TimeoutException ;
17+
1218/**
1319 * An OpenFeature {@link FeatureProvider} which enables the use of the LaunchDarkly Server-Side SDK for Java
1420 * with OpenFeature.
1521 * <pre><code>
16- *import dev.openfeature.sdk.OpenFeatureAPI;
17- *import com.launchdarkly.sdk.server.LDClient;
22+ * import dev.openfeature.sdk.OpenFeatureAPI;
1823 *
19- *public class Main {
24+ * public class Main {
2025 * public static void main(String[] args) {
21- * LDClient ldClient = new LDClient("my-sdk-key");
22- * OpenFeatureAPI.getInstance().setProvider(new Provider(ldClient));
26+ * OpenFeatureAPI.getInstance().setProvider(new Provider("fake-key"));
2327 *
2428 * // Refer to OpenFeature documentation for getting a client and performing evaluations.
2529 * }
26- *}
30+ * }
2731 * </code></pre>
2832 */
29- public class Provider implements FeatureProvider {
33+ public class Provider extends EventProvider {
3034 private static final class ProviderMetaData implements Metadata {
3135 @ Override
3236 public String getName () {
@@ -43,39 +47,38 @@ public String getName() {
4347
4448 private final LDClientInterface client ;
4549
50+ private ProviderState state = ProviderState .NOT_READY ;
51+
4652 /**
47- * Create a provider with the given LaunchDarkly client and provider configuration.
48- * <pre><code>
49- * // Using the provider with a custom log level.
50- * new Provider(ldclient, ProviderConfiguration
51- * .builder()
52- * .logging(Components.logging().level(LDLogLevel.INFO)
53- * .build());
54- * </code></pre>
53+ * Create a provider with the specified SDK and default configuration.
54+ * <p>
55+ * If you need to specify any configuration use {@link Provider#Provider(String, LDConfig)} instead.
5556 *
56- * @param client A {@link LDClient} instance.
57- * @param config Configuration for the provider.
57+ * @param sdkKey the SDK key for your LaunchDarkly environment
5858 */
59- public Provider (LDClientInterface client , ProviderConfiguration config ) {
60- this .client = client ;
61- LoggingConfiguration loggingConfig = config .getLoggingConfigurationFactory ().build (null );
62- LDLogAdapter adapter = loggingConfig .getLogAdapter ();
63- logger = LDLogger .withAdapter (adapter , loggingConfig .getBaseLoggerName ());
64-
65- evaluationContextConverter = new EvaluationContextConverter (logger );
66- evaluationDetailConverter = new EvaluationDetailConverter (logger );
67- valueConverter = new ValueConverter (logger );
59+ public Provider (String sdkKey ) {
60+ this (sdkKey , new LDConfig .Builder ().build ());
6861 }
6962
7063 /**
71- * Create a provider with the given LaunchDarkly client.
72- * <p>
73- * The provider will be created with default configuration.
64+ * Crate a provider with the specified SDK key and configuration.
7465 *
75- * @param client A {@link LDClient} instance.
66+ * @param sdkKey the SDK key for your LaunchDarkly environment
67+ * @param config a client configuration object
7668 */
77- public Provider (LDClientInterface client ) {
78- this (client , ProviderConfiguration .builder ().build ());
69+ public Provider (String sdkKey , LDConfig config ) {
70+ this (new LDClient (sdkKey , LDConfig .Builder .fromConfig (config )
71+ .wrapper (Components .wrapperInfo ()
72+ .wrapperName ("open-feature-java-server" )
73+ .wrapperVersion (Version .SDK_VERSION )).build ()));
74+ }
75+
76+ Provider (LDClientInterface client ) {
77+ this .client = client ;
78+ logger = client .getLogger ();
79+ evaluationContextConverter = new EvaluationContextConverter (logger );
80+ evaluationDetailConverter = new EvaluationDetailConverter (logger );
81+ valueConverter = new ValueConverter (logger );
7982 }
8083
8184 @ Override
@@ -86,41 +89,121 @@ public Metadata getMetadata() {
8689 @ Override
8790 public ProviderEvaluation <Boolean > getBooleanEvaluation (String key , Boolean defaultValue , EvaluationContext ctx ) {
8891 EvaluationDetail <Boolean > detail
89- = this .client .boolVariationDetail (key , evaluationContextConverter .toLdContext (ctx ), defaultValue );
92+ = this .client .boolVariationDetail (key , evaluationContextConverter .toLdContext (ctx ), defaultValue );
9093
9194 return evaluationDetailConverter .toEvaluationDetails (detail );
9295 }
9396
9497 @ Override
9598 public ProviderEvaluation <String > getStringEvaluation (String key , String defaultValue , EvaluationContext ctx ) {
9699 EvaluationDetail <String > detail
97- = this .client .stringVariationDetail (key , evaluationContextConverter .toLdContext (ctx ), defaultValue );
100+ = this .client .stringVariationDetail (key , evaluationContextConverter .toLdContext (ctx ), defaultValue );
98101
99102 return evaluationDetailConverter .toEvaluationDetails (detail );
100103 }
101104
102105 @ Override
103106 public ProviderEvaluation <Integer > getIntegerEvaluation (String key , Integer defaultValue , EvaluationContext ctx ) {
104107 EvaluationDetail <Integer > detail
105- = this .client .intVariationDetail (key , evaluationContextConverter .toLdContext (ctx ), defaultValue );
108+ = this .client .intVariationDetail (key , evaluationContextConverter .toLdContext (ctx ), defaultValue );
106109
107110 return evaluationDetailConverter .toEvaluationDetails (detail );
108111 }
109112
110113 @ Override
111114 public ProviderEvaluation <Double > getDoubleEvaluation (String key , Double defaultValue , EvaluationContext ctx ) {
112115 EvaluationDetail <Double > detail
113- = this .client .doubleVariationDetail (key , evaluationContextConverter .toLdContext (ctx ), defaultValue );
116+ = this .client .doubleVariationDetail (key , evaluationContextConverter .toLdContext (ctx ), defaultValue );
114117
115118 return evaluationDetailConverter .toEvaluationDetails (detail );
116119 }
117120
118121 @ Override
119122 public ProviderEvaluation <Value > getObjectEvaluation (String key , Value defaultValue , EvaluationContext ctx ) {
120123 EvaluationDetail <LDValue > detail
121- = this .client .jsonValueVariationDetail (
122- key , evaluationContextConverter .toLdContext (ctx ), valueConverter .toLdValue (defaultValue ));
124+ = this .client .jsonValueVariationDetail (
125+ key , evaluationContextConverter .toLdContext (ctx ), valueConverter .toLdValue (defaultValue ));
123126
124127 return evaluationDetailConverter .toEvaluationDetailsLdValue (detail );
125128 }
129+
130+ @ Override
131+ public ProviderState getState () {
132+ return state ;
133+ }
134+
135+ @ Override
136+ public void initialize (EvaluationContext evaluationContext ) throws Exception {
137+ // If we are ready, then set the state. Don't return, because we still need to listen for future
138+ // changes.
139+ if (client .isInitialized ()) {
140+ state = ProviderState .READY ;
141+ }
142+
143+ client .getFlagTracker ().addFlagChangeListener (detail -> {
144+ emitProviderConfigurationChanged (
145+ ProviderEventDetails .builder ().flagsChanged (Collections .singletonList (detail .getKey ())).build ());
146+ });
147+ // Listen for future status changes.
148+ client .getDataSourceStatusProvider ().addStatusListener ((res ) -> {
149+ switch (res .getState ()) {
150+ // We will not re-enter INITIALIZING, but it is here to make the switch exhaustive.
151+ case INITIALIZING : {
152+ }
153+ break ;
154+ case INTERRUPTED : {
155+ state = ProviderState .STALE ;
156+ var message = res .getLastError () != null ? res .getLastError ().getMessage () : "encountered an unknown error" ;
157+ emitProviderStale (ProviderEventDetails .builder ().message (message ).build ());
158+ }
159+ break ;
160+ case VALID : {
161+ // If we are ready, then we don't want to emit it again. Other conditions we may be updating the
162+ // reason we are stale or interrupted, so we want to emit an event each time.
163+ if (state != ProviderState .READY ) {
164+ state = ProviderState .READY ;
165+ emitProviderReady (ProviderEventDetails .builder ().build ());
166+ }
167+ }
168+ break ;
169+ case OFF : {
170+ // Currently there is not a shutdown state.
171+ // Our client/provider cannot be restarted, so we just go to error.
172+ state = ProviderState .ERROR ;
173+ emitProviderError (ProviderEventDetails .builder ().message ("Provider shutdown" ).build ());
174+ }
175+ }
176+ });
177+ if (state == ProviderState .READY ) {
178+ return ;
179+ }
180+
181+ boolean initialized = client .getDataSourceStatusProvider ().waitFor (DataSourceStatusProvider .State .VALID ,
182+ ChronoUnit .FOREVER .getDuration ());
183+
184+ if (!initialized ) {
185+ // Here we throw an exception for the OpenFeature SDK, which will handle emitting an event.
186+ throw new RuntimeException ("Failed to initialize LaunchDarkly client." );
187+ }
188+ }
189+
190+ @ Override
191+ public void shutdown () {
192+ try {
193+ client .close ();
194+ } catch (IOException e ) {
195+ throw new RuntimeException (e );
196+ }
197+ }
198+
199+ /**
200+ * Get the LaunchDarkly client associated with this provider.
201+ * <p>
202+ * This can be used to access LaunchDarkly features which are not available in OpenFeature.
203+ *
204+ * @return the launchdarkly client instance
205+ */
206+ public LDClientInterface getLdClient () {
207+ return client ;
208+ }
126209}
0 commit comments