forked from open-feature/java-sdk
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathOpenFeatureClient.java
436 lines (376 loc) · 16.6 KB
/
OpenFeatureClient.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
package dev.openfeature.sdk;
import dev.openfeature.sdk.exceptions.*;
import dev.openfeature.sdk.internal.AutoCloseableLock;
import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
import dev.openfeature.sdk.internal.ObjectUtils;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.util.*;
import java.util.function.Consumer;
/**
* OpenFeature Client implementation.
* You should not instantiate this or reference this class.
* Use the dev.openfeature.sdk.Client interface instead.
*
* @see Client
* @deprecated // TODO: eventually we will make this non-public. See issue #872
*/
@Slf4j
@SuppressWarnings({"PMD.DataflowAnomalyAnalysis", "PMD.BeanMembersShouldSerialize", "PMD.UnusedLocalVariable",
"unchecked", "rawtypes"})
@Deprecated() // TODO: eventually we will make this non-public. See issue #872
public class OpenFeatureClient implements Client {
private final OpenFeatureAPI openfeatureApi;
@Getter
private final String domain;
@Getter
private final String version;
private final List<Hook> clientHooks;
private final HookSupport hookSupport;
AutoCloseableReentrantReadWriteLock hooksLock = new AutoCloseableReentrantReadWriteLock();
AutoCloseableReentrantReadWriteLock contextLock = new AutoCloseableReentrantReadWriteLock();
private EvaluationContext evaluationContext;
/**
* Deprecated public constructor. Use OpenFeature.API.getClient() instead.
*
* @param openFeatureAPI Backing global singleton
* @param domain An identifier which logically binds clients with providers (used by observability tools).
* @param version Version of the client (used by observability tools).
* @deprecated Do not use this constructor. It's for internal use only.
* Clients created using it will not run event handlers.
* Use the OpenFeatureAPI's getClient factory method instead.
*/
@Deprecated() // TODO: eventually we will make this non-public. See issue #872
public OpenFeatureClient(
OpenFeatureAPI openFeatureAPI,
String domain,
String version
) {
this.openfeatureApi = openFeatureAPI;
this.domain = domain;
this.version = version;
this.clientHooks = new ArrayList<>();
this.hookSupport = new HookSupport();
}
@Override
public ProviderState getProviderState() {
return openfeatureApi.getFeatureProviderStateManager(domain).getState();
}
/**
* {@inheritDoc}
*/
@Override
public OpenFeatureClient addHooks(Hook... hooks) {
try (AutoCloseableLock __ = this.hooksLock.writeLockAutoCloseable()) {
this.clientHooks.addAll(Arrays.asList(hooks));
}
return this;
}
/**
* {@inheritDoc}
*/
@Override
public List<Hook> getHooks() {
try (AutoCloseableLock __ = this.hooksLock.readLockAutoCloseable()) {
return this.clientHooks;
}
}
/**
* {@inheritDoc}
*/
@Override
public OpenFeatureClient setEvaluationContext(EvaluationContext evaluationContext) {
try (AutoCloseableLock __ = contextLock.writeLockAutoCloseable()) {
this.evaluationContext = evaluationContext;
}
return this;
}
/**
* {@inheritDoc}
*/
@Override
public EvaluationContext getEvaluationContext() {
try (AutoCloseableLock __ = contextLock.readLockAutoCloseable()) {
return this.evaluationContext;
}
}
private <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key, T defaultValue,
EvaluationContext ctx, FlagEvaluationOptions options) {
FlagEvaluationOptions flagOptions = ObjectUtils.defaultIfNull(options,
() -> FlagEvaluationOptions.builder().build());
Map<String, Object> hints = Collections.unmodifiableMap(flagOptions.getHookHints());
ctx = ObjectUtils.defaultIfNull(ctx, () -> new ImmutableContext());
FlagEvaluationDetails<T> details = null;
List<Hook> mergedHooks = null;
HookContext<T> afterHookContext = null;
FeatureProvider provider;
try {
FeatureProviderStateManager stateManager = openfeatureApi.getFeatureProviderStateManager(this.domain);
// provider must be accessed once to maintain a consistent reference
provider = stateManager.getProvider();
ProviderState state = stateManager.getState();
if (ProviderState.NOT_READY.equals(state)) {
throw new ProviderNotReadyError("provider not yet initialized");
}
if (ProviderState.FATAL.equals(state)) {
throw new FatalError("provider is in an irrecoverable error state");
}
mergedHooks = ObjectUtils.merge(provider.getProviderHooks(), flagOptions.getHooks(), clientHooks,
openfeatureApi.getHooks());
EvaluationContext mergedCtx = hookSupport.beforeHooks(type, HookContext.from(key, type, this.getMetadata(),
provider.getMetadata(), mergeEvaluationContext(ctx), defaultValue), mergedHooks, hints);
afterHookContext = HookContext.from(key, type, this.getMetadata(),
provider.getMetadata(), mergedCtx, defaultValue);
ProviderEvaluation<T> providerEval = (ProviderEvaluation<T>) createProviderEvaluation(type, key,
defaultValue, provider, mergedCtx);
details = FlagEvaluationDetails.from(providerEval, key);
if (details.getErrorCode() != null) {
OpenFeatureError error = ExceptionUtils.instantiateErrorByErrorCode(
details.getErrorCode(),
details.getErrorMessage());
enrichDetailsWithErrorDefaults(defaultValue, details);
hookSupport.errorHooks(type, afterHookContext, error, mergedHooks, hints);
} else {
hookSupport.afterHooks(type, afterHookContext, details, mergedHooks, hints);
}
} catch (Exception e) {
if (details == null) {
details = FlagEvaluationDetails.<T>builder().build();
}
if (e instanceof OpenFeatureError) {
details.setErrorCode(((OpenFeatureError) e).getErrorCode());
} else {
details.setErrorCode(ErrorCode.GENERAL);
}
details.setErrorMessage(e.getMessage());
enrichDetailsWithErrorDefaults(defaultValue, details);
hookSupport.errorHooks(type, afterHookContext, e, mergedHooks, hints);
} finally {
hookSupport.afterAllHooks(type, afterHookContext, mergedHooks, hints);
}
return details;
}
private static <T> void enrichDetailsWithErrorDefaults(T defaultValue, FlagEvaluationDetails<T> details) {
details.setValue(defaultValue);
details.setReason(Reason.ERROR.toString());
}
/**
* Merge invocation contexts with API, transaction and client contexts.
* Does not merge before context.
*
* @param invocationContext invocation context
* @return merged evaluation context
*/
private EvaluationContext mergeEvaluationContext(EvaluationContext invocationContext) {
final EvaluationContext apiContext = openfeatureApi.getEvaluationContext() != null
? openfeatureApi.getEvaluationContext()
: new ImmutableContext();
final EvaluationContext clientContext = this.getEvaluationContext() != null
? this.getEvaluationContext()
: new ImmutableContext();
final EvaluationContext transactionContext = openfeatureApi.getTransactionContext() != null
? openfeatureApi.getTransactionContext()
: new ImmutableContext();
return apiContext.merge(transactionContext.merge(clientContext.merge(invocationContext)));
}
private <T> ProviderEvaluation<?> createProviderEvaluation(
FlagValueType type,
String key,
T defaultValue,
FeatureProvider provider,
EvaluationContext invocationContext) {
switch (type) {
case BOOLEAN:
return provider.getBooleanEvaluation(key, (Boolean) defaultValue, invocationContext);
case STRING:
return provider.getStringEvaluation(key, (String) defaultValue, invocationContext);
case INTEGER:
return provider.getIntegerEvaluation(key, (Integer) defaultValue, invocationContext);
case DOUBLE:
return provider.getDoubleEvaluation(key, (Double) defaultValue, invocationContext);
case OBJECT:
return provider.getObjectEvaluation(key, (Value) defaultValue, invocationContext);
default:
throw new GeneralError("Unknown flag type");
}
}
@Override
public Boolean getBooleanValue(String key, Boolean defaultValue) {
return getBooleanDetails(key, defaultValue).getValue();
}
@Override
public Boolean getBooleanValue(String key, Boolean defaultValue, EvaluationContext ctx) {
return getBooleanDetails(key, defaultValue, ctx).getValue();
}
@Override
public Boolean getBooleanValue(String key, Boolean defaultValue, EvaluationContext ctx,
FlagEvaluationOptions options) {
return getBooleanDetails(key, defaultValue, ctx, options).getValue();
}
@Override
public FlagEvaluationDetails<Boolean> getBooleanDetails(String key, Boolean defaultValue) {
return getBooleanDetails(key, defaultValue, null);
}
@Override
public FlagEvaluationDetails<Boolean> getBooleanDetails(String key, Boolean defaultValue, EvaluationContext ctx) {
return getBooleanDetails(key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
}
@Override
public FlagEvaluationDetails<Boolean> getBooleanDetails(String key, Boolean defaultValue, EvaluationContext ctx,
FlagEvaluationOptions options) {
return this.evaluateFlag(FlagValueType.BOOLEAN, key, defaultValue, ctx, options);
}
@Override
public String getStringValue(String key, String defaultValue) {
return getStringDetails(key, defaultValue).getValue();
}
@Override
public String getStringValue(String key, String defaultValue, EvaluationContext ctx) {
return getStringDetails(key, defaultValue, ctx).getValue();
}
@Override
public String getStringValue(String key, String defaultValue, EvaluationContext ctx,
FlagEvaluationOptions options) {
return getStringDetails(key, defaultValue, ctx, options).getValue();
}
@Override
public FlagEvaluationDetails<String> getStringDetails(String key, String defaultValue) {
return getStringDetails(key, defaultValue, null);
}
@Override
public FlagEvaluationDetails<String> getStringDetails(String key, String defaultValue, EvaluationContext ctx) {
return getStringDetails(key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
}
@Override
public FlagEvaluationDetails<String> getStringDetails(String key, String defaultValue, EvaluationContext ctx,
FlagEvaluationOptions options) {
return this.evaluateFlag(FlagValueType.STRING, key, defaultValue, ctx, options);
}
@Override
public Integer getIntegerValue(String key, Integer defaultValue) {
return getIntegerDetails(key, defaultValue).getValue();
}
@Override
public Integer getIntegerValue(String key, Integer defaultValue, EvaluationContext ctx) {
return getIntegerDetails(key, defaultValue, ctx).getValue();
}
@Override
public Integer getIntegerValue(String key, Integer defaultValue, EvaluationContext ctx,
FlagEvaluationOptions options) {
return getIntegerDetails(key, defaultValue, ctx, options).getValue();
}
@Override
public FlagEvaluationDetails<Integer> getIntegerDetails(String key, Integer defaultValue) {
return getIntegerDetails(key, defaultValue, null);
}
@Override
public FlagEvaluationDetails<Integer> getIntegerDetails(String key, Integer defaultValue, EvaluationContext ctx) {
return getIntegerDetails(key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
}
@Override
public FlagEvaluationDetails<Integer> getIntegerDetails(String key, Integer defaultValue, EvaluationContext ctx,
FlagEvaluationOptions options) {
return this.evaluateFlag(FlagValueType.INTEGER, key, defaultValue, ctx, options);
}
@Override
public Double getDoubleValue(String key, Double defaultValue) {
return getDoubleValue(key, defaultValue, null);
}
@Override
public Double getDoubleValue(String key, Double defaultValue, EvaluationContext ctx) {
return getDoubleValue(key, defaultValue, ctx, null);
}
@Override
public Double getDoubleValue(String key, Double defaultValue, EvaluationContext ctx,
FlagEvaluationOptions options) {
return this.evaluateFlag(FlagValueType.DOUBLE, key, defaultValue, ctx, options).getValue();
}
@Override
public FlagEvaluationDetails<Double> getDoubleDetails(String key, Double defaultValue) {
return getDoubleDetails(key, defaultValue, null);
}
@Override
public FlagEvaluationDetails<Double> getDoubleDetails(String key, Double defaultValue, EvaluationContext ctx) {
return getDoubleDetails(key, defaultValue, ctx, null);
}
@Override
public FlagEvaluationDetails<Double> getDoubleDetails(String key, Double defaultValue, EvaluationContext ctx,
FlagEvaluationOptions options) {
return this.evaluateFlag(FlagValueType.DOUBLE, key, defaultValue, ctx, options);
}
@Override
public Value getObjectValue(String key, Value defaultValue) {
return getObjectDetails(key, defaultValue).getValue();
}
@Override
public Value getObjectValue(String key, Value defaultValue, EvaluationContext ctx) {
return getObjectDetails(key, defaultValue, ctx).getValue();
}
@Override
public Value getObjectValue(String key, Value defaultValue, EvaluationContext ctx,
FlagEvaluationOptions options) {
return getObjectDetails(key, defaultValue, ctx, options).getValue();
}
@Override
public FlagEvaluationDetails<Value> getObjectDetails(String key, Value defaultValue) {
return getObjectDetails(key, defaultValue, null);
}
@Override
public FlagEvaluationDetails<Value> getObjectDetails(String key, Value defaultValue,
EvaluationContext ctx) {
return getObjectDetails(key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
}
@Override
public FlagEvaluationDetails<Value> getObjectDetails(String key, Value defaultValue, EvaluationContext ctx,
FlagEvaluationOptions options) {
return this.evaluateFlag(FlagValueType.OBJECT, key, defaultValue, ctx, options);
}
@Override
public ClientMetadata getMetadata() {
return () -> domain;
}
/**
* {@inheritDoc}
*/
@Override
public Client onProviderReady(Consumer<EventDetails> handler) {
return on(ProviderEvent.PROVIDER_READY, handler);
}
/**
* {@inheritDoc}
*/
@Override
public Client onProviderConfigurationChanged(Consumer<EventDetails> handler) {
return on(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, handler);
}
/**
* {@inheritDoc}
*/
@Override
public Client onProviderError(Consumer<EventDetails> handler) {
return on(ProviderEvent.PROVIDER_ERROR, handler);
}
/**
* {@inheritDoc}
*/
@Override
public Client onProviderStale(Consumer<EventDetails> handler) {
return on(ProviderEvent.PROVIDER_STALE, handler);
}
/**
* {@inheritDoc}
*/
@Override
public Client on(ProviderEvent event, Consumer<EventDetails> handler) {
OpenFeatureAPI.getInstance().addHandler(domain, event, handler);
return this;
}
/**
* {@inheritDoc}
*/
@Override
public Client removeHandler(ProviderEvent event, Consumer<EventDetails> handler) {
OpenFeatureAPI.getInstance().removeHandler(domain, event, handler);
return this;
}
}