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

com.vaadin.server.JsonCodec Maven / Gradle / Ivy

There is a newer version: 8.27.3
Show newest version
/*
 * Copyright (C) 2000-2024 Vaadin Ltd
 *
 * This program is available under Vaadin Commercial License and Service Terms.
 *
 * See  for the full
 * license.
 */

package com.vaadin.server;

import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import com.vaadin.server.communication.DateSerializer;
import com.vaadin.server.communication.JSONSerializer;
import com.vaadin.shared.Connector;
import com.vaadin.shared.JsonConstants;
import com.vaadin.shared.communication.UidlValue;
import com.vaadin.ui.Component;
import com.vaadin.ui.ConnectorTracker;
import com.vaadin.util.ReflectTools;

import elemental.json.Json;
import elemental.json.JsonArray;
import elemental.json.JsonException;
import elemental.json.JsonNull;
import elemental.json.JsonObject;
import elemental.json.JsonString;
import elemental.json.JsonType;
import elemental.json.JsonValue;
import elemental.json.impl.JreJsonArray;

/**
 * Decoder for converting RPC parameters and other values from JSON in transfer
 * between the client and the server and vice versa.
 *
 * @since 7.0
 */
public class JsonCodec implements Serializable {

    /* Immutable Encode Result representing null */
    private static final EncodeResult ENCODE_RESULT_NULL = new EncodeResult(
            Json.createNull());

    /* Immutable empty JSONArray */
    private static final JsonArray EMPTY_JSON_ARRAY = new JreJsonArray(
            Json.instance()) {
        @Override
        public void set(int index, JsonValue value) {
            throw new UnsupportedOperationException(
                    "Immutable empty JsonArray.");
        }

        @Override
        public void set(int index, String string) {
            throw new UnsupportedOperationException(
                    "Immutable empty JsonArray.");
        }

        @Override
        public void set(int index, double number) {
            throw new UnsupportedOperationException(
                    "Immutable empty JsonArray.");
        }

        @Override
        public void set(int index, boolean bool) {
            throw new UnsupportedOperationException(
                    "Immutable empty JsonArray.");
        }
    };

    public static interface BeanProperty extends Serializable {
        public Object getValue(Object bean) throws Exception;

        public void setValue(Object bean, Object value) throws Exception;

        public String getName();

        public Type getType();
    }

    private static class FieldProperty implements BeanProperty {
        private final Field field;

        public FieldProperty(Field field) {
            this.field = field;
        }

        @Override
        public Object getValue(Object bean) throws Exception {
            return field.get(bean);
        }

        @Override
        public void setValue(Object bean, Object value) throws Exception {
            field.set(bean, value);
        }

        @Override
        public String getName() {
            return field.getName();
        }

        @Override
        public Type getType() {
            return field.getGenericType();
        }

        public static Collection find(Class type)
                throws IntrospectionException {
            Field[] fields = type.getFields();
            Collection properties = new ArrayList<>(
                    fields.length);
            for (Field field : fields) {
                if (!Modifier.isStatic(field.getModifiers())) {
                    properties.add(new FieldProperty(field));
                }
            }

            return properties;
        }

    }

    private static class MethodProperty implements BeanProperty {
        private final PropertyDescriptor pd;

        public MethodProperty(PropertyDescriptor pd) {
            this.pd = pd;
        }

        @Override
        public Object getValue(Object bean) throws Exception {
            Method readMethod = pd.getReadMethod();
            return readMethod.invoke(bean);
        }

        @Override
        public void setValue(Object bean, Object value) throws Exception {
            pd.getWriteMethod().invoke(bean, value);
        }

        @Override
        public String getName() {
            String fieldName = pd.getWriteMethod().getName().substring(3);
            fieldName = Character.toLowerCase(fieldName.charAt(0))
                    + fieldName.substring(1);
            return fieldName;
        }

        public static Collection find(Class type)
                throws IntrospectionException {
            Collection properties = new ArrayList<>();

            for (PropertyDescriptor pd : Introspector.getBeanInfo(type)
                    .getPropertyDescriptors()) {
                if (pd.getReadMethod() == null || pd.getWriteMethod() == null) {
                    continue;
                }

                properties.add(new MethodProperty(pd));
            }
            return properties;
        }

        @Override
        public Type getType() {
            return pd.getReadMethod().getGenericReturnType();
        }

    }

