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;
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);
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);
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;
}
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);
}
valueType = value.getClass();
}
- String transportType = getTransportType(valueType);
+ String internalTransportType = getInternalTransportType(valueType);
if (value instanceof String[]) {
String[] array = (String[]) value;
JSONArray jsonArray = new JSONArray();
} 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);
}
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));
}
}
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;
}
Application application) throws JSONException {
JSONArray jsonArray = new JSONArray();
for (Object o : collection) {
- // TODO handle object graph loops?
jsonArray.put(encode(o, application));
}
return jsonArray;
"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;
}
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.
* @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();
}
}