1/*
2 * Copyright 2010 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.google.android.testing.mocking;
17
18import javassist.CannotCompileException;
19import javassist.ClassClassPath;
20import javassist.ClassPool;
21import javassist.CtClass;
22import javassist.CtConstructor;
23import javassist.CtField;
24import javassist.CtMethod;
25import javassist.CtNewConstructor;
26import javassist.NotFoundException;
27
28import java.io.IOException;
29import java.lang.reflect.Constructor;
30import java.lang.reflect.Method;
31import java.lang.reflect.Modifier;
32import java.util.ArrayList;
33import java.util.Arrays;
34import java.util.HashMap;
35import java.util.List;
36import java.util.Map;
37
38
39/**
40 * AndroidMockGenerator creates the subclass and interface required for mocking
41 * a given Class.
42 *
43 * The only public method of AndroidMockGenerator is createMocksForClass. See
44 * the javadocs for this method for more information about AndroidMockGenerator.
45 *
46 * @author swoodward@google.com (Stephen Woodward)
47 */
48class AndroidMockGenerator {
49  public AndroidMockGenerator() {
50    ClassPool.doPruning = false;
51    ClassPool.getDefault().insertClassPath(new ClassClassPath(MockObject.class));
52  }
53
54  /**
55   * Creates a List of javassist.CtClass objects representing all of the
56   * interfaces and subclasses required to meet the Mocking requests of the
57   * Class specified by {@code clazz}.
58   *
59   * A test class can request that a Class be prepared for mocking by using the
60   * {@link UsesMocks} annotation at either the Class or Method level. All
61   * classes specified by these annotations will have exactly two CtClass
62   * objects created, one for a generated interface, and one for a generated
63   * subclass. The interface and subclass both define the same methods which
64   * comprise all of the mockable methods of the provided class. At present, for
65   * a method to be mockable, it must be non-final and non-static, although this
66   * may expand in the future.
67   *
68   * The class itself must be mockable, otherwise this method will ignore the
69   * requested mock and print a warning. At present, a class is mockable if it
70   * is a non-final publicly-instantiable Java class that is assignable from the
71   * java.lang.Object class. See the javadocs for
72   * {@link java.lang.Class#isAssignableFrom(Class)} for more information about
73   * what "is assignable from the Object class" means. As a non-exhaustive
74   * example, if a given Class represents an Enum, Annotation, Primitive or
75   * Array, then it is not assignable from Object. Interfaces are also ignored
76   * since these need no modifications in order to be mocked.
77   *
78   * @param clazz the Class object to have all of its UsesMocks annotations
79   *        processed and the corresponding Mock Classes created.
80   * @return a List of CtClass objects representing the Classes and Interfaces
81   *         required for mocking the classes requested by {@code clazz}
82   * @throws ClassNotFoundException
83   * @throws CannotCompileException
84   * @throws IOException
85   */
86  public List<GeneratedClassFile> createMocksForClass(Class<?> clazz)
87      throws ClassNotFoundException, IOException, CannotCompileException {
88    return this.createMocksForClass(clazz, SdkVersion.UNKNOWN);
89  }
90
91  public List<GeneratedClassFile> createMocksForClass(Class<?> clazz, SdkVersion sdkVersion)
92      throws ClassNotFoundException, IOException, CannotCompileException {
93    if (!classIsSupportedType(clazz)) {
94      reportReasonForUnsupportedType(clazz);
95      return Arrays.asList(new GeneratedClassFile[0]);
96    }
97    CtClass newInterfaceCtClass = generateInterface(clazz, sdkVersion);
98    GeneratedClassFile newInterface = new GeneratedClassFile(newInterfaceCtClass.getName(),
99        newInterfaceCtClass.toBytecode());
100    CtClass mockDelegateCtClass = generateSubClass(clazz, newInterfaceCtClass, sdkVersion);
101    GeneratedClassFile mockDelegate = new GeneratedClassFile(mockDelegateCtClass.getName(),
102        mockDelegateCtClass.toBytecode());
103    return Arrays.asList(new GeneratedClassFile[] {newInterface, mockDelegate});
104  }
105
106  private void reportReasonForUnsupportedType(Class<?> clazz) {
107    String reason = null;
108    if (clazz.isInterface()) {
109      // do nothing to make sure none of the other conditions apply.
110    } else if (clazz.isEnum()) {
111      reason = "Cannot mock an Enum";
112    } else if (clazz.isAnnotation()) {
113      reason = "Cannot mock an Annotation";
114    } else if (clazz.isArray()) {
115      reason = "Cannot mock an Array";
116    } else if (Modifier.isFinal(clazz.getModifiers())) {
117      reason = "Cannot mock a Final class";
118    } else if (clazz.isPrimitive()) {
119      reason = "Cannot mock primitives";
120    } else if (!Object.class.isAssignableFrom(clazz)) {
121      reason = "Cannot mock non-classes";
122    } else if (!containsUsableConstructor(clazz)) {
123      reason = "Cannot mock a class with no public constructors";
124    } else {
125      // Whatever the reason is, it's not one that we care about.
126    }
127    if (reason != null) {
128      // Sometimes we want to be silent, so check 'reason' against null.
129      System.err.println(reason + ": " + clazz.getName());
130    }
131  }
132
133  private boolean containsUsableConstructor(Class<?> clazz) {
134    Constructor<?>[] constructors = clazz.getDeclaredConstructors();
135    for (Constructor<?> constructor : constructors) {
136      if (Modifier.isPublic(constructor.getModifiers()) ||
137          Modifier.isProtected(constructor.getModifiers())) {
138        return true;
139      }
140    }
141    return false;
142  }
143
144  boolean classIsSupportedType(Class<?> clazz) {
145    return (containsUsableConstructor(clazz)) && Object.class.isAssignableFrom(clazz)
146        && !clazz.isInterface() && !clazz.isEnum() && !clazz.isAnnotation() && !clazz.isArray()
147        && !Modifier.isFinal(clazz.getModifiers());
148  }
149
150  void saveCtClass(CtClass clazz) throws ClassNotFoundException, IOException {
151    try {
152      clazz.writeFile();
153    } catch (NotFoundException e) {
154      throw new ClassNotFoundException("Error while saving modified class " + clazz.getName(), e);
155    } catch (CannotCompileException e) {
156      throw new RuntimeException("Internal Error: Attempt to save syntactically incorrect code "
157          + "for class " + clazz.getName(), e);
158    }
159  }
160
161  CtClass generateInterface(Class<?> originalClass, SdkVersion sdkVersion) {
162    ClassPool classPool = getClassPool();
163    try {
164      return classPool.getCtClass(FileUtils.getInterfaceNameFor(originalClass, sdkVersion));
165    } catch (NotFoundException e) {
166      CtClass newInterface =
167          classPool.makeInterface(FileUtils.getInterfaceNameFor(originalClass, sdkVersion));
168      addInterfaceMethods(originalClass, newInterface);
169      return newInterface;
170    }
171  }
172
173  String getInterfaceMethodSource(Method method) throws UnsupportedOperationException {
174    StringBuilder methodBody = getMethodSignature(method);
175    methodBody.append(";");
176    return methodBody.toString();
177  }
178
179  private StringBuilder getMethodSignature(Method method) {
180    int modifiers = method.getModifiers();
181    if (Modifier.isFinal(modifiers) || Modifier.isStatic(modifiers)) {
182      throw new UnsupportedOperationException(
183          "Cannot specify final or static methods in an interface");
184    }
185    StringBuilder methodSignature = new StringBuilder("public ");
186    methodSignature.append(getClassName(method.getReturnType()));
187    methodSignature.append(" ");
188    methodSignature.append(method.getName());
189    methodSignature.append("(");
190    int i = 0;
191    for (Class<?> arg : method.getParameterTypes()) {
192      methodSignature.append(getClassName(arg));
193      methodSignature.append(" arg");
194      methodSignature.append(i);
195      if (i < method.getParameterTypes().length - 1) {
196        methodSignature.append(",");
197      }
198      i++;
199    }
200    methodSignature.append(")");
201    if (method.getExceptionTypes().length > 0) {
202      methodSignature.append(" throws ");
203    }
204    i = 0;
205    for (Class<?> exception : method.getExceptionTypes()) {
206      methodSignature.append(getClassName(exception));
207      if (i < method.getExceptionTypes().length - 1) {
208        methodSignature.append(",");
209      }
210      i++;
211    }
212    return methodSignature;
213  }
214
215  private String getClassName(Class<?> clazz) {
216    return clazz.getCanonicalName();
217  }
218
219  static ClassPool getClassPool() {
220    return ClassPool.getDefault();
221  }
222
223  private boolean classExists(String name) {
224    // The following line is the ideal, but doesn't work (bug in library).
225    // return getClassPool().find(name) != null;
226    try {
227      getClassPool().get(name);
228      return true;
229    } catch (NotFoundException e) {
230      return false;
231    }
232  }
233
234  CtClass generateSubClass(Class<?> superClass, CtClass newInterface, SdkVersion sdkVersion)
235      throws ClassNotFoundException {
236    if (classExists(FileUtils.getSubclassNameFor(superClass, sdkVersion))) {
237      try {
238        return getClassPool().get(FileUtils.getSubclassNameFor(superClass, sdkVersion));
239      } catch (NotFoundException e) {
240        throw new ClassNotFoundException("This should be impossible, since we just checked for "
241            + "the existence of the class being created", e);
242      }
243    }
244    CtClass newClass = generateSkeletalClass(superClass, newInterface, sdkVersion);
245    if (!newClass.isFrozen()) {
246      newClass.addInterface(newInterface);
247      try {
248        newClass.addInterface(getClassPool().get(MockObject.class.getName()));
249      } catch (NotFoundException e) {
250        throw new ClassNotFoundException("Could not find " + MockObject.class.getName(), e);
251      }
252      addMethods(superClass, newClass);
253      addGetDelegateMethod(newClass);
254      addSetDelegateMethod(newClass, newInterface);
255      addConstructors(newClass, superClass);
256    }
257    return newClass;
258  }
259
260  private void addConstructors(CtClass clazz, Class<?> superClass) throws ClassNotFoundException {
261    CtClass superCtClass = getCtClassForClass(superClass);
262
263    CtConstructor[] constructors = superCtClass.getDeclaredConstructors();
264    for (CtConstructor constructor : constructors) {
265      int modifiers = constructor.getModifiers();
266      if (Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers)) {
267         CtConstructor ctConstructor;
268        try {
269          ctConstructor = CtNewConstructor.make(constructor.getParameterTypes(),
270               constructor.getExceptionTypes(), clazz);
271          clazz.addConstructor(ctConstructor);
272        } catch (CannotCompileException e) {
273          throw new RuntimeException("Internal Error - Could not add constructors.", e);
274        } catch (NotFoundException e) {
275          throw new RuntimeException("Internal Error - Constructor suddenly could not be found", e);
276        }
277      }
278    }
279  }
280
281  CtClass getCtClassForClass(Class<?> clazz) throws ClassNotFoundException {
282    ClassPool classPool = getClassPool();
283    try {
284      return classPool.get(clazz.getName());
285    } catch (NotFoundException e) {
286      throw new ClassNotFoundException("Class not found when finding the class to be mocked: "
287          + clazz.getName(), e);
288    }
289  }
290
291  private void addSetDelegateMethod(CtClass clazz, CtClass newInterface) {
292    try {
293      clazz.addMethod(CtMethod.make(getSetDelegateMethodSource(newInterface), clazz));
294    } catch (CannotCompileException e) {
295      throw new RuntimeException("Internal error while creating the setDelegate() method", e);
296    }
297  }
298
299  String getSetDelegateMethodSource(CtClass newInterface) {
300    return "public void setDelegate___AndroidMock(" + newInterface.getName() + " obj) { this."
301        + getDelegateFieldName() + " = obj;}";
302  }
303
304  private void addGetDelegateMethod(CtClass clazz) {
305    try {
306      CtMethod newMethod = CtMethod.make(getGetDelegateMethodSource(), clazz);
307      try {
308        CtMethod existingMethod = clazz.getMethod(newMethod.getName(), newMethod.getSignature());
309        clazz.removeMethod(existingMethod);
310      } catch (NotFoundException e) {
311        // expected path... sigh.
312      }
313      clazz.addMethod(newMethod);
314    } catch (CannotCompileException e) {
315      throw new RuntimeException("Internal error while creating the getDelegate() method", e);
316    }
317  }
318
319  private String getGetDelegateMethodSource() {
320    return "public Object getDelegate___AndroidMock() { return this." + getDelegateFieldName()
321        + "; }";
322  }
323
324  String getDelegateFieldName() {
325    return "delegateMockObject";
326  }
327
328  void addInterfaceMethods(Class<?> originalClass, CtClass newInterface) {
329    Method[] methods = getAllMethods(originalClass);
330    for (Method method : methods) {
331      try {
332        if (isMockable(method)) {
333          CtMethod newMethod = CtMethod.make(getInterfaceMethodSource(method), newInterface);
334          newInterface.addMethod(newMethod);
335        }
336      } catch (UnsupportedOperationException e) {
337        // Can't handle finals and statics.
338      } catch (CannotCompileException e) {
339        throw new RuntimeException(
340            "Internal error while creating a new Interface method for class "
341                + originalClass.getName() + ".  Method name: " + method.getName(), e);
342      }
343    }
344  }
345
346  void addMethods(Class<?> superClass, CtClass newClass) {
347    Method[] methods = getAllMethods(superClass);
348    if (newClass.isFrozen()) {
349      newClass.defrost();
350    }
351    List<CtMethod> existingMethods = Arrays.asList(newClass.getDeclaredMethods());
352    for (Method method : methods) {
353      try {
354        if (isMockable(method)) {
355          CtMethod newMethod = CtMethod.make(getDelegateMethodSource(method), newClass);
356          if (!existingMethods.contains(newMethod)) {
357            newClass.addMethod(newMethod);
358          }
359        }
360      } catch (UnsupportedOperationException e) {
361        // Can't handle finals and statics.
362      } catch (CannotCompileException e) {
363        throw new RuntimeException("Internal Error while creating subclass methods for "
364            + newClass.getName() + " method: " + method.getName(), e);
365      }
366    }
367  }
368
369  Method[] getAllMethods(Class<?> clazz) {
370    Map<String, Method> methodMap = getAllMethodsMap(clazz);
371    return methodMap.values().toArray(new Method[0]);
372  }
373
374  private Map<String, Method> getAllMethodsMap(Class<?> clazz) {
375    Map<String, Method> methodMap = new HashMap<String, Method>();
376    Class<?> superClass = clazz.getSuperclass();
377    if (superClass != null) {
378      methodMap.putAll(getAllMethodsMap(superClass));
379    }
380    List<Method> methods = new ArrayList<Method>(Arrays.asList(clazz.getDeclaredMethods()));
381    for (Method method : methods) {
382      String key = method.getName();
383      for (Class<?> param : method.getParameterTypes()) {
384        key += param.getCanonicalName();
385      }
386      methodMap.put(key, method);
387    }
388    return methodMap;
389  }
390
391  boolean isMockable(Method method) {
392    if (isForbiddenMethod(method)) {
393      return false;
394    }
395    int modifiers = method.getModifiers();
396    return !Modifier.isFinal(modifiers) && !Modifier.isStatic(modifiers) && !method.isBridge()
397        && (Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers));
398  }
399
400  boolean isForbiddenMethod(Method method) {
401    if (method.getName().equals("equals")) {
402      return method.getParameterTypes().length == 1
403          && method.getParameterTypes()[0].equals(Object.class);
404    } else if (method.getName().equals("toString")) {
405      return method.getParameterTypes().length == 0;
406    } else if (method.getName().equals("hashCode")) {
407      return method.getParameterTypes().length == 0;
408    }
409    return false;
410  }
411
412  private String getReturnDefault(Method method) {
413    Class<?> returnType = method.getReturnType();
414    if (!returnType.isPrimitive()) {
415      return "null";
416    } else if (returnType == Boolean.TYPE) {
417      return "false";
418    } else if (returnType == Void.TYPE) {
419      return "";
420    } else {
421      return "(" + returnType.getName() + ")0";
422    }
423  }
424
425  String getDelegateMethodSource(Method method) {
426    StringBuilder methodBody = getMethodSignature(method);
427    methodBody.append("{");
428    methodBody.append("if(this.");
429    methodBody.append(getDelegateFieldName());
430    methodBody.append("==null){return ");
431    methodBody.append(getReturnDefault(method));
432    methodBody.append(";}");
433    if (!method.getReturnType().equals(Void.TYPE)) {
434      methodBody.append("return ");
435    }
436    methodBody.append("this.");
437    methodBody.append(getDelegateFieldName());
438    methodBody.append(".");
439    methodBody.append(method.getName());
440    methodBody.append("(");
441    for (int i = 0; i < method.getParameterTypes().length; ++i) {
442      methodBody.append("arg");
443      methodBody.append(i);
444      if (i < method.getParameterTypes().length - 1) {
445        methodBody.append(",");
446      }
447    }
448    methodBody.append(");}");
449    return methodBody.toString();
450  }
451
452  CtClass generateSkeletalClass(Class<?> superClass, CtClass newInterface, SdkVersion sdkVersion)
453      throws ClassNotFoundException {
454    ClassPool classPool = getClassPool();
455    CtClass superCtClass = getCtClassForClass(superClass);
456    String subclassName = FileUtils.getSubclassNameFor(superClass, sdkVersion);
457
458    CtClass newClass;
459    try {
460      newClass = classPool.makeClass(subclassName, superCtClass);
461    } catch (RuntimeException e) {
462      if (e.getMessage().contains("frozen class")) {
463        try {
464          return classPool.get(subclassName);
465        } catch (NotFoundException ex) {
466          throw new ClassNotFoundException("Internal Error: could not find class", ex);
467        }
468      }
469      throw e;
470    }
471
472    try {
473      newClass.addField(new CtField(newInterface, getDelegateFieldName(), newClass));
474    } catch (CannotCompileException e) {
475      throw new RuntimeException("Internal error adding the delegate field to "
476          + newClass.getName(), e);
477    }
478    return newClass;
479  }
480}
481