]> source.dussan.org Git - vaadin-framework.git/commitdiff
Refactored JsonCodec to use a given type for deserialization (#8667)
authorArtur Signell <artur@vaadin.com>
Tue, 17 Apr 2012 22:02:48 +0000 (01:02 +0300)
committerArtur Signell <artur@vaadin.com>
Wed, 18 Apr 2012 20:09:02 +0000 (23:09 +0300)
src/com/vaadin/terminal/gwt/server/JsonCodec.java

index 8da8fdc9334c2d3c9e27ec2f5096911728236cc5..0d2b0c20823eb6688d8665f7a1198a5cc6cce308 100644 (file)
@@ -10,6 +10,8 @@ import java.beans.PropertyDescriptor;
 import java.io.Serializable;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
@@ -37,6 +39,12 @@ public class JsonCodec implements Serializable {
 
     private static Map<Class<?>, String> typeToTransportType = new HashMap<Class<?>, String>();
 
+    /**
+     * Note! This does not contain primitives.
+     * <p>
+     */
+    private static Map<String, Class<?>> transportTypeToType = new HashMap<String, Class<?>>();
+
     static {
         registerType(String.class, JsonEncoder.VTYPE_STRING);
         registerType(Connector.class, JsonEncoder.VTYPE_CONNECTOR);
@@ -50,8 +58,6 @@ public class JsonCodec implements Serializable {
         registerType(double.class, JsonEncoder.VTYPE_DOUBLE);
         registerType(Long.class, JsonEncoder.VTYPE_LONG);
         registerType(long.class, JsonEncoder.VTYPE_LONG);
-        // transported as string representation
-        registerType(Enum.class, JsonEncoder.VTYPE_STRING);
         registerType(String[].class, JsonEncoder.VTYPE_STRINGARRAY);
         registerType(Object[].class, JsonEncoder.VTYPE_ARRAY);
         registerType(Map.class, JsonEncoder.VTYPE_MAP);
@@ -61,92 +67,235 @@ public class JsonCodec implements Serializable {
 
     private static void registerType(Class<?> type, String transportType) {
         typeToTransportType.put(type, transportType);
+        if (!type.isPrimitive()) {
+            transportTypeToType.put(transportType, type);
+        }
+    }
+
+    public static boolean isInternalTransportType(String transportType) {
+        return transportTypeToType.containsKey(transportType);
+    }
+
+    public static boolean isInternalType(Type type) {
+        if (type instanceof Class && ((Class<?>) type).isPrimitive()) {
+            // All primitive types are handled internally
+            return true;
+        }
+        return typeToTransportType.containsKey(getClassForType(type));
+    }
+
+    private static Class<?> getClassForType(Type type) {
+        if (type instanceof ParameterizedType) {
+            return (Class<?>) (((ParameterizedType) type).getRawType());
+        } else {
+            return (Class<?>) type;
+        }
+    }
+
+    public static String getTransportType(JSONArray encodedValue)
+            throws JSONException {
+        return encodedValue.getString(0);
+    }
+
+    private static Class<?> getType(String transportType) {
+        return transportTypeToType.get(transportType);
     }
 
     /**
-     * Convert a JSON array with two elements (type and value) into a
-     * server-side type, recursively if necessary.
+     * Decodes the given value and type, restricted to using only internal
+     * types.
      * 
-     * @param value
-     *            JSON array with two elements
+     * @param valueAndType
      * @param application
-     *            mapper between connector ID and {@link Connector} objects
-     * @return converted value (does not contain JSON types)
      * @throws JSONException
-     *             if the conversion fails
      */
-    public static Object decode(JSONArray value, Application application)
-            throws JSONException {
-        return decodeVariableValue(value.getString(0), value.get(1),
+    @Deprecated
+    public static Object decodeInternalType(JSONArray valueAndType,
+            Application application) throws JSONException {
+        String transportType = getTransportType(valueAndType);
+        return decodeInternalType(getType(transportType), true, valueAndType,
                 application);
     }
 
-    private static Object decodeVariableValue(String variableType,
-            Object value, Application application) throws JSONException {
-        Object val = null;
-        // TODO type checks etc.
-        if (JsonEncoder.VTYPE_ARRAY.equals(variableType)) {
-            val = decodeArray((JSONArray) value, application);
-        } else if (JsonEncoder.VTYPE_LIST.equals(variableType)) {
-            val = decodeList((JSONArray) value, application);
-        } else if (JsonEncoder.VTYPE_SET.equals(variableType)) {
-            val = decodeSet((JSONArray) value, application);
-        } else if (JsonEncoder.VTYPE_MAP_CONNECTOR.equals(variableType)) {
-            val = decodeConnectorMap((JSONObject) value, application);
-        } else if (JsonEncoder.VTYPE_MAP.equals(variableType)) {
-            val = decodeMap((JSONObject) value, application);
-        } else if (JsonEncoder.VTYPE_STRINGARRAY.equals(variableType)) {
-            val = decodeStringArray((JSONArray) value);
-        } else if (JsonEncoder.VTYPE_STRING.equals(variableType)) {
-            val = value;
-        } else if (JsonEncoder.VTYPE_INTEGER.equals(variableType)) {
-            // TODO handle properly
-            val = Integer.valueOf(String.valueOf(value));
-        } else if (JsonEncoder.VTYPE_LONG.equals(variableType)) {
-            // TODO handle properly
-            val = Long.valueOf(String.valueOf(value));
-        } else if (JsonEncoder.VTYPE_FLOAT.equals(variableType)) {
-            // TODO handle properly
-            val = Float.valueOf(String.valueOf(value));
-        } else if (JsonEncoder.VTYPE_DOUBLE.equals(variableType)) {
-            // TODO handle properly
-            val = Double.valueOf(String.valueOf(value));
-        } else if (JsonEncoder.VTYPE_BOOLEAN.equals(variableType)) {
-            // TODO handle properly
-            val = Boolean.valueOf(String.valueOf(value));
-        } else if (JsonEncoder.VTYPE_CONNECTOR.equals(variableType)) {
-            val = application.getConnector(String.valueOf(value));
-        } else if (JsonEncoder.VTYPE_NULL.equals(variableType)) {
-            val = null;
+    public static <T> T decodeInternalOrCustomType(Type targetType,
+            JSONArray valueAndType, Application application)
+            throws JSONException {
+        if (isInternalType(targetType)) {
+            return decodeInternalType(targetType, false, valueAndType,
+                    application);
         } else {
-            // Try to decode object using fields
-            return decodeObject(variableType, (JSONObject) value, application);
+            return decodeCustomType(targetType, valueAndType, application);
+        }
+    }
 
+    public static <T> T decodeCustomType(Type targetType,
+            JSONArray valueAndType, Application application)
+            throws JSONException {
+        if (isInternalType(targetType)) {
+            throw new JSONException("decodeCustomType cannot be used for "
+                    + targetType + ", which is an internal type");
+        }
+        String transportType = getCustomTransportType(getClassForType(targetType));
+        String encodedTransportType = valueAndType.getString(0);
+        if (!encodedTransportType.equals(transportType)) {
+            throw new JSONException("Expected a value of type " + transportType
+                    + ", received " + encodedTransportType);
         }
 
-        return val;
+        // Try to decode object using fields
+        return decodeObject(targetType, (JSONObject) valueAndType.get(1),
+                application);
     }
 
-    private static Object decodeMap(JSONObject jsonMap, Application application)
-            throws JSONException {
+    /**
+     * 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 valueAndType
+     *            The encoded value and type array
+     * @param application
+     *            A reference to the application
+     * @param enforceGenericsInCollections
+     *            true if generics should be enforce, false to only allow
+     *            internal types in collections
+     * @return
+     * @throws JSONException
+     */
+    public static <T> T decodeInternalType(Type targetType,
+            boolean restrictToInternalTypes, JSONArray valueAndType,
+            Application application) throws JSONException {
+        if (!isInternalType(targetType)) {
+            throw new JSONException("Cannot decode internal type. Type "
+                    + targetType + " is not supported.");
+        }
+        String transportType = getInternalTransportType(targetType);
+        String encodedTransportType = valueAndType.getString(0);
+        if (!encodedTransportType.equals(transportType)) {
+            throw new JSONException("Expected a value of type " + transportType
+                    + ", received " + encodedTransportType);
+        }
+
+        Object encodedJsonValue = valueAndType.get(1);
+
+        // Collections
+        if (JsonEncoder.VTYPE_LIST.equals(transportType)) {
+            return (T) decodeList(targetType, restrictToInternalTypes,
+                    (JSONArray) encodedJsonValue, application);
+        } else if (JsonEncoder.VTYPE_SET.equals(transportType)) {
+            return (T) decodeSet(targetType, restrictToInternalTypes,
+                    (JSONArray) encodedJsonValue, application);
+        } else if (JsonEncoder.VTYPE_MAP_CONNECTOR.equals(transportType)) {
+            return (T) decodeConnectorToObjectMap(targetType,
+                    restrictToInternalTypes, (JSONObject) encodedJsonValue,
+                    application);
+        } else if (JsonEncoder.VTYPE_MAP.equals(transportType)) {
+            return (T) decodeStringToObjectMap(targetType,
+                    restrictToInternalTypes, (JSONObject) encodedJsonValue,
+                    application);
+        }
+
+        // Arrays
+        if (JsonEncoder.VTYPE_ARRAY.equals(transportType)) {
+
+            return (T) decodeObjectArray(targetType,
+                    (JSONArray) encodedJsonValue, application);
+
+        } else if (JsonEncoder.VTYPE_STRINGARRAY.equals(transportType)) {
+            return (T) decodeStringArray((JSONArray) encodedJsonValue);
+        }
+
+        // Special Vaadin types
+
+        String stringValue = String.valueOf(encodedJsonValue);
+
+        if (JsonEncoder.VTYPE_CONNECTOR.equals(transportType)) {
+            return (T) application.getConnector(stringValue);
+        }
+
+        // Standard Java types
+
+        if (JsonEncoder.VTYPE_STRING.equals(transportType)) {
+            return (T) stringValue;
+        } else if (JsonEncoder.VTYPE_INTEGER.equals(transportType)) {
+            return (T) Integer.valueOf(stringValue);
+        } else if (JsonEncoder.VTYPE_LONG.equals(transportType)) {
+            return (T) Long.valueOf(stringValue);
+        } else if (JsonEncoder.VTYPE_FLOAT.equals(transportType)) {
+            return (T) Float.valueOf(stringValue);
+        } else if (JsonEncoder.VTYPE_DOUBLE.equals(transportType)) {
+            return (T) Double.valueOf(stringValue);
+        } else if (JsonEncoder.VTYPE_BOOLEAN.equals(transportType)) {
+            return (T) Boolean.valueOf(stringValue);
+        } else if (JsonEncoder.VTYPE_NULL.equals(transportType)) {
+            return null;
+        }
+
+        throw new JSONException("Unknown type " + transportType);
+    }
+
+    @Deprecated
+    private static Map<String, Object> decodeStringToObjectMap(Type targetType,
+            boolean restrictToInternalTypes, JSONObject jsonMap,
+            Application application) throws JSONException {
         HashMap<String, Object> map = new HashMap<String, Object>();
         Iterator<String> it = jsonMap.keys();
         while (it.hasNext()) {
             String key = it.next();
-            map.put(key, decode(jsonMap.getJSONArray(key), application));
+            JSONArray encodedValueAndType = jsonMap.getJSONArray(key);
+            Object decodedChild;
+            if (!restrictToInternalTypes
+                    && targetType instanceof ParameterizedType) {
+                Type mapValueType = ((ParameterizedType) targetType)
+                        .getActualTypeArguments()[1];
+                // Only decode the given type
+                decodedChild = decodeInternalOrCustomType(mapValueType,
+                        encodedValueAndType, application);
+            } else {
+                // Only internal types when not enforcing a given type to avoid
+                // security issues
+                decodedChild = decodeInternalType(encodedValueAndType,
+                        application);
+            }
+            map.put(key, decodedChild);
         }
         return map;
     }
 
-    private static Object decodeConnectorMap(JSONObject jsonMap,
+    @Deprecated
+    private static Object decodeConnectorToObjectMap(Type targetType,
+            boolean restrictToInternalTypes, JSONObject jsonMap,
             Application application) throws JSONException {
         HashMap<Connector, Object> map = new HashMap<Connector, Object>();
         Iterator<String> it = jsonMap.keys();
         while (it.hasNext()) {
             String connectorId = it.next();
             Connector connector = application.getConnector(connectorId);
-            map.put(connector,
-                    decode(jsonMap.getJSONArray(connectorId), application));
+            JSONArray encodedValueAndType = jsonMap.getJSONArray(connectorId);
+            Object decodedChild;
+            if (!restrictToInternalTypes
+                    && targetType instanceof ParameterizedType) {
+                Type mapValueType = ((ParameterizedType) targetType)
+                        .getActualTypeArguments()[1];
+                // Only decode the given type
+                decodedChild = decodeInternalOrCustomType(mapValueType,
+                        encodedValueAndType, application);
+            } else {
+                // Only internal types when not enforcing a given type to avoid
+                // security issues
+                decodedChild = decodeInternalType(encodedValueAndType,
+                        application);
+            }
+            map.put(connector, decodedChild);
         }
         return map;
     }
@@ -161,43 +310,103 @@ public class JsonCodec implements Serializable {
         return tokens.toArray(new String[tokens.size()]);
     }
 
-    private static Object decodeArray(JSONArray jsonArray,
-            Application application) throws JSONException {
-        List list = decodeList(jsonArray, application);
+    private static Object[] decodeObjectArray(Type targetType,
+            JSONArray jsonArray, Application application) throws JSONException {
+        List list = decodeList(List.class, true, jsonArray, application);
         return list.toArray(new Object[list.size()]);
     }
 
-    private static List<Object> decodeList(JSONArray jsonArray,
+    private static List<Object> decodeList(Type targetType,
+            boolean restrictToInternalTypes, JSONArray jsonArray,
             Application application) throws JSONException {
         List<Object> list = new ArrayList<Object>();
         for (int i = 0; i < jsonArray.length(); ++i) {
             // each entry always has two elements: type and value
             JSONArray entryArray = jsonArray.getJSONArray(i);
-            list.add(decode(entryArray, application));
+            Object decodedChild;
+            if (!restrictToInternalTypes
+                    && targetType instanceof ParameterizedType) {
+                // Only decode the given type
+                Type childType = ((ParameterizedType) targetType)
+                        .getActualTypeArguments()[0];
+                decodedChild = decodeInternalOrCustomType(childType,
+                        entryArray, application);
+            } else {
+                // Only internal types when not enforcing a given type to avoid
+                // security issues
+                decodedChild = decodeInternalType(entryArray, application);
+            }
+            list.add(decodedChild);
         }
         return list;
     }
 
-    private static Set<Object> decodeSet(JSONArray jsonArray,
+    private static Set<Object> decodeSet(Type targetType,
+            boolean restrictToInternalTypes, JSONArray jsonArray,
             Application application) throws JSONException {
         HashSet<Object> set = new HashSet<Object>();
-        set.addAll(decodeList(jsonArray, application));
+        set.addAll(decodeList(List.class, restrictToInternalTypes, jsonArray,
+                application));
         return set;
     }
 
     /**
-     * Encode a value to a JSON representation for transport from the server to
-     * the client.
+     * Returns the name that should be used as field name in the JSON. We strip
+     * "set" from the setter, keeping the result - this is easy to do on both
+     * server and client, avoiding some issues with cASE. E.g setZIndex()
+     * becomes "ZIndex". Also ensures that both getter and setter are present,
+     * returning null otherwise.
      * 
-     * @param value
-     *            value to convert
-     * @param application
-     *            mapper between connector ID and {@link Connector} objects
-     * @return JSON representation of the value
-     * @throws JSONException
-     *             if encoding a value fails (e.g. NaN or infinite number)
+     * @param pd
+     * @return the name to be used or null if both getter and setter are not
+     *         found.
      */
-    public static JSONArray encode(Object value, Application application)
+    private static String getTransportFieldName(PropertyDescriptor pd) {
+        if (pd.getReadMethod() == null || pd.getWriteMethod() == null) {
+            return null;
+        }
+        return pd.getWriteMethod().getName().substring(3);
+    }
+
+    private static <T> T decodeObject(Type targetType,
+            JSONObject serializedObject, Application application)
+            throws JSONException {
+
+        Class<?> targetClass = getClassForType(targetType);
+        try {
+            Object decodedObject = targetClass.newInstance();
+            for (PropertyDescriptor pd : Introspector.getBeanInfo(targetClass)
+                    .getPropertyDescriptors()) {
+
+                String fieldName = getTransportFieldName(pd);
+                if (fieldName == null) {
+                    continue;
+                }
+                JSONArray encodedFieldValue = serializedObject
+                        .getJSONArray(fieldName);
+                Type fieldType = pd.getReadMethod().getGenericReturnType();
+                Object decodedFieldValue = decodeInternalOrCustomType(
+                        fieldType, encodedFieldValue, application);
+
+                pd.getWriteMethod().invoke(decodedObject, decodedFieldValue);
+            }
+
+            return (T) decodedObject;
+        } catch (IllegalArgumentException e) {
+            throw new JSONException(e);
+        } catch (IllegalAccessException e) {
+            throw new JSONException(e);
+        } catch (InvocationTargetException e) {
+            throw new JSONException(e);
+        } catch (InstantiationException e) {
+            throw new JSONException(e);
+        } catch (IntrospectionException e) {
+            throw new JSONException(e);
+        }
+    }
+
+    @Deprecated
+    private static JSONArray encode(Object value, Application application)
             throws JSONException {
         return encode(value, null, application);
     }
@@ -213,7 +422,7 @@ public class JsonCodec implements Serializable {
             valueType = value.getClass();
         }
 
-        String transportType = getTransportType(valueType);
+        String internalTransportType = getInternalTransportType(valueType);
         if (value instanceof String[]) {
             String[] array = (String[]) value;
             JSONArray jsonArray = new JSONArray();
@@ -226,16 +435,16 @@ public class JsonCodec implements Serializable {
         } else if (value instanceof Boolean) {
             return combineTypeAndValue(JsonEncoder.VTYPE_BOOLEAN, value);
         } else if (value instanceof Number) {
-            return combineTypeAndValue(transportType, value);
+            return combineTypeAndValue(internalTransportType, value);
         } else if (value instanceof Collection) {
-            if (transportType == null) {
+            if (internalTransportType == null) {
                 throw new RuntimeException(
                         "Unable to serialize unsupported type: " + valueType);
             }
             Collection<?> collection = (Collection<?>) value;
             JSONArray jsonArray = encodeCollection(collection, application);
 
-            return combineTypeAndValue(transportType, jsonArray);
+            return combineTypeAndValue(internalTransportType, jsonArray);
         } else if (value instanceof Object[]) {
             Object[] array = (Object[]) value;
             JSONArray jsonArray = encodeArrayContents(array, application);
@@ -260,12 +469,13 @@ public class JsonCodec implements Serializable {
             }
             return combineTypeAndValue(JsonEncoder.VTYPE_CONNECTOR,
                     connector.getConnectorId());
-        } else if (transportType != null) {
-            return combineTypeAndValue(transportType, String.valueOf(value));
+        } else if (internalTransportType != null) {
+            return combineTypeAndValue(internalTransportType,
+                    String.valueOf(value));
         } else {
             // Any object that we do not know how to encode we encode by looping
             // through fields
-            return combineTypeAndValue(valueType.getName(),
+            return combineTypeAndValue(getCustomTransportType(valueType),
                     encodeObject(value, application));
         }
     }
@@ -298,68 +508,11 @@ public class JsonCodec implements Serializable {
         return jsonMap;
     }
 
-    /**
-     * Returns the name that should be used as field name in the JSON. We strip
-     * "set" from the setter, keeping the result - this is easy to do on both
-     * server and client, avoiding some issues with cASE. E.g setZIndex()
-     * becomes "ZIndex". Also ensures that both getter and setter are present,
-     * returning null otherwise.
-     * 
-     * @param pd
-     * @return the name to be used or null if both getter and setter are not
-     *         found.
-     */
-    private static String getTransportFieldName(PropertyDescriptor pd) {
-        if (pd.getReadMethod() == null || pd.getWriteMethod() == null) {
-            return null;
-        }
-        return pd.getWriteMethod().getName().substring(3);
-    }
-
-    private static Object decodeObject(String type,
-            JSONObject serializedObject, Application application)
-            throws JSONException {
-
-        Class<?> cls;
-        try {
-            cls = Class.forName(type);
-
-            Object decodedObject = cls.newInstance();
-            for (PropertyDescriptor pd : Introspector.getBeanInfo(cls)
-                    .getPropertyDescriptors()) {
-
-                String fieldName = getTransportFieldName(pd);
-                if (fieldName == null) {
-                    continue;
-                }
-                JSONArray encodedObject = serializedObject
-                        .getJSONArray(fieldName);
-                pd.getWriteMethod().invoke(decodedObject,
-                        decode(encodedObject, application));
-            }
-
-            return decodedObject;
-        } catch (ClassNotFoundException e) {
-            throw new JSONException(e);
-        } catch (IllegalArgumentException e) {
-            throw new JSONException(e);
-        } catch (IllegalAccessException e) {
-            throw new JSONException(e);
-        } catch (InvocationTargetException e) {
-            throw new JSONException(e);
-        } catch (InstantiationException e) {
-            throw new JSONException(e);
-        } catch (IntrospectionException e) {
-            throw new JSONException(e);
-        }
-    }
-
     private static JSONArray encodeArrayContents(Object[] array,
             Application application) throws JSONException {
         JSONArray jsonArray = new JSONArray();
         for (Object o : array) {
-            // TODO handle object graph loops?
-            jsonArray.put(encode(o, application));
+            jsonArray.put(encode(o, null, application));
         }
         return jsonArray;
     }
@@ -368,7 +521,6 @@ public class JsonCodec implements Serializable {
             Application application) throws JSONException {
         JSONArray jsonArray = new JSONArray();
         for (Object o : collection) {
-            // TODO handle object graph loops?
             jsonArray.put(encode(o, application));
         }
         return jsonArray;
@@ -388,7 +540,7 @@ public class JsonCodec implements Serializable {
                         "Only maps with String/Connector keys are currently supported (#8602)");
             }
 
-            jsonMap.put((String) mapKey, encode(mapValue, application));
+            jsonMap.put((String) mapKey, encode(mapValue, null, application));
         }
         return jsonMap;
     }
@@ -404,21 +556,6 @@ public class JsonCodec implements Serializable {
         return outerArray;
     }
 
-    /**
-     * Gets the transport type for the value. Returns null if no transport type
-     * can be found.
-     * 
-     * @param value
-     * @return
-     * @throws JSONException
-     */
-    private static String getTransportType(Object value) {
-        if (null == value) {
-            return JsonEncoder.VTYPE_NULL;
-        }
-        return getTransportType(value.getClass());
-    }
-
     /**
      * Gets the transport type for the given class. Returns null if no transport
      * type can be found.
@@ -428,9 +565,12 @@ public class JsonCodec implements Serializable {
      * @return
      * @throws JSONException
      */
-    private static String getTransportType(Class<?> valueType) {
+    private static String getInternalTransportType(Type valueType) {
         return typeToTransportType.get(valueType);
+    }
 
+    private static String getCustomTransportType(Class<?> targetType) {
+        return targetType.getName();
     }
 
 }