/* * Copyright 2010 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.testing.mocking; import javassist.CannotCompileException; import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; import javax.annotation.processing.SupportedOptions; import javax.annotation.processing.SupportedSourceVersion; import javax.lang.model.SourceVersion; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; import javax.tools.Diagnostic.Kind; import javax.tools.JavaFileObject; /** * Annotation Processor to generate the mocks for Android Mock. * * This processor will automatically create mocks for all classes * specified by {@link UsesMocks} annotations. * * @author swoodward@google.com (Stephen Woodward) */ @SupportedAnnotationTypes("com.google.android.testing.mocking.UsesMocks") @SupportedSourceVersion(SourceVersion.RELEASE_5) @SupportedOptions({ UsesMocksProcessor.REGENERATE_FRAMEWORK_MOCKS, UsesMocksProcessor.LOGFILE, UsesMocksProcessor.BIN_DIR }) public class UsesMocksProcessor extends AbstractProcessor { public static final String LOGFILE = "logfile"; public static final String REGENERATE_FRAMEWORK_MOCKS = "RegenerateFrameworkMocks"; public static final String BIN_DIR = "bin_dir"; private AndroidMockGenerator mockGenerator = new AndroidMockGenerator(); private AndroidFrameworkMockGenerator frameworkMockGenerator = new AndroidFrameworkMockGenerator(); ProcessorLogger logger; /** * Main entry point of the processor. This is called by the Annotation framework. * {@link javax.annotation.processing.AbstractProcessor} for more details. */ @Override public boolean process(Set annotations, RoundEnvironment environment) { try { prepareLogger(); List> classesToMock = getClassesToMock(environment); Set mockedClassesSet = getMocksFor(classesToMock); writeMocks(mockedClassesSet); } catch (Exception e) { logger.printMessage(Kind.ERROR, e); } finally { logger.close(); } return false; } /** * Returns a Set of GeneratedClassFile objects which represent all of the classes to be mocked. * * @param classesToMock the list of classes which need to be mocked. * @return a set of mock support classes to support the mocking of all the classes specified in * {@literal classesToMock}. */ private Set getMocksFor(List> classesToMock) throws IOException, CannotCompileException { logger.printMessage(Kind.NOTE, "Found " + classesToMock.size() + " classes to mock"); boolean regenerateFrameworkMocks = processingEnv.getOptions().get( REGENERATE_FRAMEWORK_MOCKS) != null; if (regenerateFrameworkMocks) { logger.printMessage(Kind.NOTE, "Regenerating Framework Mocks on Request"); } Set mockedClassesSet = getClassMocks(classesToMock, regenerateFrameworkMocks); logger.printMessage(Kind.NOTE, "Found " + mockedClassesSet.size() + " mocked classes to save"); return mockedClassesSet; } /** * @param environment the environment for this round of processing as provided to the main * {@link #process(Set, RoundEnvironment)} method. * @return a List of Class objects for the classes that need to be mocked. */ private List> getClassesToMock(RoundEnvironment environment) { logger.printMessage(Kind.NOTE, "Start Processing Annotations"); List> classesToMock = new ArrayList>(); classesToMock.addAll( findClassesToMock(environment.getElementsAnnotatedWith(UsesMocks.class))); return classesToMock; } private void prepareLogger() { if (logger == null) { logger = new ProcessorLogger(processingEnv.getOptions().get(LOGFILE), processingEnv); } } /** * Finds all of the classes that should be mocked, based on {@link UsesMocks} annotations * in the various source files being compiled. * * @param annotatedElements a Set of all elements holding {@link UsesMocks} annotations. * @return all of the classes that should be mocked. */ List> findClassesToMock(Set annotatedElements) { logger.printMessage(Kind.NOTE, "Processing " + annotatedElements); List> classList = new ArrayList>(); for (Element annotation : annotatedElements) { List mirrors = annotation.getAnnotationMirrors(); for (AnnotationMirror mirror : mirrors) { if (mirror.getAnnotationType().toString().equals(UsesMocks.class.getName())) { for (AnnotationValue annotationValue : mirror.getElementValues().values()) { for (Object classFileName : (Iterable) annotationValue.getValue()) { String className = classFileName.toString(); if (className.endsWith(".class")) { className = className.substring(0, className.length() - 6); } logger.printMessage(Kind.NOTE, "Adding Class to Mocking List: " + className); try { classList.add(Class.forName(className, false, getClass().getClassLoader())); } catch (ClassNotFoundException e) { logger.reportClasspathError(className, e); } } } } } } return classList; } /** * Gets a set of GeneratedClassFiles to represent all of the support classes required to * mock the List of classes provided in {@code classesToMock}. * @param classesToMock the list of classes to be mocked. * @param regenerateFrameworkMocks if true, then mocks for the framework classes will be created * instead of pulled from the existing set of framework support classes. * @return a Set of {@link GeneratedClassFile} for all of the mocked classes. */ Set getClassMocks(List> classesToMock, boolean regenerateFrameworkMocks) throws IOException, CannotCompileException { Set mockedClassesSet = new HashSet(); for (Class clazz : classesToMock) { try { logger.printMessage(Kind.NOTE, "Mocking " + clazz); if (!AndroidMock.isAndroidClass(clazz) || regenerateFrameworkMocks) { mockedClassesSet.addAll(getAndroidMockGenerator().createMocksForClass(clazz)); } else { mockedClassesSet.addAll(getAndroidFrameworkMockGenerator().getMocksForClass(clazz)); } } catch (ClassNotFoundException e) { logger.reportClasspathError(clazz.getName(), e); } catch (NoClassDefFoundError e) { logger.reportClasspathError(clazz.getName(), e); } } return mockedClassesSet; } private AndroidFrameworkMockGenerator getAndroidFrameworkMockGenerator() { return frameworkMockGenerator; } /** * Writes the provided mocks from {@code mockedClassesSet} to the bin folder alongside the * .class files being generated by the javac call which invoked this annotation processor. * In Eclipse, additional information is needed as the Eclipse annotation processor framework * is missing key functionality required by this method. Instead the classes are saved using * a FileOutputStream and the -Abin_dir processor option must be set. * @param mockedClassesSet the set of mocks to be saved. */ void writeMocks(Set mockedClassesSet) { for (GeneratedClassFile clazz : mockedClassesSet) { OutputStream classFileStream; try { logger.printMessage(Kind.NOTE, "Saving " + clazz.getClassName()); JavaFileObject classFile = processingEnv.getFiler().createClassFile(clazz.getClassName()); classFileStream = classFile.openOutputStream(); classFileStream.write(clazz.getContents()); classFileStream.close(); } catch (IOException e) { logger.printMessage(Kind.ERROR, "Internal Error saving mock: " + clazz.getClassName()); logger.printMessage(Kind.ERROR, e); } catch (UnsupportedOperationException e) { // Eclipse annotation processing doesn't support class creation. logger.printMessage(Kind.NOTE, "Saving via Eclipse " + clazz.getClassName()); saveMocksEclipse(clazz, processingEnv.getOptions().get(BIN_DIR).toString().trim()); } } logger.printMessage(Kind.NOTE, "Finished Processing Mocks"); } /** * Workaround to save the mocks for Eclipse's annotation processing framework which doesn't * support the JavaFileObject object. * @param clazz the class to save. * @param outputFolderName the output folder where the class will be saved. */ private void saveMocksEclipse(GeneratedClassFile clazz, String outputFolderName) { try { FileUtils.saveClassToFolder(clazz, outputFolderName); } catch (FileNotFoundException e) { logger.printMessage(Kind.ERROR, e); } catch (IOException e) { logger.printMessage(Kind.ERROR, e); } } private AndroidMockGenerator getAndroidMockGenerator() { return mockGenerator; } }