com.launchdarkly.sdk.server.FeatureFlagsState Maven / Gradle / Ivy
package com.launchdarkly.sdk.server;
import com.google.gson.TypeAdapter;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import com.launchdarkly.sdk.EvaluationReason;
import com.launchdarkly.sdk.LDValue;
import com.launchdarkly.sdk.json.JsonSerializable;
import com.launchdarkly.sdk.server.interfaces.LDClientInterface;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import static com.launchdarkly.sdk.server.JsonHelpers.gsonInstance;
/**
* A snapshot of the state of all feature flags with regard to a specific user, generated by
* calling {@link LDClientInterface#allFlagsState(com.launchdarkly.sdk.LDUser, FlagsStateOption...)}.
*
* LaunchDarkly defines a standard JSON encoding for this object, suitable for
* bootstrapping
* the LaunchDarkly JavaScript browser SDK. You can convert it to JSON in any of these ways:
*
* - With {@link com.launchdarkly.sdk.json.JsonSerialization}.
*
- With Gson, if and only if you configure your {@code Gson} instance with
* {@link com.launchdarkly.sdk.json.LDGson}.
*
- With Jackson, if and only if you configure your {@code ObjectMapper} instance with
* {@link com.launchdarkly.sdk.json.LDJackson}.
*
*
* @since 4.3.0
*/
@JsonAdapter(FeatureFlagsState.JsonSerialization.class)
public final class FeatureFlagsState implements JsonSerializable {
private final Map flagValues;
private final Map flagMetadata;
private final boolean valid;
static class FlagMetadata {
final Integer variation;
final EvaluationReason reason;
final Integer version;
final Boolean trackEvents;
final Long debugEventsUntilDate;
FlagMetadata(Integer variation, EvaluationReason reason, Integer version, boolean trackEvents,
Long debugEventsUntilDate) {
this.variation = variation;
this.reason = reason;
this.version = version;
this.trackEvents = trackEvents ? Boolean.TRUE : null;
this.debugEventsUntilDate = debugEventsUntilDate;
}
@Override
public boolean equals(Object other) {
if (other instanceof FlagMetadata) {
FlagMetadata o = (FlagMetadata)other;
return Objects.equals(variation, o.variation) &&
Objects.equals(reason, o.reason) &&
Objects.equals(version, o.version) &&
Objects.equals(trackEvents, o.trackEvents) &&
Objects.equals(debugEventsUntilDate, o.debugEventsUntilDate);
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(variation, version, trackEvents, debugEventsUntilDate);
}
}
private FeatureFlagsState(Map flagValues,
Map flagMetadata, boolean valid) {
this.flagValues = Collections.unmodifiableMap(flagValues);
this.flagMetadata = Collections.unmodifiableMap(flagMetadata);
this.valid = valid;
}
/**
* Returns true if this object contains a valid snapshot of feature flag state, or false if the
* state could not be computed (for instance, because the client was offline or there was no user).
* @return true if the state is valid
*/
public boolean isValid() {
return valid;
}
/**
* Returns the value of an individual feature flag at the time the state was recorded.
* @param key the feature flag key
* @return the flag's JSON value; {@link LDValue#ofNull()} if the flag returned the default value;
* {@code null} if there was no such flag
*/
public LDValue getFlagValue(String key) {
return flagValues.get(key);
}
/**
* Returns the evaluation reason for an individual feature flag at the time the state was recorded.
* @param key the feature flag key
* @return an {@link EvaluationReason}; null if reasons were not recorded, or if there was no such flag
*/
public EvaluationReason getFlagReason(String key) {
FlagMetadata data = flagMetadata.get(key);
return data == null ? null : data.reason;
}
/**
* Returns a map of flag keys to flag values. If a flag would have evaluated to the default value,
* its value will be null.
*
* Do not use this method if you are passing data to the front end to "bootstrap" the JavaScript client.
* Instead, serialize the FeatureFlagsState object to JSON using {@code Gson.toJson()} or {@code Gson.toJsonTree()}.
* @return an immutable map of flag keys to JSON values
*/
public Map toValuesMap() {
return flagValues;
}
@Override
public boolean equals(Object other) {
if (other instanceof FeatureFlagsState) {
FeatureFlagsState o = (FeatureFlagsState)other;
return flagValues.equals(o.flagValues) &&
flagMetadata.equals(o.flagMetadata) &&
valid == o.valid;
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(flagValues, flagMetadata, valid);
}
static class Builder {
private Map flagValues = new HashMap<>();
private Map flagMetadata = new HashMap<>();
private final boolean saveReasons;
private final boolean detailsOnlyForTrackedFlags;
private boolean valid = true;
Builder(FlagsStateOption... options) {
saveReasons = FlagsStateOption.hasOption(options, FlagsStateOption.WITH_REASONS);
detailsOnlyForTrackedFlags = FlagsStateOption.hasOption(options, FlagsStateOption.DETAILS_ONLY_FOR_TRACKED_FLAGS);
}
Builder valid(boolean valid) {
this.valid = valid;
return this;
}
Builder addFlag(DataModel.FeatureFlag flag, Evaluator.EvalResult eval) {
flagValues.put(flag.getKey(), eval.getValue());
final boolean flagIsTracked = flag.isTrackEvents() ||
(flag.getDebugEventsUntilDate() != null && flag.getDebugEventsUntilDate() > System.currentTimeMillis());
final boolean wantDetails = !detailsOnlyForTrackedFlags || flagIsTracked;
FlagMetadata data = new FlagMetadata(
eval.isDefault() ? null : eval.getVariationIndex(),
(saveReasons && wantDetails) ? eval.getReason() : null,
wantDetails ? flag.getVersion() : null,
flag.isTrackEvents(),
flag.getDebugEventsUntilDate());
flagMetadata.put(flag.getKey(), data);
return this;
}
FeatureFlagsState build() {
return new FeatureFlagsState(flagValues, flagMetadata, valid);
}
}
static class JsonSerialization extends TypeAdapter {
@Override
public void write(JsonWriter out, FeatureFlagsState state) throws IOException {
out.beginObject();
for (Map.Entry entry: state.flagValues.entrySet()) {
out.name(entry.getKey());
gsonInstance().toJson(entry.getValue(), LDValue.class, out);
}
out.name("$flagsState");
out.beginObject();
for (Map.Entry entry: state.flagMetadata.entrySet()) {
out.name(entry.getKey());
FlagMetadata meta = entry.getValue();
out.beginObject();
// Here we're serializing FlagMetadata properties individually because if we rely on
// Gson's reflection mechanism, it won't reliably drop null properties (that only works
// if the destination really is Gson, not if a Jackson adapter is being used).
if (meta.variation != null) {
out.name("variation");
out.value(meta.variation.intValue());
}
if (meta.reason != null) {
out.name("reason");
gsonInstance().toJson(meta.reason, EvaluationReason.class, out);
}
if (meta.version != null) {
out.name("version");
out.value(meta.version.intValue());
}
if (meta.trackEvents != null) {
out.name("trackEvents");
out.value(meta.trackEvents.booleanValue());
}
if (meta.debugEventsUntilDate != null) {
out.name("debugEventsUntilDate");
out.value(meta.debugEventsUntilDate.longValue());
}
out.endObject();
}
out.endObject();
out.name("$valid");
out.value(state.valid);
out.endObject();
}
// There isn't really a use case for deserializing this, but we have to implement it
@Override
public FeatureFlagsState read(JsonReader in) throws IOException {
Map flagValues = new HashMap<>();
Map flagMetadata = new HashMap<>();
boolean valid = true;
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
if (name.equals("$flagsState")) {
in.beginObject();
while (in.hasNext()) {
String metaName = in.nextName();
FlagMetadata meta = gsonInstance().fromJson(in, FlagMetadata.class);
flagMetadata.put(metaName, meta);
}
in.endObject();
} else if (name.equals("$valid")) {
valid = in.nextBoolean();
} else {
LDValue value = gsonInstance().fromJson(in, LDValue.class);
flagValues.put(name, value);
}
}
in.endObject();
return new FeatureFlagsState(flagValues, flagMetadata, valid);
}
}
}