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

com.launchdarkly.client.FeatureFlag Maven / Gradle / Ivy

package com.launchdarkly.client;

import com.google.gson.annotations.JsonAdapter;
import com.launchdarkly.client.value.LDValue;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.launchdarkly.client.VersionedDataKind.FEATURES;

@JsonAdapter(JsonHelpers.PostProcessingDeserializableTypeAdapterFactory.class)
class FeatureFlag implements VersionedData, JsonHelpers.PostProcessingDeserializable {
  private final static Logger logger = LoggerFactory.getLogger(FeatureFlag.class);

  private String key;
  private int version;
  private boolean on;
  private List prerequisites;
  private String salt;
  private List targets;
  private List rules;
  private VariationOrRollout fallthrough;
  private Integer offVariation; //optional
  private List variations;
  private boolean clientSide;
  private boolean trackEvents;
  private boolean trackEventsFallthrough;
  private Long debugEventsUntilDate;
  private boolean deleted;

  // We need this so Gson doesn't complain in certain java environments that restrict unsafe allocation
  FeatureFlag() {}

  FeatureFlag(String key, int version, boolean on, List prerequisites, String salt, List targets,
      List rules, VariationOrRollout fallthrough, Integer offVariation, List variations,
      boolean clientSide, boolean trackEvents, boolean trackEventsFallthrough,
      Long debugEventsUntilDate, boolean deleted) {
    this.key = key;
    this.version = version;
    this.on = on;
    this.prerequisites = prerequisites;
    this.salt = salt;
    this.targets = targets;
    this.rules = rules;
    this.fallthrough = fallthrough;
    this.offVariation = offVariation;
    this.variations = variations;
    this.clientSide = clientSide;
    this.trackEvents = trackEvents;
    this.trackEventsFallthrough = trackEventsFallthrough;
    this.debugEventsUntilDate = debugEventsUntilDate;
    this.deleted = deleted;
  }

  EvalResult evaluate(LDUser user, FeatureStore featureStore, EventFactory eventFactory) {
    List prereqEvents = new ArrayList<>();

    if (user == null || user.getKey() == null) {
      // this should have been prevented by LDClient.evaluateInternal
      logger.warn("Null user or null user key when evaluating flag \"{}\"; returning null", key);
      return new EvalResult(EvaluationDetail.error(EvaluationReason.ErrorKind.USER_NOT_SPECIFIED, LDValue.ofNull()), prereqEvents);
    }

    EvaluationDetail details = evaluate(user, featureStore, prereqEvents, eventFactory);
    return new EvalResult(details, prereqEvents);    
  }

  private EvaluationDetail evaluate(LDUser user, FeatureStore featureStore, List events,
      EventFactory eventFactory) {
    if (!isOn()) {
      return getOffValue(EvaluationReason.off());
    }
    
    EvaluationReason prereqFailureReason = checkPrerequisites(user, featureStore, events, eventFactory);
    if (prereqFailureReason != null) {
      return getOffValue(prereqFailureReason);
    }
    
    // Check to see if targets match
    if (targets != null) {
      for (Target target: targets) {
        if (target.getValues().contains(user.getKey().stringValue())) {
          return getVariation(target.getVariation(), EvaluationReason.targetMatch());
        }
      }
    }
    // Now walk through the rules and see if any match
    if (rules != null) {
      for (int i = 0; i < rules.size(); i++) {
        Rule rule = rules.get(i);
        if (rule.matchesUser(featureStore, user)) {
          EvaluationReason.RuleMatch precomputedReason = rule.getRuleMatchReason();
          EvaluationReason.RuleMatch reason = precomputedReason != null ? precomputedReason : EvaluationReason.ruleMatch(i, rule.getId());
          return getValueForVariationOrRollout(rule, user, reason);
        }
      }
    }
    // Walk through the fallthrough and see if it matches
    return getValueForVariationOrRollout(fallthrough, user, EvaluationReason.fallthrough());
  }

