package com.airbnb.epoxy;
import com.squareup.javapoet.ClassName;
import com.sun.source.util.Trees;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.VarSymbol;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeScanner;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
/**
* Scans R files and and compiles layout resource values in those R classes. This allows us to look
* up raw layout resource values (eg 23523452) and convert that to the resource name (eg
* R.layout.my_view) so that we can properly reference that resource. This is important in library
* projects where the R value at process time can be different from the final R value in the app.
* <p>
* This is adapted from Butterknife. https://github.com/JakeWharton/butterknife/pull/613
*/
class LayoutResourceProcessor {
private final ErrorLogger errorLogger;
private final Elements elementUtils;
private final Types typeUtils;
private Trees trees;
private final Map<String, ClassName> rClassNameMap = new HashMap<>();
private final AnnotationLayoutParamScanner scanner = new AnnotationLayoutParamScanner();
LayoutResourceProcessor(ProcessingEnvironment processingEnv, ErrorLogger errorLogger,
Elements elementUtils, Types typeUtils) {
this.errorLogger = errorLogger;
this.elementUtils = elementUtils;
this.typeUtils = typeUtils;
try {
trees = Trees.instance(processingEnv);
} catch (IllegalArgumentException ignored) {
}
}
LayoutResource getLayoutInAnnotation(TypeElement element, Class annotationClass) {
List<LayoutResource> layouts = getLayoutsInAnnotation(element, annotationClass);
if (layouts.size() != 1) {
errorLogger.logError(
"Expected exactly 1 layout resource in the %s annotation but received %s. Annotated "
+ "element is %s",
annotationClass.getSimpleName(), layouts.size(), element.getSimpleName());
if (layouts.isEmpty()) {
// Just pass back something so the code can compile before the error logger prints
return new LayoutResource(0);
}
}
return layouts.get(0);
}
/**
* Get detailed information about the layout resources that are parameters to the given
* annotation.
*/
List<LayoutResource> getLayoutsInAnnotation(Element element, Class annotationClass) {
List<Integer> layoutValues = getLayoutValues(element, annotationClass);
List<LayoutResource> resources = new ArrayList<>(layoutValues.size());
JCTree tree = (JCTree) trees.getTree(element, getAnnotationMirror(element, annotationClass));
// tree can be null if the references are compiled types and not source
if (tree != null) {
// Collects details about the layout resource used for the annotation parameter
scanner.clearResults();
scanner.setCurrentAnnotationDetails(element, annotationClass);
tree.accept(scanner);
List<ScannerResult> scannerResults = scanner.getResults();
for (ScannerResult scannerResult : scannerResults) {
resources.add(new LayoutResource(
getClassName(scannerResult.rClass),
scannerResult.resourceName,
scannerResult.resourceValue
));
}
}
// Layout values may not have been picked up by the scanner if they are hardcoded.
// In that case we just use the hardcoded value without an R class
if (resources.size() != layoutValues.size()) {
for (int layoutValue : layoutValues) {
if (!isLayoutValueInResources(resources, layoutValue)) {
resources.add(new LayoutResource(layoutValue));
}
}
}
return resources;
}
private boolean isLayoutValueInResources(List<LayoutResource> resources, int layoutValue) {
for (LayoutResource resource : resources) {
if (resource.value == layoutValue) {
return true;
}
}
return false;
}
private static List<Integer> getLayoutValues(Element element, Class annotationClass) {
Annotation annotation = element.getAnnotation(annotationClass);
// We could do this in a more generic way if we ever need to support more annotation types
List<Integer> layoutResources = new ArrayList<>();
if (annotation instanceof EpoxyModelClass) {
layoutResources.add(((EpoxyModelClass) annotation).layout());
} else if (annotation instanceof EpoxyDataBindingLayouts) {
for (int layoutRes : ((EpoxyDataBindingLayouts) annotation).value()) {
layoutResources.add(layoutRes);
}
}
return layoutResources;
}
private AnnotationMirror getAnnotationMirror(Element element, Class annotationClass) {
for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) {
if (annotationMirror.getAnnotationType().toString()
.equals(annotationClass.getCanonicalName())) {
return annotationMirror;
}
}
errorLogger.logError("Unable to get %s annotation on model %",
annotationClass.getSimpleName(), element.getSimpleName());
return null;
}
List<ClassName> getRClassNames() {
return new ArrayList<>(rClassNameMap.values());
}
/**
* Scans annotations that have layout resources as parameters. It supports both one layout
* parameter, and parameters in an array. The R class, resource name, and value, is extract from
* each layout to create a corresponding {@link ScannerResult} for each layout.
*/
private class AnnotationLayoutParamScanner extends TreeScanner {
private final List<ScannerResult> results = new ArrayList<>();
private Element element;
private Class annotationClass;
void clearResults() {
results.clear();
}
List<ScannerResult> getResults() {
return new ArrayList<>(results);
}
@Override
public void visitSelect(JCTree.JCFieldAccess jcFieldAccess) {
// This "visit" method is called for each parameter in the annotation, but only if the
// parameter is a field type (eg R.layout.resource_name is a field inside the R.layout
// class). This means this method will not pick up things like booleans and strings.
// This is the layout resource parameter inside the EpoxyModelClass annotation
Symbol symbol = jcFieldAccess.sym;
if (symbol instanceof VarSymbol
&& symbol.getEnclosingElement() != null // The R.layout class
&& symbol.getEnclosingElement().getEnclosingElement() != null // The R class
&& symbol.getEnclosingElement().getEnclosingElement().enclClass() != null) {
ScannerResult result = parseResourceSymbol((VarSymbol) symbol);
if (result != null) {
results.add(result);
}
}
}
private ScannerResult parseResourceSymbol(VarSymbol symbol) {
// eg com.airbnb.epoxy.R
String rClass = symbol.getEnclosingElement().getEnclosingElement().enclClass().className();
// eg com.airbnb.epoxy.R.layout
String layoutClass = symbol.getEnclosingElement().getQualifiedName().toString();
// Make sure this is a layout resource
if (!(rClass + ".layout").equals(layoutClass)) {
errorLogger
.logError("%s annotation requires a layout resource but received %s. (Element: %s)",
annotationClass.getSimpleName(), layoutClass, element.getSimpleName());
return null;
}
// eg button_layout, as in R.layout.button_layout
String layoutResourceName = symbol.getSimpleName().toString();
Object layoutValue = symbol.getConstantValue();
if (!(layoutValue instanceof Integer)) {
errorLogger.logError("%s annotation requires an int value but received %s. (Element: %s)",
annotationClass.getSimpleName(), symbol.getQualifiedName(), element.getSimpleName());
return null;
}
return new ScannerResult(rClass, layoutResourceName, (int) layoutValue);
}
void setCurrentAnnotationDetails(Element element, Class annotationClass) {
this.element = element;
this.annotationClass = annotationClass;
}
}
private static class ScannerResult {
final String rClass;
final String resourceName;
final int resourceValue;
private ScannerResult(String rClass, String resourceName, int resourceValue) {
this.rClass = rClass;
this.resourceName = resourceName;
this.resourceValue = resourceValue;
}
}
/**
* Builds a JavaPoet ClassName from the string value of an R class. This is memoized since there
* should be very few different R classes used.
*/
private ClassName getClassName(String rClass) {
ClassName className = rClassNameMap.get(rClass);
if (className == null) {
Element rClassElement = Utils.getElementByName(rClass, elementUtils, typeUtils);
String rClassPackageName =
elementUtils.getPackageOf(rClassElement).getQualifiedName().toString();
className = ClassName.get(rClassPackageName, "R", "layout");
rClassNameMap.put(rClass, className);
}
return className;
}
}