getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx);
/**
- * This method is called before a provider is used to evaluate flags. Providers can overwrite this method,
- * if they have special initialization needed prior being called for flag evaluation.
+ * This method is called before a provider is used to evaluate flags. Providers
+ * can overwrite this method,
+ * if they have special initialization needed prior being called for flag
+ * evaluation.
*
- * It is ok, if the method is expensive as it is executed in the background. All runtime exceptions will be
+ * It is ok, if the method is expensive as it is executed in the background. All
+ * runtime exceptions will be
* caught and logged.
*
*/
- default void initialize() {
+ default void initialize(EvaluationContext evaluationContext) throws Exception {
// Intentionally left blank
}
/**
- * This method is called when a new provider is about to be used to evaluate flags, or the SDK is shut down.
- * Providers can overwrite this method, if they have special shutdown actions needed.
+ * This method is called when a new provider is about to be used to evaluate
+ * flags, or the SDK is shut down.
+ * Providers can overwrite this method, if they have special shutdown actions
+ * needed.
*
- * It is ok, if the method is expensive as it is executed in the background. All runtime exceptions will be
+ * It is ok, if the method is expensive as it is executed in the background. All
+ * runtime exceptions will be
* caught and logged.
*
*/
@@ -47,4 +55,14 @@ default void shutdown() {
// Intentionally left blank
}
+ /**
+ * Returns a representation of the current readiness of the provider.
+ * Providers which do not implement this method are assumed to be ready immediately.
+ *
+ * @return ProviderState
+ */
+ default ProviderState getState() {
+ return ProviderState.READY;
+ }
+
}
diff --git a/src/main/java/dev/openfeature/sdk/NoOpProvider.java b/src/main/java/dev/openfeature/sdk/NoOpProvider.java
index c2e841a5..d3d9ca21 100644
--- a/src/main/java/dev/openfeature/sdk/NoOpProvider.java
+++ b/src/main/java/dev/openfeature/sdk/NoOpProvider.java
@@ -10,6 +10,12 @@ public class NoOpProvider implements FeatureProvider {
@Getter
private final String name = "No-op Provider";
+ // The Noop provider is ALWAYS NOT_READY, otherwise READY handlers would run immediately when attached.
+ @Override
+ public ProviderState getState() {
+ return ProviderState.NOT_READY;
+ }
+
@Override
public Metadata getMetadata() {
return new Metadata() {
diff --git a/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java b/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java
index 2e921a74..42ff4708 100644
--- a/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java
+++ b/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java
@@ -1,31 +1,33 @@
package dev.openfeature.sdk;
-import dev.openfeature.sdk.internal.AutoCloseableLock;
-import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
-import lombok.extern.slf4j.Slf4j;
-
-import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+
+import javax.annotation.Nullable;
+
+import dev.openfeature.sdk.internal.AutoCloseableLock;
+import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
+import lombok.extern.slf4j.Slf4j;
/**
- * A global singleton which holds base configuration for the OpenFeature library.
+ * A global singleton which holds base configuration for the OpenFeature
+ * library.
* Configuration here will be shared across all {@link Client}s.
*/
@Slf4j
-public class OpenFeatureAPI {
+public class OpenFeatureAPI implements EventBus {
// package-private multi-read/single-write lock
- static AutoCloseableReentrantReadWriteLock hooksLock = new AutoCloseableReentrantReadWriteLock();
- static AutoCloseableReentrantReadWriteLock contextLock = new AutoCloseableReentrantReadWriteLock();
-
+ static AutoCloseableReentrantReadWriteLock lock = new AutoCloseableReentrantReadWriteLock();
+ private EvaluationContext evaluationContext;
private final List apiHooks;
-
private ProviderRepository providerRepository = new ProviderRepository();
- private EvaluationContext evaluationContext;
+ private EventSupport eventSupport = new EventSupport();
protected OpenFeatureAPI() {
- this.apiHooks = new ArrayList<>();
+ apiHooks = new ArrayList<>();
}
private static class SingletonHolder {
@@ -49,23 +51,34 @@ public Metadata getProviderMetadata(String clientName) {
return getProvider(clientName).getMetadata();
}
+ /**
+ * {@inheritDoc}
+ */
public Client getClient() {
return getClient(null, null);
}
+ /**
+ * {@inheritDoc}
+ */
public Client getClient(@Nullable String name) {
return getClient(name, null);
}
+ /**
+ * {@inheritDoc}
+ */
public Client getClient(@Nullable String name, @Nullable String version) {
- return new OpenFeatureClient(this, name, version);
+ return new OpenFeatureClient(this,
+ name,
+ version);
}
/**
* {@inheritDoc}
*/
public void setEvaluationContext(EvaluationContext evaluationContext) {
- try (AutoCloseableLock __ = contextLock.writeLockAutoCloseable()) {
+ try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
this.evaluationContext = evaluationContext;
}
}
@@ -74,7 +87,7 @@ public void setEvaluationContext(EvaluationContext evaluationContext) {
* {@inheritDoc}
*/
public EvaluationContext getEvaluationContext() {
- try (AutoCloseableLock __ = contextLock.readLockAutoCloseable()) {
+ try (AutoCloseableLock __ = lock.readLockAutoCloseable()) {
return this.evaluationContext;
}
}
@@ -83,7 +96,14 @@ public EvaluationContext getEvaluationContext() {
* Set the default provider.
*/
public void setProvider(FeatureProvider provider) {
- providerRepository.setProvider(provider);
+ try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
+ providerRepository.setProvider(
+ provider,
+ (p) -> attachEventProvider(p),
+ (p) -> emitReady(p),
+ (p) -> detachEventProvider(p),
+ (p, message) -> emitError(p, message));
+ }
}
/**
@@ -93,7 +113,37 @@ public void setProvider(FeatureProvider provider) {
* @param provider The provider to set.
*/
public void setProvider(String clientName, FeatureProvider provider) {
- providerRepository.setProvider(clientName, provider);
+ try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
+ providerRepository.setProvider(clientName,
+ provider,
+ this::attachEventProvider,
+ this::emitReady,
+ this::detachEventProvider,
+ this::emitError);
+ }
+ }
+
+ private void attachEventProvider(FeatureProvider provider) {
+ if (provider instanceof EventProvider) {
+ ((EventProvider)provider).attach((p, event, details) -> {
+ runHandlersForProvider(p, event, details);
+ });
+ }
+ }
+
+ private void emitReady(FeatureProvider provider) {
+ runHandlersForProvider(provider, ProviderEvent.PROVIDER_READY, ProviderEventDetails.builder().build());
+ }
+
+ private void detachEventProvider(FeatureProvider provider) {
+ if (provider instanceof EventProvider) {
+ ((EventProvider)provider).detach();
+ }
+ }
+
+ private void emitError(FeatureProvider provider, String message) {
+ runHandlersForProvider(provider, ProviderEvent.PROVIDER_ERROR,
+ ProviderEventDetails.builder().message(message).build());
}
/**
@@ -117,7 +167,7 @@ public FeatureProvider getProvider(String name) {
* {@inheritDoc}
*/
public void addHooks(Hook... hooks) {
- try (AutoCloseableLock __ = hooksLock.writeLockAutoCloseable()) {
+ try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
this.apiHooks.addAll(Arrays.asList(hooks));
}
}
@@ -126,7 +176,7 @@ public void addHooks(Hook... hooks) {
* {@inheritDoc}
*/
public List getHooks() {
- try (AutoCloseableLock __ = hooksLock.readLockAutoCloseable()) {
+ try (AutoCloseableLock __ = lock.readLockAutoCloseable()) {
return this.apiHooks;
}
}
@@ -135,19 +185,124 @@ public List getHooks() {
* {@inheritDoc}
*/
public void clearHooks() {
- try (AutoCloseableLock __ = hooksLock.writeLockAutoCloseable()) {
+ try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
this.apiHooks.clear();
}
}
public void shutdown() {
providerRepository.shutdown();
+ eventSupport.shutdown();
}
/**
- * This method is only here for testing as otherwise all tests after the API shutdown test would fail.
+ * {@inheritDoc}
*/
- final void resetProviderRepository() {
+ @Override
+ public OpenFeatureAPI onProviderReady(Consumer handler) {
+ return this.on(ProviderEvent.PROVIDER_READY, handler);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public OpenFeatureAPI onProviderConfigurationChanged(Consumer handler) {
+ return this.on(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, handler);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public OpenFeatureAPI onProviderStale(Consumer handler) {
+ return this.on(ProviderEvent.PROVIDER_STALE, handler);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public OpenFeatureAPI onProviderError(Consumer handler) {
+ return this.on(ProviderEvent.PROVIDER_ERROR, handler);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public OpenFeatureAPI on(ProviderEvent event, Consumer handler) {
+ try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
+ this.eventSupport.addGlobalHandler(event, handler);
+ return this;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public OpenFeatureAPI removeHandler(ProviderEvent event, Consumer handler) {
+ this.eventSupport.removeGlobalHandler(event, handler);
+ return this;
+ }
+
+ void removeHandler(String clientName, ProviderEvent event, Consumer handler) {
+ try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
+ eventSupport.removeClientHandler(clientName, event, handler);
+ }
+ }
+
+ void addHandler(String clientName, ProviderEvent event, Consumer handler) {
+ try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
+ // if the provider is READY, run immediately
+ if (ProviderEvent.PROVIDER_READY.equals(event)
+ && ProviderState.READY.equals(this.providerRepository.getProvider(clientName).getState())) {
+ eventSupport.runHandler(handler, EventDetails.builder().clientName(clientName).build());
+ }
+ eventSupport.addClientHandler(clientName, event, handler);
+ }
+ }
+
+ /**
+ * This method is only here for testing as otherwise all tests after the API
+ * shutdown test would fail.
+ */
+ final void reset() {
providerRepository = new ProviderRepository();
+ eventSupport = new EventSupport();
+ }
+
+ /**
+ * Runs the handlers associated with a particular provider.
+ *
+ * @param provider the provider from where this event originated
+ * @param event the event type
+ * @param details the event details
+ */
+ private void runHandlersForProvider(FeatureProvider provider, ProviderEvent event, ProviderEventDetails details) {
+ try (AutoCloseableLock __ = lock.readLockAutoCloseable()) {
+
+ List clientNamesForProvider = providerRepository
+ .getClientNamesForProvider(provider);
+
+ // run the global handlers
+ eventSupport.runGlobalHandlers(event, EventDetails.fromProviderEventDetails(details));
+
+ // run the handlers associated with named clients for this provider
+ clientNamesForProvider.forEach(name -> {
+ eventSupport.runClientHandlers(name, event, EventDetails.fromProviderEventDetails(details, name));
+ });
+
+ if (providerRepository.isDefaultProvider(provider)) {
+ // run handlers for clients that have no bound providers (since this is the default)
+ Set allClientNames = eventSupport.getAllClientNames();
+ Set boundClientNames = providerRepository.getAllBoundClientNames();
+ allClientNames.removeAll(boundClientNames);
+ allClientNames.forEach(name -> {
+ eventSupport.runClientHandlers(name, event, EventDetails.fromProviderEventDetails(details, name));
+ });
+ }
+ }
}
}
diff --git a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java
index 44febf77..05d79d02 100644
--- a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java
+++ b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java
@@ -5,6 +5,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.function.Consumer;
import dev.openfeature.sdk.exceptions.GeneralError;
import dev.openfeature.sdk.exceptions.OpenFeatureError;
@@ -33,13 +34,16 @@ public class OpenFeatureClient implements Client {
private EvaluationContext evaluationContext;
/**
- * Client for evaluating the flag. There may be multiples of these floating
- * around.
+ * Deprecated public constructor. Use OpenFeature.API.getClient() instead.
*
* @param openFeatureAPI Backing global singleton
* @param name Name of the client (used by observability tools).
* @param version Version of the client (used by observability tools).
+ * @deprecated Do not use this constructor. It's for internal use only.
+ * Clients created using it will not run event handlers.
+ * Use the OpenFeatureAPI's getClient factory method instead.
*/
+ @Deprecated() // TODO: eventually we will make this non-public
public OpenFeatureClient(OpenFeatureAPI openFeatureAPI, String name, String version) {
this.openfeatureApi = openFeatureAPI;
this.name = name;
@@ -95,7 +99,6 @@ private FlagEvaluationDetails evaluateFlag(FlagValueType type, String key
Map hints = Collections.unmodifiableMap(flagOptions.getHookHints());
ctx = ObjectUtils.defaultIfNull(ctx, () -> new ImmutableContext());
-
FlagEvaluationDetails details = null;
List mergedHooks = null;
HookContext hookCtx = null;
@@ -341,4 +344,54 @@ public FlagEvaluationDetails getObjectDetails(String key, Value defaultVa
public Metadata getMetadata() {
return () -> name;
}
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Client onProviderReady(Consumer handler) {
+ return on(ProviderEvent.PROVIDER_READY, handler);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Client onProviderConfigurationChanged(Consumer handler) {
+ return on(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, handler);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Client onProviderError(Consumer handler) {
+ return on(ProviderEvent.PROVIDER_ERROR, handler);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Client onProviderStale(Consumer handler) {
+ return on(ProviderEvent.PROVIDER_STALE, handler);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Client on(ProviderEvent event, Consumer handler) {
+ OpenFeatureAPI.getInstance().addHandler(name, event, handler);
+ return this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Client removeHandler(ProviderEvent event, Consumer handler) {
+ OpenFeatureAPI.getInstance().removeHandler(name, event, handler);
+ return this;
+ }
}
diff --git a/src/main/java/dev/openfeature/sdk/ProviderEvent.java b/src/main/java/dev/openfeature/sdk/ProviderEvent.java
new file mode 100644
index 00000000..dcefd606
--- /dev/null
+++ b/src/main/java/dev/openfeature/sdk/ProviderEvent.java
@@ -0,0 +1,8 @@
+package dev.openfeature.sdk;
+
+/**
+ * Provider event types.
+ */
+public enum ProviderEvent {
+ PROVIDER_READY, PROVIDER_CONFIGURATION_CHANGED, PROVIDER_ERROR, PROVIDER_STALE;
+}
diff --git a/src/main/java/dev/openfeature/sdk/ProviderEventDetails.java b/src/main/java/dev/openfeature/sdk/ProviderEventDetails.java
new file mode 100644
index 00000000..149c92a7
--- /dev/null
+++ b/src/main/java/dev/openfeature/sdk/ProviderEventDetails.java
@@ -0,0 +1,18 @@
+package dev.openfeature.sdk;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import lombok.Data;
+import lombok.experimental.SuperBuilder;
+
+/**
+ * The details of a particular event.
+ */
+@Data @SuperBuilder(toBuilder = true)
+public class ProviderEventDetails {
+ @Nullable private List flagsChanged;
+ @Nullable private String message;
+ @Nullable private ImmutableMetadata eventMetadata;
+}
diff --git a/src/main/java/dev/openfeature/sdk/ProviderRepository.java b/src/main/java/dev/openfeature/sdk/ProviderRepository.java
index 5a360eb6..0ff3b70b 100644
--- a/src/main/java/dev/openfeature/sdk/ProviderRepository.java
+++ b/src/main/java/dev/openfeature/sdk/ProviderRepository.java
@@ -1,24 +1,28 @@
package dev.openfeature.sdk;
-import lombok.extern.slf4j.Slf4j;
-
+import java.util.List;
import java.util.Map;
import java.util.Optional;
+import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiConsumer;
import java.util.function.Consumer;
+import java.util.stream.Collectors;
import java.util.stream.Stream;
+import javax.annotation.Nullable;
+
+import lombok.extern.slf4j.Slf4j;
+
@Slf4j
class ProviderRepository {
private final Map providers = new ConcurrentHashMap<>();
- private final ExecutorService taskExecutor = Executors.newCachedThreadPool();
- private final Map initializingNamedProviders = new ConcurrentHashMap<>();
private final AtomicReference defaultProvider = new AtomicReference<>(new NoOpProvider());
- private FeatureProvider initializingDefaultProvider;
+ private final ExecutorService taskExecutor = Executors.newCachedThreadPool();
/**
* Return the default provider.
@@ -37,14 +41,32 @@ public FeatureProvider getProvider(String name) {
return Optional.ofNullable(name).map(this.providers::get).orElse(this.defaultProvider.get());
}
+ public List getClientNamesForProvider(FeatureProvider provider) {
+ return providers.entrySet().stream()
+ .filter(entry -> entry.getValue().equals(provider))
+ .map(entry -> entry.getKey()).collect(Collectors.toList());
+ }
+
+ public Set getAllBoundClientNames() {
+ return providers.keySet();
+ }
+
+ public boolean isDefaultProvider(FeatureProvider provider) {
+ return this.getProvider().equals(provider);
+ }
+
/**
* Set the default provider.
*/
- public void setProvider(FeatureProvider provider) {
+ public void setProvider(FeatureProvider provider,
+ Consumer afterSet,
+ Consumer afterInit,
+ Consumer afterShutdown,
+ BiConsumer afterError) {
if (provider == null) {
throw new IllegalArgumentException("Provider cannot be null");
}
- initializeProvider(provider);
+ initializeProvider(null, provider, afterSet, afterInit, afterShutdown, afterError);
}
/**
@@ -53,76 +75,51 @@ public void setProvider(FeatureProvider provider) {
* @param clientName The name of the client.
* @param provider The provider to set.
*/
- public void setProvider(String clientName, FeatureProvider provider) {
+ public void setProvider(String clientName,
+ FeatureProvider provider,
+ Consumer afterSet,
+ Consumer afterInit,
+ Consumer afterShutdown,
+ BiConsumer afterError) {
if (provider == null) {
throw new IllegalArgumentException("Provider cannot be null");
}
if (clientName == null) {
throw new IllegalArgumentException("clientName cannot be null");
}
- initializeProvider(clientName, provider);
- }
-
- private void initializeProvider(FeatureProvider provider) {
- initializingDefaultProvider = provider;
- initializeProvider(provider, this::updateDefaultProviderAfterInitialization);
- }
-
- private void initializeProvider(String clientName, FeatureProvider provider) {
- initializingNamedProviders.put(clientName, provider);
- initializeProvider(provider, newProvider -> updateProviderAfterInit(clientName, newProvider));
- }
-
- private void initializeProvider(FeatureProvider provider, Consumer afterInitialization) {
+ initializeProvider(clientName, provider, afterSet, afterInit, afterShutdown, afterError);
+ }
+
+ private void initializeProvider(@Nullable String clientName,
+ FeatureProvider newProvider,
+ Consumer afterSet,
+ Consumer afterInit,
+ Consumer afterShutdown,
+ BiConsumer afterError) {
+ // provider is set immediately, on this thread
+ FeatureProvider oldProvider = clientName != null
+ ? this.providers.put(clientName, newProvider)
+ : this.defaultProvider.getAndSet(newProvider);
+ afterSet.accept(newProvider);
taskExecutor.submit(() -> {
+ // initialization happens in a different thread
try {
- if (!isProviderRegistered(provider)) {
- provider.initialize();
+ if (ProviderState.NOT_READY.equals(newProvider.getState())) {
+ newProvider.initialize(OpenFeatureAPI.getInstance().getEvaluationContext());
+ afterInit.accept(newProvider);
}
- afterInitialization.accept(provider);
+ shutDownOld(oldProvider, afterShutdown);
} catch (Exception e) {
- log.error("Exception when initializing feature provider {}", provider.getClass().getName(), e);
+ log.error("Exception when initializing feature provider {}", newProvider.getClass().getName(), e);
+ afterError.accept(newProvider, e.getMessage());
}
});
}
- private void updateProviderAfterInit(String clientName, FeatureProvider newProvider) {
- Optional
- .ofNullable(initializingNamedProviders.get(clientName))
- .filter(initializingProvider -> initializingProvider.equals(newProvider))
- .ifPresent(provider -> updateNamedProviderAfterInitialization(clientName, provider));
- }
-
- private void updateDefaultProviderAfterInitialization(FeatureProvider initializedProvider) {
- Optional
- .ofNullable(this.initializingDefaultProvider)
- .filter(initializingProvider -> initializingProvider.equals(initializedProvider))
- .ifPresent(this::replaceDefaultProvider);
- }
-
- private void replaceDefaultProvider(FeatureProvider provider) {
- FeatureProvider oldProvider = this.defaultProvider.getAndSet(provider);
- if (isOldProviderNotBoundByName(oldProvider)) {
- shutdownProvider(oldProvider);
- }
- }
-
- private boolean isOldProviderNotBoundByName(FeatureProvider oldProvider) {
- return !this.providers.containsValue(oldProvider);
- }
-
- private void updateNamedProviderAfterInitialization(String clientName, FeatureProvider initializedProvider) {
- Optional
- .ofNullable(this.initializingNamedProviders.get(clientName))
- .filter(initializingProvider -> initializingProvider.equals(initializedProvider))
- .ifPresent(provider -> replaceNamedProviderAndShutdownOldOne(clientName, provider));
- }
-
- private void replaceNamedProviderAndShutdownOldOne(String clientName, FeatureProvider provider) {
- FeatureProvider oldProvider = this.providers.put(clientName, provider);
- this.initializingNamedProviders.remove(clientName, provider);
+ private void shutDownOld(FeatureProvider oldProvider,Consumer afterShutdown) {
if (!isProviderRegistered(oldProvider)) {
shutdownProvider(oldProvider);
+ afterShutdown.accept(oldProvider);
}
}
@@ -133,6 +130,7 @@ private boolean isProviderRegistered(FeatureProvider oldProvider) {
private void shutdownProvider(FeatureProvider provider) {
taskExecutor.submit(() -> {
try {
+ // detachProviderEvents(provider);
provider.shutdown();
} catch (Exception e) {
log.error("Exception when shutting down feature provider {}", provider.getClass().getName(), e);
@@ -141,7 +139,8 @@ private void shutdownProvider(FeatureProvider provider) {
}
/**
- * Shutdowns this repository which includes shutting down all FeatureProviders that are registered,
+ * Shuts down this repository which includes shutting down all FeatureProviders
+ * that are registered,
* including the default feature provider.
*/
public void shutdown() {
@@ -149,7 +148,16 @@ public void shutdown() {
.concat(Stream.of(this.defaultProvider.get()), this.providers.values().stream())
.distinct()
.forEach(this::shutdownProvider);
- setProvider(new NoOpProvider());
+ setProvider(new NoOpProvider(),
+ (FeatureProvider fp) -> {
+ },
+ (FeatureProvider fp) -> {
+ },
+ (FeatureProvider fp) -> {
+ },
+ (FeatureProvider fp,
+ String message) -> {
+ });
this.providers.clear();
taskExecutor.shutdown();
}
diff --git a/src/main/java/dev/openfeature/sdk/ProviderState.java b/src/main/java/dev/openfeature/sdk/ProviderState.java
new file mode 100644
index 00000000..6685f8fe
--- /dev/null
+++ b/src/main/java/dev/openfeature/sdk/ProviderState.java
@@ -0,0 +1,8 @@
+package dev.openfeature.sdk;
+
+/**
+ * Indicates the state of the provider.
+ */
+public enum ProviderState {
+ READY, NOT_READY, ERROR;
+}
diff --git a/src/main/java/dev/openfeature/sdk/internal/ObjectUtils.java b/src/main/java/dev/openfeature/sdk/internal/ObjectUtils.java
index ff16422e..34caadae 100644
--- a/src/main/java/dev/openfeature/sdk/internal/ObjectUtils.java
+++ b/src/main/java/dev/openfeature/sdk/internal/ObjectUtils.java
@@ -69,5 +69,4 @@ public static List merge(List... sources) {
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
-
}
diff --git a/src/main/java/dev/openfeature/sdk/internal/TriConsumer.java b/src/main/java/dev/openfeature/sdk/internal/TriConsumer.java
new file mode 100644
index 00000000..723f4aeb
--- /dev/null
+++ b/src/main/java/dev/openfeature/sdk/internal/TriConsumer.java
@@ -0,0 +1,38 @@
+package dev.openfeature.sdk.internal;
+
+import java.util.Objects;
+
+/**
+ * Like {@link java.util.function.BiConsumer} but with 3 params.
+ *
+ * @see java.util.function.BiConsumer
+ */
+@FunctionalInterface
+public interface TriConsumer {
+
+ /**
+ * Performs this operation on the given arguments.
+ *
+ * @param t the first input argument
+ * @param u the second input argument
+ * @param v the third input argument
+ */
+ void accept(T t, U u, V v);
+
+ /**
+ * Returns a composed {@code TriConsumer} that performs an additional operation.
+ *
+ * @param after the operation to perform after this operation
+ * @return a composed {@code TriConsumer} that performs in sequence this
+ * operation followed by the {@code after} operation
+ * @throws NullPointerException if {@code after} is null
+ */
+ default TriConsumer andThen(TriConsumer after) {
+ Objects.requireNonNull(after);
+
+ return (t, u, v) -> {
+ accept(t, u, v);
+ after.accept(t, u, v);
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/dev/openfeature/sdk/EventProviderTest.java b/src/test/java/dev/openfeature/sdk/EventProviderTest.java
new file mode 100644
index 00000000..cb73b529
--- /dev/null
+++ b/src/test/java/dev/openfeature/sdk/EventProviderTest.java
@@ -0,0 +1,130 @@
+package dev.openfeature.sdk;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import dev.openfeature.sdk.internal.TriConsumer;
+
+class EventProviderTest {
+
+ @Test
+ @DisplayName("should run attached onEmit with emitters")
+ void emitsEventsWhenAttached() {
+ TestEventProvider eventProvider = new TestEventProvider();
+ TriConsumer onEmit = mockOnEmit();
+ eventProvider.attach(onEmit);
+
+ ProviderEventDetails details = ProviderEventDetails.builder().build();
+ eventProvider.emit(ProviderEvent.PROVIDER_READY, details);
+ eventProvider.emitProviderReady(details);
+ eventProvider.emitProviderConfigurationChanged(details);
+ eventProvider.emitProviderStale(details);
+ eventProvider.emitProviderError(details);
+
+ verify(onEmit, times(2)).accept(eventProvider, ProviderEvent.PROVIDER_READY, details);
+ verify(onEmit, times(1)).accept(eventProvider, ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, details);
+ verify(onEmit, times(1)).accept(eventProvider, ProviderEvent.PROVIDER_STALE, details);
+ verify(onEmit, times(1)).accept(eventProvider, ProviderEvent.PROVIDER_ERROR, details);
+ }
+
+ @Test
+ @DisplayName("should do nothing with emitters if no onEmit attached")
+ void doesNotEmitsEventsWhenNotAttached() {
+ TestEventProvider eventProvider = new TestEventProvider();
+
+ // don't attach this emitter
+ TriConsumer onEmit = mockOnEmit();
+
+ ProviderEventDetails details = ProviderEventDetails.builder().build();
+ eventProvider.emit(ProviderEvent.PROVIDER_READY, details);
+ eventProvider.emitProviderReady(details);
+ eventProvider.emitProviderConfigurationChanged(details);
+ eventProvider.emitProviderStale(details);
+ eventProvider.emitProviderError(details);
+
+ // should not be called
+ verify(onEmit, never()).accept(any(), any(), any());
+ }
+
+ @Test
+ @DisplayName("should throw if second different onEmit attached")
+ void throwsWhenOnEmitDifferent() {
+ TestEventProvider eventProvider = new TestEventProvider();
+ TriConsumer onEmit1 = mockOnEmit();
+ TriConsumer onEmit2 = mockOnEmit();
+ eventProvider.attach(onEmit1);
+ assertThrows(IllegalStateException.class, () -> eventProvider.attach(onEmit2));
+ }
+
+
+ @Test
+ @DisplayName("should not throw if second same onEmit attached")
+ void doesNotThrowWhenOnEmitSame() {
+ TestEventProvider eventProvider = new TestEventProvider();
+ TriConsumer onEmit1 = mockOnEmit();
+ TriConsumer onEmit2 = onEmit1;
+ eventProvider.attach(onEmit1);
+ eventProvider.attach(onEmit2); // should not throw, same instance. noop
+ }
+
+
+ class TestEventProvider extends EventProvider {
+
+ @Override
+ public Metadata getMetadata() {
+ return new Metadata() {
+ @Override
+ public String getName() {
+ return "TestEventProvider";
+ }
+ };
+ }
+
+ @Override
+ public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue,
+ EvaluationContext ctx) {
+ // TODO Auto-generated method stub
+ throw new UnsupportedOperationException("Unimplemented method 'getBooleanEvaluation'");
+ }
+
+ @Override
+ public ProviderEvaluation getStringEvaluation(String key, String defaultValue,
+ EvaluationContext ctx) {
+ // TODO Auto-generated method stub
+ throw new UnsupportedOperationException("Unimplemented method 'getStringEvaluation'");
+ }
+
+ @Override
+ public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue,
+ EvaluationContext ctx) {
+ // TODO Auto-generated method stub
+ throw new UnsupportedOperationException("Unimplemented method 'getIntegerEvaluation'");
+ }
+
+ @Override
+ public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue,
+ EvaluationContext ctx) {
+ // TODO Auto-generated method stub
+ throw new UnsupportedOperationException("Unimplemented method 'getDoubleEvaluation'");
+ }
+
+ @Override
+ public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue,
+ EvaluationContext ctx) {
+ // TODO Auto-generated method stub
+ throw new UnsupportedOperationException("Unimplemented method 'getObjectEvaluation'");
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private TriConsumer mockOnEmit() {
+ return (TriConsumer)mock(TriConsumer.class);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/dev/openfeature/sdk/EventsTest.java b/src/test/java/dev/openfeature/sdk/EventsTest.java
new file mode 100644
index 00000000..70f81657
--- /dev/null
+++ b/src/test/java/dev/openfeature/sdk/EventsTest.java
@@ -0,0 +1,599 @@
+package dev.openfeature.sdk;
+
+import static org.awaitility.Awaitility.await;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.after;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Consumer;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentMatcher;
+
+import dev.openfeature.sdk.testutils.TestEventsProvider;
+import io.cucumber.java.AfterAll;
+
+class EventsTest {
+
+ private static final int TIMEOUT = 200;
+ private static final int INIT_DELAY = TIMEOUT / 2;
+
+ @AfterAll
+ public static void resetDefaultProvider() {
+ OpenFeatureAPI.getInstance().setProvider(new NoOpProvider());
+ }
+
+ @Nested
+ class ApiEvents {
+
+ @Nested
+ @DisplayName("named provider")
+ class NamedProvider {
+
+ @Nested
+ @DisplayName("initialization")
+ class Initialization {
+
+ @Test
+ @DisplayName("should fire initial READY event when provider init succeeds")
+ @Specification(number = "5.3.1", text = "If the provider's initialize function terminates normally," +
+ " PROVIDER_READY handlers MUST run.")
+ void apiInitReady() {
+ final Consumer handler = (Consumer)mockHandler();
+ final String name = "apiInitReady";
+
+ TestEventsProvider provider = new TestEventsProvider(INIT_DELAY);
+ OpenFeatureAPI.getInstance().onProviderReady(handler);
+ OpenFeatureAPI.getInstance().setProvider(name, provider);
+ verify(handler, timeout(TIMEOUT).atLeastOnce())
+ .accept(any());
+ }
+
+ @Test
+ @DisplayName("should fire initial ERROR event when provider init errors")
+ @Specification(number = "5.3.2", text = "If the provider's initialize function terminates abnormally," +
+ " PROVIDER_ERROR handlers MUST run.")
+ void apiInitError() {
+ final Consumer handler = mockHandler();
+ final String name = "apiInitError";
+ final String errMessage = "oh no!";
+
+ TestEventsProvider provider = new TestEventsProvider(INIT_DELAY, true, errMessage);
+ OpenFeatureAPI.getInstance().onProviderError(handler);
+ OpenFeatureAPI.getInstance().setProvider(name, provider);
+ verify(handler, timeout(TIMEOUT)).accept(argThat(details -> {
+ return errMessage.equals(details.getMessage());
+ }));
+ }
+ }
+
+ @Nested
+ @DisplayName("provider events")
+ class ProviderEvents {
+
+ @Test
+ @DisplayName("should propagate events")
+ @Specification(number = "5.1.2", text = "When a provider signals the occurrence of a particular event, "
+ +
+ "the associated client and API event handlers MUST run.")
+ void apiShouldPropagateEvents() {
+ final Consumer handler = mockHandler();
+ final String name = "apiShouldPropagateEvents";
+
+ TestEventsProvider provider = new TestEventsProvider(INIT_DELAY);
+ OpenFeatureAPI.getInstance().setProvider(name, provider);
+ OpenFeatureAPI.getInstance().onProviderConfigurationChanged(handler);
+
+ provider.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, EventDetails.builder().build());
+ verify(handler, timeout(TIMEOUT)).accept(any());
+ }
+
+ @Test
+ @DisplayName("should support all event types")
+ @Specification(number = "5.1.1", text = "The provider MAY define a mechanism for signaling the occurrence "
+ + "of one of a set of events, including PROVIDER_READY, PROVIDER_ERROR, "
+ + "PROVIDER_CONFIGURATION_CHANGED and PROVIDER_STALE, with a provider event details payload.")
+ @Specification(number = "5.2.2", text = "The API MUST provide a function for associating handler functions"
+ +
+ " with a particular provider event type.")
+ void apiShouldSupportAllEventTypes() throws Exception {
+ final String name = "apiShouldSupportAllEventTypes";
+ final Consumer handler1 = mockHandler();
+ final Consumer handler2 = mockHandler();
+ final Consumer handler3 = mockHandler();
+ final Consumer handler4 = mockHandler();
+
+ TestEventsProvider provider = new TestEventsProvider(INIT_DELAY);
+ OpenFeatureAPI.getInstance().setProvider(name, provider);
+
+ OpenFeatureAPI.getInstance().onProviderReady(handler1);
+ OpenFeatureAPI.getInstance().onProviderConfigurationChanged(handler2);
+ OpenFeatureAPI.getInstance().onProviderStale(handler3);
+ OpenFeatureAPI.getInstance().onProviderError(handler4);
+
+ Arrays.asList(ProviderEvent.values()).stream().forEach(eventType -> {
+ provider.mockEvent(eventType, ProviderEventDetails.builder().build());
+ });
+
+ verify(handler1, timeout(TIMEOUT).atLeastOnce()).accept(any());
+ verify(handler2, timeout(TIMEOUT).atLeastOnce()).accept(any());
+ verify(handler3, timeout(TIMEOUT).atLeastOnce()).accept(any());
+ verify(handler4, timeout(TIMEOUT).atLeastOnce()).accept(any());
+ }
+ }
+ }
+ }
+
+ @Nested
+ @DisplayName("client events")
+ class ClientEvents {
+
+ @Nested
+ @DisplayName("default provider")
+ class DefaultProvider {
+
+ @Nested
+ @DisplayName("provider events")
+ class ProviderEvents {
+
+ @Test
+ @DisplayName("should propagate events for default provider and anonymous client")
+ @Specification(number = "5.1.2", text = "When a provider signals the occurrence of a particular event, the associated client and API event handlers MUST run.")
+ void shouldPropagateDefaultAndAnon() {
+ final Consumer handler = mockHandler();
+
+ TestEventsProvider provider = new TestEventsProvider(INIT_DELAY);
+ // set provider before getting a client
+ OpenFeatureAPI.getInstance().setProvider(provider);
+ Client client = OpenFeatureAPI.getInstance().getClient();
+ client.onProviderStale(handler);
+
+ provider.mockEvent(ProviderEvent.PROVIDER_STALE, EventDetails.builder().build());
+ verify(handler, timeout(TIMEOUT)).accept(any());
+ }
+
+ @Test
+ @DisplayName("should propagate events for default provider and named client")
+ @Specification(number = "5.1.2", text = "When a provider signals the occurrence of a particular event, the associated client and API event handlers MUST run.")
+ void shouldPropagateDefaultAndNamed() {
+ final Consumer handler = mockHandler();
+ final String name = "shouldPropagateDefaultAndNamed";
+
+ TestEventsProvider provider = new TestEventsProvider(INIT_DELAY);
+ // set provider before getting a client
+ OpenFeatureAPI.getInstance().setProvider(provider);
+ Client client = OpenFeatureAPI.getInstance().getClient(name);
+ client.onProviderStale(handler);
+
+ provider.mockEvent(ProviderEvent.PROVIDER_STALE, EventDetails.builder().build());
+ verify(handler, timeout(TIMEOUT)).accept(any());
+ }
+ }
+ }
+ }
+
+ @Nested
+ @DisplayName("named provider")
+ class NamedProvider {
+
+ @Nested
+ @DisplayName("initialization")
+ class Initialization {
+ @Test
+ @DisplayName("should fire initial READY event when provider init succeeds after client retrieved")
+ @Specification(number = "5.3.1", text = "If the provider's initialize function terminates normally, PROVIDER_READY handlers MUST run.")
+ void initReadyProviderBefore() throws InterruptedException {
+ final Consumer handler = mockHandler();
+ final String name = "initReadyProviderBefore";
+
+ TestEventsProvider provider = new TestEventsProvider(INIT_DELAY);
+ Client client = OpenFeatureAPI.getInstance().getClient(name);
+ client.onProviderReady(handler);
+ // set provider after getting a client
+ OpenFeatureAPI.getInstance().setProvider(name, provider);
+ verify(handler, timeout(TIMEOUT).atLeastOnce())
+ .accept(argThat(details -> details.getClientName().equals(name)));
+ }
+
+ @Test
+ @DisplayName("should fire initial READY event when provider init succeeds before client retrieved")
+ @Specification(number = "5.3.1", text = "If the provider's initialize function terminates normally, PROVIDER_READY handlers MUST run.")
+ void initReadyProviderAfter() {
+ final Consumer handler = mockHandler();
+ final String name = "initReadyProviderAfter";
+
+ TestEventsProvider provider = new TestEventsProvider(INIT_DELAY);
+ // set provider before getting a client
+ OpenFeatureAPI.getInstance().setProvider(name, provider);
+ Client client = OpenFeatureAPI.getInstance().getClient(name);
+ client.onProviderReady(handler);
+ verify(handler, timeout(TIMEOUT).atLeastOnce())
+ .accept(argThat(details -> details.getClientName().equals(name)));
+ }
+
+ @Test
+ @DisplayName("should fire initial ERROR event when provider init errors after client retrieved")
+ @Specification(number = "5.3.2", text = "If the provider's initialize function terminates abnormally, PROVIDER_ERROR handlers MUST run.")
+ void initErrorProviderAfter() {
+ final Consumer handler = mockHandler();
+ final String name = "initErrorProviderAfter";
+ final String errMessage = "oh no!";
+
+ TestEventsProvider provider = new TestEventsProvider(INIT_DELAY, true, errMessage);
+ Client client = OpenFeatureAPI.getInstance().getClient(name);
+ client.onProviderError(handler);
+ // set provider after getting a client
+ OpenFeatureAPI.getInstance().setProvider(name, provider);
+ verify(handler, timeout(TIMEOUT)).accept(argThat(details -> {
+ return name.equals(details.getClientName())
+ && errMessage.equals(details.getMessage());
+ }));
+ }
+
+ @Test
+ @DisplayName("should fire initial ERROR event when provider init errors before client retrieved")
+ @Specification(number = "5.3.2", text = "If the provider's initialize function terminates abnormally, PROVIDER_ERROR handlers MUST run.")
+ void initErrorProviderBefore() {
+ final Consumer handler = mockHandler();
+ final String name = "initErrorProviderBefore";
+ final String errMessage = "oh no!";
+
+ TestEventsProvider provider = new TestEventsProvider(INIT_DELAY, true, errMessage);
+ // set provider after getting a client
+ OpenFeatureAPI.getInstance().setProvider(name, provider);
+ Client client = OpenFeatureAPI.getInstance().getClient(name);
+ client.onProviderError(handler);
+ verify(handler, timeout(TIMEOUT)).accept(argThat(details -> {
+ return name.equals(details.getClientName())
+ && errMessage.equals(details.getMessage());
+ }));
+ }
+ }
+
+ @Nested
+ @DisplayName("provider events")
+ class ProviderEvents {
+
+ @Test
+ @DisplayName("should propagate events when provider set before client retrieved")
+ @Specification(number = "5.1.2", text = "When a provider signals the occurrence of a particular event, the associated client and API event handlers MUST run.")
+ void shouldPropagateBefore() {
+ final Consumer handler = mockHandler();
+ final String name = "shouldPropagateBefore";
+
+ TestEventsProvider provider = new TestEventsProvider(INIT_DELAY);
+ // set provider before getting a client
+ OpenFeatureAPI.getInstance().setProvider(name, provider);
+ Client client = OpenFeatureAPI.getInstance().getClient(name);
+ client.onProviderConfigurationChanged(handler);
+
+ provider.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, EventDetails.builder().build());
+ verify(handler, timeout(TIMEOUT)).accept(argThat(details -> details.getClientName().equals(name)));
+ }
+
+ @Test
+ @DisplayName("should propagate events when provider set after client retrieved")
+ @Specification(number = "5.1.2", text = "When a provider signals the occurrence of a particular event, the associated client and API event handlers MUST run.")
+ void shouldPropagateAfter() {
+
+ final Consumer handler = mockHandler();
+ final String name = "shouldPropagateAfter";
+
+ TestEventsProvider provider = new TestEventsProvider(INIT_DELAY);
+ Client client = OpenFeatureAPI.getInstance().getClient(name);
+ client.onProviderConfigurationChanged(handler);
+ // set provider after getting a client
+ OpenFeatureAPI.getInstance().setProvider(name, provider);
+
+ provider.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, EventDetails.builder().build());
+ verify(handler, timeout(TIMEOUT)).accept(argThat(details -> details.getClientName().equals(name)));
+ }
+
+ @Test
+ @DisplayName("should support all event types")
+ @Specification(number = "5.1.1", text = "The provider MAY define a mechanism for signaling the occurrence "
+ + "of one of a set of events, including PROVIDER_READY, PROVIDER_ERROR, "
+ + "PROVIDER_CONFIGURATION_CHANGED and PROVIDER_STALE, with a provider event details payload.")
+ @Specification(number = "5.2.1", text = "The client MUST provide a function for associating handler functions"
+ +
+ " with a particular provider event type.")
+ void shouldSupportAllEventTypes() throws Exception {
+ final String name = "shouldSupportAllEventTypes";
+ final Consumer handler1 = mockHandler();
+ final Consumer handler2 = mockHandler();
+ final Consumer handler3 = mockHandler();
+ final Consumer handler4 = mockHandler();
+
+ TestEventsProvider provider = new TestEventsProvider(INIT_DELAY);
+ OpenFeatureAPI.getInstance().setProvider(name, provider);
+ Client client = OpenFeatureAPI.getInstance().getClient(name);
+
+ client.onProviderReady(handler1);
+ client.onProviderConfigurationChanged(handler2);
+ client.onProviderStale(handler3);
+ client.onProviderError(handler4);
+
+ Arrays.asList(ProviderEvent.values()).stream().forEach(eventType -> {
+ provider.mockEvent(eventType, ProviderEventDetails.builder().build());
+ });
+ ArgumentMatcher nameMatches = (EventDetails details) -> details.getClientName()
+ .equals(name);
+ verify(handler1, timeout(TIMEOUT).atLeastOnce()).accept(argThat(nameMatches));
+ verify(handler2, timeout(TIMEOUT).atLeastOnce()).accept(argThat(nameMatches));
+ verify(handler3, timeout(TIMEOUT).atLeastOnce()).accept(argThat(nameMatches));
+ verify(handler4, timeout(TIMEOUT).atLeastOnce()).accept(argThat(nameMatches));
+ }
+ }
+ }
+
+ @Test
+ @DisplayName("shutdown provider should not run handlers")
+ void shouldNotRunHandlers() throws Exception {
+ final Consumer handler1 = mockHandler();
+ final Consumer handler2 = mockHandler();
+ final String name = "shouldNotRunHandlers";
+
+ TestEventsProvider provider1 = new TestEventsProvider(INIT_DELAY);
+ TestEventsProvider provider2 = new TestEventsProvider(INIT_DELAY);
+ OpenFeatureAPI.getInstance().setProvider(name, provider1);
+ Client client = OpenFeatureAPI.getInstance().getClient(name);
+
+ // attached handlers
+ OpenFeatureAPI.getInstance().onProviderConfigurationChanged(handler1);
+ client.onProviderConfigurationChanged(handler2);
+
+ OpenFeatureAPI.getInstance().setProvider(name, provider2);
+
+ // wait for the new provider to be ready and make sure things are cleaned up.
+ await().until(() -> provider1.isShutDown());
+
+ // fire old event
+ provider1.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, EventDetails.builder().build());
+
+ // a bit of waiting here, but we want to make sure these are indeed never
+ // called.
+ verify(handler1, after(TIMEOUT).never()).accept(any());
+ verify(handler2, never()).accept(any());
+ }
+
+ @Test
+ @DisplayName("other client handlers should not run")
+ @Specification(number = "5.1.3", text = "When a provider signals the occurrence of a particular event, " +
+ "event handlers on clients which are not associated with that provider MUST NOT run.")
+ void otherClientHandlersShouldNotRun() throws Exception {
+ final String name1 = "otherClientHandlersShouldNotRun1";
+ final String name2 = "otherClientHandlersShouldNotRun2";
+ final Consumer handlerToRun = mockHandler();
+ final Consumer handlerNotToRun = mockHandler();
+
+ TestEventsProvider provider1 = new TestEventsProvider(INIT_DELAY);
+ TestEventsProvider provider2 = new TestEventsProvider(INIT_DELAY);
+ OpenFeatureAPI.getInstance().setProvider(name1, provider1);
+ OpenFeatureAPI.getInstance().setProvider(name2, provider2);
+
+ Client client1 = OpenFeatureAPI.getInstance().getClient(name1);
+ Client client2 = OpenFeatureAPI.getInstance().getClient(name2);
+
+ client1.onProviderConfigurationChanged(handlerToRun);
+ client2.onProviderConfigurationChanged(handlerNotToRun);
+
+ provider1.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().build());
+
+ verify(handlerToRun, timeout(TIMEOUT)).accept(any());
+ verify(handlerNotToRun, never()).accept(any());
+ }
+
+ @Test
+ @DisplayName("bound named client handlers should not run with default")
+ @Specification(number = "5.1.3", text = "When a provider signals the occurrence of a particular event, " +
+ "event handlers on clients which are not associated with that provider MUST NOT run.")
+ void boundShouldNotRunWithDefault() throws Exception {
+ final String name = "boundShouldNotRunWithDefault";
+ final Consumer handlerNotToRun = mockHandler();
+
+ TestEventsProvider namedProvider = new TestEventsProvider(INIT_DELAY);
+ TestEventsProvider defaultProvider = new TestEventsProvider(INIT_DELAY);
+ OpenFeatureAPI.getInstance().setProvider(defaultProvider);
+
+ Client client = OpenFeatureAPI.getInstance().getClient(name);
+ client.onProviderConfigurationChanged(handlerNotToRun);
+ OpenFeatureAPI.getInstance().setProvider(name, namedProvider);
+
+ // await the new provider to make sure the old one is shut down
+ await().until(() -> namedProvider.getState().equals(ProviderState.READY));
+
+ // fire event on default provider
+ defaultProvider.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().build());
+
+ verify(handlerNotToRun, after(TIMEOUT).never()).accept(any());
+ OpenFeatureAPI.getInstance().setProvider(new NoOpProvider());
+ }
+
+ @Test
+ @DisplayName("unbound named client handlers should run with default")
+ @Specification(number = "5.1.3", text = "When a provider signals the occurrence of a particular event, " +
+ "event handlers on clients which are not associated with that provider MUST NOT run.")
+ void unboundShouldRunWithDefault() throws Exception {
+ final String name = "unboundShouldRunWithDefault";
+ final Consumer handlerToRun = mockHandler();
+
+ TestEventsProvider defaultProvider = new TestEventsProvider(INIT_DELAY);
+ OpenFeatureAPI.getInstance().setProvider(defaultProvider);
+
+ Client client = OpenFeatureAPI.getInstance().getClient(name);
+ client.onProviderConfigurationChanged(handlerToRun);
+
+ // await the new provider to make sure the old one is shut down
+ await().until(() -> defaultProvider.getState().equals(ProviderState.READY));
+
+ // fire event on default provider
+ defaultProvider.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().build());
+
+ verify(handlerToRun, timeout(TIMEOUT)).accept(any());
+ OpenFeatureAPI.getInstance().setProvider(new NoOpProvider());
+ }
+
+ @Test
+ @DisplayName("subsequent handlers run if earlier throws")
+ @Specification(number = "5.2.5", text = "If a handler function terminates abnormally, other handler functions MUST run.")
+ void handlersRunIfOneThrows() throws Exception {
+ final String name = "handlersRunIfOneThrows";
+ final Consumer errorHandler = mockHandler();
+ doThrow(new NullPointerException()).when(errorHandler).accept(any());
+ final Consumer nextHandler = mockHandler();
+ final Consumer lastHandler = mockHandler();
+
+ TestEventsProvider provider = new TestEventsProvider(INIT_DELAY);
+ OpenFeatureAPI.getInstance().setProvider(name, provider);
+
+ Client client1 = OpenFeatureAPI.getInstance().getClient(name);
+
+ client1.onProviderConfigurationChanged(errorHandler);
+ client1.onProviderConfigurationChanged(nextHandler);
+ client1.onProviderConfigurationChanged(lastHandler);
+
+ provider.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().build());
+ verify(errorHandler, timeout(TIMEOUT)).accept(any());
+ verify(nextHandler, timeout(TIMEOUT)).accept(any());
+ verify(lastHandler, timeout(TIMEOUT)).accept(any());
+ }
+
+ @Test
+ @DisplayName("should have all properties")
+ @Specification(number = "5.2.4", text = "The handler function MUST accept a event details parameter.")
+ @Specification(number = "5.2.3", text = "The event details MUST contain the client name associated with the event.")
+ void shouldHaveAllProperties() throws Exception {
+ final Consumer handler1 = mockHandler();
+ final Consumer handler2 = mockHandler();
+ final String name = "shouldHaveAllProperties";
+
+ TestEventsProvider provider = new TestEventsProvider(INIT_DELAY);
+ OpenFeatureAPI.getInstance().setProvider(name, provider);
+ Client client = OpenFeatureAPI.getInstance().getClient(name);
+
+ // attached handlers
+ OpenFeatureAPI.getInstance().onProviderConfigurationChanged(handler1);
+ client.onProviderConfigurationChanged(handler2);
+
+ List flagsChanged = Arrays.asList("flag");
+ ImmutableMetadata metadata = ImmutableMetadata.builder().addInteger("int", 1).build();
+ String message = "a message";
+ ProviderEventDetails details = ProviderEventDetails.builder()
+ .eventMetadata(metadata)
+ .flagsChanged(flagsChanged)
+ .message(message)
+ .build();
+
+ provider.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, details);
+
+ // both global and client handler should have all the fields.
+ verify(handler1, timeout(TIMEOUT))
+ .accept(argThat((EventDetails eventDetails) -> {
+ return metadata.equals(eventDetails.getEventMetadata())
+ // TODO: issue for client name in events
+ && flagsChanged.equals(eventDetails.getFlagsChanged())
+ && message.equals(eventDetails.getMessage());
+ }));
+ verify(handler2, timeout(TIMEOUT))
+ .accept(argThat((EventDetails eventDetails) -> {
+ return metadata.equals(eventDetails.getEventMetadata())
+ && flagsChanged.equals(eventDetails.getFlagsChanged())
+ && message.equals(eventDetails.getMessage())
+ && name.equals(eventDetails.getClientName());
+ }));
+ }
+
+ @Test
+ @DisplayName("if the provider is ready handlers must run immediately")
+ @Specification(number = "5.3.3", text = "PROVIDER_READY handlers attached after the provider is already in a ready state MUST run immediately.")
+ void readyMustRunImmediately() throws Exception {
+ final String name = "readyMustRunImmediately";
+ final Consumer handler = mockHandler();
+
+ // provider which is already ready
+ TestEventsProvider provider = new TestEventsProvider(ProviderState.READY);
+ OpenFeatureAPI.getInstance().setProvider(name, provider);
+
+ // should run even thought handler was added after ready
+ Client client = OpenFeatureAPI.getInstance().getClient(name);
+ client.onProviderReady(handler);
+ verify(handler, timeout(TIMEOUT)).accept(any());
+ }
+
+ @Test
+ @DisplayName("must persist across changes")
+ @Specification(number = "5.2.6", text = "Event handlers MUST persist across provider changes.")
+ void mustPersistAcrossChanges() throws Exception {
+ final String name = "mustPersistAcrossChanges";
+ final Consumer handler = mockHandler();
+
+ TestEventsProvider provider1 = new TestEventsProvider(INIT_DELAY);
+ TestEventsProvider provider2 = new TestEventsProvider(INIT_DELAY);
+
+ OpenFeatureAPI.getInstance().setProvider(name, provider1);
+ Client client = OpenFeatureAPI.getInstance().getClient(name);
+ client.onProviderConfigurationChanged(handler);
+
+ provider1.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().build());
+ ArgumentMatcher nameMatches = (EventDetails details) -> details.getClientName().equals(name);
+
+ verify(handler, timeout(TIMEOUT).times(1)).accept(argThat(nameMatches));
+
+ // wait for the new provider to be ready.
+ OpenFeatureAPI.getInstance().setProvider(name, provider2);
+ await().until(() -> provider2.getState().equals(ProviderState.READY));
+
+ // verify that with the new provider under the same name, the handler is called
+ // again.
+ provider2.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().build());
+ verify(handler, timeout(TIMEOUT).times(2)).accept(argThat(nameMatches));
+ }
+
+ @Nested
+ class HandlerRemoval {
+ @Test
+ @DisplayName("should not run removed events")
+ void removedEventsShouldNotRun() {
+ final String name = "removedEventsShouldNotRun";
+ final Consumer handler1 = mockHandler();
+ final Consumer handler2 = mockHandler();
+
+ TestEventsProvider provider = new TestEventsProvider(INIT_DELAY);
+ OpenFeatureAPI.getInstance().setProvider(name, provider);
+ Client client = OpenFeatureAPI.getInstance().getClient(name);
+
+ // attached handlers
+ OpenFeatureAPI.getInstance().onProviderStale(handler1);
+ client.onProviderConfigurationChanged(handler2);
+
+ OpenFeatureAPI.getInstance().removeHandler(ProviderEvent.PROVIDER_STALE, handler1);
+ client.removeHandler(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, handler2);
+
+ // emit event
+ provider.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().build());
+
+ // both global and client handlers should not run.
+ verify(handler1, after(TIMEOUT).never()).accept(any());
+ verify(handler2, never()).accept(any());
+ }
+ }
+
+ @Specification(number = "5.1.4", text = "PROVIDER_ERROR events SHOULD populate the provider event details's error message field.")
+ @Test
+ void thisIsAProviderRequirement() {
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Consumer mockHandler() {
+ return mock(Consumer.class);
+ }
+}
diff --git a/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java b/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java
index 57f0c045..eb41fd95 100644
--- a/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java
+++ b/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java
@@ -62,20 +62,20 @@ void getApiInstance() {
assertSame(OpenFeatureAPI.getInstance(), OpenFeatureAPI.getInstance());
}
- @Specification(number="1.1.2", text="The API MUST provide a function to set the global provider singleton, which accepts an API-conformant provider implementation.")
+ @Specification(number="1.1.2.1", text="The API MUST define a provider mutator, a function to set the default provider, which accepts an API-conformant provider implementation.")
@Test void provider() {
FeatureProvider mockProvider = mock(FeatureProvider.class);
FeatureProviderTestUtils.setFeatureProvider(mockProvider);
assertThat(api.getProvider()).isEqualTo(mockProvider);
}
- @Specification(number="1.1.4", text="The API MUST provide a function for retrieving the metadata field of the configured provider.")
+ @Specification(number="1.1.5", text="The API MUST provide a function for retrieving the metadata field of the configured provider.")
@Test void provider_metadata() {
FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider());
assertThat(api.getProviderMetadata().getName()).isEqualTo(DoSomethingProvider.name);
}
- @Specification(number="1.1.3", text="The API MUST provide a function to add hooks which accepts one or more API-conformant hooks, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.")
+ @Specification(number="1.1.4", text="The API MUST provide a function to add hooks which accepts one or more API-conformant hooks, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.")
@Test void hook_addition() {
Hook h1 = mock(Hook.class);
Hook h2 = mock(Hook.class);
@@ -89,7 +89,7 @@ void getApiInstance() {
assertEquals(h2, api.getHooks().get(1));
}
- @Specification(number="1.1.5", text="The API MUST provide a function for creating a client which accepts the following options: - name (optional): A logical string identifier for the client.")
+ @Specification(number="1.1.6", text="The API MUST provide a function for creating a client which accepts the following options: - name (optional): A logical string identifier for the client.")
@Test void namedClient() {
assertThatCode(() -> api.getClient("Sir Calls-a-lot")).doesNotThrowAnyException();
// TODO: Doesn't say that you can *get* the client name.. which seems useful?
@@ -286,7 +286,7 @@ void getApiInstance() {
@Specification(number="1.3.3", text="The client SHOULD guarantee the returned value of any typed flag evaluation method is of the expected type. If the value returned by the underlying provider implementation does not match the expected type, it's to be considered abnormal execution, and the supplied default value should be returned.")
@Test void type_system_prevents_this() {}
- @Specification(number="1.1.6", text="The client creation function MUST NOT throw, or otherwise abnormally terminate.")
+ @Specification(number="1.1.7", text="The client creation function MUST NOT throw, or otherwise abnormally terminate.")
@Test void constructor_does_not_throw() {}
@Specification(number="1.4.11", text="The client SHOULD provide asynchronous or non-blocking mechanisms for flag evaluation.")
diff --git a/src/test/java/dev/openfeature/sdk/InitializeBehaviorSpecTest.java b/src/test/java/dev/openfeature/sdk/InitializeBehaviorSpecTest.java
index 7061719f..0ab5e371 100644
--- a/src/test/java/dev/openfeature/sdk/InitializeBehaviorSpecTest.java
+++ b/src/test/java/dev/openfeature/sdk/InitializeBehaviorSpecTest.java
@@ -21,12 +21,13 @@ class DefaultProvider {
@Test
@DisplayName("must call initialize function of the newly registered provider before using it for "
+ "flag evaluation")
- void mustCallInitializeFunctionOfTheNewlyRegisteredProviderBeforeUsingItForFlagEvaluation() {
+ void mustCallInitializeFunctionOfTheNewlyRegisteredProviderBeforeUsingItForFlagEvaluation() throws Exception {
FeatureProvider featureProvider = mock(FeatureProvider.class);
+ doReturn(ProviderState.NOT_READY).when(featureProvider).getState();
OpenFeatureAPI.getInstance().setProvider(featureProvider);
- verify(featureProvider, timeout(1000)).initialize();
+ verify(featureProvider, timeout(1000)).initialize(any());
}
@Specification(number = "1.4.9", text = "Methods, functions, or operations on the client MUST NOT throw "
@@ -35,14 +36,15 @@ void mustCallInitializeFunctionOfTheNewlyRegisteredProviderBeforeUsingItForFlagE
+ "the purposes for configuration or setup.")
@Test
@DisplayName("should catch exception thrown by the provider on initialization")
- void shouldCatchExceptionThrownByTheProviderOnInitialization() {
+ void shouldCatchExceptionThrownByTheProviderOnInitialization() throws Exception {
FeatureProvider featureProvider = mock(FeatureProvider.class);
- doThrow(TestException.class).when(featureProvider).initialize();
+ doReturn(ProviderState.NOT_READY).when(featureProvider).getState();
+ doThrow(TestException.class).when(featureProvider).initialize(any());
assertThatCode(() -> OpenFeatureAPI.getInstance().setProvider(featureProvider))
.doesNotThrowAnyException();
- verify(featureProvider, timeout(1000)).initialize();
+ verify(featureProvider, timeout(1000)).initialize(any());
}
}
@@ -54,12 +56,13 @@ class ProviderForNamedClient {
@Test
@DisplayName("must call initialize function of the newly registered named provider before using it "
+ "for flag evaluation")
- void mustCallInitializeFunctionOfTheNewlyRegisteredNamedProviderBeforeUsingItForFlagEvaluation() {
+ void mustCallInitializeFunctionOfTheNewlyRegisteredNamedProviderBeforeUsingItForFlagEvaluation() throws Exception {
FeatureProvider featureProvider = mock(FeatureProvider.class);
+ doReturn(ProviderState.NOT_READY).when(featureProvider).getState();
OpenFeatureAPI.getInstance().setProvider("clientName", featureProvider);
- verify(featureProvider, timeout(1000)).initialize();
+ verify(featureProvider, timeout(1000)).initialize(any());
}
@Specification(number = "1.4.9", text = "Methods, functions, or operations on the client MUST NOT throw "
@@ -68,14 +71,15 @@ void mustCallInitializeFunctionOfTheNewlyRegisteredNamedProviderBeforeUsingItFor
+ "the purposes for configuration or setup.")
@Test
@DisplayName("should catch exception thrown by the named client provider on initialization")
- void shouldCatchExceptionThrownByTheNamedClientProviderOnInitialization() {
+ void shouldCatchExceptionThrownByTheNamedClientProviderOnInitialization() throws Exception {
FeatureProvider featureProvider = mock(FeatureProvider.class);
- doThrow(TestException.class).when(featureProvider).initialize();
+ doReturn(ProviderState.NOT_READY).when(featureProvider).getState();
+ doThrow(TestException.class).when(featureProvider).initialize(any());
assertThatCode(() -> OpenFeatureAPI.getInstance().setProvider("clientName", featureProvider))
.doesNotThrowAnyException();
- verify(featureProvider, timeout(1000)).initialize();
+ verify(featureProvider, timeout(1000)).initialize(any());
}
}
}
diff --git a/src/test/java/dev/openfeature/sdk/LockingTest.java b/src/test/java/dev/openfeature/sdk/LockingTest.java
index 3d8d90c8..d9601e85 100644
--- a/src/test/java/dev/openfeature/sdk/LockingTest.java
+++ b/src/test/java/dev/openfeature/sdk/LockingTest.java
@@ -6,19 +6,20 @@
import static org.mockito.Mockito.when;
import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.Consumer;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
class LockingTest {
-
+
private static OpenFeatureAPI api;
private OpenFeatureClient client;
- private AutoCloseableReentrantReadWriteLock apiContextLock;
- private AutoCloseableReentrantReadWriteLock apiHooksLock;
+ private AutoCloseableReentrantReadWriteLock apiLock;
private AutoCloseableReentrantReadWriteLock clientContextLock;
private AutoCloseableReentrantReadWriteLock clientHooksLock;
@@ -31,10 +32,8 @@ static void beforeAll() {
void beforeEach() {
client = (OpenFeatureClient) api.getClient();
- apiContextLock = setupLock(apiContextLock, mockInnerReadLock(), mockInnerWriteLock());
- apiHooksLock = setupLock(apiHooksLock, mockInnerReadLock(), mockInnerWriteLock());
- OpenFeatureAPI.contextLock = apiContextLock;
- OpenFeatureAPI.hooksLock = apiHooksLock;
+ apiLock = setupLock(apiLock, mockInnerReadLock(), mockInnerWriteLock());
+ OpenFeatureAPI.lock = apiLock;
clientContextLock = setupLock(clientContextLock, mockInnerReadLock(), mockInnerWriteLock());
clientHooksLock = setupLock(clientHooksLock, mockInnerReadLock(), mockInnerWriteLock());
@@ -42,6 +41,101 @@ void beforeEach() {
client.hooksLock = clientHooksLock;
}
+ @Nested
+ class EventsLocking {
+
+ @Nested
+ class Api {
+
+ @Test
+ void onShouldWriteLockAndUnlock() {
+ Consumer handler = mock(Consumer.class);
+ api.on(ProviderEvent.PROVIDER_READY, handler);
+ verify(apiLock.writeLock()).lock();
+ verify(apiLock.writeLock()).unlock();
+ }
+
+ @Test
+ void onProviderReadyShouldWriteLockAndUnlock() {
+ Consumer handler = mock(Consumer.class);
+ api.onProviderReady(handler);
+ verify(apiLock.writeLock()).lock();
+ verify(apiLock.writeLock()).unlock();
+ }
+
+ @Test
+ void onProviderConfigurationChangedShouldWriteLockAndUnlock() {
+ Consumer handler = mock(Consumer.class);
+ api.onProviderConfigurationChanged(handler);
+ verify(apiLock.writeLock()).lock();
+ verify(apiLock.writeLock()).unlock();
+ }
+
+ @Test
+ void onProviderStaleShouldWriteLockAndUnlock() {
+ Consumer handler = mock(Consumer.class);
+ api.onProviderStale(handler);
+ verify(apiLock.writeLock()).lock();
+ verify(apiLock.writeLock()).unlock();
+ }
+
+ @Test
+ void onProviderErrorShouldWriteLockAndUnlock() {
+ Consumer handler = mock(Consumer.class);
+ api.onProviderError(handler);
+ verify(apiLock.writeLock()).lock();
+ verify(apiLock.writeLock()).unlock();
+ }
+ }
+
+ @Nested
+ class Client {
+
+ // Note that the API lock is used for adding client handlers, they are all added (indirectly) on the API object.
+
+ @Test
+ void onShouldApiWriteLockAndUnlock() {
+ Consumer handler = mock(Consumer.class);
+ client.on(ProviderEvent.PROVIDER_READY, handler);
+ verify(apiLock.writeLock()).lock();
+ verify(apiLock.writeLock()).unlock();
+ }
+
+ @Test
+ void onProviderReadyShouldApiWriteLockAndUnlock() {
+ Consumer handler = mock(Consumer.class);
+ api.onProviderReady(handler);
+ verify(apiLock.writeLock()).lock();
+ verify(apiLock.writeLock()).unlock();
+ }
+
+ @Test
+ void onProviderConfigurationChangedProviderReadyShouldApiWriteLockAndUnlock() {
+ Consumer handler = mock(Consumer.class);
+ api.onProviderConfigurationChanged(handler);
+ verify(apiLock.writeLock()).lock();
+ verify(apiLock.writeLock()).unlock();
+ }
+
+ @Test
+ void onProviderStaleProviderReadyShouldApiWriteLockAndUnlock() {
+ Consumer handler = mock(Consumer.class);
+ api.onProviderStale(handler);
+ verify(apiLock.writeLock()).lock();
+ verify(apiLock.writeLock()).unlock();
+ }
+
+ @Test
+ void onProviderErrorProviderReadyShouldApiWriteLockAndUnlock() {
+ Consumer handler = mock(Consumer.class);
+ api.onProviderError(handler);
+ verify(apiLock.writeLock()).lock();
+ verify(apiLock.writeLock()).unlock();
+ }
+ }
+ }
+
+
@Test
void addHooksShouldWriteLockAndUnlock() {
client.addHooks(new Hook() {
@@ -51,8 +145,8 @@ void addHooksShouldWriteLockAndUnlock() {
api.addHooks(new Hook() {
});
- verify(apiHooksLock.writeLock()).lock();
- verify(apiHooksLock.writeLock()).unlock();
+ verify(apiLock.writeLock()).lock();
+ verify(apiLock.writeLock()).unlock();
}
@Test
@@ -62,8 +156,8 @@ void getHooksShouldReadLockAndUnlock() {
verify(clientHooksLock.readLock()).unlock();
api.getHooks();
- verify(apiHooksLock.readLock()).lock();
- verify(apiHooksLock.readLock()).unlock();
+ verify(apiLock.readLock()).lock();
+ verify(apiLock.readLock()).unlock();
}
@Test
@@ -73,8 +167,8 @@ void setContextShouldWriteLockAndUnlock() {
verify(clientContextLock.writeLock()).unlock();
api.setEvaluationContext(new ImmutableContext());
- verify(apiContextLock.writeLock()).lock();
- verify(apiContextLock.writeLock()).unlock();
+ verify(apiLock.writeLock()).lock();
+ verify(apiLock.writeLock()).unlock();
}
@Test
@@ -84,16 +178,16 @@ void getContextShouldReadLockAndUnlock() {
verify(clientContextLock.readLock()).unlock();
api.getEvaluationContext();
- verify(apiContextLock.readLock()).lock();
- verify(apiContextLock.readLock()).unlock();
+ verify(apiLock.readLock()).lock();
+ verify(apiLock.readLock()).unlock();
}
@Test
void clearHooksShouldWriteLockAndUnlock() {
api.clearHooks();
- verify(apiHooksLock.writeLock()).lock();
- verify(apiHooksLock.writeLock()).unlock();
+ verify(apiLock.writeLock()).lock();
+ verify(apiLock.writeLock()).unlock();
}
private static ReentrantReadWriteLock.ReadLock mockInnerReadLock() {
diff --git a/src/test/java/dev/openfeature/sdk/ProviderRepositoryTest.java b/src/test/java/dev/openfeature/sdk/ProviderRepositoryTest.java
index 00c7949e..5b6dac1b 100644
--- a/src/test/java/dev/openfeature/sdk/ProviderRepositoryTest.java
+++ b/src/test/java/dev/openfeature/sdk/ProviderRepositoryTest.java
@@ -1,35 +1,40 @@
package dev.openfeature.sdk;
-import dev.openfeature.sdk.testutils.exception.TestException;
-import lombok.SneakyThrows;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Nested;
-import org.junit.jupiter.api.Test;
+import static dev.openfeature.sdk.fixtures.ProviderFixture.createMockedErrorProvider;
+import static dev.openfeature.sdk.fixtures.ProviderFixture.createMockedProvider;
+import static dev.openfeature.sdk.fixtures.ProviderFixture.createMockedReadyProvider;
+import static dev.openfeature.sdk.testutils.stubbing.ConditionStubber.doDelayResponse;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.awaitility.Awaitility.await;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
import java.time.Duration;
-import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
import java.util.function.Function;
-import static dev.openfeature.sdk.fixtures.ProviderFixture.*;
-import static dev.openfeature.sdk.testutils.stubbing.ConditionStubber.doBlock;
-import static dev.openfeature.sdk.testutils.stubbing.ConditionStubber.doDelayResponse;
-import static java.util.concurrent.TimeUnit.SECONDS;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatCode;
-import static org.awaitility.Awaitility.await;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.*;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import dev.openfeature.sdk.testutils.exception.TestException;
class ProviderRepositoryTest {
private static final String CLIENT_NAME = "client name";
private static final String ANOTHER_CLIENT_NAME = "another client name";
- private static final String FEATURE_KEY = "some key";
+ private static final int TIMEOUT = 5000;
private final ExecutorService executorService = Executors.newCachedThreadPool();
@@ -49,7 +54,8 @@ class DefaultProvider {
@Test
@DisplayName("should reject null as default provider")
void shouldRejectNullAsDefaultProvider() {
- assertThatCode(() -> providerRepository.setProvider(null)).isInstanceOf(IllegalArgumentException.class);
+ assertThatCode(() -> providerRepository.setProvider(null, mockAfterSet(), mockAfterInit(),
+ mockAfterShutdown(), mockAfterError())).isInstanceOf(IllegalArgumentException.class);
}
@Test
@@ -60,78 +66,31 @@ void shouldHaveNoOpProviderSetAsDefaultOnInitialization() {
@Test
@DisplayName("should immediately return when calling the provider mutator")
- void shouldImmediatelyReturnWhenCallingTheProviderMutator() {
+ void shouldImmediatelyReturnWhenCallingTheProviderMutator() throws Exception {
FeatureProvider featureProvider = createMockedProvider();
- doDelayResponse(Duration.ofSeconds(10)).when(featureProvider).initialize();
+ doDelayResponse(Duration.ofSeconds(10)).when(featureProvider).initialize(new ImmutableContext());
await()
.alias("wait for provider mutator to return")
.pollDelay(Duration.ofMillis(1))
.atMost(Duration.ofSeconds(1))
.until(() -> {
- providerRepository.setProvider(featureProvider);
- verify(featureProvider, timeout(100)).initialize();
+ providerRepository.setProvider(featureProvider, mockAfterSet(), mockAfterInit(),
+ mockAfterShutdown(), mockAfterError());
+ verify(featureProvider, timeout(TIMEOUT)).initialize(any());
return true;
});
- verify(featureProvider).initialize();
- }
-
- @Test
- @DisplayName("should not return set provider if initialize has not yet been finished executing")
- void shouldNotReturnSetProviderIfItsInitializeMethodHasNotYetBeenFinishedExecuting() {
- CountDownLatch latch = new CountDownLatch(1);
- FeatureProvider newProvider = createMockedProvider();
- doBlock(latch).when(newProvider).initialize();
- FeatureProvider oldProvider = providerRepository.getProvider();
-
- providerRepository.setProvider(newProvider);
-
- FeatureProvider providerWhileInitialization = providerRepository.getProvider();
- latch.countDown();
-
- assertThat(providerWhileInitialization).isEqualTo(oldProvider);
- await()
- .pollDelay(Duration.ofMillis(1))
- .atMost(Duration.ofSeconds(1))
- .untilAsserted(() -> assertThat(providerRepository.getProvider()).isEqualTo(newProvider));
- verify(newProvider, timeout(100)).initialize();
- }
-
- @SneakyThrows
- @Test
- @DisplayName("should discard provider still initializing if a newer has finished before")
- void shouldDiscardProviderStillInitializingIfANewerHasFinishedBefore() {
- CountDownLatch latch = new CountDownLatch(1);
- CountDownLatch testBlockingLatch = new CountDownLatch(1);
- FeatureProvider blockedProvider = createBlockedProvider(latch, testBlockingLatch::countDown);
- FeatureProvider fastProvider = createUnblockingProvider(latch);
-
- providerRepository.setProvider(blockedProvider);
- providerRepository.setProvider(fastProvider);
-
- assertThat(testBlockingLatch.await(2, SECONDS))
- .as("blocking provider initialization not completed within 2 seconds")
- .isTrue();
-
- await()
- .pollDelay(Duration.ofMillis(1))
- .atMost(Duration.ofSeconds(1))
- .untilAsserted(() -> assertThat(providerRepository.getProvider()).isEqualTo(fastProvider));
-
- verify(blockedProvider, timeout(100)).initialize();
- verify(fastProvider, timeout(100)).initialize();
+ verify(featureProvider, timeout(TIMEOUT)).initialize(any());
}
@Test
@DisplayName("should avoid additional initialization call if provider has been initialized already")
- void shouldAvoidAdditionalInitializationCallIfProviderHasBeenInitializedAlready() {
- FeatureProvider provider = createMockedProvider();
- setFeatureProvider(CLIENT_NAME, provider);
-
+ void shouldAvoidAdditionalInitializationCallIfProviderHasBeenInitializedAlready() throws Exception {
+ FeatureProvider provider = createMockedReadyProvider();
setFeatureProvider(provider);
-
- verify(provider).initialize();
+
+ verify(provider, never()).initialize(any());
}
}
@@ -141,90 +100,45 @@ class NamedProvider {
@Test
@DisplayName("should reject null as named provider")
void shouldRejectNullAsNamedProvider() {
- assertThatCode(() -> providerRepository.setProvider(CLIENT_NAME, null)).isInstanceOf(IllegalArgumentException.class);
+ assertThatCode(() -> providerRepository.setProvider(CLIENT_NAME, null, mockAfterSet(), mockAfterInit(),
+ mockAfterShutdown(), mockAfterError()))
+ .isInstanceOf(IllegalArgumentException.class);
}
@Test
@DisplayName("should reject null as client name")
void shouldRejectNullAsDefaultProvider() {
NoOpProvider provider = new NoOpProvider();
- assertThatCode(() -> providerRepository.setProvider(null, provider)).isInstanceOf(IllegalArgumentException.class);
+ assertThatCode(() -> providerRepository.setProvider(null, provider, mockAfterSet(), mockAfterInit(),
+ mockAfterShutdown(), mockAfterError()))
+ .isInstanceOf(IllegalArgumentException.class);
}
@Test
@DisplayName("should immediately return when calling the named client provider mutator")
- void shouldImmediatelyReturnWhenCallingTheNamedClientProviderMutator() {
+ void shouldImmediatelyReturnWhenCallingTheNamedClientProviderMutator() throws Exception {
FeatureProvider featureProvider = createMockedProvider();
- doDelayResponse(Duration.ofSeconds(10)).when(featureProvider).initialize();
+ doDelayResponse(Duration.ofSeconds(10)).when(featureProvider).initialize(any());
await()
.alias("wait for provider mutator to return")
.pollDelay(Duration.ofMillis(1))
.atMost(Duration.ofSeconds(1))
.until(() -> {
- providerRepository.setProvider("named client", featureProvider);
- verify(featureProvider, timeout(1000)).initialize();
+ providerRepository.setProvider("named client", featureProvider, mockAfterSet(),
+ mockAfterInit(), mockAfterShutdown(), mockAfterError());
+ verify(featureProvider, timeout(TIMEOUT)).initialize(any());
return true;
});
}
- @Test
- @DisplayName("should not return set provider if it's initialization has not yet been finished executing")
- void shouldNotReturnSetProviderIfItsInitializeMethodHasNotYetBeenFinishedExecuting() {
- CountDownLatch latch = new CountDownLatch(1);
- FeatureProvider newProvider = createMockedProvider();
- doBlock(latch).when(newProvider).initialize();
- FeatureProvider oldProvider = createMockedProvider();
- setFeatureProvider(CLIENT_NAME, oldProvider);
-
- providerRepository.setProvider(CLIENT_NAME, newProvider);
- FeatureProvider providerWhileInitialization = getNamedProvider();
- latch.countDown();
-
- assertThat(providerWhileInitialization).isEqualTo(oldProvider);
- await()
- .pollDelay(Duration.ofMillis(1))
- .atMost(Duration.ofSeconds(1))
- .untilAsserted(() -> assertThat(getNamedProvider()).isEqualTo(newProvider));
- verify(newProvider, timeout(100)).initialize();
- }
-
- @SneakyThrows
- @Test
- @DisplayName("should discard provider still initializing if a newer has finished before")
- void shouldDiscardProviderStillInitializingIfANewerHasFinishedBefore() {
- String clientName = "clientName";
- CountDownLatch latch = new CountDownLatch(1);
- CountDownLatch testBlockingLatch = new CountDownLatch(1);
- FeatureProvider blockedProvider = createBlockedProvider(latch, testBlockingLatch::countDown);
- FeatureProvider unblockingProvider = createUnblockingProvider(latch);
-
- providerRepository.setProvider(clientName, blockedProvider);
- providerRepository.setProvider(clientName, unblockingProvider);
-
- assertThat(testBlockingLatch.await(2, SECONDS))
- .as("blocking provider initialization not completed within 2 seconds")
- .isTrue();
-
- await()
- .pollDelay(Duration.ofMillis(1))
- .atMost(Duration.ofSeconds(1))
- .untilAsserted(() -> assertThat(providerRepository.getProvider(clientName))
- .isEqualTo(unblockingProvider));
-
- verify(blockedProvider, timeout(100)).initialize();
- verify(unblockingProvider, timeout(100)).initialize();
- }
-
@Test
@DisplayName("should avoid additional initialization call if provider has been initialized already")
- void shouldAvoidAdditionalInitializationCallIfProviderHasBeenInitializedAlready() {
- FeatureProvider provider = createMockedProvider();
- setFeatureProvider(provider);
-
+ void shouldAvoidAdditionalInitializationCallIfProviderHasBeenInitializedAlready() throws Exception {
+ FeatureProvider provider = createMockedReadyProvider();
setFeatureProvider(CLIENT_NAME, provider);
- verify(provider).initialize();
+ verify(provider, never()).initialize(any());
}
}
}
@@ -237,43 +151,22 @@ class DefaultProvider {
@Test
@DisplayName("should immediately return when calling the provider mutator")
- void shouldImmediatelyReturnWhenCallingTheProviderMutator() {
+ void shouldImmediatelyReturnWhenCallingTheProviderMutator() throws Exception {
FeatureProvider newProvider = createMockedProvider();
- doDelayResponse(Duration.ofSeconds(10)).when(newProvider).initialize();
+ doDelayResponse(Duration.ofSeconds(10)).when(newProvider).initialize(any());
await()
.alias("wait for provider mutator to return")
.pollDelay(Duration.ofMillis(1))
.atMost(Duration.ofSeconds(1))
.until(() -> {
- providerRepository.setProvider(newProvider);
- verify(newProvider, timeout(100)).initialize();
+ providerRepository.setProvider(newProvider, mockAfterSet(), mockAfterInit(),
+ mockAfterShutdown(), mockAfterError());
+ verify(newProvider, timeout(TIMEOUT)).initialize(any());
return true;
});
- verify(newProvider).initialize();
- }
-
- @Test
- @DisplayName("should use old provider if replacing one has not yet been finished initializing")
- void shouldUseOldProviderIfReplacingOneHasNotYetBeenFinishedInitializing() {
- CountDownLatch latch = new CountDownLatch(1);
- FeatureProvider newProvider = createMockedProvider();
- doBlock(latch).when(newProvider).initialize();
- FeatureProvider oldProvider = createMockedProvider();
-
- setFeatureProvider(oldProvider);
- providerRepository.setProvider(newProvider);
-
- providerRepository.getProvider().getBooleanEvaluation("some key", true, null);
- latch.countDown();
-
- await()
- .atMost(Duration.ofSeconds(1))
- .pollDelay(Duration.ofMillis(1))
- .untilAsserted(() -> assertThat(getProvider()).isEqualTo(newProvider));
- verify(oldProvider, timeout(100)).getBooleanEvaluation(any(), any(), any());
- verify(newProvider, never()).getBooleanEvaluation(any(), any(), any());
+ verify(newProvider, timeout(TIMEOUT)).initialize(any());
}
@Test
@@ -295,12 +188,13 @@ class NamedProvider {
@Test
@DisplayName("should immediately return when calling the provider mutator")
- void shouldImmediatelyReturnWhenCallingTheProviderMutator() {
+ void shouldImmediatelyReturnWhenCallingTheProviderMutator() throws Exception {
FeatureProvider newProvider = createMockedProvider();
- doDelayResponse(Duration.ofSeconds(10)).when(newProvider).initialize();
+ doDelayResponse(Duration.ofSeconds(10)).when(newProvider).initialize(any());
Future> providerMutation = executorService
- .submit(() -> providerRepository.setProvider(CLIENT_NAME, newProvider));
+ .submit(() -> providerRepository.setProvider(CLIENT_NAME, newProvider, mockAfterSet(),
+ mockAfterInit(), mockAfterShutdown(), mockAfterError()));
await()
.alias("wait for provider mutator to return")
@@ -309,34 +203,13 @@ void shouldImmediatelyReturnWhenCallingTheProviderMutator() {
.until(providerMutation::isDone);
}
- @Test
- @DisplayName("should use old provider if replacement one has not yet been finished initializing")
- void shouldUseOldProviderIfReplacementHasNotYetBeenFinishedInitializing() {
- CountDownLatch latch = new CountDownLatch(1);
- FeatureProvider newProvider = createMockedProvider();
- doBlock(latch).when(newProvider).initialize();
- FeatureProvider oldProvider = createMockedProvider();
-
- setFeatureProvider(CLIENT_NAME, oldProvider);
- providerRepository.setProvider(CLIENT_NAME, newProvider);
-
- providerRepository.getProvider(CLIENT_NAME).getBooleanEvaluation(FEATURE_KEY, true, null);
- latch.countDown();
-
- await()
- .pollDelay(Duration.ofMillis(1))
- .atMost(Duration.ofSeconds(1))
- .untilAsserted(() -> assertThat(getNamedProvider()).isEqualTo(newProvider));
- verify(oldProvider, timeout(100)).getBooleanEvaluation(eq(FEATURE_KEY), any(), any());
- verify(newProvider, never()).getBooleanEvaluation(any(), any(), any());
- }
-
@Test
@DisplayName("should not call shutdown if replaced provider is bound to multiple names")
- void shouldNotCallShutdownIfReplacedProviderIsBoundToMultipleNames() {
+ void shouldNotCallShutdownIfReplacedProviderIsBoundToMultipleNames() throws InterruptedException {
FeatureProvider oldProvider = createMockedProvider();
FeatureProvider newProvider = createMockedProvider();
setFeatureProvider(CLIENT_NAME, oldProvider);
+
setFeatureProvider(ANOTHER_CLIENT_NAME, oldProvider);
setFeatureProvider(CLIENT_NAME, newProvider);
@@ -366,7 +239,48 @@ void shouldNotThrowExceptionIfProviderThrowsOneOnShutdown() {
assertThatCode(() -> setFeatureProvider(new NoOpProvider())).doesNotThrowAnyException();
- verify(provider).shutdown();
+ verify(provider, timeout(TIMEOUT)).shutdown();
+ }
+ }
+
+ @Nested
+ class LifecyleLambdas {
+ @Test
+ @DisplayName("should run afterSet, afterInit, afterShutdown on successful set/init")
+ @SuppressWarnings("unchecked")
+ void shouldRunLambdasOnSuccessful() {
+ Consumer afterSet = mock(Consumer.class);
+ Consumer afterInit = mock(Consumer.class);
+ Consumer afterShutdown = mock(Consumer.class);
+ BiConsumer afterError = mock(BiConsumer.class);
+
+ FeatureProvider oldProvider = providerRepository.getProvider();
+ FeatureProvider featureProvider1 = createMockedProvider();
+ FeatureProvider featureProvider2 = createMockedProvider();
+
+ setFeatureProvider(featureProvider1, afterSet, afterInit, afterShutdown, afterError);
+ setFeatureProvider(featureProvider2);
+ verify(afterSet, timeout(TIMEOUT)).accept(featureProvider1);
+ verify(afterInit, timeout(TIMEOUT)).accept(featureProvider1);
+ verify(afterShutdown, timeout(TIMEOUT)).accept(oldProvider);
+ verify(afterError, never()).accept(any(), any());
+ }
+
+ @Test
+ @DisplayName("should run afterSet, afterError on unsuccessful set/init")
+ @SuppressWarnings("unchecked")
+ void shouldRunLambdasOnError() throws Exception {
+ Consumer afterSet = mock(Consumer.class);
+ Consumer afterInit = mock(Consumer.class);
+ Consumer afterShutdown = mock(Consumer.class);
+ BiConsumer afterError = mock(BiConsumer.class);
+
+ FeatureProvider errorFeatureProvider = createMockedErrorProvider();
+
+ setFeatureProvider(errorFeatureProvider, afterSet, afterInit, afterShutdown, afterError);
+ verify(afterSet, timeout(TIMEOUT)).accept(errorFeatureProvider);
+ verify(afterInit, never()).accept(any());;
+ verify(afterError, timeout(TIMEOUT)).accept(eq(errorFeatureProvider), any());
}
}
}
@@ -385,31 +299,34 @@ void shouldShutdownAllFeatureProvidersOnShutdown() {
await()
.pollDelay(Duration.ofMillis(1))
- .atMost(Duration.ofSeconds(1))
+ .atMost(Duration.ofSeconds(TIMEOUT))
.untilAsserted(() -> {
assertThat(providerRepository.getProvider()).isInstanceOf(NoOpProvider.class);
assertThat(providerRepository.getProvider(CLIENT_NAME)).isInstanceOf(NoOpProvider.class);
assertThat(providerRepository.getProvider(ANOTHER_CLIENT_NAME)).isInstanceOf(NoOpProvider.class);
});
- verify(featureProvider1).shutdown();
- verify(featureProvider2).shutdown();
+ verify(featureProvider1, timeout(TIMEOUT)).shutdown();
+ verify(featureProvider2, timeout(TIMEOUT)).shutdown();
}
- private FeatureProvider getProvider() {
- return providerRepository.getProvider();
+ private void setFeatureProvider(FeatureProvider provider) {
+ providerRepository.setProvider(provider, mockAfterSet(), mockAfterInit(), mockAfterShutdown(),
+ mockAfterError());
+ waitForSettingProviderHasBeenCompleted(ProviderRepository::getProvider, provider);
}
- private FeatureProvider getNamedProvider() {
- return providerRepository.getProvider(CLIENT_NAME);
- }
- private void setFeatureProvider(FeatureProvider provider) {
- providerRepository.setProvider(provider);
+ private void setFeatureProvider(FeatureProvider provider, Consumer afterSet,
+ Consumer afterInit, Consumer afterShutdown,
+ BiConsumer afterError) {
+ providerRepository.setProvider(provider, afterSet, afterInit, afterShutdown,
+ afterError);
waitForSettingProviderHasBeenCompleted(ProviderRepository::getProvider, provider);
}
private void setFeatureProvider(String namedProvider, FeatureProvider provider) {
- providerRepository.setProvider(namedProvider, provider);
+ providerRepository.setProvider(namedProvider, provider, mockAfterSet(), mockAfterInit(), mockAfterShutdown(),
+ mockAfterError());
waitForSettingProviderHasBeenCompleted(repository -> repository.getProvider(namedProvider), provider);
}
@@ -418,8 +335,30 @@ private void waitForSettingProviderHasBeenCompleted(
FeatureProvider provider) {
await()
.pollDelay(Duration.ofMillis(1))
- .atMost(Duration.ofSeconds(1))
- .until(() -> extractor.apply(providerRepository) == provider);
+ .atMost(Duration.ofSeconds(5))
+ .until(() -> {
+ return extractor.apply(providerRepository) == provider;
+ });
+ }
+
+ private Consumer mockAfterSet() {
+ return fp -> {
+ };
+ }
+
+ private Consumer mockAfterInit() {
+ return fp -> {
+ };
+ }
+
+ private Consumer mockAfterShutdown() {
+ return fp -> {
+ };
+ }
+
+ private BiConsumer mockAfterError() {
+ return (fp, message) -> {
+ };
}
}
diff --git a/src/test/java/dev/openfeature/sdk/ProviderSpecTest.java b/src/test/java/dev/openfeature/sdk/ProviderSpecTest.java
index 31a6a5e8..f5e5e6a4 100644
--- a/src/test/java/dev/openfeature/sdk/ProviderSpecTest.java
+++ b/src/test/java/dev/openfeature/sdk/ProviderSpecTest.java
@@ -18,7 +18,7 @@ void name_accessor() {
@Specification(number = "2.2.2.1", text = "The feature provider interface MUST define methods for typed " +
"flag resolution, including boolean, numeric, string, and structure.")
@Specification(number = "2.2.3", text = "In cases of normal execution, the `provider` MUST populate the `resolution details` structure's `value` field with the resolved flag value.")
- @Specification(number = "2.2.1", text = "The `feature provider` interface MUST define methods to resolve flag values, with parameters `flag key` (string, required), `default value` (boolean | number | string | structure, required) + and `evaluation context` (optional), which returns a `resolution details` structure.")
+ @Specification(number = "2.2.1", text = "The `feature provider` interface MUST define methods to resolve flag values, with parameters `flag key` (string, required), `default value` (boolean | number | string | structure, required) and `evaluation context` (optional), which returns a `resolution details` structure.")
@Specification(number = "2.2.8.1", text = "The `resolution details` structure SHOULD accept a generic argument (or use an equivalent language feature) which indicates the type of the wrapped `value` field.")
@Test
void flag_value_set() {
diff --git a/src/test/java/dev/openfeature/sdk/ShutdownBehaviorSpecTest.java b/src/test/java/dev/openfeature/sdk/ShutdownBehaviorSpecTest.java
index d191c8c4..e470819f 100644
--- a/src/test/java/dev/openfeature/sdk/ShutdownBehaviorSpecTest.java
+++ b/src/test/java/dev/openfeature/sdk/ShutdownBehaviorSpecTest.java
@@ -89,7 +89,7 @@ void shouldCatchExceptionThrownByTheNamedClientProviderOnShutdown() {
@Nested
class General {
- @Specification(number = "1.6.1", text = "The API MUST define a shutdown function which, when called, must call the respective shutdown function on the active provider.")
+ @Specification(number = "1.6.1", text = "The API MUST define a mechanism to propagate a shutdown request to active providers.")
@Test
@DisplayName("must shutdown all providers on shutting down api")
void mustShutdownAllProvidersOnShuttingDownApi() {
@@ -110,7 +110,7 @@ void mustShutdownAllProvidersOnShuttingDownApi() {
verify(namedProvider).shutdown();
});
- api.resetProviderRepository();
+ api.reset();
}
}
}
diff --git a/src/test/java/dev/openfeature/sdk/fixtures/ProviderFixture.java b/src/test/java/dev/openfeature/sdk/fixtures/ProviderFixture.java
index f0b78642..c00b8ff2 100644
--- a/src/test/java/dev/openfeature/sdk/fixtures/ProviderFixture.java
+++ b/src/test/java/dev/openfeature/sdk/fixtures/ProviderFixture.java
@@ -1,24 +1,47 @@
package dev.openfeature.sdk.fixtures;
-import dev.openfeature.sdk.FeatureProvider;
-import lombok.experimental.UtilityClass;
-import org.mockito.stubbing.Answer;
+import static dev.openfeature.sdk.testutils.stubbing.ConditionStubber.doBlock;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import java.io.FileNotFoundException;
import java.util.concurrent.CountDownLatch;
-import static dev.openfeature.sdk.testutils.stubbing.ConditionStubber.doBlock;
-import static org.mockito.Mockito.*;
+import org.mockito.stubbing.Answer;
+
+import dev.openfeature.sdk.FeatureProvider;
+import dev.openfeature.sdk.ImmutableContext;
+import dev.openfeature.sdk.ProviderState;
+import lombok.experimental.UtilityClass;
@UtilityClass
public class ProviderFixture {
public static FeatureProvider createMockedProvider() {
- return mock(FeatureProvider.class);
+ FeatureProvider provider = mock(FeatureProvider.class);
+ doReturn(ProviderState.NOT_READY).when(provider).getState();
+ return provider;
+ }
+
+ public static FeatureProvider createMockedReadyProvider() {
+ FeatureProvider provider = mock(FeatureProvider.class);
+ doReturn(ProviderState.READY).when(provider).getState();
+ return provider;
+ }
+
+ public static FeatureProvider createMockedErrorProvider() throws Exception {
+ FeatureProvider provider = mock(FeatureProvider.class);
+ doReturn(ProviderState.NOT_READY).when(provider).getState();
+ doThrow(FileNotFoundException.class).when(provider).initialize(any());
+ return provider;
}
- public static FeatureProvider createBlockedProvider(CountDownLatch latch, Runnable onAnswer) {
+ public static FeatureProvider createBlockedProvider(CountDownLatch latch, Runnable onAnswer) throws Exception {
FeatureProvider provider = createMockedProvider();
- doBlock(latch, createAnswerExecutingCode(onAnswer)).when(provider).initialize();
+ doBlock(latch, createAnswerExecutingCode(onAnswer)).when(provider).initialize(new ImmutableContext());
doReturn("blockedProvider").when(provider).toString();
return provider;
}
@@ -30,12 +53,12 @@ private static Answer> createAnswerExecutingCode(Runnable onAnswer) {
};
}
- public static FeatureProvider createUnblockingProvider(CountDownLatch latch) {
+ public static FeatureProvider createUnblockingProvider(CountDownLatch latch) throws Exception {
FeatureProvider provider = createMockedProvider();
doAnswer(invocation -> {
latch.countDown();
return null;
- }).when(provider).initialize();
+ }).when(provider).initialize(new ImmutableContext());
doReturn("unblockingProvider").when(provider).toString();
return provider;
}
diff --git a/src/test/java/dev/openfeature/sdk/internal/TriConsumerTest.java b/src/test/java/dev/openfeature/sdk/internal/TriConsumerTest.java
new file mode 100644
index 00000000..0c85a7cc
--- /dev/null
+++ b/src/test/java/dev/openfeature/sdk/internal/TriConsumerTest.java
@@ -0,0 +1,34 @@
+package dev.openfeature.sdk.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class TriConsumerTest {
+
+ @Test
+ @DisplayName("should run accept")
+ void shouldRunAccept() {
+ AtomicInteger result = new AtomicInteger(0);
+ TriConsumer triConsumer = (num1, num2, num3) -> {
+ result.set(result.get() + num1 + num2 + num3);
+ };
+ triConsumer.accept(1, 2, 3);
+ assertEquals(6, result.get());
+ }
+
+ @Test
+ @DisplayName("should run after accept")
+ void shouldRunAfterAccept() {
+ AtomicInteger result = new AtomicInteger(0);
+ TriConsumer triConsumer = (num1, num2, num3) -> {
+ result.set(result.get() + num1 + num2 + num3);
+ };
+ TriConsumer composed = triConsumer.andThen(triConsumer);
+ composed.accept(1, 2, 3);
+ assertEquals(12, result.get());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/dev/openfeature/sdk/testutils/TestEventsProvider.java b/src/test/java/dev/openfeature/sdk/testutils/TestEventsProvider.java
new file mode 100644
index 00000000..3fcb5888
--- /dev/null
+++ b/src/test/java/dev/openfeature/sdk/testutils/TestEventsProvider.java
@@ -0,0 +1,99 @@
+package dev.openfeature.sdk.testutils;
+
+import dev.openfeature.sdk.EvaluationContext;
+import dev.openfeature.sdk.EventProvider;
+import dev.openfeature.sdk.Metadata;
+import dev.openfeature.sdk.ProviderEvaluation;
+import dev.openfeature.sdk.ProviderEvent;
+import dev.openfeature.sdk.ProviderEventDetails;
+import dev.openfeature.sdk.ProviderState;
+import dev.openfeature.sdk.Value;
+
+public class TestEventsProvider extends EventProvider {
+
+ private boolean initError = false;
+ private String initErrorMessage;
+ private ProviderState state = ProviderState.NOT_READY;
+ private boolean shutDown = false;
+ private int initTimeout = 0;
+
+ @Override
+ public ProviderState getState() {
+ return this.state;
+ }
+
+ public TestEventsProvider(int initTimeout) {
+ this.initTimeout = initTimeout;
+ }
+
+ public TestEventsProvider(int initTimeout, boolean initError, String initErrorMessage) {
+ this.initTimeout = initTimeout;
+ this.initError = initError;
+ this.initErrorMessage = initErrorMessage;
+ }
+
+ public TestEventsProvider(ProviderState initialState) {
+ this.state = initialState;
+ }
+
+ public void mockEvent(ProviderEvent event, ProviderEventDetails details) {
+ emit(event, details);
+ }
+
+ public boolean isShutDown() {
+ return this.shutDown;
+ }
+
+ @Override
+ public void shutdown() {
+ this.shutDown = true;
+ }
+
+ @Override
+ public void initialize(EvaluationContext evaluationContext) throws Exception {
+ if (ProviderState.NOT_READY.equals(state)) {
+ // wait half the TIMEOUT, otherwise some init/errors can be fired before we add handlers
+ Thread.sleep(initTimeout);
+ if (this.initError) {
+ this.state = ProviderState.ERROR;
+ throw new Exception(initErrorMessage);
+ }
+ this.state = ProviderState.READY;
+ }
+ }
+
+ @Override
+ public Metadata getMetadata() {
+ throw new UnsupportedOperationException("Unimplemented method 'getMetadata'");
+ }
+
+ @Override
+ public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue,
+ EvaluationContext ctx) {
+ throw new UnsupportedOperationException("Unimplemented method 'getBooleanEvaluation'");
+ }
+
+ @Override
+ public ProviderEvaluation getStringEvaluation(String key, String defaultValue,
+ EvaluationContext ctx) {
+ throw new UnsupportedOperationException("Unimplemented method 'getStringEvaluation'");
+ }
+
+ @Override
+ public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue,
+ EvaluationContext ctx) {
+ throw new UnsupportedOperationException("Unimplemented method 'getIntegerEvaluation'");
+ }
+
+ @Override
+ public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue,
+ EvaluationContext ctx) {
+ throw new UnsupportedOperationException("Unimplemented method 'getDoubleEvaluation'");
+ }
+
+ @Override
+ public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue,
+ EvaluationContext ctx) {
+ throw new UnsupportedOperationException("Unimplemented method 'getObjectEvaluation'");
+ }
+};
\ No newline at end of file
From 40d1f0a1d52ca09df2a0e6a5d39604fb8162a4f7 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 13 Jul 2023 16:01:15 +0000
Subject: [PATCH 132/134] chore(deps): update github/codeql-action digest to
c552617 (#506)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
.github/workflows/pullrequest.yml | 4 ++--
.github/workflows/static-code-scanning.yaml | 6 +++---
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml
index 5a1b2d35..f3db5d6a 100644
--- a/.github/workflows/pullrequest.yml
+++ b/.github/workflows/pullrequest.yml
@@ -26,7 +26,7 @@ jobs:
cache: maven
- name: Initialize CodeQL
- uses: github/codeql-action/init@12aa0a6e0191c2100e62e485c1ff0670e7cf0f35
+ uses: github/codeql-action/init@c5526174a564f5a5444d71884af87163f19cf394
with:
languages: java
@@ -51,4 +51,4 @@ jobs:
verbose: true # optional (default = false)
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@12aa0a6e0191c2100e62e485c1ff0670e7cf0f35
+ uses: github/codeql-action/analyze@c5526174a564f5a5444d71884af87163f19cf394
diff --git a/.github/workflows/static-code-scanning.yaml b/.github/workflows/static-code-scanning.yaml
index dbbba159..ffcf6db1 100644
--- a/.github/workflows/static-code-scanning.yaml
+++ b/.github/workflows/static-code-scanning.yaml
@@ -33,12 +33,12 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@12aa0a6e0191c2100e62e485c1ff0670e7cf0f35
+ uses: github/codeql-action/init@c5526174a564f5a5444d71884af87163f19cf394
with:
languages: java
- name: Autobuild
- uses: github/codeql-action/autobuild@12aa0a6e0191c2100e62e485c1ff0670e7cf0f35
+ uses: github/codeql-action/autobuild@c5526174a564f5a5444d71884af87163f19cf394
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@12aa0a6e0191c2100e62e485c1ff0670e7cf0f35
+ uses: github/codeql-action/analyze@c5526174a564f5a5444d71884af87163f19cf394
From c115e96ae67ce7d006d8ee495685d07895c06774 Mon Sep 17 00:00:00 2001
From: Todd Baert
Date: Thu, 13 Jul 2023 15:49:39 -0400
Subject: [PATCH 133/134] chore: update readme for events (#507)
Signed-off-by: Todd Baert
---
README.md | 105 +++++++++++++++++++++++++++++++++++++++---------------
1 file changed, 76 insertions(+), 29 deletions(-)
diff --git a/README.md b/README.md
index 473d9350..2d2d79bd 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
OpenFeature Java SDK
-[](https://github.com/open-feature/spec/tree/v0.5.2)
+[](https://github.com/open-feature/spec/tree/v0.6.0)
[](https://maven-badges.herokuapp.com/maven-central/dev.openfeature/sdk)
[](https://javadoc.io/doc/dev.openfeature/sdk)
[](https://www.repostatus.org/#active)
@@ -28,13 +28,13 @@
Standardizing feature flags unifies tools and vendors behind a common interface which avoids vendor lock-in at the code level. Additionally, it offers a framework for building extensions and integrations and allows providers to focus on their unique value proposition.
-## ๐ Requirements:
+## ๐ Requirements
- Java 8+ (compiler target is 1.8)
Note that this library is intended to be used in server-side contexts and has not been evaluated for use in mobile devices.
-## ๐ฆ Installation:
+## ๐ฆ Installation
### Maven
@@ -76,16 +76,14 @@ dependencies {
We publish SBOMs with all of our releases as of 0.3.0. You can find them in Maven Central alongside the artifacts.
-## ๐ Features:
+## ๐ Features
- support for various backend [providers](https://openfeature.dev/docs/reference/concepts/provider)
- easy integration and extension via [hooks](https://openfeature.dev/docs/reference/concepts/hooks)
- bool, string, numeric, and object flag types
- [context-aware](https://openfeature.dev/docs/reference/concepts/evaluation-context) evaluation
-## ๐ Usage:
-
-### Basics:
+## ๐ Usage
```java
public void example(){
@@ -102,7 +100,7 @@ public void example(){
}
```
-### Context-aware evaluation:
+### Context-aware evaluation
Sometimes the value of a flag must take into account some dynamic criteria about the application or user, such as the user location, IP, email address, or the location of the server.
In OpenFeature, we refer to this as [`targeting`](https://openfeature.dev/specification/glossary#targeting).
@@ -127,9 +125,73 @@ EvaluationContext reqCtx = new ImmutableContext(targetingKey, attributes);
boolean flagValue = client.getBooleanValue("some-flag", false, reqCtx);
```
+### Events
+
+Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions.
+Initialization events (`PROVIDER_READY` on success, `PROVIDER_ERROR` on failure) are dispatched for every provider.
+Some providers support additional events, such as `PROVIDER_CONFIGURATION_CHANGED`.
+Please refer to the documentation of the provider you're using to see what events are supported.
+
+```java
+// add an event handler to a client
+client.onProviderConfigurationChanged((EventDetails eventDetails) -> {
+ // do something when the provider's flag settings change
+});
+
+// add an event handler to the global API
+OpenFeatureAPI.getInstance().onProviderStale((EventDetails eventDetails) -> {
+ // do something when the provider's cache goes stale
+});
+```
+
+### Hooks
+
+A hook is a mechanism that allows for adding arbitrary behavior at well-defined points of the flag evaluation life-cycle.
+Use cases include validating the resolved flag value, modifying or adding data to the evaluation context, logging, telemetry, and tracking.
+
+```java
+public class MyHook implements Hook {
+ /**
+ *
+ * @param ctx Information about the particular flag evaluation
+ * @param details Information about how the flag was resolved, including any resolved values.
+ * @param hints An immutable mapping of data for users to communicate to the hooks.
+ */
+ @Override
+ public void after(HookContext ctx, FlagEvaluationDetails details, Map hints) {
+ System.out.println("After evaluation!");
+ }
+}
+```
+
+See [here](https://openfeature.dev/ecosystem?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Hook&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=Java) for a catalog of available hooks.
+
+### Logging:
+
+The Java SDK uses SLF4J. See the [SLF4J manual](https://slf4j.org/manual.html) for complete documentation.
+
+### Named clients
+
+Clients can be given a name.
+A name is a logical identifier which can be used to associate clients with a particular provider.
+If a name has no associated provider, clients with that name use the global provider.
+
+```java
+FeatureProvider scopedProvider = new MyProvider();
+
+// set this provider for clients named "my-name"
+OpenFeatureAPI.getInstance().setProvider("my-name", provider);
+
+// create a client bound to the provider above
+Client client = OpenFeatureAPI.getInstance().getClient("my-name");
+```
+
### Providers:
-To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency. This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/java-sdk-contrib) available under the OpenFeature organization. Finally, youโll then need to write the provider itself. This can be accomplished by implementing the `FeatureProvider` interface exported by the OpenFeature SDK.
+To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency.
+This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/java-sdk-contrib) available under the OpenFeature organization.
+Finally, youโll then need to write the provider itself.
+This can be accomplished by implementing the `FeatureProvider` interface exported by the OpenFeature SDK.
```java
public class MyProvider implements FeatureProvider {
@@ -167,31 +229,16 @@ public class MyProvider implements FeatureProvider {
See [here](https://openfeature.dev/ecosystem?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Provider&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=Java) for a catalog of available providers.
-### Hooks:
+### Shutdown
-A hook is a mechanism that allows for adding arbitrary behavior at well-defined points of the flag evaluation life-cycle. Use cases include validating the resolved flag value, modifying or adding data to the evaluation context, logging, telemetry, and tracking.
+The OpenFeature API provides a close function to perform a cleanup of all registered providers.
+This should only be called when your application is in the process of shutting down.
```java
-public class MyHook implements Hook {
- /**
- *
- * @param ctx Information about the particular flag evaluation
- * @param details Information about how the flag was resolved, including any resolved values.
- * @param hints An immutable mapping of data for users to communicate to the hooks.
- */
- @Override
- public void after(HookContext ctx, FlagEvaluationDetails details, Map hints) {
- System.out.println("After evaluation!");
- }
-}
+// shut down all providers
+OpenFeatureAPI.getInstance().shutdown();
```
-See [here](https://openfeature.dev/ecosystem?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Hook&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=Java) for a catalog of available hooks.
-
-### Logging:
-
-The Java SDK uses SLF4J. See the [SLF4J manual](https://slf4j.org/manual.html) for complete documentation.
-
### Complete API documentation:
See [here](https://www.javadoc.io/doc/dev.openfeature/sdk/latest/index.html) for the complete API documentation.
From 9a3a07f774805b6c72a14535086e571d3eb1a87e Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Thu, 13 Jul 2023 15:52:03 -0400
Subject: [PATCH 134/134] chore(main): release 1.4.0 (#364)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 145 ++++++++++++++++++++++++++++++++++
README.md | 4 +-
pom.xml | 2 +-
version.txt | 2 +-
5 files changed, 150 insertions(+), 5 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 00d9fffc..802de1e8 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1 +1 @@
-{".":"1.3.1"}
\ No newline at end of file
+{".":"1.4.0"}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e59bc30f..3b72181f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,150 @@
# Changelog
+## [1.4.0](https://github.com/open-feature/java-sdk/compare/v1.3.1...v1.4.0) (2023-07-13)
+
+
+### ๐ Bug Fixes
+
+* **deps:** update dependency io.cucumber:cucumber-bom to v7.12.0 ([#413](https://github.com/open-feature/java-sdk/issues/413)) ([f0f5d28](https://github.com/open-feature/java-sdk/commit/f0f5d284169081cae8fc88cffa04d17ac776a51f))
+* **deps:** update dependency io.cucumber:cucumber-bom to v7.12.1 ([#461](https://github.com/open-feature/java-sdk/issues/461)) ([c26b755](https://github.com/open-feature/java-sdk/commit/c26b75593edaf3a6f87c85bb9065ec485612c723))
+* **deps:** update dependency io.cucumber:cucumber-bom to v7.13.0 ([#499](https://github.com/open-feature/java-sdk/issues/499)) ([da00304](https://github.com/open-feature/java-sdk/commit/da0030408307b580fd912f5bf390b599f7a79024))
+* **deps:** update dependency org.projectlombok:lombok to v1.18.28 ([#448](https://github.com/open-feature/java-sdk/issues/448)) ([dfb214c](https://github.com/open-feature/java-sdk/commit/dfb214c52f89a84becd170925721179a2ff24c75))
+* **deps:** update junit5 monorepo ([#410](https://github.com/open-feature/java-sdk/issues/410)) ([854d0be](https://github.com/open-feature/java-sdk/commit/854d0be0f473c212884163680e2ee5df5eded0c6))
+
+
+### โจ New Features
+
+* add empty constructors to data classes ([#491](https://github.com/open-feature/java-sdk/issues/491)) ([693721e](https://github.com/open-feature/java-sdk/commit/693721e36c5b31adacd96afc55bc38ed53534db4))
+* add flag metadata ([#459](https://github.com/open-feature/java-sdk/issues/459)) ([3ed40a3](https://github.com/open-feature/java-sdk/commit/3ed40a388797dc6939bff5d06e7c4528773df791))
+* add initialize and shutdown behavior ([#456](https://github.com/open-feature/java-sdk/issues/456)) ([5f173ff](https://github.com/open-feature/java-sdk/commit/5f173ff8607e8430bf14a57e7782dc0e8460317a))
+* events ([#476](https://github.com/open-feature/java-sdk/issues/476)) ([bad5b0a](https://github.com/open-feature/java-sdk/commit/bad5b0a7f5167d0b57bf502ce86b32b1c538746c))
+* Support mapping a client to a given provider. ([#388](https://github.com/open-feature/java-sdk/issues/388)) ([d4c43d7](https://github.com/open-feature/java-sdk/commit/d4c43d74bc37371fc19dc1983e96e7c904d5a1e7))
+
+
+### ๐งน Chore
+
+* **deps:** update actions/cache digest to 67b839e ([#473](https://github.com/open-feature/java-sdk/issues/473)) ([6d456ca](https://github.com/open-feature/java-sdk/commit/6d456ca618ba78eadcfe00bd63383b9f7dba32b0))
+* **deps:** update actions/checkout digest to 47fbe2d ([#393](https://github.com/open-feature/java-sdk/issues/393)) ([43a75d0](https://github.com/open-feature/java-sdk/commit/43a75d080c3594669fe6c594b2818ee9fe22955e))
+* **deps:** update actions/checkout digest to 83b7061 ([#389](https://github.com/open-feature/java-sdk/issues/389)) ([f3e65db](https://github.com/open-feature/java-sdk/commit/f3e65db54e24926f529e939b5a27f605c46f3185))
+* **deps:** update actions/checkout digest to 8e5e7e5 ([#391](https://github.com/open-feature/java-sdk/issues/391)) ([9c98e83](https://github.com/open-feature/java-sdk/commit/9c98e83ed6a1a471b6c5488b8c87681fd92dd77d))
+* **deps:** update actions/checkout digest to 96f5310 ([#471](https://github.com/open-feature/java-sdk/issues/471)) ([fe42073](https://github.com/open-feature/java-sdk/commit/fe420733850018b1d579601a1d3b4149a93605d6))
+* **deps:** update actions/checkout digest to f095bcc ([#398](https://github.com/open-feature/java-sdk/issues/398)) ([3015571](https://github.com/open-feature/java-sdk/commit/30155712bc35a070febc5761b1ad03dc25183a26))
+* **deps:** update actions/setup-java digest to 191ba8c ([#375](https://github.com/open-feature/java-sdk/issues/375)) ([bdb08d7](https://github.com/open-feature/java-sdk/commit/bdb08d7af809bfb4593cf38830b843fb433a95ae))
+* **deps:** update actions/setup-java digest to 1f2faad ([#484](https://github.com/open-feature/java-sdk/issues/484)) ([c3528da](https://github.com/open-feature/java-sdk/commit/c3528da7024fa585ce265a620bca1f936ec508c1))
+* **deps:** update actions/setup-java digest to 45058d7 ([#479](https://github.com/open-feature/java-sdk/issues/479)) ([ec6d44a](https://github.com/open-feature/java-sdk/commit/ec6d44ae8969f73098fac8e98830800486b73a9a))
+* **deps:** update actions/setup-java digest to 75c6561 ([#503](https://github.com/open-feature/java-sdk/issues/503)) ([2d3b644](https://github.com/open-feature/java-sdk/commit/2d3b6448963e794242babe016597fb5aa198afaf))
+* **deps:** update actions/setup-java digest to 87c1c70 ([#469](https://github.com/open-feature/java-sdk/issues/469)) ([89cedb9](https://github.com/open-feature/java-sdk/commit/89cedb9d2ec709d7ad218e8b94852f8b947eb7f6))
+* **deps:** update actions/setup-java digest to ddb82ce ([#381](https://github.com/open-feature/java-sdk/issues/381)) ([a737c3a](https://github.com/open-feature/java-sdk/commit/a737c3a36bb5899c7e4b1efab69a3d4f13f24325))
+* **deps:** update actions/setup-java digest to e42168c ([#371](https://github.com/open-feature/java-sdk/issues/371)) ([0ce5b43](https://github.com/open-feature/java-sdk/commit/0ce5b43a81d5334460e8724f55798486bc9813d0))
+* **deps:** update amannn/action-semantic-pull-request digest to 00282d6 ([#490](https://github.com/open-feature/java-sdk/issues/490)) ([8b9e050](https://github.com/open-feature/java-sdk/commit/8b9e0500924475bccd3f069f5967b5af59d50f12))
+* **deps:** update amannn/action-semantic-pull-request digest to 3bb5af3 ([#435](https://github.com/open-feature/java-sdk/issues/435)) ([88e7d60](https://github.com/open-feature/java-sdk/commit/88e7d6054f60c15dc6f1130c4b6fe77be7098a5d))
+* **deps:** update codecov/codecov-action digest to 1dd0ce3 ([#414](https://github.com/open-feature/java-sdk/issues/414)) ([9d7d3d4](https://github.com/open-feature/java-sdk/commit/9d7d3d41f6a8e75b9a2b02e755a4c4048a3bc611))
+* **deps:** update codecov/codecov-action digest to 40a12dc ([#385](https://github.com/open-feature/java-sdk/issues/385)) ([5072553](https://github.com/open-feature/java-sdk/commit/507255316614cef8653c833d7c83322f999485ef))
+* **deps:** update codecov/codecov-action digest to 49c20db ([#431](https://github.com/open-feature/java-sdk/issues/431)) ([106df46](https://github.com/open-feature/java-sdk/commit/106df4661dd8e46da698ea4c90e8447aa958e6b7))
+* **deps:** update codecov/codecov-action digest to 5bf2504 ([#418](https://github.com/open-feature/java-sdk/issues/418)) ([19415ed](https://github.com/open-feature/java-sdk/commit/19415edb713533d606b6483fb8fdfea4b838b133))
+* **deps:** update codecov/codecov-action digest to 6757614 ([#400](https://github.com/open-feature/java-sdk/issues/400)) ([427d5a6](https://github.com/open-feature/java-sdk/commit/427d5a627251061651040e841d5e4db582f03cd4))
+* **deps:** update codecov/codecov-action digest to 894ff02 ([#402](https://github.com/open-feature/java-sdk/issues/402)) ([212590e](https://github.com/open-feature/java-sdk/commit/212590e5e26317a4e70caca4d201e21cb7ffa7b4))
+* **deps:** update codecov/codecov-action digest to 91e1847 ([#372](https://github.com/open-feature/java-sdk/issues/372)) ([dfa08b9](https://github.com/open-feature/java-sdk/commit/dfa08b90e1cf3f698f9058f95e7e41567a1934bb))
+* **deps:** update codecov/codecov-action digest to b4dfea7 ([#419](https://github.com/open-feature/java-sdk/issues/419)) ([b7dd2fc](https://github.com/open-feature/java-sdk/commit/b7dd2fc5a2ad9d00fb6500b1647ffb70dd539b45))
+* **deps:** update codecov/codecov-action digest to cf8e3e4 ([#428](https://github.com/open-feature/java-sdk/issues/428)) ([59d8a10](https://github.com/open-feature/java-sdk/commit/59d8a10ba311f7808a02db2773bad64356dda8e3))
+* **deps:** update codecov/codecov-action digest to eaaf4be ([#433](https://github.com/open-feature/java-sdk/issues/433)) ([3ff9995](https://github.com/open-feature/java-sdk/commit/3ff9995a437508ebb227c24e1c731dee447fc092))
+* **deps:** update dependency com.github.spotbugs:spotbugs-maven-plugin to v4.7.3.4 ([#380](https://github.com/open-feature/java-sdk/issues/380)) ([ec3111f](https://github.com/open-feature/java-sdk/commit/ec3111f5d7fb12343a45ff70296238cf747554ef))
+* **deps:** update dependency com.github.spotbugs:spotbugs-maven-plugin to v4.7.3.5 ([#482](https://github.com/open-feature/java-sdk/issues/482)) ([b8b927e](https://github.com/open-feature/java-sdk/commit/b8b927ef4a418c32effb9fbc644667ec48a4ce7e))
+* **deps:** update dependency com.google.guava:guava to v32 ([#455](https://github.com/open-feature/java-sdk/issues/455)) ([5888aea](https://github.com/open-feature/java-sdk/commit/5888aead97a70495b8fd9489aa1a8b23ea2f365e))
+* **deps:** update dependency com.google.guava:guava to v32.0.1-jre ([#470](https://github.com/open-feature/java-sdk/issues/470)) ([3946211](https://github.com/open-feature/java-sdk/commit/3946211c5d042f17a04d6941430462f70b27a7d2))
+* **deps:** update dependency com.google.guava:guava to v32.1.0-jre ([#492](https://github.com/open-feature/java-sdk/issues/492)) ([207a221](https://github.com/open-feature/java-sdk/commit/207a221d4674c8cda7881ee41c1515048a0a059e))
+* **deps:** update dependency com.google.guava:guava to v32.1.1-jre ([#494](https://github.com/open-feature/java-sdk/issues/494)) ([a7c7d42](https://github.com/open-feature/java-sdk/commit/a7c7d4287960d6825a57d14d9878032d2d2170d0))
+* **deps:** update dependency dev.openfeature.contrib.providers:flagd to v0.5.10 ([#429](https://github.com/open-feature/java-sdk/issues/429)) ([5388fa1](https://github.com/open-feature/java-sdk/commit/5388fa12b61d127588aca02999d26bc3c9986b1c))
+* **deps:** update dependency dev.openfeature.contrib.providers:flagd to v0.5.8 ([#360](https://github.com/open-feature/java-sdk/issues/360)) ([de9a928](https://github.com/open-feature/java-sdk/commit/de9a928f93679295ad9244b7dc6def1af1d9f7fc))
+* **deps:** update dependency dev.openfeature.contrib.providers:flagd to v0.5.9 ([#416](https://github.com/open-feature/java-sdk/issues/416)) ([434da5a](https://github.com/open-feature/java-sdk/commit/434da5a6080a8c3827a9a9cbb08ed98107c14264))
+* **deps:** update dependency org.apache.maven.plugins:maven-checkstyle-plugin to v3.2.2 ([#403](https://github.com/open-feature/java-sdk/issues/403)) ([311b73f](https://github.com/open-feature/java-sdk/commit/311b73fe353ea723f2aa8df70b7ec92e91b8d0f8))
+* **deps:** update dependency org.apache.maven.plugins:maven-checkstyle-plugin to v3.3.0 ([#444](https://github.com/open-feature/java-sdk/issues/444)) ([f9523ec](https://github.com/open-feature/java-sdk/commit/f9523ecd8b4585619ea6e12caffcb90c42eb354c))
+* **deps:** update dependency org.apache.maven.plugins:maven-dependency-plugin to v3.6.0 ([#445](https://github.com/open-feature/java-sdk/issues/445)) ([eb6f9e6](https://github.com/open-feature/java-sdk/commit/eb6f9e69ef8729d2850f8c1e63a66f30c0a8dd51))
+* **deps:** update dependency org.apache.maven.plugins:maven-failsafe-plugin to v3.1.0 ([#425](https://github.com/open-feature/java-sdk/issues/425)) ([839fddb](https://github.com/open-feature/java-sdk/commit/839fddb927575d92ed114518d9f2c16a92a0994b))
+* **deps:** update dependency org.apache.maven.plugins:maven-failsafe-plugin to v3.1.2 ([#464](https://github.com/open-feature/java-sdk/issues/464)) ([24f0923](https://github.com/open-feature/java-sdk/commit/24f092319dfade89b2a6a62b86cce2d88b81fa3a))
+* **deps:** update dependency org.apache.maven.plugins:maven-gpg-plugin to v3.1.0 ([#423](https://github.com/open-feature/java-sdk/issues/423)) ([64f79cd](https://github.com/open-feature/java-sdk/commit/64f79cd513c698656eed2b10903b60ef3891c141))
+* **deps:** update dependency org.apache.maven.plugins:maven-pmd-plugin to v3.21.0 ([#434](https://github.com/open-feature/java-sdk/issues/434)) ([4d65590](https://github.com/open-feature/java-sdk/commit/4d655900d94351de8700120acb90d4429e15a136))
+* **deps:** update dependency org.apache.maven.plugins:maven-source-plugin to v3.3.0 ([#443](https://github.com/open-feature/java-sdk/issues/443)) ([bcbaff8](https://github.com/open-feature/java-sdk/commit/bcbaff8e4f15122d7e083ce68af7c0446adaf9fa))
+* **deps:** update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.1.0 ([#426](https://github.com/open-feature/java-sdk/issues/426)) ([0ccf337](https://github.com/open-feature/java-sdk/commit/0ccf337384a3ffd286560ad29a3f4531998e8e2b))
+* **deps:** update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.1.2 ([#465](https://github.com/open-feature/java-sdk/issues/465)) ([6107e91](https://github.com/open-feature/java-sdk/commit/6107e91be4eef92e5dfa96e6b7b862d7e3a85df1))
+* **deps:** update dependency org.codehaus.mojo:build-helper-maven-plugin to v3.4.0 ([#432](https://github.com/open-feature/java-sdk/issues/432)) ([aa495b2](https://github.com/open-feature/java-sdk/commit/aa495b28470d9a75bd64c148260b352dd8e6c6c2))
+* **deps:** update dependency org.cyclonedx:cyclonedx-maven-plugin to v2.7.6 ([#370](https://github.com/open-feature/java-sdk/issues/370)) ([d7b3ca0](https://github.com/open-feature/java-sdk/commit/d7b3ca0513f80e933d25d6ada2ef3cbbbf961b38))
+* **deps:** update dependency org.cyclonedx:cyclonedx-maven-plugin to v2.7.7 ([#396](https://github.com/open-feature/java-sdk/issues/396)) ([a5eaf79](https://github.com/open-feature/java-sdk/commit/a5eaf79cf9d039ab5319d8c4101b0fd8c395166e))
+* **deps:** update dependency org.cyclonedx:cyclonedx-maven-plugin to v2.7.8 ([#408](https://github.com/open-feature/java-sdk/issues/408)) ([c426e66](https://github.com/open-feature/java-sdk/commit/c426e6646f55e6575f0e1c044e4aa2a8efc2e0c0))
+* **deps:** update dependency org.cyclonedx:cyclonedx-maven-plugin to v2.7.9 ([#438](https://github.com/open-feature/java-sdk/issues/438)) ([c3e82e9](https://github.com/open-feature/java-sdk/commit/c3e82e97ddf071916b9c9e287bc935f5d177d01d))
+* **deps:** update dependency org.jacoco:jacoco-maven-plugin to v0.8.10 ([#407](https://github.com/open-feature/java-sdk/issues/407)) ([5b10d39](https://github.com/open-feature/java-sdk/commit/5b10d399cb2fdf38fe46105d53fa2ae36eb2e0b2))
+* **deps:** update dependency org.jacoco:jacoco-maven-plugin to v0.8.9 ([#374](https://github.com/open-feature/java-sdk/issues/374)) ([ade4878](https://github.com/open-feature/java-sdk/commit/ade4878abc1efd993f2dc2977adbc0cc45b84be2))
+* **deps:** update github/codeql-action digest to 0ac1815 ([#477](https://github.com/open-feature/java-sdk/issues/477)) ([3501425](https://github.com/open-feature/java-sdk/commit/3501425f48feef82a50161ed072a68bae97053c9))
+* **deps:** update github/codeql-action digest to 11ea309 ([#447](https://github.com/open-feature/java-sdk/issues/447)) ([8d675ca](https://github.com/open-feature/java-sdk/commit/8d675ca38751e9c7bb8c7dd74591b9992cb696ec))
+* **deps:** update github/codeql-action digest to 1245696 ([#446](https://github.com/open-feature/java-sdk/issues/446)) ([e393b64](https://github.com/open-feature/java-sdk/commit/e393b64715f27cd96a1823c537c6d4c58030e0a2))
+* **deps:** update github/codeql-action digest to 12aa0a6 ([#505](https://github.com/open-feature/java-sdk/issues/505)) ([893d0da](https://github.com/open-feature/java-sdk/commit/893d0da6126ce49c73a90d20094a2e0123300ebb))
+* **deps:** update github/codeql-action digest to 130884e ([#430](https://github.com/open-feature/java-sdk/issues/430)) ([6405100](https://github.com/open-feature/java-sdk/commit/6405100b275f6465bbdcd25b5158ff6aad386f8b))
+* **deps:** update github/codeql-action digest to 1e1aca8 ([#421](https://github.com/open-feature/java-sdk/issues/421)) ([7aade9a](https://github.com/open-feature/java-sdk/commit/7aade9a875245ededdb56597a89f4745f0d58622))
+* **deps:** update github/codeql-action digest to 2d031a3 ([#451](https://github.com/open-feature/java-sdk/issues/451)) ([fa1e144](https://github.com/open-feature/java-sdk/commit/fa1e14451d04663dabc314e25e6ddf5ba1fb2ecf))
+* **deps:** update github/codeql-action digest to 318bcc7 ([#420](https://github.com/open-feature/java-sdk/issues/420)) ([42b9317](https://github.com/open-feature/java-sdk/commit/42b931776a559fcc35ddee442d60bce6e86b16dd))
+* **deps:** update github/codeql-action digest to 46a6823 ([#493](https://github.com/open-feature/java-sdk/issues/493)) ([331d511](https://github.com/open-feature/java-sdk/commit/331d5110dab6e4806a5a45301d2f16c86d764644))
+* **deps:** update github/codeql-action digest to 5f061ca ([#450](https://github.com/open-feature/java-sdk/issues/450)) ([79222e1](https://github.com/open-feature/java-sdk/commit/79222e1cf7223ceee75f587804904492ca004b74))
+* **deps:** update github/codeql-action digest to 66aeadb ([#377](https://github.com/open-feature/java-sdk/issues/377)) ([5c335d4](https://github.com/open-feature/java-sdk/commit/5c335d45393227cdeb3813630ee6ef9d4196916d))
+* **deps:** update github/codeql-action digest to 6a07b2a ([#502](https://github.com/open-feature/java-sdk/issues/502)) ([b0201c7](https://github.com/open-feature/java-sdk/commit/b0201c7d4311f4c4ababa91984cf937b48ec7d35))
+* **deps:** update github/codeql-action digest to 6bd8101 ([#454](https://github.com/open-feature/java-sdk/issues/454)) ([cc155b3](https://github.com/open-feature/java-sdk/commit/cc155b354c4978274d386eb769260f02535bc198))
+* **deps:** update github/codeql-action digest to 6cfb483 ([#439](https://github.com/open-feature/java-sdk/issues/439)) ([1af8e96](https://github.com/open-feature/java-sdk/commit/1af8e966a461d2eff40fdd3749df09e849339134))
+* **deps:** update github/codeql-action digest to 84c0579 ([#498](https://github.com/open-feature/java-sdk/issues/498)) ([10bee74](https://github.com/open-feature/java-sdk/commit/10bee74d16bba1bcaa110d58160e0f4eb9e7a960))
+* **deps:** update github/codeql-action digest to 85c77f1 ([#500](https://github.com/open-feature/java-sdk/issues/500)) ([4f6d7ff](https://github.com/open-feature/java-sdk/commit/4f6d7ff46d931c5f8bbdd454dda7c9b2c09578e8))
+* **deps:** update github/codeql-action digest to 8b0f2cf ([#462](https://github.com/open-feature/java-sdk/issues/462)) ([7f91942](https://github.com/open-feature/java-sdk/commit/7f9194231c6340a712a23b7298772fba3b4f4824))
+* **deps:** update github/codeql-action digest to 8ba77ef ([#485](https://github.com/open-feature/java-sdk/issues/485)) ([dac79f0](https://github.com/open-feature/java-sdk/commit/dac79f0bd5f856230a86b7bc3e3842db92a5f8b6))
+* **deps:** update github/codeql-action digest to 8ca5570 ([#415](https://github.com/open-feature/java-sdk/issues/415)) ([0de764d](https://github.com/open-feature/java-sdk/commit/0de764db19e793b81eeea345bcec8be6bc83b2b6))
+* **deps:** update github/codeql-action digest to 926a489 ([#460](https://github.com/open-feature/java-sdk/issues/460)) ([0b1315e](https://github.com/open-feature/java-sdk/commit/0b1315eaaf4cb36bfb6c45a31d337e3ae31c0ea5))
+* **deps:** update github/codeql-action digest to 95a5fda ([#504](https://github.com/open-feature/java-sdk/issues/504)) ([00c8120](https://github.com/open-feature/java-sdk/commit/00c812045926e627743ec5ff699acf6ea6797f8f))
+* **deps:** update github/codeql-action digest to 95cfca7 ([#427](https://github.com/open-feature/java-sdk/issues/427)) ([20628a2](https://github.com/open-feature/java-sdk/commit/20628a23054768238cdef503382ee6b3c6d34476))
+* **deps:** update github/codeql-action digest to 96f2840 ([#458](https://github.com/open-feature/java-sdk/issues/458)) ([401d7a8](https://github.com/open-feature/java-sdk/commit/401d7a8a5fe19835710eadce3fa88a2fcb0ee5c9))
+* **deps:** update github/codeql-action digest to 988e1bc ([#379](https://github.com/open-feature/java-sdk/issues/379)) ([9b77827](https://github.com/open-feature/java-sdk/commit/9b778277968851752bd569a09f6609f2cb3ffe48))
+* **deps:** update github/codeql-action digest to 98f7bbd ([#383](https://github.com/open-feature/java-sdk/issues/383)) ([037d611](https://github.com/open-feature/java-sdk/commit/037d61128e1e8a06a16d5ac899c2c92762baa4b3))
+* **deps:** update github/codeql-action digest to 9a866ed ([#395](https://github.com/open-feature/java-sdk/issues/395)) ([2ff65b8](https://github.com/open-feature/java-sdk/commit/2ff65b8344d0a3ffe6daebc3fb9b40ade21e2d7e))
+* **deps:** update github/codeql-action digest to 9d2dd7c ([#457](https://github.com/open-feature/java-sdk/issues/457)) ([e1a0432](https://github.com/open-feature/java-sdk/commit/e1a0432ae988c5311bc00008fd3e8687d3a3839f))
+* **deps:** update github/codeql-action digest to a2d725d ([#497](https://github.com/open-feature/java-sdk/issues/497)) ([2f028f6](https://github.com/open-feature/java-sdk/commit/2f028f699012fb160f156249ef9c85ecd8c2df13))
+* **deps:** update github/codeql-action digest to a42c0ca ([#496](https://github.com/open-feature/java-sdk/issues/496)) ([9ddc9f1](https://github.com/open-feature/java-sdk/commit/9ddc9f1cb2c85c2d096a493342e429120ff36e92))
+* **deps:** update github/codeql-action digest to a8affb0 ([#401](https://github.com/open-feature/java-sdk/issues/401)) ([c92cd2c](https://github.com/open-feature/java-sdk/commit/c92cd2ccfeb028a43637a545e56142305f76c833))
+* **deps:** update github/codeql-action digest to a9648ea ([#405](https://github.com/open-feature/java-sdk/issues/405)) ([a5f076b](https://github.com/open-feature/java-sdk/commit/a5f076b37c0cba94cc7ae22577abdba43ee011ea))
+* **deps:** update github/codeql-action digest to afdf30f ([#397](https://github.com/open-feature/java-sdk/issues/397)) ([b55ed6c](https://github.com/open-feature/java-sdk/commit/b55ed6cf7417f248b44d9ca86535deee7c80cfcc))
+* **deps:** update github/codeql-action digest to b8f204c ([#474](https://github.com/open-feature/java-sdk/issues/474)) ([d309d16](https://github.com/open-feature/java-sdk/commit/d309d1633018217e1c2fad8bff8f3b55706aa016))
+* **deps:** update github/codeql-action digest to bb28e7e ([#368](https://github.com/open-feature/java-sdk/issues/368)) ([5e648f6](https://github.com/open-feature/java-sdk/commit/5e648f6332f08c72a5e232bd6ae2171e6476a05e))
+* **deps:** update github/codeql-action digest to bcb460d ([#495](https://github.com/open-feature/java-sdk/issues/495)) ([a8e3410](https://github.com/open-feature/java-sdk/commit/a8e34100a02fdd102a605030b5be47796258ec23))
+* **deps:** update github/codeql-action digest to be2b53b ([#394](https://github.com/open-feature/java-sdk/issues/394)) ([28e191d](https://github.com/open-feature/java-sdk/commit/28e191d4231c6c04971724e3d88166260a96bef4))
+* **deps:** update github/codeql-action digest to c552617 ([#506](https://github.com/open-feature/java-sdk/issues/506)) ([40d1f0a](https://github.com/open-feature/java-sdk/commit/40d1f0a1d52ca09df2a0e6a5d39604fb8162a4f7))
+* **deps:** update github/codeql-action digest to c5f3f01 ([#404](https://github.com/open-feature/java-sdk/issues/404)) ([6898514](https://github.com/open-feature/java-sdk/commit/6898514fca1f4c97edd1453217b4b6d70d996803))
+* **deps:** update github/codeql-action digest to c6dff34 ([#481](https://github.com/open-feature/java-sdk/issues/481)) ([ea54bff](https://github.com/open-feature/java-sdk/commit/ea54bff9cc6a452fd6e329d0c3f2bad678e498a5))
+* **deps:** update github/codeql-action digest to ca6b925 ([#436](https://github.com/open-feature/java-sdk/issues/436)) ([468c42d](https://github.com/open-feature/java-sdk/commit/468c42d4e3902085cda852901097b5c197fd7906))
+* **deps:** update github/codeql-action digest to cdcdbb5 ([#463](https://github.com/open-feature/java-sdk/issues/463)) ([736cf24](https://github.com/open-feature/java-sdk/commit/736cf24cbf54680c7c9ce66b05ef74402743f899))
+* **deps:** update github/codeql-action digest to cff3d9e ([#486](https://github.com/open-feature/java-sdk/issues/486)) ([6cd588b](https://github.com/open-feature/java-sdk/commit/6cd588b87a091ba11ccf3db8b2f72ffffbde358b))
+* **deps:** update github/codeql-action digest to d944b34 ([#390](https://github.com/open-feature/java-sdk/issues/390)) ([519c32a](https://github.com/open-feature/java-sdk/commit/519c32a087e94376b9a245ad9c1a4fab360adfe2))
+* **deps:** update github/codeql-action digest to da583b0 ([#409](https://github.com/open-feature/java-sdk/issues/409)) ([5abe971](https://github.com/open-feature/java-sdk/commit/5abe971bdba796cfb435ee02e72179ae406a05f0))
+* **deps:** update github/codeql-action digest to dc04638 ([#392](https://github.com/open-feature/java-sdk/issues/392)) ([813c7e2](https://github.com/open-feature/java-sdk/commit/813c7e21ab933680f507dc077ceabbdbda9299e0))
+* **deps:** update github/codeql-action digest to dc81ae3 ([#367](https://github.com/open-feature/java-sdk/issues/367)) ([bac2af3](https://github.com/open-feature/java-sdk/commit/bac2af3033245db5bb5da18790f86e657a773686))
+* **deps:** update github/codeql-action digest to dcf71cf ([#411](https://github.com/open-feature/java-sdk/issues/411)) ([2df3205](https://github.com/open-feature/java-sdk/commit/2df3205c747a8f156e38f8510d3f95f49527f6a8))
+* **deps:** update github/codeql-action digest to de74ca6 ([#480](https://github.com/open-feature/java-sdk/issues/480)) ([bd3042b](https://github.com/open-feature/java-sdk/commit/bd3042ba0d15e0bd9a2f0d68693633adb555f6e2))
+* **deps:** update github/codeql-action digest to deb312c ([#422](https://github.com/open-feature/java-sdk/issues/422)) ([af3e3d6](https://github.com/open-feature/java-sdk/commit/af3e3d60dc12f37199e79a0f6dd5f7b065944a49))
+* **deps:** update github/codeql-action digest to e287d85 ([#472](https://github.com/open-feature/java-sdk/issues/472)) ([fa94c0e](https://github.com/open-feature/java-sdk/commit/fa94c0e0ddbcb0bf5e6af7d1b6f53c1b885d7270))
+* **deps:** update github/codeql-action digest to ed6c499 ([#386](https://github.com/open-feature/java-sdk/issues/386)) ([f1ecfac](https://github.com/open-feature/java-sdk/commit/f1ecfac6aaac1102bd380a25935a42c64eda441b))
+* **deps:** update github/codeql-action digest to f0a422f ([#373](https://github.com/open-feature/java-sdk/issues/373)) ([6a8c911](https://github.com/open-feature/java-sdk/commit/6a8c911287d8b3d2e35f6455af3496a532a71553))
+* **deps:** update github/codeql-action digest to f31a31c ([#412](https://github.com/open-feature/java-sdk/issues/412)) ([be9d652](https://github.com/open-feature/java-sdk/commit/be9d6523ff0cb3d42e68d8f4d36fe9661ec25eca))
+* **deps:** update github/codeql-action digest to f32426b ([#378](https://github.com/open-feature/java-sdk/issues/378)) ([ae30789](https://github.com/open-feature/java-sdk/commit/ae307892a5fbc9ac02db47e42acc1a723b714938))
+* **deps:** update github/codeql-action digest to f8b1cb6 ([#453](https://github.com/open-feature/java-sdk/issues/453)) ([1dddd68](https://github.com/open-feature/java-sdk/commit/1dddd68c4243a8823bb1b92091d3b25871e50ed8))
+* **deps:** update github/codeql-action digest to fa7cce4 ([#376](https://github.com/open-feature/java-sdk/issues/376)) ([23c4c4c](https://github.com/open-feature/java-sdk/commit/23c4c4cef9ff0d18aec44af5c0c808439124d142))
+* **deps:** update github/codeql-action digest to fff3a80 ([#365](https://github.com/open-feature/java-sdk/issues/365)) ([3ae2a54](https://github.com/open-feature/java-sdk/commit/3ae2a541a1c8a9fc568a97aa02301df1353e092b))
+* **deps:** update google-github-actions/release-please-action digest to 01f98cb ([#489](https://github.com/open-feature/java-sdk/issues/489)) ([7f01ded](https://github.com/open-feature/java-sdk/commit/7f01deda5b5fb20ca126019e8553c4ac10ce460f))
+* **deps:** update google-github-actions/release-please-action digest to 51ee8ae ([#452](https://github.com/open-feature/java-sdk/issues/452)) ([58df782](https://github.com/open-feature/java-sdk/commit/58df782b767617c63628ddb9ece3ed3816d865ad))
+* **deps:** update google-github-actions/release-please-action digest to 8475937 ([#406](https://github.com/open-feature/java-sdk/issues/406)) ([cd27e38](https://github.com/open-feature/java-sdk/commit/cd27e38f676417e37f7a75cc8413b42350c088cc))
+* **deps:** update google-github-actions/release-please-action digest to c078ea3 ([#387](https://github.com/open-feature/java-sdk/issues/387)) ([702957c](https://github.com/open-feature/java-sdk/commit/702957c517345906db80c0805e02e22ee18fa70c))
+* **deps:** update google-github-actions/release-please-action digest to ee9822e ([#366](https://github.com/open-feature/java-sdk/issues/366)) ([6d7c43d](https://github.com/open-feature/java-sdk/commit/6d7c43d120d025d180a446ba7769109b94e1be3c))
+* **deps:** update google-github-actions/release-please-action digest to f7edb9e ([#384](https://github.com/open-feature/java-sdk/issues/384)) ([22828d1](https://github.com/open-feature/java-sdk/commit/22828d1d3f59371205d36b8419dd61647046043f))
+* expose get value for metadata ([#468](https://github.com/open-feature/java-sdk/issues/468)) ([93dde1d](https://github.com/open-feature/java-sdk/commit/93dde1d259e86b00db701a753b84ad2c253e21ec))
+* rename flag metadata ([#478](https://github.com/open-feature/java-sdk/issues/478)) ([ecfeddf](https://github.com/open-feature/java-sdk/commit/ecfeddf0f67c4d9cf34530f957d139344b622b51))
+* rename integration tests e2e ([#417](https://github.com/open-feature/java-sdk/issues/417)) ([a5c93ac](https://github.com/open-feature/java-sdk/commit/a5c93aca0a718a5760bc346f27fd70b59432d11a))
+* seperate release plugins to a profile ([#467](https://github.com/open-feature/java-sdk/issues/467)) ([31f2148](https://github.com/open-feature/java-sdk/commit/31f214826453a10d7bef2d1d59033febf75dbb76))
+* update copy and links on the readme ([#488](https://github.com/open-feature/java-sdk/issues/488)) ([6cd2081](https://github.com/open-feature/java-sdk/commit/6cd208198ce786ce173eea2dbcffb6338ba28c86))
+* update readme for events ([#507](https://github.com/open-feature/java-sdk/issues/507)) ([c115e96](https://github.com/open-feature/java-sdk/commit/c115e96ae67ce7d006d8ee495685d07895c06774))
+* update readme using template ([#382](https://github.com/open-feature/java-sdk/issues/382)) ([f51d020](https://github.com/open-feature/java-sdk/commit/f51d0201c62b558a89a1e3ab77e666ce98ecba0b))
+
## [1.3.1](https://github.com/open-feature/java-sdk/compare/v1.3.0...v1.3.1) (2023-03-28)
diff --git a/README.md b/README.md
index 2d2d79bd..8ef4a053 100644
--- a/README.md
+++ b/README.md
@@ -43,7 +43,7 @@ Note that this library is intended to be used in server-side contexts and has no
dev.openfeature
sdk
- 1.3.1
+ 1.4.0
```
@@ -67,7 +67,7 @@ If you would like snapshot builds, this is the relevant repository information:
```groovy
dependencies {
- implementation 'dev.openfeature:sdk:1.3.1'
+ implementation 'dev.openfeature:sdk:1.4.0'
}
```
diff --git a/pom.xml b/pom.xml
index af8807b8..3ec58097 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
dev.openfeature
sdk
- 1.3.1
+ 1.4.0
UTF-8
diff --git a/version.txt b/version.txt
index 3a3cd8cc..88c5fb89 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-1.3.1
+1.4.0