  // Checks prerequisites if any; returns null if successful, or an EvaluationReason if we have to
  // short-circuit due to a prerequisite failure.
  private EvaluationReason checkPrerequisites(LDUser user, FeatureStore featureStore, List events,
      EventFactory eventFactory) {
    if (prerequisites == null) {
      return null;
    }
    for (int i = 0; i < prerequisites.size(); i++) {
      boolean prereqOk = true;
      Prerequisite prereq = prerequisites.get(i);
      FeatureFlag prereqFeatureFlag = featureStore.get(FEATURES, prereq.getKey());
      if (prereqFeatureFlag == null) {
        logger.error("Could not retrieve prerequisite flag \"{}\" when evaluating \"{}\"", prereq.getKey(), key);
        prereqOk = false;
      } else {
        EvaluationDetail prereqEvalResult = prereqFeatureFlag.evaluate(user, featureStore, events, eventFactory);
        // Note that if the prerequisite flag is off, we don't consider it a match no matter what its
        // off variation was. But we still need to evaluate it in order to generate an event.
        if (!prereqFeatureFlag.isOn() || prereqEvalResult == null || prereqEvalResult.getVariationIndex() != prereq.getVariation()) {
          prereqOk = false;
        }
        events.add(eventFactory.newPrerequisiteFeatureRequestEvent(prereqFeatureFlag, user, prereqEvalResult, this));
      }
      if (!prereqOk) {
        EvaluationReason.PrerequisiteFailed precomputedReason = prereq.getPrerequisiteFailedReason();
        return precomputedReason != null ? precomputedReason : EvaluationReason.prerequisiteFailed(prereq.getKey());
      }
    }
    return null;
  }

  private EvaluationDetail getVariation(int variation, EvaluationReason reason) {
    if (variation < 0 || variation >= variations.size()) {
      logger.error("Data inconsistency in feature flag \"{}\": invalid variation index", key);
      return EvaluationDetail.error(EvaluationReason.ErrorKind.MALFORMED_FLAG, LDValue.ofNull());
    }
    LDValue value = LDValue.normalize(variations.get(variation));
    // normalize() ensures that nulls become LDValue.ofNull() - Gson may give us nulls
    return EvaluationDetail.fromValue(value, variation, reason);
  }

  private EvaluationDetail getOffValue(EvaluationReason reason) {
    if (offVariation == null) { // off variation unspecified - return default value
      return EvaluationDetail.fromValue(LDValue.ofNull(), null, reason);
    }
    return getVariation(offVariation, reason);
  }
  
  private EvaluationDetail getValueForVariationOrRollout(VariationOrRollout vr, LDUser user, EvaluationReason reason) {
    Integer index = vr.variationIndexForUser(user, key, salt);
    if (index == null) {
      logger.error("Data inconsistency in feature flag \"{}\": variation/rollout object with no variation or rollout", key);
      return EvaluationDetail.error(EvaluationReason.ErrorKind.MALFORMED_FLAG, LDValue.ofNull()); 
    }
    return getVariation(index, reason);
  }
  
  public int getVersion() {
    return version;
  }

  public String getKey() {
    return key;
  }

  public boolean isTrackEvents() {
    return trackEvents;
  }
  
  public boolean isTrackEventsFallthrough() {
    return trackEventsFallthrough;
  }
  
  public Long getDebugEventsUntilDate() {
    return debugEventsUntilDate;
  }
  
  public boolean isDeleted() {
    return deleted;
  }

  boolean isOn() {
    return on;
  }

  List getPrerequisites() {
    return prerequisites;
  }

  String getSalt() {
    return salt;
  }

  List getTargets() {
    return targets;
  }

  List getRules() {
    return rules;
  }

  VariationOrRollout getFallthrough() {
    return fallthrough;
  }

  List getVariations() {
    return variations;
  }

  Integer getOffVariation() {
    return offVariation;
  }

  boolean isClientSide() {
    return clientSide;
  }
  
  // Precompute some invariant values for improved efficiency during evaluations - called from JsonHelpers.PostProcessingDeserializableTypeAdapter
  public void afterDeserialized() {
    if (prerequisites != null) {
      for (Prerequisite p: prerequisites) {
        p.setPrerequisiteFailedReason(EvaluationReason.prerequisiteFailed(p.getKey()));
      }
    }
    if (rules != null) {
      for (int i = 0; i < rules.size(); i++) {
        Rule r = rules.get(i);
        r.setRuleMatchReason(EvaluationReason.ruleMatch(i, r.getId()));
      }
    }
  }
  
  static class EvalResult {
    private final EvaluationDetail details;
    private final List prerequisiteEvents;

    private EvalResult(EvaluationDetail details, List prerequisiteEvents) {
      checkNotNull(details);
      checkNotNull(prerequisiteEvents);
      this.details = details;
      this.prerequisiteEvents = prerequisiteEvents;
    }

    EvaluationDetail getDetails() {
      return details;
    }

    List getPrerequisiteEvents() {
      return prerequisiteEvents;
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy