onEmit) {
if (this.onEmit != null && this.onEmit != onEmit) {
@@ -50,11 +49,14 @@ void detach() {
/**
* Emit the specified {@link ProviderEvent}.
- *
+ *
* @param event The event type
* @param details The details of the event
*/
public void emit(ProviderEvent event, ProviderEventDetails details) {
+ if (eventProviderListener != null) {
+ eventProviderListener.onEmit(event, details);
+ }
if (this.onEmit != null) {
this.onEmit.accept(this, event, details);
}
@@ -63,7 +65,7 @@ public void emit(ProviderEvent event, ProviderEventDetails details) {
/**
* Emit a {@link ProviderEvent#PROVIDER_READY} event.
* Shorthand for {@link #emit(ProviderEvent, ProviderEventDetails)}
- *
+ *
* @param details The details of the event
*/
public void emitProviderReady(ProviderEventDetails details) {
@@ -74,7 +76,7 @@ public void emitProviderReady(ProviderEventDetails details) {
* Emit a
* {@link ProviderEvent#PROVIDER_CONFIGURATION_CHANGED}
* event. Shorthand for {@link #emit(ProviderEvent, ProviderEventDetails)}
- *
+ *
* @param details The details of the event
*/
public void emitProviderConfigurationChanged(ProviderEventDetails details) {
@@ -84,7 +86,7 @@ public void emitProviderConfigurationChanged(ProviderEventDetails details) {
/**
* Emit a {@link ProviderEvent#PROVIDER_STALE} event.
* Shorthand for {@link #emit(ProviderEvent, ProviderEventDetails)}
- *
+ *
* @param details The details of the event
*/
public void emitProviderStale(ProviderEventDetails details) {
@@ -94,7 +96,7 @@ public void emitProviderStale(ProviderEventDetails details) {
/**
* Emit a {@link ProviderEvent#PROVIDER_ERROR} event.
* Shorthand for {@link #emit(ProviderEvent, ProviderEventDetails)}
- *
+ *
* @param details The details of the event
*/
public void emitProviderError(ProviderEventDetails details) {
diff --git a/src/main/java/dev/openfeature/sdk/EventProviderListener.java b/src/main/java/dev/openfeature/sdk/EventProviderListener.java
new file mode 100644
index 000000000..c1f839aab
--- /dev/null
+++ b/src/main/java/dev/openfeature/sdk/EventProviderListener.java
@@ -0,0 +1,6 @@
+package dev.openfeature.sdk;
+
+@FunctionalInterface
+interface EventProviderListener {
+ void onEmit(ProviderEvent event, ProviderEventDetails details);
+}
diff --git a/src/main/java/dev/openfeature/sdk/FeatureProvider.java b/src/main/java/dev/openfeature/sdk/FeatureProvider.java
index e1e06d0ab..706818e85 100644
--- a/src/main/java/dev/openfeature/sdk/FeatureProvider.java
+++ b/src/main/java/dev/openfeature/sdk/FeatureProvider.java
@@ -60,13 +60,25 @@ default void shutdown() {
* If the provider needs to be initialized, it should return {@link ProviderState#NOT_READY}.
* If the provider is in an error state, it should return {@link ProviderState#ERROR}.
* If the provider is functioning normally, it should return {@link ProviderState#READY}.
- *
+ *
* Providers which do not implement this method are assumed to be ready immediately.
- *
+ *
* @return ProviderState
+ * @deprecated The state is handled by the SDK internally. Query the state from the {@link Client} instead.
*/
+ @Deprecated
default ProviderState getState() {
return ProviderState.READY;
}
+ /**
+ * Feature provider implementations can opt in for to support Tracking by implementing this method.
+ *
+ * @param eventName The name of the tracking event
+ * @param context Evaluation context used in flag evaluation (Optional)
+ * @param details Data pertinent to a particular tracking event (Optional)
+ */
+ default void track(String eventName, EvaluationContext context, TrackingEventDetails details) {
+
+ }
}
diff --git a/src/main/java/dev/openfeature/sdk/FeatureProviderStateManager.java b/src/main/java/dev/openfeature/sdk/FeatureProviderStateManager.java
new file mode 100644
index 000000000..973d46997
--- /dev/null
+++ b/src/main/java/dev/openfeature/sdk/FeatureProviderStateManager.java
@@ -0,0 +1,71 @@
+package dev.openfeature.sdk;
+
+import dev.openfeature.sdk.exceptions.OpenFeatureError;
+import lombok.Getter;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+class FeatureProviderStateManager implements EventProviderListener {
+ private final FeatureProvider delegate;
+ private final AtomicBoolean isInitialized = new AtomicBoolean();
+ @Getter
+ private ProviderState state = ProviderState.NOT_READY;
+
+ public FeatureProviderStateManager(FeatureProvider delegate) {
+ this.delegate = delegate;
+ if (delegate instanceof EventProvider) {
+ ((EventProvider) delegate).setEventProviderListener(this);
+ }
+ }
+
+ public void initialize(EvaluationContext evaluationContext) throws Exception {
+ if (isInitialized.getAndSet(true)) {
+ return;
+ }
+ try {
+ delegate.initialize(evaluationContext);
+ state = ProviderState.READY;
+ } catch (OpenFeatureError openFeatureError) {
+ if (ErrorCode.PROVIDER_FATAL.equals(openFeatureError.getErrorCode())) {
+ state = ProviderState.FATAL;
+ } else {
+ state = ProviderState.ERROR;
+ }
+ isInitialized.set(false);
+ throw openFeatureError;
+ } catch (Exception e) {
+ state = ProviderState.ERROR;
+ isInitialized.set(false);
+ throw e;
+ }
+ }
+
+ public void shutdown() {
+ delegate.shutdown();
+ state = ProviderState.NOT_READY;
+ isInitialized.set(false);
+ }
+
+ @Override
+ public void onEmit(ProviderEvent event, ProviderEventDetails details) {
+ if (ProviderEvent.PROVIDER_ERROR.equals(event)) {
+ if (details != null && details.getErrorCode() == ErrorCode.PROVIDER_FATAL) {
+ state = ProviderState.FATAL;
+ } else {
+ state = ProviderState.ERROR;
+ }
+ } else if (ProviderEvent.PROVIDER_STALE.equals(event)) {
+ state = ProviderState.STALE;
+ } else if (ProviderEvent.PROVIDER_READY.equals(event)) {
+ state = ProviderState.READY;
+ }
+ }
+
+ FeatureProvider getProvider() {
+ return delegate;
+ }
+
+ public boolean hasSameProvider(FeatureProvider featureProvider) {
+ return this.delegate.equals(featureProvider);
+ }
+}
diff --git a/src/main/java/dev/openfeature/sdk/HookSupport.java b/src/main/java/dev/openfeature/sdk/HookSupport.java
index 52c5b9727..f0216b255 100644
--- a/src/main/java/dev/openfeature/sdk/HookSupport.java
+++ b/src/main/java/dev/openfeature/sdk/HookSupport.java
@@ -1,13 +1,11 @@
package dev.openfeature.sdk;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
-import java.util.stream.Stream;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -19,11 +17,7 @@ class HookSupport {
public EvaluationContext beforeHooks(FlagValueType flagValueType, HookContext hookCtx, List hooks,
Map hints) {
- Stream result = callBeforeHooks(flagValueType, hookCtx, hooks, hints);
- return hookCtx.getCtx().merge(
- result.reduce(hookCtx.getCtx(), (EvaluationContext accumulated, EvaluationContext current) -> {
- return accumulated.merge(current);
- }));
+ return callBeforeHooks(flagValueType, hookCtx, hooks, hints);
}
public void afterHooks(FlagValueType flagValueType, HookContext hookContext, FlagEvaluationDetails details,
@@ -46,10 +40,11 @@ private void executeHooks(
String hookMethod,
Consumer> hookCode) {
if (hooks != null) {
- hooks
- .stream()
- .filter(hook -> hook.supportsFlagValueType(flagValueType))
- .forEach(hook -> executeChecked(hook, hookCode, hookMethod));
+ for (Hook hook : hooks) {
+ if (hook.supportsFlagValueType(flagValueType)) {
+ executeChecked(hook, hookCode, hookMethod);
+ }
+ }
}
}
@@ -68,29 +63,29 @@ private void executeHooksUnchecked(
FlagValueType flagValueType, List hooks,
Consumer> hookCode) {
if (hooks != null) {
- hooks
- .stream()
- .filter(hook -> hook.supportsFlagValueType(flagValueType))
- .forEach(hookCode::accept);
+ for (Hook hook : hooks) {
+ if (hook.supportsFlagValueType(flagValueType)) {
+ hookCode.accept(hook);
+ }
+ }
}
}
- private Stream callBeforeHooks(FlagValueType flagValueType, HookContext hookCtx,
+ private EvaluationContext callBeforeHooks(FlagValueType flagValueType, HookContext hookCtx,
List hooks, Map hints) {
// These traverse backwards from normal.
- List reversedHooks = IntStream
- .range(0, hooks.size())
- .map(i -> hooks.size() - 1 - i)
- .mapToObj(hooks::get)
- .collect(Collectors.toList());
-
- return reversedHooks
- .stream()
- .filter(hook -> hook.supportsFlagValueType(flagValueType))
- .map(hook -> hook.before(hookCtx, hints))
- .filter(Objects::nonNull)
- .filter(Optional::isPresent)
- .map(Optional::get)
- .map(EvaluationContext.class::cast);
+ List reversedHooks = new ArrayList<>(hooks);
+ Collections.reverse(reversedHooks);
+ EvaluationContext context = hookCtx.getCtx();
+ for (Hook hook : reversedHooks) {
+ if (hook.supportsFlagValueType(flagValueType)) {
+ Optional optional = Optional.ofNullable(hook.before(hookCtx, hints))
+ .orElse(Optional.empty());
+ if (optional.isPresent()) {
+ context = context.merge(optional.get());
+ }
+ }
+ }
+ return context;
}
}
diff --git a/src/main/java/dev/openfeature/sdk/ImmutableContext.java b/src/main/java/dev/openfeature/sdk/ImmutableContext.java
index fd2ff2a68..d0dae6051 100644
--- a/src/main/java/dev/openfeature/sdk/ImmutableContext.java
+++ b/src/main/java/dev/openfeature/sdk/ImmutableContext.java
@@ -3,6 +3,7 @@
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
+
import dev.openfeature.sdk.internal.ExcludeFromGeneratedCoverageReport;
import lombok.ToString;
import lombok.experimental.Delegate;
@@ -10,7 +11,8 @@
/**
* The EvaluationContext is a container for arbitrary contextual data
* that can be used as a basis for dynamic evaluation.
- * The ImmutableContext is an EvaluationContext implementation which is threadsafe, and whose attributes can
+ * The ImmutableContext is an EvaluationContext implementation which is
+ * threadsafe, and whose attributes can
* not be modified after instantiation.
*/
@ToString
@@ -21,7 +23,8 @@ public final class ImmutableContext implements EvaluationContext {
private final ImmutableStructure structure;
/**
- * Create an immutable context with an empty targeting_key and attributes provided.
+ * Create an immutable context with an empty targeting_key and attributes
+ * provided.
*/
public ImmutableContext() {
this(new HashMap<>());
@@ -42,7 +45,7 @@ public ImmutableContext(String targetingKey) {
* @param attributes evaluation context attributes
*/
public ImmutableContext(Map attributes) {
- this("", attributes);
+ this(null, attributes);
}
/**
@@ -53,9 +56,7 @@ public ImmutableContext(Map attributes) {
*/
public ImmutableContext(String targetingKey, Map attributes) {
if (targetingKey != null && !targetingKey.trim().isEmpty()) {
- final Map actualAttribs = new HashMap<>(attributes);
- actualAttribs.put(TARGETING_KEY, new Value(targetingKey));
- this.structure = new ImmutableStructure(actualAttribs);
+ this.structure = new ImmutableStructure(targetingKey, attributes);
} else {
this.structure = new ImmutableStructure(attributes);
}
@@ -71,28 +72,33 @@ public String getTargetingKey() {
}
/**
- * Merges this EvaluationContext object with the passed EvaluationContext, overriding in case of conflict.
+ * Merges this EvaluationContext object with the passed EvaluationContext,
+ * overriding in case of conflict.
*
* @param overridingContext overriding context
* @return new, resulting merged context
*/
@Override
public EvaluationContext merge(EvaluationContext overridingContext) {
- if (overridingContext == null) {
- return new ImmutableContext(this.asMap());
+ if (overridingContext == null || overridingContext.isEmpty()) {
+ return new ImmutableContext(this.asUnmodifiableMap());
+ }
+ if (this.isEmpty()) {
+ return new ImmutableContext(overridingContext.asUnmodifiableMap());
}
- return new ImmutableContext(
- this.merge(ImmutableStructure::new, this.asMap(), overridingContext.asMap()));
+ Map attributes = this.asMap();
+ EvaluationContext.mergeMaps(ImmutableStructure::new, attributes,
+ overridingContext.asUnmodifiableMap());
+ return new ImmutableContext(attributes);
}
@SuppressWarnings("all")
private static class DelegateExclusions {
@ExcludeFromGeneratedCoverageReport
- public Map merge(Function