package com.airbnb.epoxy;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.annotation.processing.Messager;
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.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import static com.airbnb.epoxy.Utils.EPOXY_MODEL_TYPE;
import static com.airbnb.epoxy.Utils.belongToTheSamePackage;
import static com.airbnb.epoxy.Utils.isEpoxyModel;
import static com.airbnb.epoxy.Utils.isSubtype;
import static com.airbnb.epoxy.Utils.validateFieldAccessibleViaGeneratedCode;
import static javax.lang.model.element.Modifier.ABSTRACT;
import static javax.lang.model.element.Modifier.STATIC;
class ModelProcessor {
private final Messager messager;
private final Elements elementUtils;
private final Types typeUtils;
private final ConfigManager configManager;
private final ErrorLogger errorLogger;
private final GeneratedModelWriter modelWriter;
private LinkedHashMap<TypeElement, GeneratedModelInfo> modelClassMap;
ModelProcessor(Messager messager, Elements elementUtils, Types typeUtils,
ConfigManager configManager, ErrorLogger errorLogger, GeneratedModelWriter modelWriter) {
this.messager = messager;
this.elementUtils = elementUtils;
this.typeUtils = typeUtils;
this.configManager = configManager;
this.errorLogger = errorLogger;
this.modelWriter = modelWriter;
}
Collection<GeneratedModelInfo> processModels(RoundEnvironment roundEnv) {
modelClassMap = new LinkedHashMap<>();
for (Element attribute : roundEnv.getElementsAnnotatedWith(EpoxyAttribute.class)) {
try {
addAttributeToGeneratedClass(attribute, modelClassMap);
} catch (Exception e) {
errorLogger.logError(e);
}
}
for (Element clazz : roundEnv.getElementsAnnotatedWith(EpoxyModelClass.class)) {
try {
getOrCreateTargetClass(modelClassMap, (TypeElement) clazz);
} catch (Exception e) {
errorLogger.logError(e);
}
}
try {
addAttributesFromOtherModules(modelClassMap);
} catch (Exception e) {
errorLogger.logError(e);
}
try {
updateClassesForInheritance(modelClassMap);
} catch (Exception e) {
errorLogger.logError(e);
}
for (Entry<TypeElement, GeneratedModelInfo> modelEntry : modelClassMap.entrySet()) {
try {
modelWriter.generateClassForModel(modelEntry.getValue());
} catch (Exception e) {
errorLogger.logError(e, "Error generating model classes");
}
}
validateAttributesImplementHashCode(modelClassMap.values());
return modelClassMap.values();
}
private void validateAttributesImplementHashCode(
Collection<GeneratedModelInfo> generatedClasses) {
HashCodeValidator hashCodeValidator = new HashCodeValidator(typeUtils);
for (GeneratedModelInfo generatedClass : generatedClasses) {
for (AttributeInfo attributeInfo : generatedClass.getAttributeInfo()) {
if (configManager.requiresHashCode(attributeInfo)
&& attributeInfo.useInHash()
&& !attributeInfo.ignoreRequireHashCode()) {
try {
hashCodeValidator.validate(attributeInfo);
} catch (EpoxyProcessorException e) {
errorLogger.logError(e);
}
}
}
}
}
private void addAttributeToGeneratedClass(Element attribute,
Map<TypeElement, GeneratedModelInfo> modelClassMap) {
TypeElement classElement = (TypeElement) attribute.getEnclosingElement();
GeneratedModelInfo helperClass = getOrCreateTargetClass(modelClassMap, classElement);
helperClass.addAttribute(buildAttributeInfo(attribute));
}
private AttributeInfo buildAttributeInfo(Element attribute) {
validateFieldAccessibleViaGeneratedCode(attribute, EpoxyAttribute.class, errorLogger, true);
return new BaseModelAttributeInfo(attribute, typeUtils, elementUtils, errorLogger);
}
private GeneratedModelInfo getOrCreateTargetClass(
Map<TypeElement, GeneratedModelInfo> modelClassMap, TypeElement classElement) {
GeneratedModelInfo generatedModelInfo = modelClassMap.get(classElement);
boolean isFinal = classElement.getModifiers().contains(Modifier.FINAL);
if (isFinal) {
errorLogger.logError("Class with %s annotations cannot be final: %s",
EpoxyAttribute.class.getSimpleName(), classElement.getSimpleName());
}
// Nested classes must be static
if (classElement.getNestingKind().isNested()) {
if (!classElement.getModifiers().contains(STATIC)) {
errorLogger.logError(
"Nested model classes must be static. (class: %s)",
classElement.getSimpleName());
}
}
if (!isEpoxyModel(classElement.asType())) {
errorLogger.logError("Class with %s annotations must extend %s (%s)",
EpoxyAttribute.class.getSimpleName(), EPOXY_MODEL_TYPE,
classElement.getSimpleName());
}
if (configManager.requiresAbstractModels(classElement)
&& !classElement.getModifiers().contains(ABSTRACT)) {
errorLogger
.logError("Epoxy model class must be abstract (%s)", classElement.getSimpleName());
}
if (generatedModelInfo == null) {
generatedModelInfo = new BasicGeneratedModelInfo(typeUtils, elementUtils, classElement,
errorLogger);
modelClassMap.put(classElement, generatedModelInfo);
}
return generatedModelInfo;
}
/**
* Looks for attributes on super classes that weren't included in this processor's coverage. Super
* classes are already found if they are in the same module since the processor will pick them up
* with the rest of the annotations.
*/
private void addAttributesFromOtherModules(
Map<TypeElement, GeneratedModelInfo> modelClassMap) {
// Copy the entries in the original map so we can add new entries to the map while we iterate
// through the old entries
Set<Entry<TypeElement, GeneratedModelInfo>> originalEntries =
new HashSet<>(modelClassMap.entrySet());
for (Entry<TypeElement, GeneratedModelInfo> entry : originalEntries) {
TypeElement currentEpoxyModel = entry.getKey();
TypeMirror superclassType = currentEpoxyModel.getSuperclass();
GeneratedModelInfo generatedModelInfo = entry.getValue();
while (isEpoxyModel(superclassType)) {
TypeElement superclassEpoxyModel = (TypeElement) typeUtils.asElement(superclassType);
if (!modelClassMap.keySet().contains(superclassEpoxyModel)) {
for (Element element : superclassEpoxyModel.getEnclosedElements()) {
if (element.getAnnotation(EpoxyAttribute.class) != null) {
AttributeInfo attributeInfo = buildAttributeInfo(element);
if (!belongToTheSamePackage(currentEpoxyModel, superclassEpoxyModel, elementUtils)
&& attributeInfo.isPackagePrivate()) {
// We can't inherit a package private attribute if we're not in the same package
continue;
}
// We add just the attribute info to the class in our module. We do NOT want to
// generate a class for the super class EpoxyModel in the other module since one
// will be created when that module is processed. If we make one as well there will
// be a duplicate (causes proguard errors and is just wrong).
generatedModelInfo.addAttribute(attributeInfo);
}
}
}
superclassType = superclassEpoxyModel.getSuperclass();
}
}
}
/**
* Check each model for super classes that also have attributes. For each super class with
* attributes we add those attributes to the attributes of the generated class, so that a
* generated class contains all the attributes of its super classes combined.
* <p>
* One caveat is that if a sub class is in a different package than its super class we can't
* include attributes that are package private, otherwise the generated class won't compile.
*/
private void updateClassesForInheritance(
Map<TypeElement, GeneratedModelInfo> helperClassMap) {
for (Entry<TypeElement, GeneratedModelInfo> entry : helperClassMap.entrySet()) {
TypeElement thisClass = entry.getKey();
Map<TypeElement, GeneratedModelInfo> otherClasses = new LinkedHashMap<>(helperClassMap);
otherClasses.remove(thisClass);
for (Entry<TypeElement, GeneratedModelInfo> otherEntry : otherClasses.entrySet()) {
TypeElement otherClass = otherEntry.getKey();
if (!isSubtype(thisClass, otherClass, typeUtils)) {
continue;
}
Set<AttributeInfo> otherAttributes = otherEntry.getValue().getAttributeInfo();
if (belongToTheSamePackage(thisClass, otherClass, elementUtils)) {
entry.getValue().addAttributes(otherAttributes);
} else {
for (AttributeInfo attribute : otherAttributes) {
if (!attribute.isPackagePrivate()) {
entry.getValue().addAttribute(attribute);
}
}
}
}
}
}
}