getProviderHooks() {
* 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
@@ -45,6 +46,7 @@ default void initialize(EvaluationContext evaluationContext) throws Exception {
* 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
@@ -71,4 +73,12 @@ 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
index 973d46997..5fd70221b 100644
--- a/src/main/java/dev/openfeature/sdk/FeatureProviderStateManager.java
+++ b/src/main/java/dev/openfeature/sdk/FeatureProviderStateManager.java
@@ -1,15 +1,15 @@
package dev.openfeature.sdk;
import dev.openfeature.sdk.exceptions.OpenFeatureError;
-import lombok.Getter;
-
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import lombok.extern.slf4j.Slf4j;
+@Slf4j
class FeatureProviderStateManager implements EventProviderListener {
private final FeatureProvider delegate;
private final AtomicBoolean isInitialized = new AtomicBoolean();
- @Getter
- private ProviderState state = ProviderState.NOT_READY;
+ private final AtomicReference state = new AtomicReference<>(ProviderState.NOT_READY);
public FeatureProviderStateManager(FeatureProvider delegate) {
this.delegate = delegate;
@@ -24,17 +24,17 @@ public void initialize(EvaluationContext evaluationContext) throws Exception {
}
try {
delegate.initialize(evaluationContext);
- state = ProviderState.READY;
+ setState(ProviderState.READY);
} catch (OpenFeatureError openFeatureError) {
if (ErrorCode.PROVIDER_FATAL.equals(openFeatureError.getErrorCode())) {
- state = ProviderState.FATAL;
+ setState(ProviderState.FATAL);
} else {
- state = ProviderState.ERROR;
+ setState(ProviderState.ERROR);
}
isInitialized.set(false);
throw openFeatureError;
} catch (Exception e) {
- state = ProviderState.ERROR;
+ setState(ProviderState.ERROR);
isInitialized.set(false);
throw e;
}
@@ -42,7 +42,7 @@ public void initialize(EvaluationContext evaluationContext) throws Exception {
public void shutdown() {
delegate.shutdown();
- state = ProviderState.NOT_READY;
+ setState(ProviderState.NOT_READY);
isInitialized.set(false);
}
@@ -50,17 +50,34 @@ public void shutdown() {
public void onEmit(ProviderEvent event, ProviderEventDetails details) {
if (ProviderEvent.PROVIDER_ERROR.equals(event)) {
if (details != null && details.getErrorCode() == ErrorCode.PROVIDER_FATAL) {
- state = ProviderState.FATAL;
+ setState(ProviderState.FATAL);
} else {
- state = ProviderState.ERROR;
+ setState(ProviderState.ERROR);
}
} else if (ProviderEvent.PROVIDER_STALE.equals(event)) {
- state = ProviderState.STALE;
+ setState(ProviderState.STALE);
} else if (ProviderEvent.PROVIDER_READY.equals(event)) {
- state = ProviderState.READY;
+ setState(ProviderState.READY);
+ }
+ }
+
+ private void setState(ProviderState state) {
+ ProviderState oldState = this.state.getAndSet(state);
+ if (oldState != state) {
+ String providerName;
+ if (delegate.getMetadata() == null || delegate.getMetadata().getName() == null) {
+ providerName = "unknown";
+ } else {
+ providerName = delegate.getMetadata().getName();
+ }
+ log.info("Provider {} transitioned from state {} to state {}", providerName, oldState, state);
}
}
+ public ProviderState getState() {
+ return state.get();
+ }
+
FeatureProvider getProvider() {
return delegate;
}
diff --git a/src/main/java/dev/openfeature/sdk/Features.java b/src/main/java/dev/openfeature/sdk/Features.java
index ba25021a9..1f0b73d43 100644
--- a/src/main/java/dev/openfeature/sdk/Features.java
+++ b/src/main/java/dev/openfeature/sdk/Features.java
@@ -15,8 +15,8 @@ public interface Features {
FlagEvaluationDetails getBooleanDetails(String key, Boolean defaultValue, EvaluationContext ctx);
- FlagEvaluationDetails getBooleanDetails(String key, Boolean defaultValue, EvaluationContext ctx,
- FlagEvaluationOptions options);
+ FlagEvaluationDetails getBooleanDetails(
+ String key, Boolean defaultValue, EvaluationContext ctx, FlagEvaluationOptions options);
String getStringValue(String key, String defaultValue);
@@ -28,8 +28,8 @@ FlagEvaluationDetails getBooleanDetails(String key, Boolean defaultValu
FlagEvaluationDetails getStringDetails(String key, String defaultValue, EvaluationContext ctx);
- FlagEvaluationDetails getStringDetails(String key, String defaultValue, EvaluationContext ctx,
- FlagEvaluationOptions options);
+ FlagEvaluationDetails getStringDetails(
+ String key, String defaultValue, EvaluationContext ctx, FlagEvaluationOptions options);
Integer getIntegerValue(String key, Integer defaultValue);
@@ -41,8 +41,8 @@ FlagEvaluationDetails getStringDetails(String key, String defaultValue,
FlagEvaluationDetails getIntegerDetails(String key, Integer defaultValue, EvaluationContext ctx);
- FlagEvaluationDetails getIntegerDetails(String key, Integer defaultValue, EvaluationContext ctx,
- FlagEvaluationOptions options);
+ FlagEvaluationDetails getIntegerDetails(
+ String key, Integer defaultValue, EvaluationContext ctx, FlagEvaluationOptions options);
Double getDoubleValue(String key, Double defaultValue);
@@ -54,22 +54,19 @@ FlagEvaluationDetails getIntegerDetails(String key, Integer defaultValu
FlagEvaluationDetails getDoubleDetails(String key, Double defaultValue, EvaluationContext ctx);
- FlagEvaluationDetails getDoubleDetails(String key, Double defaultValue, EvaluationContext ctx,
- FlagEvaluationOptions options);
+ FlagEvaluationDetails getDoubleDetails(
+ String key, Double defaultValue, EvaluationContext ctx, FlagEvaluationOptions options);
Value getObjectValue(String key, Value defaultValue);
Value getObjectValue(String key, Value defaultValue, EvaluationContext ctx);
- Value getObjectValue(String key, Value defaultValue, EvaluationContext ctx,
- FlagEvaluationOptions options);
+ Value getObjectValue(String key, Value defaultValue, EvaluationContext ctx, FlagEvaluationOptions options);
FlagEvaluationDetails getObjectDetails(String key, Value defaultValue);
- FlagEvaluationDetails getObjectDetails(String key, Value defaultValue,
- EvaluationContext ctx);
+ FlagEvaluationDetails getObjectDetails(String key, Value defaultValue, EvaluationContext ctx);
- FlagEvaluationDetails getObjectDetails(String key, Value defaultValue,
- EvaluationContext ctx,
- FlagEvaluationOptions options);
+ FlagEvaluationDetails getObjectDetails(
+ String key, Value defaultValue, EvaluationContext ctx, FlagEvaluationOptions options);
}
diff --git a/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java b/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java
index 4562ea1e5..f1697e309 100644
--- a/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java
+++ b/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java
@@ -1,7 +1,6 @@
package dev.openfeature.sdk;
import java.util.Optional;
-
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@@ -25,6 +24,7 @@ public class FlagEvaluationDetails implements BaseEvaluation {
private String reason;
private ErrorCode errorCode;
private String errorMessage;
+
@Builder.Default
private ImmutableMetadata flagMetadata = ImmutableMetadata.builder().build();
@@ -44,8 +44,8 @@ public static FlagEvaluationDetails from(ProviderEvaluation providerEv
.reason(providerEval.getReason())
.errorMessage(providerEval.getErrorMessage())
.errorCode(providerEval.getErrorCode())
- .flagMetadata(
- Optional.ofNullable(providerEval.getFlagMetadata()).orElse(ImmutableMetadata.builder().build()))
+ .flagMetadata(Optional.ofNullable(providerEval.getFlagMetadata())
+ .orElse(ImmutableMetadata.builder().build()))
.build();
}
}
diff --git a/src/main/java/dev/openfeature/sdk/FlagEvaluationOptions.java b/src/main/java/dev/openfeature/sdk/FlagEvaluationOptions.java
index 5fa1a93f1..01ecb9b2e 100644
--- a/src/main/java/dev/openfeature/sdk/FlagEvaluationOptions.java
+++ b/src/main/java/dev/openfeature/sdk/FlagEvaluationOptions.java
@@ -3,7 +3,6 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-
import lombok.Builder;
import lombok.Singular;
@@ -13,6 +12,7 @@
public class FlagEvaluationOptions {
@Singular
List hooks;
+
@Builder.Default
Map hookHints = new HashMap<>();
}
diff --git a/src/main/java/dev/openfeature/sdk/FlagValueType.java b/src/main/java/dev/openfeature/sdk/FlagValueType.java
index 11d43afb3..a8938d454 100644
--- a/src/main/java/dev/openfeature/sdk/FlagValueType.java
+++ b/src/main/java/dev/openfeature/sdk/FlagValueType.java
@@ -2,5 +2,9 @@
@SuppressWarnings("checkstyle:MissingJavadocType")
public enum FlagValueType {
- STRING, INTEGER, DOUBLE, OBJECT, BOOLEAN;
+ STRING,
+ INTEGER,
+ DOUBLE,
+ OBJECT,
+ BOOLEAN;
}
diff --git a/src/main/java/dev/openfeature/sdk/Hook.java b/src/main/java/dev/openfeature/sdk/Hook.java
index 3856af266..08aa18314 100644
--- a/src/main/java/dev/openfeature/sdk/Hook.java
+++ b/src/main/java/dev/openfeature/sdk/Hook.java
@@ -16,7 +16,7 @@ public interface Hook {
* @param ctx Information about the particular flag evaluation
* @param hints An immutable mapping of data for users to communicate to the hooks.
* @return An optional {@link EvaluationContext}. If returned, it will be merged with the EvaluationContext
- * instances from other hooks, the client and API.
+ * instances from other hooks, the client and API.
*/
default Optional before(HookContext ctx, Map hints) {
return Optional.empty();
@@ -29,8 +29,7 @@ default Optional before(HookContext ctx, Map ctx, FlagEvaluationDetails details, Map hints) {
- }
+ default void after(HookContext ctx, FlagEvaluationDetails details, Map hints) {}
/**
* Run when evaluation encounters an error. This will always run. Errors thrown will be swallowed.
@@ -39,8 +38,7 @@ default void after(HookContext ctx, FlagEvaluationDetails details, Map ctx, Exception error, Map hints) {
- }
+ default void error(HookContext ctx, Exception error, Map hints) {}
/**
* Run after flag evaluation, including any error processing. This will always run. Errors will be swallowed.
@@ -48,8 +46,7 @@ default void error(HookContext ctx, Exception error, Map hint
* @param ctx Information about the particular flag evaluation
* @param hints An immutable mapping of data for users to communicate to the hooks.
*/
- default void finallyAfter(HookContext ctx, Map hints) {
- }
+ default void finallyAfter(HookContext ctx, FlagEvaluationDetails details, Map hints) {}
default boolean supportsFlagValueType(FlagValueType flagValueType) {
return true;
diff --git a/src/main/java/dev/openfeature/sdk/HookContext.java b/src/main/java/dev/openfeature/sdk/HookContext.java
index 5c9091b89..8d4d2e13a 100644
--- a/src/main/java/dev/openfeature/sdk/HookContext.java
+++ b/src/main/java/dev/openfeature/sdk/HookContext.java
@@ -1,37 +1,75 @@
package dev.openfeature.sdk;
-import lombok.Builder;
+import dev.openfeature.sdk.internal.ExcludeFromGeneratedCoverageReport;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import java.util.Objects;
+import lombok.EqualsAndHashCode;
import lombok.NonNull;
-import lombok.Value;
-import lombok.With;
+import lombok.ToString;
/**
* A data class to hold immutable context that {@link Hook} instances use.
*
* @param the type for the flag being evaluated
*/
-@Value @Builder @With
-public class HookContext {
- @NonNull String flagKey;
- @NonNull FlagValueType type;
- @NonNull T defaultValue;
- @NonNull EvaluationContext ctx;
- ClientMetadata clientMetadata;
- Metadata providerMetadata;
-
- /**
- * Builds a {@link HookContext} instances from request data.
- * @param key feature flag key
- * @param type flag value type
- * @param clientMetadata info on which client is calling
+@EqualsAndHashCode
+@ToString
+public final class HookContext {
+ private final SharedHookContext sharedContext;
+ private EvaluationContext ctx;
+ private final HookData hookData;
+
+ HookContext(@NonNull SharedHookContext sharedContext, EvaluationContext evaluationContext, HookData hookData) {
+ this.sharedContext = sharedContext;
+ ctx = evaluationContext;
+ this.hookData = hookData;
+ }
+
+ /**
+ * Obsolete constructor.
+ * This constructor is retained for binary compatibility but is no longer part of the public API.
+ *
+ * @param flagKey feature flag key
+ * @param type flag value type
+ * @param clientMetadata info on which client is calling
+ * @param providerMetadata info on the provider
+ * @param ctx Evaluation Context for the request
+ * @param defaultValue Fallback value
+ * @deprecated HookContext is initialized by the SDK and passed to hooks. Users should not create new instances.
+ */
+ @Deprecated
+ HookContext(
+ @NonNull String flagKey,
+ @NonNull FlagValueType type,
+ @NonNull T defaultValue,
+ @NonNull EvaluationContext ctx,
+ ClientMetadata clientMetadata,
+ Metadata providerMetadata,
+ HookData hookData) {
+ this(new SharedHookContext<>(flagKey, type, clientMetadata, providerMetadata, defaultValue), ctx, hookData);
+ }
+
+ /**
+ * Builds {@link HookContext} instances from request data.
+ *
+ * @param key feature flag key
+ * @param type flag value type
+ * @param clientMetadata info on which client is calling
* @param providerMetadata info on the provider
- * @param ctx Evaluation Context for the request
- * @param defaultValue Fallback value
- * @param type that the flag is evaluating against
+ * @param ctx Evaluation Context for the request
+ * @param defaultValue Fallback value
+ * @param type that the flag is evaluating against
* @return resulting context for hook
+ * @deprecated HookContext is initialized by the SDK and passed to hooks. Users should not create new instances.
*/
- public static HookContext from(String key, FlagValueType type, ClientMetadata clientMetadata,
- Metadata providerMetadata, EvaluationContext ctx, T defaultValue) {
+ @Deprecated
+ public static HookContext from(
+ String key,
+ FlagValueType type,
+ ClientMetadata clientMetadata,
+ Metadata providerMetadata,
+ EvaluationContext ctx,
+ T defaultValue) {
return HookContext.builder()
.flagKey(key)
.type(type)
@@ -39,6 +77,286 @@ public static HookContext from(String key, FlagValueType type, ClientMeta
.providerMetadata(providerMetadata)
.ctx(ctx)
.defaultValue(defaultValue)
+ .hookData(null)
.build();
}
+
+ /**
+ * Creates a new builder for {@link HookContext}.
+ *
+ * @param the type for the flag being evaluated
+ * @return a new builder
+ * @deprecated HookContext is initialized by the SDK and passed to hooks. Users should not create new instances.
+ */
+ @Deprecated
+ public static HookContextBuilder builder() {
+ return new HookContextBuilder();
+ }
+
+ public @NonNull String getFlagKey() {
+ return sharedContext.getFlagKey();
+ }
+
+ public @NonNull FlagValueType getType() {
+ return sharedContext.getType();
+ }
+
+ public @NonNull T getDefaultValue() {
+ return sharedContext.getDefaultValue();
+ }
+
+ public @NonNull EvaluationContext getCtx() {
+ return this.ctx;
+ }
+
+ public ClientMetadata getClientMetadata() {
+ return sharedContext.getClientMetadata();
+ }
+
+ public Metadata getProviderMetadata() {
+ return sharedContext.getProviderMetadata();
+ }
+
+ @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "Intentional exposure of hookData")
+ public HookData getHookData() {
+ return this.hookData;
+ }
+
+ void setCtx(@NonNull EvaluationContext ctx) {
+ this.ctx = ctx;
+ }
+
+ /**
+ * Returns a new HookContext with the provided flagKey if it is different from the current one.
+ *
+ * @param flagKey new flag key
+ * @return new HookContext with updated flagKey or the same instance if unchanged
+ * @deprecated HookContext is initialized by the SDK and passed to hooks. Users should not create new instances.
+ */
+ @ExcludeFromGeneratedCoverageReport
+ @Deprecated
+ public HookContext withFlagKey(@NonNull String flagKey) {
+ return Objects.equals(this.getFlagKey(), flagKey)
+ ? this
+ : new HookContext(
+ flagKey,
+ this.getType(),
+ this.getDefaultValue(),
+ this.getCtx(),
+ this.getClientMetadata(),
+ this.getProviderMetadata(),
+ this.hookData);
+ }
+
+ /**
+ * Returns a new HookContext with the provided type if it is different from the current one.
+ *
+ * @param type new flag value type
+ * @return new HookContext with updated type or the same instance if unchanged
+ * @deprecated HookContext is initialized by the SDK and passed to hooks. Users should not create new instances.
+ */
+ @ExcludeFromGeneratedCoverageReport
+ @Deprecated
+ public HookContext withType(@NonNull FlagValueType type) {
+ return this.getType() == type
+ ? this
+ : new HookContext(
+ this.getFlagKey(),
+ type,
+ this.getDefaultValue(),
+ this.getCtx(),
+ this.getClientMetadata(),
+ this.getProviderMetadata(),
+ this.hookData);
+ }
+
+ /**
+ * Returns a new HookContext with the provided defaultValue if it is different from the current one.
+ *
+ * @param defaultValue new default value
+ * @return new HookContext with updated defaultValue or the same instance if unchanged
+ * @deprecated HookContext is initialized by the SDK and passed to hooks. Users should not create new instances.
+ */
+ @ExcludeFromGeneratedCoverageReport
+ @Deprecated
+ public HookContext withDefaultValue(@NonNull T defaultValue) {
+ return this.getDefaultValue() == defaultValue
+ ? this
+ : new HookContext(
+ this.getFlagKey(),
+ this.getType(),
+ defaultValue,
+ this.getCtx(),
+ this.getClientMetadata(),
+ this.getProviderMetadata(),
+ this.hookData);
+ }
+
+ /**
+ * Returns a new HookContext with the provided ctx if it is different from the current one.
+ *
+ * @param ctx new evaluation context
+ * @return new HookContext with updated ctx or the same instance if unchanged
+ * @deprecated HookContext is initialized by the SDK and passed to hooks. Users should not create new instances.
+ */
+ @ExcludeFromGeneratedCoverageReport
+ @Deprecated
+ public HookContext withCtx(@NonNull EvaluationContext ctx) {
+ return this.ctx == ctx
+ ? this
+ : new HookContext(
+ this.getFlagKey(),
+ this.getType(),
+ this.getDefaultValue(),
+ ctx,
+ this.getClientMetadata(),
+ this.getProviderMetadata(),
+ this.hookData);
+ }
+
+ /**
+ * Returns a new HookContext with the provided clientMetadata if it is different from the current one.
+ *
+ * @param clientMetadata new client metadata
+ * @return new HookContext with updated clientMetadata or the same instance if unchanged
+ * @deprecated HookContext is initialized by the SDK and passed to hooks. Users should not create new instances.
+ */
+ @ExcludeFromGeneratedCoverageReport
+ @Deprecated
+ public HookContext withClientMetadata(ClientMetadata clientMetadata) {
+ return this.getClientMetadata() == clientMetadata
+ ? this
+ : new HookContext(
+ this.getFlagKey(),
+ this.getType(),
+ this.getDefaultValue(),
+ this.getCtx(),
+ clientMetadata,
+ this.getProviderMetadata(),
+ this.hookData);
+ }
+
+ /**
+ * Returns a new HookContext with the provided providerMetadata if it is different from the current one.
+ *
+ * @param providerMetadata new provider metadata
+ * @return new HookContext with updated providerMetadata or the same instance if unchanged
+ * @deprecated HookContext is initialized by the SDK and passed to hooks. Users should not create new instances.
+ */
+ @ExcludeFromGeneratedCoverageReport
+ @Deprecated
+ public HookContext withProviderMetadata(Metadata providerMetadata) {
+ return this.getProviderMetadata() == providerMetadata
+ ? this
+ : new HookContext(
+ this.getFlagKey(),
+ this.getType(),
+ this.getDefaultValue(),
+ this.getCtx(),
+ this.getClientMetadata(),
+ providerMetadata,
+ this.hookData);
+ }
+
+ /**
+ * Returns a new HookContext with the provided hookData if it is different from the current one.
+ *
+ * @param hookData new hook data
+ * @return new HookContext with updated hookData or the same instance if unchanged
+ * @deprecated HookContext is initialized by the SDK and passed to hooks. Users should not create new instances.
+ */
+ @ExcludeFromGeneratedCoverageReport
+ @Deprecated
+ public HookContext withHookData(HookData hookData) {
+ return this.hookData == hookData
+ ? this
+ : new HookContext(
+ this.getFlagKey(),
+ this.getType(),
+ this.getDefaultValue(),
+ this.getCtx(),
+ this.getClientMetadata(),
+ this.getProviderMetadata(),
+ hookData);
+ }
+
+ /**
+ * Builder for HookContext.
+ *
+ * @param The flag type.
+ * @deprecated HookContext is initialized by the SDK and passed to hooks. Users should not create new instances.
+ */
+ @Deprecated
+ @ToString
+ public static class HookContextBuilder {
+ private String flagKey;
+ private FlagValueType type;
+ private T defaultValue;
+ private EvaluationContext ctx;
+ private ClientMetadata clientMetadata;
+ private Metadata providerMetadata;
+ private HookData hookData;
+
+ HookContextBuilder() {}
+
+ @ExcludeFromGeneratedCoverageReport
+ public HookContextBuilder flagKey(@NonNull String flagKey) {
+ this.flagKey = flagKey;
+ return this;
+ }
+
+ @ExcludeFromGeneratedCoverageReport
+ public HookContextBuilder type(@NonNull FlagValueType type) {
+ this.type = type;
+ return this;
+ }
+
+ @ExcludeFromGeneratedCoverageReport
+ public HookContextBuilder defaultValue(@NonNull T defaultValue) {
+ this.defaultValue = defaultValue;
+ return this;
+ }
+
+ @ExcludeFromGeneratedCoverageReport
+ public HookContextBuilder ctx(@NonNull EvaluationContext ctx) {
+ this.ctx = ctx;
+ return this;
+ }
+
+ @ExcludeFromGeneratedCoverageReport
+ public HookContextBuilder clientMetadata(ClientMetadata clientMetadata) {
+ this.clientMetadata = clientMetadata;
+ return this;
+ }
+
+ @ExcludeFromGeneratedCoverageReport
+ public HookContextBuilder providerMetadata(Metadata providerMetadata) {
+ this.providerMetadata = providerMetadata;
+ return this;
+ }
+
+ @SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "Intentional exposure of hookData")
+ @ExcludeFromGeneratedCoverageReport
+ public HookContextBuilder hookData(HookData hookData) {
+ this.hookData = hookData;
+ return this;
+ }
+
+ /**
+ * Builds the HookContext instance.
+ *
+ * @return a new HookContext
+ */
+ @ExcludeFromGeneratedCoverageReport
+ public HookContext build() {
+ return new HookContext(
+ this.flagKey,
+ this.type,
+ this.defaultValue,
+ this.ctx,
+ this.clientMetadata,
+ this.providerMetadata,
+ this.hookData);
+ }
+ }
}
diff --git a/src/main/java/dev/openfeature/sdk/HookData.java b/src/main/java/dev/openfeature/sdk/HookData.java
new file mode 100644
index 000000000..bd2c5dba9
--- /dev/null
+++ b/src/main/java/dev/openfeature/sdk/HookData.java
@@ -0,0 +1,35 @@
+package dev.openfeature.sdk;
+
+/**
+ * Hook data provides a way for hooks to maintain state across their execution stages.
+ * Each hook instance gets its own isolated data store that persists only for the duration
+ * of a single flag evaluation.
+ */
+public interface HookData {
+ /**
+ * Sets a value for the given key.
+ *
+ * @param key the key to store the value under
+ * @param value the value to store
+ */
+ void set(String key, Object value);
+
+ /**
+ * Gets the value for the given key.
+ *
+ * @param key the key to retrieve the value for
+ * @return the value, or null if not found
+ */
+ Object get(String key);
+
+ /**
+ * Gets the value for the given key, cast to the specified type.
+ *
+ * @param the type to cast to
+ * @param key the key to retrieve the value for
+ * @param type the class to cast to
+ * @return the value cast to the specified type, or null if not found
+ * @throws ClassCastException if the value cannot be cast to the specified type
+ */
+ T get(String key, Class type);
+}
diff --git a/src/main/java/dev/openfeature/sdk/HookSupport.java b/src/main/java/dev/openfeature/sdk/HookSupport.java
index f0216b255..c7a7630da 100644
--- a/src/main/java/dev/openfeature/sdk/HookSupport.java
+++ b/src/main/java/dev/openfeature/sdk/HookSupport.java
@@ -3,89 +3,124 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
-import java.util.Map;
import java.util.Optional;
-import java.util.function.Consumer;
-
-import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+/**
+ * Helper class to run hooks. Initialize {@link HookSupportData} by calling setHooks, setHookContexts
+ * & updateEvaluationContext in this exact order.
+ */
@Slf4j
-@RequiredArgsConstructor
-@SuppressWarnings({ "unchecked", "rawtypes" })
class HookSupport {
- public EvaluationContext beforeHooks(FlagValueType flagValueType, HookContext hookCtx, List hooks,
- Map hints) {
- return callBeforeHooks(flagValueType, hookCtx, hooks, hints);
+ /**
+ * Sets the {@link Hook}-{@link HookContext}-{@link Pair} list in the given data object with {@link HookContext}
+ * set to null. Filters hooks by supported {@link FlagValueType}.
+ *
+ * @param hookSupportData the data object to modify
+ * @param hooks the hooks to set
+ * @param type the flag value type to filter unsupported hooks
+ */
+ public void setHooks(HookSupportData hookSupportData, List hooks, FlagValueType type) {
+ List> hookContextPairs = new ArrayList<>();
+ for (Hook hook : hooks) {
+ if (hook.supportsFlagValueType(type)) {
+ hookContextPairs.add(Pair.of(hook, null));
+ }
+ }
+ hookSupportData.hooks = hookContextPairs;
}
- public void afterHooks(FlagValueType flagValueType, HookContext hookContext, FlagEvaluationDetails details,
- List hooks, Map hints) {
- executeHooksUnchecked(flagValueType, hooks, hook -> hook.after(hookContext, details, hints));
+ /**
+ * Creates & sets a {@link HookContext} for every {@link Hook}-{@link HookContext}-{@link Pair}
+ * in the given data object with a new {@link HookData} instance.
+ *
+ * @param hookSupportData the data object to modify
+ * @param sharedContext the shared context from which the new {@link HookContext} is created
+ */
+ public void setHookContexts(HookSupportData hookSupportData, SharedHookContext sharedContext) {
+ for (int i = 0; i < hookSupportData.hooks.size(); i++) {
+ Pair hookContextPair = hookSupportData.hooks.get(i);
+ HookContext curHookContext = sharedContext.hookContextFor(null, new DefaultHookData());
+ hookContextPair.setValue(curHookContext);
+ }
}
- public void afterAllHooks(FlagValueType flagValueType, HookContext hookCtx, List hooks,
- Map hints) {
- executeHooks(flagValueType, hooks, "finally", hook -> hook.finallyAfter(hookCtx, hints));
+ /**
+ * Updates the evaluation context in the given data object's eval context and each hooks eval context.
+ *
+ * @param hookSupportData the data object to modify
+ * @param evaluationContext the new context to set
+ */
+ public void updateEvaluationContext(HookSupportData hookSupportData, EvaluationContext evaluationContext) {
+ hookSupportData.evaluationContext = evaluationContext;
+ if (hookSupportData.hooks != null) {
+ for (Pair hookContextPair : hookSupportData.hooks) {
+ var curHookContext = hookContextPair.getValue();
+ if (curHookContext != null) {
+ curHookContext.setCtx(evaluationContext);
+ }
+ }
+ }
}
- public void errorHooks(FlagValueType flagValueType, HookContext hookCtx, Exception e, List hooks,
- Map hints) {
- executeHooks(flagValueType, hooks, "error", hook -> hook.error(hookCtx, e, hints));
- }
+ public void executeBeforeHooks(HookSupportData data) {
+ // These traverse backwards from normal.
+ List> reversedHooks = new ArrayList<>(data.getHooks());
+ Collections.reverse(reversedHooks);
- private void executeHooks(
- FlagValueType flagValueType, List hooks,
- String hookMethod,
- Consumer> hookCode) {
- if (hooks != null) {
- for (Hook hook : hooks) {
- if (hook.supportsFlagValueType(flagValueType)) {
- executeChecked(hook, hookCode, hookMethod);
- }
+ for (Pair hookContextPair : reversedHooks) {
+ var hook = hookContextPair.getKey();
+ var hookContext = hookContextPair.getValue();
+
+ Optional returnedEvalContext = Optional.ofNullable(
+ hook.before(hookContext, data.getHints()))
+ .orElse(Optional.empty());
+ if (returnedEvalContext.isPresent()) {
+ // update shared evaluation context for all hooks
+ updateEvaluationContext(data, data.getEvaluationContext().merge(returnedEvalContext.get()));
}
}
}
- // before, error, and finally hooks shouldn't throw
- private void executeChecked(Hook hook, Consumer> hookCode, String hookMethod) {
- try {
- hookCode.accept(hook);
- } catch (Exception exception) {
- log.error("Unhandled exception when running {} hook {} (only 'after' hooks should throw)", hookMethod,
- hook.getClass(), exception);
+ public void executeErrorHooks(HookSupportData data, Exception error) {
+ for (Pair hookContextPair : data.getHooks()) {
+ var hook = hookContextPair.getKey();
+ var hookContext = hookContextPair.getValue();
+ try {
+ hook.error(hookContext, error, data.getHints());
+ } catch (Exception e) {
+ log.error(
+ "Unhandled exception when running {} hook {} (only 'after' hooks should throw)",
+ "error",
+ hook.getClass(),
+ e);
+ }
}
}
// after hooks can throw in order to do validation
- private void executeHooksUnchecked(
- FlagValueType flagValueType, List hooks,
- Consumer> hookCode) {
- if (hooks != null) {
- for (Hook hook : hooks) {
- if (hook.supportsFlagValueType(flagValueType)) {
- hookCode.accept(hook);
- }
- }
+ public void executeAfterHooks(HookSupportData data, FlagEvaluationDetails details) {
+ for (Pair hookContextPair : data.getHooks()) {
+ var hook = hookContextPair.getKey();
+ var hookContext = hookContextPair.getValue();
+ hook.after(hookContext, details, data.getHints());
}
}
- private EvaluationContext callBeforeHooks(FlagValueType flagValueType, HookContext hookCtx,
- List hooks, Map hints) {
- // These traverse backwards from normal.
- 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());
- }
+ public void executeAfterAllHooks(HookSupportData data, FlagEvaluationDetails details) {
+ for (Pair hookContextPair : data.getHooks()) {
+ var hook = hookContextPair.getKey();
+ var hookContext = hookContextPair.getValue();
+ try {
+ hook.finallyAfter(hookContext, details, data.getHints());
+ } catch (Exception e) {
+ log.error(
+ "Unhandled exception when running {} hook {} (only 'after' hooks should throw)",
+ "finally",
+ hook.getClass(),
+ e);
}
}
- return context;
}
}
diff --git a/src/main/java/dev/openfeature/sdk/HookSupportData.java b/src/main/java/dev/openfeature/sdk/HookSupportData.java
new file mode 100644
index 000000000..2d3346ba1
--- /dev/null
+++ b/src/main/java/dev/openfeature/sdk/HookSupportData.java
@@ -0,0 +1,18 @@
+package dev.openfeature.sdk;
+
+import java.util.List;
+import java.util.Map;
+import lombok.Getter;
+
+/**
+ * Encapsulates data for hook execution per flag evaluation.
+ */
+@Getter
+class HookSupportData {
+
+ List> hooks;
+ EvaluationContext evaluationContext;
+ Map hints;
+
+ HookSupportData() {}
+}
diff --git a/src/main/java/dev/openfeature/sdk/ImmutableContext.java b/src/main/java/dev/openfeature/sdk/ImmutableContext.java
index 9b27cdd59..e4916dfca 100644
--- a/src/main/java/dev/openfeature/sdk/ImmutableContext.java
+++ b/src/main/java/dev/openfeature/sdk/ImmutableContext.java
@@ -1,27 +1,33 @@
package dev.openfeature.sdk;
+import dev.openfeature.sdk.internal.ExcludeFromGeneratedCoverageReport;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
-import dev.openfeature.sdk.internal.ExcludeFromGeneratedCoverageReport;
+import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.experimental.Delegate;
/**
* 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
+@EqualsAndHashCode
@SuppressWarnings("PMD.BeanMembersShouldSerialize")
public final class ImmutableContext implements EvaluationContext {
+ public static final ImmutableContext EMPTY = new ImmutableContext();
+
@Delegate(excludes = DelegateExclusions.class)
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 +48,7 @@ public ImmutableContext(String targetingKey) {
* @param attributes evaluation context attributes
*/
public ImmutableContext(Map attributes) {
- this("", attributes);
+ this(null, attributes);
}
/**
@@ -53,9 +59,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,7 +75,8 @@ 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
@@ -79,23 +84,24 @@ public String getTargetingKey() {
@Override
public EvaluationContext merge(EvaluationContext overridingContext) {
if (overridingContext == null || overridingContext.isEmpty()) {
- return new ImmutableContext(this.asMap());
+ return new ImmutableContext(this.asUnmodifiableMap());
}
if (this.isEmpty()) {
- return new ImmutableContext(overridingContext.asMap());
+ 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