From b6e26fd60608660c059375d035afb293c4293628 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Mon, 11 Aug 2014 09:28:10 -0400 Subject: [PATCH] Import Gson's type resolver. --- .../java/com/squareup/moshi/TypeLiteral.java | 105 ++++ .../main/java/com/squareup/moshi/Types.java | 542 ++++++++++++++++++ .../java/com/squareup/moshi/TypesTest.java | 134 +++++ 3 files changed, 781 insertions(+) create mode 100644 moshi/src/main/java/com/squareup/moshi/TypeLiteral.java create mode 100644 moshi/src/main/java/com/squareup/moshi/Types.java create mode 100644 moshi/src/test/java/com/squareup/moshi/TypesTest.java diff --git a/moshi/src/main/java/com/squareup/moshi/TypeLiteral.java b/moshi/src/main/java/com/squareup/moshi/TypeLiteral.java new file mode 100644 index 0000000..6172a55 --- /dev/null +++ b/moshi/src/main/java/com/squareup/moshi/TypeLiteral.java @@ -0,0 +1,105 @@ +/* + * 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.squareup.moshi; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +/** + * Represents a generic type {@code T}. Java doesn't yet provide a way to represent generic types, + * so this class does. Forces clients to create a subclass of this class which enables retrieval the + * type information even at runtime. + * + *

For example, to create a type literal for {@code List}, you can create an empty + * anonymous inner class:

   {@code
+ *
+ *   TypeToken> list = new TypeToken>() {};
+ * }
+ * + *

