From 21802d467aca3dcce8478ae7cdde58080452015a Mon Sep 17 00:00:00 2001 From: Adam Lowe Date: Wed, 20 Jun 2012 11:35:06 +0300 Subject: [PATCH] Adjusting general strategy to accept @Named in place of @SerializedName. Adding TypeAdapterFactory to handle deserialization based on constructor annotations (Inject/Named and/or ConstructorProperties). --- .../org/jclouds/json/config/GsonModule.java | 26 +- ...ructorAndReflectiveTypeAdapterFactory.java | 256 ++++++++++++++++++ .../json/internal/NamingStrategies.java | 226 ++++++++++++++++ ...orAndReflectiveTypeAdapterFactoryTest.java | 245 +++++++++++++++++ .../json/internal/NamingStrategiesTest.java | 203 ++++++++++++++ 5 files changed, 954 insertions(+), 2 deletions(-) create mode 100644 core/src/main/java/org/jclouds/json/internal/DeserializationConstructorAndReflectiveTypeAdapterFactory.java create mode 100644 core/src/main/java/org/jclouds/json/internal/NamingStrategies.java create mode 100644 core/src/test/java/org/jclouds/json/internal/DeserializationConstructorAndReflectiveTypeAdapterFactoryTest.java create mode 100644 core/src/test/java/org/jclouds/json/internal/NamingStrategiesTest.java diff --git a/core/src/main/java/org/jclouds/json/config/GsonModule.java b/core/src/main/java/org/jclouds/json/config/GsonModule.java index 937f518fc4..e5a3011b19 100644 --- a/core/src/main/java/org/jclouds/json/config/GsonModule.java +++ b/core/src/main/java/org/jclouds/json/config/GsonModule.java @@ -18,6 +18,7 @@ */ package org.jclouds.json.config; +import java.beans.ConstructorProperties; import java.io.IOException; import java.lang.reflect.Type; import java.util.Date; @@ -35,20 +36,29 @@ import org.jclouds.crypto.CryptoStreams; import org.jclouds.date.DateService; import org.jclouds.domain.JsonBall; import org.jclouds.json.Json; +import org.jclouds.json.internal.DeserializationConstructorAndReflectiveTypeAdapterFactory; import org.jclouds.json.internal.EnumTypeAdapterThatReturnsFromValue; import org.jclouds.json.internal.GsonWrapper; +import org.jclouds.json.internal.NamingStrategies.AnnotationConstructorNamingStrategy; +import org.jclouds.json.internal.NamingStrategies.AnnotationOrNameFieldNamingStrategy; +import org.jclouds.json.internal.NamingStrategies.ExtractNamed; +import org.jclouds.json.internal.NamingStrategies.ExtractSerializedName; import org.jclouds.json.internal.NullHackJsonLiteralAdapter; import org.jclouds.json.internal.OptionalTypeAdapterFactory; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap.Builder; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.primitives.Bytes; +import com.google.gson.FieldNamingStrategy; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapterFactory; +import com.google.gson.internal.ConstructorConstructor; +import com.google.gson.internal.Excluder; import com.google.gson.internal.JsonReaderInternalAccess; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; @@ -69,8 +79,12 @@ public class GsonModule extends AbstractModule { @Singleton Gson provideGson(TypeAdapter jsonAdapter, DateAdapter adapter, ByteListAdapter byteListAdapter, ByteArrayAdapter byteArrayAdapter, PropertiesAdapter propertiesAdapter, JsonAdapterBindings bindings) - throws ClassNotFoundException, Exception { - GsonBuilder builder = new GsonBuilder(); + throws Exception { + + FieldNamingStrategy serializationPolicy = new AnnotationOrNameFieldNamingStrategy(new ExtractSerializedName(), + new ExtractNamed()); + + GsonBuilder builder = new GsonBuilder().setFieldNamingStrategy(serializationPolicy); // simple (type adapters) builder.registerTypeAdapter(Properties.class, propertiesAdapter.nullSafe()); @@ -81,6 +95,14 @@ public class GsonModule extends AbstractModule { builder.registerTypeAdapter(JsonBall.class, jsonAdapter.nullSafe()); builder.registerTypeAdapterFactory(new OptionalTypeAdapterFactory()); + AnnotationConstructorNamingStrategy deserializationPolicy = + new AnnotationConstructorNamingStrategy( + ImmutableSet.of(ConstructorProperties.class, Inject.class), ImmutableSet.of(new ExtractNamed())); + + builder.registerTypeAdapterFactory( + new DeserializationConstructorAndReflectiveTypeAdapterFactory(new ConstructorConstructor(), + serializationPolicy, Excluder.DEFAULT, deserializationPolicy)); + // complicated (serializers/deserializers as they need context to operate) builder.registerTypeHierarchyAdapter(Enum.class, new EnumTypeAdapterThatReturnsFromValue()); diff --git a/core/src/main/java/org/jclouds/json/internal/DeserializationConstructorAndReflectiveTypeAdapterFactory.java b/core/src/main/java/org/jclouds/json/internal/DeserializationConstructorAndReflectiveTypeAdapterFactory.java new file mode 100644 index 0000000000..14ce076ce0 --- /dev/null +++ b/core/src/main/java/org/jclouds/json/internal/DeserializationConstructorAndReflectiveTypeAdapterFactory.java @@ -0,0 +1,256 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.json.internal; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Type; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.jclouds.json.internal.NamingStrategies.ConstructorFieldNamingStrategy; + +import com.google.gson.FieldNamingStrategy; +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.internal.$Gson$Types; +import com.google.gson.internal.ConstructorConstructor; +import com.google.gson.internal.Excluder; +import com.google.gson.internal.bind.ReflectiveTypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +/** + * Creates type adapters for types handled in the following ways: + *

+ *

+ *

Example: Using javax inject to select a constructor and corresponding named parameters

+ *

+ *

+ *    
+ * import NamingStrategies.*;
+ *
+ * serializationStrategy = new AnnotationOrNameFieldNamingStrategy(
+ *    new ExtractSerializedName(), new ExtractNamed());
+ * 
+ * deserializationStrategy = new AnnotationConstructorNamingStrategy(
+ *    Collections.singleton(javax.inject.Inject.class),
+ *    Collections.singleton(new ExtractNamed()));
+ *    
+ * factory = new DeserializationConstructorAndReflectiveTypeAdapterFactory(new ConstructorConstructor(),
+ *      serializationStrategy, Excluder.DEFAULT, deserializationStrategy);
+ *
+ * gson = new GsonBuilder(serializationStrategy).registerTypeAdapterFactory(factory).create();
+ *
+ * 
+ *

+ * The above would work fine on the following class, which has no gson-specific annotations: + *

+ *

+ * private static class ImmutableAndVerifiedInCtor {
+ *    final int foo;
+ *    @Named("_bar")
+ *    final int bar;
+ *
+ *    @Inject
+ *    ImmutableAndVerifiedInCtor(@Named("foo") int foo, @Named("_bar") int bar) {
+ *       if (foo < 0)
+ *          throw new IllegalArgumentException("negative!");
+ *       this.foo = foo;
+ *       this.bar = bar;
+ *    }
+ * }
+ * 
+ *

+ *
+ * + * @author Adrian Cole + * @author Adam Lowe + */ +public final class DeserializationConstructorAndReflectiveTypeAdapterFactory implements TypeAdapterFactory { + private final ConstructorFieldNamingStrategy constructorFieldNamingPolicy; + private final ReflectiveTypeAdapterFactory delegateFactory; + + /** + * @param constructorConstructor passed through to delegate ReflectiveTypeAdapterFactory for serialization + * @param serializationFieldNamingPolicy passed through to delegate ReflectiveTypeAdapterFactory for serialization + * @param excluder passed through to delegate ReflectiveTypeAdapterFactory for serialization + * @param deserializationFieldNamingPolicy + * determines which constructor to use and how to determine field names for + * deserialization + * @see ReflectiveTypeAdapterFactory + */ + public DeserializationConstructorAndReflectiveTypeAdapterFactory( + ConstructorConstructor constructorConstructor, + FieldNamingStrategy serializationFieldNamingPolicy, + Excluder excluder, + ConstructorFieldNamingStrategy deserializationFieldNamingPolicy) { + this.constructorFieldNamingPolicy = checkNotNull(deserializationFieldNamingPolicy, "deserializationFieldNamingPolicy"); + this.delegateFactory = new ReflectiveTypeAdapterFactory(constructorConstructor, checkNotNull(serializationFieldNamingPolicy, "fieldNamingPolicy"), checkNotNull(excluder, "excluder")); + } + + @SuppressWarnings("unchecked") + public TypeAdapter create(Gson gson, final TypeToken type) { + Class raw = type.getRawType(); + Constructor deserializationCtor = constructorFieldNamingPolicy.getDeserializationConstructor(raw); + + if (deserializationCtor == null) { + return null; // allow GSON to choose the correct Adapter (can't simply return delegateFactory.create()) + } else { + deserializationCtor.setAccessible(true); + return new DeserializeWithParameterizedConstructorSerializeWithDelegate(delegateFactory.create(gson, type), deserializationCtor, + getParameterReaders(gson, type, deserializationCtor)); + } + } + + private final class DeserializeWithParameterizedConstructorSerializeWithDelegate extends TypeAdapter { + private final Constructor parameterizedCtor; + private final Map parameterReaders; + private final TypeAdapter delegate; + + private DeserializeWithParameterizedConstructorSerializeWithDelegate(TypeAdapter delegate, + Constructor parameterizedCtor, Map parameterReaders) { + this.delegate = delegate; + this.parameterizedCtor = parameterizedCtor; + this.parameterReaders = parameterReaders; + } + + @Override + public T read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + + Class[] paramTypes = parameterizedCtor.getParameterTypes(); + Object[] ctorParams = new Object[paramTypes.length]; + + // TODO determine if we can drop this + for (int i = 0; i < paramTypes.length; i++) { + if (paramTypes[i] == boolean.class) { + ctorParams[i] = Boolean.FALSE; + } else if (paramTypes[i].isPrimitive()) { + ctorParams[i] = 0; + } + } + + try { + in.beginObject(); + while (in.hasNext()) { + String name = in.nextName(); + ParameterReader parameter = parameterReaders.get(name); + if (parameter == null) { + in.skipValue(); + } else { + Object value = parameter.read(in); + if (value != null) ctorParams[parameter.index] = value; + } + } + } catch (IllegalStateException e) { + throw new JsonSyntaxException(e); + } + + for (int i = 0; i < paramTypes.length; i++) { + if (paramTypes[i].isPrimitive()) { + checkArgument(ctorParams[i] != null, "Primative param[" + i + "] in constructor " + parameterizedCtor + + " cannot be absent!"); + } + } + in.endObject(); + return newInstance(ctorParams); + } + + /** + * pass to delegate + */ + @Override + public void write(JsonWriter out, T value) throws IOException { + delegate.write(out, value); + } + + @SuppressWarnings("unchecked") + private T newInstance(Object[] ctorParams) throws AssertionError { + try { + return (T) parameterizedCtor.newInstance(ctorParams); + } catch (InstantiationException e) { + throw new AssertionError(e); + } catch (IllegalAccessException e) { + throw new AssertionError(e); + } catch (InvocationTargetException e) { + if (e.getCause() instanceof RuntimeException) + throw RuntimeException.class.cast(e.getCause()); + throw new AssertionError(e); + } + } + } + + // logic borrowed from ReflectiveTypeAdapterFactory + static class ParameterReader { + final String name; + final int index; + final TypeAdapter typeAdapter; + + ParameterReader(String name, int index, TypeAdapter typeAdapter) { + this.name = name; + this.index = index; + this.typeAdapter = typeAdapter; + } + + public Object read(JsonReader reader) throws IOException { + return typeAdapter.read(reader); + } + } + + @SuppressWarnings("unchecked") + private Map getParameterReaders(Gson context, TypeToken declaring, Constructor constructor) { + Map result = new LinkedHashMap(); + + for (int index = 0; index < constructor.getGenericParameterTypes().length; index++) { + Type parameterType = getTypeOfConstructorParameter(declaring, constructor, index); + TypeAdapter adapter = context.getAdapter(TypeToken.get(parameterType)); + String parameterName = constructorFieldNamingPolicy.translateName(constructor, index); + checkArgument(parameterName != null, constructor + " parameter " + 0 + " failed to be named by " + constructorFieldNamingPolicy); + ParameterReader parameterReader = new ParameterReader(parameterName, index, adapter); + ParameterReader previous = result.put(parameterReader.name, parameterReader); + checkArgument(previous == null, constructor + " declares multiple JSON parameters named " + parameterReader.name); + } + + return result; + } + + private Type getTypeOfConstructorParameter(TypeToken declaring, Constructor constructor, int index) { + Type genericParameter = constructor.getGenericParameterTypes()[index]; + return $Gson$Types.resolve(declaring.getType(), declaring.getRawType(), genericParameter); + } +} diff --git a/core/src/main/java/org/jclouds/json/internal/NamingStrategies.java b/core/src/main/java/org/jclouds/json/internal/NamingStrategies.java new file mode 100644 index 0000000000..e3a3c3d55d --- /dev/null +++ b/core/src/main/java/org/jclouds/json/internal/NamingStrategies.java @@ -0,0 +1,226 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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 org.jclouds.json.internal; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import java.beans.ConstructorProperties; +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.util.Map; +import java.util.Set; + +import javax.inject.Named; + +import com.google.common.base.Function; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; +import com.google.gson.FieldNamingStrategy; +import com.google.gson.annotations.SerializedName; + +/** + * NamingStrategies used for JSON deserialization using GSON + * + * @author Adrian Cole + * @author Adam Lowe + */ +public class NamingStrategies { + /** + * Specifies how to extract the name from an annotation for use in determining the serialized + * name. + * + * @see com.google.gson.annotations.SerializedName + * @see ExtractSerializedName + */ + public abstract static class NameExtractor { + protected final Class annotationType; + + protected NameExtractor(Class annotationType) { + this.annotationType = checkNotNull(annotationType, "annotationType"); + } + + public abstract String extractName(A in); + + public Class annotationType() { + return annotationType; + } + + @Override + public String toString() { + return "nameExtractor(" + annotationType.getSimpleName() + ")"; + } + + @Override + public int hashCode() { + return annotationType.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null || getClass() != obj.getClass()) + return false; + return annotationType.equals(NameExtractor.class.cast(obj).annotationType); + } + } + + public static class ExtractSerializedName extends NameExtractor { + public ExtractSerializedName() { + super(SerializedName.class); + } + + @Override + public String extractName(SerializedName in) { + return checkNotNull(in, "input annotation").value(); + } + } + + public static class ExtractNamed extends NameExtractor { + public ExtractNamed() { + super(Named.class); + } + + @Override + public String extractName(Named in) { + return checkNotNull(in, "input annotation").value(); + } + } + + public static abstract class AnnotationBasedNamingStrategy { + protected final Map, ? extends NameExtractor> annotationToNameExtractor; + private String forToString; + + @SuppressWarnings("unchecked") + public AnnotationBasedNamingStrategy(Iterable extractors) { + checkNotNull(extractors, "means to extract names by annotations"); + + this.annotationToNameExtractor = Maps.uniqueIndex(extractors, new Function>() { + @Override + public Class apply(NameExtractor input) { + return input.annotationType(); + } + }); + this.forToString = Joiner.on(",").join(Iterables.transform(extractors, new Function() { + @Override + public String apply(NameExtractor input) { + return input.annotationType().getName(); + } + })); + } + + @Override + public String toString() { + return "AnnotationBasedNamingStrategy requiring one of " + forToString; + } + } + + /** + * Definition of field naming policy for annotation-based field + */ + public static class AnnotationFieldNamingStrategy extends AnnotationBasedNamingStrategy implements FieldNamingStrategy { + + public AnnotationFieldNamingStrategy(Iterable extractors) { + super(extractors); + checkArgument(extractors.iterator().hasNext(), "you must supply at least one name extractor, for example: " + + ExtractSerializedName.class.getSimpleName()); + } + + @SuppressWarnings("unchecked") + @Override + public String translateName(Field f) { + for (Annotation annotation : f.getAnnotations()) { + if (annotationToNameExtractor.containsKey(annotation.annotationType())) { + return annotationToNameExtractor.get(annotation.annotationType()).extractName(annotation); + } + } + return null; + } + } + + public static class AnnotationOrNameFieldNamingStrategy extends AnnotationFieldNamingStrategy implements FieldNamingStrategy { + public AnnotationOrNameFieldNamingStrategy(NameExtractor... extractors) { + this(ImmutableSet.copyOf(extractors)); + } + + public AnnotationOrNameFieldNamingStrategy(Iterable extractors) { + super(extractors); + } + + @Override + public String translateName(Field f) { + String result = super.translateName(f); + return result == null ? f.getName() : result; + } + } + + public static interface ConstructorFieldNamingStrategy { + public String translateName(Constructor c, int index); + + public Constructor getDeserializationConstructor(Class raw); + + } + + /** + * Determines field naming from constructor annotations + */ + public static class AnnotationConstructorNamingStrategy extends AnnotationBasedNamingStrategy implements ConstructorFieldNamingStrategy { + private final Set> markers; + + public AnnotationConstructorNamingStrategy(Iterable> markers, Iterable extractors) { + super(extractors); + this.markers = ImmutableSet.copyOf(checkNotNull(markers, "you must supply at least one annotation to mark deserialization constructors")); + } + + @SuppressWarnings("unchecked") + public Constructor getDeserializationConstructor(Class raw) { + for (Constructor ctor : raw.getDeclaredConstructors()) + for (Class deserializationCtorAnnotation : markers) + if (ctor.isAnnotationPresent(deserializationCtorAnnotation)) + return (Constructor) ctor; + + return null; + } + + @SuppressWarnings("unchecked") + @Override + public String translateName(Constructor c, int index) { + String name = null; + + if (markers.contains(ConstructorProperties.class) && c.getAnnotation(ConstructorProperties.class) != null) { + String[] names = c.getAnnotation(ConstructorProperties.class).value(); + if (names != null && names.length > index) { + name = names[index]; + } + } + + for (Annotation annotation : c.getParameterAnnotations()[index]) { + if (annotationToNameExtractor.containsKey(annotation.annotationType())) { + name = annotationToNameExtractor.get(annotation.annotationType()).extractName(annotation); + break; + } + } + return name; + } + } +} diff --git a/core/src/test/java/org/jclouds/json/internal/DeserializationConstructorAndReflectiveTypeAdapterFactoryTest.java b/core/src/test/java/org/jclouds/json/internal/DeserializationConstructorAndReflectiveTypeAdapterFactoryTest.java new file mode 100644 index 0000000000..995474fe0f --- /dev/null +++ b/core/src/test/java/org/jclouds/json/internal/DeserializationConstructorAndReflectiveTypeAdapterFactoryTest.java @@ -0,0 +1,245 @@ +package org.jclouds.json.internal; +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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. + */ + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotSame; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.fail; + +import java.beans.ConstructorProperties; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; +import javax.inject.Named; + +import org.jclouds.json.internal.NamingStrategies.AnnotationOrNameFieldNamingStrategy; +import org.jclouds.json.internal.NamingStrategies.ExtractNamed; +import org.jclouds.json.internal.NamingStrategies.ExtractSerializedName; +import org.testng.annotations.Test; + +import com.google.common.collect.ImmutableSet; +import com.google.gson.FieldNamingStrategy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.TypeAdapter; +import com.google.gson.internal.ConstructorConstructor; +import com.google.gson.internal.Excluder; +import com.google.gson.reflect.TypeToken; + +/** + * @author Adrian Cole + * @author Adam Lowe + */ +@Test(testName = "DeserializationConstructorTypeAdapterFactoryTest") +public final class DeserializationConstructorAndReflectiveTypeAdapterFactoryTest { + + Gson gson = new Gson(); + + DeserializationConstructorAndReflectiveTypeAdapterFactory parameterizedCtorFactory = parameterizedCtorFactory(); + + static DeserializationConstructorAndReflectiveTypeAdapterFactory parameterizedCtorFactory() { + FieldNamingStrategy serializationPolicy = new AnnotationOrNameFieldNamingStrategy( + ImmutableSet.of(new ExtractSerializedName(), new ExtractNamed()) + ); + NamingStrategies.AnnotationConstructorNamingStrategy deserializationPolicy = + new NamingStrategies.AnnotationConstructorNamingStrategy( + ImmutableSet.of(ConstructorProperties.class, Inject.class), + ImmutableSet.of(new ExtractNamed())); + + return new DeserializationConstructorAndReflectiveTypeAdapterFactory(new ConstructorConstructor(), + serializationPolicy, Excluder.DEFAULT, deserializationPolicy); + } + + public void testNullWhenPrimitive() { + assertNull(parameterizedCtorFactory.create(gson, TypeToken.get(int.class))); + } + + private static class DefaultConstructor { + int foo; + int bar; + + private DefaultConstructor() { + } + + @Override + public boolean equals(Object obj) { + DefaultConstructor other = DefaultConstructor.class.cast(obj); + if (bar != other.bar) + return false; + if (foo != other.foo) + return false; + return true; + } + + } + + public void testRejectsIfNoConstuctorMarked() throws IOException { + TypeAdapter adapter = parameterizedCtorFactory.create(gson, TypeToken.get(DefaultConstructor.class)); + assertNull(adapter); + } + + private static class WithDeserializationConstructorButWithoutSerializedName { + final int foo; + + @Inject + WithDeserializationConstructorButWithoutSerializedName(int foo) { + this.foo = foo; + } + } + + public void testSerializedNameRequiredOnAllParameters() { + try { + parameterizedCtorFactory.create(gson, TypeToken + .get(WithDeserializationConstructorButWithoutSerializedName.class)); + fail(); + } catch (IllegalArgumentException actual) { + assertEquals(actual.getMessage(), + "org.jclouds.json.internal.DeserializationConstructorAndReflectiveTypeAdapterFactoryTest$WithDeserializationConstructorButWithoutSerializedName(int)" + + " parameter 0 failed to be named by AnnotationBasedNamingStrategy requiring one of javax.inject.Named"); + } + } + + private static class DuplicateSerializedNames { + final int foo; + final int bar; + + @Inject + DuplicateSerializedNames(@Named("foo") int foo, @Named("foo") int bar) { + this.foo = foo; + this.bar = bar; + } + } + + public void testNoDuplicateSerializedNamesRequiredOnAllParameters() { + try { + parameterizedCtorFactory.create(gson, TypeToken.get(DuplicateSerializedNames.class)); + fail(); + } catch (IllegalArgumentException actual) { + assertEquals(actual.getMessage(), + "org.jclouds.json.internal.DeserializationConstructorAndReflectiveTypeAdapterFactoryTest$DuplicateSerializedNames(int,int)" + + " declares multiple JSON parameters named foo"); + } + } + + private static class ValidatedConstructor { + final int foo; + final int bar; + + @Inject + ValidatedConstructor(@Named("foo") int foo, @Named("bar") int bar) { + if (foo < 0) + throw new IllegalArgumentException("negative!"); + this.foo = foo; + this.bar = bar; + } + + @Override + public boolean equals(Object obj) { + ValidatedConstructor other = ValidatedConstructor.class.cast(obj); + if (bar != other.bar) + return false; + if (foo != other.foo) + return false; + return true; + } + + } + + public void testValidatedConstructor() throws IOException { + TypeAdapter adapter = parameterizedCtorFactory.create(gson, TypeToken + .get(ValidatedConstructor.class)); + assertEquals(new ValidatedConstructor(0, 1), adapter.fromJson("{\"foo\":0,\"bar\":1}")); + try { + adapter.fromJson("{\"foo\":-1,\"bar\":1}"); + fail(); + } catch (IllegalArgumentException expected) { + assertEquals("negative!", expected.getMessage()); + } + } + + private static class GenericParamsCopiedIn { + final List foo; + final Map bar; + + @Inject + GenericParamsCopiedIn(@Named("foo") List foo, @Named("bar") Map bar) { + this.foo = new ArrayList(foo); + this.bar = new HashMap(bar); + } + + } + + public void testGenericParamsCopiedIn() throws IOException { + TypeAdapter adapter = parameterizedCtorFactory.create(gson, TypeToken + .get(GenericParamsCopiedIn.class)); + List inputFoo = new ArrayList(); + inputFoo.add("one"); + HashMap inputBar = new HashMap(); + inputBar.put("2", "two"); + + GenericParamsCopiedIn toTest = adapter.fromJson("{ \"foo\":[\"one\"], \"bar\":{ \"2\":\"two\"}}"); + assertEquals(inputFoo, toTest.foo); + assertNotSame(inputFoo, toTest.foo); + assertEquals(inputBar, toTest.bar); + + } + + private static class RenamedFields { + final int foo; + @Named("_bar") + final int bar; + + @ConstructorProperties({"foo", "_bar"}) + RenamedFields(int foo, int bar) { + if (foo < 0) + throw new IllegalArgumentException("negative!"); + this.foo = foo; + this.bar = bar; + } + + @Override + public boolean equals(Object obj) { + RenamedFields other = RenamedFields.class.cast(obj); + if (bar != other.bar) + return false; + if (foo != other.foo) + return false; + return true; + } + + } + + public void testRenamedFields() throws IOException { + TypeAdapter adapter = parameterizedCtorFactory.create(gson, TypeToken.get(RenamedFields.class)); + assertEquals(new RenamedFields(0, 1), adapter.fromJson("{\"foo\":0,\"_bar\":1}")); + assertEquals(adapter.toJson(new RenamedFields(0, 1)), "{\"foo\":0,\"_bar\":1}"); + } + + public void testCanOverrideDefault() throws IOException { + Gson gson = new GsonBuilder().registerTypeAdapterFactory(parameterizedCtorFactory).create(); + + assertEquals(new RenamedFields(0, 1), gson.fromJson("{\"foo\":0,\"_bar\":1}", RenamedFields.class)); + assertEquals(gson.toJson(new RenamedFields(0, 1)), "{\"foo\":0,\"_bar\":1}"); + } +} diff --git a/core/src/test/java/org/jclouds/json/internal/NamingStrategiesTest.java b/core/src/test/java/org/jclouds/json/internal/NamingStrategiesTest.java new file mode 100644 index 0000000000..1397775bef --- /dev/null +++ b/core/src/test/java/org/jclouds/json/internal/NamingStrategiesTest.java @@ -0,0 +1,203 @@ +package org.jclouds.json.internal; +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you 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. + */ + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.fail; + +import java.beans.ConstructorProperties; +import java.lang.reflect.Constructor; + +import javax.inject.Inject; +import javax.inject.Named; + +import org.jclouds.json.internal.NamingStrategies.AnnotationConstructorNamingStrategy; +import org.jclouds.json.internal.NamingStrategies.AnnotationFieldNamingStrategy; +import org.jclouds.json.internal.NamingStrategies.AnnotationOrNameFieldNamingStrategy; +import org.jclouds.json.internal.NamingStrategies.ConstructorFieldNamingStrategy; +import org.jclouds.json.internal.NamingStrategies.ExtractNamed; +import org.jclouds.json.internal.NamingStrategies.ExtractSerializedName; +import org.jclouds.json.internal.NamingStrategies.NameExtractor; +import org.testng.annotations.Test; + +import com.google.common.collect.ImmutableSet; +import com.google.gson.FieldNamingStrategy; +import com.google.gson.annotations.SerializedName; + +/** + * @author Adam Lowe + */ +@Test(testName = "NamingStrategiesTest") +public final class NamingStrategiesTest { + + private static class SimpleTest { + @SerializedName("aardvark") + private String a; + private String b; + @Named("cat") + private String c; + @Named("dog") + private String d; + + @ConstructorProperties({"aardvark", "bat", "coyote", "dog"}) + private SimpleTest(String aa, String bb, String cc, @Named("dingo") String dd) { + } + + @Inject + private SimpleTest(@Named("aa") String aa, @Named("bb") String bb, @Named("cc") String cc, @Named("dd") String dd, boolean nothing) { + } + } + + private static class MixedConstructorTest { + @Inject + @ConstructorProperties("thiscanbeoverriddenbyNamed") + private MixedConstructorTest(@Named("aardvark") String aa, @Named("bat") String bb, @Named("cat") String cc, @Named("dog") String dd) { + } + } + + + public void testExtractSerializedName() throws Exception { + NameExtractor extractor = new ExtractSerializedName(); + assertEquals(extractor.extractName(SimpleTest.class.getDeclaredField("a").getAnnotation(SerializedName.class)), + "aardvark"); + try { + extractor.extractName(SimpleTest.class.getDeclaredField("b").getAnnotation(SerializedName.class)); + fail(); + } catch (NullPointerException e) { + } + try { + extractor.extractName(SimpleTest.class.getDeclaredField("c").getAnnotation(SerializedName.class)); + fail(); + } catch (NullPointerException e) { + } + try { + extractor.extractName(SimpleTest.class.getDeclaredField("d").getAnnotation(SerializedName.class)); + fail(); + } catch (NullPointerException e) { + } + } + + public void testExtractNamed() throws Exception { + NameExtractor extractor = new ExtractNamed(); + try { + extractor.extractName(SimpleTest.class.getDeclaredField("a").getAnnotation(Named.class)); + } catch (NullPointerException e) { + } + try { + extractor.extractName(SimpleTest.class.getDeclaredField("b").getAnnotation(Named.class)); + fail(); + } catch (NullPointerException e) { + } + assertEquals(extractor.extractName(SimpleTest.class.getDeclaredField("c").getAnnotation(Named.class)), + "cat"); + assertEquals(extractor.extractName(SimpleTest.class.getDeclaredField("d").getAnnotation(Named.class)), + "dog"); + } + + public void testAnnotationFieldNamingStrategy() throws Exception { + FieldNamingStrategy strategy = new AnnotationFieldNamingStrategy(ImmutableSet.of(new ExtractNamed())); + + assertNull(strategy.translateName(SimpleTest.class.getDeclaredField("a"))); + assertNull(strategy.translateName(SimpleTest.class.getDeclaredField("b"))); + assertEquals(strategy.translateName(SimpleTest.class.getDeclaredField("c")), "cat"); + assertEquals(strategy.translateName(SimpleTest.class.getDeclaredField("d")), "dog"); + } + + public void testAnnotationOrNameFieldNamingStrategy() throws Exception { + FieldNamingStrategy strategy = new AnnotationOrNameFieldNamingStrategy(ImmutableSet.of(new ExtractNamed())); + + assertEquals(strategy.translateName(SimpleTest.class.getDeclaredField("a")), "a"); + assertEquals(strategy.translateName(SimpleTest.class.getDeclaredField("b")), "b"); + assertEquals(strategy.translateName(SimpleTest.class.getDeclaredField("c")), "cat"); + assertEquals(strategy.translateName(SimpleTest.class.getDeclaredField("d")), "dog"); + } + + public void testAnnotationConstructorFieldNamingStrategyCPAndNamed() throws Exception { + ConstructorFieldNamingStrategy strategy = new AnnotationConstructorNamingStrategy( + ImmutableSet.of(ConstructorProperties.class), ImmutableSet.of(new ExtractNamed())); + + Constructor constructor = strategy.getDeserializationConstructor(SimpleTest.class); + assertNotNull(constructor); + assertEquals(constructor.getParameterTypes().length, 4); + + assertEquals(strategy.translateName(constructor, 0), "aardvark"); + assertEquals(strategy.translateName(constructor, 1), "bat"); + assertEquals(strategy.translateName(constructor, 2), "coyote"); + // Note: @Named overrides the ConstructorProperties setting + assertEquals(strategy.translateName(constructor, 3), "dingo"); + + Constructor mixedCtor = strategy.getDeserializationConstructor(MixedConstructorTest.class); + assertNotNull(mixedCtor); + assertEquals(mixedCtor.getParameterTypes().length, 4); + + assertEquals(strategy.translateName(mixedCtor, 0), "aardvark"); + assertEquals(strategy.translateName(mixedCtor, 1), "bat"); + assertEquals(strategy.translateName(mixedCtor, 2), "cat"); + assertEquals(strategy.translateName(mixedCtor, 3), "dog"); + } + + public void testAnnotationConstructorFieldNamingStrategyCP() throws Exception { + ConstructorFieldNamingStrategy strategy = new AnnotationConstructorNamingStrategy( + ImmutableSet.of(ConstructorProperties.class), ImmutableSet.of()); + + Constructor constructor = strategy.getDeserializationConstructor(SimpleTest.class); + assertNotNull(constructor); + assertEquals(constructor.getParameterTypes().length, 4); + + assertEquals(strategy.translateName(constructor, 0), "aardvark"); + assertEquals(strategy.translateName(constructor, 1), "bat"); + assertEquals(strategy.translateName(constructor, 2), "coyote"); + assertEquals(strategy.translateName(constructor, 3), "dog"); + + Constructor mixedCtor = strategy.getDeserializationConstructor(MixedConstructorTest.class); + assertNotNull(mixedCtor); + assertEquals(mixedCtor.getParameterTypes().length, 4); + + assertEquals(strategy.translateName(mixedCtor, 0), "thiscanbeoverriddenbyNamed"); + assertNull(strategy.translateName(mixedCtor, 1)); + assertNull(strategy.translateName(mixedCtor, 2)); + assertNull(strategy.translateName(mixedCtor, 3)); + } + + public void testAnnotationConstructorFieldNamingStrategyInject() throws Exception { + ConstructorFieldNamingStrategy strategy = new AnnotationConstructorNamingStrategy( + ImmutableSet.of(Inject.class), ImmutableSet.of(new ExtractNamed())); + + Constructor constructor = strategy.getDeserializationConstructor(SimpleTest.class); + assertNotNull(constructor); + assertEquals(constructor.getParameterTypes().length, 5); + + assertEquals(strategy.translateName(constructor, 0), "aa"); + assertEquals(strategy.translateName(constructor, 1), "bb"); + assertEquals(strategy.translateName(constructor, 2), "cc"); + assertEquals(strategy.translateName(constructor, 3), "dd"); + + Constructor mixedCtor = strategy.getDeserializationConstructor(MixedConstructorTest.class); + assertNotNull(mixedCtor); + assertEquals(mixedCtor.getParameterTypes().length, 4); + + assertEquals(strategy.translateName(mixedCtor, 0), "aardvark"); + assertEquals(strategy.translateName(mixedCtor, 1), "bat"); + assertEquals(strategy.translateName(mixedCtor, 2), "cat"); + assertEquals(strategy.translateName(mixedCtor, 3), "dog"); + } + +}