package com.airbnb.epoxy;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import com.squareup.javapoet.ArrayTypeName;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.MethodSpec.Builder;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import java.io.IOException;
import java.lang.annotation.AnnotationTypeMismatchException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.annotation.processing.Filer;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Types;
import static com.airbnb.epoxy.Utils.EPOXY_CONTROLLER_TYPE;
import static com.airbnb.epoxy.Utils.EPOXY_VIEW_HOLDER_TYPE;
import static com.airbnb.epoxy.Utils.GENERATED_MODEL_INTERFACE;
import static com.airbnb.epoxy.Utils.MODEL_CLICK_LISTENER_TYPE;
import static com.airbnb.epoxy.Utils.ON_BIND_MODEL_LISTENER_TYPE;
import static com.airbnb.epoxy.Utils.ON_UNBIND_MODEL_LISTENER_TYPE;
import static com.airbnb.epoxy.Utils.UNTYPED_EPOXY_MODEL_TYPE;
import static com.airbnb.epoxy.Utils.WRAPPED_LISTENER_TYPE;
import static com.airbnb.epoxy.Utils.getClassName;
import static com.airbnb.epoxy.Utils.implementsMethod;
import static com.airbnb.epoxy.Utils.isDataBindingModel;
import static com.airbnb.epoxy.Utils.isEpoxyModel;
import static com.airbnb.epoxy.Utils.isEpoxyModelWithHolder;
import static com.squareup.javapoet.TypeName.BOOLEAN;
import static com.squareup.javapoet.TypeName.BYTE;
import static com.squareup.javapoet.TypeName.CHAR;
import static com.squareup.javapoet.TypeName.DOUBLE;
import static com.squareup.javapoet.TypeName.FLOAT;
import static com.squareup.javapoet.TypeName.INT;
import static com.squareup.javapoet.TypeName.LONG;
import static com.squareup.javapoet.TypeName.SHORT;
import static javax.lang.model.element.Modifier.FINAL;
import static javax.lang.model.element.Modifier.PRIVATE;
import static javax.lang.model.element.Modifier.PUBLIC;
class GeneratedModelWriter {
/**
* Use this suffix on helper fields added to the generated class so that we don't clash with
* fields on the original model.
*/
static final String GENERATED_FIELD_SUFFIX = "_epoxyGeneratedModel";
private static final String CREATE_NEW_HOLDER_METHOD_NAME = "createNewHolder";
private static final String GET_DEFAULT_LAYOUT_METHOD_NAME = "getDefaultLayout";
private final Filer filer;
private final Types typeUtils;
private final ErrorLogger errorLogger;
private final LayoutResourceProcessor layoutResourceProcessor;
private final ConfigManager configManager;
private final DataBindingModuleLookup dataBindingModuleLookup;
interface BeforeBuildCallback {
void modifyBuilder(TypeSpec.Builder builder);
}
GeneratedModelWriter(Filer filer, Types typeUtils, ErrorLogger errorLogger,
LayoutResourceProcessor layoutResourceProcessor, ConfigManager configManager,
DataBindingModuleLookup dataBindingModuleLookup) {
this.filer = filer;
this.typeUtils = typeUtils;
this.errorLogger = errorLogger;
this.layoutResourceProcessor = layoutResourceProcessor;
this.configManager = configManager;
this.dataBindingModuleLookup = dataBindingModuleLookup;
}
void generateClassForModel(GeneratedModelInfo info) throws IOException {
generateClassForModel(info, null);
}
void generateClassForModel(GeneratedModelInfo info, BeforeBuildCallback beforeBuildCallback)
throws IOException {
if (!info.shouldGenerateModel()) {
return;
}
TypeSpec.Builder builder = TypeSpec.classBuilder(info.getGeneratedName())
.addJavadoc("Generated file. Do not modify!")
.addModifiers(PUBLIC)
.superclass(info.getSuperClassName())
.addSuperinterface(getGeneratedModelInterface(info))
.addTypeVariables(info.getTypeVariables())
.addFields(generateFields(info))
.addMethods(generateConstructors(info));
generateDebugAddToMethodIfNeeded(builder);
builder
.addMethods(generateBindMethods(info))
.addMethods(generateSettersAndGetters(info))
.addMethods(generateMethodsReturningClassType(info))
.addMethods(generateDefaultMethodImplementations(info))
.addMethods(generateDataBindingMethodsIfNeeded(info))
.addMethod(generateReset(info))
.addMethod(generateEquals(info))
.addMethod(generateHashCode(info))
.addMethod(generateToString(info));
if (beforeBuildCallback != null) {
beforeBuildCallback.modifyBuilder(builder);
}
JavaFile.builder(info.getGeneratedName().packageName(), builder.build())
.build()
.writeTo(filer);
}
@NonNull
private ParameterizedTypeName getGeneratedModelInterface(GeneratedModelInfo info) {
return ParameterizedTypeName.get(
getClassName(GENERATED_MODEL_INTERFACE),
info.getModelType()
);
}
private Iterable<FieldSpec> generateFields(GeneratedModelInfo classInfo) {
List<FieldSpec> fields = new ArrayList<>();
// Add fields for the bind/unbind listeners
ParameterizedTypeName onBindListenerType = ParameterizedTypeName.get(
getClassName(ON_BIND_MODEL_LISTENER_TYPE),
classInfo.getParameterizedGeneratedName(),
classInfo.getModelType()
);
fields
.add(FieldSpec.builder(onBindListenerType, modelBindListenerFieldName(), PRIVATE).build());
ParameterizedTypeName onUnbindListenerType = ParameterizedTypeName.get(
getClassName(ON_UNBIND_MODEL_LISTENER_TYPE),
classInfo.getParameterizedGeneratedName(),
classInfo.getModelType()
);
fields.add(
FieldSpec.builder(onUnbindListenerType, modelUnbindListenerFieldName(), PRIVATE).build());
for (AttributeInfo attributeInfo : classInfo.getAttributeInfo()) {
if (attributeInfo.isGenerated) {
fields.add(FieldSpec.builder(
attributeInfo.getTypeName(),
attributeInfo.name,
PRIVATE
).build()
);
}
if (attributeInfo.isViewClickListener()) {
// Create our own field to store a model click listener. We will later wrap the model click
// listener in a view click listener to set on the original model's View.OnClickListener
// field
fields.add(FieldSpec.builder(
getModelClickListenerType(classInfo),
attributeInfo.getModelClickListenerName(),
PRIVATE
).build()
);
}
}
return fields;
}
@NonNull
private String modelUnbindListenerFieldName() {
return "onModelUnboundListener" + GENERATED_FIELD_SUFFIX;
}
@NonNull
private String modelBindListenerFieldName() {
return "onModelBoundListener" + GENERATED_FIELD_SUFFIX;
}
private ParameterizedTypeName getModelClickListenerType(GeneratedModelInfo classInfo) {
return ParameterizedTypeName.get(
getClassName(MODEL_CLICK_LISTENER_TYPE),
classInfo.getParameterizedGeneratedName(),
classInfo.getModelType());
}
/** Include any constructors that are in the super class. */
private Iterable<MethodSpec> generateConstructors(GeneratedModelInfo info) {
List<MethodSpec> constructors = new ArrayList<>(info.getConstructors().size());
for (GeneratedModelInfo.ConstructorInfo constructorInfo : info.getConstructors()) {
Builder builder = MethodSpec.constructorBuilder()
.addModifiers(constructorInfo.modifiers)
.addParameters(constructorInfo.params)
.varargs(constructorInfo.varargs);
StringBuilder statementBuilder = new StringBuilder("super(");
generateParams(statementBuilder, constructorInfo.params);
constructors.add(builder
.addStatement(statementBuilder.toString())
.build());
}
return constructors;
}
private void generateDebugAddToMethodIfNeeded(TypeSpec.Builder classBuilder) {
if (!configManager.shouldValidateModelUsage()) {
return;
}
MethodSpec addToMethod = MethodSpec.methodBuilder("addTo")
.addParameter(getClassName(EPOXY_CONTROLLER_TYPE), "controller")
.addAnnotation(Override.class)
.addModifiers(PUBLIC)
.addStatement("super.addTo(controller)")
.addStatement("addWithDebugValidation(controller)")
.build();
classBuilder.addMethod(addToMethod);
}
private Iterable<MethodSpec> generateBindMethods(GeneratedModelInfo classInfo) {
List<MethodSpec> methods = new ArrayList<>();
// Add bind/unbind methods so the class can set the epoxyModelBoundObject and
// boundEpoxyViewHolder fields for the model click listener to access
TypeName viewHolderType = getClassName(EPOXY_VIEW_HOLDER_TYPE);
ParameterSpec viewHolderParam = ParameterSpec.builder(viewHolderType, "holder", FINAL).build();
ParameterSpec boundObjectParam =
ParameterSpec.builder(classInfo.getModelType(), "object", FINAL).build();
Builder preBindBuilder = MethodSpec.methodBuilder("handlePreBind")
.addModifiers(PUBLIC)
.addAnnotation(Override.class)
.addParameter(viewHolderParam)
.addParameter(boundObjectParam)
.addParameter(TypeName.INT, "position");
addHashCodeValidationIfNecessary(preBindBuilder,
"The model was changed between being added to the controller and being bound.");
ClassName viewType = getClassName("android.view.View");
ClassName clickWrapperType = getClassName(WRAPPED_LISTENER_TYPE);
ClassName modelClickListenerType = getClassName(MODEL_CLICK_LISTENER_TYPE);
for (AttributeInfo attribute : classInfo.getAttributeInfo()) {
if (!attribute.isViewClickListener()) {
continue;
}
// Generate a View.OnClickListener that wraps the model click listener and set it as the
// view click listener of the original model.
String modelClickListenerField = attribute.getModelClickListenerName();
preBindBuilder.beginControlFlow("if ($L != null)", modelClickListenerField);
CodeBlock clickListenerCodeBlock = CodeBlock.of(
"new $T($L) {\n"
+ " @Override\n"
+ " protected void wrappedOnClick($T v, $T originalClickListener) {\n"
+ " originalClickListener.onClick($L.this, object, v,\n"
+ " holder.getAdapterPosition());\n"
+ " }\n"
+ " }",
clickWrapperType, modelClickListenerField, viewType, modelClickListenerType,
classInfo.getGeneratedName());
preBindBuilder
.addStatement(attribute.setterCode(), clickListenerCodeBlock)
.endControlFlow();
}
methods.add(preBindBuilder.build());
Builder postBindBuilder = MethodSpec.methodBuilder("handlePostBind")
.addModifiers(PUBLIC)
.addAnnotation(Override.class)
.addParameter(boundObjectParam)
.addParameter(TypeName.INT, "position")
.beginControlFlow("if ($L != null)", modelBindListenerFieldName())
.addStatement("$L.onModelBound(this, object, position)", modelBindListenerFieldName())
.endControlFlow();
addHashCodeValidationIfNecessary(postBindBuilder,
"The model was changed during the bind call.");
methods.add(postBindBuilder
.build());
ParameterizedTypeName onBindListenerType = ParameterizedTypeName.get(
getClassName(ON_BIND_MODEL_LISTENER_TYPE),
classInfo.getParameterizedGeneratedName(),
classInfo.getModelType()
);
ParameterSpec bindListenerParam = ParameterSpec.builder(onBindListenerType, "listener").build();
MethodSpec.Builder onBind = MethodSpec.methodBuilder("onBind")
.addJavadoc("Register a listener that will be called when this model is bound to a view.\n"
+ "<p>\n"
+ "The listener will contribute to this model's hashCode state per the {@link\n"
+ "com.airbnb.epoxy.EpoxyAttribute.Option#DoNotHash} rules.\n"
+ "<p>\n"
+ "You may clear the listener by setting a null value, or by calling "
+ "{@link #reset()}")
.addModifiers(PUBLIC)
.returns(classInfo.getParameterizedGeneratedName())
.addParameter(bindListenerParam);
addOnMutationCall(onBind)
.addStatement("this.$L = listener", modelBindListenerFieldName())
.addStatement("return this")
.build();
methods.add(onBind.build());
ParameterSpec unbindObjectParam =
ParameterSpec.builder(classInfo.getModelType(), "object").build();
Builder unbindBuilder = MethodSpec.methodBuilder("unbind")
.addAnnotation(Override.class)
.addModifiers(PUBLIC)
.addParameter(unbindObjectParam);
unbindBuilder
.addStatement("super.unbind(object)")
.beginControlFlow("if ($L != null)", modelUnbindListenerFieldName())
.addStatement("$L.onModelUnbound(this, object)", modelUnbindListenerFieldName())
.endControlFlow();
methods.add(unbindBuilder
.build());
ParameterizedTypeName onUnbindListenerType = ParameterizedTypeName.get(
getClassName(ON_UNBIND_MODEL_LISTENER_TYPE),
classInfo.getParameterizedGeneratedName(),
classInfo.getModelType()
);
ParameterSpec unbindListenerParam =
ParameterSpec.builder(onUnbindListenerType, "listener").build();
Builder onUnbind = MethodSpec.methodBuilder("onUnbind")
.addJavadoc("Register a listener that will be called when this model is unbound from a "
+ "view.\n"
+ "<p>\n"
+ "The listener will contribute to this model's hashCode state per the {@link\n"
+ "com.airbnb.epoxy.EpoxyAttribute.Option#DoNotHash} rules.\n"
+ "<p>\n"
+ "You may clear the listener by setting a null value, or by calling "
+ "{@link #reset()}")
.addModifiers(PUBLIC)
.returns(classInfo.getParameterizedGeneratedName());
addOnMutationCall(onUnbind)
.addParameter(unbindListenerParam)
.addStatement("this.$L = listener", modelUnbindListenerFieldName())
.addStatement("return this");
methods.add(onUnbind.build());
return methods;
}
private Iterable<MethodSpec> generateMethodsReturningClassType(GeneratedModelInfo info) {
List<MethodSpec> methods = new ArrayList<>(info.getMethodsReturningClassType().size());
for (GeneratedModelInfo.MethodInfo methodInfo : info.getMethodsReturningClassType()) {
Builder builder = MethodSpec.methodBuilder(methodInfo.name)
.addModifiers(methodInfo.modifiers)
.addParameters(methodInfo.params)
.addAnnotation(Override.class)
.varargs(methodInfo.varargs)
.returns(info.getParameterizedGeneratedName());
StringBuilder statementBuilder = new StringBuilder(String.format("super.%s(",
methodInfo.name));
generateParams(statementBuilder, methodInfo.params);
methods.add(builder
.addStatement(statementBuilder.toString())
.addStatement("return this")
.build());
}
return methods;
}
/**
* Generates default implementations of certain model methods if the model is abstract and doesn't
* implement them.
*/
private Iterable<MethodSpec> generateDefaultMethodImplementations(GeneratedModelInfo info) {
List<MethodSpec> methods = new ArrayList<>();
addCreateHolderMethodIfNeeded(info, methods);
addDefaultLayoutMethodIfNeeded(info, methods);
return methods;
}
/**
* 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.
*/
private void addCreateHolderMethodIfNeeded(GeneratedModelInfo modelClassInfo,
List<MethodSpec> methods) {
TypeElement originalClassElement = modelClassInfo.getSuperClassElement();
if (!isEpoxyModelWithHolder(originalClassElement)) {
return;
}
MethodSpec createHolderMethod = MethodSpec.methodBuilder(
CREATE_NEW_HOLDER_METHOD_NAME)
.addAnnotation(Override.class)
.addModifiers(Modifier.PROTECTED)
.build();
if (implementsMethod(originalClassElement, createHolderMethod, typeUtils)) {
return;
}
createHolderMethod = createHolderMethod.toBuilder()
.returns(modelClassInfo.getModelType())
.addStatement("return new $T()", modelClassInfo.getModelType())
.build();
methods.add(createHolderMethod);
}
/**
* If there is no existing implementation of getDefaultLayout we can generate an implementation.
* This relies on a layout res being set in the @EpoxyModelClass annotation.
*/
private void addDefaultLayoutMethodIfNeeded(GeneratedModelInfo modelInfo,
List<MethodSpec> methods) {
MethodSpec getDefaultLayoutMethod = MethodSpec.methodBuilder(
GET_DEFAULT_LAYOUT_METHOD_NAME)
.addAnnotation(Override.class)
.addAnnotation(LayoutRes.class)
.addModifiers(Modifier.PROTECTED)
.returns(TypeName.INT)
.build();
// TODO: This is pretty ugly and could be abstracted/decomposed better. We could probably
// make a small class to contain this logic, or build it into the model info classes
LayoutResource layout;
if (modelInfo instanceof DataBindingModelInfo) {
layout = ((DataBindingModelInfo) modelInfo).getLayoutResource();
} else {
TypeElement superClassElement = modelInfo.getSuperClassElement();
if (implementsMethod(superClassElement, getDefaultLayoutMethod, typeUtils)) {
return;
}
TypeElement modelClassWithAnnotation = findSuperClassWithClassAnnotation(superClassElement);
if (modelClassWithAnnotation == null) {
errorLogger
.logError("Model must use %s annotation if it does not implement %s. (class: %s)",
EpoxyModelClass.class,
GET_DEFAULT_LAYOUT_METHOD_NAME,
modelInfo.getSuperClassName());
return;
}
layout = layoutResourceProcessor
.getLayoutInAnnotation(modelClassWithAnnotation, EpoxyModelClass.class);
}
getDefaultLayoutMethod = getDefaultLayoutMethod.toBuilder()
.addStatement("return $L", layout.code)
.build();
methods.add(getDefaultLayoutMethod);
}
/**
* Add `setDataBindingVariables` for DataBinding models if they haven't implemented it. This adds
* the basic method and a method that checks for payload changes and only sets the variables that
* changed.
*/
private Iterable<MethodSpec> generateDataBindingMethodsIfNeeded(GeneratedModelInfo info) {
if (!isDataBindingModel(info.getSuperClassElement())) {
return Collections.emptyList();
}
MethodSpec bindVariablesMethod = MethodSpec.methodBuilder("setDataBindingVariables")
.addAnnotation(Override.class)
.addParameter(ClassName.get("android.databinding", "ViewDataBinding"), "binding")
.addModifiers(Modifier.PROTECTED)
.returns(TypeName.VOID)
.build();
// If the base method is already implemented don't bother checking for the payload method
if (implementsMethod(info.getSuperClassElement(), bindVariablesMethod, typeUtils)) {
return Collections.emptyList();
}
ClassName generatedModelClass = info.getGeneratedName();
String moduleName = dataBindingModuleLookup.getModuleName(info.getSuperClassElement());
Builder baseMethodBuilder = bindVariablesMethod.toBuilder();
Builder payloadMethodBuilder = bindVariablesMethod
.toBuilder()
.addParameter(getClassName(UNTYPED_EPOXY_MODEL_TYPE), "previousModel")
.beginControlFlow("if (!(previousModel instanceof $T))", generatedModelClass)
.addStatement("setDataBindingVariables(binding)")
.addStatement("return")
.endControlFlow()
.addStatement("$T that = ($T) previousModel", generatedModelClass, generatedModelClass);
ClassName brClass = ClassName.get(moduleName, "BR");
boolean validateAttributes = configManager.shouldValidateModelUsage();
for (AttributeInfo attribute : info.getAttributeInfo()) {
String attrName = attribute.getName();
CodeBlock setVariableBlock =
CodeBlock.of("binding.setVariable($T.$L, $L)", brClass, attrName, attribute.getterCode());
if (validateAttributes) {
// The setVariable method returns false if the variable id was not found in the layout.
// We can warn the user about this if they have model validations turned on, otherwise
// it fails silently.
baseMethodBuilder
.beginControlFlow("if (!$L)", setVariableBlock)
.addStatement(
"throw new $T(\"The attribute $L was defined in your data binding model ($L) but "
+ "a data variable of that name was not found in the layout.\")",
IllegalStateException.class, attrName, info.getSuperClassName())
.endControlFlow();
} else {
baseMethodBuilder.addStatement("$L", setVariableBlock);
}
// Handle binding variables only if they changed
startNotEqualsControlFlow(payloadMethodBuilder, attribute)
.addStatement("$L", setVariableBlock)
.endControlFlow();
}
ArrayList<MethodSpec> methods = new ArrayList<>();
methods.add(baseMethodBuilder.build());
methods.add(payloadMethodBuilder.build());
return methods;
}
/**
* Looks for {@link EpoxyModelClass} annotation in the original class and his parents.
*/
private TypeElement findSuperClassWithClassAnnotation(TypeElement classElement) {
if (!isEpoxyModel(classElement)) {
return null;
}
EpoxyModelClass annotation = classElement.getAnnotation(EpoxyModelClass.class);
if (annotation == null) {
// This is an error. The model must have an EpoxyModelClass annotation
// since getDefaultLayout is not implemented
return null;
}
int layoutRes;
try {
layoutRes = annotation.layout();
} catch (AnnotationTypeMismatchException e) {
errorLogger.logError("Invalid layout value in %s annotation. (class: %s). %s: %s",
EpoxyModelClass.class,
classElement.getSimpleName(),
e.getClass().getSimpleName(),
e.getMessage());
return null;
}
if (layoutRes != 0) {
return classElement;
}
// This model did not specify a layout in its EpoxyModelClass annotation,
// but its superclass might
TypeElement superClass = (TypeElement) typeUtils.asElement(classElement.getSuperclass());
TypeElement superClassWithAnnotation = findSuperClassWithClassAnnotation(superClass);
if (superClassWithAnnotation != null) {
return superClassWithAnnotation;
}
errorLogger
.logError(
"Model must specify a valid layout resource in the %s annotation. (class: %s)",
EpoxyModelClass.class,
classElement.getSimpleName());
return null;
}
private void generateParams(StringBuilder statementBuilder, List<ParameterSpec> params) {
boolean first = true;
for (ParameterSpec param : params) {
if (!first) {
statementBuilder.append(", ");
}
first = false;
statementBuilder.append(param.name);
}
statementBuilder.append(")");
}
private List<MethodSpec> generateSettersAndGetters(GeneratedModelInfo helperClass) {
List<MethodSpec> methods = new ArrayList<>();
for (AttributeInfo attributeInfo : helperClass.getAttributeInfo()) {
if (attributeInfo.isViewClickListener()) {
methods.add(generateSetClickModelListener(helperClass, attributeInfo));
}
if (attributeInfo.generateSetter() && !attributeInfo.hasFinalModifier()) {
methods.add(generateSetter(helperClass, attributeInfo));
}
if (attributeInfo.generateGetter()) {
methods.add(generateGetter(attributeInfo));
}
}
return methods;
}
private MethodSpec generateSetClickModelListener(GeneratedModelInfo classInfo,
AttributeInfo attribute) {
String attributeName = attribute.getName();
ParameterSpec param =
ParameterSpec.builder(getModelClickListenerType(classInfo), attributeName, FINAL).build();
Builder builder = MethodSpec.methodBuilder(attributeName)
.addJavadoc("Set a click listener that will provide the parent view, model, and adapter "
+ "position of the clicked view. This will clear the normal View.OnClickListener "
+ "if one has been set")
.addModifiers(PUBLIC)
.returns(classInfo.getParameterizedGeneratedName())
.addParameter(param)
.addAnnotations(attribute.getSetterAnnotations());
ClassName viewType = getClassName("android.view.View");
ClassName clickWrapperType = getClassName(WRAPPED_LISTENER_TYPE);
ClassName modelClickListenerType = getClassName(MODEL_CLICK_LISTENER_TYPE);
// This creates a View.OnClickListener and sets it on the original model's click listener field.
// This click listener has an empty onClick implementation, and will be replaced in
// `handlePreBind`
// when we can create a functional click listener with the view holder we bind to.
// However, we use this stub version for now since it has the same hashCode implementation
// as the future click listener, so when we create the real click listener in `handlePreBind`
// it won't change the hashCode of the model.
CodeBlock clickListenerCodeBlock = CodeBlock.of(
"new $T($L) {\n"
+ " @Override\n"
+ " protected void wrappedOnClick($T v, $T "
+ "originalClickListener) {\n"
+ " \n"
+ " }\n"
+ " }",
clickWrapperType, attributeName, viewType, modelClickListenerType);
addOnMutationCall(builder)
.addStatement("this.$L = $L", attribute.getModelClickListenerName(), attributeName)
.beginControlFlow("if ($L == null)", attributeName)
.addStatement(attribute.setterCode(), "null")
.endControlFlow()
.beginControlFlow("else")
.addStatement(attribute.setterCode(), clickListenerCodeBlock)
.endControlFlow()
.addStatement("return this");
return builder.build();
}
private MethodSpec generateEquals(GeneratedModelInfo helperClass) {
Builder builder = MethodSpec.methodBuilder("equals")
.addAnnotation(Override.class)
.addModifiers(PUBLIC)
.returns(boolean.class)
.addParameter(Object.class, "o")
.beginControlFlow("if (o == this)")
.addStatement("return true")
.endControlFlow()
.beginControlFlow("if (!(o instanceof $T))", helperClass.getGeneratedName())
.addStatement("return false")
.endControlFlow()
.beginControlFlow("if (!super.equals(o))")
.addStatement("return false")
.endControlFlow()
.addStatement("$T that = ($T) o", helperClass.getGeneratedName(),
helperClass.getGeneratedName());
startNotEqualsControlFlow(
builder,
false,
getClassName(ON_BIND_MODEL_LISTENER_TYPE),
modelBindListenerFieldName())
.addStatement("return false")
.endControlFlow();
startNotEqualsControlFlow(
builder,
false,
getClassName(ON_UNBIND_MODEL_LISTENER_TYPE),
modelUnbindListenerFieldName())
.addStatement("return false")
.endControlFlow();
for (AttributeInfo attributeInfo : helperClass.getAttributeInfo()) {
TypeName type = attributeInfo.getTypeName();
if (!attributeInfo.useInHash() && type.isPrimitive()) {
continue;
}
startNotEqualsControlFlow(builder, attributeInfo)
.addStatement("return false")
.endControlFlow();
}
return builder
.addStatement("return true")
.build();
}
private static MethodSpec.Builder startNotEqualsControlFlow(MethodSpec.Builder methodBuilder,
AttributeInfo attribute) {
TypeName attributeType = attribute.getTypeName();
boolean useHash = attributeType.isPrimitive() || attribute.useInHash();
return startNotEqualsControlFlow(methodBuilder, useHash, attributeType, attribute.getterCode());
}
private static MethodSpec.Builder startNotEqualsControlFlow(Builder builder,
boolean useObjectHashCode, TypeName type, String accessorCode) {
if (useObjectHashCode) {
if (type == FLOAT) {
builder
.beginControlFlow("if (Float.compare(that.$L, $L) != 0)", accessorCode, accessorCode);
} else if (type == DOUBLE) {
builder
.beginControlFlow("if (Double.compare(that.$L, $L) != 0)", accessorCode, accessorCode);
} else if (type.isPrimitive()) {
builder.beginControlFlow("if ($L != that.$L)", accessorCode, accessorCode);
} else if (type instanceof ArrayTypeName) {
builder
.beginControlFlow("if (!$T.equals($L, that.$L))", TypeName.get(Arrays.class),
accessorCode, accessorCode);
} else {
builder
.beginControlFlow("if ($L != null ? !$L.equals(that.$L) : that.$L != null)",
accessorCode, accessorCode, accessorCode, accessorCode);
}
} else {
builder
.beginControlFlow("if (($L == null) != (that.$L == null))", accessorCode, accessorCode);
}
return builder;
}
private MethodSpec generateHashCode(GeneratedModelInfo helperClass) {
Builder builder = MethodSpec.methodBuilder("hashCode")
.addAnnotation(Override.class)
.addModifiers(PUBLIC)
.returns(int.class)
.addStatement("int result = super.hashCode()");
addHashCodeLineForType(
builder,
false,
getClassName(ON_BIND_MODEL_LISTENER_TYPE),
modelBindListenerFieldName()
);
addHashCodeLineForType(
builder,
false,
getClassName(ON_UNBIND_MODEL_LISTENER_TYPE),
modelUnbindListenerFieldName()
);
for (AttributeInfo attributeInfo : helperClass.getAttributeInfo()) {
if (!attributeInfo.useInHash()) {
continue;
}
if (attributeInfo.getTypeName() == DOUBLE) {
builder.addStatement("long temp");
break;
}
}
for (AttributeInfo attributeInfo : helperClass.getAttributeInfo()) {
TypeName type = attributeInfo.getTypeName();
if (!attributeInfo.useInHash() && type.isPrimitive()) {
continue;
}
addHashCodeLineForType(builder, attributeInfo.useInHash(), type, attributeInfo.getterCode());
}
return builder
.addStatement("return result")
.build();
}
private static void addHashCodeLineForType(Builder builder, boolean useObjectHashCode,
TypeName type, String accessorCode) {
if (useObjectHashCode) {
if ((type == BYTE) || (type == CHAR) || (type == SHORT) || (type == INT)) {
builder.addStatement("result = 31 * result + $L", accessorCode);
} else if (type == LONG) {
builder.addStatement("result = 31 * result + (int) ($L ^ ($L >>> 32))", accessorCode,
accessorCode);
} else if (type == FLOAT) {
builder.addStatement("result = 31 * result + ($L != +0.0f "
+ "? Float.floatToIntBits($L) : 0)", accessorCode, accessorCode);
} else if (type == DOUBLE) {
builder.addStatement("temp = Double.doubleToLongBits($L)", accessorCode)
.addStatement("result = 31 * result + (int) (temp ^ (temp >>> 32))");
} else if (type == BOOLEAN) {
builder.addStatement("result = 31 * result + ($L ? 1 : 0)", accessorCode);
} else if (type instanceof ArrayTypeName) {
builder.addStatement("result = 31 * result + Arrays.hashCode($L)", accessorCode);
} else {
builder
.addStatement("result = 31 * result + ($L != null ? $L.hashCode() : 0)", accessorCode,
accessorCode);
}
} else {
builder.addStatement("result = 31 * result + ($L != null ? 1 : 0)", accessorCode);
}
}
private MethodSpec generateToString(GeneratedModelInfo helperClass) {
Builder builder = MethodSpec.methodBuilder("toString")
.addAnnotation(Override.class)
.addModifiers(PUBLIC)
.returns(String.class);
StringBuilder sb = new StringBuilder();
sb.append(String.format("\"%s{\" +\n", helperClass.getGeneratedName().simpleName()));
boolean first = true;
for (AttributeInfo attributeInfo : helperClass.getAttributeInfo()) {
String attributeName = attributeInfo.getName();
if (first) {
sb.append(String.format("\"%s=\" + %s +\n", attributeName, attributeInfo.getterCode()));
first = false;
} else {
sb.append(String.format("\", %s=\" + %s +\n", attributeName, attributeInfo.getterCode()));
}
}
sb.append("\"}\" + super.toString()");
return builder
.addStatement("return $L", sb.toString())
.build();
}
private MethodSpec generateGetter(AttributeInfo data) {
return MethodSpec.methodBuilder(data.getName())
.addModifiers(PUBLIC)
.returns(data.getTypeName())
.addAnnotations(data.getGetterAnnotations())
.addStatement("return $L", data.getterCode())
.build();
}
private MethodSpec generateSetter(GeneratedModelInfo helperClass, AttributeInfo attribute) {
String attributeName = attribute.getName();
Builder builder = MethodSpec.methodBuilder(attributeName)
.addModifiers(PUBLIC)
.returns(helperClass.getParameterizedGeneratedName())
.addParameter(ParameterSpec.builder(attribute.getTypeName(), attributeName)
.addAnnotations(attribute.getSetterAnnotations()).build());
addOnMutationCall(builder)
.addStatement(attribute.setterCode(), attributeName);
if (attribute.isViewClickListener()) {
// Null out the model click listener since this view click listener should replace it
builder.addStatement("this.$L = null", attribute.getModelClickListenerName());
}
// Call the super setter if it exists.
// No need to do this if the attribute is private since we already called the super setter to
// set it
if (!attribute.isPrivate && attribute.hasSuperSetterMethod()) {
builder.addStatement("super.$L($L)", attributeName, attributeName);
}
return builder
.addStatement("return this")
.build();
}
private MethodSpec generateReset(GeneratedModelInfo helperClass) {
Builder builder = MethodSpec.methodBuilder("reset")
.addAnnotation(Override.class)
.addModifiers(PUBLIC)
.returns(helperClass.getParameterizedGeneratedName())
.addStatement("$L = null", modelBindListenerFieldName())
.addStatement("$L = null", modelUnbindListenerFieldName());
for (AttributeInfo attributeInfo : helperClass.getAttributeInfo()) {
if (!attributeInfo.hasFinalModifier()) {
builder.addStatement(attributeInfo.setterCode(),
getDefaultValue(attributeInfo.getTypeName()));
}
if (attributeInfo.isViewClickListener()) {
builder.addStatement("$L = null", attributeInfo.getModelClickListenerName());
}
}
return builder
.addStatement("super.reset()")
.addStatement("return this")
.build();
}
private MethodSpec.Builder addOnMutationCall(MethodSpec.Builder method) {
return method.addStatement("onMutation()");
}
private MethodSpec.Builder addHashCodeValidationIfNecessary(MethodSpec.Builder method,
String message) {
if (configManager.shouldValidateModelUsage()) {
method.addStatement("validateStateHasNotChangedSinceAdded($S, position)", message);
}
return method;
}
private static String getDefaultValue(TypeName attributeType) {
if (attributeType == BOOLEAN) {
return "false";
} else if (attributeType == INT) {
return "0";
} else if (attributeType == BYTE) {
return "(byte) 0";
} else if (attributeType == CHAR) {
return "(char) 0";
} else if (attributeType == SHORT) {
return "(short) 0";
} else if (attributeType == LONG) {
return "0L";
} else if (attributeType == FLOAT) {
return "0.0f";
} else if (attributeType == DOUBLE) {
return "0.0d";
} else {
return "null";
}
}
}