From 71e30eb3ef4e3b57cdbfe51d65a3c76181554899 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Mon, 14 May 2012 00:49:15 +0300 Subject: [PATCH] Added support for map keys of any type (#8602) --- .../gwt/client/communication/JsonDecoder.java | 33 ++-- .../gwt/client/communication/JsonEncoder.java | 43 ++---- .../vaadin/terminal/gwt/server/JsonCodec.java | 87 ++++------- .../gwt/server/JSONSerializerTest.java | 146 ++++++++++++++++++ 4 files changed, 205 insertions(+), 104 deletions(-) create mode 100644 tests/client-side/com/vaadin/terminal/gwt/server/JSONSerializerTest.java diff --git a/src/com/vaadin/terminal/gwt/client/communication/JsonDecoder.java b/src/com/vaadin/terminal/gwt/client/communication/JsonDecoder.java index d7cf764f75..9ed20b6c79 100644 --- a/src/com/vaadin/terminal/gwt/client/communication/JsonDecoder.java +++ b/src/com/vaadin/terminal/gwt/client/communication/JsonDecoder.java @@ -14,10 +14,10 @@ import java.util.Set; import com.google.gwt.json.client.JSONArray; import com.google.gwt.json.client.JSONObject; +import com.google.gwt.json.client.JSONParser; import com.google.gwt.json.client.JSONString; import com.google.gwt.json.client.JSONValue; import com.vaadin.terminal.gwt.client.ApplicationConnection; -import com.vaadin.terminal.gwt.client.Connector; import com.vaadin.terminal.gwt.client.ConnectorMap; import com.vaadin.terminal.gwt.client.ServerConnector; @@ -64,8 +64,6 @@ public class JsonDecoder { val = decodeArray((JSONArray) value, idMapper, connection); } else if (JsonEncoder.VTYPE_MAP.equals(variableType)) { val = decodeMap((JSONObject) value, idMapper, connection); - } else if (JsonEncoder.VTYPE_MAP_CONNECTOR.equals(variableType)) { - val = decodeConnectorMap((JSONObject) value, idMapper, connection); } else if (JsonEncoder.VTYPE_LIST.equals(variableType)) { val = decodeList((JSONArray) value, idMapper, connection); } else if (JsonEncoder.VTYPE_SET.equals(variableType)) { @@ -111,30 +109,19 @@ public class JsonDecoder { return object; } - private static Map decodeMap(JSONObject jsonMap, + private static Map decodeMap(JSONObject jsonMap, ConnectorMap idMapper, ApplicationConnection connection) { - HashMap map = new HashMap(); + HashMap map = new HashMap(); Iterator it = jsonMap.keySet().iterator(); while (it.hasNext()) { String key = it.next(); - map.put(key, - decodeValue((JSONArray) jsonMap.get(key), null, idMapper, - connection)); - } - return map; - } - - private static Map decodeConnectorMap( - JSONObject jsonMap, ConnectorMap idMapper, - ApplicationConnection connection) { - HashMap map = new HashMap(); - Iterator it = jsonMap.keySet().iterator(); - while (it.hasNext()) { - String connectorId = it.next(); - Connector connector = idMapper.getConnector(connectorId); - map.put(connector, - decodeValue((JSONArray) jsonMap.get(connectorId), null, - idMapper, connection)); + JSONArray encodedKey = (JSONArray) JSONParser.parseStrict(key); + JSONArray encodedValue = (JSONArray) jsonMap.get(key); + Object decodedKey = decodeValue(encodedKey, null, idMapper, + connection); + Object decodedValue = decodeValue(encodedValue, null, idMapper, + connection); + map.put(decodedKey, decodedValue); } return map; } diff --git a/src/com/vaadin/terminal/gwt/client/communication/JsonEncoder.java b/src/com/vaadin/terminal/gwt/client/communication/JsonEncoder.java index 10b6f49a79..f09536a9f7 100644 --- a/src/com/vaadin/terminal/gwt/client/communication/JsonEncoder.java +++ b/src/com/vaadin/terminal/gwt/client/communication/JsonEncoder.java @@ -42,10 +42,6 @@ public class JsonEncoder { public static final String VTYPE_ARRAY = "a"; public static final String VTYPE_STRINGARRAY = "S"; public static final String VTYPE_MAP = "m"; - // Hack to support Map. Should be replaced by generic support - // for any object as key (#8602) - @Deprecated - public static final String VTYPE_MAP_CONNECTOR = "M"; public static final String VTYPE_LIST = "L"; public static final String VTYPE_SET = "q"; public static final String VTYPE_NULL = "n"; @@ -93,28 +89,8 @@ public class JsonEncoder { return encodeEnum(e, connectorMap, connection); } } else if (value instanceof Map) { - Map map = (Map) value; - JSONObject jsonMap = new JSONObject(); - String type = VTYPE_MAP; - for (Object mapKey : map.keySet()) { - Object mapValue = map.get(mapKey); - if (mapKey instanceof Connector) { - mapKey = ((Connector) mapKey).getConnectorId(); - type = VTYPE_MAP_CONNECTOR; - } - - if (!(mapKey instanceof String)) { - throw new RuntimeException( - "Only Map and Map is currently supported." - + " Failed map used " - + mapKey.getClass().getName() + " as keys"); - } - jsonMap.put( - (String) mapKey, - encode(mapValue, restrictToInternalTypes, connectorMap, - connection)); - } - return combineTypeAndValue(type, jsonMap); + return encodeMap((Map) value, restrictToInternalTypes, + connectorMap, connection); } else if (value instanceof Connector) { Connector connector = (Connector) value; return combineTypeAndValue(VTYPE_CONNECTOR, new JSONString( @@ -141,6 +117,21 @@ public class JsonEncoder { } } + private static JSONValue encodeMap(Map map, + boolean restrictToInternalTypes, ConnectorMap connectorMap, + ApplicationConnection connection) { + JSONObject jsonMap = new JSONObject(); + for (Object mapKey : map.keySet()) { + Object mapValue = map.get(mapKey); + JSONValue encodedKey = encode(mapKey, restrictToInternalTypes, + connectorMap, connection); + JSONValue encodedValue = encode(mapValue, restrictToInternalTypes, + connectorMap, connection); + jsonMap.put(encodedKey.toString(), encodedValue); + } + return combineTypeAndValue(VTYPE_MAP, jsonMap); + } + private static JSONValue encodeEnum(Enum e, ConnectorMap connectorMap, ApplicationConnection connection) { return combineTypeAndValue(e.getClass().getName(), diff --git a/src/com/vaadin/terminal/gwt/server/JsonCodec.java b/src/com/vaadin/terminal/gwt/server/JsonCodec.java index ed2bf66ced..e082eca47e 100644 --- a/src/com/vaadin/terminal/gwt/server/JsonCodec.java +++ b/src/com/vaadin/terminal/gwt/server/JsonCodec.java @@ -61,6 +61,7 @@ public class JsonCodec implements Serializable { registerType(String[].class, JsonEncoder.VTYPE_STRINGARRAY); registerType(Object[].class, JsonEncoder.VTYPE_ARRAY); registerType(Map.class, JsonEncoder.VTYPE_MAP); + registerType(HashMap.class, JsonEncoder.VTYPE_MAP); registerType(List.class, JsonEncoder.VTYPE_LIST); registerType(Set.class, JsonEncoder.VTYPE_SET); } @@ -201,12 +202,8 @@ public class JsonCodec implements Serializable { } else if (JsonEncoder.VTYPE_SET.equals(transportType)) { return decodeSet(targetType, restrictToInternalTypes, (JSONArray) encodedJsonValue, application); - } else if (JsonEncoder.VTYPE_MAP_CONNECTOR.equals(transportType)) { - return decodeConnectorToObjectMap(targetType, - restrictToInternalTypes, (JSONObject) encodedJsonValue, - application); } else if (JsonEncoder.VTYPE_MAP.equals(transportType)) { - return decodeStringToObjectMap(targetType, restrictToInternalTypes, + return decodeMap(targetType, restrictToInternalTypes, (JSONObject) encodedJsonValue, application); } @@ -262,37 +259,22 @@ public class JsonCodec implements Serializable { return false; } - @Deprecated - private static Map decodeStringToObjectMap(Type targetType, + private static Map decodeMap(Type targetType, boolean restrictToInternalTypes, JSONObject jsonMap, Application application) throws JSONException { - HashMap map = new HashMap(); + HashMap map = new HashMap(); + Iterator it = jsonMap.keys(); while (it.hasNext()) { String key = it.next(); - JSONArray encodedValueAndType = jsonMap.getJSONArray(key); - Object decodedChild = decodeChild(targetType, - restrictToInternalTypes, 1, encodedValueAndType, - application); - map.put(key, decodedChild); - } - return map; - } + JSONArray encodedKey = new JSONArray(key); + JSONArray encodedValue = jsonMap.getJSONArray(key); - @Deprecated - private static Map 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); - JSONArray encodedValueAndType = jsonMap.getJSONArray(connectorId); - Object decodedChild = decodeChild(targetType, - restrictToInternalTypes, 1, encodedValueAndType, - application); - map.put(connector, decodedChild); + Object decodedKey = decodeParametrizedType(targetType, + restrictToInternalTypes, 0, encodedKey, application); + Object decodedValue = decodeParametrizedType(targetType, + restrictToInternalTypes, 1, encodedValue, application); + map.put(decodedKey, decodedValue); } return map; } @@ -308,7 +290,7 @@ public class JsonCodec implements Serializable { * @return * @throws JSONException */ - private static Object decodeChild(Type targetType, + private static Object decodeParametrizedType(Type targetType, boolean restrictToInternalTypes, int typeIndex, JSONArray encodedValueAndType, Application application) throws JSONException { @@ -353,7 +335,7 @@ public class JsonCodec implements Serializable { for (int i = 0; i < jsonArray.length(); ++i) { // each entry always has two elements: type and value JSONArray encodedValueAndType = jsonArray.getJSONArray(i); - Object decodedChild = decodeChild(targetType, + Object decodedChild = decodeParametrizedType(targetType, restrictToInternalTypes, 0, encodedValueAndType, application); list.add(decodedChild); @@ -381,7 +363,7 @@ public class JsonCodec implements Serializable { * @return the name to be used or null if both getter and setter are not * found. */ - private static String getTransportFieldName(PropertyDescriptor pd) { + static String getTransportFieldName(PropertyDescriptor pd) { if (pd.getReadMethod() == null || pd.getWriteMethod() == null) { return null; } @@ -476,16 +458,9 @@ public class JsonCodec implements Serializable { JSONArray jsonArray = encodeArrayContents(array, application); return combineTypeAndValue(JsonEncoder.VTYPE_ARRAY, jsonArray); } else if (value instanceof Map) { - Map map = (Map) value; - JSONObject jsonMap = encodeMapContents(map, application); - // Hack to support Connector as map key. Should be fixed by # - if (!map.isEmpty() - && map.keySet().iterator().next() instanceof Connector) { - return combineTypeAndValue(JsonEncoder.VTYPE_MAP_CONNECTOR, - jsonMap); - } else { - return combineTypeAndValue(JsonEncoder.VTYPE_MAP, jsonMap); - } + JSONObject jsonMap = encodeMap(valueType, (Map) value, + application); + return combineTypeAndValue(JsonEncoder.VTYPE_MAP, jsonMap); } else if (value instanceof Connector) { Connector connector = (Connector) value; if (value instanceof Component @@ -611,22 +586,24 @@ public class JsonCodec implements Serializable { } } - private static JSONObject encodeMapContents(Map map, + private static JSONObject encodeMap(Type mapType, Map map, Application application) throws JSONException { + 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"); + } + JSONObject jsonMap = new JSONObject(); for (Object mapKey : map.keySet()) { Object mapValue = map.get(mapKey); - - if (mapKey instanceof ClientConnector) { - mapKey = ((ClientConnector) mapKey).getConnectorId(); - } - if (!(mapKey instanceof String)) { - throw new JSONException( - "Only maps with String/Connector keys are currently supported (#8602)"); - } - - jsonMap.put((String) mapKey, - encode(mapValue, null, null, application)); + JSONArray encodedKey = encode(mapKey, null, keyType, application); + JSONArray encodedValue = encode(mapValue, null, valueType, + application); + jsonMap.put(encodedKey.toString(), encodedValue); } return jsonMap; } diff --git a/tests/client-side/com/vaadin/terminal/gwt/server/JSONSerializerTest.java b/tests/client-side/com/vaadin/terminal/gwt/server/JSONSerializerTest.java new file mode 100644 index 0000000000..926f026b40 --- /dev/null +++ b/tests/client-side/com/vaadin/terminal/gwt/server/JSONSerializerTest.java @@ -0,0 +1,146 @@ +package com.vaadin.terminal.gwt.server; + +/* + @VaadinApache2LicenseForJavaFiles@ + */ +import java.beans.BeanInfo; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import junit.framework.TestCase; + +import com.vaadin.external.json.JSONArray; +import com.vaadin.terminal.gwt.client.communication.JsonDecoder; +import com.vaadin.terminal.gwt.client.communication.JsonEncoder; +import com.vaadin.terminal.gwt.client.ui.splitpanel.AbstractSplitPanelState; + +/** + * Tests for {@link JsonCodec}, {@link JsonEncoder}, {@link JsonDecoder} + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0 + * + */ +public class JSONSerializerTest extends TestCase { + HashMap stringToStateMap; + HashMap stateToStringMap; + + public void testStringToBeanMapSerialization() throws Exception { + Type mapType = getClass().getDeclaredField("stringToStateMap") + .getGenericType(); + stringToStateMap = new HashMap(); + AbstractSplitPanelState s = new AbstractSplitPanelState(); + AbstractSplitPanelState s2 = new AbstractSplitPanelState(); + s.setCaption("State 1"); + s.setDebugId("foo"); + s2.setCaption("State 2"); + s2.setDebugId("bar"); + stringToStateMap.put("string - state 1", s); + stringToStateMap.put("String - state 2", s2); + + JSONArray encodedMap = JsonCodec.encode(stringToStateMap, null, + mapType, null); + + ensureDecodedCorrectly(stringToStateMap, encodedMap, mapType); + } + + public void testBeanToStringMapSerialization() throws Exception { + Type mapType = getClass().getDeclaredField("stateToStringMap") + .getGenericType(); + stateToStringMap = new HashMap(); + AbstractSplitPanelState s = new AbstractSplitPanelState(); + AbstractSplitPanelState s2 = new AbstractSplitPanelState(); + s.setCaption("State 1"); + s2.setCaption("State 2"); + stateToStringMap.put(s, "string - state 1"); + stateToStringMap.put(s2, "String - state 2"); + + JSONArray encodedMap = JsonCodec.encode(stateToStringMap, null, + mapType, null); + + ensureDecodedCorrectly(stateToStringMap, encodedMap, mapType); + } + + private void ensureDecodedCorrectly(Object original, JSONArray encoded, + Type type) throws Exception { + Object serverSideDecoded = JsonCodec.decodeInternalOrCustomType(type, + encoded, null); + assertTrue("Server decoded", equals(original, serverSideDecoded)); + + // Object clientSideDecoded = JsonDecoder.decodeValue( + // (com.google.gwt.json.client.JSONArray) JSONParser + // .parseStrict(encoded.toString()), null, null, null); + // assertTrue("Client decoded", + // equals(original, clientSideDecoded)); + + } + + private boolean equals(Object o1, Object o2) throws Exception { + if (o1 == null) { + return (o2 == null); + } + if (o2 == null) { + return false; + } + + if (o1 instanceof Map) { + if (!(o2 instanceof Map)) { + return false; + } + return equalsMap((Map) o1, (Map) o2); + } + + if (o1.getClass() != o2.getClass()) { + return false; + } + + if (o1 instanceof Collection || o1 instanceof Number + || o1 instanceof String) { + return o1.equals(o2); + } + + return equalsBean(o1, o2); + } + + private boolean equalsBean(Object o1, Object o2) throws Exception { + BeanInfo beanInfo = Introspector.getBeanInfo(o1.getClass()); + for (PropertyDescriptor pd : beanInfo.getPropertyDescriptors()) { + String fieldName = JsonCodec.getTransportFieldName(pd); + if (fieldName == null) { + continue; + } + + Object c1 = pd.getReadMethod().invoke(o1); + Object c2 = pd.getReadMethod().invoke(o2); + if (!equals(c1, c2)) { + return false; + } + } + return true; + } + + private boolean equalsMap(Map o1, Map o2) throws Exception { + for (Object key1 : o1.keySet()) { + Object key2 = key1; + if (!(o2.containsKey(key2))) { + // Try to fins a key that is equal + for (Object k2 : o2.keySet()) { + if (equals(key1, k2)) { + key2 = k2; + break; + } + } + } + if (!equals(o1.get(key1), o2.get(key2))) { + return false; + } + + } + return true; + } +} -- 2.39.5