|
1 | 1 | package dev.openfeature.javasdk;
|
2 | 2 |
|
3 |
| -import com.google.common.collect.ImmutableList; |
4 |
| -import com.google.common.collect.ImmutableMap; |
5 |
| -import com.google.common.collect.Lists; |
6 |
| -import dev.openfeature.javasdk.exceptions.*; |
7 |
| -import lombok.Getter; |
8 |
| -import org.slf4j.Logger; |
9 |
| -import org.slf4j.LoggerFactory; |
| 3 | +import java.util.*; |
10 | 4 |
|
11 |
| -import java.util.ArrayList; |
12 |
| -import java.util.Arrays; |
13 |
| -import java.util.List; |
14 |
| -import java.util.Optional; |
| 5 | +import dev.openfeature.javasdk.exceptions.GeneralError; |
| 6 | +import dev.openfeature.javasdk.internal.ObjectUtils; |
| 7 | +import lombok.Getter; |
| 8 | +import org.slf4j.*; |
15 | 9 |
|
16 |
| -@SuppressWarnings("PMD.DataflowAnomalyAnalysis") |
| 10 | +@SuppressWarnings({"PMD.DataflowAnomalyAnalysis", "unchecked", "rawtypes"}) |
17 | 11 | public class OpenFeatureClient implements Client {
|
18 |
| - private transient final OpenFeatureAPI openfeatureApi; |
19 |
| - @Getter private final String name; |
20 |
| - @Getter private final String version; |
21 |
| - @Getter private final List<Hook> clientHooks; |
22 | 12 | private static final Logger log = LoggerFactory.getLogger(OpenFeatureClient.class);
|
23 | 13 |
|
| 14 | + private final OpenFeatureAPI openfeatureApi; |
| 15 | + @Getter |
| 16 | + private final String name; |
| 17 | + @Getter |
| 18 | + private final String version; |
| 19 | + @Getter |
| 20 | + private final List<Hook> clientHooks; |
| 21 | + private final HookSupport hookSupport; |
| 22 | + |
24 | 23 | public OpenFeatureClient(OpenFeatureAPI openFeatureAPI, String name, String version) {
|
25 | 24 | this.openfeatureApi = openFeatureAPI;
|
26 | 25 | this.name = name;
|
27 | 26 | this.version = version;
|
28 | 27 | this.clientHooks = new ArrayList<>();
|
| 28 | + this.hookSupport = new HookSupport(log); |
29 | 29 | }
|
30 | 30 |
|
31 | 31 | @Override
|
32 | 32 | public void addHooks(Hook... hooks) {
|
33 |
| - this.clientHooks.addAll(Arrays.asList(hooks)); |
| 33 | + this.clientHooks.addAll(List.of(hooks)); |
34 | 34 | }
|
35 | 35 |
|
36 |
| - <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key, T defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) { |
37 |
| - FeatureProvider provider = this.openfeatureApi.getProvider(); |
38 |
| - ImmutableMap<String, Object> hints = options.getHookHints(); |
| 36 | + private <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key, T defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) { |
| 37 | + var flagOptions = ObjectUtils.defaultIfNull(options, () -> FlagEvaluationOptions.builder().build()); |
| 38 | + FeatureProvider provider = openfeatureApi.getProvider(); |
| 39 | + Map<String, Object> hints = Collections.unmodifiableMap(flagOptions.getHookHints()); |
39 | 40 | if (ctx == null) {
|
40 | 41 | ctx = new EvaluationContext();
|
41 | 42 | }
|
42 | 43 |
|
43 | 44 | // merge of: API.context, client.context, invocation.context
|
44 | 45 |
|
45 | 46 | // TODO: Context transformation?
|
46 |
| - HookContext hookCtx = HookContext.from(key, type, this.getMetadata(), OpenFeatureAPI.getInstance().getProvider().getMetadata(), ctx, defaultValue); |
47 |
| - |
48 |
| - List<Hook> mergedHooks; |
49 |
| - if (options != null && options.getHooks() != null) { |
50 |
| - mergedHooks = ImmutableList.<Hook>builder() |
51 |
| - .addAll(options.getHooks()) |
52 |
| - .addAll(clientHooks) |
53 |
| - .addAll(openfeatureApi.getApiHooks()) |
54 |
| - .build(); |
55 |
| - } else { |
56 |
| - mergedHooks = clientHooks; |
57 |
| - } |
| 47 | + HookContext<T> hookCtx = HookContext.from(key, type, this.getMetadata(), openfeatureApi.getProvider().getMetadata(), ctx, defaultValue); |
| 48 | + |
| 49 | + List<Hook> mergedHooks = ObjectUtils.merge(flagOptions.getHooks(), clientHooks, openfeatureApi.getApiHooks()); |
58 | 50 |
|
59 | 51 | FlagEvaluationDetails<T> details = null;
|
60 | 52 | try {
|
61 |
| - EvaluationContext ctxFromHook = this.beforeHooks(hookCtx, mergedHooks, hints); |
| 53 | + EvaluationContext ctxFromHook = hookSupport.beforeHooks(hookCtx, mergedHooks, hints); |
62 | 54 | EvaluationContext invocationContext = EvaluationContext.merge(ctxFromHook, ctx);
|
63 | 55 |
|
64 |
| - ProviderEvaluation<T> providerEval; |
65 |
| - if (type == FlagValueType.BOOLEAN) { |
66 |
| - // TODO: Can we guarantee that defaultValue is a boolean? If not, how to we handle that? |
67 |
| - providerEval = (ProviderEvaluation<T>) provider.getBooleanEvaluation(key, (Boolean) defaultValue, invocationContext, options); |
68 |
| - } else if(type == FlagValueType.STRING) { |
69 |
| - providerEval = (ProviderEvaluation<T>) provider.getStringEvaluation(key, (String) defaultValue, invocationContext, options); |
70 |
| - } else if (type == FlagValueType.INTEGER) { |
71 |
| - providerEval = (ProviderEvaluation<T>) provider.getIntegerEvaluation(key, (Integer) defaultValue, invocationContext, options); |
72 |
| - } else if (type == FlagValueType.OBJECT) { |
73 |
| - providerEval = (ProviderEvaluation<T>) provider.getObjectEvaluation(key, defaultValue, invocationContext, options); |
74 |
| - } else { |
75 |
| - throw new GeneralError("Unknown flag type"); |
76 |
| - } |
| 56 | + ProviderEvaluation<T> providerEval = (ProviderEvaluation<T>) createProviderEvaluation(type, key, defaultValue, options, provider, invocationContext); |
77 | 57 |
|
78 | 58 | details = FlagEvaluationDetails.from(providerEval, key);
|
79 |
| - this.afterHooks(hookCtx, details, mergedHooks, hints); |
| 59 | + hookSupport.afterHooks(type, hookCtx, details, mergedHooks, hints); |
80 | 60 | } catch (Exception e) {
|
81 | 61 | log.error("Unable to correctly evaluate flag with key {} due to exception {}", key, e.getMessage());
|
82 | 62 | if (details == null) {
|
83 |
| - details = FlagEvaluationDetails.<T>builder().value(defaultValue).reason(Reason.ERROR).build(); |
| 63 | + details = FlagEvaluationDetails.<T>builder().build(); |
84 | 64 | }
|
85 |
| - details.value = defaultValue; |
86 |
| - details.reason = Reason.ERROR; |
87 |
| - details.errorCode = e.getMessage(); |
88 |
| - this.errorHooks(hookCtx, e, mergedHooks, hints); |
| 65 | + details.setValue(defaultValue); |
| 66 | + details.setReason(Reason.ERROR); |
| 67 | + details.setErrorCode(e.getMessage()); |
| 68 | + hookSupport.errorHooks(type, hookCtx, e, mergedHooks, hints); |
89 | 69 | } finally {
|
90 |
| - this.afterAllHooks(hookCtx, mergedHooks, hints); |
| 70 | + hookSupport.afterAllHooks(type, hookCtx, mergedHooks, hints); |
91 | 71 | }
|
92 | 72 |
|
93 | 73 | return details;
|
94 | 74 | }
|
95 | 75 |
|
96 |
| - private void errorHooks(HookContext hookCtx, Exception e, List<Hook> hooks, ImmutableMap<String, Object> hints) { |
97 |
| - for (Hook hook : hooks) { |
98 |
| - try { |
99 |
| - hook.error(hookCtx, e, hints); |
100 |
| - } catch (Exception inner_exception) { |
101 |
| - log.error("Exception when running error hooks " + hook.getClass().toString(), inner_exception); |
102 |
| - } |
103 |
| - } |
104 |
| - } |
105 |
| - |
106 |
| - private void afterAllHooks(HookContext hookCtx, List<Hook> hooks, ImmutableMap<String, Object> hints) { |
107 |
| - for (Hook hook : hooks) { |
108 |
| - try { |
109 |
| - hook.finallyAfter(hookCtx, hints); |
110 |
| - } catch (Exception inner_exception) { |
111 |
| - log.error("Exception when running finally hooks " + hook.getClass().toString(), inner_exception); |
112 |
| - } |
113 |
| - } |
114 |
| - } |
115 |
| - |
116 |
| - private <T> void afterHooks(HookContext hookContext, FlagEvaluationDetails<T> details, List<Hook> hooks, ImmutableMap<String, Object> hints) { |
117 |
| - for (Hook hook : hooks) { |
118 |
| - hook.after(hookContext, details, hints); |
119 |
| - } |
120 |
| - } |
121 |
| - |
122 |
| - private EvaluationContext beforeHooks(HookContext hookCtx, List<Hook> hooks, ImmutableMap<String, Object> hints) { |
123 |
| - // These traverse backwards from normal. |
124 |
| - EvaluationContext ctx = hookCtx.getCtx(); |
125 |
| - for (Hook hook : Lists.reverse(hooks)) { |
126 |
| - Optional<EvaluationContext> newCtx = hook.before(hookCtx, hints); |
127 |
| - if (newCtx != null && newCtx.isPresent()) { |
128 |
| - ctx = EvaluationContext.merge(ctx, newCtx.get()); |
129 |
| - hookCtx = hookCtx.withCtx(ctx); |
130 |
| - } |
| 76 | + private <T> ProviderEvaluation<?> createProviderEvaluation( |
| 77 | + FlagValueType type, |
| 78 | + String key, |
| 79 | + T defaultValue, |
| 80 | + FlagEvaluationOptions options, |
| 81 | + FeatureProvider provider, |
| 82 | + EvaluationContext invocationContext |
| 83 | + ) { |
| 84 | + switch (type) { |
| 85 | + case BOOLEAN: |
| 86 | + return provider.getBooleanEvaluation(key, (Boolean) defaultValue, invocationContext, options); |
| 87 | + case STRING: |
| 88 | + return provider.getStringEvaluation(key, (String) defaultValue, invocationContext, options); |
| 89 | + case INTEGER: |
| 90 | + return provider.getIntegerEvaluation(key, (Integer) defaultValue, invocationContext, options); |
| 91 | + case OBJECT: |
| 92 | + return provider.getObjectEvaluation(key, defaultValue, invocationContext, options); |
| 93 | + default: |
| 94 | + throw new GeneralError("Unknown flag type"); |
131 | 95 | }
|
132 |
| - return ctx; |
133 | 96 | }
|
134 | 97 |
|
135 | 98 | @Override
|
@@ -179,7 +142,7 @@ public String getStringValue(String key, String defaultValue, EvaluationContext
|
179 | 142 |
|
180 | 143 | @Override
|
181 | 144 | public FlagEvaluationDetails<String> getStringDetails(String key, String defaultValue) {
|
182 |
| - return getStringDetails(key, defaultValue, null); |
| 145 | + return getStringDetails(key, defaultValue, null); |
183 | 146 | }
|
184 | 147 |
|
185 | 148 | @Override
|
@@ -254,11 +217,6 @@ public <T> FlagEvaluationDetails<T> getObjectDetails(String key, T defaultValue,
|
254 | 217 |
|
255 | 218 | @Override
|
256 | 219 | public Metadata getMetadata() {
|
257 |
| - return new Metadata() { |
258 |
| - @Override |
259 |
| - public String getName() { |
260 |
| - return name; |
261 |
| - } |
262 |
| - }; |
| 220 | + return () -> name; |
263 | 221 | }
|
264 | 222 | }
|
0 commit comments