Skip to content

Commit

Permalink
Generate model for abstract class if class annotation is present
Browse files Browse the repository at this point in the history
  • Loading branch information
elihart committed Feb 5, 2017
1 parent cbfd18b commit d96c44c
Show file tree
Hide file tree
Showing 11 changed files with 538 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import java.lang.annotation.Target;

/**
* Used to annotate {@link EpoxyModel} classes in order to generate a
* Used to annotate EpoxyModel classes in order to generate a
* subclass of that model with getters, setters, equals, and hashcode for the annotated fields.
*/
@Target(ElementType.TYPE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,35 +29,40 @@
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;

public class ClassToGenerateInfo {

private static final String GENERATED_CLASS_NAME_SUFFIX = "_";
private static final String RESET_METHOD = "reset";

private final Elements elementUtils;
private final TypeName originalClassName;
private final TypeName originalClassNameWithoutType;
private final TypeElement originalClassElement;
private final TypeName parameterizedClassName;
private final ClassName generatedClassName;
private final boolean isOriginalClassAbstract;
private final boolean shouldGenerateSubClass;
private final Set<AttributeInfo> attributeInfo = new HashSet<>();
private final List<TypeVariableName> typeVariableNames = new ArrayList<>();
private final List<ConstructorInfo> constructors = new ArrayList<>();
private final Set<MethodInfo> methodsReturningClassType = new LinkedHashSet<>();
private final Types typeUtils;

public ClassToGenerateInfo(Types typeUtils, TypeElement originalClassName,
ClassName generatedClassName, boolean isOriginalClassAbstract) {
ClassToGenerateInfo(Types typeUtils, Elements elementUtils, TypeElement originalClassElement) {
this.typeUtils = typeUtils;
this.originalClassName = ParameterizedTypeName.get(originalClassName.asType());
this.originalClassNameWithoutType = ClassName.get(originalClassName);
this.elementUtils = elementUtils;
this.originalClassName = ParameterizedTypeName.get(originalClassElement.asType());
this.originalClassNameWithoutType = ClassName.get(originalClassElement);
this.originalClassElement = originalClassElement;
ClassName generatedClassName = getGeneratedClassName(originalClassElement);

for (TypeParameterElement typeParameterElement : originalClassName.getTypeParameters()) {
for (TypeParameterElement typeParameterElement : originalClassElement.getTypeParameters()) {
typeVariableNames.add(TypeVariableName.get(typeParameterElement));
}

collectOriginalClassConstructors(originalClassName);
collectMethodsReturningClassType(originalClassName);
collectOriginalClassConstructors(originalClassElement);
collectMethodsReturningClassType(originalClassElement);

if (!typeVariableNames.isEmpty()) {
TypeVariableName[] typeArguments =
Expand All @@ -68,12 +73,32 @@ public ClassToGenerateInfo(Types typeUtils, TypeElement originalClassName,
}

this.generatedClassName = generatedClassName;
this.isOriginalClassAbstract = isOriginalClassAbstract;
this.shouldGenerateSubClass = shouldGenerateSubclass(originalClassElement);
}

private ClassName getGeneratedClassName(TypeElement classElement) {
String packageName = elementUtils.getPackageOf(classElement).getQualifiedName().toString();

int packageLen = packageName.length() + 1;
String className =
classElement.getQualifiedName().toString().substring(packageLen).replace('.', '$');

return ClassName.get(packageName, className + GENERATED_CLASS_NAME_SUFFIX);
}

private boolean shouldGenerateSubclass(TypeElement classElement) {
boolean hasEpoxyClassAnnotation = classElement.getAnnotation(EpoxyModelClass.class) != null;
boolean isAbstract = classElement.getModifiers().contains(Modifier.ABSTRACT);

// By default we don't extend classes that are abstract; if they don't contain all required
// methods then our generated class won't compile. If there is a EpoxyModelClass annotation
// though we will always generate the subclass
return !isAbstract || hasEpoxyClassAnnotation;
}

/**
* Get information about constructors of the original class so we can duplicate
them in the generated class and call through to super with the proper parameters
* Get information about constructors of the original class so we can duplicate them in the
* generated class and call through to super with the proper parameters
*/
private void collectOriginalClassConstructors(TypeElement originalClass) {
for (Element subElement : originalClass.getEnclosedElements()) {
Expand All @@ -89,8 +114,8 @@ private void collectOriginalClassConstructors(TypeElement originalClass) {
}

/**
* Get information about methods returning class type of the original class so we can
* duplicate them in the generated class for chaining purposes
* Get information about methods returning class type of the original class so we can duplicate
* them in the generated class for chaining purposes
*/
private void collectMethodsReturningClassType(TypeElement originalClass) {
TypeElement clazz = originalClass;
Expand Down Expand Up @@ -143,6 +168,10 @@ public void addAttributes(Collection<AttributeInfo> attributeInfo) {
this.attributeInfo.addAll(attributeInfo);
}

public TypeElement getOriginalClassElement() {
return originalClassElement;
}

public TypeName getOriginalClassName() {
return originalClassName;
}
Expand All @@ -167,8 +196,8 @@ public Set<AttributeInfo> getAttributeInfo() {
return attributeInfo;
}

public boolean isOriginalClassAbstract() {
return isOriginalClassAbstract;
public boolean shouldGenerateSubClass() {
return shouldGenerateSubClass;
}

public Iterable<TypeVariableName> getTypeVariables() {
Expand Down
120 changes: 51 additions & 69 deletions epoxy-processor/src/main/java/com/airbnb/epoxy/EpoxyProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableSet;
import com.squareup.javapoet.ArrayTypeName;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.MethodSpec.Builder;
Expand Down Expand Up @@ -35,13 +34,16 @@
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;

import static com.airbnb.epoxy.ProcessorUtils.EPOXY_MODEL_TYPE;
import static com.airbnb.epoxy.ProcessorUtils.getEpoxyObjectType;
import static com.airbnb.epoxy.ProcessorUtils.implementsMethod;
import static com.airbnb.epoxy.ProcessorUtils.isEpoxyModel;
import static com.airbnb.epoxy.ProcessorUtils.isEpoxyModelWithHolder;
import static com.squareup.javapoet.TypeName.BOOLEAN;
import static com.squareup.javapoet.TypeName.BYTE;
import static com.squareup.javapoet.TypeName.CHAR;
Expand All @@ -66,9 +68,8 @@
*/
@AutoService(Processor.class)
public class EpoxyProcessor extends AbstractProcessor {
private static final String GENERATED_CLASS_NAME_SUFFIX = "_";
private static final String EPOXY_MODEL_TYPE = "com.airbnb.epoxy.EpoxyModel<?>";

public static final String CREATE_NEW_HOLDER_METHOD_NAME = "createNewHolder";
private Filer filer;
private Messager messager;
private Elements elementUtils;
Expand Down Expand Up @@ -115,7 +116,7 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
for (Entry<TypeElement, ClassToGenerateInfo> modelEntry : modelClassMap.entrySet()) {
try {
generateClassForModel(modelEntry.getValue());
} catch (IOException e) {
} catch (IOException | EpoxyProcessorException e) {
writeError(e);
}
}
Expand Down Expand Up @@ -233,26 +234,13 @@ private ClassToGenerateInfo getOrCreateTargetClass(
}

if (classToGenerateInfo == null) {
ClassName generatedClassName = getGeneratedClassName(classElement);
boolean isAbstract = classElement.getModifiers().contains(Modifier.ABSTRACT);
classToGenerateInfo = new ClassToGenerateInfo(typeUtils, classElement, generatedClassName,
isAbstract);
classToGenerateInfo = new ClassToGenerateInfo(typeUtils, elementUtils, classElement);
modelClassMap.put(classElement, classToGenerateInfo);
}

return classToGenerateInfo;
}

private ClassName getGeneratedClassName(TypeElement classElement) {
String packageName = elementUtils.getPackageOf(classElement).getQualifiedName().toString();

int packageLen = packageName.length() + 1;
String className =
classElement.getQualifiedName().toString().substring(packageLen).replace('.', '$');

return ClassName.get(packageName, className + GENERATED_CLASS_NAME_SUFFIX);
}

/**
* 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
Expand Down Expand Up @@ -343,54 +331,9 @@ private boolean isSubtype(TypeMirror e1, TypeMirror e2) {
return typeUtils.isSubtype(e1, typeUtils.erasure(e2));
}

private boolean isEpoxyModel(TypeMirror type) {
return isSubtypeOfType(type, EPOXY_MODEL_TYPE);
}

private boolean isSubtypeOfType(TypeMirror typeMirror, String otherType) {
if (otherType.equals(typeMirror.toString())) {
return true;
}
if (typeMirror.getKind() != TypeKind.DECLARED) {
return false;
}
DeclaredType declaredType = (DeclaredType) typeMirror;
List<? extends TypeMirror> typeArguments = declaredType.getTypeArguments();
if (typeArguments.size() > 0) {
StringBuilder typeString = new StringBuilder(declaredType.asElement().toString());
typeString.append('<');
for (int i = 0; i < typeArguments.size(); i++) {
if (i > 0) {
typeString.append(',');
}
typeString.append('?');
}
typeString.append('>');
if (typeString.toString().equals(otherType)) {
return true;
}
}
Element element = declaredType.asElement();
if (!(element instanceof TypeElement)) {
return false;
}
TypeElement typeElement = (TypeElement) element;
TypeMirror superType = typeElement.getSuperclass();
if (isSubtypeOfType(superType, otherType)) {
return true;
}
for (TypeMirror interfaceType : typeElement.getInterfaces()) {
if (isSubtypeOfType(interfaceType, otherType)) {
return true;
}
}
return false;
}

private void generateClassForModel(ClassToGenerateInfo info) throws IOException {
if (info.isOriginalClassAbstract()) {
// Don't extend classes that are abstract. If they don't contain all required
// methods then our generated class won't compile
private void generateClassForModel(ClassToGenerateInfo info)
throws IOException, EpoxyProcessorException {
if (!info.shouldGenerateSubClass()) {
return;
}

Expand All @@ -402,6 +345,7 @@ private void generateClassForModel(ClassToGenerateInfo info) throws IOException
.addMethods(generateConstructors(info))
.addMethods(generateSettersAndGetters(info))
.addMethods(generateMethodsReturningClassType(info))
.addMethods(generateDefaultMethodImplementations(info))
.addMethod(generateReset(info))
.addMethod(generateEquals(info))
.addMethod(generateHashCode(info))
Expand Down Expand Up @@ -458,6 +402,44 @@ private Iterable<MethodSpec> generateMethodsReturningClassType(ClassToGenerateIn
return methods;
}

/**
* Generates default implementations of certain model methods if the model is abstract and doesn't
* implement them.
*/
private Iterable<MethodSpec> generateDefaultMethodImplementations(ClassToGenerateInfo info)
throws EpoxyProcessorException {
List<MethodSpec> methods = new ArrayList<>();

// If the model is a holder and doesn't implement the "createNewHolder" method we can
// generate a default implementation by getting the class type and creating a new instance
// of it.
TypeElement originalClassElement = info.getOriginalClassElement();
if (isEpoxyModelWithHolder(originalClassElement)) {

MethodSpec createHolderMethod = MethodSpec.methodBuilder(CREATE_NEW_HOLDER_METHOD_NAME)
.addAnnotation(Override.class)
.addModifiers(Modifier.PROTECTED)
.build();

if (!implementsMethod(originalClassElement, createHolderMethod, typeUtils)) {
TypeMirror epoxyObjectType = getEpoxyObjectType(originalClassElement, typeUtils);

if (epoxyObjectType == null) {
throwError("Return type for createNewHolder method could not be found");
}

createHolderMethod = createHolderMethod.toBuilder()
.returns(TypeName.get(epoxyObjectType))
.addStatement("return new $T()", epoxyObjectType)
.build();

methods.add(createHolderMethod);
}
}

return methods;
}

private void generateParams(StringBuilder statementBuilder, List<ParameterSpec> params) {
boolean first = true;
for (ParameterSpec param : params) {
Expand Down Expand Up @@ -537,7 +519,7 @@ private MethodSpec generateEquals(ClassToGenerateInfo helperClass) {
}
} else {
builder.beginControlFlow("if ($L != null && that.$L == null"
+ " || $L == null && that.$L != null)",
+ " || $L == null && that.$L != null)",
name, name, name, name)
.addStatement("return false")
.endControlFlow();
Expand Down
Loading

0 comments on commit d96c44c

Please sign in to comment.