diff --git a/src/main/java/dev/openfeature/sdk/HookSupport.java b/src/main/java/dev/openfeature/sdk/HookSupport.java
index 78a4f61b2..e936fdeda 100644
--- a/src/main/java/dev/openfeature/sdk/HookSupport.java
+++ b/src/main/java/dev/openfeature/sdk/HookSupport.java
@@ -1,6 +1,7 @@
package dev.openfeature.sdk;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.List;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
@@ -15,19 +16,40 @@ class HookSupport {
/**
* 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}.
+ * Sources are iterated in order: provider, options, client, API (reversed for the {@code before} stage by
+ * {@link #executeBeforeHooks}).
+ *
+ *
The four hook sources are accepted as separate collections to avoid allocation on the evaluation hot path.
*
* @param hookSupportData the data object to modify
- * @param hooks the hooks to set
+ * @param providerHooks provider-level hooks
+ * @param optionHooks per-evaluation option hooks
+ * @param clientHooks client-level hooks
+ * @param apiHooks API-level hooks
* @param type the flag value type to filter unsupported hooks
*/
- public void setHooks(HookSupportData hookSupportData, List hooks, FlagValueType type) {
+ public void setHooks(
+ HookSupportData hookSupportData,
+ Collection providerHooks,
+ Collection optionHooks,
+ Collection clientHooks,
+ Collection apiHooks,
+ FlagValueType type) {
List> hookContextPairs = new ArrayList<>();
- for (Hook hook : hooks) {
+ addFilteredHooks(hookContextPairs, providerHooks, type);
+ addFilteredHooks(hookContextPairs, optionHooks, type);
+ addFilteredHooks(hookContextPairs, clientHooks, type);
+ addFilteredHooks(hookContextPairs, apiHooks, type);
+ hookSupportData.hooks = hookContextPairs;
+ }
+
+ private static void addFilteredHooks(
+ List> dest, Collection source, FlagValueType type) {
+ for (Hook hook : source) {
if (hook.supportsFlagValueType(type)) {
- hookContextPairs.add(Pair.of(hook, null));
+ dest.add(Pair.of(hook, null));
}
}
- hookSupportData.hooks = hookContextPairs;
}
/**
diff --git a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java
index 117b15cc4..818583724 100644
--- a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java
+++ b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java
@@ -5,7 +5,6 @@
import dev.openfeature.sdk.exceptions.GeneralError;
import dev.openfeature.sdk.exceptions.OpenFeatureError;
import dev.openfeature.sdk.exceptions.ProviderNotReadyError;
-import dev.openfeature.sdk.internal.ObjectUtils;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.ArrayList;
import java.util.Arrays;
@@ -188,9 +187,13 @@ private FlagEvaluationDetails evaluateFlag(
final var state = stateManager.getState();
// Hooks are initialized as early as possible to enable the execution of error stages
- var mergedHooks = ObjectUtils.merge(
- provider.getProviderHooks(), flagOptions.getHooks(), clientHooks, openfeatureApi.getMutableHooks());
- hookSupport.setHooks(hookSupportData, mergedHooks, type);
+ hookSupport.setHooks(
+ hookSupportData,
+ provider.getProviderHooks(),
+ flagOptions.getHooks(),
+ clientHooks,
+ openfeatureApi.getMutableHooks(),
+ type);
var sharedHookContext =
new SharedHookContext(key, type, this.getMetadata(), provider.getMetadata(), defaultValue);
diff --git a/src/main/java/dev/openfeature/sdk/internal/ObjectUtils.java b/src/main/java/dev/openfeature/sdk/internal/ObjectUtils.java
index 86a9ddd70..298712f13 100644
--- a/src/main/java/dev/openfeature/sdk/internal/ObjectUtils.java
+++ b/src/main/java/dev/openfeature/sdk/internal/ObjectUtils.java
@@ -63,7 +63,9 @@ public static T defaultIfNull(T source, Supplier defaultValue) {
* @param sources bunch of lists.
* @param list type
* @return resulting object
+ * @deprecated Not used in the project anymore. This method will be removed in a future release.
*/
+ @Deprecated(forRemoval = true)
@SafeVarargs
public static List merge(Collection... sources) {
List merged = new ArrayList<>();
diff --git a/src/test/java/dev/openfeature/sdk/HookSupportTest.java b/src/test/java/dev/openfeature/sdk/HookSupportTest.java
index 3b21aff84..f7583321a 100644
--- a/src/test/java/dev/openfeature/sdk/HookSupportTest.java
+++ b/src/test/java/dev/openfeature/sdk/HookSupportTest.java
@@ -9,6 +9,7 @@
import dev.openfeature.sdk.fixtures.HookFixtures;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -38,7 +39,13 @@ void shouldMergeEvaluationContextsOnBeforeHooksCorrectly() {
var sharedContext = getBaseHookContextForType(FlagValueType.STRING);
var hookSupportData = new HookSupportData();
hookSupportData.evaluationContext = layered;
- hookSupport.setHooks(hookSupportData, Arrays.asList(hook1, hook2), FlagValueType.STRING);
+ hookSupport.setHooks(
+ hookSupportData,
+ Collections.emptyList(),
+ Collections.emptyList(),
+ Arrays.asList(hook1, hook2),
+ Collections.emptyList(),
+ FlagValueType.STRING);
hookSupport.setHookContexts(hookSupportData, sharedContext, layered);
hookSupport.executeBeforeHooks(hookSupportData);
@@ -57,7 +64,13 @@ void shouldAlwaysCallGenericHook(FlagValueType flagValueType) {
Hook> genericHook = mockGenericHook();
var hookSupportData = new HookSupportData();
- hookSupport.setHooks(hookSupportData, List.of(genericHook), flagValueType);
+ hookSupport.setHooks(
+ hookSupportData,
+ List.of(genericHook),
+ Collections.emptyList(),
+ Collections.emptyList(),
+ Collections.emptyList(),
+ flagValueType);
callAllHooks(hookSupportData);
@@ -73,7 +86,13 @@ void shouldAlwaysCallGenericHook(FlagValueType flagValueType) {
void shouldPassDataAcrossStages(FlagValueType flagValueType) {
var testHook = new TestHookWithData();
var hookSupportData = new HookSupportData();
- hookSupport.setHooks(hookSupportData, List.of(testHook), flagValueType);
+ hookSupport.setHooks(
+ hookSupportData,
+ Collections.emptyList(),
+ List.of(testHook),
+ Collections.emptyList(),
+ Collections.emptyList(),
+ flagValueType);
hookSupport.setHookContexts(
hookSupportData,
getBaseHookContextForType(flagValueType),
@@ -102,7 +121,13 @@ void shouldIsolateDataBetweenHooks(FlagValueType flagValueType) {
var testHook2 = new TestHookWithData(2);
var hookSupportData = new HookSupportData();
- hookSupport.setHooks(hookSupportData, List.of(testHook1, testHook2), flagValueType);
+ hookSupport.setHooks(
+ hookSupportData,
+ Collections.emptyList(),
+ Collections.emptyList(),
+ Collections.emptyList(),
+ List.of(testHook1, testHook2),
+ flagValueType);
hookSupport.setHookContexts(
hookSupportData,
getBaseHookContextForType(flagValueType),
@@ -114,6 +139,28 @@ void shouldIsolateDataBetweenHooks(FlagValueType flagValueType) {
assertHookData(testHook2, 2, "before", "after", "finallyAfter", "error");
}
+ @Test
+ @DisplayName("should place hooks in provider → options → client → API order")
+ void shouldOrderHooksBySource() {
+ Hook> providerHook = mockGenericHook();
+ Hook> optionHook = mockGenericHook();
+ Hook> clientHook = mockGenericHook();
+ Hook> apiHook = mockGenericHook();
+
+ var hookSupportData = new HookSupportData();
+ hookSupport.setHooks(
+ hookSupportData,
+ List.of(providerHook),
+ List.of(optionHook),
+ List.of(clientHook),
+ List.of(apiHook),
+ FlagValueType.STRING);
+
+ assertThat(hookSupportData.getHooks())
+ .extracting(Pair::getKey)
+ .containsExactly(providerHook, optionHook, clientHook, apiHook);
+ }
+
@Test
void hookThatReturnsTheGivenContext_doesNotResultInAStackOverflow() {
var hookSupportData = new HookSupportData();
@@ -132,7 +179,13 @@ public Optional before(HookContext ctx, Map hints) {
var layeredEvaluationContext =
new LayeredEvaluationContext(evaluationContextWithValue("key", "value"), null, null, null);
hookSupportData.evaluationContext = layeredEvaluationContext;
- hookSupport.setHooks(hookSupportData, List.of(recursiveHook, emptyHook), FlagValueType.STRING);
+ hookSupport.setHooks(
+ hookSupportData,
+ Collections.emptyList(),
+ Collections.emptyList(),
+ List.of(recursiveHook, emptyHook),
+ Collections.emptyList(),
+ FlagValueType.STRING);
hookSupport.setHookContexts(
hookSupportData, getBaseHookContextForType(FlagValueType.STRING), layeredEvaluationContext);