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

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

There is a newer version: 4.61
Show newest version
package com.launchdarkly.client;

import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;

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

import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

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

class FeatureFlag implements VersionedData {
  private final static Logger logger = LoggerFactory.getLogger(FeatureFlag.class);

  private static final Type mapType = new TypeToken>() {
  }.getType();

  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 Long debugEventsUntilDate;
  private boolean deleted;

  static FeatureFlag fromJson(LDConfig config, String json) {
    return config.gson.fromJson(json, FeatureFlag.class);
  }

  static Map fromJsonMap(LDConfig config, String json) {
    return config.gson.fromJson(json, mapType);
  }

  // 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, 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.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, null), 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) {
        for (String v : target.getValues()) {
          if (v.equals(user.getKey().getAsString())) {
            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)) {
          return getValueForVariationOrRollout(rule, user, EvaluationReason.ruleMatch(i, rule.getId()));
        }
      }
    }
    // 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) {
        return 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, null);
    }
    return new EvaluationDetail(reason, variation, variations.get(variation));
  }

  private EvaluationDetail getOffValue(EvaluationReason reason) {
    if (offVariation == null) { // off variation unspecified - return default value
      return new EvaluationDetail(reason, null, null);
    }
    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, null); 
    }
    return getVariation(index, reason);
  }
  
  public int getVersion() {
    return version;
  }

  public String getKey() {
    return key;
  }

  public boolean isTrackEvents() {
    return trackEvents;
  }
  
  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;
  }
    
  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 - 2024 Weber Informatics LLC | Privacy Policy