From 95a345234fb1d51ada03f37d50769cd5198b5af3 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Mon, 11 Jul 2011 21:45:09 +0000 Subject: [PATCH] MiniGSON Map adapters and support for nulls. --- .../gson/mini/CollectionTypeAdapter.java | 11 ++ .../com/google/gson/mini/MapTypeAdapter.java | 113 ++++++++++++++++++ .../java/com/google/gson/mini/MiniGson.java | 14 ++- .../gson/mini/ReflectiveTypeAdapter.java | 25 ++-- .../com/google/gson/mini/TypeAdapter.java | 2 +- .../com/google/gson/mini/MiniGsonTest.java | 110 +++++++++++++---- 6 files changed, 243 insertions(+), 32 deletions(-) create mode 100644 extras/src/main/java/com/google/gson/mini/MapTypeAdapter.java diff --git a/extras/src/main/java/com/google/gson/mini/CollectionTypeAdapter.java b/extras/src/main/java/com/google/gson/mini/CollectionTypeAdapter.java index 61b7b4e7..79973b35 100644 --- a/extras/src/main/java/com/google/gson/mini/CollectionTypeAdapter.java +++ b/extras/src/main/java/com/google/gson/mini/CollectionTypeAdapter.java @@ -19,6 +19,7 @@ package com.google.gson.mini; import com.google.gson.internal.$Gson$Types; 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.Constructor; @@ -82,6 +83,11 @@ final class CollectionTypeAdapter extends TypeAdapter> { } public Collection read(JsonReader reader) throws IOException { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull(); // TODO: does this belong here? + return null; + } + Collection collection = MiniGson.newInstance(constructor); reader.beginArray(); while (reader.hasNext()) { @@ -93,6 +99,11 @@ final class CollectionTypeAdapter extends TypeAdapter> { } public void write(JsonWriter writer, Collection collection) throws IOException { + if (collection == null) { + writer.nullValue(); // TODO: better policy here? + return; + } + writer.beginArray(); for (E element : collection) { elementTypeAdapter.write(writer, element); diff --git a/extras/src/main/java/com/google/gson/mini/MapTypeAdapter.java b/extras/src/main/java/com/google/gson/mini/MapTypeAdapter.java new file mode 100644 index 00000000..cc7f66e4 --- /dev/null +++ b/extras/src/main/java/com/google/gson/mini/MapTypeAdapter.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2011 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.mini; + +import com.google.gson.internal.$Gson$Types; +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.Constructor; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Adapt a homogeneous collection of objects. + */ +final class MapTypeAdapter extends TypeAdapter> { + public static final Factory FACTORY = new Factory() { + public TypeAdapter create(MiniGson context, TypeToken typeToken) { + Type type = typeToken.getType(); + if (!(type instanceof ParameterizedType)) { + return null; + } + + Class rawType = typeToken.getRawType(); + if (!Map.class.isAssignableFrom(rawType)) { + return null; + } + + Type[] keyAndValueTypes = $Gson$Types.getMapKeyAndValueTypes(type, rawType); + if (keyAndValueTypes[0] != String.class) { + return null; // TODO: return an array-style map adapter + } + TypeAdapter valueAdapter = context.getAdapter(TypeToken.get(keyAndValueTypes[1])); + + Class constructorType; + + if (rawType == Map.class) { + constructorType = LinkedHashMap.class; + } else { + constructorType = rawType; + } + + Constructor constructor; + try { + constructor = constructorType.getConstructor(); + } catch (NoSuchMethodException e) { + return null; + } + + @SuppressWarnings("unchecked") // we don't define a type parameter for the key or value types + TypeAdapter result = new MapTypeAdapter(valueAdapter, constructor); + return result; + } + }; + + private final TypeAdapter valueTypeAdapter; + private final Constructor> constructor; + + public MapTypeAdapter(TypeAdapter valueTypeAdapter, + Constructor> constructor) { + this.valueTypeAdapter = valueTypeAdapter; + this.constructor = constructor; + } + + public Map read(JsonReader reader) throws IOException { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull(); // TODO: does this belong here? + return null; + } + + Map map = MiniGson.newInstance(constructor); + reader.beginObject(); + while (reader.hasNext()) { + String key = reader.nextName(); + V value = valueTypeAdapter.read(reader); + map.put(key, value); + } + reader.endObject(); + return map; + } + + 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()) { + writer.name(entry.getKey()); + valueTypeAdapter.write(writer, entry.getValue()); + } + writer.endObject(); + } +} diff --git a/extras/src/main/java/com/google/gson/mini/MiniGson.java b/extras/src/main/java/com/google/gson/mini/MiniGson.java index 4d7ddfbd..2fa98ce0 100644 --- a/extras/src/main/java/com/google/gson/mini/MiniGson.java +++ b/extras/src/main/java/com/google/gson/mini/MiniGson.java @@ -39,6 +39,7 @@ public final class MiniGson { factories.add(TypeAdapters.STRING_FACTORY); factories.add(ReflectiveTypeAdapter.FACTORY); factories.add(CollectionTypeAdapter.FACTORY); + factories.add(MapTypeAdapter.FACTORY); this.factories = Collections.unmodifiableList(factories); } @@ -65,6 +66,7 @@ public final class MiniGson { * deserialize {@code type}. */ public TypeAdapter getAdapter(TypeToken type) { + // TODO: create a cache here for (TypeAdapter.Factory factory : factories) { TypeAdapter candidate = factory.create(this, type); if (candidate != null) { @@ -94,20 +96,24 @@ public final class MiniGson { public static final class Builder { private final List factories = new ArrayList(); - public void factory(TypeAdapter.Factory factory) { + public Builder factory(TypeAdapter.Factory factory) { factories.add(factory); + return this; } - public void typeAdapter(final Class type, final TypeAdapter typeAdapter) { + public Builder typeAdapter(final Class type, final TypeAdapter typeAdapter) { factories.add(TypeAdapters.newFactory(type, typeAdapter)); + return this; } - public void typeAdapter(TypeToken type, TypeAdapter typeAdapter) { + public Builder typeAdapter(TypeToken type, TypeAdapter typeAdapter) { factories.add(TypeAdapters.newFactory(type, typeAdapter)); + return this; } - public void typeHierarchyAdapter(TypeToken type, TypeAdapter typeAdapter) { + public Builder typeHierarchyAdapter(TypeToken type, TypeAdapter typeAdapter) { factories.add(TypeAdapters.newTypeHierarchyFactory(type, typeAdapter)); + return this; } public MiniGson build() { diff --git a/extras/src/main/java/com/google/gson/mini/ReflectiveTypeAdapter.java b/extras/src/main/java/com/google/gson/mini/ReflectiveTypeAdapter.java index 05daf9de..4f1c40f5 100644 --- a/extras/src/main/java/com/google/gson/mini/ReflectiveTypeAdapter.java +++ b/extras/src/main/java/com/google/gson/mini/ReflectiveTypeAdapter.java @@ -19,6 +19,7 @@ package com.google.gson.mini; import com.google.gson.internal.$Gson$Types; 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.Constructor; @@ -36,14 +37,13 @@ final class ReflectiveTypeAdapter extends TypeAdapter { Class raw = type.getRawType(); if (!Object.class.isAssignableFrom(raw)) { - // TODO: does this catch primitives? - return null; + return null; // it's a primitive! } // TODO: use Joel's constructor calling code (with setAccessible) - Constructor constructor; + Constructor constructor; try { - constructor = (Constructor) raw.getDeclaredConstructor(); + constructor = raw.getDeclaredConstructor(); } catch (NoSuchMethodException e) { return null; } @@ -67,18 +67,24 @@ final class ReflectiveTypeAdapter extends TypeAdapter { } }; - private final Constructor constructor; + private final Constructor constructor; private final Map> map; private final BoundField[] boundFields; - ReflectiveTypeAdapter(Constructor constructor, Map> map) { + ReflectiveTypeAdapter(Constructor constructor, Map> map) { this.constructor = constructor; this.map = map; this.boundFields = map.values().toArray(new BoundField[map.size()]); } public T read(JsonReader reader) throws IOException { - T instance = MiniGson.newInstance(constructor); + if (reader.peek() == JsonToken.NULL) { + reader.nextNull(); // TODO: does this belong here? + return null; + } + + @SuppressWarnings("unchecked") // the '? super T' is a raw T (the only kind we can construct) + T instance = (T) MiniGson.newInstance(constructor); // TODO: null out the other fields? @@ -98,6 +104,11 @@ final class ReflectiveTypeAdapter extends TypeAdapter { } public void write(JsonWriter writer, T value) throws IOException { + if (value == null) { + writer.nullValue(); // TODO: better policy here? + return; + } + writer.beginObject(); for (BoundField boundField : boundFields) { writer.name(boundField.name); diff --git a/extras/src/main/java/com/google/gson/mini/TypeAdapter.java b/extras/src/main/java/com/google/gson/mini/TypeAdapter.java index 99ea6cd5..624b9030 100644 --- a/extras/src/main/java/com/google/gson/mini/TypeAdapter.java +++ b/extras/src/main/java/com/google/gson/mini/TypeAdapter.java @@ -46,7 +46,7 @@ public abstract class TypeAdapter { public final T read(Reader in) throws IOException { JsonReader reader = new JsonReader(in); - reader.setLenient(true); // TODO: why? + reader.setLenient(true); return read(reader); } diff --git a/extras/src/test/java/com/google/gson/mini/MiniGsonTest.java b/extras/src/test/java/com/google/gson/mini/MiniGsonTest.java index 38a1f206..7b1cedec 100644 --- a/extras/src/test/java/com/google/gson/mini/MiniGsonTest.java +++ b/extras/src/test/java/com/google/gson/mini/MiniGsonTest.java @@ -16,60 +16,130 @@ package com.google.gson.mini; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; import java.io.IOException; import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import junit.framework.TestCase; public final class MiniGsonTest extends TestCase { + private MiniGson miniGson = new MiniGson.Builder().build(); + private TypeAdapter truckAdapter = miniGson.getAdapter(Truck.class); + private TypeAdapter> mapAdapter + = miniGson.getAdapter(new TypeToken>() {}); + public void testSerialize() throws IOException { - Person jesse = new Person("Jesse", 29); - Person jodie = new Person("Jodie", 29); Truck truck = new Truck(); - truck.passengers = Arrays.asList(jesse, jodie); + truck.passengers = Arrays.asList(new Person("Jesse", 29), new Person("Jodie", 29)); truck.horsePower = 300; - MiniGson miniGson = new MiniGson.Builder().build(); - TypeAdapter truckAdapter = miniGson.getAdapter(Truck.class); - - String json = truckAdapter.toJson(truck); assertEquals("{'horsePower':300.0," + "'passengers':[{'age':29,'name':'Jesse'},{'age':29,'name':'Jodie'}]}", - json.replace('\"', '\'')); + truckAdapter.toJson(truck).replace('\"', '\'')); } public void testDeserialize() throws IOException { String json = "{'horsePower':300.0," + "'passengers':[{'age':29,'name':'Jesse'},{'age':29,'name':'Jodie'}]}"; - - MiniGson miniGson = new MiniGson.Builder().build(); - TypeAdapter truckAdapter = miniGson.getAdapter(Truck.class); Truck truck = truckAdapter.fromJson(json); - assertEquals(300.0, truck.horsePower); - Person jesse = truck.passengers.get(0); - assertEquals("Jesse", jesse.name); - assertEquals(29, jesse.age); - Person jodie = truck.passengers.get(1); - assertEquals("Jodie", jodie.name); - assertEquals(29, jodie.age); + assertEquals(Arrays.asList(new Person("Jesse", 29), new Person("Jodie", 29)), truck.passengers); + } + + public void testSerializeNullField() throws IOException { + Truck truck = new Truck(); + truck.passengers = null; + assertEquals("{'horsePower':0.0,'passengers':null}", + truckAdapter.toJson(truck).replace('\"', '\'')); + } + + public void testDeserializeNullField() throws IOException { + Truck truck = truckAdapter.fromJson("{'horsePower':0.0,'passengers':null}"); + assertNull(truck.passengers); + } + + public void testSerializeNullObject() throws IOException { + Truck truck = new Truck(); + truck.passengers = Arrays.asList((Person) null); + assertEquals("{'horsePower':0.0,'passengers':[null]}", + truckAdapter.toJson(truck).replace('\"', '\'')); + } + + public void testDeserializeNullObject() throws IOException { + Truck truck = truckAdapter.fromJson("{'horsePower':0.0,'passengers':[null]}"); + assertEquals(Arrays.asList((Person) null), truck.passengers); + } + + public void testSerializeWithCustomTypeAdapter() throws IOException { + usePersonNameAdapter(); + Truck truck = new Truck(); + truck.passengers = Arrays.asList(new Person("Jesse", 29), new Person("Jodie", 29)); + assertEquals("{'horsePower':0.0,'passengers':['Jesse','Jodie']}", + truckAdapter.toJson(truck).replace('\"', '\'')); + } + + public void testDeserializeWithCustomTypeAdapter() throws IOException { + usePersonNameAdapter(); + Truck truck = truckAdapter.fromJson("{'horsePower':0.0,'passengers':['Jesse','Jodie']}"); + assertEquals(Arrays.asList(new Person("Jesse", -1), new Person("Jodie", -1)), truck.passengers); + } + + private void usePersonNameAdapter() { + TypeAdapter personNameAdapter = new TypeAdapter() { + @Override public Person read(JsonReader reader) throws IOException { + String name = reader.nextString(); + return new Person(name, -1); + } + @Override public void write(JsonWriter writer, Person value) throws IOException { + writer.value(value.name); + } + }; + miniGson = new MiniGson.Builder().typeAdapter(Person.class, personNameAdapter).build(); + truckAdapter = miniGson.getAdapter(Truck.class); + } + + public void testSerializeMap() throws IOException { + Map map = new LinkedHashMap(); + map.put("a", 5.0); + map.put("b", 10.0); + assertEquals("{'a':5.0,'b':10.0}", mapAdapter.toJson(map).replace('"', '\'')); + } + + public void testDeserializeMap() throws IOException { + Map map = new LinkedHashMap(); + map.put("a", 5.0); + map.put("b", 10.0); + assertEquals(map, mapAdapter.fromJson("{'a':5.0,'b':10.0}")); } static class Truck { double horsePower; - List passengers; + List passengers = Collections.emptyList(); } static class Person { int age; String name; - Person(String name, int age) { this.name = name; this.age = age; } public Person() {} // TODO: use Joel's constructor code so we don't need this + + @Override public boolean equals(Object o) { + return o instanceof Person + && ((Person) o).name.equals(name) + && ((Person) o).age == age; + } + @Override public int hashCode() { + return name.hashCode() ^ age; + } } }