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

com.launchdarkly.sdk.LDValue Maven / Gradle / Ivy

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

import com.google.gson.Gson;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.stream.JsonWriter;
import com.launchdarkly.sdk.json.JsonSerializable;
import com.launchdarkly.sdk.json.JsonSerialization;
import com.launchdarkly.sdk.json.SerializationException;

import java.io.IOException;
import java.util.Arrays;
import java.util.Map;

import static com.launchdarkly.sdk.Helpers.transform;
import static java.util.Collections.emptyList;

/**
 * An immutable instance of any data type that is allowed in JSON.
 * 

* An {@link LDValue} instance can be a null (that is, an instance that represents a JSON null value, * rather than a Java null reference), a boolean, a number (always encoded internally as double-precision * floating-point, but can be treated as an integer), a string, an ordered list of {@link LDValue} * values (a JSON array), or a map of strings to {@link LDValue} values (a JSON object). It is easily * convertible to standard Java types. *

* This can be used to represent complex data in a user custom attribute (see {@link LDUser.Builder#custom(String, LDValue)}), * or to get a feature flag value that uses a complex type or that does not always use the same * type (see the client's {@code jsonValueVariation} methods). *

* While the LaunchDarkly SDK uses Gson internally for JSON parsing, it uses {@link LDValue} rather * than Gson's {@code JsonElement} type for two reasons. First, this allows Gson types to be excluded * from the API, so the SDK does not expose this dependency and cannot cause version conflicts in * applications that use Gson themselves. Second, Gson's array and object types are mutable, which can * cause concurrency risks. *

* {@link LDValue} can be converted to and from JSON in any of these ways: *

    *
  1. With the {@link LDValue} methods {@link #toJsonString()} and {@link #parse(String)}. *
  2. With {@link com.launchdarkly.sdk.json.JsonSerialization}. *
  3. With Gson, if and only if you configure your {@code Gson} instance with * {@link com.launchdarkly.sdk.json.LDGson}. *
  4. With Jackson, if and only if you configure your {@code ObjectMapper} instance with * {@link com.launchdarkly.sdk.json.LDJackson}. *
*/ @JsonAdapter(LDValueTypeAdapter.class) public abstract class LDValue implements JsonSerializable { /** * Returns the same value if non-null, or {@link #ofNull()} if null. * * @param value an {@link LDValue} or null * @return an {@link LDValue} which will never be a null reference */ public static LDValue normalize(LDValue value) { return value == null ? ofNull() : value; } /** * Returns an instance for a null value. The same instance is always used. * * @return an LDValue containing null */ public static LDValue ofNull() { return LDValueNull.INSTANCE; } /** * Returns an instance for a boolean value. The same instances for {@code true} and {@code false} * are always used. * * @param value a boolean value * @return an LDValue containing that value */ public static LDValue of(boolean value) { return LDValueBool.fromBoolean(value); } /** * Returns an instance for a numeric value. * * @param value an integer numeric value * @return an LDValue containing that value */ public static LDValue of(int value) { return LDValueNumber.fromDouble(value); } /** * Returns an instance for a numeric value. *

* Note that the LaunchDarkly service, and most of the SDKs, represent numeric values internally * in 64-bit floating-point, which has slightly less precision than a signed 64-bit {@code long}; * therefore, the full range of {@code long} values cannot be accurately represented. If you need * to set a user attribute to a numeric value with more significant digits than will fit in a * {@code double}, it is best to encode it as a string. * * @param value a long integer numeric value * @return an LDValue containing that value */ public static LDValue of(long value) { return LDValueNumber.fromDouble(value); } /** * Returns an instance for a numeric value. * * @param value a floating-point numeric value * @return an LDValue containing that value */ public static LDValue of(float value) { return LDValueNumber.fromDouble(value); } /** * Returns an instance for a numeric value. * * @param value a floating-point numeric value * @return an LDValue containing that value */ public static LDValue of(double value) { return LDValueNumber.fromDouble(value); } /** * Returns an instance for a string value (or a null). * * @param value a nullable String reference * @return an LDValue containing a string, or {@link #ofNull()} if the value was null. */ public static LDValue of(String value) { return value == null ? ofNull() : LDValueString.fromString(value); } /** * Starts building an array value. The elements can be of any type supported by LDValue. *


   *     LDValue arrayOfInts = LDValue.buildArray().add(2).add("three").build():
   * 
* If the values are all of the same type, you may also use {@link LDValue.Converter#arrayFrom(Iterable)} * or {@link LDValue.Converter#arrayOf(Object...)}. * * @return an {@link ArrayBuilder} */ public static ArrayBuilder buildArray() { return new ArrayBuilder(); } /** * Creates an array value from the specified values. The elements can be of any type supported by LDValue. *

   *     LDValue arrayOfMixedValues = LDValue.arrayOf(LDValue.of(2), LDValue.of("three"));
   * 
* If the values are all of the same type, you may also use {@link LDValue.Converter#arrayFrom(Iterable)} * or {@link LDValue.Converter#arrayOf(Object...)}. * * @param values any number of values * @return an immutable array value */ public static LDValue arrayOf(LDValue... values) { return LDValueArray.fromList(values == null ? null : Arrays.asList(values)); } /** * Starts building an object value. *

   *     LDValue objectVal = LDValue.buildObject().put("key", LDValue.int(1)).build():
   * 
* If the values are all of the same type, you may also use {@link LDValue.Converter#objectFrom(Map)}. * * @return an {@link ObjectBuilder} */ public static ObjectBuilder buildObject() { return new ObjectBuilder(); } /** * Parses an LDValue from a JSON representation. *

* This convenience method is equivalent to using {@link JsonSerialization#deserialize(String, Class)} * with the {@code LDValue} class, except for two things: *

* 1. You do not have to provide the class parameter. *

* 2. Parsing errors are thrown as an unchecked {@code RuntimeException} that wraps the checked * {@link SerializationException}, making this method somewhat more convenient in cases such as * test code where explicit error handling is less important. * * @param json a JSON string * @return an LDValue */ public static LDValue parse(String json) { try { return LDValue.normalize(JsonSerialization.deserialize(json, LDValue.class)); } catch (SerializationException e) { throw new RuntimeException(e); } } /** * Gets the JSON type for this value. * * @return the appropriate {@link LDValueType} */ public abstract LDValueType getType(); /** * Tests whether this value is a null. * * @return {@code true} if this is a null value */ public boolean isNull() { return false; } /** * Returns this value as a boolean if it is explicitly a boolean. Otherwise returns {@code false}. * * @return a boolean */ public boolean booleanValue() { return false; } /** * Tests whether this value is a number (not a numeric string). * * @return {@code true} if this is a numeric value */ public boolean isNumber() { return false; } /** * Tests whether this value is a number that is also an integer. *

* JSON does not have separate types for integer and floating-point values; they are both just * numbers. This method returns true if and only if the actual numeric value has no fractional * component, so {@code LDValue.of(2).isInt()} and {@code LDValue.of(2.0f).isInt()} are both true. * * @return {@code true} if this is an integer value */ public boolean isInt() { return false; } /** * Returns this value as an {@code int} if it is numeric. Returns zero for all non-numeric values. *

* If the value is a number but not an integer, it will be rounded toward zero (truncated). * This is consistent with Java casting behavior, and with most other LaunchDarkly SDKs. * * @return an {@code int} value */ public int intValue() { return 0; } /** * Returns this value as a {@code long} if it is numeric. Returns zero for all non-numeric values. *

* If the value is a number but not an integer, it will be rounded toward zero (truncated). * This is consistent with Java casting behavior, and with most other LaunchDarkly SDKs. * * @return a {@code long} value */ public long longValue() { return 0; } /** * Returns this value as a {@code float} if it is numeric. Returns zero for all non-numeric values. * * @return a {@code float} value */ public float floatValue() { return 0; } /** * Returns this value as a {@code double} if it is numeric. Returns zero for all non-numeric values. * * @return a {@code double} value */ public double doubleValue() { return 0; } /** * Tests whether this value is a string. * * @return {@code true} if this is a string value */ public boolean isString() { return false; } /** * Returns this value as a {@code String} if it is a string. Returns {@code null} for all non-string values. * * @return a nullable string value */ public String stringValue() { return null; } /** * Returns the number of elements in an array or object. Returns zero for all other types. * * @return the number of array elements or object properties */ public int size() { return 0; } /** * Enumerates the property names in an object. Returns an empty iterable for all other types. * * @return the property names */ public Iterable keys() { return emptyList(); } /** * Enumerates the values in an array or object. Returns an empty iterable for all other types. * * @return an iterable of {@link LDValue} values */ public Iterable values() { return emptyList(); } /** * Enumerates the values in an array or object, converting them to a specific type. Returns an empty * iterable for all other types. *

* This is an efficient method because it does not copy values to a new list, but returns a view * into the existing array. *

* Example: *


   *     LDValue anArrayOfInts = LDValue.Convert.Integer.arrayOf(1, 2, 3);
   *     for (int i: anArrayOfInts.valuesAs(LDValue.Convert.Integer)) { println(i); }
   * 
*

* For boolean and numeric types, even though the corresponding Java type is a nullable class like * {@code Boolean} or {@code Integer}, {@code valuesAs} will never return a null element; instead, * it will use the appropriate default value for the primitive type (false or zero). * * @param the desired type * @param converter the {@link Converter} for the specified type * @return an iterable of values of the specified type */ public Iterable valuesAs(final Converter converter) { return transform(values(), new Function() { @Override public T apply(LDValue a) { return converter.toType(a); } }); } /** * Returns an array element by index. Returns {@link #ofNull()} if this is not an array or if the * index is out of range (will never throw an exception). * * @param index the array index * @return the element value or {@link #ofNull()} */ public LDValue get(int index) { return ofNull(); } /** * Returns an object property by name. Returns {@link #ofNull()} if this is not an object or if the * key is not found (will never throw an exception). * * @param name the property name * @return the property value or {@link #ofNull()} */ public LDValue get(String name) { return ofNull(); } /** * Converts this value to its JSON serialization. *

* This method is equivalent to passing the {@code LDValue} instance to * {@link JsonSerialization#serialize(JsonSerializable)}. * * @return a JSON string */ public String toJsonString() { return JsonSerialization.serialize(this); } abstract void write(JsonWriter writer) throws IOException; static boolean isInteger(double value) { return value == (double)((int)value); } /** * Returns a string representation of this value. *

* This method currently returns the same JSON serialization as {@link #toJsonString()}. However, * like most {@code toString()} implementations, it is intended mainly for convenience in * debugging or other use cases where the goal is simply to have a human-readable format; it is * not guaranteed to always match {@link #toJsonString()} in the future. If you need to verify * the value type or other properties programmatically, use the getter methods of {@code LDValue}. */ @Override public String toString() { return toJsonString(); } /** * Returns true if the other object is an {@link LDValue} that is logically equal. *

* This is a deep equality comparison: for JSON arrays each element is compared recursively, and * for JSON objects all property names and values must be deeply equal regardless of ordering. */ @Override public boolean equals(Object o) { if (o instanceof LDValue) { if (o == this) { return true; } LDValue other = (LDValue)o; if (getType() == other.getType()) { switch (getType()) { case NULL: return other.isNull(); // COVERAGE: won't hit this case because ofNull() is a singleton, so (o == this) will be true case NUMBER: return doubleValue() == other.doubleValue(); case BOOLEAN: return false; // boolean true and false are singletons, so if o != this, they're unequal case STRING: return stringValue().equals(other.stringValue()); case ARRAY: if (size() != other.size()) { return false; } for (int i = 0; i < size(); i++) { if (!get(i).equals(other.get(i))) { return false; } } return true; case OBJECT: if (size() != other.size()) { return false; } for (String name: keys()) { if (!get(name).equals(other.get(name))) { return false; } } return true; default: break; } } } return false; } @Override public int hashCode() { switch (getType()) { case BOOLEAN: return booleanValue() ? 1 : 0; case NUMBER: return intValue(); case STRING: return stringValue().hashCode(); case ARRAY: int ah = 0; for (LDValue v: values()) { ah = ah * 31 + v.hashCode(); } return ah; case OBJECT: int oh = 0; for (String name: keys()) { oh = (oh * 31 + name.hashCode()) * 31 + get(name).hashCode(); } return oh; default: return 0; } } /** * Defines a conversion between {@link LDValue} and some other type. *

* Besides converting individual values, this provides factory methods like {@link #arrayOf} * which transform a collection of the specified type to the corresponding {@link LDValue} * complex type. * * @param the type to convert from/to */ public static abstract class Converter { /** * Converts a value of the specified type to an {@link LDValue}. *

* This method should never throw an exception; if for some reason the value is invalid, * it should return {@link LDValue#ofNull()}. * * @param value a value of this type * @return an {@link LDValue} */ public abstract LDValue fromType(T value); /** * Converts an {@link LDValue} to a value of the specified type. *

* This method should never throw an exception; if the conversion cannot be done, it should * return the default value of the given type (zero for numbers, null for nullable types). * * @param value an {@link LDValue} * @return a value of this type */ public abstract T toType(LDValue value); /** * Initializes an {@link LDValue} as an array, from a sequence of this type. *

* Values are copied, so subsequent changes to the source values do not affect the array. *

* Example: *


     *     List<Integer> listOfInts = ImmutableList.<Integer>builder().add(1).add(2).add(3).build();
     *     LDValue arrayValue = LDValue.Convert.Integer.arrayFrom(listOfInts);
     * 
* * @param values a sequence of elements of the specified type * @return a value representing a JSON array, or {@link LDValue#ofNull()} if the parameter was null * @see LDValue#buildArray() */ public LDValue arrayFrom(Iterable values) { ArrayBuilder ab = LDValue.buildArray(); for (T value: values) { ab.add(fromType(value)); } return ab.build(); } /** * Initializes an {@link LDValue} as an array, from a sequence of this type. *

* Values are copied, so subsequent changes to the source values do not affect the array. *

* Example: *


     *     LDValue arrayValue = LDValue.Convert.Integer.arrayOf(1, 2, 3);
     * 
* * @param values a sequence of elements of the specified type * @return a value representing a JSON array, or {@link LDValue#ofNull()} if the parameter was null * @see LDValue#buildArray() */ @SuppressWarnings("unchecked") public LDValue arrayOf(T... values) { ArrayBuilder ab = LDValue.buildArray(); for (T value: values) { ab.add(fromType(value)); } return ab.build(); } /** * Initializes an {@link LDValue} as an object, from a map containing this type. *

* Values are copied, so subsequent changes to the source map do not affect the array. *

* Example: *


     *     Map<String, Integer> mapOfInts = ImmutableMap.<String, Integer>builder().put("a", 1).build();
     *     LDValue objectValue = LDValue.Convert.Integer.objectFrom(mapOfInts);
     * 
* * @param map a map with string keys and values of the specified type * @return a value representing a JSON object, or {@link LDValue#ofNull()} if the parameter was null * @see LDValue#buildObject() */ public LDValue objectFrom(Map map) { ObjectBuilder ob = LDValue.buildObject(); for (String key: map.keySet()) { ob.put(key, fromType(map.get(key))); } return ob.build(); } } /** * Predefined instances of {@link LDValue.Converter} for commonly used types. *

* These are mostly useful for methods that convert {@link LDValue} to or from a collection of * some type, such as {@link LDValue.Converter#arrayOf(Object...)} and * {@link LDValue#valuesAs(Converter)}. */ public static abstract class Convert { private Convert() {} /** * A {@link LDValue.Converter} for booleans. */ public static final Converter Boolean = new Converter() { public LDValue fromType(java.lang.Boolean value) { return value == null ? LDValue.ofNull() : LDValue.of(value.booleanValue()); } public java.lang.Boolean toType(LDValue value) { return java.lang.Boolean.valueOf(value.booleanValue()); } }; /** * A {@link LDValue.Converter} for integers. */ public static final Converter Integer = new Converter() { public LDValue fromType(java.lang.Integer value) { return value == null ? LDValue.ofNull() : LDValue.of(value.intValue()); } public java.lang.Integer toType(LDValue value) { return java.lang.Integer.valueOf(value.intValue()); } }; /** * A {@link LDValue.Converter} for long integers. *

* Note that the LaunchDarkly service, and most of the SDKs, represent numeric values internally * in 64-bit floating-point, which has slightly less precision than a signed 64-bit {@code long}; * therefore, the full range of {@code long} values cannot be accurately represented. If you need * to set a user attribute to a numeric value with more significant digits than will fit in a * {@code double}, it is best to encode it as a string. */ public static final Converter Long = new Converter() { public LDValue fromType(java.lang.Long value) { return value == null ? LDValue.ofNull() : LDValue.of(value.longValue()); } public java.lang.Long toType(LDValue value) { return java.lang.Long.valueOf(value.longValue()); } }; /** * A {@link LDValue.Converter} for floats. */ public static final Converter Float = new Converter() { public LDValue fromType(java.lang.Float value) { return value == null ? LDValue.ofNull() : LDValue.of(value.floatValue()); } public java.lang.Float toType(LDValue value) { return java.lang.Float.valueOf(value.floatValue()); } }; /** * A {@link LDValue.Converter} for doubles. */ public static final Converter Double = new Converter() { public LDValue fromType(java.lang.Double value) { return value == null ? LDValue.ofNull() : LDValue.of(value.doubleValue()); } public java.lang.Double toType(LDValue value) { return java.lang.Double.valueOf(value.doubleValue()); } }; /** * A {@link LDValue.Converter} for strings. */ public static final Converter String = new Converter() { public LDValue fromType(java.lang.String value) { return LDValue.of(value); } public java.lang.String toType(LDValue value) { return value.stringValue(); } }; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy