11package dev .openfeature .sdk .multiprovider ;
22
3+ import dev .openfeature .sdk .ClientMetadata ;
34import dev .openfeature .sdk .EvaluationContext ;
45import dev .openfeature .sdk .EventProvider ;
56import dev .openfeature .sdk .FeatureProvider ;
7+ import dev .openfeature .sdk .FlagEvaluationDetails ;
8+ import dev .openfeature .sdk .FlagValueType ;
9+ import dev .openfeature .sdk .Hook ;
10+ import dev .openfeature .sdk .HookContext ;
611import dev .openfeature .sdk .Metadata ;
712import dev .openfeature .sdk .ProviderEvaluation ;
813import dev .openfeature .sdk .Value ;
1520import java .util .List ;
1621import java .util .Map ;
1722import java .util .Objects ;
23+ import java .util .Optional ;
1824import java .util .concurrent .Callable ;
1925import java .util .concurrent .ExecutorService ;
2026import java .util .concurrent .Executors ;
2834 * <p>This provider delegates flag evaluations to multiple underlying providers using a configurable
2935 * {@link Strategy}. It also exposes combined metadata containing the original metadata of each
3036 * underlying provider.
37+ *
38+ * <p>Hooks registered on the child providers are executed around each child evaluation, so a child
39+ * provider's own hooks observe the evaluation it takes part in.
3140 */
3241@ Slf4j
3342public class MultiProvider extends EventProvider {
@@ -40,6 +49,9 @@ public class MultiProvider extends EventProvider {
4049
4150 private final Map <String , FeatureProvider > providers ;
4251 private final Strategy strategy ;
52+ private final ThreadLocal <HookExecutionContext > hookExecutionContextThreadLocal = new ThreadLocal <>();
53+ private final ClientMetadata hookClientMetadata = MultiProvider ::getNAME ;
54+ private final MultiProviderHookExecutor hookExecutor = new MultiProviderHookExecutor (hookClientMetadata );
4355 private MultiProviderMetadata metadata ;
4456
4557 /**
@@ -63,6 +75,49 @@ public MultiProvider(List<FeatureProvider> providers, Strategy strategy) {
6375 this .strategy = Objects .requireNonNull (strategy , "strategy must not be null" );
6476 }
6577
78+ @ SuppressWarnings ({"rawtypes" , "unchecked" })
79+ private final List <Hook > providerHooks = List .of (new Hook () {
80+ @ Override
81+ public Optional before (HookContext ctx , Map hints ) {
82+ hookExecutionContextThreadLocal .set (
83+ new HookExecutionContext (ctx .getClientMetadata (), snapshotHints (hints )));
84+ return Optional .empty ();
85+ }
86+
87+ @ Override
88+ public void finallyAfter (HookContext ctx , FlagEvaluationDetails details , Map hints ) {
89+ hookExecutionContextThreadLocal .remove ();
90+ }
91+ });
92+
93+ /**
94+ * Returns provider-level hooks for this MultiProvider.
95+ *
96+ * <p>Includes a {@code before} hook that captures the {@link ClientMetadata}
97+ * and hook hints from the SDK's hook lifecycle. This context is then available
98+ * during per-child-provider hook execution, matching the JS SDK's WeakMap-based
99+ * approach for passing hook context into the provider evaluation.
100+ *
101+ * @return the list of provider hooks
102+ */
103+ @ Override
104+ public List <Hook > getProviderHooks () {
105+ return providerHooks ;
106+ }
107+
108+ /**
109+ * Defensively copies the hook hints. {@code FlagEvaluationOptions.hookHints} is backed by a
110+ * mutable map, and the captured hints may be read from other threads when a strategy evaluates
111+ * providers in parallel. A plain copy (rather than {@code Map.copyOf}) is used so that hints
112+ * containing null values are still supported.
113+ */
114+ private static Map <String , Object > snapshotHints (Map <String , Object > hints ) {
115+ if (hints == null || hints .isEmpty ()) {
116+ return Collections .emptyMap ();
117+ }
118+ return Collections .unmodifiableMap (new HashMap <>(hints ));
119+ }
120+
66121 protected static Map <String , FeatureProvider > buildProviders (List <FeatureProvider > providers ) {
67122 Map <String , FeatureProvider > providersMap = new LinkedHashMap <>(providers .size ());
68123 for (FeatureProvider provider : providers ) {
@@ -147,29 +202,96 @@ public Metadata getMetadata() {
147202
148203 @ Override
149204 public ProviderEvaluation <Boolean > getBooleanEvaluation (String key , Boolean defaultValue , EvaluationContext ctx ) {
205+ HookExecutionContext hookExecutionContext = currentHookExecutionContext ();
150206 return strategy .evaluate (
151- providers , key , defaultValue , ctx , p -> p .getBooleanEvaluation (key , defaultValue , ctx ));
207+ providers ,
208+ key ,
209+ defaultValue ,
210+ ctx ,
211+ provider -> hookExecutor .evaluate (
212+ provider ,
213+ key ,
214+ defaultValue ,
215+ ctx ,
216+ hookExecutionContext ,
217+ FlagValueType .BOOLEAN ,
218+ (p , evaluationContext ) -> p .getBooleanEvaluation (key , defaultValue , evaluationContext )));
152219 }
153220
154221 @ Override
155222 public ProviderEvaluation <String > getStringEvaluation (String key , String defaultValue , EvaluationContext ctx ) {
156- return strategy .evaluate (providers , key , defaultValue , ctx , p -> p .getStringEvaluation (key , defaultValue , ctx ));
223+ HookExecutionContext hookExecutionContext = currentHookExecutionContext ();
224+ return strategy .evaluate (
225+ providers ,
226+ key ,
227+ defaultValue ,
228+ ctx ,
229+ provider -> hookExecutor .evaluate (
230+ provider ,
231+ key ,
232+ defaultValue ,
233+ ctx ,
234+ hookExecutionContext ,
235+ FlagValueType .STRING ,
236+ (p , evaluationContext ) -> p .getStringEvaluation (key , defaultValue , evaluationContext )));
157237 }
158238
159239 @ Override
160240 public ProviderEvaluation <Integer > getIntegerEvaluation (String key , Integer defaultValue , EvaluationContext ctx ) {
241+ HookExecutionContext hookExecutionContext = currentHookExecutionContext ();
161242 return strategy .evaluate (
162- providers , key , defaultValue , ctx , p -> p .getIntegerEvaluation (key , defaultValue , ctx ));
243+ providers ,
244+ key ,
245+ defaultValue ,
246+ ctx ,
247+ provider -> hookExecutor .evaluate (
248+ provider ,
249+ key ,
250+ defaultValue ,
251+ ctx ,
252+ hookExecutionContext ,
253+ FlagValueType .INTEGER ,
254+ (p , evaluationContext ) -> p .getIntegerEvaluation (key , defaultValue , evaluationContext )));
163255 }
164256
165257 @ Override
166258 public ProviderEvaluation <Double > getDoubleEvaluation (String key , Double defaultValue , EvaluationContext ctx ) {
167- return strategy .evaluate (providers , key , defaultValue , ctx , p -> p .getDoubleEvaluation (key , defaultValue , ctx ));
259+ HookExecutionContext hookExecutionContext = currentHookExecutionContext ();
260+ return strategy .evaluate (
261+ providers ,
262+ key ,
263+ defaultValue ,
264+ ctx ,
265+ provider -> hookExecutor .evaluate (
266+ provider ,
267+ key ,
268+ defaultValue ,
269+ ctx ,
270+ hookExecutionContext ,
271+ FlagValueType .DOUBLE ,
272+ (p , evaluationContext ) -> p .getDoubleEvaluation (key , defaultValue , evaluationContext )));
168273 }
169274
170275 @ Override
171276 public ProviderEvaluation <Value > getObjectEvaluation (String key , Value defaultValue , EvaluationContext ctx ) {
172- return strategy .evaluate (providers , key , defaultValue , ctx , p -> p .getObjectEvaluation (key , defaultValue , ctx ));
277+ HookExecutionContext hookExecutionContext = currentHookExecutionContext ();
278+ return strategy .evaluate (
279+ providers ,
280+ key ,
281+ defaultValue ,
282+ ctx ,
283+ provider -> hookExecutor .evaluate (
284+ provider ,
285+ key ,
286+ defaultValue ,
287+ ctx ,
288+ hookExecutionContext ,
289+ FlagValueType .OBJECT ,
290+ (p , evaluationContext ) -> p .getObjectEvaluation (key , defaultValue , evaluationContext )));
291+ }
292+
293+ private HookExecutionContext currentHookExecutionContext () {
294+ return hookExecutionContextThreadLocal .get ();
173295 }
174296
175297 @ Override
0 commit comments