All Downloads are FREE. Search and download functionalities are using the official Maven repository.

cloud.prefab.client.config.ConfigResolver Maven / Gradle / Ivy

Go to download

API Client for https://prefab.cloud: rate limits, feature flags and semaphores as a service

The newest version!
package cloud.prefab.client.config;

import cloud.prefab.client.ConfigStore;
import cloud.prefab.domain.Prefab;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
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 ConfigResolver {

  public static final String NAMESPACE_KEY = "NAMESPACE";
  public static final String LOOKUP_KEY = "LOOKUP";

  private static final Logger LOG = LoggerFactory.getLogger(ConfigResolver.class);

  private final ConfigStore configStore;
  private final WeightedValueEvaluator weightedValueEvaluator;

  private long projectEnvId = 0;

  public ConfigResolver(ConfigStore configStoreImpl) {
    this(configStoreImpl, 0L);
  }

  public ConfigResolver(ConfigStore configStoreImpl, long projectEnvId) {
    this.weightedValueEvaluator = new WeightedValueEvaluator();
    this.projectEnvId = projectEnvId;
    this.configStore = configStoreImpl;
  }

  public Optional getConfigValue(String key) {
    return getConfigValue(key, new HashMap<>());
  }

  public Optional getMatch(
    String key,
    Map properties
  ) {
    if (!configStore.containsKey(key)) {
      LOG.warn("No config value found for key {}", key);
      return Optional.empty();
    }
    final ConfigElement configElement = configStore.getElement(key);

    return evalConfigElementMatch(configElement, properties);
  }

  public Optional getConfigValue(
    String key,
    Map properties
  ) {
    return getMatch(key, properties).map(Match::getConfigValue);
  }

  /**
   * find if we have a match for the given properties
   *
   * @param configElement
   * @param properties
   * @return
   */
  Optional evalConfigElementMatch(
    ConfigElement configElement,
    Map properties
  ) {
    // 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)
    final Optional match = configElement
      .getRowsProjEnvFirst(projectEnvId)
      .map(configRow -> {
        Map rowProperties = new HashMap<>(
          properties.size() + configRow.getPropertiesMap().size()
        );
        rowProperties.putAll(properties);

        // Add row properties like "active"
        rowProperties.putAll(configRow.getPropertiesMap());
        // Return the value of the first matching set of criteria
        for (Prefab.ConditionalValue conditionalValue : configRow.getValuesList()) {
          Optional optionalMatch = evaluateConditionalValue(
            conditionalValue,
            rowProperties,
            configElement
          );
          if (optionalMatch.isPresent()) {
            return optionalMatch.get();
          }
        }
        return null;
      })
      .filter(Objects::nonNull)
      .findFirst();

    return match;
  }

  /**
   * If all of the conditions match, return a true match
   *
   * @param conditionalValue
   * @param rowProperties
   * @param configElement
   * @return
   */
  private Optional evaluateConditionalValue(
    Prefab.ConditionalValue conditionalValue,
    Map rowProperties,
    ConfigElement configElement
  ) {
    final List evaluatedCriterionStream = conditionalValue
      .getCriteriaList()
      .stream()
      .flatMap(criterion -> evaluateCriterionMatch(criterion, rowProperties).stream())
      .collect(Collectors.toList());

    if (evaluatedCriterionStream.stream().allMatch(EvaluatedCriterion::isMatch)) {
      Prefab.ConfigValue simplified = simplify(
        conditionalValue,
        configElement.getConfig().getKey(),
        rowProperties
      );

      return Optional.of(new Match(simplified, configElement, evaluatedCriterionStream));
    } else {
      return Optional.empty();
    }
  }

  /**
   * A ConfigValue may be a WeightedValue. If so break it down so we can return a simpler form.
   */
  private Prefab.ConfigValue simplify(
    Prefab.ConditionalValue conditionalValue,
    String key,
    Map rowProperties
  ) {
    if (conditionalValue.getValue().hasWeightedValues()) {
      return weightedValueEvaluator.toValue(
        conditionalValue.getValue().getWeightedValues(),
        key,
        lookupKey(rowProperties)
      );
    } else {
      return conditionalValue.getValue();
    }
  }

  private Optional lookupKey(Map attributes) {
    if (attributes.containsKey(LOOKUP_KEY)) {
      return Optional.of(attributes.get(LOOKUP_KEY).getString());
    } else {
      return Optional.empty();
    }
  }

  private Optional prop(
    String key,
    Map attributes
  ) {
    if (attributes.containsKey(key)) {
      return Optional.of(attributes.get(key));
    } else {
      return Optional.empty();
    }
  }

  /**
   * Does this criterion match?
   *
   * @param criterion
   * @param attributes
   * @return
   */
  List evaluateCriterionMatch(
    Prefab.Criterion criterion,
    Map attributes
  ) {
    Optional lookupKey = lookupKey(attributes);
    final Optional prop = prop(
      criterion.getPropertyName(),
      attributes
    );

    switch (criterion.getOperator()) {
      case ALWAYS_TRUE:
        return List.of(new EvaluatedCriterion(criterion, true));
      case LOOKUP_KEY_IN:
        if (!lookupKey.isPresent()) {
          return List.of(new EvaluatedCriterion(criterion, false));
        }
        boolean match = criterion
          .getValueToMatch()
          .getStringList()
          .getValuesList()
          .contains(lookupKey.get());
        return List.of(new EvaluatedCriterion(criterion, lookupKey.get(), match));
      case LOOKUP_KEY_NOT_IN:
        if (!lookupKey.isPresent()) {
          return List.of(new EvaluatedCriterion(criterion, false));
        }
        boolean notMatch = !criterion
          .getValueToMatch()
          .getStringList()
          .getValuesList()
          .contains(lookupKey.get());
        return List.of(new EvaluatedCriterion(criterion, lookupKey.get(), notMatch));
      case HIERARCHICAL_MATCH:
        if (prop.isPresent()) {
          if (prop.get().hasString() && criterion.getValueToMatch().hasString()) {
            final String propertyString = attributes
              .get(criterion.getPropertyName())
              .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(),
          attributes
        );

        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 = getConfigValue(
          criterion.getValueToMatch().getString(),
          attributes
        );

        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 (!prop.isPresent()) {
          return List.of(new EvaluatedCriterion(criterion, false));
        }
        // assumption that property is a String
        return List.of(
          new EvaluatedCriterion(
            criterion,
            prop.get().getString(),
            criterion
              .getValueToMatch()
              .getStringList()
              .getValuesList()
              .contains(prop.get().getString())
          )
        );
      case PROP_IS_NOT_ONE_OF:
        if (!prop.isPresent()) {
          return List.of(new EvaluatedCriterion(criterion, false));
        }
        return List.of(
          new EvaluatedCriterion(
            criterion,
            prop.get().getString(),
            !criterion
              .getValueToMatch()
              .getStringList()
              .getValuesList()
              .contains(prop.get().getString())
          )
        );
      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));
        }
    }
    // 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 boolean setProjectEnvId(Prefab.Configs configs) {
    if (configs.hasConfigServicePointer()) {
      this.projectEnvId = configs.getConfigServicePointer().getProjectEnvId();
      return true;
    }
    return false;
  }

  public Collection getKeys() {
    return configStore.getKeys();
  }

  public String contentsString() {
    StringBuilder sb = new StringBuilder("\n");
    List sortedKeys = new ArrayList(getKeys());
    Collections.sort(sortedKeys);
    for (String key : sortedKeys) {
      ConfigElement configElement = configStore.getElement(key);
      final Optional match = evalConfigElementMatch(
        configElement,
        new HashMap<>()
      );

      if (match.isPresent()) {
        sb.append(padded(key, 30));
        sb.append(padded(toS(match.get().getConfigValue()), 40));
        sb.append(padded(configElement.getProvenance().toString(), 40));
        sb.append(padded(match.get().getReason(), 40));
      }
      sb.append("\n");
    }
    System.out.println(sb.toString());
    return sb.toString();
  }

  private String toS(Prefab.ConfigValue configValue) {
    if (configValue.getTypeCase() == Prefab.ConfigValue.TypeCase.STRING) {
      return configValue.getString();
    } else if (configValue.getTypeCase() == Prefab.ConfigValue.TypeCase.INT) {
      return Long.toString(configValue.getInt());
    } else if (configValue.getTypeCase() == Prefab.ConfigValue.TypeCase.BOOL) {
      return Boolean.toString(configValue.getBool());
    } else if (configValue.getTypeCase() == Prefab.ConfigValue.TypeCase.BYTES) {
      return "Bytes";
    } else if (configValue.getTypeCase() == Prefab.ConfigValue.TypeCase.DOUBLE) {
      return Double.toString(configValue.getDouble());
    } else if (configValue.getTypeCase() == Prefab.ConfigValue.TypeCase.LOG_LEVEL) {
      return configValue.getLogLevel().toString();
    } else {
      return "Unknown";
    }
  }

  private String padded(String s, int size) {
    return String.format(
      "%-" + size + "s",
      s.substring(0, Math.min(s.length(), size - 1))
    );
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy