diff --git a/kotlin/src/main/java/com/squareup/moshi/kotlin/KotlinJsonAdapter.kt b/kotlin/src/main/java/com/squareup/moshi/kotlin/KotlinJsonAdapter.kt index 3f005c9..922cc11 100644 --- a/kotlin/src/main/java/com/squareup/moshi/kotlin/KotlinJsonAdapter.kt +++ b/kotlin/src/main/java/com/squareup/moshi/kotlin/KotlinJsonAdapter.kt @@ -23,6 +23,7 @@ import com.squareup.moshi.JsonWriter import com.squareup.moshi.Moshi import com.squareup.moshi.Types import com.squareup.moshi.internal.Util +import com.squareup.moshi.internal.Util.resolve import java.lang.reflect.Modifier import java.lang.reflect.Type import java.util.AbstractMap.SimpleEntry @@ -225,7 +226,7 @@ class KotlinJsonAdapterFactory : JsonAdapter.Factory { } val name = jsonAnnotation?.name ?: property.name - val resolvedPropertyType = Types.resolve(type, rawType, property.returnType.javaType) + val resolvedPropertyType = resolve(type, rawType, property.returnType.javaType) val adapter = moshi.adapter( resolvedPropertyType, Util.jsonAnnotations(allAnnotations.toTypedArray())) diff --git a/moshi/src/main/java/com/squareup/moshi/AdapterMethodsFactory.java b/moshi/src/main/java/com/squareup/moshi/AdapterMethodsFactory.java index 266e018..91396b8 100644 --- a/moshi/src/main/java/com/squareup/moshi/AdapterMethodsFactory.java +++ b/moshi/src/main/java/com/squareup/moshi/AdapterMethodsFactory.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Set; import javax.annotation.Nullable; +import static com.squareup.moshi.internal.Util.canonicalize; import static com.squareup.moshi.internal.Util.jsonAnnotations; final class AdapterMethodsFactory implements JsonAdapter.Factory { @@ -289,7 +290,7 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory { AdapterMethod(Type type, Set annotations, Object adapter, Method method, int parameterCount, int adaptersOffset, boolean nullable) { - this.type = Types.canonicalize(type); + this.type = canonicalize(type); this.annotations = annotations; this.adapter = adapter; this.method = method; diff --git a/moshi/src/main/java/com/squareup/moshi/ClassJsonAdapter.java b/moshi/src/main/java/com/squareup/moshi/ClassJsonAdapter.java index 1fcb918..d3d0e06 100644 --- a/moshi/src/main/java/com/squareup/moshi/ClassJsonAdapter.java +++ b/moshi/src/main/java/com/squareup/moshi/ClassJsonAdapter.java @@ -28,6 +28,8 @@ import java.util.Set; import java.util.TreeMap; import javax.annotation.Nullable; +import static com.squareup.moshi.internal.Util.resolve; + /** * Emits a regular class as a JSON object by mapping Java fields to JSON object properties. * @@ -92,7 +94,7 @@ final class ClassJsonAdapter extends JsonAdapter { if (!includeField(platformType, field.getModifiers())) continue; // Look up a type adapter for this type. - Type fieldType = Types.resolve(type, rawType, field.getGenericType()); + Type fieldType = resolve(type, rawType, field.getGenericType()); Set annotations = Util.jsonAnnotations(field); JsonAdapter adapter = moshi.adapter(fieldType, annotations); diff --git a/moshi/src/main/java/com/squareup/moshi/Moshi.java b/moshi/src/main/java/com/squareup/moshi/Moshi.java index 41708c6..c74b741 100644 --- a/moshi/src/main/java/com/squareup/moshi/Moshi.java +++ b/moshi/src/main/java/com/squareup/moshi/Moshi.java @@ -30,6 +30,8 @@ import java.util.Set; import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; +import static com.squareup.moshi.internal.Util.canonicalize; + /** * Coordinates binding between JSON values and Java objects. */ @@ -93,7 +95,7 @@ public final class Moshi { throw new NullPointerException("annotations == null"); } - type = Types.canonicalize(type); + type = canonicalize(type); // If there's an equivalent adapter in the cache, we're done! Object cacheKey = cacheKey(type, annotations); @@ -146,7 +148,7 @@ public final class Moshi { Set annotations) { if (annotations == null) throw new NullPointerException("annotations == null"); - type = Types.canonicalize(type); + type = canonicalize(type); int skipPastIndex = factories.indexOf(skipPast); if (skipPastIndex == -1) { diff --git a/moshi/src/main/java/com/squareup/moshi/Types.java b/moshi/src/main/java/com/squareup/moshi/Types.java index ae6fdf0..2ebb180 100644 --- a/moshi/src/main/java/com/squareup/moshi/Types.java +++ b/moshi/src/main/java/com/squareup/moshi/Types.java @@ -15,10 +15,12 @@ */ package com.squareup.moshi; +import com.squareup.moshi.internal.Util.GenericArrayTypeImpl; +import com.squareup.moshi.internal.Util.ParameterizedTypeImpl; +import com.squareup.moshi.internal.Util.WildcardTypeImpl; import java.lang.annotation.Annotation; import java.lang.reflect.Array; import java.lang.reflect.GenericArrayType; -import java.lang.reflect.GenericDeclaration; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; @@ -31,16 +33,18 @@ import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Map; -import java.util.NoSuchElementException; import java.util.Properties; import java.util.Set; import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; -/** Factory methods for types. */ -public final class Types { - static final Type[] EMPTY_TYPE_ARRAY = new Type[] {}; +import static com.squareup.moshi.internal.Util.EMPTY_TYPE_ARRAY; +import static com.squareup.moshi.internal.Util.getGenericSupertype; +import static com.squareup.moshi.internal.Util.resolve; +/** Factory methods for types. */ +@CheckReturnValue +public final class Types { private Types() { } @@ -49,7 +53,7 @@ public final class Types { * Returns the subset of {@code annotations} without {@code jsonQualifier}, or null if {@code * annotations} does not contain {@code jsonQualifier}. */ - @CheckReturnValue public static @Nullable Set nextAnnotations( + public static @Nullable Set nextAnnotations( Set annotations, Class jsonQualifier) { if (!jsonQualifier.isAnnotationPresent(JsonQualifier.class)) { @@ -72,7 +76,6 @@ public final class Types { * Returns a new parameterized type, applying {@code typeArguments} to {@code rawType}. Use this * method if {@code rawType} is not enclosed in another type. */ - @CheckReturnValue public static ParameterizedType newParameterizedType(Type rawType, Type... typeArguments) { return new ParameterizedTypeImpl(null, rawType, typeArguments); } @@ -81,13 +84,13 @@ public final class Types { * Returns a new parameterized type, applying {@code typeArguments} to {@code rawType}. Use this * method if {@code rawType} is enclosed in {@code ownerType}. */ - @CheckReturnValue public static ParameterizedType newParameterizedTypeWithOwner( + 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}. */ - @CheckReturnValue public static GenericArrayType arrayOf(Type componentType) { + public static GenericArrayType arrayOf(Type componentType) { return new GenericArrayTypeImpl(componentType); } @@ -97,7 +100,7 @@ public final class Types { * {@code bound} is {@code Object.class}, this returns {@code ?}, which is shorthand for {@code * ? extends Object}. */ - @CheckReturnValue public static WildcardType subtypeOf(Type bound) { + public static WildcardType subtypeOf(Type bound) { return new WildcardTypeImpl(new Type[] { bound }, EMPTY_TYPE_ARRAY); } @@ -105,41 +108,11 @@ public final class Types { * 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}. */ - @CheckReturnValue public static WildcardType supertypeOf(Type bound) { + 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()}. - */ - 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) { - if (type instanceof ParameterizedTypeImpl) return type; - ParameterizedType p = (ParameterizedType) type; - return new ParameterizedTypeImpl(p.getOwnerType(), - p.getRawType(), p.getActualTypeArguments()); - - } else if (type instanceof GenericArrayType) { - if (type instanceof GenericArrayTypeImpl) return type; - GenericArrayType g = (GenericArrayType) type; - return new GenericArrayTypeImpl(g.getGenericComponentType()); - - } else if (type instanceof WildcardType) { - if (type instanceof WildcardTypeImpl) return type; - WildcardType w = (WildcardType) type; - return new WildcardTypeImpl(w.getUpperBounds(), w.getLowerBounds()); - - } else { - return type; // This type is unsupported! - } - } - - @CheckReturnValue public static Class getRawType(Type type) { + public static Class getRawType(Type type) { if (type instanceof Class) { // type is a normal class. return (Class) type; @@ -171,41 +144,24 @@ public final class Types { } } - @SuppressWarnings("unchecked") - static T createJsonQualifierImplementation(final Class annotationType) { - if (!annotationType.isAnnotation()) { - throw new IllegalArgumentException(annotationType + " must be an annotation."); + /** + * 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 (!annotationType.isAnnotationPresent(JsonQualifier.class)) { - throw new IllegalArgumentException(annotationType + " must have @JsonQualifier."); + if (collectionType instanceof ParameterizedType) { + return ((ParameterizedType) collectionType).getActualTypeArguments()[0]; } - if (annotationType.getDeclaredMethods().length != 0) { - throw new IllegalArgumentException(annotationType + " must not declare methods."); - } - return (T) Proxy.newProxyInstance(annotationType.getClassLoader(), - new Class[] { annotationType }, new InvocationHandler() { - @Override public Object invoke(Object proxy, Method method, Object[] args) - throws Throwable { - String methodName = method.getName(); - switch (methodName) { - case "annotationType": - return annotationType; - case "equals": - Object o = args[0]; - return annotationType.isInstance(o); - case "hashCode": - return 0; - case "toString": - return "@" + annotationType.getName() + "()"; - default: - return method.invoke(proxy, args); - } - } - }); + return Object.class; } /** Returns true if {@code a} and {@code b} are equal. */ - @CheckReturnValue public static boolean equals(@Nullable Type a, @Nullable Type b) { + public static boolean equals(@Nullable Type a, @Nullable Type b) { if (a == b) { return true; // Also handles (a == null && b == null). @@ -260,51 +216,54 @@ public final class Types { } } - static int hashCodeOrZero(@Nullable Object o) { - return o != null ? o.hashCode() : 0; - } - - static String typeToString(Type type) { - return type instanceof Class ? ((Class) type).getName() : type.toString(); + @SuppressWarnings("unchecked") + static T createJsonQualifierImplementation(final Class annotationType) { + if (!annotationType.isAnnotation()) { + throw new IllegalArgumentException(annotationType + " must be an annotation."); + } + if (!annotationType.isAnnotationPresent(JsonQualifier.class)) { + throw new IllegalArgumentException(annotationType + " must have @JsonQualifier."); + } + if (annotationType.getDeclaredMethods().length != 0) { + throw new IllegalArgumentException(annotationType + " must not declare methods."); + } + return (T) Proxy.newProxyInstance(annotationType.getClassLoader(), + new Class[] { annotationType }, new InvocationHandler() { + @Override public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable { + String methodName = method.getName(); + switch (methodName) { + case "annotationType": + return annotationType; + case "equals": + Object o = args[0]; + return annotationType.isInstance(o); + case "hashCode": + return 0; + case "toString": + return "@" + annotationType.getName() + "()"; + default: + return method.invoke(proxy, args); + } + } + }); } /** - * 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}. + * Returns a two element array containing this map's key and value types in positions 0 and 1 + * respectively. */ - static Type getGenericSupertype(Type context, Class rawType, Class toResolve) { - if (toResolve == rawType) { - return context; - } + 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 }; - // 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); - } - } + Type mapType = getSupertype(context, contextRawType, Map.class); + if (mapType instanceof ParameterizedType) { + ParameterizedType mapParameterizedType = (ParameterizedType) mapType; + return mapParameterizedType.getActualTypeArguments(); } - - // 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; + return new Type[] { Object.class, Object.class }; } /** @@ -339,126 +298,6 @@ public final class Types { } } - /** - * Returns the element type of this collection type. - * @throws IllegalArgumentException if this type is not a collection. - */ - @CheckReturnValue - 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. - */ - 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 }; - } - - @CheckReturnValue // TODO(eric): Move this to internal Utils. - 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 - ? new ParameterizedTypeImpl(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; - } - /** * Returns true if this is a Type supported by {@link StandardJsonAdapters#FACTORY}. */ @@ -474,178 +313,4 @@ public final class Types { || type == String.class || type == Object.class; } - - 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 @Nullable Class declaringClassOf(TypeVariable typeVariable) { - GenericDeclaration genericDeclaration = typeVariable.getGenericDeclaration(); - return genericDeclaration instanceof Class ? (Class) genericDeclaration : null; - } - - static void checkNotPrimitive(Type type) { - if ((type instanceof Class) && ((Class) type).isPrimitive()) { - throw new IllegalArgumentException("Unexpected primitive " + type + ". Use the boxed type."); - } - } - - private static final class ParameterizedTypeImpl implements ParameterizedType { - private final @Nullable Type ownerType; - private final Type rawType; - final Type[] typeArguments; - - ParameterizedTypeImpl(@Nullable Type ownerType, Type rawType, Type... typeArguments) { - // Require an owner type if the raw type needs it. - if (rawType instanceof Class) { - Class enclosingClass = ((Class) rawType).getEnclosingClass(); - if (ownerType != null) { - if (enclosingClass == null || Types.getRawType(ownerType) != enclosingClass) { - throw new IllegalArgumentException( - "unexpected owner type for " + rawType + ": " + ownerType); - } - } else if (enclosingClass != null) { - throw new IllegalArgumentException( - "unexpected owner type for " + rawType + ": null"); - } - } - - 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]); - } - } - - @Override public Type[] getActualTypeArguments() { - return typeArguments.clone(); - } - - @Override public Type getRawType() { - return rawType; - } - - @Override public @Nullable 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; - - GenericArrayTypeImpl(Type componentType) { - this.componentType = canonicalize(componentType); - } - - @Override 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 @Nullable Type lowerBound; - - 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]); - } - } - - @Override public Type[] getUpperBounds() { - return new Type[] { upperBound }; - } - - @Override 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/main/java/com/squareup/moshi/internal/Util.java b/moshi/src/main/java/com/squareup/moshi/internal/Util.java index 8ee3707..0744728 100644 --- a/moshi/src/main/java/com/squareup/moshi/internal/Util.java +++ b/moshi/src/main/java/com/squareup/moshi/internal/Util.java @@ -16,15 +16,29 @@ package com.squareup.moshi.internal; import com.squareup.moshi.JsonQualifier; +import com.squareup.moshi.Types; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.GenericDeclaration; +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.Collections; import java.util.LinkedHashSet; +import java.util.NoSuchElementException; import java.util.Set; +import javax.annotation.Nullable; + +import static com.squareup.moshi.Types.arrayOf; +import static com.squareup.moshi.Types.subtypeOf; +import static com.squareup.moshi.Types.supertypeOf; public final class Util { public static final Set NO_ANNOTATIONS = Collections.emptySet(); + public static final Type[] EMPTY_TYPE_ARRAY = new Type[] {}; private Util() { } @@ -80,4 +94,340 @@ public final class Util { || name.startsWith("kotlin.") || name.startsWith("scala."); } + + /** + * 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) { + if (type instanceof ParameterizedTypeImpl) return type; + ParameterizedType p = (ParameterizedType) type; + return new ParameterizedTypeImpl(p.getOwnerType(), + p.getRawType(), p.getActualTypeArguments()); + + } else if (type instanceof GenericArrayType) { + if (type instanceof GenericArrayTypeImpl) return type; + GenericArrayType g = (GenericArrayType) type; + return new GenericArrayTypeImpl(g.getGenericComponentType()); + + } else if (type instanceof WildcardType) { + if (type instanceof WildcardTypeImpl) return type; + WildcardType w = (WildcardType) type; + return new WildcardTypeImpl(w.getUpperBounds(), w.getLowerBounds()); + + } else { + return type; // This type is unsupported! + } + } + + 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 + ? new ParameterizedTypeImpl(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; + } + + /** + * 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}. + */ + public 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; + } + + static int hashCodeOrZero(@Nullable Object o) { + return o != null ? o.hashCode() : 0; + } + + static String typeToString(Type type) { + return type instanceof Class ? ((Class) type).getName() : type.toString(); + } + + 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. + */ + static @Nullable Class declaringClassOf(TypeVariable typeVariable) { + GenericDeclaration genericDeclaration = typeVariable.getGenericDeclaration(); + return genericDeclaration instanceof Class ? (Class) genericDeclaration : null; + } + + static void checkNotPrimitive(Type type) { + if ((type instanceof Class) && ((Class) type).isPrimitive()) { + throw new IllegalArgumentException("Unexpected primitive " + type + ". Use the boxed type."); + } + } + + public static final class ParameterizedTypeImpl implements ParameterizedType { + private final @Nullable Type ownerType; + private final Type rawType; + public final Type[] typeArguments; + + public ParameterizedTypeImpl(@Nullable Type ownerType, Type rawType, Type... typeArguments) { + // Require an owner type if the raw type needs it. + if (rawType instanceof Class) { + Class enclosingClass = ((Class) rawType).getEnclosingClass(); + if (ownerType != null) { + if (enclosingClass == null || Types.getRawType(ownerType) != enclosingClass) { + throw new IllegalArgumentException( + "unexpected owner type for " + rawType + ": " + ownerType); + } + } else if (enclosingClass != null) { + throw new IllegalArgumentException( + "unexpected owner type for " + rawType + ": null"); + } + } + + 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]); + } + } + + @Override public Type[] getActualTypeArguments() { + return typeArguments.clone(); + } + + @Override public Type getRawType() { + return rawType; + } + + @Override public @Nullable 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(); + } + } + + public static final class GenericArrayTypeImpl implements GenericArrayType { + private final Type componentType; + + public GenericArrayTypeImpl(Type componentType) { + this.componentType = canonicalize(componentType); + } + + @Override 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. + */ + public static final class WildcardTypeImpl implements WildcardType { + private final Type upperBound; + private final @Nullable 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]); + } + } + + @Override public Type[] getUpperBounds() { + return new Type[] { upperBound }; + } + + @Override 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 index 888ff1e..85262c9 100644 --- a/moshi/src/test/java/com/squareup/moshi/TypesTest.java +++ b/moshi/src/test/java/com/squareup/moshi/TypesTest.java @@ -28,6 +28,7 @@ import java.util.Properties; import java.util.Set; import org.junit.Test; +import static com.squareup.moshi.internal.Util.canonicalize; import static java.lang.annotation.RetentionPolicy.RUNTIME; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; @@ -174,7 +175,7 @@ public final class TypesTest { ParameterizedType ptype = (ParameterizedType) type; Type[] actualTypeArguments = ptype.getActualTypeArguments(); if (actualTypeArguments.length == 0) return null; - return Types.canonicalize(actualTypeArguments[0]); + return canonicalize(actualTypeArguments[0]); } Map mapOfStringInteger;