com.fitbur.mockito.internal.util.reflection.GenericMetadataSupport Maven / Gradle / Ivy
/*
* Copyright (c) 2007 Mockito contributors
* This program is made available under the terms of the MIT License.
*/
package com.fitbur.mockito.internal.util.reflection;
import com.fitbur.mockito.exceptions.base.MockitoException;
import com.fitbur.mockito.internal.util.Checks;
import java.lang.reflect.*;
import java.util.*;
/**
* This class can retrieve generic meta-data that the compiler stores on classes
* and accessible members.
*
*
* The main idea of this code is to create a Map that will help to resolve return types.
* In order to actually work with nested generics, this map will have to be passed along new instances
* as a type context.
*
*
*
* Hence :
*
* - A new instance representing the metadata is created using the {@link #inferFrom(Type)} method from a real
*
Class
or from a ParameterizedType
, other types are not yet supported.
*
* - Then from this metadata, we can extract meta-data for a generic return type of a method, using
* {@link #resolveGenericReturnType(Method)}.
*
*
*
*
* For now this code support the following kind of generic declarations :
*
* interface GenericsNest<K extends Comparable<K> & Cloneable> extends Map<K, Set<Number>> {
* Set<Number> remove(Object key); // override with fixed ParameterizedType
* List<? super Integer> returning_wildcard_with_class_lower_bound();
* List<? super K> returning_wildcard_with_typeVar_lower_bound();
* List<? extends K> returning_wildcard_with_typeVar_upper_bound();
* K returningK();
* <O extends K> List<O> paramType_with_type_params();
* <S, T extends S> T two_type_params();
* <O extends K> O typeVar_with_type_params();
* Number returningNonGeneric();
* }
*
*
* @see #inferFrom(Type)
* @see #resolveGenericReturnType(Method)
* @see com.fitbur.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs
*/
public abstract class GenericMetadataSupport {
// public static MockitoLogger logger = new ConsoleMockitoLogger();
/**
* Represents actual type variables resolved for current class.
*/
protected Map contextualActualTypeParameters = new HashMap();
protected void registerTypeVariablesOn(Type classType) {
if (!(classType instanceof ParameterizedType)) {
return;
}
ParameterizedType parameterizedType = (ParameterizedType) classType;
TypeVariable[] typeParameters = ((Class>) parameterizedType.getRawType()).getTypeParameters();
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
for (int i = 0; i < actualTypeArguments.length; i++) {
TypeVariable typeParameter = typeParameters[i];
Type actualTypeArgument = actualTypeArguments[i];
if (actualTypeArgument instanceof WildcardType) {
contextualActualTypeParameters.put(typeParameter, boundsOf((WildcardType) actualTypeArgument));
} else if (typeParameter != actualTypeArgument) {
contextualActualTypeParameters.put(typeParameter, actualTypeArgument);
}
// logger.log("For '" + parameterizedType + "' found type variable : { '" + typeParameter + "(" + System.identityHashCode(typeParameter) + ")" + "' : '" + actualTypeArgument + "(" + System.identityHashCode(typeParameter) + ")" + "' }");
}
}
protected void registerTypeParametersOn(TypeVariable[] typeParameters) {
for (TypeVariable type : typeParameters) {
registerTypeVariableIfNotPresent(type);
}
}
private void registerTypeVariableIfNotPresent(TypeVariable typeVariable) {
if (!contextualActualTypeParameters.containsKey(typeVariable)) {
contextualActualTypeParameters.put(typeVariable, boundsOf(typeVariable));
// logger.log("For '" + typeVariable.getGenericDeclaration() + "' found type variable : { '" + typeVariable + "(" + System.identityHashCode(typeVariable) + ")" + "' : '" + boundsOf(typeVariable) + "' }");
}
}
/**
* @param typeParameter The TypeVariable parameter
* @return A {@link BoundedType} for easy bound information, if first bound is a TypeVariable
* then retrieve BoundedType of this TypeVariable
*/
private BoundedType boundsOf(TypeVariable typeParameter) {
if (typeParameter.getBounds()[0] instanceof TypeVariable) {
return boundsOf((TypeVariable) typeParameter.getBounds()[0]);
}
return new TypeVarBoundedType(typeParameter);
}
/**
* @param wildCard The WildCard type
* @return A {@link BoundedType} for easy bound information, if first bound is a TypeVariable
* then retrieve BoundedType of this TypeVariable
*/
private BoundedType boundsOf(WildcardType wildCard) {
/*
* According to JLS(http://docs.oracle.com/javase/specs/jls/se5.0/html/typesValues.html#4.5.1):
* - Lower and upper can't coexist: (for instance, this is not allowed: extends List & super MyInterface>)
* - Multiple bounds are not supported (for instance, this is not allowed: extends List & MyInterface>)
*/
WildCardBoundedType wildCardBoundedType = new WildCardBoundedType(wildCard);
if (wildCardBoundedType.firstBound() instanceof TypeVariable) {
return boundsOf((TypeVariable) wildCardBoundedType.firstBound());
}
return wildCardBoundedType;
}
/**
* @return Raw type of the current instance.
*/
public abstract Class> rawType();
/**
* @return Returns extra interfaces if relevant, otherwise empty List.
*/
public List extraInterfaces() {
return Collections.emptyList();
}
/**
* @return Returns an array with the raw types of {@link #extraInterfaces()} if relevant.
*/
public Class>[] rawExtraInterfaces() {
return new Class[0];
}
/**
* @return Returns true if metadata knows about extra-interfaces {@link #extraInterfaces()} if relevant.
*/
public boolean hasRawExtraInterfaces() {
return rawExtraInterfaces().length > 0;
}
/**
* @return Actual type arguments matching the type variables of the raw type represented by this {@link GenericMetadataSupport} instance.
*/
public Map actualTypeArguments() {
TypeVariable[] typeParameters = rawType().getTypeParameters();
LinkedHashMap actualTypeArguments = new LinkedHashMap();
for (TypeVariable typeParameter : typeParameters) {
Type actualType = getActualTypeArgumentFor(typeParameter);
actualTypeArguments.put(typeParameter, actualType);
// logger.log("For '" + rawType().getCanonicalName() + "' returning explicit TypeVariable : { '" + typeParameter + "(" + System.identityHashCode(typeParameter) + ")" + "' : '" + actualType +"' }");
}
return actualTypeArguments;
}
protected Type getActualTypeArgumentFor(TypeVariable typeParameter) {
Type type = this.contextualActualTypeParameters.get(typeParameter);
if (type instanceof TypeVariable) {
TypeVariable typeVariable = (TypeVariable) type;
return getActualTypeArgumentFor(typeVariable);
}
return type;
}
/**
* Resolve current method generic return type to a {@link GenericMetadataSupport}.
*
* @param method Method to resolve the return type.
* @return {@link GenericMetadataSupport} representing this generic return type.
*/
public GenericMetadataSupport resolveGenericReturnType(Method method) {
Type genericReturnType = method.getGenericReturnType();
// logger.log("Method '" + method.toGenericString() + "' has return type : " + genericReturnType.getClass().getInterfaces()[0].getSimpleName() + " : " + genericReturnType);
int arity = 0;
while(genericReturnType instanceof GenericArrayType) {
arity++;
genericReturnType = ((GenericArrayType) genericReturnType).getGenericComponentType();
}
GenericMetadataSupport genericMetadataSupport = resolveGenericType(genericReturnType, method);
if (arity == 0) {
return genericMetadataSupport;
} else {
return new GenericArrayReturnType(genericMetadataSupport, arity);
}
}
private GenericMetadataSupport resolveGenericType(Type type, Method method) {
if (type instanceof Class) {
return new NotGenericReturnTypeSupport(type);
}
if (type instanceof ParameterizedType) {
return new ParameterizedReturnType(this, method.getTypeParameters(), (ParameterizedType) type);
}
if (type instanceof TypeVariable) {
return new TypeVariableReturnType(this, method.getTypeParameters(), (TypeVariable) type);
}
throw new MockitoException("Ouch, it shouldn't happen, type '" + type.getClass().getCanonicalName() + "' on method : '" + method.toGenericString() + "' is not supported : " + type);
}
/**
* Create an new instance of {@link GenericMetadataSupport} inferred from a {@link Type}.
*
*
* At the moment type
can only be a {@link Class} or a {@link ParameterizedType}, otherwise
* it'll throw a {@link MockitoException}.
*
*
* @param type The class from which the {@link GenericMetadataSupport} should be built.
* @return The new {@link GenericMetadataSupport}.
* @throws MockitoException Raised if type is not a {@link Class} or a {@link ParameterizedType}.
*/
public static GenericMetadataSupport inferFrom(Type type) {
Checks.checkNotNull(type, "type");
if (type instanceof Class) {
return new FromClassGenericMetadataSupport((Class>) type);
}
if (type instanceof ParameterizedType) {
return new FromParameterizedTypeGenericMetadataSupport((ParameterizedType) type);
}
throw new MockitoException("Type meta-data for this Type (" + type.getClass().getCanonicalName() + ") is not supported : " + type);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//// Below are specializations of GenericMetadataSupport that could handle retrieval of possible Types
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Generic metadata implementation for {@link Class}.
*
* Offer support to retrieve generic metadata on a {@link Class} by reading type parameters and type variables on
* the class and its ancestors and interfaces.
*/
private static class FromClassGenericMetadataSupport extends GenericMetadataSupport {
private final Class> clazz;
public FromClassGenericMetadataSupport(Class> clazz) {
this.clazz = clazz;
for (Class> currentExploredClass = clazz;
currentExploredClass != null && currentExploredClass != Object.class;
currentExploredClass = superClassOf(currentExploredClass)) {
readActualTypeParametersOnDeclaringClass(currentExploredClass);
}
}
private Class superClassOf(Class> currentExploredClass) {
Type genericSuperclass = currentExploredClass.getGenericSuperclass();
if (genericSuperclass instanceof ParameterizedType) {
Type rawType = ((ParameterizedType) genericSuperclass).getRawType();
return (Class>) rawType;
}
return (Class>) genericSuperclass;
}
private void readActualTypeParametersOnDeclaringClass(Class> clazz) {
registerTypeParametersOn(clazz.getTypeParameters());
registerTypeVariablesOn(clazz.getGenericSuperclass());
for (Type genericInterface : clazz.getGenericInterfaces()) {
registerTypeVariablesOn(genericInterface);
}
}
@Override
public Class> rawType() {
return clazz;
}
}
/**
* Generic metadata implementation for "standalone" {@link ParameterizedType}.
*
* Offer support to retrieve generic metadata on a {@link ParameterizedType} by reading type variables of
* the related raw type and declared type variable of this parameterized type.
*
* This class is not designed to work on ParameterizedType returned by {@link Method#getGenericReturnType()}, as
* the ParameterizedType instance return in these cases could have Type Variables that refer to type declaration(s).
* That's what meant the "standalone" word at the beginning of the Javadoc.
* Instead use {@link ParameterizedReturnType}.
*/
private static class FromParameterizedTypeGenericMetadataSupport extends GenericMetadataSupport {
private final ParameterizedType parameterizedType;
public FromParameterizedTypeGenericMetadataSupport(ParameterizedType parameterizedType) {
this.parameterizedType = parameterizedType;
readActualTypeParameters();
}
private void readActualTypeParameters() {
registerTypeVariablesOn(parameterizedType.getRawType());
registerTypeVariablesOn(parameterizedType);
}
@Override
public Class> rawType() {
return (Class>) parameterizedType.getRawType();
}
}
/**
* Generic metadata specific to {@link ParameterizedType} returned via {@link Method#getGenericReturnType()}.
*/
private static class ParameterizedReturnType extends GenericMetadataSupport {
private final ParameterizedType parameterizedType;
private final TypeVariable[] typeParameters;
public ParameterizedReturnType(GenericMetadataSupport source, TypeVariable[] typeParameters, ParameterizedType parameterizedType) {
this.parameterizedType = parameterizedType;
this.typeParameters = typeParameters;
this.contextualActualTypeParameters = source.contextualActualTypeParameters;
readTypeParameters();
readTypeVariables();
}
private void readTypeParameters() {
registerTypeParametersOn(typeParameters);
}
private void readTypeVariables() {
registerTypeVariablesOn(parameterizedType);
}
@Override
public Class> rawType() {
return (Class>) parameterizedType.getRawType();
}
}
/**
* Generic metadata for {@link TypeVariable} returned via {@link Method#getGenericReturnType()}.
*/
private static class TypeVariableReturnType extends GenericMetadataSupport {
private final TypeVariable typeVariable;
private final TypeVariable[] typeParameters;
private Class> rawType;
public TypeVariableReturnType(GenericMetadataSupport source, TypeVariable[] typeParameters, TypeVariable typeVariable) {
this.typeParameters = typeParameters;
this.typeVariable = typeVariable;
this.contextualActualTypeParameters = source.contextualActualTypeParameters;
readTypeParameters();
readTypeVariables();
}
private void readTypeParameters() {
registerTypeParametersOn(typeParameters);
}
private void readTypeVariables() {
for (Type type : typeVariable.getBounds()) {
registerTypeVariablesOn(type);
}
registerTypeParametersOn(new TypeVariable[] { typeVariable });
registerTypeVariablesOn(getActualTypeArgumentFor(typeVariable));
}
@Override
public Class> rawType() {
if (rawType == null) {
rawType = extractRawTypeOf(typeVariable);
}
return rawType;
}
private Class> extractRawTypeOf(Type type) {
if (type instanceof Class) {
return (Class>) type;
}
if (type instanceof ParameterizedType) {
return (Class>) ((ParameterizedType) type).getRawType();
}
if (type instanceof BoundedType) {
return extractRawTypeOf(((BoundedType) type).firstBound());
}
if (type instanceof TypeVariable) {
/*
* If type is a TypeVariable, then it is needed to gather data elsewhere. Usually TypeVariables are declared
* on the class definition, such as such as List.
*/
return extractRawTypeOf(contextualActualTypeParameters.get(type));
}
throw new MockitoException("Raw extraction not supported for : '" + type + "'");
}
@Override
public List extraInterfaces() {
Type type = extractActualBoundedTypeOf(typeVariable);
if (type instanceof BoundedType) {
return Arrays.asList(((BoundedType) type).interfaceBounds());
}
if (type instanceof ParameterizedType) {
return Collections.singletonList(type);
}
if (type instanceof Class) {
return Collections.emptyList();
}
throw new MockitoException("Cannot extract extra-interfaces from '" + typeVariable + "' : '" + type + "'");
}
/**
* @return Returns an array with the extracted raw types of {@link #extraInterfaces()}.
* @see #extractRawTypeOf(java.lang.reflect.Type)
*/
public Class>[] rawExtraInterfaces() {
List extraInterfaces = extraInterfaces();
List> rawExtraInterfaces = new ArrayList>();
for (Type extraInterface : extraInterfaces) {
Class> rawInterface = extractRawTypeOf(extraInterface);
// avoid interface collision with actual raw type (with typevariables, resolution ca be quite aggressive)
if(!rawType().equals(rawInterface)) {
rawExtraInterfaces.add(rawInterface);
}
}
return rawExtraInterfaces.toArray(new Class[rawExtraInterfaces.size()]);
}
private Type extractActualBoundedTypeOf(Type type) {
if (type instanceof TypeVariable) {
/*
If type is a TypeVariable, then it is needed to gather data elsewhere. Usually TypeVariables are declared
on the class definition, such as such as List.
*/
return extractActualBoundedTypeOf(contextualActualTypeParameters.get(type));
}
if (type instanceof BoundedType) {
Type actualFirstBound = extractActualBoundedTypeOf(((BoundedType) type).firstBound());
if (!(actualFirstBound instanceof BoundedType)) {
return type; // avoid going one step further, ie avoid : O(TypeVar) -> K(TypeVar) -> Some ParamType
}
return actualFirstBound;
}
return type; // irrelevant, we don't manage other types as they are not bounded.
}
}
private static class GenericArrayReturnType extends GenericMetadataSupport {
private final GenericMetadataSupport genericArrayType;
private final int arity;
public GenericArrayReturnType(GenericMetadataSupport genericArrayType, int arity) {
this.genericArrayType = genericArrayType;
this.arity = arity;
}
@Override
public Class> rawType() {
Class> rawComponentType = genericArrayType.rawType();
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < arity; i++) {
stringBuilder.append("[");
}
try {
return Class.forName(stringBuilder.append("L").append(rawComponentType.getName()).append(";").toString(), false, rawComponentType.getClassLoader());
} catch (ClassNotFoundException e) {
throw new IllegalStateException("This was not supposed to happend", e);
}
}
}
/**
* Non-Generic metadata for {@link Class} returned via {@link Method#getGenericReturnType()}.
*/
private static class NotGenericReturnTypeSupport extends GenericMetadataSupport {
private final Class> returnType;
public NotGenericReturnTypeSupport(Type genericReturnType) {
returnType = (Class>) genericReturnType;
}
@Override
public Class> rawType() {
return returnType;
}
}
/**
* Type representing bounds of a type
*
* @see TypeVarBoundedType
* @see http://docs.oracle.com/javase/specs/jls/se5.0/html/typesValues.html#4.4
* @see WildCardBoundedType
* @see http://docs.oracle.com/javase/specs/jls/se5.0/html/typesValues.html#4.5.1
*/
public interface BoundedType extends Type {
Type firstBound();
Type[] interfaceBounds();
}
/**
* Type representing bounds of a type variable, allows to keep all bounds information.
*
* It uses the first bound in the array, as this array is never null and always contains at least
* one element (Object is always here if no bounds are declared).
*
* If upper bounds are declared with SomeClass and additional interfaces, then firstBound will be SomeClass and
* interfacesBound will be an array of the additional interfaces.
*
* i.e. SomeClass
.
*
* interface UpperBoundedTypeWithClass & Cloneable> {
* E get();
* }
* // will return Comparable type
*
*
*
* @see http://docs.oracle.com/javase/specs/jls/se5.0/html/typesValues.html#4.4
*/
public static class TypeVarBoundedType implements BoundedType {
private final TypeVariable typeVariable;
public TypeVarBoundedType(TypeVariable typeVariable) {
this.typeVariable = typeVariable;
}
/**
* @return either a class or an interface (parameterized or not), if no bounds declared Object is returned.
*/
public Type firstBound() {
return typeVariable.getBounds()[0]; //
}
/**
* On a Type Variable (typeVar extends C_0 & I_1 & I_2 & etc), will return an array
* containing I_1 and I_2.
*
* @return other bounds for this type, these bounds can only be only interfaces as the JLS says,
* empty array if no other bound declared.
*/
public Type[] interfaceBounds() {
Type[] interfaceBounds = new Type[typeVariable.getBounds().length - 1];
System.arraycopy(typeVariable.getBounds(), 1, interfaceBounds, 0, typeVariable.getBounds().length - 1);
return interfaceBounds;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
return typeVariable.equals(((TypeVarBoundedType) o).typeVariable);
}
@Override
public int hashCode() {
return typeVariable.hashCode();
}
@Override
public String toString() {
return "{firstBound=" + firstBound() + ", interfaceBounds=" + Arrays.deepToString(interfaceBounds()) + '}';
}
public TypeVariable typeVariable() {
return typeVariable;
}
}
/**
* Type representing bounds of a wildcard, allows to keep all bounds information.
*
* The JLS says that lower bound and upper bound are mutually exclusive, and that multiple bounds
* are not allowed.
*
* @see http://docs.oracle.com/javase/specs/jls/se5.0/html/typesValues.html#4.4
*/
public static class WildCardBoundedType implements BoundedType {
private final WildcardType wildcard;
public WildCardBoundedType(WildcardType wildcard) {
this.wildcard = wildcard;
}
/**
* @return The first bound, either a type or a reference to a TypeVariable
*/
public Type firstBound() {
Type[] lowerBounds = wildcard.getLowerBounds();
Type[] upperBounds = wildcard.getUpperBounds();
return lowerBounds.length != 0 ? lowerBounds[0] : upperBounds[0];
}
/**
* @return An empty array as, wildcard don't support multiple bounds.
*/
public Type[] interfaceBounds() {
return new Type[0];
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
return wildcard.equals(((TypeVarBoundedType) o).typeVariable);
}
@Override
public int hashCode() {
return wildcard.hashCode();
}
@Override
public String toString() {
return "{firstBound=" + firstBound() + ", interfaceBounds=[]}";
}
public WildcardType wildCard() {
return wildcard;
}
}
}