diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java index 17eefe2c..34300cb2 100644 --- a/gson/src/main/java/com/google/gson/Gson.java +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -170,8 +170,8 @@ public final class Gson { this(DEFAULT_EXCLUSION_STRATEGY, DEFAULT_EXCLUSION_STRATEGY, DEFAULT_NAMING_POLICY, DefaultTypeAdapters.getDefaultInstanceCreators(), false, DefaultTypeAdapters.getAllDefaultSerializers(), - DefaultTypeAdapters.getAllDefaultDeserializers(), DEFAULT_JSON_NON_EXECUTABLE, true, false, - false, LongSerializationPolicy.DEFAULT); + DefaultTypeAdapters.getAllDefaultDeserializers(), false, DEFAULT_JSON_NON_EXECUTABLE, true, + false, false, LongSerializationPolicy.DEFAULT); } Gson(final ExclusionStrategy deserializationExclusionStrategy, @@ -180,8 +180,9 @@ public final class Gson { final ParameterizedTypeHandlerMap> instanceCreators, boolean serializeNulls, final ParameterizedTypeHandlerMap> serializers, final ParameterizedTypeHandlerMap> deserializers, - boolean generateNonExecutableGson, boolean htmlSafe, boolean prettyPrinting, - boolean serializeSpecialFloatingPointValues, LongSerializationPolicy longSerializationPolicy) { + boolean complexMapKeySerialization, boolean generateNonExecutableGson, boolean htmlSafe, + boolean prettyPrinting, boolean serializeSpecialFloatingPointValues, + LongSerializationPolicy longSerializationPolicy) { this.deserializationExclusionStrategy = deserializationExclusionStrategy; this.serializationExclusionStrategy = serializationExclusionStrategy; this.fieldNamingPolicy = fieldNamingPolicy; @@ -245,12 +246,12 @@ public final class Gson { .factory(TypeAdapters.INET_ADDRESS_FACTORY) .typeAdapter(BigDecimal.class, new BigDecimalTypeAdapter()) .typeAdapter(BigInteger.class, new BigIntegerTypeAdapter()) - .factory(new MapTypeAdapterFactory(constructorConstructor)) .factory(new CollectionTypeAdapterFactory(constructorConstructor)) .factory(ObjectTypeAdapter.FACTORY) .factory(new GsonToMiniGsonTypeAdapterFactory(serializers, deserializers, new JsonDeserializationContext(this), new JsonSerializationContext(this), serializeNulls )) + .factory(new MapTypeAdapterFactory(constructorConstructor, complexMapKeySerialization)) .factory(ArrayTypeAdapter.FACTORY) .factory(reflectiveTypeAdapterFactory); diff --git a/gson/src/main/java/com/google/gson/GsonBuilder.java b/gson/src/main/java/com/google/gson/GsonBuilder.java index 09f580c0..113346c9 100644 --- a/gson/src/main/java/com/google/gson/GsonBuilder.java +++ b/gson/src/main/java/com/google/gson/GsonBuilder.java @@ -28,7 +28,6 @@ import java.util.Date; import java.util.HashSet; import java.util.LinkedList; import java.util.List; -import java.util.Map; import java.util.Set; /** @@ -65,8 +64,6 @@ import java.util.Set; * @author Joel Leitch */ public final class GsonBuilder { - private static final MapAsArrayTypeAdapter COMPLEX_KEY_MAP_TYPE_ADAPTER = - new MapAsArrayTypeAdapter(); private static final InnerClassExclusionStrategy innerClassExclusionStrategy = new InnerClassExclusionStrategy(); private static final ExposeAnnotationDeserializationExclusionStrategy @@ -94,6 +91,7 @@ public final class GsonBuilder { private String datePattern; private int dateStyle; private int timeStyle; + private boolean complexMapKeySerialization = false; private boolean serializeSpecialFloatingPointValues; private boolean escapeHtmlChars; private boolean prettyPrinting; @@ -273,7 +271,7 @@ public final class GsonBuilder { * @since 1.7 */ public GsonBuilder enableComplexMapKeySerialization() { - registerTypeHierarchyAdapter(Map.class, COMPLEX_KEY_MAP_TYPE_ADAPTER); + complexMapKeySerialization = true; return this; } @@ -694,8 +692,9 @@ public final class GsonBuilder { return new Gson(new DisjunctionExclusionStrategy(deserializationStrategies), new DisjunctionExclusionStrategy(serializationStrategies), fieldNamingPolicy, instanceCreators, serializeNulls, - customSerializers, customDeserializers, generateNonExecutableJson, escapeHtmlChars, - prettyPrinting, serializeSpecialFloatingPointValues, longSerializationPolicy); + customSerializers, customDeserializers, complexMapKeySerialization, + generateNonExecutableJson, escapeHtmlChars, prettyPrinting, + serializeSpecialFloatingPointValues, longSerializationPolicy); } private static void addTypeAdaptersForDate(String datePattern, int dateStyle, int timeStyle, diff --git a/gson/src/main/java/com/google/gson/MapAsArrayTypeAdapter.java b/gson/src/main/java/com/google/gson/MapAsArrayTypeAdapter.java deleted file mode 100644 index 6d2a2511..00000000 --- a/gson/src/main/java/com/google/gson/MapAsArrayTypeAdapter.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (C) 2010 Google Inc. - * - * 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.google.gson; - -import java.lang.reflect.Type; -import java.util.Map; - -/** - * Adapts maps containing complex keys as arrays of map entries. - * - *

Maps as JSON objects

- * The standard GSON map type adapter converts Java {@link Map Maps} to JSON - * Objects. This requires that map keys can be serialized as strings; this is - * insufficient for some key types. For example, consider a map whose keys are - * points on a grid. The default JSON form encodes reasonably:
   {@code
- *   Map original = new LinkedHashMap();
- *   original.put(new Point(5, 6), "a");
- *   original.put(new Point(8, 8), "b");
- *   System.out.println(gson.toJson(original, type));
- * }
- * The above code prints this JSON object:
   {@code
- *   {
- *     "(5,6)": "a",
- *     "(8,8)": "b"
- *   }
- * }
- * But GSON is unable to deserialize this value because the JSON string name is - * just the {@link Object#toString() toString()} of the map key. Attempting to - * convert the above JSON to an object fails with a parse exception: - *
com.google.gson.JsonParseException: Expecting object found: "(5,6)"
- *   at com.google.gson.JsonObjectDeserializationVisitor.visitFieldUsingCustomHandler
- *   at com.google.gson.ObjectNavigator.navigateClassFields
- *   ...
- * - *

Maps as JSON arrays

- * An alternative approach taken by this type adapter is to encode maps as - * arrays of map entries. Each map entry is a two element array containing a key - * and a value. This approach is more flexible because any type can be used as - * the map's key; not just strings. But it's also less portable because the - * receiver of such JSON must be aware of the map entry convention. - * - *

Register this adapter when you are creating your GSON instance. - *

   {@code
- *   Gson gson = new GsonBuilder()
- *     .registerTypeAdapter(Map.class, new MapAsArrayTypeAdapter())
- *     .create();
- * }
- * This will change the structure of the JSON emitted by the code above. Now we - * get an array. In this case the arrays elements are map entries: - *
   {@code
- *   [
- *     [
- *       {
- *         "x": 5,
- *         "y": 6
- *       },
- *       "a",
- *     ],
- *     [
- *       {
- *         "x": 8,
- *         "y": 8
- *       },
- *       "b"
- *     ]
- *   ]
- * }
- * This format will serialize and deserialize just fine as long as this adapter - * is registered. - * - *

This adapter returns regular JSON objects for maps whose keys are not - * complex. A key is complex if its JSON-serialized form is an array or an - * object. - */ -final class MapAsArrayTypeAdapter - implements JsonSerializer>, JsonDeserializer> { - - public Map deserialize(JsonElement json, Type typeOfT, - JsonDeserializationContext context) throws JsonParseException { - // TODO - throw new UnsupportedOperationException(); - } - - public JsonElement serialize(Map src, Type typeOfSrc, JsonSerializationContext context) { - // TODO - throw new UnsupportedOperationException(); - } -} diff --git a/gson/src/main/java/com/google/gson/internal/$Gson$Types.java b/gson/src/main/java/com/google/gson/internal/$Gson$Types.java index 9fc70ed6..ed69376d 100644 --- a/gson/src/main/java/com/google/gson/internal/$Gson$Types.java +++ b/gson/src/main/java/com/google/gson/internal/$Gson$Types.java @@ -298,7 +298,7 @@ public final class $Gson$Types { */ public static Type getCollectionElementType(Type context, Class contextRawType) { Type collectionType = getSupertype(context, contextRawType, Collection.class); - + if (collectionType instanceof WildcardType) { collectionType = ((WildcardType)collectionType).getUpperBounds()[0]; } @@ -323,8 +323,12 @@ public final class $Gson$Types { } Type mapType = getSupertype(context, contextRawType, Map.class); - ParameterizedType mapParameterizedType = (ParameterizedType) mapType; - return mapParameterizedType.getActualTypeArguments(); + // TODO: strip wildcards? + if (mapType instanceof ParameterizedType) { + ParameterizedType mapParameterizedType = (ParameterizedType) mapType; + return mapParameterizedType.getActualTypeArguments(); + } + return new Type[] { Object.class, Object.class }; } @SuppressWarnings("unchecked") diff --git a/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java index c8e6c664..4cb89a7e 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java +++ b/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java @@ -16,25 +16,96 @@ package com.google.gson.internal.bind; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSyntaxException; import com.google.gson.internal.$Gson$Types; import com.google.gson.internal.ConstructorConstructor; import com.google.gson.internal.ObjectConstructor; +import com.google.gson.internal.Streams; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; import java.io.IOException; import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; import java.util.Map; /** - * Adapt a map whose keys are any type. + * Adapts maps to either JSON objects or JSON arrays. + * + *

Maps as JSON objects

+ * For primitive keys or when complex map key serialization is not enabled, this + * converts Java {@link Map Maps} to JSON Objects. This requires that map keys + * can be serialized as strings; this is insufficient for some key types. For + * example, consider a map whose keys are points on a grid. The default JSON + * form encodes reasonably:
   {@code
+ *   Map original = new LinkedHashMap();
+ *   original.put(new Point(5, 6), "a");
+ *   original.put(new Point(8, 8), "b");
+ *   System.out.println(gson.toJson(original, type));
+ * }
+ * The above code prints this JSON object:
   {@code
+ *   {
+ *     "(5,6)": "a",
+ *     "(8,8)": "b"
+ *   }
+ * }
+ * But GSON is unable to deserialize this value because the JSON string name is + * just the {@link Object#toString() toString()} of the map key. Attempting to + * convert the above JSON to an object fails with a parse exception: + *
com.google.gson.JsonParseException: Expecting object found: "(5,6)"
+ *   at com.google.gson.JsonObjectDeserializationVisitor.visitFieldUsingCustomHandler
+ *   at com.google.gson.ObjectNavigator.navigateClassFields
+ *   ...
+ * + *

Maps as JSON arrays

+ * An alternative approach taken by this type adapter when it is required and + * complex map key serialization is enabled is to encode maps as arrays of map + * entries. Each map entry is a two element array containing a key and a value. + * This approach is more flexible because any type can be used as the map's key; + * not just strings. But it's also less portable because the receiver of such + * JSON must be aware of the map entry convention. + * + *

Register this adapter when you are creating your GSON instance. + *

   {@code
+ *   Gson gson = new GsonBuilder()
+ *     .registerTypeAdapter(Map.class, new MapAsArrayTypeAdapter())
+ *     .create();
+ * }
+ * This will change the structure of the JSON emitted by the code above. Now we + * get an array. In this case the arrays elements are map entries: + *
   {@code
+ *   [
+ *     [
+ *       {
+ *         "x": 5,
+ *         "y": 6
+ *       },
+ *       "a",
+ *     ],
+ *     [
+ *       {
+ *         "x": 8,
+ *         "y": 8
+ *       },
+ *       "b"
+ *     ]
+ *   ]
+ * }
+ * This format will serialize and deserialize just fine as long as this adapter + * is registered. */ public final class MapTypeAdapterFactory implements TypeAdapter.Factory { private final ConstructorConstructor constructorConstructor; + private final boolean complexMapKeySerialization; - public MapTypeAdapterFactory(ConstructorConstructor constructorConstructor) { + public MapTypeAdapterFactory(ConstructorConstructor constructorConstructor, + boolean complexMapKeySerialization) { this.constructorConstructor = constructorConstructor; + this.complexMapKeySerialization = complexMapKeySerialization; } public TypeAdapter create(MiniGson context, TypeToken typeToken) { @@ -46,56 +117,120 @@ public final class MapTypeAdapterFactory implements TypeAdapter.Factory { } Class rawTypeOfSrc = $Gson$Types.getRawType(type); - Type childGenericType = $Gson$Types.getMapKeyAndValueTypes(type, rawTypeOfSrc)[1]; - TypeAdapter valueAdapter = context.getAdapter(TypeToken.get(childGenericType)); + Type[] keyAndValueTypes = $Gson$Types.getMapKeyAndValueTypes(type, rawTypeOfSrc); + TypeAdapter keyAdapter = context.getAdapter(TypeToken.get(keyAndValueTypes[0])); + TypeAdapter valueAdapter = context.getAdapter(TypeToken.get(keyAndValueTypes[1])); ObjectConstructor constructor = constructorConstructor.getConstructor(typeToken); @SuppressWarnings("unchecked") // we don't define a type parameter for the key or value types - TypeAdapter result = new Adapter(valueAdapter, constructor); + TypeAdapter result = new Adapter(keyAdapter, valueAdapter, constructor); return result; } - private final class Adapter extends TypeAdapter> { + private final class Adapter extends TypeAdapter> { + private final TypeAdapter keyTypeAdapter; private final TypeAdapter valueTypeAdapter; - private final ObjectConstructor> constructor; + private final ObjectConstructor> constructor; - public Adapter(TypeAdapter valueTypeAdapter, - ObjectConstructor> constructor) { + public Adapter(TypeAdapter keyTypeAdapter, TypeAdapter valueTypeAdapter, + ObjectConstructor> constructor) { + this.keyTypeAdapter = keyTypeAdapter; this.valueTypeAdapter = valueTypeAdapter; this.constructor = constructor; } - public Map read(JsonReader reader) throws IOException { - if (reader.peek() == JsonToken.NULL) { + public Map read(JsonReader reader) throws IOException { + JsonToken peek = reader.peek(); + if (peek == JsonToken.NULL) { reader.nextNull(); // TODO: does this belong here? return null; } - Map map = constructor.construct(); + Map map = constructor.construct(); - reader.beginObject(); - while (reader.hasNext()) { - String key = reader.nextName(); - V value = valueTypeAdapter.read(reader); - map.put(key, value); // TODO: convert to the map's key type? + if (peek == JsonToken.BEGIN_ARRAY) { + reader.beginArray(); + while (reader.hasNext()) { + reader.beginArray(); // entry array + K key = keyTypeAdapter.read(reader); + V value = valueTypeAdapter.read(reader); + V replaced = map.put(key, value); + if (replaced != null) { + throw new JsonSyntaxException("duplicate key: " + key); + } + reader.endArray(); + } + reader.endArray(); + } else { + reader.beginObject(); + while (reader.hasNext()) { + String keyString = reader.nextName(); + K key = keyTypeAdapter.fromJsonElement(new JsonPrimitive(keyString)); + V value = valueTypeAdapter.read(reader); + V replaced = map.put(key, value); + if (replaced != null) { + throw new JsonSyntaxException("duplicate key: " + key); + } + } + reader.endObject(); } - reader.endObject(); return map; } - public void write(JsonWriter writer, Map map) throws IOException { + public void write(JsonWriter writer, Map map) throws IOException { if (map == null) { writer.nullValue(); // TODO: better policy here? return; } - writer.beginObject(); - for (Map.Entry entry : map.entrySet()) { - String key = String.valueOf(entry.getKey()); - writer.name(key); - valueTypeAdapter.write(writer, entry.getValue()); + boolean hasComplexKeys = false; + List keys = new ArrayList(map.size()); + + List values = new ArrayList(map.size()); + for (Map.Entry entry : map.entrySet()) { + JsonElement keyElement = keyTypeAdapter.toJsonElement(entry.getKey()); + keys.add(keyElement); + values.add(entry.getValue()); + hasComplexKeys |= keyElement.isJsonArray() || keyElement.isJsonObject(); + } + + if (complexMapKeySerialization && hasComplexKeys) { + writer.beginArray(); + for (int i = 0; i < keys.size(); i++) { + writer.beginArray(); // entry array + Streams.write(keys.get(i), true, writer); + valueTypeAdapter.write(writer, values.get(i)); + writer.endArray(); + } + writer.endArray(); + } else { + writer.beginObject(); + for (int i = 0; i < keys.size(); i++) { + JsonElement keyElement = keys.get(i); + writer.name(keyToString(keyElement)); + valueTypeAdapter.write(writer, values.get(i)); + } + writer.endObject(); + } + } + + private String keyToString(JsonElement keyElement) { + if (keyElement.isJsonPrimitive()) { + JsonPrimitive primitive = keyElement.getAsJsonPrimitive(); + if (primitive.isNumber()) { + return String.valueOf(primitive.getAsNumber()); + } else if (primitive.isBoolean()) { + return Boolean.toString(primitive.getAsBoolean()); + } else if (primitive.isString()) { + return primitive.getAsString(); + } else { + throw new AssertionError(); + } + } else if (keyElement.isJsonNull()) { + return "null"; + } else { + throw new AssertionError(); } - writer.endObject(); } } } diff --git a/gson/src/test/java/com/google/gson/FunctionWithInternalDependenciesTest.java b/gson/src/test/java/com/google/gson/FunctionWithInternalDependenciesTest.java index 6c197eb4..144bb7d6 100644 --- a/gson/src/test/java/com/google/gson/FunctionWithInternalDependenciesTest.java +++ b/gson/src/test/java/com/google/gson/FunctionWithInternalDependenciesTest.java @@ -39,8 +39,8 @@ public class FunctionWithInternalDependenciesTest extends TestCase { Gson gson = new Gson(exclusionStrategy, exclusionStrategy, Gson.DEFAULT_NAMING_POLICY, DefaultTypeAdapters.getDefaultInstanceCreators(), false, DefaultTypeAdapters.getDefaultSerializers(), - DefaultTypeAdapters.getDefaultDeserializers(), Gson.DEFAULT_JSON_NON_EXECUTABLE, true, - false, false, LongSerializationPolicy.DEFAULT); + DefaultTypeAdapters.getDefaultDeserializers(), false, Gson.DEFAULT_JSON_NON_EXECUTABLE, + true, false, false, LongSerializationPolicy.DEFAULT); assertEquals("{}", gson.toJson(new ClassWithNoFields() { // empty anonymous class })); diff --git a/gson/src/test/java/com/google/gson/functional/MapAsArrayTypeAdapterTest.java b/gson/src/test/java/com/google/gson/functional/MapAsArrayTypeAdapterTest.java index a2d4bdaf..c7cfcdf9 100644 --- a/gson/src/test/java/com/google/gson/functional/MapAsArrayTypeAdapterTest.java +++ b/gson/src/test/java/com/google/gson/functional/MapAsArrayTypeAdapterTest.java @@ -53,7 +53,7 @@ public class MapAsArrayTypeAdapterTest extends TestCase { new TypeToken>() {}.getType())); } - public void testTwoTypesCollapseToOneSerialize() { + public void disabled_testTwoTypesCollapseToOneSerialize() { Gson gson = new GsonBuilder() .enableComplexMapKeySerialization() .create(); @@ -63,7 +63,7 @@ public class MapAsArrayTypeAdapterTest extends TestCase { original.put(new Float(1.0), "b"); try { gson.toJson(original, new TypeToken>() {}.getType()); - fail(); + fail(); // we no longer hash keys at serialization time } catch (JsonSyntaxException expected) { } } diff --git a/gson/src/test/java/com/google/gson/internal/ParameterizedTypeHandlerMapTest.java b/gson/src/test/java/com/google/gson/internal/ParameterizedTypeHandlerMapTest.java index 1b7b4f76..7a6a9692 100644 --- a/gson/src/test/java/com/google/gson/internal/ParameterizedTypeHandlerMapTest.java +++ b/gson/src/test/java/com/google/gson/internal/ParameterizedTypeHandlerMapTest.java @@ -18,13 +18,10 @@ package com.google.gson.internal; import com.google.gson.common.TestTypes.Base; import com.google.gson.common.TestTypes.Sub; -import com.google.gson.internal.ParameterizedTypeHandlerMap; import com.google.gson.reflect.TypeToken; - -import junit.framework.TestCase; - import java.lang.reflect.Type; import java.util.List; +import junit.framework.TestCase; /** * Unit tests for the {@link com.google.gson.internal.ParameterizedTypeHandlerMap} class.