package com.airbnb.epoxy.processor; import com.airbnb.epoxy.EpoxyAttribute; import com.airbnb.epoxy.EpoxyAttribute.Option; import com.squareup.javapoet.AnnotationSpec; import com.squareup.javapoet.ClassName; import java.lang.annotation.ElementType; import java.lang.annotation.Target; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; import static com.airbnb.epoxy.processor.Utils.capitalizeFirstLetter; import static com.airbnb.epoxy.processor.Utils.isFieldPackagePrivate; import static com.airbnb.epoxy.processor.Utils.startsWithIs; import static javax.lang.model.element.Modifier.FINAL; import static javax.lang.model.element.Modifier.PRIVATE; import static javax.lang.model.element.Modifier.STATIC; class BaseModelAttributeInfo extends AttributeInfo { private final TypeElement classElement; protected Types typeUtils; BaseModelAttributeInfo(Element attribute, Types typeUtils, Elements elements, Logger logger, Memoizer memoizer) { this.typeUtils = typeUtils; this.setFieldName(attribute.getSimpleName().toString()); setTypeMirror(attribute.asType(), memoizer); setJavaDocString(elements.getDocComment(attribute)); classElement = (TypeElement) attribute.getEnclosingElement(); setRootClass(classElement.getSimpleName().toString()); setPackageName(elements.getPackageOf(classElement).getQualifiedName().toString()); this.setHasSuperSetter(hasSuperMethod(classElement, attribute)); this.setHasFinalModifier(attribute.getModifiers().contains(FINAL)); this.setPackagePrivate(isFieldPackagePrivate(attribute)); EpoxyAttribute annotation = attribute.getAnnotation(EpoxyAttribute.class); Set<Option> options = new HashSet<>(Arrays.asList(annotation.value())); validateAnnotationOptions(logger, annotation, options); //noinspection deprecation setUseInHash(annotation.hash() && !options.contains(Option.DoNotHash)); setIgnoreRequireHashCode(options.contains(Option.IgnoreRequireHashCode)); setDoNotUseInToString(options.contains(Option.DoNotUseInToString)); //noinspection deprecation setGenerateSetter(annotation.setter() && !options.contains(Option.NoSetter)); setGenerateGetter(!options.contains(Option.NoGetter)); setPrivate(attribute.getModifiers().contains(PRIVATE)); if (isPrivate()) { findGetterAndSetterForPrivateField(logger); } buildAnnotationLists(attribute.getAnnotationMirrors()); } /** * Check if the given class or any of its super classes have a super method with the given name. * Private methods are ignored since the generated subclass can't call super on those. */ protected boolean hasSuperMethod(TypeElement classElement, Element attribute) { if (!Utils.isEpoxyModel(classElement.asType())) { return false; } for (Element subElement : SynchronizationKt.getEnclosedElementsThreadSafe(classElement)) { if (subElement.getKind() == ElementKind.METHOD) { ExecutableElement method = (ExecutableElement) subElement; List<VariableElement> parameters = SynchronizationKt.getParametersThreadSafe(method); if (!method.getModifiers().contains(Modifier.PRIVATE) && method.getSimpleName().toString().equals(attribute.getSimpleName().toString()) && parameters.size() == 1 && parameters.get(0).asType().equals(attribute.asType())) { return true; } } } Element superClass = KotlinUtilsKt.superClassElement(classElement, typeUtils); return (superClass instanceof TypeElement) && hasSuperMethod((TypeElement) superClass, attribute); } private void validateAnnotationOptions(Logger logger, EpoxyAttribute annotation, Set<Option> options) { if (options.contains(Option.IgnoreRequireHashCode) && options.contains(Option.DoNotHash)) { logger.logError("Illegal to use both %s and %s options in an %s annotation. (%s#%s)", Option.DoNotHash, Option.IgnoreRequireHashCode, EpoxyAttribute.class.getSimpleName(), classElement.getSimpleName(), getFieldName()); } // Don't let legacy values be mixed with the new Options values if (!options.isEmpty()) { if (!annotation.hash()) { logger.logError("Don't use hash=false in an %s if you are using options. Instead, use the" + " %s option. (%s#%s)", EpoxyAttribute.class.getSimpleName(), Option.DoNotHash, classElement.getSimpleName(), getFieldName()); } if (!annotation.setter()) { logger.logError("Don't use setter=false in an %s if you are using options. Instead, use the" + " %s option. (%s#%s)", EpoxyAttribute.class.getSimpleName(), Option.NoSetter, classElement.getSimpleName(), getFieldName()); } } } /** * Checks if the given private field has getter and setter for access to it */ private void findGetterAndSetterForPrivateField(Logger logger) { for (Element element : SynchronizationKt.getEnclosedElementsThreadSafe(classElement)) { if (element.getKind() == ElementKind.METHOD) { ExecutableElement method = (ExecutableElement) element; String methodName = method.getSimpleName().toString(); List<VariableElement> parameters = SynchronizationKt.getParametersThreadSafe(method); // check if it is a valid getter if ((methodName.equals(String.format("get%s", capitalizeFirstLetter(getFieldName()))) || methodName.equals(String.format("is%s", capitalizeFirstLetter(getFieldName()))) || (methodName.equals(getFieldName()) && startsWithIs(getFieldName()))) && !method.getModifiers().contains(PRIVATE) && !method.getModifiers().contains(STATIC) && parameters.isEmpty()) { setGetterMethodName(methodName); } // check if it is a valid setter if ((methodName.equals(String.format("set%s", capitalizeFirstLetter(getFieldName()))) || (startsWithIs(getFieldName()) && methodName.equals(String.format("set%s", getFieldName().substring(2, getFieldName().length()))))) && !method.getModifiers().contains(PRIVATE) && !method.getModifiers().contains(STATIC) && parameters.size() == 1) { setSetterMethodName(methodName); } } } if (getGetterMethodName() == null || getSetterMethodName() == null) { // We disable the "private" field setting so that we can still generate // some code that compiles in an ok manner (ie via direct field access) setPrivate(false); logger .logError("%s annotations must not be on private fields" + " without proper getter and setter methods. (class: %s, field: %s)", EpoxyAttribute.class.getSimpleName(), classElement.getSimpleName(), getFieldName()); } } /** * Keeps track of annotations on the attribute so that they can be used in the generated setter * and getter method. Setter and getter annotations are stored separately since the annotation may * not target both method and parameter types. */ private void buildAnnotationLists(List<? extends AnnotationMirror> annotationMirrors) { for (AnnotationMirror annotationMirror : annotationMirrors) { if (!annotationMirror.getElementValues().isEmpty()) { // Not supporting annotations with values for now continue; } ClassName annotationClass = ClassName.bestGuess(annotationMirror.getAnnotationType().toString()); if (annotationClass.equals(ClassName.get(EpoxyAttribute.class))) { // Don't include our own annotation continue; } DeclaredType annotationType = annotationMirror.getAnnotationType(); // A target may exist on an annotation type to specify where the annotation can // be used, for example fields, methods, or parameters. Target targetAnnotation = annotationType.asElement().getAnnotation(Target.class); // Allow all target types if no target was specified on the annotation List<ElementType> elementTypes = Arrays.asList(targetAnnotation == null ? ElementType.values() : targetAnnotation.value()); AnnotationSpec annotationSpec = AnnotationSpec.builder(annotationClass).build(); if (elementTypes.contains(ElementType.PARAMETER)) { getSetterAnnotations().add(annotationSpec); } if (elementTypes.contains(ElementType.METHOD)) { getGetterAnnotations().add(annotationSpec); } } } }