Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
cloud.prefab.client.internal.ConfigRuleEvaluator Maven / Gradle / Ivy
Go to download
API Client for https://prefab.cloud: rate limits, feature flags and semaphores as a service
package cloud.prefab.client.internal;
import cloud.prefab.client.ConfigStore;
import cloud.prefab.client.config.ConfigElement;
import cloud.prefab.client.config.ConfigValueUtils;
import cloud.prefab.client.config.EvaluatedCriterion;
import cloud.prefab.client.config.Match;
import cloud.prefab.client.config.logging.AbstractLoggingListener;
import cloud.prefab.domain.Prefab;
import com.google.common.collect.Streams;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ConfigRuleEvaluator {
public static final String CURRENT_TIME_KEY = "prefab.current-time";
private static final Logger LOG = LoggerFactory.getLogger(ConfigRuleEvaluator.class);
private final ConfigStore configStore;
private final WeightedValueEvaluator weightedValueEvaluator;
public ConfigRuleEvaluator(
ConfigStore configStoreImpl,
WeightedValueEvaluator weightedValueEvaluator
) {
this.weightedValueEvaluator = weightedValueEvaluator;
this.configStore = configStoreImpl;
}
public Optional getMatch(String key, LookupContext lookupContext) {
final ConfigElement configElement = configStore.getElement(key);
if (configElement == null) {
// logging lookups generate a lot of misses so skip those
if (!key.startsWith(AbstractLoggingListener.LOG_LEVEL_PREFIX)) {
LOG.trace("No config value found for key {}", key);
}
return Optional.empty();
}
return getMatch(configElement, lookupContext);
}
/**
* find if we have a match for the given properties
*
* @param configElement
* @param lookupContext
* @return
*/
public Optional getMatch(
ConfigElement configElement,
LookupContext lookupContext
) {
return getMatch(configElement, lookupContext, new LinkedList<>());
}
private Optional getMatch(
String key,
LookupContext lookupContext,
Deque> rowPropertiesStack
) {
if (!configStore.containsKey(key)) {
// logging lookups generate a lot of misses so skip those
if (!key.startsWith(AbstractLoggingListener.LOG_LEVEL_PREFIX)) {
LOG.trace("No config value found for key {}", key);
}
return Optional.empty();
}
final ConfigElement configElement = configStore.getElement(key);
return getMatch(configElement, lookupContext, rowPropertiesStack);
}
private Optional getMatch(
ConfigElement configElement,
LookupContext lookupContext,
Deque> rowPropertiesStack
) {
// Prefer rows that have a projEnvId to ones that don't
// There will be 0-1 rows with projenv and 0-1 rows without (the default row)
return Streams
.mapWithIndex(
configElement.getRowsProjEnvFirst(configStore.getProjectEnvironmentId()),
(configRow, rowIndex) -> {
if (!configRow.getPropertiesMap().isEmpty()) {
rowPropertiesStack.push(configRow.getPropertiesMap());
}
// Return the value of the first matching set of criteria
int conditionalValueIndex = 0;
for (Prefab.ConditionalValue conditionalValue : configRow.getValuesList()) {
Optional optionalMatch = evaluateConditionalValue(
rowIndex,
conditionalValue,
conditionalValueIndex,
lookupContext,
rowPropertiesStack,
configElement
);
if (optionalMatch.isPresent()) {
return optionalMatch.get();
}
conditionalValueIndex++;
}
if (!configRow.getPropertiesMap().isEmpty()) {
rowPropertiesStack.pop();
}
return null;
}
)
.filter(Objects::nonNull)
.findFirst();
}
/**
* If all of the conditions match, return a true match
*
* @param conditionalValue
* @param rowProperties
* @param configElement
* @return
*/
private Optional evaluateConditionalValue(
long rowIndex,
Prefab.ConditionalValue conditionalValue,
int conditionalValueIndex,
LookupContext lookupContext,
Deque> rowProperties,
ConfigElement configElement
) {
List evaluatedCriteria = new ArrayList<>();
for (Prefab.Criterion criterion : conditionalValue.getCriteriaList()) {
for (EvaluatedCriterion evaluateCriterion : evaluateCriterionMatch(
criterion,
lookupContext,
rowProperties
)) {
if (!evaluateCriterion.isMatch()) {
return Optional.empty();
}
evaluatedCriteria.add(evaluateCriterion);
}
}
return Optional.of(
simplifyToMatch(
rowIndex,
conditionalValue,
conditionalValueIndex,
configElement,
lookupContext,
evaluatedCriteria
)
);
}
/**
* A ConfigValue may be a WeightedValue. If so break it down so we can return a simpler form.
*/
private Match simplifyToMatch(
long rowIndex,
Prefab.ConditionalValue selectedConditionalValue,
int conditionalValueIndex,
ConfigElement configElement,
LookupContext lookupContext,
List evaluatedCriteria
) {
if (selectedConditionalValue.getValue().hasWeightedValues()) {
WeightedValueEvaluator.Result result = weightedValueEvaluator.toResult(
selectedConditionalValue.getValue().getWeightedValues(),
configElement.getConfig().getKey(),
lookupContext
);
return new Match(
result.getValue(),
configElement,
evaluatedCriteria,
(int) rowIndex,
conditionalValueIndex,
Optional.of(result.getIndex())
);
} else {
return new Match(
selectedConditionalValue.getValue(),
configElement,
evaluatedCriteria,
(int) rowIndex,
conditionalValueIndex,
Optional.empty()
);
}
}
private List keyAndLowerCasedKey(String key) {
String lowerCased = key.toLowerCase();
if (lowerCased.equals(key)) {
return Collections.singletonList(key);
}
return List.of(key, lowerCased);
}
private Optional prop(
String key,
LookupContext lookupContext,
Deque> rowPropertiesStack
) {
List keysToLookup = keyAndLowerCasedKey(key);
for (Map rowProperties : rowPropertiesStack) {
for (String keyToLookup : keysToLookup) {
Prefab.ConfigValue rowPropValue = rowProperties.get(keyToLookup);
if (rowPropValue != null) {
return Optional.of(rowPropValue);
}
}
}
for (String keyToLookup : keysToLookup) {
Prefab.ConfigValue valueFromLookupContext = lookupContext
.getExpandedProperties()
.get(keyToLookup);
if (valueFromLookupContext != null) {
return Optional.of(valueFromLookupContext);
}
}
//TODO: move this current time injection into a ContextResolver class?
if (CURRENT_TIME_KEY.equals(key)) {
return Optional.of(
Prefab.ConfigValue.newBuilder().setInt(System.currentTimeMillis()).build()
);
}
return Optional.empty();
}
private Optional getPropFromContextWrapper(
List keysToLookup,
ContextWrapper contextWrapper
) {
for (String keyToLookup : keysToLookup) {
Prefab.ConfigValue configValue = contextWrapper
.getConfigValueMap()
.get(keyToLookup);
if (configValue != null) {
return Optional.of(configValue);
}
}
return Optional.empty();
}
List evaluateCriterionMatch(
Prefab.Criterion criterion,
LookupContext lookupContext
) {
return evaluateCriterionMatch(criterion, lookupContext, new LinkedList<>());
}
/**
* Does this criterion match?
*
* @param criterion
* @return
*/
List evaluateCriterionMatch(
Prefab.Criterion criterion,
LookupContext lookupContext,
Deque> rowPropertiesStack
) {
final Optional prop = prop(
criterion.getPropertyName(),
lookupContext,
rowPropertiesStack
);
Optional propStringValue = prop.flatMap(ConfigValueUtils::coerceToString);
switch (criterion.getOperator()) {
case ALWAYS_TRUE:
return List.of(new EvaluatedCriterion(criterion, true));
case HIERARCHICAL_MATCH:
if (prop.isPresent()) {
if (prop.get().hasString() && criterion.getValueToMatch().hasString()) {
final String propertyString = prop.get().getString();
return List.of(
new EvaluatedCriterion(
criterion,
criterion.getValueToMatch(),
hierarchicalMatch(propertyString, criterion.getValueToMatch().getString())
)
);
}
}
return List.of(
new EvaluatedCriterion(criterion, criterion.getValueToMatch(), false)
);
// The string here is the key of the Segment
case IN_SEG:
final Optional evaluatedSegment = getMatch(
criterion.getValueToMatch().getString(),
lookupContext,
rowPropertiesStack
);
if (
evaluatedSegment.isPresent() &&
evaluatedSegment.get().getConfigValue().hasBool() &&
evaluatedSegment.get().getConfigValue().getBool()
) {
return evaluatedSegment.get().getEvaluatedCriterion();
} else {
return List.of(
new EvaluatedCriterion(
criterion,
"Missing Segment " + criterion.getValueToMatch().getString(),
false
)
);
}
case NOT_IN_SEG:
final Optional evaluatedNotSegment = getMatch(
criterion.getValueToMatch().getString(),
lookupContext
)
.map(Match::getConfigValue);
if (evaluatedNotSegment.isPresent() && evaluatedNotSegment.get().hasBool()) {
return List.of(
new EvaluatedCriterion(
criterion,
criterion.getValueToMatch(),
!evaluatedNotSegment.get().getBool()
)
);
} else {
return List.of(
new EvaluatedCriterion(
criterion,
"Missing Segment " + criterion.getValueToMatch().getString(),
true
)
);
}
case PROP_IS_ONE_OF:
if (propStringValue.isEmpty()) {
return List.of(new EvaluatedCriterion(criterion, false));
}
// assumption that property is a String
return List.of(
new EvaluatedCriterion(
criterion,
propStringValue.get(),
criterion
.getValueToMatch()
.getStringList()
.getValuesList()
.contains(propStringValue.get())
)
);
case PROP_IS_NOT_ONE_OF:
if (propStringValue.isEmpty()) {
return List.of(new EvaluatedCriterion(criterion, false));
}
return List.of(
new EvaluatedCriterion(
criterion,
propStringValue.get(),
!criterion
.getValueToMatch()
.getStringList()
.getValuesList()
.contains(propStringValue.get())
)
);
case PROP_ENDS_WITH_ONE_OF:
if (prop.isPresent() && prop.get().hasString()) {
final boolean matched = criterion
.getValueToMatch()
.getStringList()
.getValuesList()
.stream()
.anyMatch(value -> prop.get().getString().endsWith(value));
return List.of(new EvaluatedCriterion(criterion, prop.get(), matched));
} else {
return List.of(new EvaluatedCriterion(criterion, false));
}
case PROP_DOES_NOT_END_WITH_ONE_OF:
if (prop.isPresent() && prop.get().hasString()) {
final boolean matched = criterion
.getValueToMatch()
.getStringList()
.getValuesList()
.stream()
.anyMatch(value -> prop.get().getString().endsWith(value));
return List.of(new EvaluatedCriterion(criterion, prop.get(), !matched));
} else {
return List.of(new EvaluatedCriterion(criterion, true));
}
case IN_INT_RANGE:
if (
prop.isPresent() &&
prop.get().hasInt() &&
criterion.getValueToMatch().hasIntRange()
) {
return List.of(
new EvaluatedCriterion(
criterion,
IntRangeWrapper
.of(criterion.getValueToMatch().getIntRange())
.contains(prop.get().getInt())
)
);
}
default:
LOG.debug(
"Unexpected operator {} found in criterion {}",
criterion.getOperator(),
criterion
);
}
// Unknown Operator
return List.of(new EvaluatedCriterion(criterion, false));
}
/**
* a.b.c match a.b -> true
* a.b match a.b.c -> false
*
* @param valueToMatch
* @param propertyString
* @return
*/
boolean hierarchicalMatch(String propertyString, String valueToMatch) {
return propertyString.startsWith(valueToMatch);
}
public Collection getKeys() {
return configStore.getKeys();
}
public Collection getKeysOfConfigType(Prefab.ConfigType configType) {
return configStore
.getElements()
.stream()
.map(ConfigElement::getConfig)
.filter(config -> config.getConfigType() == configType)
.map(Prefab.Config::getKey)
.collect(Collectors.toList());
}
public ConfigElement getRaw(String key) {
return configStore.getElement(key);
}
public boolean containsKey(String key) {
return configStore.containsKey(key);
}
}