com.launchdarkly.sdk.server.DataModel Maven / Gradle / Ivy
package com.launchdarkly.sdk.server;
import com.google.common.collect.ImmutableList;
import com.google.gson.annotations.JsonAdapter;
import com.launchdarkly.sdk.EvaluationReason;
import com.launchdarkly.sdk.LDValue;
import com.launchdarkly.sdk.UserAttribute;
import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.DataKind;
import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptySet;
/**
* Contains information about the internal data model for feature flags and user segments.
*
* The details of the data model are not public to application code (although of course developers can easily
* look at the code or the data) so that changes to LaunchDarkly SDK implementation details will not be breaking
* changes to the application. Therefore, most of the members of this class are package-private. The public
* members provide a high-level description of model objects so that custom integration code or test code can
* store or serialize them.
*/
public abstract class DataModel {
private DataModel() {}
/**
* The {@link DataKind} instance that describes feature flag data.
*
* Applications should not need to reference this object directly. It is public so that custom integrations
* and test code can serialize or deserialize data or inject it into a data store.
*/
public static DataKind FEATURES = new DataKind("features",
DataModel::serializeItem,
s -> deserializeItem(s, FeatureFlag.class));
/**
* The {@link DataKind} instance that describes user segment data.
*
* Applications should not need to reference this object directly. It is public so that custom integrations
* and test code can serialize or deserialize data or inject it into a data store.
*/
public static DataKind SEGMENTS = new DataKind("segments",
DataModel::serializeItem,
s -> deserializeItem(s, Segment.class));
/**
* An enumeration of all supported {@link DataKind} types.
*
* Applications should not need to reference this object directly. It is public so that custom data store
* implementations can determine ahead of time what kinds of model objects may need to be stored, if
* necessary.
*/
public static Iterable ALL_DATA_KINDS = ImmutableList.of(FEATURES, SEGMENTS);
private static ItemDescriptor deserializeItem(String s, Class extends VersionedData> itemClass) {
VersionedData o = JsonHelpers.deserialize(s, itemClass);
return o.isDeleted() ? ItemDescriptor.deletedItem(o.getVersion()) : new ItemDescriptor(o.getVersion(), o);
}
private static String serializeItem(ItemDescriptor item) {
Object o = item.getItem();
if (o != null) {
return JsonHelpers.serialize(o);
}
return "{\"version\":" + item.getVersion() + ",\"deleted\":true}";
}
// All of these inner data model classes should have package-private scope. They should have only property
// accessors; the evaluator logic is in Evaluator, EvaluatorBucketing, and EvaluatorOperators.
/**
* Common interface for FeatureFlag and Segment, for convenience in accessing their common properties.
* @since 3.0.0
*/
interface VersionedData {
String getKey();
int getVersion();
/**
* True if this is a placeholder for a deleted item.
* @return true if deleted
*/
boolean isDeleted();
}
@JsonAdapter(JsonHelpers.PostProcessingDeserializableTypeAdapterFactory.class)
static final class FeatureFlag implements VersionedData, JsonHelpers.PostProcessingDeserializable {
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;
}
public int getVersion() {
return version;
}
public String getKey() {
return key;
}
boolean isTrackEvents() {
return trackEvents;
}
boolean isTrackEventsFallthrough() {
return trackEventsFallthrough;
}
Long getDebugEventsUntilDate() {
return debugEventsUntilDate;
}
public boolean isDeleted() {
return deleted;
}
boolean isOn() {
return on;
}
List getPrerequisites() {
return prerequisites == null ? emptyList() : prerequisites;
}
String getSalt() {
return salt;
}
// Guaranteed non-null
List getTargets() {
return targets == null ? emptyList() : targets;
}
// Guaranteed non-null
List getRules() {
return rules == null ? emptyList() : rules;
}
VariationOrRollout getFallthrough() {
return fallthrough;
}
// Guaranteed non-null
List getVariations() {
return variations == null ? emptyList() : 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() {
EvaluatorPreprocessing.preprocessFlag(this);
}
}
static final class Prerequisite {
private String key;
private int variation;
private transient EvaluationReason prerequisiteFailedReason;
Prerequisite() {}
Prerequisite(String key, int variation) {
this.key = key;
this.variation = variation;
}
String getKey() {
return key;
}
int getVariation() {
return variation;
}
// This value is precomputed when we deserialize a FeatureFlag from JSON
EvaluationReason getPrerequisiteFailedReason() {
return prerequisiteFailedReason;
}
void setPrerequisiteFailedReason(EvaluationReason prerequisiteFailedReason) {
this.prerequisiteFailedReason = prerequisiteFailedReason;
}
}
static final class Target {
private Set values;
private int variation;
Target() {}
Target(Set values, int variation) {
this.values = values;
this.variation = variation;
}
// Guaranteed non-null
Collection getValues() {
return values == null ? emptySet() : values;
}
int getVariation() {
return variation;
}
}
/**
* Expresses a set of AND-ed matching conditions for a user, along with either the fixed variation or percent rollout
* to serve if the conditions match.
* Invariant: one of the variation or rollout must be non-nil.
*/
static final class Rule extends VariationOrRollout {
private String id;
private List clauses;
private boolean trackEvents;
private transient EvaluationReason ruleMatchReason;
Rule() {
super();
}
Rule(String id, List clauses, Integer variation, Rollout rollout, boolean trackEvents) {
super(variation, rollout);
this.id = id;
this.clauses = clauses;
this.trackEvents = trackEvents;
}
String getId() {
return id;
}
// Guaranteed non-null
List getClauses() {
return clauses == null ? emptyList() : clauses;
}
boolean isTrackEvents() {
return trackEvents;
}
// This value is precomputed when we deserialize a FeatureFlag from JSON
EvaluationReason getRuleMatchReason() {
return ruleMatchReason;
}
void setRuleMatchReason(EvaluationReason ruleMatchReason) {
this.ruleMatchReason = ruleMatchReason;
}
}
static final class Clause {
private UserAttribute attribute;
private Operator op;
private List values; //interpreted as an OR of values
private boolean negate;
// The following property is marked transient because it is not to be serialized or deserialized;
// it is (if necessary) precomputed in FeatureFlag.afterDeserialized() to speed up evaluations.
transient EvaluatorPreprocessing.ClauseExtra preprocessed;
Clause() {
}
Clause(UserAttribute attribute, Operator op, List values, boolean negate) {
this.attribute = attribute;
this.op = op;
this.values = values;
this.negate = negate;
}
UserAttribute getAttribute() {
return attribute;
}
Operator getOp() {
return op;
}
// Guaranteed non-null
List getValues() {
return values == null ? emptyList() : values;
}
boolean isNegate() {
return negate;
}
EvaluatorPreprocessing.ClauseExtra getPreprocessed() {
return preprocessed;
}
void setPreprocessed(EvaluatorPreprocessing.ClauseExtra preprocessed) {
this.preprocessed = preprocessed;
}
}
static final class Rollout {
private List variations;
private UserAttribute bucketBy;
Rollout() {}
Rollout(List variations, UserAttribute bucketBy) {
this.variations = variations;
this.bucketBy = bucketBy;
}
// Guaranteed non-null
List getVariations() {
return variations == null ? emptyList() : variations;
}
UserAttribute getBucketBy() {
return bucketBy;
}
}
/**
* Contains either a fixed variation or percent rollout to serve.
* Invariant: one of the variation or rollout must be non-nil.
*/
static class VariationOrRollout {
private Integer variation;
private Rollout rollout;
VariationOrRollout() {}
VariationOrRollout(Integer variation, Rollout rollout) {
this.variation = variation;
this.rollout = rollout;
}
Integer getVariation() {
return variation;
}
Rollout getRollout() {
return rollout;
}
}
static final class WeightedVariation {
private int variation;
private int weight;
WeightedVariation() {}
WeightedVariation(int variation, int weight) {
this.variation = variation;
this.weight = weight;
}
int getVariation() {
return variation;
}
int getWeight() {
return weight;
}
}
@JsonAdapter(JsonHelpers.PostProcessingDeserializableTypeAdapterFactory.class)
static final class Segment implements VersionedData, JsonHelpers.PostProcessingDeserializable {
private String key;
private Set included;
private Set excluded;
private String salt;
private List rules;
private int version;
private boolean deleted;
Segment() {}
Segment(String key, Set included, Set excluded, String salt, List rules, int version, boolean deleted) {
this.key = key;
this.included = included;
this.excluded = excluded;
this.salt = salt;
this.rules = rules;
this.version = version;
this.deleted = deleted;
}
public String getKey() {
return key;
}
// Guaranteed non-null
Collection getIncluded() {
return included == null ? emptySet() : included;
}
// Guaranteed non-null
Collection getExcluded() {
return excluded == null ? emptySet() : excluded;
}
String getSalt() {
return salt;
}
// Guaranteed non-null
List getRules() {
return rules == null ? emptyList() : rules;
}
public int getVersion() {
return version;
}
public boolean isDeleted() {
return deleted;
}
// Precompute some invariant values for improved efficiency during evaluations - called from JsonHelpers.PostProcessingDeserializableTypeAdapter
public void afterDeserialized() {
EvaluatorPreprocessing.preprocessSegment(this);
}
}
static final class SegmentRule {
private final List clauses;
private final Integer weight;
private final UserAttribute bucketBy;
SegmentRule(List clauses, Integer weight, UserAttribute bucketBy) {
this.clauses = clauses;
this.weight = weight;
this.bucketBy = bucketBy;
}
// Guaranteed non-null
List getClauses() {
return clauses == null ? emptyList() : clauses;
}
Integer getWeight() {
return weight;
}
UserAttribute getBucketBy() {
return bucketBy;
}
}
/**
* This enum can be directly deserialized from JSON, avoiding the need for a mapping of strings to
* operators. The implementation of each operator is in EvaluatorOperators.
*/
static enum Operator {
in,
endsWith,
startsWith,
matches,
contains,
lessThan,
lessThanOrEqual,
greaterThan,
greaterThanOrEqual,
before,
after,
semVerEqual,
semVerLessThan,
semVerGreaterThan,
segmentMatch
}
}