From 6996029d9d521db35f70c0a017e15cb4bdcf4a78 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Wed, 18 Apr 2012 01:02:48 +0300 Subject: [PATCH] Refactored JsonCodec to use a given type for deserialization (#8667) --- .../vaadin/terminal/gwt/server/JsonCodec.java | 456 ++++++++++++------ 1 file changed, 298 insertions(+), 158 deletions(-) diff --git a/src/com/vaadin/terminal/gwt/server/JsonCodec.java b/src/com/vaadin/terminal/gwt/server/JsonCodec.java index 8da8fdc933..0d2b0c2082 100644 --- a/src/com/vaadin/terminal/gwt/server/JsonCodec.java +++ b/src/com/vaadin/terminal/gwt/server/JsonCodec.java @@ -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, String> typeToTransportType = new HashMap, String>(); + /** + * Note! This does not contain primitives. + *

+ */ + private static Map> transportTypeToType = new HashMap>(); + 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 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 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. + *

+ * 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 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 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 decodeStringToObjectMap(Type targetType, + boolean restrictToInternalTypes, JSONObject jsonMap, + Application application) throws JSONException { HashMap map = new HashMap(); Iterator 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 map = new HashMap(); Iterator 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 decodeList(JSONArray jsonArray, + private static List decodeList(Type targetType, + boolean restrictToInternalTypes, JSONArray jsonArray, Application application) throws JSONException { List list = new ArrayList(); 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 decodeSet(JSONArray jsonArray, + private static Set decodeSet(Type targetType, + boolean restrictToInternalTypes, JSONArray jsonArray, Application application) throws JSONException { HashSet set = new HashSet(); - 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 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(); } } -- 2.39.5