Home | History | Annotate | Download | only in mocking
      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  */
     16 package com.google.android.testing.mocking;
     17 
     18 import javassist.CannotCompileException;
     19 
     20 import java.io.FileNotFoundException;
     21 import java.io.IOException;
     22 import java.io.OutputStream;
     23 import java.util.ArrayList;
     24 import java.util.HashSet;
     25 import java.util.List;
     26 import java.util.Set;
     27 
     28 import javax.annotation.processing.AbstractProcessor;
     29 import javax.annotation.processing.RoundEnvironment;
     30 import javax.annotation.processing.SupportedAnnotationTypes;
     31 import javax.annotation.processing.SupportedOptions;
     32 import javax.annotation.processing.SupportedSourceVersion;
     33 import javax.lang.model.SourceVersion;
     34 import javax.lang.model.element.AnnotationMirror;
     35 import javax.lang.model.element.AnnotationValue;
     36 import javax.lang.model.element.Element;
     37 import javax.lang.model.element.TypeElement;
     38 import javax.tools.Diagnostic.Kind;
     39 import javax.tools.JavaFileObject;
     40 
     41 
     42 /**
     43  * Annotation Processor to generate the mocks for Android Mock.
     44  *
     45  * This processor will automatically create mocks for all classes
     46  * specified by {@link UsesMocks} annotations.
     47  *
     48  * @author swoodward (at) google.com (Stephen Woodward)
     49  */
     50 @SupportedAnnotationTypes("com.google.android.testing.mocking.UsesMocks")
     51 @SupportedSourceVersion(SourceVersion.RELEASE_5)
     52 @SupportedOptions({
     53     UsesMocksProcessor.REGENERATE_FRAMEWORK_MOCKS,
     54     UsesMocksProcessor.LOGFILE,
     55     UsesMocksProcessor.BIN_DIR
     56 })
     57 public class UsesMocksProcessor extends AbstractProcessor {
     58   public static final String LOGFILE = "logfile";
     59   public static final String REGENERATE_FRAMEWORK_MOCKS = "RegenerateFrameworkMocks";
     60   public static final String BIN_DIR = "bin_dir";
     61   private AndroidMockGenerator mockGenerator = new AndroidMockGenerator();
     62   private AndroidFrameworkMockGenerator frameworkMockGenerator =
     63       new AndroidFrameworkMockGenerator();
     64   ProcessorLogger logger;
     65 
     66   /**
     67    * Main entry point of the processor.  This is called by the Annotation framework.
     68    * {@link javax.annotation.processing.AbstractProcessor} for more details.
     69    */
     70   @Override
     71   public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment environment) {
     72     try {
     73       prepareLogger();
     74       List<Class<?>> classesToMock = getClassesToMock(environment);
     75       Set<GeneratedClassFile> mockedClassesSet = getMocksFor(classesToMock);
     76       writeMocks(mockedClassesSet);
     77     } catch (Exception e) {
     78       logger.printMessage(Kind.ERROR, e);
     79     } finally {
     80       logger.close();
     81     }
     82     return false;
     83   }
     84 
     85   /**
     86    * Returns a Set of GeneratedClassFile objects which represent all of the classes to be mocked.
     87    *
     88    * @param classesToMock the list of classes which need to be mocked.
     89    * @return a set of mock support classes to support the mocking of all the classes specified in
     90    *         {@literal classesToMock}.
     91    */
     92   private Set<GeneratedClassFile> getMocksFor(List<Class<?>> classesToMock) throws IOException,
     93       CannotCompileException {
     94     logger.printMessage(Kind.NOTE, "Found " + classesToMock.size() + " classes to mock");
     95     boolean regenerateFrameworkMocks = processingEnv.getOptions().get(
     96         REGENERATE_FRAMEWORK_MOCKS) != null;
     97     if (regenerateFrameworkMocks) {
     98       logger.printMessage(Kind.NOTE, "Regenerating Framework Mocks on Request");
     99     }
    100     Set<GeneratedClassFile> mockedClassesSet =
    101         getClassMocks(classesToMock, regenerateFrameworkMocks);
    102     logger.printMessage(Kind.NOTE, "Found " + mockedClassesSet.size()
    103         + " mocked classes to save");
    104     return mockedClassesSet;
    105   }
    106 
    107   /**
    108    * @param environment the environment for this round of processing as provided to the main
    109    *        {@link #process(Set, RoundEnvironment)} method.
    110    * @return a List of Class objects for the classes that need to be mocked.
    111    */
    112   private List<Class<?>> getClassesToMock(RoundEnvironment environment) {
    113     logger.printMessage(Kind.NOTE, "Start Processing Annotations");
    114     List<Class<?>> classesToMock = new ArrayList<Class<?>>();
    115     classesToMock.addAll(
    116         findClassesToMock(environment.getElementsAnnotatedWith(UsesMocks.class)));
    117     return classesToMock;
    118   }
    119 
    120   private void prepareLogger() {
    121     if (logger == null) {
    122       logger = new ProcessorLogger(processingEnv.getOptions().get(LOGFILE), processingEnv);
    123     }
    124   }
    125 
    126   /**
    127    * Finds all of the classes that should be mocked, based on {@link UsesMocks} annotations
    128    * in the various source files being compiled.
    129    *
    130    * @param annotatedElements a Set of all elements holding {@link UsesMocks} annotations.
    131    * @return all of the classes that should be mocked.
    132    */
    133   List<Class<?>> findClassesToMock(Set<? extends Element> annotatedElements) {
    134     logger.printMessage(Kind.NOTE, "Processing " + annotatedElements);
    135     List<Class<?>> classList = new ArrayList<Class<?>>();
    136     for (Element annotation : annotatedElements) {
    137       List<? extends AnnotationMirror> mirrors = annotation.getAnnotationMirrors();
    138       for (AnnotationMirror mirror : mirrors) {
    139         if (mirror.getAnnotationType().toString().equals(UsesMocks.class.getName())) {
    140           for (AnnotationValue annotationValue : mirror.getElementValues().values()) {
    141             for (Object classFileName : (Iterable<?>) annotationValue.getValue()) {
    142               String className = classFileName.toString();
    143               if (className.endsWith(".class")) {
    144                 className = className.substring(0, className.length() - 6);
    145               }
    146               logger.printMessage(Kind.NOTE, "Adding Class to Mocking List: " + className);
    147               try {
    148                 classList.add(Class.forName(className, false, getClass().getClassLoader()));
    149               } catch (ClassNotFoundException e) {
    150                 logger.reportClasspathError(className, e);
    151               }
    152             }
    153           }
    154         }
    155       }
    156     }
    157     return classList;
    158   }
    159 
    160   /**
    161    * Gets a set of GeneratedClassFiles to represent all of the support classes required to
    162    * mock the List of classes provided in {@code classesToMock}.
    163    * @param classesToMock the list of classes to be mocked.
    164    * @param regenerateFrameworkMocks if true, then mocks for the framework classes will be created
    165    *        instead of pulled from the existing set of framework support classes.
    166    * @return a Set of {@link GeneratedClassFile} for all of the mocked classes.
    167    */
    168   Set<GeneratedClassFile> getClassMocks(List<Class<?>> classesToMock,
    169       boolean regenerateFrameworkMocks) throws IOException, CannotCompileException {
    170     Set<GeneratedClassFile> mockedClassesSet = new HashSet<GeneratedClassFile>();
    171     for (Class<?> clazz : classesToMock) {
    172       try {
    173         logger.printMessage(Kind.NOTE, "Mocking " + clazz);
    174         if (!AndroidMock.isAndroidClass(clazz) || regenerateFrameworkMocks) {
    175           mockedClassesSet.addAll(getAndroidMockGenerator().createMocksForClass(clazz));
    176         } else {
    177           mockedClassesSet.addAll(getAndroidFrameworkMockGenerator().getMocksForClass(clazz));
    178         }
    179       } catch (ClassNotFoundException e) {
    180         logger.reportClasspathError(clazz.getName(), e);
    181       } catch (NoClassDefFoundError e) {
    182         logger.reportClasspathError(clazz.getName(), e);
    183       }
    184     }
    185     return mockedClassesSet;
    186   }
    187 
    188   private AndroidFrameworkMockGenerator getAndroidFrameworkMockGenerator() {
    189     return frameworkMockGenerator;
    190   }
    191 
    192   /**
    193    * Writes the provided mocks from {@code mockedClassesSet} to the bin folder alongside the
    194    * .class files being generated by the javac call which invoked this annotation processor.
    195    * In Eclipse, additional information is needed as the Eclipse annotation processor framework
    196    * is missing key functionality required by this method.  Instead the classes are saved using
    197    * a FileOutputStream and the -Abin_dir processor option must be set.
    198    * @param mockedClassesSet the set of mocks to be saved.
    199    */
    200   void writeMocks(Set<GeneratedClassFile> mockedClassesSet) {
    201     for (GeneratedClassFile clazz : mockedClassesSet) {
    202       OutputStream classFileStream;
    203       try {
    204         logger.printMessage(Kind.NOTE, "Saving " + clazz.getClassName());
    205         JavaFileObject classFile = processingEnv.getFiler().createClassFile(clazz.getClassName());
    206         classFileStream = classFile.openOutputStream();
    207         classFileStream.write(clazz.getContents());
    208         classFileStream.close();
    209       } catch (IOException e) {
    210         logger.printMessage(Kind.ERROR, "Internal Error saving mock: " + clazz.getClassName());
    211         logger.printMessage(Kind.ERROR, e);
    212       } catch (UnsupportedOperationException e) {
    213         // Eclipse annotation processing doesn't support class creation.
    214         logger.printMessage(Kind.NOTE, "Saving via Eclipse " + clazz.getClassName());
    215         saveMocksEclipse(clazz, processingEnv.getOptions().get(BIN_DIR).toString().trim());
    216       }
    217     }
    218     logger.printMessage(Kind.NOTE, "Finished Processing Mocks");
    219   }
    220 
    221   /**
    222    * Workaround to save the mocks for Eclipse's annotation processing framework which doesn't
    223    * support the JavaFileObject object.
    224    * @param clazz the class to save.
    225    * @param outputFolderName the output folder where the class will be saved.
    226    */
    227   private void saveMocksEclipse(GeneratedClassFile clazz, String outputFolderName) {
    228     try {
    229       FileUtils.saveClassToFolder(clazz, outputFolderName);
    230     } catch (FileNotFoundException e) {
    231       logger.printMessage(Kind.ERROR, e);
    232     } catch (IOException e) {
    233       logger.printMessage(Kind.ERROR, e);
    234     }
    235   }
    236 
    237   private AndroidMockGenerator getAndroidMockGenerator() {
    238     return mockGenerator;
    239   }
    240 }
    241