From 83604e943a38a365e9b8e7ce3df8e2aff39c94e5 Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Wed, 29 Oct 2014 21:04:06 -0700 Subject: [PATCH] JCLOUDS-750 support serializing auto-value types without custom type adapters. --- core/pom.xml | 5 + ...ructorAndReflectiveTypeAdapterFactory.java | 22 +++- .../test/java/org/jclouds/json/JsonTest.java | 101 +++++++++++++----- project/pom.xml | 6 ++ 4 files changed, 104 insertions(+), 30 deletions(-) diff --git a/core/pom.xml b/core/pom.xml index 3b9589c740..2d0d53ccda 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -108,6 +108,11 @@ 4.2.0 provided + + com.google.auto.value + auto-value + test + diff --git a/core/src/main/java/org/jclouds/json/internal/DeserializationConstructorAndReflectiveTypeAdapterFactory.java b/core/src/main/java/org/jclouds/json/internal/DeserializationConstructorAndReflectiveTypeAdapterFactory.java index 87e9d32846..de985fd8fc 100644 --- a/core/src/main/java/org/jclouds/json/internal/DeserializationConstructorAndReflectiveTypeAdapterFactory.java +++ b/core/src/main/java/org/jclouds/json/internal/DeserializationConstructorAndReflectiveTypeAdapterFactory.java @@ -22,6 +22,7 @@ import static org.jclouds.reflect.Reflection2.typeToken; import java.io.IOException; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; import java.util.List; import java.util.Map; @@ -118,14 +119,25 @@ public final class DeserializationConstructorAndReflectiveTypeAdapterFactory imp public TypeAdapter create(Gson gson, TypeToken type) { com.google.common.reflect.TypeToken token = typeToken(type.getType()); - Invokable deserializationCtor = constructorFieldNamingPolicy.getDeserializer(token); + Invokable deserializationTarget = constructorFieldNamingPolicy.getDeserializer(token); - if (deserializationCtor == null) { + if (deserializationTarget == null) { return null; // allow GSON to choose the correct Adapter (can't simply return delegateFactory.create()) - } else { - return new DeserializeIntoParameterizedConstructor(delegateFactory.create(gson, type), deserializationCtor, - getParameterReaders(gson, deserializationCtor)); } + // @AutoValue is SOURCE retention, which means it cannot be looked up at runtime. + // Assume abstract types built by static methods are AutoValue. + if (Modifier.isAbstract(type.getRawType().getModifiers()) && deserializationTarget.isStatic()) { + // Lookup the generated AutoValue class, whose fields must be read for serialization. + String packageName = type.getRawType().getPackage().getName(); + String autoClassName = type.getRawType().getName().replace('$', '_') + .replace(packageName + ".", packageName + ".AutoValue_"); + try { + type = (TypeToken) TypeToken.get(Class.forName(autoClassName)); + } catch (ClassNotFoundException ignored) { + } + } + return new DeserializeIntoParameterizedConstructor(delegateFactory.create(gson, type), deserializationTarget, + getParameterReaders(gson, deserializationTarget)); } private final class DeserializeIntoParameterizedConstructor extends TypeAdapter { diff --git a/core/src/test/java/org/jclouds/json/JsonTest.java b/core/src/test/java/org/jclouds/json/JsonTest.java index 6a0b6aa360..cf477b482f 100644 --- a/core/src/test/java/org/jclouds/json/JsonTest.java +++ b/core/src/test/java/org/jclouds/json/JsonTest.java @@ -20,24 +20,35 @@ import static com.google.common.io.BaseEncoding.base16; import static com.google.common.primitives.Bytes.asList; import static org.testng.Assert.assertEquals; +import java.io.IOException; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.Set; import org.jclouds.javax.annotation.Nullable; import org.jclouds.json.config.GsonModule; import org.jclouds.json.config.GsonModule.DefaultExclusionStrategy; import org.testng.annotations.Test; -import com.google.common.base.Objects; +import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.gson.FieldAttributes; import com.google.gson.FieldNamingPolicy; import com.google.gson.FieldNamingStrategy; +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; import com.google.inject.AbstractModule; import com.google.inject.Guice; +import com.google.inject.Provides; import com.google.inject.TypeLiteral; @Test @@ -214,7 +225,8 @@ public class JsonTest { EnumInsideWithParser.Test.UNRECOGNIZED); } - private abstract static class UpperCamelCasedType { + @AutoValue + abstract static class UpperCamelCasedType { abstract String id(); @Nullable abstract Map volumes(); @@ -222,7 +234,7 @@ public class JsonTest { // Currently, this only works for deserialization. Need to set a naming policy for serialization! @SerializedNames({ "Id", "Volumes" }) private static UpperCamelCasedType create(String id, Map volumes) { - return new UpperCamelCasedTypeImpl(id, volumes); + return new AutoValue_JsonTest_UpperCamelCasedType(id, volumes); } } @@ -251,36 +263,75 @@ public class JsonTest { UpperCamelCasedType.create("1234", null)); } - private static class UpperCamelCasedTypeImpl extends UpperCamelCasedType { - private final String id; - private final Map volumes; + @AutoValue + abstract static class NestedCamelCasedType { + abstract UpperCamelCasedType item(); - private UpperCamelCasedTypeImpl(String id, Map volumes) { - this.id = id; - this.volumes = volumes; + abstract List items(); + + // Currently, this only works for deserialization. Need to set a naming policy for serialization! + @SerializedNames({ "Item", "Items" }) + private static NestedCamelCasedType create(UpperCamelCasedType item, List items) { + return new AutoValue_JsonTest_NestedCamelCasedType(item, items); } + } - @Override String id() { - return id; - } + private final NestedCamelCasedType nested = NestedCamelCasedType + .create(UpperCamelCasedType.create("1234", Collections.emptyMap()), + Arrays.asList(UpperCamelCasedType.create("5678", ImmutableMap.of("Foo", "Bar")))); - @Override @Nullable Map volumes() { - return volumes; - } - - @Override public boolean equals(Object o) { - if (o == this) { - return true; + public void nestedCamelCasedType() { + Json json = Guice.createInjector(new GsonModule(), new AbstractModule() { + @Override protected void configure() { + bind(FieldNamingStrategy.class).toInstance(FieldNamingPolicy.UPPER_CAMEL_CASE); } - if (o instanceof UpperCamelCasedType) { - UpperCamelCasedType that = (UpperCamelCasedType) o; - return Objects.equal(this.id, that.id()) && Objects.equal(this.volumes, that.volumes()); + }).getInstance(Json.class); + + String spinalJson = "{\"Item\":{\"Id\":\"1234\",\"Volumes\":{}},\"Items\":[{\"Id\":\"5678\",\"Volumes\":{\"Foo\":\"Bar\"}}]}"; + + assertEquals(json.toJson(nested), spinalJson); + assertEquals(json.fromJson(spinalJson, NestedCamelCasedType.class), nested); + } + + public void nestedCamelCasedTypeOverriddenTypeAdapterFactory() { + Json json = Guice.createInjector(new GsonModule(), new AbstractModule() { + @Override protected void configure() { } - return false; + + @Provides public Set typeAdapterFactories() { + return ImmutableSet.of(new NestedCamelCasedTypeAdapterFactory()); + } + }).getInstance(Json.class); + + assertEquals(json.toJson(nested), "{\"id\":\"1234\",\"count\":1}"); + assertEquals(json.fromJson("{\"id\":\"1234\",\"count\":1}", NestedCamelCasedType.class), nested); + } + + private class NestedCamelCasedTypeAdapterFactory extends TypeAdapter + implements TypeAdapterFactory { + + @Override public TypeAdapter create(Gson gson, TypeToken typeToken) { + if (!(NestedCamelCasedType.class.isAssignableFrom(typeToken.getRawType()))) { + return null; + } + return (TypeAdapter) this; } - @Override public int hashCode() { - return Objects.hashCode(id, volumes); + @Override public void write(JsonWriter out, NestedCamelCasedType value) throws IOException { + out.beginObject(); + out.name("id").value(value.item().id()); + out.name("count").value(value.items().size()); + out.endObject(); + } + + @Override public NestedCamelCasedType read(JsonReader in) throws IOException { + in.beginObject(); + in.nextName(); + in.nextString(); + in.nextName(); + in.nextInt(); + in.endObject(); + return nested; } } } diff --git a/project/pom.xml b/project/pom.xml index 566d7f9bb0..a621e1b96a 100644 --- a/project/pom.xml +++ b/project/pom.xml @@ -196,6 +196,7 @@ 2.17 1.6.1 1.2.0 + 1.0-rc2 http://archive.apache.org/dist/commons/logging/binaries/commons-logging-1.1.1-bin.tar.gz @@ -290,6 +291,11 @@ assertj-guava ${assertj-guava.version} + + com.google.auto.value + auto-value + ${auto-value.version} +