com.vaadin.server.JsonCodec.java Source code

Java tutorial

Introduction

Here is the source code for com.vaadin.server.JsonCodec.java

Source

/*
 * Copyright 2000-2018 Vaadin Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the 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<FieldProperty> find(Class<?> type) throws IntrospectionException {
            Field[] fields = type.getFields();
            Collection<FieldProperty> 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<MethodProperty> find(Class<?> type) throws IntrospectionException {
            Collection<MethodProperty> 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<Class<?>, Collection<BeanProperty>> TYPE_PROPERTY_CACHE = new ConcurrentHashMap<>();

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

    /**
     * Note! This does not contain primitives.
     * <p>
     */
    private static final Map<String, Class<?>> TRANSPORT_TYPE_TO_TYPE = new HashMap<>();

    private static final Map<Class<?>, 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.
     * <p>
     * Ensures the encoded value is of the same type as target type.
     * </p>
     * <p>
     * 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.
     * </p>
     *
     * @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(targetType, (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.
     * <p>
     * To remove a previously set serializer, call this method with the second
     * parameter set to {@code null}.
     * <p>
     * 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.
     * <p>
     * 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 <TYPE> void setCustomSerializer(Class<TYPE> clazz, JSONSerializer<TYPE> 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<Object, Object> 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<Object, Object> 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<Object, Object> 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<Object, Object> decodeConnectorMap(Type valueType, JsonObject jsonMap,
            ConnectorTracker connectorTracker) {
        Map<Object, Object> 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<Object, Object> decodeStringMap(Type valueType, JsonObject jsonMap,
            ConnectorTracker connectorTracker) {
        Map<Object, Object> 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();
        }
    }

    private static Object decodeEnum(Class<? extends Enum> cls, JsonString value) {
        return Enum.valueOf(cls, value.getString());
    }

    private static Object[] decodeObjectArray(Type targetType, JsonArray jsonArray,
            ConnectorTracker connectorTracker) {
        List<Object> list = decodeList(List.class, true, jsonArray, connectorTracker);
        return list.toArray(new Object[list.size()]);
    }

    private static List<Object> decodeList(Type targetType, boolean restrictToInternalTypes, JsonArray jsonArray,
            ConnectorTracker connectorTracker) {
        int arrayLength = jsonArray.length();
        List<Object> 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<Object> decodeSet(Type targetType, boolean restrictToInternalTypes, JsonArray jsonArray,
            ConnectorTracker connectorTracker) {
        HashSet<Object> 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);
        }
    }

    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<BeanProperty> getProperties(Class<?> type) throws IntrospectionException {
        Collection<BeanProperty> cachedProperties = TYPE_PROPERTY_CACHE.get(type);
        if (cachedProperties != null) {
            return cachedProperties;
        }
        Collection<BeanProperty> 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
     * {@link 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 <code>true</code> if both json values are the same;
     *         <code>false</code> 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.
     */
    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));
    }

    private static JsonValue serializeJson(Object value, ConnectorTracker connectorTracker) {
        JSONSerializer serializer = CUSTOM_SERIALIZERS.get(value.getClass());
        return serializer.serialize(value, connectorTracker);
    }

    private JsonCodec() {
    }
}