• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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@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
process(Set<? extends TypeElement> annotations, RoundEnvironment environment)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    */
getMocksFor(List<Class<?>> classesToMock)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    */
getClassesToMock(RoundEnvironment environment)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 
prepareLogger()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    */
findClassesToMock(Set<? extends Element> annotatedElements)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    */
getClassMocks(List<Class<?>> classesToMock, boolean regenerateFrameworkMocks)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 
getAndroidFrameworkMockGenerator()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    */
writeMocks(Set<GeneratedClassFile> mockedClassesSet)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    */
saveMocksEclipse(GeneratedClassFile clazz, String outputFolderName)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 
getAndroidMockGenerator()237   private AndroidMockGenerator getAndroidMockGenerator() {
238     return mockGenerator;
239   }
240 }
241