/**
* Copyright (c) 2017-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.litho.specmodels.generator;
import javax.lang.model.element.Modifier;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import com.facebook.litho.annotations.Param;
import com.facebook.litho.annotations.Prop;
import com.facebook.litho.annotations.ResType;
import com.facebook.litho.annotations.State;
import com.facebook.litho.specmodels.internal.ImmutableList;
import com.facebook.litho.specmodels.model.ClassNames;
import com.facebook.litho.specmodels.model.EventDeclarationModel;
import com.facebook.litho.specmodels.model.InterStageInputParamModel;
import com.facebook.litho.specmodels.model.MethodParamModel;
import com.facebook.litho.specmodels.model.PropModel;
import com.facebook.litho.specmodels.model.SpecModel;
import com.facebook.litho.specmodels.model.SpecModelUtils;
import com.facebook.litho.specmodels.model.StateParamModel;
import com.facebook.litho.specmodels.model.TreePropModel;
import com.facebook.litho.specmodels.model.UpdateStateMethodModel;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ArrayTypeName;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import static com.facebook.litho.specmodels.generator.GeneratorConstants.IMPL_CLASS_NAME_SUFFIX;
import static com.facebook.litho.specmodels.generator.GeneratorConstants.STATE_CONTAINER_FIELD_NAME;
import static com.facebook.litho.specmodels.model.ClassNames.COMPONENT;
/**
* Class that generates the preamble for a Component.
*/
public class ComponentImplGenerator {
private ComponentImplGenerator() {
}
public static TypeSpecDataHolder generate(SpecModel specModel) {
final String implClassName = getImplClassName(specModel);
final TypeSpec.Builder implClassBuilder =
TypeSpec.classBuilder(implClassName)
.addModifiers(Modifier.PRIVATE)
.superclass(
ParameterizedTypeName.get(
ClassNames.COMPONENT,
specModel.getComponentTypeName()))
.addSuperinterface(Cloneable.class);
if (!specModel.hasInjectedDependencies()) {
implClassBuilder.addModifiers(Modifier.STATIC);
implClassBuilder.addTypeVariables(specModel.getTypeVariables());
}
final boolean hasState = !specModel.getStateValues().isEmpty();
final ClassName stateContainerImplClass =
ClassName.bestGuess(getStateContainerImplClassName(specModel));
if (hasState) {
implClassBuilder.addField(stateContainerImplClass, STATE_CONTAINER_FIELD_NAME);
implClassBuilder.addMethod(
generateStateContainerGetter(specModel.getStateContainerClass()));
}
generateProps(specModel).addToTypeSpec(implClassBuilder);
generateTreeProps(specModel).addToTypeSpec(implClassBuilder);
generateInterStageInputs(specModel).addToTypeSpec(implClassBuilder);
generateEventHandlers(specModel).addToTypeSpec(implClassBuilder);
implClassBuilder.addMethod(generateImplConstructor(stateContainerImplClass, hasState));
implClassBuilder.addMethod(generateGetSimpleName(specModel));
implClassBuilder.addMethod(generateEqualsMethod(specModel, true));
generateCopyInterStageImpl(specModel).addToTypeSpec(implClassBuilder);
generateOnUpdateStateMethods(specModel).addToTypeSpec(implClassBuilder);
generateMakeShallowCopy(specModel, /* hasDeepCopy */ false, hasState)
.addToTypeSpec(implClassBuilder);
final TypeSpecDataHolder.Builder builder = TypeSpecDataHolder.newBuilder();
if (hasState) {
builder.addType(generateStateContainerImpl(specModel));
}
builder.addType(implClassBuilder.build());
return builder.build();
}
static TypeSpec generateStateContainerImpl(SpecModel specModel) {
final TypeSpec.Builder stateContainerImplClassBuilder =
TypeSpec.classBuilder(getStateContainerImplClassName(specModel))
.addSuperinterface(specModel.getStateContainerClass());
if (!specModel.hasInjectedDependencies()) {
stateContainerImplClassBuilder.addModifiers(Modifier.STATIC, Modifier.PRIVATE);
stateContainerImplClassBuilder.addTypeVariables(specModel.getTypeVariables());
}
for (StateParamModel stateValue : specModel.getStateValues()) {
stateContainerImplClassBuilder.addField(FieldSpec.builder(
stateValue.getType(),
stateValue.getName()).addAnnotation(State.class).build());
}
return stateContainerImplClassBuilder.build();
}
public static String getImplClassName(SpecModel specModel) {
return specModel.getComponentName() + IMPL_CLASS_NAME_SUFFIX;
}
static String getImplInstanceName(SpecModel specModel) {
final String implClassName = getImplClassName(specModel);
return implClassName.substring(0, 1).toLowerCase(Locale.ROOT) + implClassName.substring(1);
}
static String getStateContainerImplClassName(SpecModel specModel) {
if (specModel.getStateValues().isEmpty()) {
return specModel.getStateContainerClass().toString();
} else {
return specModel.getComponentName() + GeneratorConstants.STATE_CONTAINER_IMPL_NAME_SUFFIX;
}
}
static MethodSpec generateStateContainerGetter(TypeName stateContainerClassName) {
return MethodSpec.methodBuilder("getStateContainer")
.addModifiers(Modifier.PROTECTED)
.addAnnotation(Override.class)
.returns(stateContainerClassName)
.addStatement("return " + GeneratorConstants.STATE_CONTAINER_FIELD_NAME)
.build();
}
static TypeSpecDataHolder generateProps(SpecModel specModel) {
final TypeSpecDataHolder.Builder typeSpecDataHolder = TypeSpecDataHolder.newBuilder();
final ImmutableList<PropModel> props = specModel.getProps();
for (PropModel prop : props) {
final FieldSpec.Builder fieldBuilder = FieldSpec.builder(prop.getType(), prop.getName())
.addAnnotation(
AnnotationSpec.builder(Prop.class)
.addMember("resType", "$T.$L", ResType.class, prop.getResType())
.addMember("optional", "$L", prop.isOptional())
.build());
if (prop.hasDefault(specModel.getPropDefaults())) {
fieldBuilder.initializer("$L.$L", specModel.getSpecName(), prop.getName());
}
typeSpecDataHolder.addField(fieldBuilder.build());
}
return typeSpecDataHolder.build();
}
static TypeSpecDataHolder generateTreeProps(SpecModel specModel) {
final TypeSpecDataHolder.Builder typeSpecDataHolder = TypeSpecDataHolder.newBuilder();
final ImmutableList<TreePropModel> treeProps = specModel.getTreeProps();
for (TreePropModel treeProp : treeProps) {
typeSpecDataHolder.addField(
FieldSpec.builder(treeProp.getType(), treeProp.getName()).build());
}
return typeSpecDataHolder.build();
}
static TypeSpecDataHolder generateInterStageInputs(SpecModel specModel) {
final TypeSpecDataHolder.Builder typeSpecDataHolder = TypeSpecDataHolder.newBuilder();
final ImmutableList<InterStageInputParamModel> interStageInputs =
specModel.getInterStageInputs();
for (InterStageInputParamModel interStageInput : interStageInputs) {
typeSpecDataHolder.addField(
FieldSpec.builder(interStageInput.getType(), interStageInput.getName()).build());
}
return typeSpecDataHolder.build();
}
static TypeSpecDataHolder generateEventHandlers(SpecModel specModel) {
final TypeSpecDataHolder.Builder typeSpecDataHolder = TypeSpecDataHolder.newBuilder();
for (EventDeclarationModel eventDeclaration : specModel.getEventDeclarations()) {
typeSpecDataHolder.addField(
FieldSpec.builder(
ClassNames.EVENT_HANDLER,
getEventHandlerInstanceName(eventDeclaration.name))
.build());
}
return typeSpecDataHolder.build();
}
static String getEventHandlerInstanceName(ClassName eventHandlerClassName) {
final String eventHandlerName = eventHandlerClassName.simpleName();
return eventHandlerName.substring(0, 1).toLowerCase(Locale.ROOT) +
eventHandlerName.substring(1) +
"Handler";
}
static MethodSpec generateImplConstructor(TypeName stateContainerImplClass, boolean hasState) {
final MethodSpec.Builder builder = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PRIVATE)
.addStatement("super(get())");
if (hasState) {
builder.addStatement(STATE_CONTAINER_FIELD_NAME + " = new $T()", stateContainerImplClass);
}
return builder.build();
}
static MethodSpec generateGetSimpleName(SpecModel specModel) {
return MethodSpec.methodBuilder("getSimpleName")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.returns(ClassNames.STRING)
.addStatement("return \"" + specModel.getComponentName() + "\"")
.build();
}
static MethodSpec generateEqualsMethod(SpecModel specModel, boolean shouldCheckId) {
final String implClassName = getImplClassName(specModel);
final String implInstanceName = getImplInstanceName(specModel);
MethodSpec.Builder equalsBuilder = MethodSpec.methodBuilder("equals")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.BOOLEAN)
.addParameter(TypeName.OBJECT, "other")
.beginControlFlow("if (this == other)")
.addStatement("return true")
.endControlFlow()
.beginControlFlow("if (other == null || getClass() != other.getClass())")
.addStatement("return false")
.endControlFlow()
.addStatement(implClassName + " " + implInstanceName + " = (" + implClassName + ") other");
if (shouldCheckId) {
equalsBuilder
.beginControlFlow("if (this.getId() == " + implInstanceName + ".getId())")
.addStatement("return true")
.endControlFlow();
}
for (PropModel prop : specModel.getProps()) {
equalsBuilder.addCode(getCompareStatement(specModel, implInstanceName, prop));
}
for (StateParamModel state : specModel.getStateValues()) {
equalsBuilder.addCode(getCompareStatement(specModel, implInstanceName, state));
}
for (TreePropModel treeProp : specModel.getTreeProps()) {
equalsBuilder.addCode(getCompareStatement(specModel, implInstanceName, treeProp));
}
equalsBuilder.addStatement("return true");
return equalsBuilder.build();
}
static TypeSpecDataHolder generateCopyInterStageImpl(SpecModel specModel) {
final TypeSpecDataHolder.Builder typeSpecDataHolder = TypeSpecDataHolder.newBuilder();
final ImmutableList<InterStageInputParamModel> interStageInputs =
specModel.getInterStageInputs();
if (!interStageInputs.isEmpty()) {
final String implClassName = getImplClassName(specModel);
final String implInstanceName = getImplInstanceName(specModel);
final MethodSpec.Builder copyInterStageComponentBuilder = MethodSpec
.methodBuilder("copyInterStageImpl")
.addAnnotation(Override.class)
.addModifiers(Modifier.PROTECTED)
.returns(TypeName.VOID)
.addParameter(
ParameterizedTypeName.get(
ClassNames.COMPONENT,
specModel.getComponentTypeName()),
"impl")
.addStatement(
"$L " + implInstanceName + " = ($L) impl",
implClassName,
implClassName);
for (InterStageInputParamModel interStageInput : interStageInputs) {
copyInterStageComponentBuilder
.addStatement(
"$L = " + implInstanceName + ".$L",
interStageInput.getName(),
interStageInput.getName());
}
typeSpecDataHolder.addMethod(copyInterStageComponentBuilder.build());
}
return typeSpecDataHolder.build();
}
static TypeSpecDataHolder generateOnUpdateStateMethods(SpecModel specModel) {
TypeSpecDataHolder.Builder typeSpecDataHolder = TypeSpecDataHolder.newBuilder();
for (UpdateStateMethodModel updateStateMethodModel : specModel.getUpdateStateMethods()) {
final String stateUpdateClassName = getStateUpdateClassName(updateStateMethodModel);
final List<MethodParamModel> params = getParams(updateStateMethodModel);
final MethodSpec.Builder methodSpecBuilder = MethodSpec
.methodBuilder("create" + stateUpdateClassName)
.addModifiers(Modifier.PRIVATE)
.returns(ClassName.bestGuess(stateUpdateClassName));
for (MethodParamModel param : params) {
methodSpecBuilder
.addParameter(ParameterSpec.builder(param.getType(), param.getName()).build());
}
final CodeBlock.Builder constructor = CodeBlock.builder();
constructor.add("return new " + stateUpdateClassName + "(");
for (int i = 0, size = params.size(); i < size; i++) {
constructor.add(params.get(i).getName());
if (i < params.size() - 1) {
constructor.add(", ");
}
}
constructor.add(");\n");
methodSpecBuilder.addCode(constructor.build());
typeSpecDataHolder.addMethod(methodSpecBuilder.build());
}
return typeSpecDataHolder.build();
}
static TypeSpecDataHolder generateMakeShallowCopy(
SpecModel specModel,
boolean hasDeepCopy,
boolean hasState) {
TypeSpecDataHolder.Builder typeSpecDataHolder = TypeSpecDataHolder.newBuilder();
final List<MethodParamModel> componentsInImpl = findComponentsInImpl(specModel);
final ImmutableList<InterStageInputParamModel> interStageComponentVariables =
specModel.getInterStageInputs();
final ImmutableList<UpdateStateMethodModel> updateStateMethodModels =
specModel.getUpdateStateMethods();
if (componentsInImpl.isEmpty() &&
interStageComponentVariables.isEmpty() &&
updateStateMethodModels.isEmpty()) {
return typeSpecDataHolder.build();
}
final String implClassName = getImplClassName(specModel);
MethodSpec.Builder builder = MethodSpec.methodBuilder("makeShallowCopy")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.returns(ClassName.bestGuess(implClassName));
String deepCopy = hasDeepCopy ? "deepCopy" : "";
if (hasDeepCopy) {
builder.addParameter(ParameterSpec.builder(TypeName.BOOLEAN, "deepCopy").build());
}
builder.addStatement(
"$L $L = ($L) super.makeShallowCopy($L)",
implClassName,
"component",
implClassName,
deepCopy);
for (MethodParamModel componentParam : componentsInImpl) {
builder.addStatement(
"component.$L = component.$L != null ? component.$L.makeShallowCopy($L) : null",
componentParam.getName(),
componentParam.getName(),
componentParam.getName(),
deepCopy);
}
if (hasDeepCopy) {
builder.beginControlFlow("if (!deepCopy)");
}
for (InterStageInputParamModel interStageInput : specModel.getInterStageInputs()) {
builder.addStatement("component.$L = null", interStageInput.getName());
}
final String stateContainerImplClassName = getStateContainerImplClassName(specModel);
if (stateContainerImplClassName != null && hasState) {
builder.addStatement(
"component." + GeneratorConstants.STATE_CONTAINER_FIELD_NAME + " = new $T()",
ClassName.bestGuess(stateContainerImplClassName));
}
if (hasDeepCopy) {
builder.endControlFlow();
}
builder.addStatement("return component");
return typeSpecDataHolder.addMethod(builder.build()).build();
}
private static List<MethodParamModel> findComponentsInImpl(SpecModel specModel) {
final List<MethodParamModel> componentsInImpl = new ArrayList<>();
for (PropModel prop : specModel.getProps()) {
final TypeName typeName = prop.getType();
if (typeName.equals(ClassNames.COMPONENT) ||
(typeName instanceof ParameterizedTypeName &&
((ParameterizedTypeName) typeName).rawType.equals(COMPONENT))) {
componentsInImpl.add(prop);
}
}
return componentsInImpl;
}
private static List<MethodParamModel> getParams(UpdateStateMethodModel updateStateMethodModel) {
final List<MethodParamModel> params = new ArrayList<>();
for (MethodParamModel methodParamModel : updateStateMethodModel.methodParams) {
for (Annotation annotation : methodParamModel.getAnnotations()) {
if (annotation.annotationType().equals(Param.class)) {
params.add(methodParamModel);
break;
}
}
}
return params;
}
private static String getStateUpdateClassName(UpdateStateMethodModel updateStateMethodModel) {
String methodName = updateStateMethodModel.name.toString();
return methodName.substring(0, 1).toUpperCase(Locale.ROOT) +
methodName.substring(1) +
GeneratorConstants.STATE_UPDATE_IMPL_NAME_SUFFIX;
}
private static CodeBlock getCompareStatement(
SpecModel specModel,
String implInstanceName,
MethodParamModel field) {
final CodeBlock.Builder codeBlock = CodeBlock.builder();
final String implAccessor = getImplAccessor(specModel, field);
if (field.getType() == TypeName.FLOAT) {
codeBlock
.beginControlFlow(
"if (Float.compare($L, $L.$L) != 0)",
implAccessor,
implInstanceName,
implAccessor)
.addStatement("return false")
.endControlFlow();
} else if (field.getType() == TypeName.DOUBLE) {
codeBlock
.beginControlFlow(
"if (Double.compare($L, $L.$L) != 0)",
implAccessor,
implInstanceName,
implAccessor)
.addStatement("return false")
.endControlFlow();
} else if (field.getType() instanceof ArrayTypeName) {
codeBlock
.beginControlFlow(
"if (!$T.equals($L, $L.$L))",
Arrays.class,
implAccessor,
implInstanceName,
implAccessor)
.addStatement("return false")
.endControlFlow();
} else if (field.getType().isPrimitive()) {
codeBlock
.beginControlFlow(
"if ($L != $L.$L)",
implAccessor,
implInstanceName,
implAccessor)
.addStatement("return false")
.endControlFlow();
} else if (field.getType().equals(ClassNames.REFERENCE)) {
codeBlock
.beginControlFlow(
"if (Reference.shouldUpdate($L != $L.$L))",
implAccessor,
implInstanceName,
implAccessor)
.addStatement("return false")
.endControlFlow();
} else {
codeBlock
.beginControlFlow(
"if ($L != null ? !$L.equals($L.$L) : $L.$L != null)",
implAccessor,
implAccessor,
implInstanceName,
implAccessor,
implInstanceName,
implAccessor)
.addStatement("return false")
.endControlFlow();
}
return codeBlock.build();
}
static String getImplAccessor(SpecModel specModel, MethodParamModel methodParamModel) {
if (methodParamModel instanceof StateParamModel ||
SpecModelUtils.getStateValueWithName(specModel, methodParamModel.getName()) != null) {
return STATE_CONTAINER_FIELD_NAME + "." + methodParamModel.getName();
}
return methodParamModel.getName();
}
}