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

com.launchdarkly.sdk.server.FeatureFlagsState Maven / Gradle / Ivy

There is a newer version: 7.5.0
Show newest version
package com.launchdarkly.sdk.server;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
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.HashMap;
import java.util.Map;
import java.util.Objects;

import static com.launchdarkly.sdk.server.JsonHelpers.gsonInstanceWithNullsAllowed;

/**
 * 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: *

    *
  1. With {@link com.launchdarkly.sdk.json.JsonSerialization}. *
  2. With Gson, if and only if you configure your {@code Gson} instance with * {@link com.launchdarkly.sdk.json.LDGson}. *
  3. 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 ImmutableMap flagMetadata; private final boolean valid; static class FlagMetadata { final LDValue value; final Integer variation; final EvaluationReason reason; final Integer version; final boolean trackEvents; final boolean trackReason; final Long debugEventsUntilDate; FlagMetadata(LDValue value, Integer variation, EvaluationReason reason, Integer version, boolean trackEvents, boolean trackReason, Long debugEventsUntilDate) { this.value = LDValue.normalize(value); this.variation = variation; this.reason = reason; this.version = version; this.trackEvents = trackEvents; this.trackReason = trackReason; this.debugEventsUntilDate = debugEventsUntilDate; } @Override public boolean equals(Object other) { if (other instanceof FlagMetadata) { FlagMetadata o = (FlagMetadata)other; return value.equals(o.value) && Objects.equals(variation, o.variation) && Objects.equals(reason, o.reason) && Objects.equals(version, o.version) && trackEvents == o.trackEvents && trackReason == o.trackReason && Objects.equals(debugEventsUntilDate, o.debugEventsUntilDate); } return false; } @Override public int hashCode() { return Objects.hash(variation, version, trackEvents, trackReason, debugEventsUntilDate); } } private FeatureFlagsState(ImmutableMap flagMetadata, boolean valid) { this.flagMetadata = flagMetadata; this.valid = valid; } /** * Returns a {@link Builder} for creating instances. *

* Application code will not normally use this builder, since the SDK creates its own instances. * However, it may be useful in testing, to simulate values that might be returned by * {@link LDClient#allFlagsState(com.launchdarkly.sdk.LDUser, FlagsStateOption...)}. * * @param options the same {@link FlagsStateOption}s, if any, that would be passed to * {@link LDClient#allFlagsState(com.launchdarkly.sdk.LDUser, FlagsStateOption...)} * @return a builder object * @since 5.6.0 */ public static Builder builder(FlagsStateOption... options) { return new Builder(options); } /** * 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) { FlagMetadata data = flagMetadata.get(key); return data == null ? null : data.value; } /** * 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. *

* The returned map is unmodifiable. *

* 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 Maps.transformValues(flagMetadata, v -> v.value); } @Override public boolean equals(Object other) { if (other instanceof FeatureFlagsState) { FeatureFlagsState o = (FeatureFlagsState)other; return flagMetadata.equals(o.flagMetadata) && valid == o.valid; } return false; } @Override public int hashCode() { return Objects.hash(flagMetadata, valid); } /** * A builder for a {@link FeatureFlagsState} instance. *

* Application code will not normally use this builder, since the SDK creates its own instances. * However, it may be useful in testing, to simulate values that might be returned by * {@link LDClient#allFlagsState(com.launchdarkly.sdk.LDUser, FlagsStateOption...)}. * * @since 5.6.0 */ public static class Builder { private ImmutableMap.Builder flagMetadata = ImmutableMap.builder(); private final boolean saveReasons; private final boolean detailsOnlyForTrackedFlags; private boolean valid = true; private Builder(FlagsStateOption... options) { saveReasons = FlagsStateOption.hasOption(options, FlagsStateOption.WITH_REASONS); detailsOnlyForTrackedFlags = FlagsStateOption.hasOption(options, FlagsStateOption.DETAILS_ONLY_FOR_TRACKED_FLAGS); } /** * Sets the {@link FeatureFlagsState#isValid()} property. This is true by default. * * @param valid the new property value * @return the builder */ public Builder valid(boolean valid) { this.valid = valid; return this; } /** * Adds data to the builder representing the result of a feature flag evaluation. *

* The {@code flagVersion}, {@code trackEvents}, and {@code debugEventsUntilDate} parameters are * normally generated internally by the SDK; they are used if the {@link FeatureFlagsState} data * has been passed to front-end code, to control how analytics events are generated by the front * end. If you are using this builder in back-end test code, those values are unimportant. * * @param flagKey the feature flag key * @param value the evaluated value * @param variationIndex the evaluated variation index * @param reason the evaluation reason * @param flagVersion the current flag version * @param trackEvents true if full event tracking is turned on for this flag * @param debugEventsUntilDate if set, event debugging is turned until this time (millisecond timestamp) * @return the builder */ public Builder add( String flagKey, LDValue value, Integer variationIndex, EvaluationReason reason, int flagVersion, boolean trackEvents, Long debugEventsUntilDate ) { return add(flagKey, value, variationIndex, reason, flagVersion, trackEvents, false, debugEventsUntilDate); } /** * Adds data to the builder representing the result of a feature flag evaluation. *

* The {@code flagVersion}, {@code trackEvents}, and {@code debugEventsUntilDate} parameters are * normally generated internally by the SDK; they are used if the {@link FeatureFlagsState} data * has been passed to front-end code, to control how analytics events are generated by the front * end. If you are using this builder in back-end test code, those values are unimportant. * * @param flagKey the feature flag key * @param value the evaluated value * @param variationIndex the evaluated variation index * @param reason the evaluation reason * @param flagVersion the current flag version * @param trackEvents true if full event tracking is turned on for this flag * @param trackReason true if evaluation reasons must be included due to experimentation * @param debugEventsUntilDate if set, event debugging is turned until this time (millisecond timestamp) * @return the builder */ public Builder add( String flagKey, LDValue value, Integer variationIndex, EvaluationReason reason, int flagVersion, boolean trackEvents, boolean trackReason, Long debugEventsUntilDate ) { final boolean flagIsTracked = trackEvents || (debugEventsUntilDate != null && debugEventsUntilDate > System.currentTimeMillis()); final boolean wantDetails = !detailsOnlyForTrackedFlags || flagIsTracked; FlagMetadata data = new FlagMetadata( value, variationIndex, (saveReasons && wantDetails) || trackReason ? reason : null, wantDetails ? Integer.valueOf(flagVersion) : null, trackEvents, trackReason, debugEventsUntilDate ); flagMetadata.put(flagKey, data); return this; } Builder addFlag(DataModel.FeatureFlag flag, EvalResult eval) { return add( flag.getKey(), eval.getValue(), eval.isNoVariation() ? null : eval.getVariationIndex(), eval.getReason(), flag.getVersion(), flag.isTrackEvents() || eval.isForceReasonTracking(), eval.isForceReasonTracking(), flag.getDebugEventsUntilDate() ); } /** * Returns an object created from the builder state. * * @return an immutable {@link FeatureFlagsState} */ public FeatureFlagsState build() { return new FeatureFlagsState(flagMetadata.build(), valid); } } static class JsonSerialization extends TypeAdapter { @Override public void write(JsonWriter out, FeatureFlagsState state) throws IOException { out.beginObject(); for (Map.Entry entry: state.flagMetadata.entrySet()) { out.name(entry.getKey()); gsonInstanceWithNullsAllowed().toJson(entry.getValue().value, 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"); gsonInstanceWithNullsAllowed().toJson(meta.reason, EvaluationReason.class, out); } if (meta.version != null) { out.name("version"); out.value(meta.version.intValue()); } if (meta.trackEvents) { out.name("trackEvents"); out.value(meta.trackEvents); } if (meta.trackReason) { out.name("trackReason"); out.value(meta.trackReason); } 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 flagMetadataWithoutValues = 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 = gsonInstanceWithNullsAllowed().fromJson(in, FlagMetadata.class); flagMetadataWithoutValues.put(metaName, meta); } in.endObject(); } else if (name.equals("$valid")) { valid = in.nextBoolean(); } else { LDValue value = gsonInstanceWithNullsAllowed().fromJson(in, LDValue.class); flagValues.put(name, value); } } in.endObject(); ImmutableMap.Builder allFlagMetadata = ImmutableMap.builder(); for (Map.Entry e: flagValues.entrySet()) { FlagMetadata m0 = flagMetadataWithoutValues.get(e.getKey()); if (m0 != null) { FlagMetadata m1 = new FlagMetadata( e.getValue(), m0.variation, m0.reason, m0.version, m0.trackEvents, m0.trackReason, m0.debugEventsUntilDate ); allFlagMetadata.put(e.getKey(), m1); } } return new FeatureFlagsState(allFlagMetadata.build(), valid); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy