blob: 0ca5c4866461c5f9b2f9fbffd447e4bca8a41a31 [file] [log] [blame]
/*
* 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<? extends TypeElement> annotations, RoundEnvironment environment) {
try {
prepareLogger();
List<Class<?>> classesToMock = getClassesToMock(environment);
Set<GeneratedClassFile> 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<GeneratedClassFile> getMocksFor(List<Class<?>> 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<GeneratedClassFile> 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<Class<?>> getClassesToMock(RoundEnvironment environment) {
logger.printMessage(Kind.NOTE, "Start Processing Annotations");
List<Class<?>> classesToMock = new ArrayList<Class<?>>();
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<Class<?>> findClassesToMock(Set<? extends Element> annotatedElements) {
logger.printMessage(Kind.NOTE, "Processing " + annotatedElements);
List<Class<?>> classList = new ArrayList<Class<?>>();
for (Element annotation : annotatedElements) {
List<? extends AnnotationMirror> 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<GeneratedClassFile> getClassMocks(List<Class<?>> classesToMock,
boolean regenerateFrameworkMocks) throws IOException, CannotCompileException {
Set<GeneratedClassFile> mockedClassesSet = new HashSet<GeneratedClassFile>();
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<GeneratedClassFile> 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;
}
}