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 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 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 FlagEvaluationDetails evaluateFlag(FlagValueType type, String key, T defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) { FlagEvaluationOptions flagOptions = ObjectUtils.defaultIfNull(options, () -> FlagEvaluationOptions.builder().build()); Map hints = Collections.unmodifiableMap(flagOptions.getHookHints()); ctx = ObjectUtils.defaultIfNull(ctx, () -> new ImmutableContext()); FlagEvaluationDetails details = null; List mergedHooks = null; HookContext 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 providerEval = (ProviderEvaluation) 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.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 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 getBooleanDetails(String key, Boolean defaultValue) { return getBooleanDetails(key, defaultValue, null); } @Override public FlagEvaluationDetails getBooleanDetails(String key, Boolean defaultValue, EvaluationContext ctx) { return getBooleanDetails(key, defaultValue, ctx, FlagEvaluationOptions.builder().build()); } @Override public FlagEvaluationDetails 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 getStringDetails(String key, String defaultValue) { return getStringDetails(key, defaultValue, null); } @Override public FlagEvaluationDetails getStringDetails(String key, String defaultValue, EvaluationContext ctx) { return getStringDetails(key, defaultValue, ctx, FlagEvaluationOptions.builder().build()); } @Override public FlagEvaluationDetails 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 getIntegerDetails(String key, Integer defaultValue) { return getIntegerDetails(key, defaultValue, null); } @Override public FlagEvaluationDetails getIntegerDetails(String key, Integer defaultValue, EvaluationContext ctx) { return getIntegerDetails(key, defaultValue, ctx, FlagEvaluationOptions.builder().build()); } @Override public FlagEvaluationDetails 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 getDoubleDetails(String key, Double defaultValue) { return getDoubleDetails(key, defaultValue, null); } @Override public FlagEvaluationDetails getDoubleDetails(String key, Double defaultValue, EvaluationContext ctx) { return getDoubleDetails(key, defaultValue, ctx, null); } @Override public FlagEvaluationDetails 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 getObjectDetails(String key, Value defaultValue) { return getObjectDetails(key, defaultValue, null); } @Override public FlagEvaluationDetails getObjectDetails(String key, Value defaultValue, EvaluationContext ctx) { return getObjectDetails(key, defaultValue, ctx, FlagEvaluationOptions.builder().build()); } @Override public FlagEvaluationDetails 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 handler) { return on(ProviderEvent.PROVIDER_READY, handler); } /** * {@inheritDoc} */ @Override public Client onProviderConfigurationChanged(Consumer handler) { return on(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, handler); } /** * {@inheritDoc} */ @Override public Client onProviderError(Consumer handler) { return on(ProviderEvent.PROVIDER_ERROR, handler); } /** * {@inheritDoc} */ @Override public Client onProviderStale(Consumer handler) { return on(ProviderEvent.PROVIDER_STALE, handler); } /** * {@inheritDoc} */ @Override public Client on(ProviderEvent event, Consumer handler) { OpenFeatureAPI.getInstance().addHandler(name, event, handler); return this; } /** * {@inheritDoc} */ @Override public Client removeHandler(ProviderEvent event, Consumer handler) { OpenFeatureAPI.getInstance().removeHandler(name, event, handler); return this; } }