java-sdk/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java

398 lines
14 KiB
Java

package dev.openfeature.sdk;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import dev.openfeature.sdk.exceptions.GeneralError;
import dev.openfeature.sdk.exceptions.OpenFeatureError;
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;
/**
* {@inheritDoc}
*/
@Slf4j
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.BeanMembersShouldSerialize", "unchecked", "rawtypes" })
public class OpenFeatureClient implements Client {
private final OpenFeatureAPI openfeatureApi;
@Getter
private final String name;
@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 name Name of the client (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 name, String version) {
this.openfeatureApi = openFeatureAPI;
this.name = name;
this.version = version;
this.clientHooks = new ArrayList<>();
this.hookSupport = new HookSupport();
}
/**
* {@inheritDoc}
*/
@Override
public void addHooks(Hook... hooks) {
try (AutoCloseableLock __ = this.hooksLock.writeLockAutoCloseable()) {
this.clientHooks.addAll(Arrays.asList(hooks));
}
}
/**
* {@inheritDoc}
*/
@Override
public List<Hook> getHooks() {
try (AutoCloseableLock __ = this.hooksLock.readLockAutoCloseable()) {
return this.clientHooks;
}
}
/**
* {@inheritDoc}
*/
@Override
public void setEvaluationContext(EvaluationContext evaluationContext) {
try (AutoCloseableLock __ = contextLock.writeLockAutoCloseable()) {
this.evaluationContext = evaluationContext;
}
}
/**
* {@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> hookCtx = null;
FeatureProvider provider;
try {
final EvaluationContext apiContext;
final EvaluationContext clientContext;
// openfeatureApi.getProvider() must be called once to maintain a consistent reference
provider = openfeatureApi.getProvider(this.name);
mergedHooks = ObjectUtils.merge(provider.getProviderHooks(), flagOptions.getHooks(), clientHooks,
openfeatureApi.getHooks());
hookCtx = HookContext.from(key, type, this.getMetadata(),
provider.getMetadata(), ctx, defaultValue);
// merge of: API.context, client.context, invocation.context
apiContext = openfeatureApi.getEvaluationContext() != null
? openfeatureApi.getEvaluationContext()
: new ImmutableContext();
clientContext = this.getEvaluationContext() != null
? this.getEvaluationContext()
: new ImmutableContext();
EvaluationContext ctxFromHook = hookSupport.beforeHooks(type, hookCtx, mergedHooks, hints);
EvaluationContext invocationCtx = ctx.merge(ctxFromHook);
EvaluationContext mergedCtx = apiContext.merge(clientContext.merge(invocationCtx));
ProviderEvaluation<T> providerEval = (ProviderEvaluation<T>) createProviderEvaluation(type, key,
defaultValue, provider, mergedCtx);
details = FlagEvaluationDetails.from(providerEval, key);
hookSupport.afterHooks(type, hookCtx, details, mergedHooks, hints);
} catch (Exception e) {
log.error("Unable to correctly evaluate flag with key '{}'", key, 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());
details.setValue(defaultValue);
details.setReason(Reason.ERROR.toString());
hookSupport.errorHooks(type, hookCtx, e, mergedHooks, hints);
} finally {
hookSupport.afterAllHooks(type, hookCtx, mergedHooks, hints);
}
return details;
}
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 Metadata getMetadata() {
return () -> name;
}
/**
* {@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(name, event, handler);
return this;
}
/**
* {@inheritDoc}
*/
@Override
public Client removeHandler(ProviderEvent event, Consumer<EventDetails> handler) {
OpenFeatureAPI.getInstance().removeHandler(name, event, handler);
return this;
}
}