blob: c1f4126c961276a6e9a7c9a13faeddd55d2998cd [file] [log] [blame]
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.gwt.uibinder.rebind;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.when;
import com.google.gwt.core.ext.typeinfo.HasAnnotations;
import com.google.gwt.core.ext.typeinfo.HasTypeParameters;
import com.google.gwt.core.ext.typeinfo.JAbstractMethod;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JConstructor;
import com.google.gwt.core.ext.typeinfo.JField;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.GenericDeclaration;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* Creates stub adapters for GWT reflection using EasyMock.
* This takes in a real java reflection class and returns a JClassType which
* forwards all its method calls to the equivalent java reflection methods.
* <p>
* Most reflections are done lazily (only when the method is actually called),
* specially those which potentially require mocking a new class / method /
* field / parameter / constructor / etc.
* <p>
* The support for the typeinfo API is still incomplete, and in fact will
* always be in some way since Java doesn't support all the reflections that
* GWT's typeinfo does.
* <p>
* TODO: With a bit of generalization, this class should make its way
* to core/src/com/google/gwt/junit
*
* To make it public we need to...
* <ol>
* <li>implement the missing parts and TODOs (e.g. generics)
* <li>add tests to it (even though it's a testing utility, it does need
* tests, specially for cases like inner and anonymous classes, generic
* parameters, etc.)
* <li>decide what to do with the parts of JType reflection that java doesn't
* have an equivalent for - e.g. parameter names. This may involve making a
* slightly more complex API to inject those values.
* </ol>
*/
public class JClassTypeAdapter {
private final Map<Class<?>, JClassType> adaptedClasses =
new HashMap<Class<?>, JClassType>();
private final List<Object> allMocks = new ArrayList<Object>();
/**
* Creates a mock GWT class type for the given Java class.
*
* @param clazz the java class
* @return the gwt class
*/
public JClassType adaptJavaClass(final Class<?> clazz) {
if (clazz.isPrimitive()) {
throw new RuntimeException(
"Only classes can be passed to adaptJavaClass");
}
// First try the cache (also avoids infinite recursion if a type references
// itself).
JClassType type = adaptedClasses.get(clazz);
if (type != null) {
return type;
}
// Create and put in the cache
type = createMock(JClassType.class);
final JClassType finalType = type;
adaptedClasses.put(clazz, type);
// Adds behaviour for annotations and generics
addAnnotationBehaviour(clazz, type);
// TODO(rdamazio): Add generics behaviour
// Add behaviour for getting methods
when(type.getMethods()).thenAnswer(new Answer<JMethod[]>() {
@Override
public JMethod[] answer(InvocationOnMock invocation) throws Throwable {
Method[] realMethods = clazz.getDeclaredMethods();
JMethod[] methods = new JMethod[realMethods.length];
for (int i = 0; i < realMethods.length; i++) {
methods[i] = adaptMethod(realMethods[i], finalType);
}
return methods;
}
});
// Add behaviour for getting constructors
when(type.getConstructors()).thenAnswer(new Answer<JConstructor[]>() {
@Override
public JConstructor[] answer(InvocationOnMock invocation) throws Throwable {
Constructor<?>[] realConstructors = clazz.getDeclaredConstructors();
JConstructor[] constructors = new JConstructor[realConstructors.length];
for (int i = 0; i < realConstructors.length; i++) {
constructors[i] = adaptConstructor(realConstructors[i], finalType);
}
return constructors;
}
});
// Add behaviour for getting fields
when(type.getFields()).thenAnswer(new Answer<JField[]>() {
@Override
public JField[] answer(InvocationOnMock invocation) throws Throwable {
Field[] realFields = clazz.getDeclaredFields();
JField[] fields = new JField[realFields.length];
for (int i = 0; i < realFields.length; i++) {
fields[i] = adaptField(realFields[i], finalType);
}
return fields;
}
});
// Add behaviour for getting names
when(type.getName()).thenReturn(clazz.getName());
when(type.getQualifiedSourceName()).thenReturn(
clazz.getCanonicalName());
when(type.getSimpleSourceName()).thenReturn(clazz.getSimpleName());
// Add modifier behaviour
int modifiers = clazz.getModifiers();
when(type.isAbstract()).thenReturn(Modifier.isAbstract(modifiers));
when(type.isFinal()).thenReturn(Modifier.isFinal(modifiers));
when(type.isPublic()).thenReturn(Modifier.isPublic(modifiers));
when(type.isProtected()).thenReturn(Modifier.isProtected(modifiers));
when(type.isPrivate()).thenReturn(Modifier.isPrivate(modifiers));
// Add conversion behaviours
when(type.isArray()).thenReturn(null);
when(type.isEnum()).thenReturn(null);
when(type.isPrimitive()).thenReturn(null);
when(type.isClassOrInterface()).thenReturn(type);
if (clazz.isInterface()) {
when(type.isClass()).thenReturn(null);
when(type.isInterface()).thenReturn(type);
} else {
when(type.isClass()).thenReturn(type);
when(type.isInterface()).thenReturn(null);
}
when(type.getEnclosingType()).thenAnswer(new Answer<JClassType>() {
@Override
public JClassType answer(InvocationOnMock invocation) throws Throwable {
Class<?> enclosingClass = clazz.getEnclosingClass();
if (enclosingClass == null) {
return null;
}
return adaptJavaClass(enclosingClass);
}
});
when(type.getSuperclass()).thenAnswer(new Answer<JClassType>() {
@Override
public JClassType answer(InvocationOnMock invocation) throws Throwable {
Class<?> superclass = clazz.getSuperclass();
if (superclass == null) {
return null;
}
return adaptJavaClass(superclass);
}
});
when(type.getImplementedInterfaces()).thenAnswer(new Answer<JClassType[]>() {
@Override
public JClassType[] answer(InvocationOnMock invocation) throws Throwable {
Class<?>[] interfaces = clazz.getInterfaces();
if ((interfaces == null) || (interfaces.length == 0)) {
return null;
}
JClassType[] adaptedInterfaces = new JClassType[interfaces.length];
for (int i = 0; i < interfaces.length; i++) {
adaptedInterfaces[i] = adaptJavaClass(interfaces[i]);
}
return adaptedInterfaces;
}
});
when(type.getFlattenedSupertypeHierarchy()).thenAnswer(new Answer<Set<JClassType>>() {
@Override
public Set<JClassType> answer(InvocationOnMock invocation) throws Throwable {
return flatten(clazz);
}
private Set<JClassType> flatten(Class<?> clazz) {
Set<JClassType> flattened = new LinkedHashSet<JClassType>();
flattened.add(adaptJavaClass(clazz));
for (Class<?> intf : clazz.getInterfaces()) {
flattened.addAll(flatten(intf));
}
Class<?> superClass = clazz.getSuperclass();
if (superClass != null) {
flattened.addAll(flatten(superClass));
}
return flattened;
}
});
when(type.getInheritableMethods()).thenAnswer(new Answer<JMethod[]>() {
@Override
public JMethod[] answer(InvocationOnMock invocation) throws Throwable {
Map<String, Method> methodsBySignature = new TreeMap<String, Method>();
getInheritableMethodsOnSuperinterfacesAndMaybeThisInterface(clazz, methodsBySignature);
if (!clazz.isInterface()) {
getInheritableMethodsOnSuperclassesAndThisClass(clazz, methodsBySignature);
}
int size = methodsBySignature.size();
if (size == 0) {
return new JMethod[0];
} else {
Iterator<Method> leafMethods = methodsBySignature.values().iterator();
JMethod[] jMethods = new JMethod[size];
for (int i = 0; i < size; i++) {
Method method = leafMethods.next();
jMethods[i] = adaptMethod(method, adaptJavaClass(method.getDeclaringClass()));
}
return jMethods;
}
}
protected void getInheritableMethodsOnSuperinterfacesAndMaybeThisInterface(
Class<?> clazz,
Map<String, Method> methodsBySignature) {
// Recurse first so that more derived methods will clobber less derived
// methods.
Class<?>[] superIntfs = clazz.getInterfaces();
for (Class<?> superIntf : superIntfs) {
getInheritableMethodsOnSuperinterfacesAndMaybeThisInterface(
superIntf,
methodsBySignature);
}
Method[] declaredMethods = clazz.getMethods();
for (Method method : declaredMethods) {
String sig = computeInternalSignature(method);
Method existing = methodsBySignature.get(sig);
if (existing != null) {
Class<?> existingType = existing.getDeclaringClass();
Class<?> thisType = method.getDeclaringClass();
if (thisType.isAssignableFrom(existingType)) {
// The existing method is in a more-derived type, so don't replace it.
continue;
}
}
methodsBySignature.put(sig, method);
}
}
protected void getInheritableMethodsOnSuperclassesAndThisClass(
Class<?> clazz,
Map<String, Method> methodsBySignature) {
// Recurse first so that more derived methods will clobber less derived
// methods.
Class<?> superClass = clazz.getSuperclass();
if (superClass != null) {
getInheritableMethodsOnSuperclassesAndThisClass(
superClass,
methodsBySignature);
}
Method[] declaredMethods = clazz.getMethods();
for (Method method : declaredMethods) {
// Ensure that this method is inheritable.
if (Modifier.isPrivate(method.getModifiers())
|| Modifier.isStatic(method.getModifiers())) {
// We cannot inherit this method, so skip it.
continue;
}
// We can override this method, so record it.
String sig = computeInternalSignature(method);
methodsBySignature.put(sig, method);
}
}
private String computeInternalSignature(Method method) {
StringBuilder sb = new StringBuilder();
sb.setLength(0);
sb.append(method.getName());
Class<?>[] params = method.getParameterTypes();
for (Class<?> param : params) {
sb.append("/");
sb.append(param.getName());
}
return sb.toString();
}
});
// TODO(rdamazio): Mock out other methods as needed
// TODO(rdamazio): Figure out what to do with reflections that GWT allows
// but Java doesn't
return type;
}
/**
* Creates a mock GWT field for the given Java field.
*
* @param realField the java field
* @param enclosingType the GWT enclosing type
* @return the GWT field
*/
public JField adaptField(final Field realField, JClassType enclosingType) {
JField field = createMock(JField.class);
addAnnotationBehaviour(realField, field);
when(field.getType()).thenAnswer(new Answer<JType>() {
@Override
public JType answer(InvocationOnMock invocation) throws Throwable {
return adaptType(realField.getType());
}
});
when(field.getEnclosingType()).thenReturn(enclosingType);
when(field.getName()).thenReturn(realField.getName());
return field;
}
/**
* Creates a mock GWT constructor for the given java constructor.
*
* @param realConstructor the java constructor
* @param enclosingType the type to which the constructor belongs
* @return the GWT constructor
*/
private JConstructor adaptConstructor(final Constructor<?> realConstructor,
JClassType enclosingType) {
final JConstructor constructor = createMock(JConstructor.class);
addCommonAbstractMethodBehaviour(realConstructor, constructor,
enclosingType);
addAnnotationBehaviour(realConstructor, constructor);
// Parameters
when(constructor.getParameters()).thenAnswer(
new Answer<JParameter[]>() {
@Override
public JParameter[] answer(InvocationOnMock invocation) throws Throwable {
return adaptParameters(realConstructor.getParameterTypes(),
realConstructor.getParameterAnnotations(), constructor);
}
});
// Thrown exceptions
when(constructor.getThrows()).thenAnswer(
new Answer<JClassType[]>() {
@Override
public JClassType[] answer(InvocationOnMock invocation) throws Throwable {
Class<?>[] realThrows = realConstructor.getExceptionTypes();
JClassType[] gwtThrows = new JClassType[realThrows.length];
for (int i = 0; i < realThrows.length; i++) {
gwtThrows[i] = (JClassType) adaptType(realThrows[i]);
}
return gwtThrows;
}
});
return constructor;
}
/**
* Creates a mock GWT method for the given java method.
*
* @param realMethod the java method
* @param enclosingType the type to which the method belongs
* @return the GWT method
*/
private JMethod adaptMethod(final Method realMethod,
JClassType enclosingType) {
// TODO(rdamazio): ensure a single instance per method per class
final JMethod method = createMock(JMethod.class);
addCommonAbstractMethodBehaviour(realMethod, method, enclosingType);
addAnnotationBehaviour(realMethod, method);
addGenericsBehaviour(realMethod, method);
when(method.isStatic()).thenReturn(
Modifier.isStatic(realMethod.getModifiers()));
// Return type
when(method.getReturnType()).thenAnswer(new Answer<JType>() {
@Override
public JType answer(InvocationOnMock invocation) throws Throwable {
return adaptType(realMethod.getReturnType());
}
});
// Parameters
when(method.getParameters()).thenAnswer(new Answer<JParameter[]>() {
@Override
public JParameter[] answer(InvocationOnMock invocation) throws Throwable {
return adaptParameters(realMethod.getParameterTypes(),
realMethod.getParameterAnnotations(), method);
}
});
// Thrown exceptions
when(method.getThrows()).thenAnswer(new Answer<JClassType[]>() {
@Override
public JClassType[] answer(InvocationOnMock invocation) throws Throwable {
Class<?>[] realThrows = realMethod.getExceptionTypes();
JClassType[] gwtThrows = new JClassType[realThrows.length];
for (int i = 0; i < realThrows.length; i++) {
gwtThrows[i] = (JClassType) adaptType(realThrows[i]);
}
return gwtThrows;
}
});
return method;
}
/**
* Creates an array of mock GWT parameters for the given array of java
* parameters.
*
* @param parameterTypes the types of the parameters
* @param parameterAnnotations the list of annotations for each parameter
* @param method the method or constructor to which the parameters belong
* @return an array of GWT parameters
*/
@SuppressWarnings("unchecked")
protected JParameter[] adaptParameters(Class<?>[] parameterTypes,
Annotation[][] parameterAnnotations, JAbstractMethod method) {
JParameter[] parameters = new JParameter[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
final Class<?> realParameterType = parameterTypes[i];
JParameter parameter = createMock(JParameter.class);
parameters[i] = parameter;
// TODO(rdamazio): getName() has no plain java equivalent.
// Perhaps compiling with -g:vars ?
when(parameter.getEnclosingMethod()).thenReturn(method);
when(parameter.getType()).thenAnswer(new Answer<JType>() {
@Override
public JType answer(InvocationOnMock invocation) throws Throwable {
return adaptType(realParameterType);
}
});
// Add annotation behaviour
final Annotation[] annotations = parameterAnnotations[i];
when(parameter.isAnnotationPresent(any(Class.class))).thenAnswer(
new Answer<Boolean>() {
@Override
public Boolean answer(InvocationOnMock invocation) throws Throwable {
Class<? extends Annotation> annotationClass =
(Class<? extends Annotation>)
invocation.getArguments()[0];
for (Annotation annotation : annotations) {
if (annotation.equals(annotationClass)) {
return true;
}
}
return false;
}
});
when(parameter.getAnnotation(any(Class.class))).thenAnswer(
new Answer<Annotation>() {
@Override
public Annotation answer(InvocationOnMock invocation) throws Throwable {
Class<? extends Annotation> annotationClass =
(Class<? extends Annotation>)
invocation.getArguments()[0];
for (Annotation annotation : annotations) {
if (annotation.equals(annotationClass)) {
return annotation;
}
}
return null;
}
});
}
return parameters;
}
/**
* Creates a GWT mock type for the given java type.
* The type can be a class or a primitive type.
*
* @param type the java type
* @return the GWT type
*/
private JType adaptType(Class<?> type) {
if (!type.isPrimitive()) {
return adaptJavaClass(type);
} else {
return adaptPrimitiveType(type);
}
}
/**
* Returns the GWT primitive type for the given java primitive type.
*
* @param type the java primitive type
* @return the GWT primitive equivalent
*/
private JType adaptPrimitiveType(Class<?> type) {
if (boolean.class.equals(type)) { return JPrimitiveType.BOOLEAN; }
if (int.class.equals(type)) { return JPrimitiveType.INT; }
if (char.class.equals(type)) { return JPrimitiveType.CHAR; }
if (byte.class.equals(type)) { return JPrimitiveType.BYTE; }
if (long.class.equals(type)) { return JPrimitiveType.LONG; }
if (short.class.equals(type)) { return JPrimitiveType.SHORT; }
if (float.class.equals(type)) { return JPrimitiveType.FLOAT; }
if (double.class.equals(type)) { return JPrimitiveType.DOUBLE; }
if (void.class.equals(type)) { return JPrimitiveType.VOID; }
throw new IllegalArgumentException(
"Invalid primitive type: " + type.getName());
}
/**
* Adds expectations common to all method types (methods and constructors).
*
* @param realMember the java method
* @param member the mock GWT method
* @param enclosingType the type to which the method belongs
*/
private void addCommonAbstractMethodBehaviour(Member realMember,
JAbstractMethod member, JClassType enclosingType) {
// Attributes
int modifiers = realMember.getModifiers();
when(member.isPublic()).thenReturn(Modifier.isPublic(modifiers));
when(member.isProtected()).thenReturn(Modifier.isProtected(modifiers));
when(member.isPrivate()).thenReturn(Modifier.isPrivate(modifiers));
when(member.getName()).thenReturn(realMember.getName());
when(member.getEnclosingType()).thenReturn(enclosingType);
}
/**
* Adds expectations for getting annotations from elements (methods, classes,
* parameters, etc.).
*
* @param realElement the java element which contains annotations
* @param element the mock GWT element which contains annotations
*/
@SuppressWarnings("unchecked")
private void addAnnotationBehaviour(final AnnotatedElement realElement,
final HasAnnotations element) {
when(element.isAnnotationPresent(any(Class.class))).thenAnswer(
new Answer<Boolean>() {
@Override
public Boolean answer(InvocationOnMock invocation) throws Throwable {
Class<? extends Annotation> annotationClass =
(Class<? extends Annotation>) invocation.getArguments()[0];
return realElement.isAnnotationPresent(annotationClass);
}
});
when(element.getAnnotation(any(Class.class))).thenAnswer(
new Answer<Annotation>() {
@Override
public Annotation answer(InvocationOnMock invocation) throws Throwable {
Class<? extends Annotation> annotationClass =
(Class<? extends Annotation>) invocation.getArguments()[0];
return realElement.getAnnotation(annotationClass);
}
});
}
/**
* Adds expectations for getting generics types.
*
* @param realGeneric the java generic declaration
* @param generic the mock GWT generic declaration
*/
private void addGenericsBehaviour(final GenericDeclaration realGeneric,
final HasTypeParameters generic) {
// TODO(rdamazio): Implement when necessary
}
/**
* Creates a mock of the given class and adds it to the {@link #allMocks}
* member list.
*
* @param <T> the type of the mock
* @param clazz the class of the mock
* @return the mock
*/
private <T> T createMock(Class<T> clazz) {
T mock = Mockito.mock(clazz);
allMocks.add(mock);
return mock;
}
}