From fd4fbe41322c39329245b6a1311ae42bd10827ad Mon Sep 17 00:00:00 2001 From: Inderjeet Singh Date: Thu, 18 Oct 2012 02:37:43 +0000 Subject: [PATCH] Added support for collections, maps, and arbitrary depth of type adapters for Intercept annotation. Added more tests for the features. --- gson/src/main/java/com/google/gson/Gson.java | 24 ++++- .../gson/internal/bind/ArrayTypeAdapter.java | 9 +- .../bind/CollectionTypeAdapterFactory.java | 5 + .../internal/bind/MapTypeAdapterFactory.java | 11 +++ .../bind/ReflectiveTypeAdapterFactory.java | 1 + .../gson/functional/InterceptorTest.java | 94 +++++++++++++++++-- 6 files changed, 132 insertions(+), 12 deletions(-) diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java index dfe1d158..93023424 100644 --- a/gson/src/main/java/com/google/gson/Gson.java +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -797,7 +797,7 @@ public final class Gson { TypeToken typeToken = (TypeToken) TypeToken.get(typeOfT); TypeAdapter typeAdapter = (TypeAdapter) getAdapter(typeToken); T object = typeAdapter.read(reader); - invokeInterceptorIfNeeded(object, typeToken); + invokeInterceptorIfNeeded(object, typeToken.getRawType()); return object; } catch (EOFException e) { /* @@ -890,9 +890,15 @@ public final class Gson { } } + private void invokeInterceptorIfNeeded(T object, Type type) { + @SuppressWarnings("unchecked") + TypeToken typeToken = (TypeToken) TypeToken.get(type); + Class clazz = typeToken.getRawType(); + invokeInterceptorIfNeeded(object, clazz); + } + @SuppressWarnings({ "unchecked", "rawtypes" }) - private void invokeInterceptorIfNeeded(T object, TypeToken type) { - Class clazz = type.getRawType(); + private void invokeInterceptorIfNeeded(T object, Class clazz) { Intercept interceptor = clazz.getAnnotation(Intercept.class); if (interceptor == null) return; // TODO: We don't need to construct an instance of postDeserializer every time. we can @@ -913,4 +919,16 @@ public final class Gson { .append("}"); return sb.toString(); } + + /** + * Not part of the Gson API. Do not use. + */ + public static final class $Internal$Access { + public static void invokeInterceptor(Gson gson, T instance, Type type) { + gson.invokeInterceptorIfNeeded(instance, type); + } + public static void invokeInterceptor(Gson gson, T instance, Class clazz) { + gson.invokeInterceptorIfNeeded(instance, clazz); + } + } } diff --git a/gson/src/main/java/com/google/gson/internal/bind/ArrayTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/bind/ArrayTypeAdapter.java index 39ab7628..b20e2b5a 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/ArrayTypeAdapter.java +++ b/gson/src/main/java/com/google/gson/internal/bind/ArrayTypeAdapter.java @@ -16,9 +16,6 @@ package com.google.gson.internal.bind; -import com.google.gson.Gson; -import com.google.gson.TypeAdapter; -import com.google.gson.TypeAdapterFactory; import java.io.IOException; import java.lang.reflect.Array; import java.lang.reflect.GenericArrayType; @@ -26,6 +23,9 @@ import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; import com.google.gson.internal.$Gson$Types; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; @@ -51,10 +51,12 @@ public final class ArrayTypeAdapter extends TypeAdapter { } }; + private final Gson context; private final Class componentType; private final TypeAdapter componentTypeAdapter; public ArrayTypeAdapter(Gson context, TypeAdapter componentTypeAdapter, Class componentType) { + this.context = context; this.componentTypeAdapter = new TypeAdapterRuntimeTypeWrapper(context, componentTypeAdapter, componentType); this.componentType = componentType; @@ -70,6 +72,7 @@ public final class ArrayTypeAdapter extends TypeAdapter { in.beginArray(); while (in.hasNext()) { E instance = componentTypeAdapter.read(in); + Gson.$Internal$Access.invokeInterceptor(context, instance, componentType); list.add(instance); } in.endArray(); diff --git a/gson/src/main/java/com/google/gson/internal/bind/CollectionTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/CollectionTypeAdapterFactory.java index c8af7b98..9fedcb52 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/CollectionTypeAdapterFactory.java +++ b/gson/src/main/java/com/google/gson/internal/bind/CollectionTypeAdapterFactory.java @@ -58,12 +58,16 @@ public final class CollectionTypeAdapterFactory implements TypeAdapterFactory { } private final class Adapter extends TypeAdapter> { + private final Gson context; + private final Type elementType; private final TypeAdapter elementTypeAdapter; private final ObjectConstructor> constructor; public Adapter(Gson context, Type elementType, TypeAdapter elementTypeAdapter, ObjectConstructor> constructor) { + this.context = context; + this.elementType = elementType; this.elementTypeAdapter = new TypeAdapterRuntimeTypeWrapper(context, elementTypeAdapter, elementType); this.constructor = constructor; @@ -79,6 +83,7 @@ public final class CollectionTypeAdapterFactory implements TypeAdapterFactory { in.beginArray(); while (in.hasNext()) { E instance = elementTypeAdapter.read(in); + Gson.$Internal$Access.invokeInterceptor(context, instance, elementType); collection.add(instance); } in.endArray(); 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 e12f7634..54d9cdfa 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 @@ -144,6 +144,9 @@ public final class MapTypeAdapterFactory implements TypeAdapterFactory { } private final class Adapter extends TypeAdapter> { + private final Gson context; + private final Type keyType; + private final Type valueType; private final TypeAdapter keyTypeAdapter; private final TypeAdapter valueTypeAdapter; private final ObjectConstructor> constructor; @@ -151,6 +154,9 @@ public final class MapTypeAdapterFactory implements TypeAdapterFactory { public Adapter(Gson context, Type keyType, TypeAdapter keyTypeAdapter, Type valueType, TypeAdapter valueTypeAdapter, ObjectConstructor> constructor) { + this.context = context; + this.keyType = keyType; + this.valueType = valueType; this.keyTypeAdapter = new TypeAdapterRuntimeTypeWrapper(context, keyTypeAdapter, keyType); this.valueTypeAdapter = @@ -172,7 +178,10 @@ public final class MapTypeAdapterFactory implements TypeAdapterFactory { while (in.hasNext()) { in.beginArray(); // entry array K key = keyTypeAdapter.read(in); + Gson.$Internal$Access.invokeInterceptor(context, key, keyType); + V value = valueTypeAdapter.read(in); + Gson.$Internal$Access.invokeInterceptor(context, value, valueType); V replaced = map.put(key, value); if (replaced != null) { throw new JsonSyntaxException("duplicate key: " + key); @@ -185,7 +194,9 @@ public final class MapTypeAdapterFactory implements TypeAdapterFactory { while (in.hasNext()) { JsonReaderInternalAccess.INSTANCE.promoteNameToValue(in); K key = keyTypeAdapter.read(in); + Gson.$Internal$Access.invokeInterceptor(context, key, keyType); V value = valueTypeAdapter.read(in); + Gson.$Internal$Access.invokeInterceptor(context, value, valueType); V replaced = map.put(key, value); if (replaced != null) { throw new JsonSyntaxException("duplicate key: " + key); diff --git a/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java index 0f5564bd..68d32164 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java +++ b/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java @@ -91,6 +91,7 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { @Override void read(JsonReader reader, Object value) throws IOException, IllegalAccessException { Object fieldValue = typeAdapter.read(reader); + Gson.$Internal$Access.invokeInterceptor(context, fieldValue, fieldType.getRawType()); if (fieldValue != null || !isPrimitive) { field.set(value, fieldValue); } diff --git a/gson/src/test/java/com/google/gson/functional/InterceptorTest.java b/gson/src/test/java/com/google/gson/functional/InterceptorTest.java index e1b60757..155f0e95 100644 --- a/gson/src/test/java/com/google/gson/functional/InterceptorTest.java +++ b/gson/src/test/java/com/google/gson/functional/InterceptorTest.java @@ -15,15 +15,25 @@ */ package com.google.gson.functional; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.Collection; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import junit.framework.TestCase; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import com.google.gson.JsonParseException; +import com.google.gson.JsonSyntaxException; +import com.google.gson.TypeAdapter; import com.google.gson.internal.alpha.Intercept; import com.google.gson.internal.alpha.JsonPostDeserializer; import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; /** * Unit tests for {@link Intercept} and {@link JsonPostDeserializer}. @@ -37,7 +47,7 @@ public final class InterceptorTest extends TestCase { @Override public void setUp() throws Exception { super.setUp(); - this.gson = new Gson(); + this.gson = new GsonBuilder().enableComplexMapKeySerialization().create(); } public void testExceptionsPropagated() { @@ -47,41 +57,113 @@ public final class InterceptorTest extends TestCase { } catch (JsonParseException expected) {} } - public void testPostDeserializeTopLevelClass() { + public void testTopLevelClass() { User user = gson.fromJson("{name:'bob',password:'pwd'}", User.class); assertEquals(User.DEFAULT_EMAIL, user.email); } - public void testPostDeserializeList() { + public void testList() { List list = gson.fromJson("[{name:'bob',password:'pwd'}]", new TypeToken>(){}.getType()); User user = list.get(0); assertEquals(User.DEFAULT_EMAIL, user.email); } - public void testPostDeserializeField() { + public void testCollection() { + Collection list = gson.fromJson("[{name:'bob',password:'pwd'}]", new TypeToken>(){}.getType()); + User user = list.iterator().next(); + assertEquals(User.DEFAULT_EMAIL, user.email); + } + + public void testMapKeyAndValues() { + Type mapType = new TypeToken>(){}.getType(); + try { + gson.fromJson("[[{name:'bob',password:'pwd'},{}]]", mapType); + fail(); + } catch (JsonSyntaxException expected) {} + Map map = gson.fromJson("[[{name:'bob',password:'pwd'},{city:'Mountain View',state:'CA',zip:'94043'}]]", + mapType); + Entry entry = map.entrySet().iterator().next(); + assertEquals(User.DEFAULT_EMAIL, entry.getKey().email); + assertEquals(Address.DEFAULT_FIRST_LINE, entry.getValue().firstLine); + } + + public void testField() { UserGroup userGroup = gson.fromJson("{user:{name:'bob',password:'pwd'}}", UserGroup.class); assertEquals(User.DEFAULT_EMAIL, userGroup.user.email); } + public void testCustomTypeAdapter() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(User.class, new TypeAdapter() { + @Override public void write(JsonWriter out, User value) throws IOException { + throw new UnsupportedOperationException(); + } + @Override public User read(JsonReader in) throws IOException { + in.beginObject(); + in.nextName(); + String name = in.nextString(); + in.nextName(); + String password = in.nextString(); + in.endObject(); + return new User(name, password); + }}) + .create(); + UserGroup userGroup = gson.fromJson("{user:{name:'bob',password:'pwd'}}", UserGroup.class); + assertEquals(User.DEFAULT_EMAIL, userGroup.user.email); + } + + public void testDirectInvocationOfTypeAdapter() throws Exception { + TypeAdapter adapter = gson.getAdapter(UserGroup.class); + UserGroup userGroup = adapter.fromJson("{\"user\":{\"name\":\"bob\",\"password\":\"pwd\"}}"); + assertEquals(User.DEFAULT_EMAIL, userGroup.user.email); + } + + @SuppressWarnings("unused") private static final class UserGroup { User user; String city; } @Intercept(postDeserialize = UserValidator.class) + @SuppressWarnings("unused") private static final class User { static final String DEFAULT_EMAIL = "invalid@invalid.com"; String name; String password; String email; + Address address; + public User(String name, String password) { + this.name = name; + this.password = password; + } } private static final class UserValidator implements JsonPostDeserializer { public void postDeserialize(User user) { if (user.name == null || user.password == null) { - throw new JsonParseException("name and password are required fields."); + throw new JsonSyntaxException("name and password are required fields."); } if (user.email == null) user.email = User.DEFAULT_EMAIL; } } -} \ No newline at end of file + + @Intercept(postDeserialize = AddressValidator.class) + @SuppressWarnings("unused") + private static final class Address { + static final String DEFAULT_FIRST_LINE = "unknown"; + String firstLine; + String secondLine; + String city; + String state; + String zip; + } + + private static final class AddressValidator implements JsonPostDeserializer
{ + public void postDeserialize(Address address) { + if (address.city == null || address.state == null || address.zip == null) { + throw new JsonSyntaxException("Address city, state and zip are required fields."); + } + if (address.firstLine == null) address.firstLine = Address.DEFAULT_FIRST_LINE; + } + } +}