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