    /**
     * Cache the collection of bean properties for a given type to avoid doing a
     * quite expensive lookup multiple times. Will be used from any thread that
     * happens to process Vaadin requests, so it must be protected from
     * corruption caused by concurrent access.
     */
    private static final ConcurrentMap, Collection> TYPE_PROPERTY_CACHE = new ConcurrentHashMap<>();

    private static final Map, String> TYPE_TO_TRANSPORT_TYPE = new HashMap<>();

    /**
     * Note! This does not contain primitives.
     * 

*/ private static final Map> TRANSPORT_TYPE_TO_TYPE = new HashMap<>(); private static final Map, JSONSerializer> CUSTOM_SERIALIZERS = new HashMap<>(); static { CUSTOM_SERIALIZERS.put(Date.class, new DateSerializer()); } static { registerType(String.class, JsonConstants.VTYPE_STRING); registerType(Connector.class, JsonConstants.VTYPE_CONNECTOR); registerType(Boolean.class, JsonConstants.VTYPE_BOOLEAN); registerType(boolean.class, JsonConstants.VTYPE_BOOLEAN); registerType(Integer.class, JsonConstants.VTYPE_INTEGER); registerType(int.class, JsonConstants.VTYPE_INTEGER); registerType(Float.class, JsonConstants.VTYPE_FLOAT); registerType(float.class, JsonConstants.VTYPE_FLOAT); registerType(Double.class, JsonConstants.VTYPE_DOUBLE); registerType(double.class, JsonConstants.VTYPE_DOUBLE); registerType(Long.class, JsonConstants.VTYPE_LONG); registerType(long.class, JsonConstants.VTYPE_LONG); registerType(String[].class, JsonConstants.VTYPE_STRINGARRAY); registerType(Object[].class, JsonConstants.VTYPE_ARRAY); registerType(Map.class, JsonConstants.VTYPE_MAP); registerType(HashMap.class, JsonConstants.VTYPE_MAP); registerType(List.class, JsonConstants.VTYPE_LIST); registerType(Set.class, JsonConstants.VTYPE_SET); registerType(Void.class, JsonConstants.VTYPE_NULL); } private static void registerType(Class type, String transportType) { TYPE_TO_TRANSPORT_TYPE.put(type, transportType); if (!type.isPrimitive()) { TRANSPORT_TYPE_TO_TYPE.put(transportType, type); } } public static boolean isInternalTransportType(String transportType) { return TRANSPORT_TYPE_TO_TYPE.containsKey(transportType); } public static boolean isInternalType(Type type) { if (type instanceof Class && ((Class) type).isPrimitive()) { if (type == byte.class || type == char.class) { // Almost all primitive types are handled internally return false; } // All primitive types are handled internally return true; } else if (type == UidlValue.class) { // UidlValue is a special internal type wrapping type info and a // value return true; } return TYPE_TO_TRANSPORT_TYPE.containsKey(getClassForType(type)); } private static Class getClassForType(Type type) { if (type instanceof ParameterizedType) { return (Class) (((ParameterizedType) type).getRawType()); } else if (type instanceof Class) { return (Class) type; } else { return null; } } private static Class getType(String transportType) { return TRANSPORT_TYPE_TO_TYPE.get(transportType); } public static Object decodeInternalOrCustomType(Type targetType, JsonValue value, ConnectorTracker connectorTracker) { if (isInternalType(targetType)) { return decodeInternalType(targetType, false, value, connectorTracker); } else { return decodeCustomType(targetType, value, connectorTracker); } } public static Object decodeCustomType(Type targetType, JsonValue value, ConnectorTracker connectorTracker) { if (isInternalType(targetType)) { throw new JsonException("decodeCustomType cannot be used for " + targetType + ", which is an internal type"); } // Try to decode object using fields if (isJsonType(targetType)) { return value; } else if (value.getType() == JsonType.NULL) { return null; } else if (targetType == byte.class || targetType == Byte.class) { return Byte.valueOf((byte) value.asNumber()); } else if (targetType == char.class || targetType == Character.class) { return Character.valueOf(value.asString().charAt(0)); } else if (targetType instanceof Class && ((Class) targetType).isArray()) { // Legacy Object[] and String[] handled elsewhere, this takes care // of generic arrays Class componentType = ((Class) targetType).getComponentType(); return decodeArray(componentType, (JsonArray) value, connectorTracker); } else if (targetType instanceof GenericArrayType) { Type componentType = ((GenericArrayType) targetType) .getGenericComponentType(); return decodeArray(componentType, (JsonArray) value, connectorTracker); } else if (JsonValue.class .isAssignableFrom(getClassForType(targetType))) { return value; } else if (CUSTOM_SERIALIZERS .containsKey(getClassForType(targetType))) { return CUSTOM_SERIALIZERS.get(getClassForType(targetType)) .deserialize(targetType, value, connectorTracker); } else if (Enum.class.isAssignableFrom(getClassForType(targetType))) { Class classForType = getClassForType(targetType); return decodeEnum(classForType.asSubclass(Enum.class), (JsonString) value); } else { return decodeObject(targetType, (JsonObject) value, connectorTracker); } } private static boolean isJsonType(Type type) { return type instanceof Class && JsonValue.class.isAssignableFrom((Class) type); } private static Object decodeArray(Type componentType, JsonArray value, ConnectorTracker connectorTracker) { Class componentClass = getClassForType(componentType); Object array = Array.newInstance(componentClass, value.length()); for (int i = 0; i < value.length(); i++) { Object decodedValue = decodeInternalOrCustomType(componentType, value.get(i), connectorTracker); Array.set(array, i, decodedValue); } return array; } /** * Decodes a value that is of an internal type. *

* Ensures the encoded value is of the same type as target type. *

*

* Allows restricting collections so that they must be declared using * generics. If this is used then all objects in the collection are encoded * using the declared type. Otherwise only internal types are allowed in * collections. *

* * @param targetType * The type that should be returned by this method * @param restrictToInternalTypes * @param encodedJsonValue * @param connectorTracker * @return */ public static Object decodeInternalType(Type targetType, boolean restrictToInternalTypes, JsonValue encodedJsonValue, ConnectorTracker connectorTracker) { if (!isInternalType(targetType)) { throw new JsonException("Type " + targetType + " is not a supported internal type."); } String transportType = getInternalTransportType(targetType); if (encodedJsonValue.getType() == JsonType.NULL) { return null; } else if (targetType == Void.class) { throw new JsonException( "Something other than null was encoded for a null type"); } // UidlValue if (targetType == UidlValue.class) { return decodeUidlValue((JsonArray) encodedJsonValue, connectorTracker); } // Collections if (JsonConstants.VTYPE_LIST.equals(transportType)) { return decodeList(targetType, restrictToInternalTypes, (JsonArray) encodedJsonValue, connectorTracker); } else if (JsonConstants.VTYPE_SET.equals(transportType)) { return decodeSet(targetType, restrictToInternalTypes, (JsonArray) encodedJsonValue, connectorTracker); } else if (JsonConstants.VTYPE_MAP.equals(transportType)) { return decodeMap(targetType, restrictToInternalTypes, encodedJsonValue, connectorTracker); } // Arrays if (JsonConstants.VTYPE_ARRAY.equals(transportType)) { return decodeObjectArray((JsonArray) encodedJsonValue, connectorTracker); } else if (JsonConstants.VTYPE_STRINGARRAY.equals(transportType)) { return decodeArray(String.class, (JsonArray) encodedJsonValue, null); } // Special Vaadin types if (JsonConstants.VTYPE_CONNECTOR.equals(transportType)) { return connectorTracker.getConnector(encodedJsonValue.asString()); } // Legacy types if (JsonConstants.VTYPE_STRING.equals(transportType)) { return encodedJsonValue.asString(); } else if (JsonConstants.VTYPE_INTEGER.equals(transportType)) { return (int) encodedJsonValue.asNumber(); } else if (JsonConstants.VTYPE_LONG.equals(transportType)) { return (long) encodedJsonValue.asNumber(); } else if (JsonConstants.VTYPE_FLOAT.equals(transportType)) { return (float) encodedJsonValue.asNumber(); } else if (JsonConstants.VTYPE_DOUBLE.equals(transportType)) { return encodedJsonValue.asNumber(); } else if (JsonConstants.VTYPE_BOOLEAN.equals(transportType)) { return encodedJsonValue.asBoolean(); } throw new JsonException("Unknown type " + transportType); } /** * Set a custom JSONSerializer for a specific Class. Existence of custom * serializers is checked after basic types (Strings, Booleans, Numbers, * Characters), Collections and Maps, so setting custom serializers for * these won't have any effect. *

* To remove a previously set serializer, call this method with the second * parameter set to {@code null}. *

* Custom serializers should only be added from static initializers or other * places that are guaranteed to run only once. Trying to add a serializer * to a class that already has one will cause an exception. *

* Warning: removing existing custom serializers may lead into unexpected * behavior in components that expect the customized data. The framework's * custom serializers are loaded in the static initializer block of this * class. * * @see DateSerializer * @throws IllegalArgumentException * Thrown if parameter clazz is null. * @throws IllegalStateException * Thrown if serializer for parameter clazz is already * registered and parameter jsonSerializer is not null. * @param clazz * The target class. * @param jsonSerializer * Custom JSONSerializer to add. If {@code null}, remove custom * serializer from class clazz. */ public static void setCustomSerializer(Class clazz, JSONSerializer jsonSerializer) { if (clazz == null) { throw new IllegalArgumentException( "Cannot add serializer for null"); } if (jsonSerializer == null) { CUSTOM_SERIALIZERS.remove(clazz); } else { if (CUSTOM_SERIALIZERS.containsKey(clazz)) { String err = String.format( "Class %s already has a custom serializer. " + "This exception can be thrown if you try to " + "add a serializer from a non-static context. " + "Try using a static block instead.", clazz.getName()); throw new IllegalStateException(err); } CUSTOM_SERIALIZERS.put(clazz, jsonSerializer); } } private static UidlValue decodeUidlValue(JsonArray encodedJsonValue, ConnectorTracker connectorTracker) { String type = encodedJsonValue.getString(0); Object decodedValue = decodeInternalType(getType(type), true, encodedJsonValue.get(1), connectorTracker); return new UidlValue(decodedValue); } private static Map decodeMap(Type targetType, boolean restrictToInternalTypes, JsonValue jsonMap, ConnectorTracker connectorTracker) { if (jsonMap.getType() == JsonType.ARRAY) { // Client-side has no declared type information to determine // encoding method for empty maps, so these are handled separately. // See #8906. JsonArray jsonArray = (JsonArray) jsonMap; if (jsonArray.length() == 0) { return new HashMap<>(); } } if (!restrictToInternalTypes && targetType instanceof ParameterizedType) { Type keyType = ((ParameterizedType) targetType) .getActualTypeArguments()[0]; Type valueType = ((ParameterizedType) targetType) .getActualTypeArguments()[1]; if (keyType == String.class) { return decodeStringMap(valueType, (JsonObject) jsonMap, connectorTracker); } else if (keyType == Connector.class) { return decodeConnectorMap(valueType, (JsonObject) jsonMap, connectorTracker); } else { return decodeObjectMap(keyType, valueType, (JsonArray) jsonMap, connectorTracker); } } else { return decodeStringMap(UidlValue.class, (JsonObject) jsonMap, connectorTracker); } } private static Map decodeObjectMap(Type keyType, Type valueType, JsonArray jsonMap, ConnectorTracker connectorTracker) { JsonArray keys = jsonMap.getArray(0); JsonArray values = jsonMap.getArray(1); assert (keys.length() == values.length()); Map map = new HashMap<>(keys.length() * 2); for (int i = 0; i < keys.length(); i++) { Object key = decodeInternalOrCustomType(keyType, keys.get(i), connectorTracker); Object value = decodeInternalOrCustomType(valueType, values.get(i), connectorTracker); map.put(key, value); } return map; } private static Map decodeConnectorMap(Type valueType, JsonObject jsonMap, ConnectorTracker connectorTracker) { Map map = new HashMap<>(); for (String key : jsonMap.keys()) { Object value = decodeInternalOrCustomType(valueType, jsonMap.get(key), connectorTracker); if (valueType == UidlValue.class) { value = ((UidlValue) value).getValue(); } map.put(connectorTracker.getConnector(key), value); } return map; } private static Map decodeStringMap(Type valueType, JsonObject jsonMap, ConnectorTracker connectorTracker) { Map map = new HashMap<>(); for (String key : jsonMap.keys()) { Object value = decodeInternalOrCustomType(valueType, jsonMap.get(key), connectorTracker); if (valueType == UidlValue.class) { value = ((UidlValue) value).getValue(); } map.put(key, value); } return map; } /** * @param targetType * @param restrictToInternalTypes * @param typeIndex * The index of a generic type to use to define the child type * that should be decoded * @param connectorTracker * @param value * @return */ private static Object decodeParametrizedType(Type targetType, boolean restrictToInternalTypes, int typeIndex, JsonValue value, ConnectorTracker connectorTracker) { if (!restrictToInternalTypes && targetType instanceof ParameterizedType) { Type childType = ((ParameterizedType) targetType) .getActualTypeArguments()[typeIndex]; // Only decode the given type return decodeInternalOrCustomType(childType, value, connectorTracker); } else { // Only UidlValue when not enforcing a given type to avoid security // issues UidlValue decodeInternalType = (UidlValue) decodeInternalType( UidlValue.class, true, value, connectorTracker); return decodeInternalType.getValue(); } } @SuppressWarnings({ "rawtypes", "unchecked" }) private static Object decodeEnum(Class cls, JsonString value) { return Enum.valueOf(cls, value.getString()); } private static Object[] decodeObjectArray(JsonArray jsonArray, ConnectorTracker connectorTracker) { List list = decodeList(List.class, true, jsonArray, connectorTracker); return list.toArray(new Object[list.size()]); } private static List decodeList(Type targetType, boolean restrictToInternalTypes, JsonArray jsonArray, ConnectorTracker connectorTracker) { int arrayLength = jsonArray.length(); List list = new ArrayList<>(arrayLength); for (int i = 0; i < arrayLength; ++i) { // each entry always has two elements: type and value JsonValue encodedValue = jsonArray.get(i); Object decodedChild = decodeParametrizedType(targetType, restrictToInternalTypes, 0, encodedValue, connectorTracker); list.add(decodedChild); } return list; } private static Set decodeSet(Type targetType, boolean restrictToInternalTypes, JsonArray jsonArray, ConnectorTracker connectorTracker) { HashSet set = new HashSet<>(); set.addAll(decodeList(targetType, restrictToInternalTypes, jsonArray, connectorTracker)); return set; } private static Object decodeObject(Type targetType, JsonObject serializedObject, ConnectorTracker connectorTracker) { Class targetClass = getClassForType(targetType); try { Object decodedObject = ReflectTools.createInstance(targetClass); for (BeanProperty property : getProperties(targetClass)) { String fieldName = property.getName(); JsonValue encodedFieldValue = serializedObject.get(fieldName); Type fieldType = property.getType(); Object decodedFieldValue = decodeInternalOrCustomType(fieldType, encodedFieldValue, connectorTracker); property.setValue(decodedObject, decodedFieldValue); } return decodedObject; } catch (Exception e) { throw new RuntimeException(e); } } @SuppressWarnings("deprecation") public static EncodeResult encode(Object value, JsonValue diffState, Type valueType, ConnectorTracker connectorTracker) { if (null == value) { return ENCODE_RESULT_NULL; } // Storing a single reference and only returning the EncodeResult at the // end the method is much shorter in bytecode which allows inlining JsonValue toReturn; if (value instanceof JsonValue) { // all JSON compatible types are returned as is. toReturn = (JsonValue) value; } else if (value instanceof String) { toReturn = Json.create((String) value); } else if (value instanceof Boolean) { toReturn = Json.create((Boolean) value); } else if (value instanceof Number) { toReturn = Json.create(((Number) value).doubleValue()); } else if (value instanceof Character) { toReturn = Json.create(Character.toString((Character) value)); } else if (value instanceof Collection) { toReturn = encodeCollection(valueType, (Collection) value, connectorTracker); } else if (value instanceof Map) { toReturn = encodeMap(valueType, (Map) value, connectorTracker); } else if (value instanceof Connector) { if (value instanceof Component && !(LegacyCommunicationManager .isComponentVisibleToClient((Component) value))) { // an encoded null is cached, return it directly. return ENCODE_RESULT_NULL; } // Connectors are simply serialized as ID. toReturn = Json.create(((Connector) value).getConnectorId()); } else if (CUSTOM_SERIALIZERS.containsKey(value.getClass())) { toReturn = serializeJson(value, connectorTracker); } else if (value instanceof Enum) { toReturn = Json.create(((Enum) value).name()); } else if (valueType instanceof GenericArrayType) { toReturn = encodeArrayContents( ((GenericArrayType) valueType).getGenericComponentType(), value, connectorTracker); } else if (valueType instanceof Class) { if (((Class) valueType).isArray()) { toReturn = encodeArrayContents( ((Class) valueType).getComponentType(), value, connectorTracker); } else { // encodeObject returns an EncodeResult with a diff, thus it // needs to return it directly rather than assigning it to // toReturn. return encodeObject(value, (Class) valueType, (JsonObject) diffState, connectorTracker); } } else { throw new JsonException("Can not encode type " + valueType); } return new EncodeResult(toReturn); } public static Collection getProperties(Class type) throws IntrospectionException { Collection cachedProperties = TYPE_PROPERTY_CACHE .get(type); if (cachedProperties != null) { return cachedProperties; } Collection properties = new ArrayList<>(); properties.addAll(MethodProperty.find(type)); properties.addAll(FieldProperty.find(type)); // Doesn't matter if the same calculation is done multiple times from // different threads, so there's no need to do e.g. putIfAbsent TYPE_PROPERTY_CACHE.put(type, properties); return properties; } /* * Loops through the fields of value and encodes them. */ private static EncodeResult encodeObject(Object value, Class valueType, JsonObject referenceValue, ConnectorTracker connectorTracker) { JsonObject encoded = Json.createObject(); JsonObject diff = Json.createObject(); try { for (BeanProperty property : getProperties(valueType)) { String fieldName = property.getName(); // We can't use PropertyDescriptor.getPropertyType() as it does // not support generics Type fieldType = property.getType(); Object fieldValue = property.getValue(value); if (encoded.hasKey(fieldName)) { throw new RuntimeException("Can't encode " + valueType.getName() + " as it has multiple properties with the name " + fieldName.toLowerCase(Locale.ROOT) + ". This can happen if there are getters and setters for a public field (the framework can't know which to ignore) or if there are properties with only casing distinguishing between the names (e.g. getFoo() and getFOO())"); } JsonValue fieldReference; if (referenceValue != null) { fieldReference = referenceValue.get(fieldName); if (fieldReference instanceof JsonNull) { fieldReference = null; } } else { fieldReference = null; } EncodeResult encodeResult = encode(fieldValue, fieldReference, fieldType, connectorTracker); encoded.put(fieldName, encodeResult.getEncodedValue()); if (valueChanged(encodeResult.getEncodedValue(), fieldReference)) { diff.put(fieldName, encodeResult.getDiffOrValue()); } } } catch (Exception e) { // TODO: Should exceptions be handled in a different way? throw new RuntimeException(e); } return new EncodeResult(encoded, diff); } /** * Compares the value with the reference. If they match, returns false. * * @param fieldValue * @param referenceValue * @return */ private static boolean valueChanged(JsonValue fieldValue, JsonValue referenceValue) { if (fieldValue instanceof JsonNull) { fieldValue = null; } if (fieldValue == referenceValue) { return false; } else if (fieldValue == null || referenceValue == null) { return true; } else { return !jsonEquals(fieldValue, referenceValue); } } /** * Compares two json values for deep equality. * * This is a helper for overcoming the fact that * {@code JsonValue.equals(Object)} only does an identity check and * {@link JsonValue#jsEquals(JsonValue)} is defined to use JavaScript * semantics where arrays and objects are equals only based on identity. * * @since 7.4 * @param a * the first json value to check, may not be null * @param b * the second json value to check, may not be null * @return true if both json values are the same; * false otherwise */ public static boolean jsonEquals(JsonValue a, JsonValue b) { assert a != null; assert b != null; if (a == b) { return true; } JsonType type = a.getType(); if (type != b.getType()) { return false; } switch (type) { case NULL: return true; case BOOLEAN: return a.asBoolean() == b.asBoolean(); case NUMBER: return a.asNumber() == b.asNumber(); case STRING: return a.asString().equals(b.asString()); case OBJECT: return jsonObjectEquals((JsonObject) a, (JsonObject) b); case ARRAY: return jsonArrayEquals((JsonArray) a, (JsonArray) b); default: throw new RuntimeException("Unsupported JsonType: " + type); } } private static boolean jsonObjectEquals(JsonObject a, JsonObject b) { String[] keys = a.keys(); if (keys.length != b.keys().length) { return false; } for (String key : keys) { JsonValue value = b.get(key); if (value == null || !jsonEquals(a.get(key), value)) { return false; } } return true; } private static boolean jsonArrayEquals(JsonArray a, JsonArray b) { if (a.length() != b.length()) { return false; } for (int i = 0; i < a.length(); i++) { if (!jsonEquals(a.get(i), b.get(i))) { return false; } } return true; } private static JsonArray encodeArrayContents(Type componentType, Object array, ConnectorTracker connectorTracker) { JsonArray jsonArray = Json.createArray(); for (int i = 0; i < Array.getLength(array); i++) { EncodeResult encodeResult = encode(Array.get(array, i), null, componentType, connectorTracker); jsonArray.set(i, encodeResult.getEncodedValue()); } return jsonArray; } private static JsonArray encodeCollection(Type targetType, Collection collection, ConnectorTracker connectorTracker) { JsonArray jsonArray = Json.createArray(); for (Object o : collection) { jsonArray.set(jsonArray.length(), encodeChild(targetType, 0, o, connectorTracker)); } return jsonArray; } private static JsonValue encodeChild(Type targetType, int typeIndex, Object o, ConnectorTracker connectorTracker) { if (targetType instanceof ParameterizedType) { Type childType = ((ParameterizedType) targetType) .getActualTypeArguments()[typeIndex]; // Encode using the given type EncodeResult encodeResult = encode(o, null, childType, connectorTracker); return encodeResult.getEncodedValue(); } else { throw new JsonException("Collection is missing generics"); } } private static JsonValue encodeMap(Type mapType, Map map, ConnectorTracker connectorTracker) { Type keyType, valueType; if (mapType instanceof ParameterizedType) { keyType = ((ParameterizedType) mapType).getActualTypeArguments()[0]; valueType = ((ParameterizedType) mapType) .getActualTypeArguments()[1]; } else { throw new JsonException("Map is missing generics"); } if (map.isEmpty()) { // Client -> server encodes empty map as an empty array because of // #8906. Do the same for server -> client to maintain symmetry. return EMPTY_JSON_ARRAY; } if (keyType == String.class) { return encodeStringMap(valueType, map, connectorTracker); } else if (keyType == Connector.class) { return encodeConnectorMap(valueType, map, connectorTracker); } else { return encodeObjectMap(keyType, valueType, map, connectorTracker); } } private static JsonArray encodeObjectMap(Type keyType, Type valueType, Map map, ConnectorTracker connectorTracker) { JsonArray keys = Json.createArray(); JsonArray values = Json.createArray(); for (Entry entry : map.entrySet()) { EncodeResult encodedKey = encode(entry.getKey(), null, keyType, connectorTracker); EncodeResult encodedValue = encode(entry.getValue(), null, valueType, connectorTracker); keys.set(keys.length(), encodedKey.getEncodedValue()); values.set(values.length(), encodedValue.getEncodedValue()); } JsonArray jsonMap = Json.createArray(); jsonMap.set(0, keys); jsonMap.set(1, values); return jsonMap; } /* * Encodes a connector map. Invisible connectors are skipped. */ @SuppressWarnings("deprecation") private static JsonObject encodeConnectorMap(Type valueType, Map map, ConnectorTracker connectorTracker) { JsonObject jsonMap = Json.createObject(); for (Entry entry : map.entrySet()) { ClientConnector key = (ClientConnector) entry.getKey(); if (LegacyCommunicationManager.isConnectorVisibleToClient(key)) { EncodeResult encodedValue = encode(entry.getValue(), null, valueType, connectorTracker); jsonMap.put(key.getConnectorId(), encodedValue.getEncodedValue()); } } return jsonMap; } private static JsonObject encodeStringMap(Type valueType, Map map, ConnectorTracker connectorTracker) { JsonObject jsonMap = Json.createObject(); for (Entry entry : map.entrySet()) { String key = (String) entry.getKey(); EncodeResult encodedValue = encode(entry.getValue(), null, valueType, connectorTracker); jsonMap.put(key, encodedValue.getEncodedValue()); } return jsonMap; } /* * These methods looks good to inline, but are on a cold path of the * otherwise hot encode method, which needed to be shorted to allow inlining * of the hot part. */ private static String getInternalTransportType(Type valueType) { return TYPE_TO_TRANSPORT_TYPE.get(getClassForType(valueType)); } @SuppressWarnings({ "rawtypes", "unchecked" }) private static JsonValue serializeJson(Object value, ConnectorTracker connectorTracker) { JSONSerializer serializer = CUSTOM_SERIALIZERS.get(value.getClass()); return serializer.serialize(value, connectorTracker); } private JsonCodec() { } }