diff --git a/spring-core/src/main/java/org/springframework/core/ResolvableType.java b/spring-core/src/main/java/org/springframework/core/ResolvableType.java new file mode 100644 index 0000000000..1f463565d2 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/ResolvableType.java @@ -0,0 +1,914 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.core; + +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.Collection; +import java.util.Map; + +import org.springframework.util.Assert; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Encapsulates a Java {@link java.lang.reflect.Type}, providing access to + * {@link #getSuperType() supertypes} , {@link #getInterfaces() interfaces} and + * {@link #getGeneric(int...) generic parameters} along with the ability to ultimately + * {@link #resolve() resolve} to a {@link java.lang.Class}. + * + *

{@code ResolvableTypes} may be obtained from {@link #forField(Field) fields}, + * {@link #forMethodParameter(Method, int) method parameters}, + * {@link #forMethodReturn(Method) method returns}, {@link #forClass(Class) classes} or + * directly from a {@link #forType(Type) java.lang.reflect.Type}. Most methods on this class + * will themselves return {@link ResolvableType}s, allowing easy navigation. For example: + *

+ * private HashMap<Integer, List<String>> myMap;
+ *
+ * public void example() {
+ *     ResolvableType t = ResolvableType.forField(getClass().getDeclaredField("myMap"));
+ *     t.getSuperType(); // AbstractMap<Integer, List<String>>
+ *     t.asMap(); // Map<Integer, List<String>>
+ *     t.getGeneric(0).resolve(); // Integer
+ *     t.getGeneric(1).resolve(); // List
+ *     t.getGeneric(1); // List<String>
+ *     t.resolveGeneric(1, 0); // String
+ * }
+ * 
+ * + * @author Phillip Webb + * @since 4.0 + * @see TypeVariableResolver + * @see #forField(Field) + * @see #forMethodParameter(Method, int) + * @see #forMethodReturn(Method) + * @see #forConstructorParameter(Constructor, int) + * @see #forClass(Class) + * @see #forType(Type) + */ +public final class ResolvableType implements TypeVariableResolver { + + private static ConcurrentReferenceHashMap cache = + new ConcurrentReferenceHashMap(); + + + /** + * {@code ResolvableType} returned when no value is available. {@code NONE} is used + * in preference to {@code null} so that multiple method calls can be safely chained. + */ + public static final ResolvableType NONE = new ResolvableType(null, null); + + + private static final ResolvableType[] EMPTY_TYPES_ARRAY = new ResolvableType[0]; + + + /** + * The underlying java type being managed (only ever {@code null} for {@link #NONE}) + */ + private final Type type; + + /** + * The {@link TypeVariableResolver} to use or {@code null} if no resolver is availble. + */ + private final TypeVariableResolver variableResolver; + + /** + * Stored copy of the resolved value or {@code null} if the resolve method has not + * yet been called. {@code void.class} is used when the resolve method failed. + */ + private Class resolved; + + + /** + * Private constructor used to create a new {@link ResolvableType}. + * @param type the underlying java type (may only be {@code null} for {@link #NONE}) + * @param variableResolver the resolver used for {@link TypeVariable}s (may be {@code null}) + */ + private ResolvableType(Type type, TypeVariableResolver variableResolver) { + this.type = type; + this.variableResolver = variableResolver; + } + + + /** + * Return the underling java {@link Type} being managed. With the exception of + * the {@link #NONE} constant, this method will never return {@code null}. + */ + public Type getType() { + return this.type; + } + + /** + * Determines if this {@code ResolvableType} is assignable from the specified + * {@code type}. Attempts to follow the same rules as the Java compiler, considering + * if both the {@link #resolve() resolved} {@code Class} is + * {@link Class#isAssignableFrom(Class) assignable from} the given {@code type} as + * well as if all {@link #getGenerics() generics} are assignable. + * @param type the type to be checked + * @return {@code true} if the specified {@code type} can be assigned to this + * {@code type}. + */ + public boolean isAssignableFrom(ResolvableType type) { + return isAssignableFrom(type, false); + } + + private boolean isAssignableFrom(ResolvableType type, boolean checkingGeneric) { + Assert.notNull(type, "Type must not be null"); + + // If we cannot resolve types, we are not assignable + if (resolve() == null || type.resolve() == null) { + return false; + } + + // Deal with array by delegating to the component type + if (isArray()) { + return (type.isArray() && getComponentType().isAssignableFrom( + type.getComponentType())); + } + + // Deal with wildcard bounds + WildcardBounds ourBounds = WildcardBounds.get(this); + WildcardBounds typeBounds = WildcardBounds.get(type); + + // in the from X is assignable to + if (typeBounds != null) { + return (ourBounds != null && ourBounds.isSameKind(typeBounds) + && ourBounds.isAssignableFrom(typeBounds.getBounds())); + } + + // in the form is assignable to X ... + if (ourBounds != null) { + return ourBounds.isAssignableFrom(type); + } + + // Main assignability check + boolean rtn = resolve().isAssignableFrom(type.resolve()); + + // We need an exact type match for generics + // List is not assignable from List + rtn &= (!checkingGeneric || resolve().equals(type.resolve())); + + // Recursively check each generic + for (int i = 0; i < getGenerics().length; i++) { + rtn &= getGeneric(i).isAssignableFrom(type.as(resolve()).getGeneric(i), true); + } + + return rtn; + } + + /** + * Return {@code true} if this type will resolve to a Class that represents an + * array. + * @see #getComponentType() + */ + public boolean isArray() { + if (this == NONE) { + return false; + } + return (((this.type instanceof Class) && + ((Class) this.type).isArray()) || + this.type instanceof GenericArrayType || + this.resolveType().isArray()); + } + + /** + * Return the ResolvableType representing the component type of the array or + * {@link #NONE} if this type does not represent an array. + * @see #isArray() + */ + public ResolvableType getComponentType() { + if (this == NONE) { + return NONE; + } + if (this.type instanceof Class) { + Class componentType = ((Class) this.type).getComponentType(); + return (componentType == null ? NONE : forType(componentType, + this.variableResolver)); + } + if (this.type instanceof GenericArrayType) { + return forType(((GenericArrayType) this.type).getGenericComponentType(), + this.variableResolver); + } + return resolveType().getComponentType(); + } + + /** + * Convenience method to return this type as a resolvable {@link Collection} type. + * Returns {@link #NONE} if this type does not implement or extend + * {@link Collection}. + * @see #as(Class) + * @see #asMap() + */ + public ResolvableType asCollection() { + return as(Collection.class); + } + + /** + * Convenience method to return this type as a resolvable {@link Map} type. + * Returns {@link #NONE} if this type does not implement or extend + * {@link Map}. + * @see #as(Class) + * @see #asCollection() + */ + public ResolvableType asMap() { + return as(Map.class); + } + + /** + * Return this type as a {@link ResolvableType} of the specified class. Searches + * {@link #getSuperType() supertype} and {@link #getInterfaces() interface} + * hierarchies to find a match, returning {@link #NONE} if this type does not + * implement or extends the specified class. + * @param type the required class type + * @return a {@link ResolvableType} representing this object as the specified type or + * {@link #NONE} + * @see #asCollection() + * @see #asMap() + * @see #getSuperType() + * @see #getInterfaces() + */ + public ResolvableType as(Class type) { + if (this == NONE) { + return NONE; + } + if (ObjectUtils.nullSafeEquals(resolve(), type)) { + return this; + } + for (ResolvableType interfaceType : getInterfaces()) { + ResolvableType interfaceAsType = interfaceType.as(type); + if (interfaceAsType != NONE) { + return interfaceAsType; + } + } + return getSuperType().as(type); + } + + /** + * Return a {@link ResolvableType} representing the direct supertype of this type. + * If no supertype is available this method returns {@link #NONE}. + * @see #getInterfaces() + */ + public ResolvableType getSuperType() { + Class resolved = resolve(); + if (resolved == null || resolved.getGenericSuperclass() == null) { + return NONE; + } + return forType(resolved.getGenericSuperclass(), this); + } + + /** + * Return a {@link ResolvableType} array representing the direct interfaces + * implemented by this type. If this type does not implement any interfaces an + * empty array is returned. + * @see #getSuperType() + */ + public ResolvableType[] getInterfaces() { + Class resolved = resolve(); + if (resolved == null || ObjectUtils.isEmpty(resolved.getGenericInterfaces())) { + return EMPTY_TYPES_ARRAY; + } + Type[] interfaceTypes = resolved.getGenericInterfaces(); + ResolvableType[] interfaces = new ResolvableType[interfaceTypes.length]; + for (int i = 0; i < interfaceTypes.length; i++) { + interfaces[i] = forType(interfaceTypes[i], this); + } + return interfaces; + } + + /** + * Return {@code true} if this type contains generic parameters. + * @see #getGeneric(int...) + * @see #getGenerics() + */ + public boolean hasGenerics() { + return (getGenerics().length > 0); + } + + /** + * Returns a {@link ResolvableType} for the specified nesting level. See + * {@link #getNested(int, Map)} for details. + * @param nestingLevel the nesting level + * @return the {@link ResolvableType} type, or {@code #NONE} + */ + public ResolvableType getNested(int nestingLevel) { + return getNested(nestingLevel, null); + } + + /** + * Returns a {@link ResolvableType} for the specified nesting level. The nesting level + * refers to the specific generic parameter that should be returned. A nesting level + * of 1 indicates this type, 2 indicates the first nested generic, 3 the second and so + * on. For example, given {@code List>} level 1 refers to the + * {@code List}, level 2 the {@code Set} and level 3 the {@code Integer}. + * + *

The {@code typeIndexesPerLevel} map can be used to reference a specific generic + * for the given level. For example, an index of 0 would refer to a {@code Map} key, + * where as 1 would refer to the value. If the map does not contain an value for a + * specific level the last generic will be used (e.g. a {@code Map} value). + * + *

Nesting levels may also apply to array types, for example given + * {@code String[]}, a nesting level of 2 referes to {@code String}. + * + *

If a type does not {@link #hasGenerics() contain} generics the + * {@link #getSuperType() super-type} hierarchy will be considered. + * @param nestingLevel the required nesting level, indexed from 1 for the current + * type, 2 for the first nested generic, 3 for the second and so on. + * @param typeIndexesPerLevel a map containing the generic index for a given nesting + * level (may be {@code null}). + * @return a {@link ResolvableType} for the nested level or {@link #NONE}. + */ + public ResolvableType getNested(int nestingLevel, + Map typeIndexesPerLevel) { + ResolvableType result = this; + for (int i = 2; i <= nestingLevel; i++) { + if (result.isArray()) { + result = result.getComponentType(); + } + else { + // Handle derived types + while (result != ResolvableType.NONE && !result.hasGenerics()) { + result = result.getSuperType(); + } + Integer index = (typeIndexesPerLevel == null ? null + : typeIndexesPerLevel.get(i)); + index = (index == null ? result.getGenerics().length - 1 : index); + result = result.getGeneric(index); + } + } + return result; + } + + /** + * Return a {@link ResolvableType} representing the generic parameter for the given + * indexes. Indexes are zero based, for example given the type + * {@code Map>}, {@code getGeneric(0)} will access the + * {@code Integer}. Nested generics can be accessed by specifying multiple indexes, + * for example {@code getGeneric(1, 0)} will access the {@code String} from the nested + * {@code List}. For convenience, if no indexes are specified the first generic is + * returned. + * + *

If no generic is available at the specified indexes {@link #NONE} is returned. + * @param indexes the indexes that refers to the generic parameter (may be omitted to + * return the first generic) + * @return a {@link ResolvableType} for the specified generic or {@link #NONE} + * @see #hasGenerics() + * @see #getGenerics() + * @see #resolveGeneric(int...) + * @see #resolveGenerics() + */ + public ResolvableType getGeneric(int... indexes) { + try { + if (indexes == null || indexes.length == 0) { + return getGenerics()[0]; + } + ResolvableType rtn = this; + for (int index : indexes) { + rtn = rtn.getGenerics()[index]; + } + return rtn; + } + catch (IndexOutOfBoundsException ex) { + return NONE; + } + } + + /** + * Return an array of {@link ResolvableType} representing the generics parameters of + * this type. If no generics are available an empty array is returned. If you need to + * access a specific generic consider using the {@link #getGeneric(int...)} method as + * it allows access to nested generics, and protects against + * {@code IndexOutOfBoundsExceptions} + * @return an array of {@link ResolvableType}s representing the generic parameters + * (never {@code null}) + * @see #hasGenerics() + * @see #getGeneric(int...) + * @see #resolveGeneric(int...) + * @see #resolveGenerics() + */ + public ResolvableType[] getGenerics() { + if (this == NONE) { + return EMPTY_TYPES_ARRAY; + } + if (this.type instanceof ParameterizedType) { + Type[] genericTypes = ((ParameterizedType) getType()).getActualTypeArguments(); + ResolvableType[] generics = new ResolvableType[genericTypes.length]; + for (int i = 0; i < genericTypes.length; i++) { + generics[i] = forType(genericTypes[i], this); + } + return generics; + } + return resolveType().getGenerics(); + } + + /** + * Convenience method that will {@link #getGenerics() get} and {@link #resolve() + * resolve} generic parameters. + * @return an array of resolved generic parameters (the resulting array will never be + * {@code null}, but it may contain {@code null} elements}) + * @see #getGenerics() + * @see #resolve() + */ + public Class[] resolveGenerics() { + ResolvableType[] generics = getGenerics(); + Class[] resolvedGenerics = new Class[generics.length]; + for (int i = 0; i < generics.length; i++) { + resolvedGenerics[i] = generics[i].resolve(); + } + return resolvedGenerics; + } + + /** + * Convenience method that will {@link #getGeneric(int...) get} and + * {@link #resolve() resolve} a specific generic parameters. + * @param indexes the indexes that refers to the generic parameter (may be omitted to + * return the first generic) + * @return a resolved {@link Class} or {@code null} + * @see #getGeneric(int...) + * @see #resolve() + */ + public Class resolveGeneric(int... indexes) { + return getGeneric(indexes).resolve(); + } + + /** + * Resolve this type to a {@link java.lang.Class}, returning {@code null} if the type + * cannot be resolved. This method will consider bounds of {@link TypeVariable}s and + * {@link WildcardType}s if direct resolution fails. + * @return the resolved {@link Class} or {@code null} + * @see #resolve(Class) + * @see #resolveGeneric(int...) + * @see #resolveGenerics() + */ + public Class resolve() { + return resolve(null); + } + + /** + * Resolve this type to a {@link java.lang.Class}, returning the specified + * {@code fallback} if the type cannot be resolved. This method will consider bounds + * of {@link TypeVariable}s and {@link WildcardType}s if direct resolution fails. + * @param fallback the fallback class to use if resolution fails (may be {@code null}) + * @return the resolved {@link Class} or the {@code fallback} + * @see #resolve() + * @see #resolveGeneric(int...) + * @see #resolveGenerics() + */ + public Class resolve(Class fallback) { + if (this.resolved == null) { + synchronized (this) { + this.resolved = resolveClass(); + this.resolved = (this.resolved == null ? void.class : this.resolved); + } + } + return (this.resolved == void.class ? fallback : this.resolved); + } + + private Class resolveClass() { + if (this.type instanceof Class || this.type == null) { + return (Class) this.type; + } + if (this.type instanceof GenericArrayType) { + return Array.newInstance(getComponentType().resolve(), 0).getClass(); + } + return resolveType().resolve(); + } + + /** + * Resolve this type by a single level, returning the resolved value or {@link #NONE}. + */ + ResolvableType resolveType() { + Type resolved = null; + if (this.type instanceof ParameterizedType) { + resolved = ((ParameterizedType) this.type).getRawType(); + } + else if (this.type instanceof WildcardType) { + resolved = resolveBounds(((WildcardType) this.type).getUpperBounds()); + if (resolved == null) { + resolved = resolveBounds(((WildcardType) this.type).getLowerBounds()); + } + } + else if (this.type instanceof TypeVariable) { + if (this.variableResolver != null) { + resolved = this.variableResolver.resolveVariable((TypeVariable) this.type); + } + if (resolved == null) { + resolved = resolveBounds(((TypeVariable) this.type).getBounds()); + } + } + return (resolved == null ? NONE : forType(resolved, this.variableResolver)); + } + + private Type resolveBounds(Type[] bounds) { + if (ObjectUtils.isEmpty(bounds) || Object.class.equals(bounds[0])) { + return null; + } + return bounds[0]; + } + + public Type resolveVariable(TypeVariable variable) { + Assert.notNull("Variable must not be null"); + if (this.type instanceof ParameterizedType) { + + ParameterizedType parameterizedType = (ParameterizedType) this.type; + Type owner = parameterizedType.getOwnerType(); + + if (parameterizedType.getRawType().equals(variable.getGenericDeclaration())) { + TypeVariable[] variables = resolve().getTypeParameters(); + for (int i = 0; i < variables.length; i++) { + if (ObjectUtils.nullSafeEquals(variables[i].getName(), variable.getName())) { + return parameterizedType.getActualTypeArguments()[i]; + } + } + } + + Type resolved = null; + if (this.variableResolver != null) { + resolved = this.variableResolver.resolveVariable(variable); + } + if (resolved == null && owner != null) { + resolved = forType(owner, this.variableResolver).resolveVariable(variable); + } + return resolved; + } + + if (this.type instanceof TypeVariable) { + return resolveType().resolveVariable(variable); + } + + return null; + } + + /** + * Return a string representation of this type in its fully resolved form + * (including any generic parameters). + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + if (isArray()) { + return getComponentType() + "[]"; + } + StringBuilder result = new StringBuilder(); + result.append(resolve() == null ? "?" : resolve().getName()); + if (hasGenerics()) { + result.append("<"); + result.append(StringUtils.arrayToDelimitedString(getGenerics(), ", ")); + result.append(">"); + } + return result.toString(); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.type) * 31 + + ObjectUtils.nullSafeHashCode(this.variableResolver); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof ResolvableType) { + ResolvableType other = (ResolvableType) obj; + return ObjectUtils.nullSafeEquals(this.type, other.type) + && ObjectUtils.nullSafeEquals(this.variableResolver, + other.variableResolver); + } + return false; + } + + + /** + * Return a {@link ResolvableType} for the specified {@link Class}. For example + * {@code ResolvableType.forClass(MyArrayList.class)}. + * @param sourceClass the source class (must not be {@code null} + * @return a {@link ResolvableType} for the specified class + * @see #forClass(Class, Class) + */ + public static ResolvableType forClass(Class sourceClass) { + Assert.notNull(sourceClass, "Source class must not be null"); + return forType(sourceClass); + } + + /** + * Return a {@link ResolvableType} for the specified {@link Class} with a given + * implementation. For example + * {@code ResolvableType.forClass(List.class, MyArrayList.class)}. + * @param sourceClass the source class (must not be {@code null} + * @param implementationClass the implementation class (must not be {@code null}) + * @return a {@link ResolvableType} for the specified class backed by the given + * implementation class + * @see #forClass(Class) + */ + public static ResolvableType forClass(Class sourceClass, Class implementationClass) { + Assert.notNull(sourceClass, "Source class must not be null"); + Assert.notNull(implementationClass, "ImplementationClass must not be null"); + ResolvableType asType = forType(implementationClass).as(sourceClass); + return (asType == NONE ? forType(sourceClass) : asType); + } + + /** + * Return a {@link ResolvableType} for the specified {@link Field}. + * @param field the source field + * @return a {@link ResolvableType} for the specified field + * @see #forField(Field, Class) + */ + public static ResolvableType forField(Field field) { + Assert.notNull(field, "Field must not be null"); + return forType(field.getGenericType()); + } + + /** + * Return a {@link ResolvableType} for the specified {@link Field} with a given + * implementation. Use this variant when the class that declares the field includes + * generic parameter variables that are satisfied by the implementation class. + * @param field the source field + * @param implementationClass the implementation class (must not be {@code null}) + * @return a {@link ResolvableType} for the specified field + * @see #forField(Field) + */ + public static ResolvableType forField(Field field, Class implementationClass) { + Assert.notNull(field, "Field must not be null"); + Assert.notNull(implementationClass, "ImplementationClass must not be null"); + TypeVariableResolver variableResolver = forType(implementationClass).as( + field.getDeclaringClass()); + return forType(field.getGenericType(), variableResolver); + } + + /** + * Return a {@link ResolvableType} for the specified {@link Constructor} parameter. + * @param constructor the source constructor (must not be {@code null}) + * @param parameterIndex the parameter index + * @return a {@link ResolvableType} for the specified constructor parameter + * @see #forConstructorParameter(Constructor, int, Class) + */ + public static ResolvableType forConstructorParameter(Constructor constructor, + int parameterIndex) { + Assert.notNull(constructor, "Constructor must not be null"); + return forMethodParameter(MethodParameter.forMethodOrConstructor(constructor, + parameterIndex)); + } + + /** + * Return a {@link ResolvableType} for the specified {@link Constructor} parameter + * with a given implementation. Use this variant when the class that declares the + * constructor includes generic parameter variables that are satisfied by the + * implementation class. + * @param constructor the source constructor (must not be {@code null}) + * @param parameterIndex the parameter index + * @param implementationClass the implementation class (must not be {@code null}) + * @return a {@link ResolvableType} for the specified constructor parameter + * @see #forConstructorParameter(Constructor, int) + */ + public static ResolvableType forConstructorParameter(Constructor constructor, + int parameterIndex, Class implementationClass) { + Assert.notNull(constructor, "Constructor must not be null"); + Assert.notNull(implementationClass, "ImplementationClass must not be null"); + return forMethodParameter( + MethodParameter.forMethodOrConstructor(constructor, parameterIndex), + implementationClass); + } + + /** + * Return a {@link ResolvableType} for the specified {@link Method} parameter. + * @param method the source method (must not be {@code null}) + * @param parameterIndex the parameter index + * @return a {@link ResolvableType} for the specified method parameter + * @see #forMethodParameter(Method, int, Class) + * @see #forMethodParameter(MethodParameter) + */ + public static ResolvableType forMethodParameter(Method method, int parameterIndex) { + Assert.notNull(method, "Method must not be null"); + return forMethodParameter(MethodParameter.forMethodOrConstructor(method, + parameterIndex)); + } + + /** + * Return a {@link ResolvableType} for the specified {@link Method} parameter with a + * given implementation. Use this variant when the class that declares the method + * includes generic parameter variables that are satisfied by the implementation + * class. + * @param method the source method (must not be {@code null}) + * @param parameterIndex the parameter index + * @param implementationClass the implementation class (must not be {@code null}) + * @return a {@link ResolvableType} for the specified method parameter + * @see #forMethodParameter(Method, int, Class) + * @see #forMethodParameter(MethodParameter) + */ + public static ResolvableType forMethodParameter(Method method, int parameterIndex, + Class implementationClass) { + Assert.notNull(method, "Method must not be null"); + return forMethodParameter( + MethodParameter.forMethodOrConstructor(method, parameterIndex), + implementationClass); + } + + /** + * Return a {@link ResolvableType} for the specified {@link MethodParameter}. + * @param methodParameter the source method parameter (must not be {@code null}) + * @return a {@link ResolvableType} for the specified method parameter + * @see #forMethodParameter(MethodParameter, Class) + * @see #forMethodParameter(Method, int) + */ + public static ResolvableType forMethodParameter(MethodParameter methodParameter) { + Assert.notNull(methodParameter, "MethodParameter must not be null"); + return forType(methodParameter.getGenericParameterType()).getNested( + methodParameter.getNestingLevel(), methodParameter.typeIndexesPerLevel); + } + + /** + * Return a {@link ResolvableType} for the specified {@link MethodParameter} with a + * given implementation. Use this variant when the class that declares the method + * includes generic parameter variables that are satisfied by the implementation + * class. + * @param methodParameter the source method parameter (must not be {@code null}) + * @param implementationClass the implementation class (must not be {@code null}) + * @return a {@link ResolvableType} for the specified method parameter + * @see #forMethodParameter(MethodParameter) + * @see #forMethodParameter(Method, int) + */ + public static ResolvableType forMethodParameter(MethodParameter methodParameter, + Class implementationClass) { + Assert.notNull(methodParameter, "MethodParameter must not be null"); + Assert.notNull(implementationClass, "ImplementationClass must not be null"); + TypeVariableResolver variableResolver = forType(implementationClass).as( + methodParameter.getMember().getDeclaringClass()); + return forType(methodParameter.getGenericParameterType(), variableResolver).getNested( + methodParameter.getNestingLevel(), methodParameter.typeIndexesPerLevel); + } + + /** + * Return a {@link ResolvableType} for the specified {@link Method} return. + * @param method the source for the method return + * @return a {@link ResolvableType} for the specified method return + * @see #forMethodReturn(Method, Class) + */ + public static ResolvableType forMethodReturn(Method method) { + Assert.notNull(method, "Method must not be null"); + return forType(method.getGenericReturnType()); + } + + /** + * Return a {@link ResolvableType} for the specified {@link Method} return. Use this + * variant when the class that declares the method includes generic parameter + * variables that are satisfied by the implementation class. + * @param method the source for the method return + * @param implementationClass the implementation class (must not be {@code null}) + * @return a {@link ResolvableType} for the specified method return + * @see #forMethodReturn(Method) + */ + public static ResolvableType forMethodReturn(Method method, + Class implementationClass) { + Assert.notNull(method, "Method must not be null"); + Assert.notNull(implementationClass, "ImplementationClass must not be null"); + TypeVariableResolver variableResolver = forType(implementationClass).as( + method.getDeclaringClass()); + return forType(method.getGenericReturnType(), variableResolver); + } + + /** + * Return a {@link ResolvableType} for the specified {@link java.lang.reflect.Type}. + * @param type the source type (must not be {@code null}) + * @return a {@link ResolvableType} for the specified {@link java.lang.reflect.Type} + */ + public static ResolvableType forType(Type type) { + return forType(type, null); + } + + /** + * Return a {@link ResolvableType} for the specified {@link java.lang.reflect.Type} + * backed by a given {@link TypeVariableResolver}. + * @param type the source type (must not be {@code null}) + * @param variableResolver the variable resolver + * @return a {@link ResolvableType} for the specified {@link java.lang.reflect.Type} + * and {@link TypeVariableResolver} + */ + public static ResolvableType forType(Type type, TypeVariableResolver variableResolver) { + ResolvableType key = new ResolvableType(type, variableResolver); + // Check the cache, we may have a ResolvableType that may have already been resolved + ResolvableType resolvableType = cache.get(key); + if (resolvableType == null) { + resolvableType = key; + cache.put(key, resolvableType); + } + return resolvableType; + } + + + /** + * Internal helper to handle bounds from {@link WildcardType}s. + */ + private static class WildcardBounds { + + + private final Kind kind; + + private final ResolvableType[] bounds; + + + /** + * Private constructor to create a new {@link WildcardBounds} instance. + * @param kind the kind of bounds + * @param bounds the bounds + * @see #get(ResolvableType) + */ + private WildcardBounds(Kind kind, ResolvableType[] bounds) { + this.kind = kind; + this.bounds = bounds; + } + + + /** + * Return {@code true} if this bounds is the same kind as the specified bounds. + */ + public boolean isSameKind(WildcardBounds bounds) { + return this.kind == bounds.kind; + } + + /** + * Return {@code true} if this bounds is assignable to all the specified types. + * @param types the types to test against + * @return {@code true} if this bounds is assignable to all types + */ + public boolean isAssignableFrom(ResolvableType... types) { + for (ResolvableType bound : this.bounds) { + for (ResolvableType type : types) { + if (!isAssignable(bound, type)) { + return false; + } + } + } + return true; + } + + private boolean isAssignable(ResolvableType source, ResolvableType from) { + return (this.kind == Kind.UPPER ? source.isAssignableFrom(from) + : from.isAssignableFrom(source)); + } + + /** + * Return the underlying bounds. + */ + public ResolvableType[] getBounds() { + return bounds; + } + + + /** + * Get a {@link WildcardBounds} instance for the specified type, returning + * {@code null} if the specified type cannot be resolved to a {@link WildcardType}. + * @param type the source type + * @return a {@link WildcardBounds} instance or {@code null} + */ + public static WildcardBounds get(ResolvableType type) { + ResolvableType resolveToWildcard = type; + while(!(resolveToWildcard.getType() instanceof WildcardType)) { + if (resolveToWildcard == NONE) { + return null; + } + resolveToWildcard = resolveToWildcard.resolveType(); + } + WildcardType wildcardType = (WildcardType) resolveToWildcard.type; + Kind boundsType = (wildcardType.getLowerBounds().length > 0 ? Kind.LOWER + : Kind.UPPER); + Type[] bounds = boundsType == Kind.UPPER ? wildcardType.getUpperBounds() + : wildcardType.getLowerBounds(); + ResolvableType[] resolvableBounds = new ResolvableType[bounds.length]; + for (int i = 0; i < bounds.length; i++) { + resolvableBounds[i] = forType(bounds[i], type.variableResolver); + } + return new WildcardBounds(boundsType, resolvableBounds); + } + + + /** + * The various kinds of bounds. + */ + static enum Kind { UPPER, LOWER } + + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/TypeVariableResolver.java b/spring-core/src/main/java/org/springframework/core/TypeVariableResolver.java new file mode 100644 index 0000000000..71f1c1dadc --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/TypeVariableResolver.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.core; + +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; + +/** + * Strategy interface that can be used to resolve {@link java.lang.reflect.TypeVariable}s. + * + * @author Phillip Webb + * @since 4.0 + */ +public interface TypeVariableResolver { + + /** + * Resolve the specified type variable. + * @param typeVariable the type variable to resolve (must not be {@code null}) + * @return the resolved {@link java.lang.reflect.Type} for the variable or + * {@code null} if the variable cannot be resolved. + */ + Type resolveVariable(TypeVariable typeVariable); + +} diff --git a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java new file mode 100644 index 0000000000..b4a91079ec --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java @@ -0,0 +1,1285 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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 org.springframework.core; + +import java.io.Serializable; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.AbstractCollection; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.hamcrest.Matchers; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.util.MultiValueMap; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.mockito.BDDMockito.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +// FIXME nested + +/** + * Tests for {@link ResolvableType}. + * + * @author Phillip Webb + */ +@SuppressWarnings("rawtypes") +@RunWith(MockitoJUnitRunner.class) +public class ResolvableTypeTests { + + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Captor + private ArgumentCaptor> typeVariableCaptor; + + + @Test + public void noneReturnValues() throws Exception { + ResolvableType none = ResolvableType.NONE; + assertThat(none.as(Object.class), equalTo(ResolvableType.NONE)); + assertThat(none.asCollection(), equalTo(ResolvableType.NONE)); + assertThat(none.asMap(), equalTo(ResolvableType.NONE)); + assertThat(none.getComponentType(), equalTo(ResolvableType.NONE)); + assertThat(none.getGeneric(0), equalTo(ResolvableType.NONE)); + assertThat(none.getGenerics().length, equalTo(0)); + assertThat(none.getInterfaces().length, equalTo(0)); + assertThat(none.getSuperType(), equalTo(ResolvableType.NONE)); + assertThat(none.getType(), nullValue()); + assertThat(none.hasGenerics(), equalTo(false)); + assertThat(none.isArray(), equalTo(false)); + assertThat(none.resolve(), nullValue()); + assertThat(none.resolve(String.class), equalTo((Class) String.class)); + assertThat(none.resolveGeneric(0), nullValue()); + assertThat(none.resolveGenerics().length, equalTo(0)); + assertThat(none.resolveVariable(mock(TypeVariable.class)), nullValue()); + assertThat(none.toString(), equalTo("?")); + assertThat(none.isAssignableFrom(ResolvableType.forClass(Object.class)), equalTo(false)); + } + + @Test + public void forClass() throws Exception { + ResolvableType type = ResolvableType.forClass(ExtendsList.class); + assertThat(type.getType(), equalTo((Type) ExtendsList.class)); + } + + @Test + public void forClassMustNotBeNull() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Source class must not be null"); + ResolvableType.forClass(null); + } + + @Test + public void forField() throws Exception { + Field field = Fields.class.getField("charSequenceList"); + ResolvableType type = ResolvableType.forField(field); + assertThat(type.getType(), equalTo(field.getGenericType())); + } + + @Test + public void forPrivateField() throws Exception { + Field field = Fields.class.getDeclaredField("privateField"); + ResolvableType type = ResolvableType.forField(field); + assertThat(type.getType(), equalTo(field.getGenericType())); + assertThat(type.resolve(), equalTo((Class) List.class)); + } + + @Test + public void forFieldMustNotBeNull() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Field must not be null"); + ResolvableType.forField(null); + } + + @Test + public void forConstructorParameter() throws Exception { + Constructor constructor = Constructors.class.getConstructor(List.class); + ResolvableType type = ResolvableType.forConstructorParameter(constructor, 0); + assertThat(type.getType(), equalTo(constructor.getGenericParameterTypes()[0])); + } + + @Test + public void forConstructorParameterMustNotBeNull() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Constructor must not be null"); + ResolvableType.forConstructorParameter(null, 0); + } + + @Test + public void forMethodParameterByIndex() throws Exception { + Method method = Methods.class.getMethod("charSequenceParameter", List.class); + ResolvableType type = ResolvableType.forMethodParameter(method, 0); + assertThat(type.getType(), equalTo(method.getGenericParameterTypes()[0])); + } + + @Test + public void forMethodParameterByIndexMustNotBeNull() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Method must not be null"); + ResolvableType.forMethodParameter(null, 0); + } + + @Test + public void forMethodParameter() throws Exception { + Method method = Methods.class.getMethod("charSequenceParameter", List.class); + MethodParameter methodParameter = MethodParameter.forMethodOrConstructor(method, 0); + ResolvableType type = ResolvableType.forMethodParameter(methodParameter); + assertThat(type.getType(), equalTo(method.getGenericParameterTypes()[0])); + } + + @Test + public void forMethodParameterWithNesting() throws Exception { + Method method = Methods.class.getMethod("nested", Map.class); + MethodParameter methodParameter = MethodParameter.forMethodOrConstructor(method, 0); + methodParameter.increaseNestingLevel(); + ResolvableType type = ResolvableType.forMethodParameter(methodParameter); + assertThat(type.resolve(), equalTo((Class) Map.class)); + assertThat(type.getGeneric(0).resolve(), equalTo((Class) Byte.class)); + assertThat(type.getGeneric(1).resolve(), equalTo((Class) Long.class)); + } + + @Test + public void forMethodParameterWithNestingAndLevels() throws Exception { + Method method = Methods.class.getMethod("nested", Map.class); + MethodParameter methodParameter = MethodParameter.forMethodOrConstructor(method, 0); + methodParameter.increaseNestingLevel(); + methodParameter.setTypeIndexForCurrentLevel(0); + ResolvableType type = ResolvableType.forMethodParameter(methodParameter); + assertThat(type.resolve(), equalTo((Class) Map.class)); + assertThat(type.getGeneric(0).resolve(), equalTo((Class) String.class)); + assertThat(type.getGeneric(1).resolve(), equalTo((Class) Integer.class)); + } + + @Test + public void forMethodParameterMustNotBeNull() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("MethodParameter must not be null"); + ResolvableType.forMethodParameter(null); + } + + @Test + public void forMethodReturn() throws Exception { + Method method = Methods.class.getMethod("charSequenceReturn"); + ResolvableType type = ResolvableType.forMethodReturn(method); + assertThat(type.getType(), equalTo(method.getGenericReturnType())); + } + + @Test + public void forMethodReturnMustNotBeNull() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Method must not be null"); + ResolvableType.forMethodReturn(null); + } + + @Test + public void classType() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("classType")); + assertThat(type.getType().getClass(), equalTo((Class) Class.class)); + } + + @Test + public void paramaterizedType() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("parameterizedType")); + assertThat(type.getType(), instanceOf(ParameterizedType.class)); + } + + @Test + public void arrayClassType() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("arrayClassType")); + assertThat(type.getType(), instanceOf(Class.class)); + assertThat(((Class) type.getType()).isArray(), equalTo(true)); + } + + @Test + public void genericArrayType() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("genericArrayType")); + assertThat(type.getType(), instanceOf(GenericArrayType.class)); + } + + @Test + public void wildcardType() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("wildcardType")); + assertThat(type.getType(), instanceOf(ParameterizedType.class)); + assertThat(type.getGeneric().getType(), instanceOf(WildcardType.class)); + } + + @Test + public void typeVariableType() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("typeVariableType")); + assertThat(type.getType(), instanceOf(TypeVariable.class)); + } + + @Test + public void getComponentTypeForClassArray() throws Exception { + Field field = Fields.class.getField("arrayClassType"); + ResolvableType type = ResolvableType.forField(field); + assertThat(type.isArray(), equalTo(true)); + assertThat(type.getComponentType().getType(), + equalTo((Type) ((Class) field.getGenericType()).getComponentType())); + } + + @Test + public void getComponentTypeForGenericArrayType() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("genericArrayType")); + assertThat(type.isArray(), equalTo(true)); + assertThat(type.getComponentType().getType(), + equalTo(((GenericArrayType) type.getType()).getGenericComponentType())); + } + + @Test + public void getComponentTypeForVariableThatResolvesToGenericArray() throws Exception { + ResolvableType type = ResolvableType.forClass(ListOfGenericArray.class).asCollection().getGeneric(); + assertThat(type.isArray(), equalTo(true)); + assertThat(type.getType(), instanceOf(TypeVariable.class)); + assertThat(type.getComponentType().getType().toString(), + equalTo("java.util.List")); + } + + @Test + public void getComponentTypeForNonArray() throws Exception { + ResolvableType type = ResolvableType.forClass(String.class); + assertThat(type.isArray(), equalTo(false)); + assertThat(type.getComponentType(), equalTo(ResolvableType.NONE)); + } + + @Test + public void asCollection() throws Exception { + ResolvableType type = ResolvableType.forClass(ExtendsList.class).asCollection(); + assertThat(type.resolve(), equalTo((Class) Collection.class)); + assertThat(type.resolveGeneric(), equalTo((Class) CharSequence.class)); + } + + @Test + public void asMap() throws Exception { + ResolvableType type = ResolvableType.forClass(ExtendsMap.class).asMap(); + assertThat(type.resolve(), equalTo((Class) Map.class)); + assertThat(type.resolveGeneric(0), equalTo((Class) String.class)); + assertThat(type.resolveGeneric(1), equalTo((Class) Integer.class)); + } + + @Test + public void asFromInterface() throws Exception { + ResolvableType type = ResolvableType.forClass(ExtendsList.class).as(List.class); + assertThat(type.getType().toString(), equalTo("java.util.List")); + } + + @Test + public void asFromInheritedInterface() throws Exception { + ResolvableType type = ResolvableType.forClass(ExtendsList.class).as(Collection.class); + assertThat(type.getType().toString(), equalTo("java.util.Collection")); + } + + @Test + public void asFromSuperType() throws Exception { + ResolvableType type = ResolvableType.forClass(ExtendsList.class).as(ArrayList.class); + assertThat(type.getType().toString(), equalTo("java.util.ArrayList")); + } + + @Test + public void asFromInheritedSuperType() throws Exception { + ResolvableType type = ResolvableType.forClass(ExtendsList.class).as(List.class); + assertThat(type.getType().toString(), equalTo("java.util.List")); + } + + @Test + public void asNotFound() throws Exception { + ResolvableType type = ResolvableType.forClass(ExtendsList.class).as(Map.class); + assertThat(type, sameInstance(ResolvableType.NONE)); + } + + @Test + public void asSelf() throws Exception { + ResolvableType type = ResolvableType.forClass(ExtendsList.class); + assertThat(type.as(ExtendsList.class), equalTo(type)); + } + + @Test + public void getSuperType() throws Exception { + ResolvableType type = ResolvableType.forClass(ExtendsList.class).getSuperType(); + assertThat(type.resolve(), equalTo((Class) ArrayList.class)); + type = type.getSuperType(); + assertThat(type.resolve(), equalTo((Class) AbstractList.class)); + type = type.getSuperType(); + assertThat(type.resolve(), equalTo((Class) AbstractCollection.class)); + type = type.getSuperType(); + assertThat(type.resolve(), equalTo((Class) Object.class)); + } + + @Test + public void getInterfaces() throws Exception { + ResolvableType type = ResolvableType.forClass(ExtendsList.class); + assertThat(type.getInterfaces().length, equalTo(0)); + SortedSet interfaces = new TreeSet(); + for (ResolvableType interfaceType : type.getSuperType().getInterfaces()) { + interfaces.add(interfaceType.toString()); + } + assertThat(interfaces.toString(), equalTo( + "[" + + "java.io.Serializable, " + + "java.lang.Cloneable, " + + "java.util.List, " + + "java.util.RandomAccess" + + "]")); + } + + @Test + public void noSuperType() throws Exception { + assertThat(ResolvableType.forClass(Object.class).getSuperType(), + equalTo(ResolvableType.NONE)); + } + + @Test + public void noInterfaces() throws Exception { + assertThat(ResolvableType.forClass(Object.class).getInterfaces().length, + equalTo(0)); + } + + @Test + public void nested() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("nested")); + type = type.getNested(2); + assertThat(type.resolve(), equalTo((Class) Map.class)); + assertThat(type.getGeneric(0).resolve(), equalTo((Class) Byte.class)); + assertThat(type.getGeneric(1).resolve(), equalTo((Class) Long.class)); + } + + @Test + public void nestedWithIndexes() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("nested")); + type = type.getNested(2, Collections.singletonMap(2, 0)); + assertThat(type.resolve(), equalTo((Class) Map.class)); + assertThat(type.getGeneric(0).resolve(), equalTo((Class) String.class)); + assertThat(type.getGeneric(1).resolve(), equalTo((Class) Integer.class)); + } + + @Test + public void nestedWithArray() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("genericArrayType")); + type = type.getNested(2); + assertThat(type.resolve(), equalTo((Class) List.class)); + assertThat(type.resolveGeneric(), equalTo((Class) String.class)); + } + + @Test + public void getGeneric() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("stringList")); + assertThat(type.getGeneric().getType(), equalTo((Type) String.class)); + } + + @Test + public void getGenericByIndex() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("stringIntegerMultiValueMap")); + assertThat(type.getGeneric(0).getType(), equalTo((Type) String.class)); + assertThat(type.getGeneric(1).getType(), equalTo((Type) Integer.class)); + } + + @Test + public void getGenericOfGeneric() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("stringListList")); + assertThat(type.getGeneric().getType().toString(), equalTo("java.util.List")); + assertThat(type.getGeneric().getGeneric().getType(), equalTo((Type) String.class)); + } + + @Test + public void getGenericOfGenericByIndexes() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("stringListList")); + assertThat(type.getGeneric(0, 0).getType(), equalTo((Type) String.class)); + } + + @Test + public void getGenericOutOfBounds() throws Exception { + ResolvableType type = ResolvableType.forClass(List.class, ExtendsList.class); + assertThat(type.getGeneric(0), not(equalTo(ResolvableType.NONE))); + assertThat(type.getGeneric(1), equalTo(ResolvableType.NONE)); + assertThat(type.getGeneric(0, 1), equalTo(ResolvableType.NONE)); + } + + @Test + public void hasGenerics() throws Exception { + ResolvableType type = ResolvableType.forClass(ExtendsList.class); + assertThat(type.hasGenerics(), equalTo(false)); + assertThat(type.asCollection().hasGenerics(), equalTo(true)); + } + + @Test + public void getGenerics() throws Exception { + ResolvableType type = ResolvableType.forClass(List.class, ExtendsList.class); + ResolvableType[] generics = type.getGenerics(); + assertThat(generics.length, equalTo(1)); + assertThat(generics[0].resolve(), equalTo((Class) CharSequence.class)); + } + + @Test + public void noGetGenerics() throws Exception { + ResolvableType type = ResolvableType.forClass(ExtendsList.class); + ResolvableType[] generics = type.getGenerics(); + assertThat(generics.length, equalTo(0)); + } + + @Test + public void getResolvedGenerics() throws Exception { + ResolvableType type = ResolvableType.forClass(List.class, ExtendsList.class); + Class[] generics = type.resolveGenerics(); + assertThat(generics.length, equalTo(1)); + assertThat(generics[0], equalTo((Class) CharSequence.class)); + } + + @Test + public void resolveClassType() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("classType")); + assertThat(type.resolve(), equalTo((Class) List.class)); + } + + @Test + public void resolveParameterizedType() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("parameterizedType")); + assertThat(type.resolve(), equalTo((Class) List.class)); + } + + @Test + public void resolveArrayClassType() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("arrayClassType")); + assertThat(type.resolve(), equalTo((Class) List[].class)); + } + + @Test + public void resolveGenericArrayType() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("genericArrayType")); + assertThat(type.resolve(), equalTo((Class) List[].class)); + assertThat(type.getComponentType().resolve(), equalTo((Class) List.class)); + assertThat(type.getComponentType().getGeneric().resolve(), equalTo((Class) String.class)); + } + + @Test + public void resolveGenericMultiArrayType() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("genericMultiArrayType")); + assertThat(type.resolve(), equalTo((Class) List[][][].class)); + assertThat(type.getComponentType().resolve(), equalTo((Class) List[][].class)); + } + + @Test + public void resolveGenericArrayFromGeneric() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("stringArrayList")); + ResolvableType generic = type.asCollection().getGeneric(); + assertThat(generic.getType().toString(), equalTo("E")); + assertThat(generic.isArray(), equalTo(true)); + assertThat(generic.resolve(), equalTo((Class) String[].class)); + } + + @Test + public void resolveWildcardTypeUpperBounds() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("wildcardType")); + assertThat(type.getGeneric().resolve(), equalTo((Class) Number.class)); + } + + @Test + public void resolveWildcardLowerBounds() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("wildcardSuperType")); + assertThat(type.getGeneric().resolve(), equalTo((Class) Number.class)); + } + + @Test + public void resolveVariableFromFieldType() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("stringList")); + assertThat(type.resolve(), equalTo((Class) List.class)); + assertThat(type.getGeneric().resolve(), equalTo((Class) String.class)); + } + + @Test + public void resolveVariableFromFieldTypeUnknown() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("parameterizedType")); + assertThat(type.resolve(), equalTo((Class) List.class)); + assertThat(type.getGeneric().resolve(), nullValue()); + } + + @Test + public void resolveVariableFromInheritedField() throws Exception { + ResolvableType type = ResolvableType.forField( + Fields.class.getField("stringIntegerMultiValueMap")).as(Map.class); + assertThat(type.getGeneric(0).resolve(), equalTo((Class) String.class)); + assertThat(type.getGeneric(1).resolve(), equalTo((Class) List.class)); + assertThat(type.getGeneric(1, 0).resolve(), equalTo((Class) Integer.class)); + } + + @Test + public void resolveVariableFromInheritedFieldSwitched() throws Exception { + ResolvableType type = ResolvableType.forField( + Fields.class.getField("stringIntegerMultiValueMapSwitched")).as(Map.class); + assertThat(type.getGeneric(0).resolve(), equalTo((Class) String.class)); + assertThat(type.getGeneric(1).resolve(), equalTo((Class) List.class)); + assertThat(type.getGeneric(1, 0).resolve(), equalTo((Class) Integer.class)); + } + + @Test + public void doesResolveFromOuterOwner() throws Exception { + ResolvableType type = ResolvableType.forField( + Fields.class.getField("listOfListOfUnknown")).as(Collection.class); + ResolvableType generic = type.getGeneric(0); + assertThat(generic.resolve(), equalTo((Class) List.class)); + assertThat(generic.as(Collection.class).getGeneric(0).as(Collection.class).resolve(), nullValue()); + } + + @Test + public void resolveBoundedTypeVariableResult() throws Exception { + ResolvableType type = ResolvableType.forMethodReturn(Methods.class.getMethod("boundedTypeVaraibleResult")); + assertThat(type.resolve(), equalTo((Class) CharSequence.class)); + } + + @Test + public void resolveVariableNotFound() throws Exception { + ResolvableType type = ResolvableType.forMethodReturn(Methods.class.getMethod("typedReturn")); + assertThat(type.resolve(), nullValue()); + } + + @Test + public void resolveTypeVaraibleFromMethodReturn() throws Exception { + ResolvableType type = ResolvableType.forMethodReturn(Methods.class.getMethod("typedReturn")); + assertThat(type.resolve(), nullValue()); + } + + @Test + public void resolveTypeVaraibleFromMethodReturnWithInstanceClass() throws Exception { + ResolvableType type = ResolvableType.forMethodReturn( + Methods.class.getMethod("typedReturn"), TypedMethods.class); + assertThat(type.resolve(), equalTo((Class) String.class)); + } + + @Test + public void resolveTypeVaraibleFromSimpleInterfaceType() { + ResolvableType type = ResolvableType.forClass( + MySimpleInterfaceType.class).as(MyInterfaceType.class); + assertThat(type.resolveGeneric(), equalTo((Class) String.class)); + } + + @Test + public void resolveTypeVaraibleFromSimpleCollectionInterfaceType() { + ResolvableType type = ResolvableType.forClass( + MyCollectionInterfaceType.class).as(MyInterfaceType.class); + assertThat(type.resolveGeneric(), equalTo((Class) Collection.class)); + assertThat(type.resolveGeneric(0, 0), equalTo((Class) String.class)); + } + + @Test + public void resolveTypeVaraibleFromSimpleSuperclassType() { + ResolvableType type = ResolvableType.forClass( + MySimpleSuperclassType.class).as(MySuperclassType.class); + assertThat(type.resolveGeneric(), equalTo((Class) String.class)); + } + + @Test + public void resolveTypeVaraibleFromSimpleCollectionSuperclassType() { + ResolvableType type = ResolvableType.forClass( + MyCollectionSuperclassType.class).as(MySuperclassType.class); + assertThat(type.resolveGeneric(), equalTo((Class) Collection.class)); + assertThat(type.resolveGeneric(0, 0), equalTo((Class) String.class)); + } + + @Test + public void resolveTypeVariableFromFieldTypeWithImplementsClass() throws Exception { + ResolvableType type = ResolvableType.forField( + Fields.class.getField("parameterizedType"), TypedFields.class); + assertThat(type.resolve(), equalTo((Class) List.class)); + assertThat(type.getGeneric().resolve(), equalTo((Class) String.class)); + } + + @Test + public void resolveTypeVariableFromSuperType() throws Exception { + ResolvableType type = ResolvableType.forClass(ExtendsList.class); + assertThat(type.resolve(), equalTo((Class) ExtendsList.class)); + assertThat(type.asCollection().resolveGeneric(), + equalTo((Class) CharSequence.class)); + } + + @Test + public void resolveTypeVariableFromClassWithImplementsClass() throws Exception { + ResolvableType type = ResolvableType.forClass( + MySuperclassType.class, MyCollectionSuperclassType.class); + assertThat(type.resolveGeneric(), equalTo((Class) Collection.class)); + assertThat(type.resolveGeneric(0, 0), equalTo((Class) String.class)); + } + + @Test + public void resolveTypeVariableFromConstructorParameter() throws Exception { + Constructor constructor = Constructors.class.getConstructor(List.class); + ResolvableType type = ResolvableType.forConstructorParameter(constructor, 0); + assertThat(type.resolve(), equalTo((Class) List.class)); + assertThat(type.resolveGeneric(0), equalTo((Class) CharSequence.class)); + } + + @Test + public void resolveUnknownTypeVariableFromConstructorParameter() throws Exception { + Constructor constructor = Constructors.class.getConstructor(Map.class); + ResolvableType type = ResolvableType.forConstructorParameter(constructor, 0); + assertThat(type.resolve(), equalTo((Class) Map.class)); + assertThat(type.resolveGeneric(0), nullValue()); + } + + @Test + public void resolveTypeVariableFromConstructorParameterWithImplementsClass() throws Exception { + Constructor constructor = Constructors.class.getConstructor(Map.class); + ResolvableType type = ResolvableType.forConstructorParameter( + constructor, 0, TypedConstructors.class); + assertThat(type.resolve(), equalTo((Class) Map.class)); + assertThat(type.resolveGeneric(0), equalTo((Class) String.class)); + } + + @Test + public void resolveTypeVariableFromMethodParameter() throws Exception { + Method method = Methods.class.getMethod("typedParameter", Object.class); + ResolvableType type = ResolvableType.forMethodParameter(method, 0); + assertThat(type.resolve(), nullValue()); + assertThat(type.getType().toString(), equalTo("T")); + } + + @Test + public void resolveTypeVariableFromMethodParameterWithImplementsClass() throws Exception { + Method method = Methods.class.getMethod("typedParameter", Object.class); + ResolvableType type = ResolvableType.forMethodParameter(method, 0, TypedMethods.class); + assertThat(type.resolve(), equalTo((Class) String.class)); + assertThat(type.getType().toString(), equalTo("T")); + } + + @Test + public void resolveTypeVariableFromMethodParameterType() throws Exception { + Method method = Methods.class.getMethod("typedParameter", Object.class); + MethodParameter methodParameter = MethodParameter.forMethodOrConstructor(method, 0); + ResolvableType type = ResolvableType.forMethodParameter(methodParameter); + assertThat(type.resolve(), nullValue()); + assertThat(type.getType().toString(), equalTo("T")); + } + + @Test + public void resolveTypeVariableFromMethodParameterTypeWithImplementsClass() + throws Exception { + Method method = Methods.class.getMethod("typedParameter", Object.class); + MethodParameter methodParameter = MethodParameter.forMethodOrConstructor(method, 0); + ResolvableType type = ResolvableType.forMethodParameter(methodParameter, TypedMethods.class); + assertThat(type.resolve(), equalTo((Class) String.class)); + assertThat(type.getType().toString(), equalTo("T")); + } + + @Test + public void resolveTypeVariableFromMethodReturn() throws Exception { + Method method = Methods.class.getMethod("typedReturn"); + ResolvableType type = ResolvableType.forMethodReturn(method); + assertThat(type.resolve(), nullValue()); + assertThat(type.getType().toString(), equalTo("T")); + } + + @Test + public void resolveTypeVariableFromMethodReturnWithImplementsClass() throws Exception { + Method method = Methods.class.getMethod("typedReturn"); + ResolvableType type = ResolvableType.forMethodReturn(method, TypedMethods.class); + assertThat(type.resolve(), equalTo((Class) String.class)); + assertThat(type.getType().toString(), equalTo("T")); + } + + @Test + public void resolveTypeVariableFromType() throws Exception { + Type sourceType = Methods.class.getMethod("typedReturn").getGenericReturnType(); + ResolvableType type = ResolvableType.forType(sourceType); + assertThat(type.resolve(), nullValue()); + assertThat(type.getType().toString(), equalTo("T")); + } + + @Test + public void resolveTypeVariableFromTypeWithVariableResolver() throws Exception { + Type sourceType = Methods.class.getMethod("typedReturn").getGenericReturnType(); + ResolvableType type = ResolvableType.forType( + sourceType, ResolvableType.forClass(TypedMethods.class).as(Methods.class)); + assertThat(type.resolve(), equalTo((Class) String.class)); + assertThat(type.getType().toString(), equalTo("T")); + } + + @Test + public void resolveTypeWithCustomVariableResolver() throws Exception { + TypeVariableResolver variableResolver = mock(TypeVariableResolver.class); + given(variableResolver.resolveVariable((TypeVariable) anyObject())).willReturn(Long.class); + + ResolvableType variable = ResolvableType.forType( + Fields.class.getField("typeVariableType").getGenericType(), variableResolver); + ResolvableType parameterized = ResolvableType.forType( + Fields.class.getField("parameterizedType").getGenericType(), variableResolver); + + assertThat(variable.resolve(), equalTo((Class) Long.class)); + assertThat(parameterized.resolve(), equalTo((Class) List.class)); + assertThat(parameterized.resolveGeneric(), equalTo((Class) Long.class)); + verify(variableResolver, atLeastOnce()).resolveVariable(this.typeVariableCaptor.capture()); + assertThat(this.typeVariableCaptor.getValue().getName(), equalTo("T")); + } + + @Test + public void toStrings() throws Exception { + assertThat(ResolvableType.NONE.toString(), equalTo("?")); + + assertFieldToStringValue("classType", "java.util.List"); + assertFieldToStringValue("typeVariableType", "?"); + assertFieldToStringValue("parameterizedType", "java.util.List"); + assertFieldToStringValue("arrayClassType", "java.util.List[]"); + assertFieldToStringValue("genericArrayType", "java.util.List[]"); + assertFieldToStringValue("genericMultiArrayType", "java.util.List[][][]"); + assertFieldToStringValue("wildcardType", "java.util.List"); + assertFieldToStringValue("wildcardSuperType", "java.util.List"); + assertFieldToStringValue("charSequenceList", "java.util.List"); + assertFieldToStringValue("stringList", "java.util.List"); + assertFieldToStringValue("stringListList", "java.util.List>"); + assertFieldToStringValue("stringArrayList", "java.util.List"); + assertFieldToStringValue("stringIntegerMultiValueMap", "org.springframework.util.MultiValueMap"); + assertFieldToStringValue("stringIntegerMultiValueMapSwitched", VariableNameSwitch.class.getName() + ""); + assertFieldToStringValue("listOfListOfUnknown", "java.util.List"); + + assertTypedFieldToStringValue("typeVariableType", "java.lang.String"); + assertTypedFieldToStringValue("parameterizedType", "java.util.List"); + + assertThat(ResolvableType.forClass(ListOfGenericArray.class).toString(), equalTo(ListOfGenericArray.class.getName())); + assertThat(ResolvableType.forClass(List.class, ListOfGenericArray.class).toString(), equalTo("java.util.List[]>")); + } + + private void assertFieldToStringValue(String field, String expected) throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField(field)); + assertThat("field " + field + " toString", type.toString(), equalTo(expected)); + } + + private void assertTypedFieldToStringValue(String field, String expected) + throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField(field), TypedFields.class); + assertThat("field " + field + " toString", type.toString(), equalTo(expected)); + } + + @Test + public void resolveFromOuterClass() throws Exception { + Field field = EnclosedInParameterizedType.InnerTyped.class.getField("field"); + ResolvableType type = ResolvableType.forField( + field, TypedEnclosedInParameterizedType.TypedInnerTyped.class); + assertThat(type.resolve(), equalTo((Type) Integer.class)); + } + + @Test + public void isAssignableFromMustNotBeNull() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Type must not be null"); + ResolvableType.forClass(Object.class).isAssignableFrom(null); + } + + @Test + public void isAssignableFromForNone() throws Exception { + ResolvableType objectType = ResolvableType.forClass(Object.class); + assertThat(objectType.isAssignableFrom(ResolvableType.NONE), equalTo(false)); + assertThat(ResolvableType.NONE.isAssignableFrom(objectType), equalTo(false)); + } + + @Test + public void isAssignableFromForClassAndClass() throws Exception { + ResolvableType objectType = ResolvableType.forClass(Object.class); + ResolvableType charSequenceType = ResolvableType.forClass(CharSequence.class); + ResolvableType stringType = ResolvableType.forClass(String.class); + + assertAssignable(objectType, objectType, charSequenceType, stringType).equalTo(true, true, true); + assertAssignable(charSequenceType, objectType, charSequenceType, stringType).equalTo(false, true, true); + assertAssignable(stringType, objectType, charSequenceType, stringType).equalTo(false, false, true); + } + + @Test + public void isAssignableFromCannotBeResolved() throws Exception { + ResolvableType objectType = ResolvableType.forClass(Object.class); + ResolvableType unresolvableVariable = ResolvableType.forField(AssignmentBase.class.getField("o")); + assertThat(unresolvableVariable.resolve(), nullValue()); + assertAssignable(objectType, unresolvableVariable).equalTo(false); + assertAssignable(unresolvableVariable, objectType).equalTo(false); + } + + @Test + public void isAssignableFromForClassAndSimpleVariable() throws Exception { + ResolvableType objectType = ResolvableType.forClass(Object.class); + ResolvableType charSequenceType = ResolvableType.forClass(CharSequence.class); + ResolvableType stringType = ResolvableType.forClass(String.class); + + ResolvableType objectVariable = ResolvableType.forField(AssignmentBase.class.getField("o"), Assignment.class); + ResolvableType charSequenceVariable = ResolvableType.forField(AssignmentBase.class.getField("c"), Assignment.class); + ResolvableType stringVariable = ResolvableType.forField(AssignmentBase.class.getField("s"), Assignment.class); + + assertAssignable(objectType, objectVariable, charSequenceVariable, stringVariable).equalTo(true, true, true); + assertAssignable(charSequenceType, objectVariable, charSequenceVariable, stringVariable).equalTo(false, true, true); + assertAssignable(stringType, objectVariable, charSequenceVariable, stringVariable).equalTo(false, false, true); + + assertAssignable(objectVariable, objectType, charSequenceType, stringType).equalTo(true, true, true); + assertAssignable(charSequenceVariable, objectType, charSequenceType, stringType).equalTo(false, true, true); + assertAssignable(stringVariable, objectType, charSequenceType, stringType).equalTo(false, false, true); + + assertAssignable(objectVariable, objectVariable, charSequenceVariable, stringVariable).equalTo(true, true, true); + assertAssignable(charSequenceVariable, objectVariable, charSequenceVariable, stringVariable).equalTo(false, true, true); + assertAssignable(stringVariable, objectVariable, charSequenceVariable, stringVariable).equalTo(false, false, true); + } + + @Test + public void isAssignableFromForSameClassNonExtendsGenerics() throws Exception { + ResolvableType objectList = ResolvableType.forField(AssignmentBase.class.getField("listo"), Assignment.class); + ResolvableType stringList = ResolvableType.forField(AssignmentBase.class.getField("lists"), Assignment.class); + + assertAssignable(stringList, objectList).equalTo(false); + assertAssignable(objectList, stringList).equalTo(false); + assertAssignable(stringList, stringList).equalTo(true); + } + + @Test + public void isAssignableFromForSameClassExtendsGenerics() throws Exception { + + // Generic assignment can be a little confusing, given: + // + // List c1, List c2, List s; + // + // c2 = s; is allowed and is often used for argument input, for example + // see List.addAll(). You can get items from c2 but you cannot add items without + // getting a generic type 'is not applicable for the arguments' error. This makes + // sense since if you added a StringBuffer to c2 it would break the rules on s. + // + // c1 = s; not allowed. Since there is no '? extends' to cause the generic + // 'is not applicable for the arguments' error when adding (which would pollute + // s). + + ResolvableType objectList = ResolvableType.forField(AssignmentBase.class.getField("listo"), Assignment.class); + ResolvableType charSequenceList = ResolvableType.forField(AssignmentBase.class.getField("listc"), Assignment.class); + ResolvableType stringList = ResolvableType.forField(AssignmentBase.class.getField("lists"), Assignment.class); + ResolvableType extendsObjectList = ResolvableType.forField(AssignmentBase.class.getField("listxo"), Assignment.class); + ResolvableType extendsCharSequenceList = ResolvableType.forField(AssignmentBase.class.getField("listxc"), Assignment.class); + ResolvableType extendsStringList = ResolvableType.forField(AssignmentBase.class.getField("listxs"), Assignment.class); + + assertAssignable(objectList, extendsObjectList, extendsCharSequenceList, extendsStringList).equalTo(false, false, false); + assertAssignable(charSequenceList, extendsObjectList, extendsCharSequenceList, extendsStringList).equalTo(false, false, false); + assertAssignable(stringList, extendsObjectList, extendsCharSequenceList, extendsStringList).equalTo(false, false, false); + assertAssignable(extendsObjectList, objectList, charSequenceList, stringList).equalTo(true, true, true); + assertAssignable(extendsObjectList, extendsObjectList, extendsCharSequenceList, extendsStringList).equalTo(true, true, true); + assertAssignable(extendsCharSequenceList, extendsObjectList, extendsCharSequenceList, extendsStringList).equalTo(false, true, true); + assertAssignable(extendsCharSequenceList, objectList, charSequenceList, stringList).equalTo(false, true, true); + assertAssignable(extendsStringList, extendsObjectList, extendsCharSequenceList, extendsStringList).equalTo(false, false, true); + assertAssignable(extendsStringList, objectList, charSequenceList, stringList).equalTo(false, false, true); + } + + @Test + public void isAssignableFromForDifferentClassesWithGenerics() throws Exception { + ResolvableType extendsCharSequenceCollection = ResolvableType.forField(AssignmentBase.class.getField("collectionxc"), Assignment.class); + ResolvableType charSequenceCollection = ResolvableType.forField(AssignmentBase.class.getField("collectionc"), Assignment.class); + ResolvableType charSequenceList = ResolvableType.forField(AssignmentBase.class.getField("listc"), Assignment.class); + ResolvableType extendsCharSequenceList = ResolvableType.forField(AssignmentBase.class.getField("listxc"), Assignment.class); + ResolvableType extendsStringList = ResolvableType.forField(AssignmentBase.class.getField("listxs"), Assignment.class); + + assertAssignable(extendsCharSequenceCollection, charSequenceCollection, charSequenceList, extendsCharSequenceList, extendsStringList) + .equalTo(true, true, true, true); + assertAssignable(charSequenceCollection, charSequenceList, extendsCharSequenceList, extendsStringList) + .equalTo(true, false, false); + assertAssignable(charSequenceList, extendsCharSequenceCollection, charSequenceCollection) + .equalTo(false, false); + assertAssignable(extendsCharSequenceList, extendsCharSequenceCollection, charSequenceCollection) + .equalTo(false, false); + assertAssignable(extendsStringList, charSequenceCollection, charSequenceList, extendsCharSequenceList) + .equalTo(false, false, false); + } + + @Test + public void isAssignableFromForArrays() throws Exception { + ResolvableType object = ResolvableType.forField(AssignmentBase.class.getField("o"), Assignment.class); + ResolvableType objectArray = ResolvableType.forField(AssignmentBase.class.getField("oarray"), Assignment.class); + ResolvableType charSequenceArray = ResolvableType.forField(AssignmentBase.class.getField("carray"), Assignment.class); + ResolvableType stringArray = ResolvableType.forField(AssignmentBase.class.getField("sarray"), Assignment.class); + + assertAssignable(object, objectArray, charSequenceArray, stringArray). + equalTo(true, true, true); + assertAssignable(objectArray, object, objectArray, charSequenceArray, stringArray). + equalTo(false, true, true, true); + assertAssignable(charSequenceArray, object, objectArray, charSequenceArray, stringArray). + equalTo(false, false, true, true); + assertAssignable(stringArray, object, objectArray, charSequenceArray, stringArray). + equalTo(false, false, false, true); + } + + @Test + public void isAssignableFromForWildcards() throws Exception { + + ResolvableType object = ResolvableType.forClass(Object.class); + ResolvableType charSequence = ResolvableType.forClass(CharSequence.class); + ResolvableType string = ResolvableType.forClass(String.class); + ResolvableType extendsObject = ResolvableType.forField(AssignmentBase.class.getField("listxo"), Assignment.class).getGeneric(); + ResolvableType extendsCharSequence = ResolvableType.forField(AssignmentBase.class.getField("listxc"), Assignment.class).getGeneric(); + ResolvableType extendsString = ResolvableType.forField(AssignmentBase.class.getField("listxs"), Assignment.class).getGeneric(); + ResolvableType superObject = ResolvableType.forField(AssignmentBase.class.getField("listso"), Assignment.class).getGeneric(); + ResolvableType superCharSequence = ResolvableType.forField(AssignmentBase.class.getField("listsc"), Assignment.class).getGeneric(); + ResolvableType superString = ResolvableType.forField(AssignmentBase.class.getField("listss"), Assignment.class).getGeneric(); + + // Language Spec 4.5.1. Type Arguments and Wildcards + + // ? extends T <= ? extends S if T <: S + assertAssignable(extendsCharSequence, extendsObject, extendsCharSequence, extendsString). + equalTo(false, true, true); + assertAssignable(extendsCharSequence, object, charSequence, string). + equalTo(false, true, true); + + // ? super T <= ? super S if S <: T + assertAssignable(superCharSequence, superObject, superCharSequence, superString). + equalTo(true, true, false); + assertAssignable(superCharSequence, object, charSequence, string). + equalTo(true, true, false); + + // [Implied] super / extends cannot be mixed + assertAssignable(superCharSequence, extendsObject, extendsCharSequence, extendsString). + equalTo(false, false, false); + assertAssignable(extendsCharSequence, superObject, superCharSequence, superString). + equalTo(false, false, false); + + // T <= T + assertAssignable(charSequence, object, charSequence, string). + equalTo(false, true, true); + + // T <= ? extends T + assertAssignable(extendsCharSequence, object, charSequence, string). + equalTo(false, true, true); + assertAssignable(charSequence, extendsObject, extendsCharSequence, extendsString). + equalTo(false, false, false); + + // T <= ? super T + assertAssignable(superCharSequence, object, charSequence, string). + equalTo(true, true, false); + assertAssignable(charSequence, superObject, superCharSequence, superString). + equalTo(false, false, false); + } + + @Test + public void isAssignableFromForComplexWildcards() throws Exception { + ResolvableType complex1 = ResolvableType.forField(AssignmentBase.class.getField("complexWildcard1")); + ResolvableType complex2 = ResolvableType.forField(AssignmentBase.class.getField("complexWildcard2")); + ResolvableType complex3 = ResolvableType.forField(AssignmentBase.class.getField("complexWildcard3")); + ResolvableType complex4 = ResolvableType.forField(AssignmentBase.class.getField("complexWildcard4")); + + assertAssignable(complex1, complex2).equalTo(true); + assertAssignable(complex2, complex1).equalTo(false); + assertAssignable(complex3, complex4).equalTo(true); + assertAssignable(complex4, complex3).equalTo(false); + } + + @Test + public void hashCodeAndEquals() throws Exception { + ResolvableType forClass = ResolvableType.forClass(List.class); + ResolvableType forFieldDirect = ResolvableType.forField(Fields.class.getDeclaredField("stringList")); + ResolvableType forFieldViaType = ResolvableType.forType(Fields.class.getDeclaredField("stringList").getGenericType()); + ResolvableType forFieldWithImplementation = ResolvableType.forField(Fields.class.getDeclaredField("stringList"), TypedFields.class); + + assertThat(forClass, equalTo(forClass)); + assertThat(forClass.hashCode(), equalTo(forClass.hashCode())); + assertThat(forClass, not(equalTo(forFieldDirect))); + assertThat(forClass, not(equalTo(forFieldWithImplementation))); + + assertThat(forFieldDirect, equalTo(forFieldDirect)); + assertThat(forFieldDirect, equalTo(forFieldViaType)); + assertThat(forFieldDirect, not(equalTo(forFieldWithImplementation))); + } + + @SuppressWarnings("unused") + private HashMap> myMap; + + @Test + public void javaDocSample() throws Exception { + ResolvableType t = ResolvableType.forField(getClass().getDeclaredField("myMap")); + assertThat(t.getSuperType().toString(), equalTo("java.util.AbstractMap>")); + assertThat(t.asMap().toString(), equalTo("java.util.Map>")); + assertThat(t.getGeneric(0).resolve(), equalTo((Class)Integer.class)); + assertThat(t.getGeneric(1).resolve(), equalTo((Class)List.class)); + assertThat(t.getGeneric(1).toString(), equalTo("java.util.List")); + assertThat(t.resolveGeneric(1, 0), equalTo((Class) String.class)); + } + + + private static AssertAssignbleMatcher assertAssignable(final ResolvableType type, + final ResolvableType... fromTypes) { + return new AssertAssignbleMatcher() { + @Override + public void equalTo(boolean... values) { + for (int i = 0; i < fromTypes.length; i++) { + assertThat(stringDesc(type) + " isAssignableFrom " + + stringDesc(fromTypes[i]), + type.isAssignableFrom(fromTypes[i]), + Matchers.equalTo(values[i])); + } + } + }; + } + + private static String stringDesc(ResolvableType type) { + if (type == ResolvableType.NONE) { + return "NONE"; + } + if (type.getType().getClass().equals(Class.class)) { + return type.toString(); + } + return type.getType() + ":" + type; + } + + + private static interface AssertAssignbleMatcher { + + void equalTo(boolean... values); + + } + + + static class ExtendsList extends ArrayList { + + } + + + static class ExtendsMap extends HashMap { + + } + + + static class Fields { + + public List classType; + + public T typeVariableType; + + public List parameterizedType; + + public List[] arrayClassType; + + public List[] genericArrayType; + + public List[][][] genericMultiArrayType; + + public List wildcardType; + + public List wildcardSuperType = new ArrayList(); + + public List charSequenceList; + + public List stringList; + + public List> stringListList; + + public List stringArrayList; + + public MultiValueMap stringIntegerMultiValueMap; + + public VariableNameSwitch stringIntegerMultiValueMapSwitched; + + public List listOfListOfUnknown; + + @SuppressWarnings("unused") + private List privateField; + + public Map, Map> nested; + + } + + + static class TypedFields extends Fields { + + } + + + static interface Methods { + + List charSequenceReturn(); + + void charSequenceParameter(List cs); + + R boundedTypeVaraibleResult(); + + void nested(Map, Map> p); + + void typedParameter(T p); + + T typedReturn(); + + } + + + static class AssignmentBase { + + public O o; + + public C c; + + public S s; + + public List listo; + + public List listc; + + public List lists; + + public List listxo; + + public List listxc; + + public List listxs; + + public List listso; + + public List listsc; + + public List listss; + + public O[] oarray; + + public C[] carray; + + public S[] sarray; + + public Collection collectionc; + + public Collection collectionxc; + + public Map> complexWildcard1; + + public MultiValueMap complexWildcard2; + + public Collection> complexWildcard3; + + public List> complexWildcard4; + + } + + + static class Assignment extends AssignmentBase { + + } + + + static interface TypedMethods extends Methods { + + } + + + static class Constructors { + + public Constructors(List p) { + } + + public Constructors(Map p) { + } + + } + + + static class TypedConstructors extends Constructors { + + public TypedConstructors(List p) { + super(p); + } + + public TypedConstructors(Map p) { + super(p); + } + + } + + + public interface MyInterfaceType { + + } + + + public class MySimpleInterfaceType implements MyInterfaceType { + + } + + + public class MyCollectionInterfaceType implements MyInterfaceType> { + + } + + + public abstract class MySuperclassType { + + } + + + public class MySimpleSuperclassType extends MySuperclassType { + + } + + + public class MyCollectionSuperclassType extends MySuperclassType> { + + } + + + static interface Wildcard extends List { + + } + + + static interface RawExtendsWildcard extends Wildcard { + + } + + + static interface VariableNameSwitch extends MultiValueMap { + + } + + + static interface ListOfGenericArray extends List[]> { + + } + + + static class EnclosedInParameterizedType { + + static class InnerRaw { + } + + class InnerTyped { + + public T field; + } + + } + + + static class TypedEnclosedInParameterizedType extends + EnclosedInParameterizedType { + + class TypedInnerTyped extends InnerTyped { + } + + } + +}