From 194c18d20c6a05c9454ca9e0bc5503f2581ee3ea Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Thu, 20 Oct 2011 04:24:27 +0000 Subject: [PATCH] Be backwards-compatible for serialization of maps whose keys aren't primitives. --- .../internal/bind/MapTypeAdapterFactory.java | 34 ++++++--- .../gson/internal/bind/TypeAdapters.java | 18 +++++ .../com/google/gson/functional/MapTest.java | 69 ++++++++++++++++++- 3 files changed, 110 insertions(+), 11 deletions(-) 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 74ecb20c..0141b7f9 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,12 +16,6 @@ package com.google.gson.internal.bind; -import java.io.IOException; -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - import com.google.gson.JsonElement; import com.google.gson.JsonPrimitive; import com.google.gson.JsonSyntaxException; @@ -33,6 +27,11 @@ 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; /** * Adapts maps to either JSON objects or JSON arrays. @@ -119,7 +118,7 @@ public final class MapTypeAdapterFactory implements TypeAdapter.Factory { Class rawTypeOfSrc = $Gson$Types.getRawType(type); Type[] keyAndValueTypes = $Gson$Types.getMapKeyAndValueTypes(type, rawTypeOfSrc); - TypeAdapter keyAdapter = context.getAdapter(TypeToken.get(keyAndValueTypes[0])); + TypeAdapter keyAdapter = getKeyAdapter(context, keyAndValueTypes[0]); TypeAdapter valueAdapter = context.getAdapter(TypeToken.get(keyAndValueTypes[1])); ObjectConstructor constructor = constructorConstructor.getConstructor(typeToken); @@ -129,6 +128,15 @@ public final class MapTypeAdapterFactory implements TypeAdapter.Factory { return result; } + /** + * Returns a type adapter that writes the value as a string. + */ + private TypeAdapter getKeyAdapter(MiniGson context, Type keyType) { + return (keyType == boolean.class || keyType == Boolean.class) + ? TypeAdapters.BOOLEAN_AS_STRING + : context.getAdapter(TypeToken.get(keyType)); + } + private final class Adapter extends TypeAdapter> { private final TypeAdapter keyTypeAdapter; private final TypeAdapter valueTypeAdapter; @@ -188,6 +196,16 @@ public final class MapTypeAdapterFactory implements TypeAdapter.Factory { return; } + if (!complexMapKeySerialization) { + writer.beginObject(); + for (Map.Entry entry : map.entrySet()) { + writer.name(String.valueOf(entry.getKey())); + valueTypeAdapter.write(writer, entry.getValue()); + } + writer.endObject(); + return; + } + boolean hasComplexKeys = false; List keys = new ArrayList(map.size()); @@ -199,7 +217,7 @@ public final class MapTypeAdapterFactory implements TypeAdapter.Factory { hasComplexKeys |= keyElement.isJsonArray() || keyElement.isJsonObject(); } - if (complexMapKeySerialization && hasComplexKeys) { + if (hasComplexKeys) { writer.beginArray(); for (int i = 0; i < keys.size(); i++) { writer.beginArray(); // entry array diff --git a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java index 6ab72985..b1e9bc3e 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java +++ b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java @@ -127,6 +127,24 @@ public final class TypeAdapters { } }; + /** + * Writes a boolean as a string. Useful for map keys, where booleans aren't + * otherwise permitted. + */ + public static final TypeAdapter BOOLEAN_AS_STRING = new TypeAdapter() { + @Override public Boolean read(JsonReader reader) throws IOException { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull(); + return null; + } + return Boolean.valueOf(reader.nextString()); + } + + @Override public void write(JsonWriter writer, Boolean value) throws IOException { + writer.value(value == null ? "null" : value.toString()); + } + }; + public static final TypeAdapter.Factory BOOLEAN_FACTORY = newFactory(boolean.class, Boolean.class, BOOLEAN); diff --git a/gson/src/test/java/com/google/gson/functional/MapTest.java b/gson/src/test/java/com/google/gson/functional/MapTest.java index 309615ad..efbbfb10 100755 --- a/gson/src/test/java/com/google/gson/functional/MapTest.java +++ b/gson/src/test/java/com/google/gson/functional/MapTest.java @@ -21,21 +21,20 @@ import com.google.gson.GsonBuilder; import com.google.gson.InstanceCreator; import com.google.gson.JsonArray; import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; import com.google.gson.JsonPrimitive; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; import com.google.gson.common.TestTypes; import com.google.gson.internal.$Gson$Types; import com.google.gson.reflect.TypeToken; - -import junit.framework.TestCase; - import java.lang.reflect.Type; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.TreeMap; +import junit.framework.TestCase; /** * Functional test for Json serialization and deserialization for Maps @@ -438,6 +437,70 @@ public class MapTest extends TestCase { assertEquals(expected, gson.toJson(map)); } + public void testComplexKeysSerialization() { + Map map = new LinkedHashMap(); + map.put(new Point(2, 3), "a"); + map.put(new Point(5, 7), "b"); + String json = "{\"2,3\":\"a\",\"5,7\":\"b\"}"; + assertEquals(json, gson.toJson(map, new TypeToken>() {}.getType())); + assertEquals(json, gson.toJson(map, Map.class)); + } + + public void testComplexKeysDeserialization() { + String json = "{\"2,3\":\"a\",\"5,7\":\"b\"}"; + try { + gson.fromJson(json, new TypeToken>() {}.getType()); + fail(); + } catch (JsonParseException expected) { + } + } + + public void testStringKeyDeserialization() { + String json = "{\"2,3\":\"a\",\"5,7\":\"b\"}"; + Map map = new LinkedHashMap(); + map.put("2,3", "a"); + map.put("5,7", "b"); + assertEquals(map, gson.fromJson(json, new TypeToken>() {}.getType())); + } + + public void testNumberKeyDeserialization() { + String json = "{\"2.3\":\"a\",\"5.7\":\"b\"}"; + Map map = new LinkedHashMap(); + map.put(2.3, "a"); + map.put(5.7, "b"); + assertEquals(map, gson.fromJson(json, new TypeToken>() {}.getType())); + } + + public void testBooleanKeyDeserialization() { + String json = "{\"true\":\"a\",\"false\":\"b\"}"; + Map map = new LinkedHashMap(); + map.put(true, "a"); + map.put(false, "b"); + assertEquals(map, gson.fromJson(json, new TypeToken>() {}.getType())); + } + + static class Point { + private final int x; + private final int y; + + Point(int x, int y) { + this.x = x; + this.y = y; + } + + @Override public boolean equals(Object o) { + return o instanceof Point && x == ((Point) o).x && y == ((Point) o).y; + } + + @Override public int hashCode() { + return x * 37 + y; + } + + @Override public String toString() { + return x + "," + y; + } + } + static final class MapClass { private final Map bases = new HashMap(); private final Map subs = new HashMap();