![JAR search and dependency download from the Maven repository](/logo.png)
com.launchdarkly.openfeature.serverprovider.EvaluationContextConverter Maven / Gradle / Ivy
package com.launchdarkly.openfeature.serverprovider;
import com.launchdarkly.logging.LDLogger;
import com.launchdarkly.sdk.ContextBuilder;
import com.launchdarkly.sdk.ContextKind;
import com.launchdarkly.sdk.ContextMultiBuilder;
import com.launchdarkly.sdk.LDContext;
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.Value;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Converts an OpenFeature EvaluationContext into a LDContext.
*/
class EvaluationContextConverter {
private final LDLogger logger;
private final ValueConverter valueConverter;
public EvaluationContextConverter(LDLogger logger) {
this.logger = logger;
this.valueConverter = new ValueConverter(logger);
}
/**
* Create an LDContext from an EvaluationContext.
*
* A context will always be created, but the created context may be invalid.
* Log messages will be written to indicate the source of the problem.
*
* @param evaluationContext The evaluation context to convert.
* @return An LDContext containing information from the evaluation context.
*/
public LDContext toLdContext(EvaluationContext evaluationContext) {
// Using the kind as a map here because getting a value from an immutable context that doesn't exist
// throws. https://github.com/open-feature/java-sdk/pull/300
Map attributes = evaluationContext.asMap();
Value kindAsValue = attributes.get("kind");
String finalKind = "user";
if (kindAsValue != null && kindAsValue.isString()) {
String kindString = kindAsValue.asString();
if (Objects.equals(kindString, "multi")) {
// A multi-context.
return BuildMultiContext(evaluationContext);
} else {
// Single context with specified kind.
finalKind = kindString;
}
} else if (kindAsValue != null) {
logger.error("The evaluation context contained an invalid kind.");
}
// No kind specified, so it is a user kind.
String targetingKey = evaluationContext.getTargetingKey();
Value keyAsValue = attributes.get("key");
targetingKey = getTargetingKey(targetingKey, keyAsValue);
return BuildSingleContext(evaluationContext.asMap(), finalKind, targetingKey);
}
private boolean isNullOrEmpty(String value) {
return value == null || value.isEmpty();
}
/**
* Get and validate a targeting key.
*
* @param targetingKey Targeting key as a string, or null.
* @param keyAsValue Key as a Value, or null.
* @return Returns a key, or null if one is not available.
*/
private String getTargetingKey(String targetingKey, Value keyAsValue) {
// Currently the targeting key will always have a value, but it can be empty.
// So we want to treat an empty string as a not defined one. Later it could
// become null, so we will need to check that.
if (!isNullOrEmpty(targetingKey) && keyAsValue != null && keyAsValue.isString()) {
// There is both a targeting key and a key. It will work, but probably
// is not intentional.
logger.warn("EvaluationContext contained both a 'key' and 'targetingKey'.");
}
if (keyAsValue != null && !keyAsValue.isString()) {
logger.warn("A non-string 'key' attribute was provided.");
}
if (keyAsValue != null && keyAsValue.isString()) {
// Targeting key takes precedence over key, because targeting key is in the spec.
targetingKey = !isNullOrEmpty(targetingKey) ? targetingKey : keyAsValue.asString();
}
if (isNullOrEmpty(targetingKey)) {
logger.error("The EvaluationContext must contain either a 'targetingKey' or a 'key' and the type " + "must be a string.");
}
return targetingKey;
}
/**
* Build a multi-context from an evaluation context.
*
* @param evaluationContext The evaluation context containing multi-context information.
* @return The built context.
*/
private LDContext BuildMultiContext(EvaluationContext evaluationContext) {
ContextMultiBuilder multiBuilder = LDContext.multiBuilder();
evaluationContext.asMap().forEach((kind, attributes) -> {
// Do not need to do anything for the kind key.
if (Objects.equals(kind, "kind")) return;
if (!attributes.isStructure()) {
// The attributes need to be a structure to be part of a multi-context.
logger.warn("Top level attributes in a multi-kind context should be Structure types.");
return;
}
Map attributesMap = attributes.asStructure().asMap();
Value keyAsValue = attributesMap.get("key");
Value targetingKeyAsValue = attributesMap.get("targetingKey");
String targetingKey = targetingKeyAsValue != null ? targetingKeyAsValue.asString() : "";
targetingKey = getTargetingKey(targetingKey, keyAsValue);
LDContext singleContext = BuildSingleContext(attributesMap, kind, targetingKey);
multiBuilder.add(singleContext);
});
return multiBuilder.build();
}
/**
* Build either a single context, or a part of a multi-context.
*
* @param attributes The attributes for the context to contain.
* @param kind The kind of the context being generated.
* @param key The key for the context.
* @return A LDContext which can be either a single context or a part of a multi-context.
*/
private LDContext BuildSingleContext(Map attributes, String kind, String key) {
ContextBuilder builder = LDContext.builder(ContextKind.of(kind), key);
attributes.forEach((attrKey, attrValue) -> {
// Key has been processed, so we can skip it.
if (Objects.equals(attrKey, "key") || Objects.equals(attrKey, "targetingKey")) return;
if (Objects.equals(attrKey, "privateAttributes")) {
List valueList = attrValue.asList();
if (valueList == null) {
logger.error("A key of 'privateAttributes' in an evaluation context must have a list value.");
return;
}
boolean allStrings = valueList.stream().allMatch(Value::isString);
if (!allStrings) {
logger.error("A key of 'privateAttributes' must be a list of only string values.");
return;
}
builder.privateAttributes(valueList.stream().map(Value::asString).toArray(String[]::new));
return;
}
if (Objects.equals(attrKey, "anonymous")) {
if (!attrValue.isBoolean()) {
logger.error("The attribute 'anonymous' must be a boolean and it was not.");
} else {
builder.anonymous(attrValue.asBoolean());
}
return;
}
if (Objects.equals(attrKey, "name")) {
if (!attrValue.isString()) {
logger.error("The attribute 'name' must be a string and it was not.");
} else {
builder.name(attrValue.asString());
}
return;
}
builder.set(attrKey, valueConverter.toLdValue(attrValue));
});
return builder.build();
}
}