| /* |
| * 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; |
| } |
| } |