package com.airbnb.epoxy;
import com.airbnb.epoxy.GeneratedModelWriter.BeforeBuildCallback;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeSpec.Builder;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import static com.airbnb.epoxy.ClassNames.EPOXY_LITHO_MODEL;
import static com.airbnb.epoxy.Utils.getAnnotationClass;
class LithoSpecProcessor {
private final Elements elementUtils;
private final Types typeUtils;
private final ConfigManager configManager;
private final ErrorLogger errorLogger;
private final GeneratedModelWriter modelWriter;
private Class<? extends Annotation> layoutSpecAnnotationClass;
LithoSpecProcessor(Elements elementUtils, Types typeUtils,
ConfigManager configManager, ErrorLogger errorLogger, GeneratedModelWriter modelWriter) {
this.elementUtils = elementUtils;
this.typeUtils = typeUtils;
this.configManager = configManager;
this.errorLogger = errorLogger;
this.modelWriter = modelWriter;
}
Collection<LithoModelInfo> processSpecs(RoundEnvironment roundEnv) {
Map<TypeElement, LithoModelInfo> modelInfoMap = new LinkedHashMap<>();
if (!hasLithoEpoxyDependency()) {
// If the epoxy-litho module has not been included then we don't have access to the Epoxy
// litho model and can't build a model that extends it
return new ArrayList<>();
}
layoutSpecAnnotationClass =
getAnnotationClass(ClassNames.LITHO_ANNOTATION_LAYOUT_SPEC);
if (layoutSpecAnnotationClass == null) {
// There is no dependency on Litho so there aren't any litho components to check for
return new ArrayList<>();
}
for (Element lithoLayout : roundEnv.getElementsAnnotatedWith(layoutSpecAnnotationClass)) {
if (!(lithoLayout instanceof TypeElement)) {
continue;
}
TypeElement typeElement = (TypeElement) lithoLayout;
modelInfoMap.put(typeElement, new LithoModelInfo(typeUtils, elementUtils, typeElement));
}
Class<? extends Annotation> propClass = getAnnotationClass(ClassNames.LITHO_ANNOTATION_PROP);
for (Element propElement : roundEnv.getElementsAnnotatedWith(propClass)) {
LithoModelInfo lithoModelInfo = getModelInfoForProp(modelInfoMap, propElement);
if (lithoModelInfo != null) {
lithoModelInfo.addProp(propElement);
}
}
for (Entry<TypeElement, LithoModelInfo> modelInfoEntry : modelInfoMap.entrySet()) {
try {
final LithoModelInfo modelInfo = modelInfoEntry.getValue();
modelWriter.generateClassForModel(modelInfo, new BeforeBuildCallback() {
@Override
public void modifyBuilder(Builder builder) {
updateGeneratedClassForLithoComponent(modelInfo, builder);
}
});
} catch (Exception e) {
errorLogger.logError(e, "Error generating model classes");
}
}
return modelInfoMap.values();
}
private boolean hasLithoEpoxyDependency() {
// Only true if the epoxy-litho module is included in dependencies
return Utils.getClass(EPOXY_LITHO_MODEL) != null;
}
private void updateGeneratedClassForLithoComponent(LithoModelInfo modelInfo,
Builder classBuilder) {
// Adding the "buildComponent" method
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("buildComponent")
.addAnnotation(Override.class)
.addModifiers(Modifier.PROTECTED)
.returns(
ParameterizedTypeName.get(ClassNames.LITHO_COMPONENT, modelInfo.lithoComponentName)
)
.addParameter(ClassNames.LITHO_COMPONENT_CONTEXT, "context")
.addCode("return $T.create(context)", modelInfo.lithoComponentName);
for (AttributeInfo attributeInfo : modelInfo.attributeInfo) {
methodBuilder.addCode(".$L($L)", attributeInfo.getName(), attributeInfo.getName());
}
methodBuilder.addStatement(".build()");
classBuilder.addMethod(methodBuilder.build());
}
private LithoModelInfo getModelInfoForProp(Map<TypeElement, LithoModelInfo> modelInfoMap,
Element propElement) {
Element methodElement = propElement.getEnclosingElement();
if (methodElement == null) {
return null;
}
Element classElement = methodElement.getEnclosingElement();
if (classElement.getAnnotation(layoutSpecAnnotationClass) == null) {
return null;
}
return modelInfoMap.get(classElement);
}
}