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;
19
20import java.io.FileNotFoundException;
21import java.io.IOException;
22import java.io.OutputStream;
23import java.util.ArrayList;
24import java.util.HashSet;
25import java.util.List;
26import java.util.Set;
27
28import javax.annotation.processing.AbstractProcessor;
29import javax.annotation.processing.RoundEnvironment;
30import javax.annotation.processing.SupportedAnnotationTypes;
31import javax.annotation.processing.SupportedOptions;
32import javax.annotation.processing.SupportedSourceVersion;
33import javax.lang.model.SourceVersion;
34import javax.lang.model.element.AnnotationMirror;
35import javax.lang.model.element.AnnotationValue;
36import javax.lang.model.element.Element;
37import javax.lang.model.element.TypeElement;
38import javax.tools.Diagnostic.Kind;
39import 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})
57public 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