This syntax cannot be used to create type literals that have wildcard parameters, such as + * {@code Class} or {@code List}. + * + * @author Bob Lee + * @author Sven Mawson + * @author Jesse Wilson + */ +public class TypeLiteral { + final Class rawType; + final Type type; + final int hashCode; + + /** + * Constructs a new type literal. Derives represented class from type parameter. + * + *

Clients create an empty anonymous subclass. Doing so embeds the type parameter in the + * anonymous class's type hierarchy so we can reconstitute it at runtime despite erasure. + */ + @SuppressWarnings("unchecked") + protected TypeLiteral() { + this.type = getSuperclassTypeParameter(getClass()); + this.rawType = (Class) Types.getRawType(type); + this.hashCode = type.hashCode(); + } + + /** Unsafe. Constructs a type literal manually. */ + @SuppressWarnings("unchecked") + TypeLiteral(Type type) { + if (type == null) throw new NullPointerException("type == null"); + this.type = Types.canonicalize(type); + this.rawType = (Class) Types.getRawType(this.type); + this.hashCode = this.type.hashCode(); + } + + /** Returns the type from super class's type parameter in canonical for}. */ + static Type getSuperclassTypeParameter(Class subclass) { + Type superclass = subclass.getGenericSuperclass(); + if (superclass instanceof Class) throw new IllegalArgumentException("Missing type parameter."); + ParameterizedType parameterized = (ParameterizedType) superclass; + return Types.canonicalize(parameterized.getActualTypeArguments()[0]); + } + + /** Returns the raw (non-generic) type for this type literal. */ + public final Class getRawType() { + return rawType; + } + + public final Type getType() { + return type; + } + + @Override public final int hashCode() { + return this.hashCode; + } + + @Override public final boolean equals(Object o) { + return o instanceof TypeLiteral + && Types.equals(type, ((TypeLiteral) o).type); + } + + @Override public final String toString() { + return Types.typeToString(type); + } + + /** Returns a type literal for {@code type}. */ + public static TypeLiteral get(Type type) { + return new TypeLiteral(type); + } + + /** Returns a type literal for {@code type}. */ + public static TypeLiteral get(Class type) { + return new TypeLiteral(type); + } +} diff --git a/moshi/src/main/java/com/squareup/moshi/Types.java b/moshi/src/main/java/com/squareup/moshi/Types.java new file mode 100644 index 0000000..8c4f584 --- /dev/null +++ b/moshi/src/main/java/com/squareup/moshi/Types.java @@ -0,0 +1,542 @@ +/** + * 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.squareup.moshi; + +import java.lang.reflect.Array; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.GenericDeclaration; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Properties; + +/** + * Static methods for working with types. + * + * @author Bob Lee + * @author Jesse Wilson + */ +final class Types { + static final Type[] EMPTY_TYPE_ARRAY = new Type[] {}; + + private Types() { + } + + /** + * Returns a new parameterized type, applying {@code typeArguments} to {@code rawType} and + * enclosed by {@code ownerType}. + */ + public static ParameterizedType newParameterizedTypeWithOwner( + Type ownerType, Type rawType, Type... typeArguments) { + return new ParameterizedTypeImpl(ownerType, rawType, typeArguments); + } + + /** Returns an array type whose elements are all instances of {@code componentType}. */ + public static GenericArrayType arrayOf(Type componentType) { + return new GenericArrayTypeImpl(componentType); + } + + /** + * Returns a type that represents an unknown type that extends {@code bound}. For example, if + * {@code bound} is {@code CharSequence.class}, this returns {@code ? extends CharSequence}. If + * {@code bound} is {@code Object.class}, this returns {@code ?}, which is shorthand for {@code + * ? extends Object}. + */ + public static WildcardType subtypeOf(Type bound) { + return new WildcardTypeImpl(new Type[] { bound }, EMPTY_TYPE_ARRAY); + } + + /** + * Returns a type that represents an unknown supertype of {@code bound}. For example, if {@code + * bound} is {@code String.class}, this returns {@code ? super String}. + */ + public static WildcardType supertypeOf(Type bound) { + return new WildcardTypeImpl(new Type[] { Object.class }, new Type[] { bound }); + } + + /** + * Returns a type that is functionally equal but not necessarily equal according to {@link + * Object#equals(Object) Object.equals()}. + */ + public static Type canonicalize(Type type) { + if (type instanceof Class) { + Class c = (Class) type; + return c.isArray() ? new GenericArrayTypeImpl(canonicalize(c.getComponentType())) : c; + + } else if (type instanceof ParameterizedType) { + ParameterizedType p = (ParameterizedType) type; + return new ParameterizedTypeImpl(p.getOwnerType(), + p.getRawType(), p.getActualTypeArguments()); + + } else if (type instanceof GenericArrayType) { + GenericArrayType g = (GenericArrayType) type; + return new GenericArrayTypeImpl(g.getGenericComponentType()); + + } else if (type instanceof WildcardType) { + WildcardType w = (WildcardType) type; + return new WildcardTypeImpl(w.getUpperBounds(), w.getLowerBounds()); + + } else { + return type; // This type is unsupported! + } + } + + public static Class getRawType(Type type) { + if (type instanceof Class) { + // type is a normal class. + return (Class) type; + + } else if (type instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) type; + + // I'm not exactly sure why getRawType() returns Type instead of Class. Neal isn't either but + // suspects some pathological case related to nested classes exists. + Type rawType = parameterizedType.getRawType(); + return (Class) rawType; + + } else if (type instanceof GenericArrayType) { + Type componentType = ((GenericArrayType)type).getGenericComponentType(); + return Array.newInstance(getRawType(componentType), 0).getClass(); + + } else if (type instanceof TypeVariable) { + // We could use the variable's bounds, but that won't work if there are multiple. having a raw + // type that's more general than necessary is okay. + return Object.class; + + } else if (type instanceof WildcardType) { + return getRawType(((WildcardType) type).getUpperBounds()[0]); + + } else { + String className = type == null ? "null" : type.getClass().getName(); + throw new IllegalArgumentException("Expected a Class, ParameterizedType, or " + + "GenericArrayType, but <" + type + "> is of type " + className); + } + } + + static boolean equal(Object a, Object b) { + return a == b || (a != null && a.equals(b)); + } + + /** Returns true if {@code a} and {@code b} are equal. */ + public static boolean equals(Type a, Type b) { + if (a == b) { + return true; // Also handles (a == null && b == null). + + } else if (a instanceof Class) { + return a.equals(b); // Class already specifies equals(). + + } else if (a instanceof ParameterizedType) { + if (!(b instanceof ParameterizedType)) return false; + ParameterizedType pa = (ParameterizedType) a; + ParameterizedType pb = (ParameterizedType) b; + Type[] aTypeArguments = pa instanceof ParameterizedTypeImpl + ? ((ParameterizedTypeImpl) pa).typeArguments + : pa.getActualTypeArguments(); + Type[] bTypeArguments = pb instanceof ParameterizedTypeImpl + ? ((ParameterizedTypeImpl) pb).typeArguments + : pb.getActualTypeArguments(); + return equal(pa.getOwnerType(), pb.getOwnerType()) + && pa.getRawType().equals(pb.getRawType()) + && Arrays.equals(aTypeArguments, bTypeArguments); + + } else if (a instanceof GenericArrayType) { + if (!(b instanceof GenericArrayType)) return false; + GenericArrayType ga = (GenericArrayType) a; + GenericArrayType gb = (GenericArrayType) b; + return equals(ga.getGenericComponentType(), gb.getGenericComponentType()); + + } else if (a instanceof WildcardType) { + if (!(b instanceof WildcardType)) return false; + WildcardType wa = (WildcardType) a; + WildcardType wb = (WildcardType) b; + return Arrays.equals(wa.getUpperBounds(), wb.getUpperBounds()) + && Arrays.equals(wa.getLowerBounds(), wb.getLowerBounds()); + + } else if (a instanceof TypeVariable) { + if (!(b instanceof TypeVariable)) return false; + TypeVariable va = (TypeVariable) a; + TypeVariable vb = (TypeVariable) b; + return va.getGenericDeclaration() == vb.getGenericDeclaration() + && va.getName().equals(vb.getName()); + + } else { + // This isn't a supported type. Could be a generic array type, wildcard type, etc. + return false; + } + } + + private static int hashCodeOrZero(Object o) { + return o != null ? o.hashCode() : 0; + } + + public static String typeToString(Type type) { + return type instanceof Class ? ((Class) type).getName() : type.toString(); + } + + /** + * Returns the generic supertype for {@code supertype}. For example, given a class {@code + * IntegerSet}, the result for when supertype is {@code Set.class} is {@code Set} and the + * result when the supertype is {@code Collection.class} is {@code Collection}. + */ + static Type getGenericSupertype(Type context, Class rawType, Class toResolve) { + if (toResolve == rawType) { + return context; + } + + // we skip searching through interfaces if unknown is an interface + if (toResolve.isInterface()) { + Class[] interfaces = rawType.getInterfaces(); + for (int i = 0, length = interfaces.length; i < length; i++) { + if (interfaces[i] == toResolve) { + return rawType.getGenericInterfaces()[i]; + } else if (toResolve.isAssignableFrom(interfaces[i])) { + return getGenericSupertype(rawType.getGenericInterfaces()[i], interfaces[i], toResolve); + } + } + } + + // check our supertypes + if (!rawType.isInterface()) { + while (rawType != Object.class) { + Class rawSupertype = rawType.getSuperclass(); + if (rawSupertype == toResolve) { + return rawType.getGenericSuperclass(); + } else if (toResolve.isAssignableFrom(rawSupertype)) { + return getGenericSupertype(rawType.getGenericSuperclass(), rawSupertype, toResolve); + } + rawType = rawSupertype; + } + } + + // we can't resolve this further + return toResolve; + } + + /** + * Returns the generic form of {@code supertype}. For example, if this is {@code + * ArrayList}, this returns {@code Iterable} given the input {@code + * Iterable.class}. + * + * @param supertype a superclass of, or interface implemented by, this. + */ + static Type getSupertype(Type context, Class contextRawType, Class supertype) { + if (!supertype.isAssignableFrom(contextRawType)) throw new IllegalArgumentException(); + return resolve(context, contextRawType, + getGenericSupertype(context, contextRawType, supertype)); + } + + /** + * Returns the component type of this array type. + * @throws ClassCastException if this type is not an array. + */ + public static Type arrayComponentType(Type array) { + return array instanceof GenericArrayType + ? ((GenericArrayType) array).getGenericComponentType() + : ((Class) array).getComponentType(); + } + + /** + * Returns the element type of this collection type. + * @throws IllegalArgumentException if this type is not a collection. + */ + public static Type collectionElementType(Type context, Class contextRawType) { + Type collectionType = getSupertype(context, contextRawType, Collection.class); + + if (collectionType instanceof WildcardType) { + collectionType = ((WildcardType) collectionType).getUpperBounds()[0]; + } + if (collectionType instanceof ParameterizedType) { + return ((ParameterizedType) collectionType).getActualTypeArguments()[0]; + } + return Object.class; + } + + /** + * Returns a two element array containing this map's key and value types in positions 0 and 1 + * respectively. + */ + public static Type[] mapKeyAndValueTypes(Type context, Class contextRawType) { + // Work around a problem with the declaration of java.util.Properties. That class should extend + // Hashtable, but it's declared to extend Hashtable. + if (context == Properties.class) return new Type[] { String.class, String.class }; + + Type mapType = getSupertype(context, contextRawType, Map.class); + if (mapType instanceof ParameterizedType) { + ParameterizedType mapParameterizedType = (ParameterizedType) mapType; + return mapParameterizedType.getActualTypeArguments(); + } + return new Type[] { Object.class, Object.class }; + } + + public static Type resolve(Type context, Class contextRawType, Type toResolve) { + // This implementation is made a little more complicated in an attempt to avoid object-creation. + while (true) { + if (toResolve instanceof TypeVariable) { + TypeVariable typeVariable = (TypeVariable) toResolve; + toResolve = resolveTypeVariable(context, contextRawType, typeVariable); + if (toResolve == typeVariable) return toResolve; + + } else if (toResolve instanceof Class && ((Class) toResolve).isArray()) { + Class original = (Class) toResolve; + Type componentType = original.getComponentType(); + Type newComponentType = resolve(context, contextRawType, componentType); + return componentType == newComponentType + ? original + : arrayOf(newComponentType); + + } else if (toResolve instanceof GenericArrayType) { + GenericArrayType original = (GenericArrayType) toResolve; + Type componentType = original.getGenericComponentType(); + Type newComponentType = resolve(context, contextRawType, componentType); + return componentType == newComponentType + ? original + : arrayOf(newComponentType); + + } else if (toResolve instanceof ParameterizedType) { + ParameterizedType original = (ParameterizedType) toResolve; + Type ownerType = original.getOwnerType(); + Type newOwnerType = resolve(context, contextRawType, ownerType); + boolean changed = newOwnerType != ownerType; + + Type[] args = original.getActualTypeArguments(); + for (int t = 0, length = args.length; t < length; t++) { + Type resolvedTypeArgument = resolve(context, contextRawType, args[t]); + if (resolvedTypeArgument != args[t]) { + if (!changed) { + args = args.clone(); + changed = true; + } + args[t] = resolvedTypeArgument; + } + } + + return changed + ? newParameterizedTypeWithOwner(newOwnerType, original.getRawType(), args) + : original; + + } else if (toResolve instanceof WildcardType) { + WildcardType original = (WildcardType) toResolve; + Type[] originalLowerBound = original.getLowerBounds(); + Type[] originalUpperBound = original.getUpperBounds(); + + if (originalLowerBound.length == 1) { + Type lowerBound = resolve(context, contextRawType, originalLowerBound[0]); + if (lowerBound != originalLowerBound[0]) { + return supertypeOf(lowerBound); + } + } else if (originalUpperBound.length == 1) { + Type upperBound = resolve(context, contextRawType, originalUpperBound[0]); + if (upperBound != originalUpperBound[0]) { + return subtypeOf(upperBound); + } + } + return original; + + } else { + return toResolve; + } + } + } + + static Type resolveTypeVariable(Type context, Class contextRawType, TypeVariable unknown) { + Class declaredByRaw = declaringClassOf(unknown); + + // We can't reduce this further. + if (declaredByRaw == null) return unknown; + + Type declaredBy = getGenericSupertype(context, contextRawType, declaredByRaw); + if (declaredBy instanceof ParameterizedType) { + int index = indexOf(declaredByRaw.getTypeParameters(), unknown); + return ((ParameterizedType) declaredBy).getActualTypeArguments()[index]; + } + + return unknown; + } + + private static int indexOf(Object[] array, Object toFind) { + for (int i = 0; i < array.length; i++) { + if (toFind.equals(array[i])) return i; + } + throw new NoSuchElementException(); + } + + /** + * Returns the declaring class of {@code typeVariable}, or {@code null} if it was not declared by + * a class. + */ + private static Class declaringClassOf(TypeVariable typeVariable) { + GenericDeclaration genericDeclaration = typeVariable.getGenericDeclaration(); + return genericDeclaration instanceof Class ? (Class) genericDeclaration : null; + } + + private static void checkNotPrimitive(Type type) { + if ((type instanceof Class) && ((Class) type).isPrimitive()) { + throw new IllegalArgumentException(); + } + } + + private static final class ParameterizedTypeImpl implements ParameterizedType { + private final Type ownerType; + private final Type rawType; + private final Type[] typeArguments; + + public ParameterizedTypeImpl(Type ownerType, Type rawType, Type... typeArguments) { + // require an owner type if the raw type needs it + if (rawType instanceof Class) { + Class rawTypeAsClass = (Class) rawType; + boolean isStaticOrTopLevelClass = Modifier.isStatic(rawTypeAsClass.getModifiers()) + || rawTypeAsClass.getEnclosingClass() == null; + if (ownerType == null && !isStaticOrTopLevelClass) throw new IllegalArgumentException(); + } + + this.ownerType = ownerType == null ? null : canonicalize(ownerType); + this.rawType = canonicalize(rawType); + this.typeArguments = typeArguments.clone(); + for (int t = 0; t < this.typeArguments.length; t++) { + if (this.typeArguments[t] == null) throw new NullPointerException(); + checkNotPrimitive(this.typeArguments[t]); + this.typeArguments[t] = canonicalize(this.typeArguments[t]); + } + } + + public Type[] getActualTypeArguments() { + return typeArguments.clone(); + } + + public Type getRawType() { + return rawType; + } + + public Type getOwnerType() { + return ownerType; + } + + @Override public boolean equals(Object other) { + return other instanceof ParameterizedType + && Types.equals(this, (ParameterizedType) other); + } + + @Override public int hashCode() { + return Arrays.hashCode(typeArguments) + ^ rawType.hashCode() + ^ hashCodeOrZero(ownerType); + } + + @Override public String toString() { + StringBuilder result = new StringBuilder(30 * (typeArguments.length + 1)); + result.append(typeToString(rawType)); + + if (typeArguments.length == 0) { + return result.toString(); + } + + result.append("<").append(typeToString(typeArguments[0])); + for (int i = 1; i < typeArguments.length; i++) { + result.append(", ").append(typeToString(typeArguments[i])); + } + return result.append(">").toString(); + } + } + + private static final class GenericArrayTypeImpl implements GenericArrayType { + private final Type componentType; + + public GenericArrayTypeImpl(Type componentType) { + this.componentType = canonicalize(componentType); + } + + public Type getGenericComponentType() { + return componentType; + } + + @Override public boolean equals(Object o) { + return o instanceof GenericArrayType + && Types.equals(this, (GenericArrayType) o); + } + + @Override public int hashCode() { + return componentType.hashCode(); + } + + @Override public String toString() { + return typeToString(componentType) + "[]"; + } + } + + /** + * The WildcardType interface supports multiple upper bounds and multiple lower bounds. We only + * support what the Java 6 language needs - at most one bound. If a lower bound is set, the upper + * bound must be Object.class. + */ + private static final class WildcardTypeImpl implements WildcardType { + private final Type upperBound; + private final Type lowerBound; + + public WildcardTypeImpl(Type[] upperBounds, Type[] lowerBounds) { + if (lowerBounds.length > 1) throw new IllegalArgumentException(); + if (upperBounds.length != 1) throw new IllegalArgumentException(); + + if (lowerBounds.length == 1) { + if (lowerBounds[0] == null) throw new NullPointerException(); + checkNotPrimitive(lowerBounds[0]); + if (upperBounds[0] != Object.class) throw new IllegalArgumentException(); + this.lowerBound = canonicalize(lowerBounds[0]); + this.upperBound = Object.class; + + } else { + if (upperBounds[0] == null) throw new NullPointerException(); + checkNotPrimitive(upperBounds[0]); + this.lowerBound = null; + this.upperBound = canonicalize(upperBounds[0]); + } + } + + public Type[] getUpperBounds() { + return new Type[] { upperBound }; + } + + public Type[] getLowerBounds() { + return lowerBound != null ? new Type[] { lowerBound } : EMPTY_TYPE_ARRAY; + } + + @Override public boolean equals(Object other) { + return other instanceof WildcardType + && Types.equals(this, (WildcardType) other); + } + + @Override public int hashCode() { + // This equals Arrays.hashCode(getLowerBounds()) ^ Arrays.hashCode(getUpperBounds()). + return (lowerBound != null ? 31 + lowerBound.hashCode() : 1) + ^ (31 + upperBound.hashCode()); + } + + @Override public String toString() { + if (lowerBound != null) { + return "? super " + typeToString(lowerBound); + } else if (upperBound == Object.class) { + return "?"; + } else { + return "? extends " + typeToString(upperBound); + } + } + } +} diff --git a/moshi/src/test/java/com/squareup/moshi/TypesTest.java b/moshi/src/test/java/com/squareup/moshi/TypesTest.java new file mode 100644 index 0000000..83bd221 --- /dev/null +++ b/moshi/src/test/java/com/squareup/moshi/TypesTest.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2010 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.squareup.moshi; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertNull; + +public class TypesTest { + @Test public void newParameterizedTypeWithoutOwner() throws Exception { + // List. List is a top-level class. + Type type = Types.newParameterizedTypeWithOwner(null, List.class, A.class); + assertThat(getFirstTypeArgument(type)).isEqualTo(A.class); + + // A. A is a static inner class. + type = Types.newParameterizedTypeWithOwner(null, A.class, B.class); + assertThat(getFirstTypeArgument(type)).isEqualTo(B.class); + + final class D { + } + try { + // D is not allowed since D is not a static inner class. + Types.newParameterizedTypeWithOwner(null, D.class, A.class); + } catch (IllegalArgumentException expected) { + } + + // A is allowed. + type = Types.newParameterizedTypeWithOwner(null, A.class, D.class); + assertThat(getFirstTypeArgument(type)).isEqualTo(D.class); + } + + @Test public void getFirstTypeArgument() throws Exception { + assertNull(getFirstTypeArgument(A.class)); + + Type type = Types.newParameterizedTypeWithOwner(null, A.class, B.class, C.class); + assertThat(getFirstTypeArgument(type)).isEqualTo(B.class); + } + + private static final class A { + } + + private static final class B { + } + + private static final class C { + } + + /** + * Given a parameterized type {@code A}, returns B. If the specified type is not a generic + * type, returns null. + */ + public static Type getFirstTypeArgument(Type type) throws Exception { + if (!(type instanceof ParameterizedType)) return null; + ParameterizedType ptype = (ParameterizedType) type; + Type[] actualTypeArguments = ptype.getActualTypeArguments(); + if (actualTypeArguments.length == 0) return null; + return Types.canonicalize(actualTypeArguments[0]); + } + + Map mapOfStringInteger; + Map[] arrayOfMapOfStringInteger; + ArrayList> arrayListOfMapOfStringInteger; + interface StringIntegerMap extends Map { + } + + @Test public void typeLiteral() throws Exception { + TypeLiteral> typeLiteral = new TypeLiteral>() {}; + TypeLiteral fromReflection = TypeLiteral.get( + TypesTest.class.getDeclaredField("mapOfStringInteger").getGenericType()); + assertThat(typeLiteral).isEqualTo(fromReflection); + assertThat(typeLiteral.hashCode()).isEqualTo(fromReflection.hashCode()); + assertThat(typeLiteral.toString()) + .isEqualTo("java.util.Map"); + assertThat(typeLiteral.getRawType()).isEqualTo(Map.class); + } + + @Test public void arrayComponentType() throws Exception { + assertThat(Types.arrayComponentType(String[][].class)).isEqualTo(String[].class); + assertThat(Types.arrayComponentType(String[].class)).isEqualTo(String.class); + + Type arrayOfMapOfStringIntegerType = TypesTest.class.getDeclaredField( + "arrayOfMapOfStringInteger").getGenericType(); + Type mapOfStringIntegerType = TypesTest.class.getDeclaredField( + "mapOfStringInteger").getGenericType(); + assertThat(Types.arrayComponentType(arrayOfMapOfStringIntegerType)) + .isEqualTo(mapOfStringIntegerType); + } + + @Test public void collectionElementType() throws Exception { + Type arrayListOfMapOfStringIntegerType = TypesTest.class.getDeclaredField( + "arrayListOfMapOfStringInteger").getGenericType(); + Type mapOfStringIntegerType = TypesTest.class.getDeclaredField( + "mapOfStringInteger").getGenericType(); + assertThat(Types.collectionElementType(arrayListOfMapOfStringIntegerType, List.class)) + .isEqualTo(mapOfStringIntegerType); + } + + @Test public void mapKeyAndValueTypes() throws Exception { + Type mapOfStringIntegerType = TypesTest.class.getDeclaredField( + "mapOfStringInteger").getGenericType(); + assertThat(Types.mapKeyAndValueTypes(mapOfStringIntegerType, Map.class)) + .containsExactly(String.class, Integer.class); + } + + @Test public void propertiesTypes() throws Exception { + assertThat(Types.mapKeyAndValueTypes(Properties.class, Properties.class)) + .containsExactly(String.class, String.class); + } + + @Test public void fixedVariablesTypes() throws Exception { + assertThat(Types.mapKeyAndValueTypes(StringIntegerMap.class, StringIntegerMap.class)) + .containsExactly(String.class, Integer.class); + } +}