diff options
Diffstat (limited to 'gson/src/test/java/com/google/gson/functional/RuntimeTypeAdapterFactoryFunctionalTest.java')
-rw-r--r-- | gson/src/test/java/com/google/gson/functional/RuntimeTypeAdapterFactoryFunctionalTest.java | 205 |
1 files changed, 205 insertions, 0 deletions
diff --git a/gson/src/test/java/com/google/gson/functional/RuntimeTypeAdapterFactoryFunctionalTest.java b/gson/src/test/java/com/google/gson/functional/RuntimeTypeAdapterFactoryFunctionalTest.java new file mode 100644 index 00000000..c3b0898d --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/RuntimeTypeAdapterFactoryFunctionalTest.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2008 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.functional; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +import junit.framework.TestCase; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.internal.Streams; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +/** + * Functional tests for the RuntimeTypeAdapterFactory feature in extras. + */ +public final class RuntimeTypeAdapterFactoryFunctionalTest extends TestCase { + + private final Gson gson = new Gson(); + + /** + * This test also ensures that {@link TypeAdapterFactory} registered through {@link JsonAdapter} + * work correctly for {@link Gson#getDelegateAdapter(TypeAdapterFactory, TypeToken)}. + */ + public void testSubclassesAutomaticallySerialzed() throws Exception { + Shape shape = new Circle(25); + String json = gson.toJson(shape); + shape = gson.fromJson(json, Shape.class); + assertEquals(25, ((Circle)shape).radius); + + shape = new Square(15); + json = gson.toJson(shape); + shape = gson.fromJson(json, Shape.class); + assertEquals(15, ((Square)shape).side); + assertEquals(ShapeType.SQUARE, shape.type); + } + + @JsonAdapter(Shape.JsonAdapterFactory.class) + static class Shape { + final ShapeType type; + Shape(ShapeType type) { this.type = type; } + private static final class JsonAdapterFactory extends RuntimeTypeAdapterFactory<Shape> { + public JsonAdapterFactory() { + super(Shape.class, "type"); + registerSubtype(Circle.class, ShapeType.CIRCLE.toString()); + registerSubtype(Square.class, ShapeType.SQUARE.toString()); + } + } + } + + public enum ShapeType { + SQUARE, CIRCLE + } + + private static final class Circle extends Shape { + final int radius; + Circle(int radius) { super(ShapeType.CIRCLE); this.radius = radius; } + } + + private static final class Square extends Shape { + final int side; + Square(int side) { super(ShapeType.SQUARE); this.side = side; } + } + + // Copied from the extras package + static class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory { + private final Class<?> baseType; + private final String typeFieldName; + private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<String, Class<?>>(); + private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<Class<?>, String>(); + + protected RuntimeTypeAdapterFactory(Class<?> baseType, String typeFieldName) { + if (typeFieldName == null || baseType == null) { + throw new NullPointerException(); + } + this.baseType = baseType; + this.typeFieldName = typeFieldName; + } + + /** + * Creates a new runtime type adapter using for {@code baseType} using {@code + * typeFieldName} as the type field name. Type field names are case sensitive. + */ + public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) { + return new RuntimeTypeAdapterFactory<T>(baseType, typeFieldName); + } + + /** + * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as + * the type field name. + */ + public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType) { + return new RuntimeTypeAdapterFactory<T>(baseType, "type"); + } + + /** + * Registers {@code type} identified by {@code label}. Labels are case + * sensitive. + * + * @throws IllegalArgumentException if either {@code type} or {@code label} + * have already been registered on this type adapter. + */ + public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) { + if (type == null || label == null) { + throw new NullPointerException(); + } + if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) { + throw new IllegalArgumentException("types and labels must be unique"); + } + labelToSubtype.put(label, type); + subtypeToLabel.put(type, label); + return this; + } + + /** + * Registers {@code type} identified by its {@link Class#getSimpleName simple + * name}. Labels are case sensitive. + * + * @throws IllegalArgumentException if either {@code type} or its simple name + * have already been registered on this type adapter. + */ + public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) { + return registerSubtype(type, type.getSimpleName()); + } + + public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) { + if (type.getRawType() != baseType) { + return null; + } + + final Map<String, TypeAdapter<?>> labelToDelegate + = new LinkedHashMap<String, TypeAdapter<?>>(); + final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate + = new LinkedHashMap<Class<?>, TypeAdapter<?>>(); + for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) { + TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue())); + labelToDelegate.put(entry.getKey(), delegate); + subtypeToDelegate.put(entry.getValue(), delegate); + } + + return new TypeAdapter<R>() { + @Override public R read(JsonReader in) throws IOException { + JsonElement jsonElement = Streams.parse(in); + JsonElement labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName); + if (labelJsonElement == null) { + throw new JsonParseException("cannot deserialize " + baseType + + " because it does not define a field named " + typeFieldName); + } + String label = labelJsonElement.getAsString(); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter<R> delegate = (TypeAdapter<R>) labelToDelegate.get(label); + if (delegate == null) { + throw new JsonParseException("cannot deserialize " + baseType + " subtype named " + + label + "; did you forget to register a subtype?"); + } + return delegate.fromJsonTree(jsonElement); + } + + @Override public void write(JsonWriter out, R value) throws IOException { + Class<?> srcType = value.getClass(); + String label = subtypeToLabel.get(srcType); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter<R> delegate = (TypeAdapter<R>) subtypeToDelegate.get(srcType); + if (delegate == null) { + throw new JsonParseException("cannot serialize " + srcType.getName() + + "; did you forget to register a subtype?"); + } + JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject(); + if (!jsonObject.has(typeFieldName)) { + JsonObject clone = new JsonObject(); + clone.add(typeFieldName, new JsonPrimitive(label)); + for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) { + clone.add(e.getKey(), e.getValue()); + } + jsonObject = clone; + } + Streams.write(jsonObject, out); + } + }; + } + } +} |