package com.airbnb.epoxy;
import com.google.auto.service.AutoService;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import static com.airbnb.epoxy.ConfigManager.PROCESSOR_IMPLICITLY_ADD_AUTO_MODELS;
import static com.airbnb.epoxy.ConfigManager.PROCESSOR_OPTION_VALIDATE_MODEL_USAGE;
/**
* Looks for {@link EpoxyAttribute} annotations and generates a subclass for all classes that have
* those attributes. The generated subclass includes setters, getters, equals, and hashcode for the
* given field. Any constructors on the original class are duplicated. Abstract classes are ignored
* since generated classes would have to be abstract in order to guarantee they compile, and that
* reduces their usefulness and doesn't make as much sense to support.
*/
@AutoService(Processor.class)
public class EpoxyProcessor extends AbstractProcessor {
private final Map<String, String> testOptions;
private Filer filer;
private Messager messager;
private Elements elementUtils;
private Types typeUtils;
private LayoutResourceProcessor layoutResourceProcessor;
private ConfigManager configManager;
private DataBindingModuleLookup dataBindingModuleLookup;
private final ErrorLogger errorLogger = new ErrorLogger();
private GeneratedModelWriter modelWriter;
private ControllerProcessor controllerProcessor;
private DataBindingProcessor dataBindingProcessor;
private final List<GeneratedModelInfo> generatedModels = new ArrayList<>();
public EpoxyProcessor() {
this(Collections.<String, String>emptyMap());
}
/**
* Constructor to use for tests to pass annotation processor options since we can't get them from
* the build.gradle
*/
public EpoxyProcessor(Map<String, String> options) {
testOptions = options;
}
/** For testing. */
public static EpoxyProcessor withNoValidation() {
HashMap<String, String> options = new HashMap<>();
options.put(PROCESSOR_OPTION_VALIDATE_MODEL_USAGE, "false");
return new EpoxyProcessor(options);
}
/** For testing. */
public static EpoxyProcessor withImplicitAdding() {
HashMap<String, String> options = new HashMap<>();
options.put(PROCESSOR_IMPLICITLY_ADD_AUTO_MODELS, "true");
return new EpoxyProcessor(options);
}
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
filer = processingEnv.getFiler();
messager = processingEnv.getMessager();
elementUtils = processingEnv.getElementUtils();
typeUtils = processingEnv.getTypeUtils();
layoutResourceProcessor =
new LayoutResourceProcessor(processingEnv, errorLogger, elementUtils, typeUtils);
configManager =
new ConfigManager(!testOptions.isEmpty() ? testOptions : processingEnv.getOptions(),
elementUtils);
dataBindingModuleLookup =
new DataBindingModuleLookup(elementUtils, typeUtils, errorLogger, layoutResourceProcessor);
modelWriter =
new GeneratedModelWriter(filer, typeUtils, errorLogger,
layoutResourceProcessor,
configManager, dataBindingModuleLookup);
controllerProcessor = new ControllerProcessor(filer, elementUtils, typeUtils, errorLogger,
configManager);
dataBindingProcessor =
new DataBindingProcessor(elementUtils, typeUtils, errorLogger, configManager,
layoutResourceProcessor, dataBindingModuleLookup, modelWriter);
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
types.add(EpoxyModelClass.class.getCanonicalName());
types.add(EpoxyAttribute.class.getCanonicalName());
types.add(PackageEpoxyConfig.class.getCanonicalName());
types.add(AutoModel.class.getCanonicalName());
types.add(EpoxyDataBindingLayouts.class.getCanonicalName());
types.add(ClassNames.LITHO_ANNOTATION_LAYOUT_SPEC.reflectionName());
return types;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
errorLogger.logErrors(configManager.processConfigurations(roundEnv));
ModelProcessor modelProcessor = new ModelProcessor(messager,
elementUtils, typeUtils, configManager, errorLogger,
modelWriter);
generatedModels.addAll(modelProcessor.processModels(roundEnv));
dataBindingProcessor.process(roundEnv);
LithoSpecProcessor lithoSpecProcessor = new LithoSpecProcessor(
elementUtils, typeUtils, configManager, errorLogger, modelWriter);
generatedModels.addAll(lithoSpecProcessor.processSpecs(roundEnv));
controllerProcessor.process(roundEnv);
if (roundEnv.processingOver()) {
generatedModels.addAll(dataBindingProcessor.resolveDataBindingClassesAndWriteJava());
// This must be done after all generated model info is collected
controllerProcessor.resolveGeneratedModelsAndWriteJava(generatedModels);
// We wait until the very end to log errors so that all the generated classes are still
// created.
// Otherwise the compiler error output is clogged with lots of errors from the generated
// classes not existing, which makes it hard to see the actual errors.
errorLogger.writeExceptions(messager);
}
// Let any other annotation processors use our annotations if they want to
return false;
}
}