/** * 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.util.Locale; import com.facebook.litho.annotations.Param; import com.facebook.litho.specmodels.model.ClassNames; import com.facebook.litho.specmodels.model.MethodParamModel; import com.facebook.litho.specmodels.model.MethodParamModelUtils; 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.UpdateStateMethodModel; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.CodeBlock; 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.STATE_CONTAINER_FIELD_NAME; /** * Class that generates the state methods for a Component. */ public class StateGenerator { private static final String STATE_UPDATE_IMPL_NAME_SUFFIX = "StateUpdate"; private static final String STATE_CONTAINER_PARAM_NAME = "stateContainer"; private static final String STATE_CONTAINER_IMPL_NAME = "stateContainerImpl"; private static final String STATE_UPDATE_NEW_COMPONENT_NAME = "newComponent"; private static final String STATE_UPDATE_METHOD_NAME = "updateState"; private static final String LAZY_STATE_UPDATE_VALUE_PARAM = "lazyUpdateValue"; private static final String STATE_UPDATE_IS_LAZY_METHOD_NAME = "isLazyStateUpdate"; private StateGenerator() { } public static TypeSpecDataHolder generate(SpecModel specModel) { return TypeSpecDataHolder.newBuilder() .addTypeSpecDataHolder(generateHasState(specModel)) .addTypeSpecDataHolder(generateTransferState(specModel)) .addTypeSpecDataHolder(generateOnStateUpdateMethods(specModel)) .addTypeSpecDataHolder(generateStateUpdateClasses(specModel)) .addTypeSpecDataHolder(generateLazyStateUpdateMethods(specModel)) .build(); } static TypeSpecDataHolder generateHasState(SpecModel specModel) { final TypeSpecDataHolder.Builder typeSpecDataHolder = TypeSpecDataHolder.newBuilder(); if (!specModel.getStateValues().isEmpty()) { typeSpecDataHolder.addMethod( MethodSpec.methodBuilder("hasState") .addAnnotation(Override.class) .addModifiers(Modifier.PROTECTED) .returns(TypeName.BOOLEAN) .addStatement("return true") .build()); } return typeSpecDataHolder.build(); } static TypeSpecDataHolder generateTransferState(SpecModel specModel) { if (specModel.getStateValues().isEmpty()) { return TypeSpecDataHolder.newBuilder().build(); } MethodSpec.Builder methodSpec = MethodSpec.methodBuilder("transferState") .addAnnotation(Override.class) .addModifiers(Modifier.PROTECTED) .addParameter(ParameterSpec.builder(specModel.getContextClass(), "context").build()) .addParameter( ParameterSpec.builder(specModel.getStateContainerClass(), "prevStateContainer").build()) .addParameter(ParameterSpec.builder(specModel.getComponentClass(), "component").build()) .addStatement( "$L prevStateContainerImpl = ($L) prevStateContainer", ComponentImplGenerator.getStateContainerImplClassName(specModel), ComponentImplGenerator.getStateContainerImplClassName(specModel)) .addStatement( "$L componentImpl = ($L) component", ComponentImplGenerator.getImplClassName(specModel), ComponentImplGenerator.getImplClassName(specModel)); for (StateParamModel stateValue : specModel.getStateValues()) { methodSpec.addStatement( "componentImpl.$L.$L = prevStateContainerImpl.$L", STATE_CONTAINER_FIELD_NAME, stateValue.getName(), stateValue.getName()); } return TypeSpecDataHolder.newBuilder().addMethod(methodSpec.build()).build(); } static TypeSpecDataHolder generateStateUpdateClasses(SpecModel specModel) { TypeSpecDataHolder.Builder dataHolder = TypeSpecDataHolder.newBuilder(); for (UpdateStateMethodModel updateStateMethod : specModel.getUpdateStateMethods()) { dataHolder.addTypeSpecDataHolder(generateStateUpdateClass(specModel, updateStateMethod)); } return dataHolder.build(); } static TypeSpecDataHolder generateOnStateUpdateMethods(SpecModel specModel) { TypeSpecDataHolder.Builder dataHolder = TypeSpecDataHolder.newBuilder(); for (UpdateStateMethodModel updateStateMethod : specModel.getUpdateStateMethods()) { dataHolder.addTypeSpecDataHolder(generateOnStateUpdateMethod(specModel, updateStateMethod, true)); dataHolder.addTypeSpecDataHolder(generateOnStateUpdateMethod(specModel, updateStateMethod, false)); } return dataHolder.build(); } static TypeSpecDataHolder generateOnStateUpdateMethod( SpecModel specModel, UpdateStateMethodModel updateStateMethod, boolean isAsync) { final String name = isAsync ? updateStateMethod.name.toString() + "Async" : updateStateMethod.name.toString(); final MethodSpec.Builder builder = MethodSpec.methodBuilder(name) .addModifiers(Modifier.PROTECTED, Modifier.STATIC) .addParameter(specModel.getContextClass(), "c"); builder.addStatement( "$T _component = c.get$LScope()", specModel.getComponentClass(), specModel.getComponentClass().simpleName()) .addCode( CodeBlock.builder() .beginControlFlow("if (_component == null)") .addStatement("return") .endControlFlow() .build()); final CodeBlock.Builder codeBlockBuilder = CodeBlock.builder(); codeBlockBuilder.add(specModel.getComponentName() + "." + getStateUpdateClassName(updateStateMethod) + " _stateUpdate = ((" + specModel.getComponentName() +"."+ specModel.getComponentName() + "Impl) _component).create" + getStateUpdateClassName(updateStateMethod) + "("); boolean isFirstParam = true; for (MethodParamModel methodParam : updateStateMethod.methodParams) { if (MethodParamModelUtils.isAnnotatedWith(methodParam, Param.class)) { if (!isFirstParam) { codeBlockBuilder.add(", "); } else { isFirstParam = false; } builder.addParameter(methodParam.getType(), methodParam.getName()); builder.addTypeVariables(MethodParamModelUtils.getTypeVariables(methodParam)); codeBlockBuilder.add(methodParam.getName()); } } codeBlockBuilder.add(");\n"); builder.addCode(codeBlockBuilder.build()); if (isAsync) { builder.addStatement("c.updateStateAsync(_stateUpdate)"); } else { builder.addStatement("c.updateState(_stateUpdate)"); } return TypeSpecDataHolder.newBuilder().addMethod(builder.build()).build(); } static TypeSpecDataHolder generateStateUpdateClass( SpecModel specModel, UpdateStateMethodModel updateStateMethod) { final TypeSpec.Builder stateUpdateClassBuilder = TypeSpec.classBuilder(getStateUpdateClassName(updateStateMethod)) .addModifiers(Modifier.PRIVATE) .addSuperinterface(specModel.getUpdateStateInterface()); if (!specModel.hasInjectedDependencies()) { stateUpdateClassBuilder.addModifiers(Modifier.STATIC); } // Generate updateState method. final String newComponentImplName = STATE_UPDATE_NEW_COMPONENT_NAME + STATE_UPDATE_IMPL_NAME_SUFFIX; MethodSpec.Builder updateStateMethodBuilder = MethodSpec.methodBuilder(STATE_UPDATE_METHOD_NAME) .addModifiers(Modifier.PUBLIC) .addParameter(specModel.getStateContainerClass(), STATE_CONTAINER_PARAM_NAME) .addParameter(specModel.getComponentClass(), STATE_UPDATE_NEW_COMPONENT_NAME) .addStatement( "$L $L = ($L) $L", ComponentImplGenerator.getStateContainerImplClassName(specModel), STATE_CONTAINER_IMPL_NAME, ComponentImplGenerator.getStateContainerImplClassName(specModel), STATE_CONTAINER_PARAM_NAME) .addStatement( "$L $L = ($L) $L", ComponentImplGenerator.getImplClassName(specModel), newComponentImplName, ComponentImplGenerator.getImplClassName(specModel), STATE_UPDATE_NEW_COMPONENT_NAME); // Add constructor and member fields. MethodSpec.Builder constructor = MethodSpec.constructorBuilder(); for (MethodParamModel methodParam : updateStateMethod.methodParams) { if (MethodParamModelUtils.isAnnotatedWith(methodParam, Param.class)) { stateUpdateClassBuilder.addField( methodParam.getType(), getMemberName(methodParam), Modifier.PRIVATE); constructor .addParameter(methodParam.getType(), methodParam.getName()) .addStatement("$L = $L", getMemberName(methodParam), methodParam.getName()); if (!specModel.hasInjectedDependencies()) { stateUpdateClassBuilder.addTypeVariables( MethodParamModelUtils.getTypeVariables(methodParam)); } } else { // Must be a StateValue<>. updateStateMethodBuilder .addStatement( "$T $L = new $T()", methodParam.getType(), methodParam.getName(), methodParam.getType()) .addStatement( "$L.set($L.$L)", methodParam.getName(), STATE_CONTAINER_IMPL_NAME, methodParam.getName()); } } // Call the spec's update method. updateStateMethodBuilder.addStatement( SpecModelUtils.getSpecAccessor(specModel) + "." + updateStateMethod.name + "(" + getParamsForSpecUpdateMethodCall(updateStateMethod) + ")"); // Set the new value of the state. for (MethodParamModel methodParamModel : updateStateMethod.methodParams) { if (!MethodParamModelUtils.isAnnotatedWith(methodParamModel, Param.class)) { updateStateMethodBuilder .addStatement( newComponentImplName + "." + STATE_CONTAINER_FIELD_NAME + "." + methodParamModel.getName() + " = " + methodParamModel.getName() + ".get()"); } } return TypeSpecDataHolder.newBuilder() .addType(stateUpdateClassBuilder .addMethod(constructor.build()) .addMethod(updateStateMethodBuilder.build()) .addMethod(MethodSpec.methodBuilder(STATE_UPDATE_IS_LAZY_METHOD_NAME) .addModifiers(Modifier.PUBLIC) .returns(TypeName.BOOLEAN) .addStatement("return false") .build()) .build()) .build(); } static TypeSpecDataHolder generateLazyStateUpdateMethods(SpecModel specModel) { TypeSpecDataHolder.Builder dataHolder = TypeSpecDataHolder.newBuilder(); for (StateParamModel stateValue : specModel.getStateValues()) { if (stateValue.canUpdateLazily()) { dataHolder.addTypeSpecDataHolder(generateLazyStateUpdateMethod(specModel, stateValue)); } } return dataHolder.build(); } static TypeSpecDataHolder generateLazyStateUpdateMethod( SpecModel specModel, StateParamModel stateValue) { final String newComponentImplName = STATE_UPDATE_NEW_COMPONENT_NAME + STATE_UPDATE_IMPL_NAME_SUFFIX; final MethodSpec.Builder builder = MethodSpec.methodBuilder( "lazyUpdate" + stateValue.getName().substring(0, 1).toUpperCase(Locale.ROOT) + stateValue.getName().substring(1)) .addModifiers(Modifier.PROTECTED, Modifier.STATIC) .addParameter(specModel.getContextClass(), "c") .addParameter(stateValue.getType(), LAZY_STATE_UPDATE_VALUE_PARAM, Modifier.FINAL); builder.addStatement( "$T _component = c.get$LScope()", specModel.getComponentClass(), specModel.getComponentClass().simpleName()) .addCode( CodeBlock.builder() .beginControlFlow("if (_component == null)") .addStatement("return") .endControlFlow() .build()); final TypeName implClass = ClassName.bestGuess( specModel.getComponentName() + "." + ComponentImplGenerator.getImplClassName(specModel)); final MethodSpec.Builder stateUpdate = MethodSpec.methodBuilder(STATE_UPDATE_METHOD_NAME) .addParameter(specModel.getStateContainerClass(), STATE_CONTAINER_PARAM_NAME) .addParameter(specModel.getComponentClass(), STATE_UPDATE_NEW_COMPONENT_NAME) .addModifiers(Modifier.PUBLIC) .addStatement( "$T $L = ($T) $L", implClass, newComponentImplName, implClass, STATE_UPDATE_NEW_COMPONENT_NAME) .addStatement( "$T $L = new $T()", ParameterizedTypeName.get(ClassNames.STATE_VALUE, stateValue.getType().box()), stateValue.getName(), ParameterizedTypeName.get(ClassNames.STATE_VALUE, stateValue.getType().box())) .addStatement(stateValue.getName() + ".set(" + LAZY_STATE_UPDATE_VALUE_PARAM + ")") .addStatement( "$L.$L.$L = $L.get()", newComponentImplName, GeneratorConstants.STATE_CONTAINER_FIELD_NAME, stateValue.getName(), stateValue.getName()); final TypeSpec.Builder stateBuilderImpl = TypeSpec.anonymousClassBuilder("") .addSuperinterface(specModel.getUpdateStateInterface()) .addMethod(stateUpdate.build()) .addMethod(MethodSpec.methodBuilder(STATE_UPDATE_IS_LAZY_METHOD_NAME) .addModifiers(Modifier.PUBLIC) .returns(TypeName.BOOLEAN) .addStatement("return true") .build()); builder.addStatement( "$T _stateUpdate = $L", specModel.getUpdateStateInterface(), stateBuilderImpl.build()); builder.addStatement("c.updateStateLazy(_stateUpdate)"); return TypeSpecDataHolder.newBuilder().addMethod(builder.build()).build(); } private static String getStateUpdateClassName(UpdateStateMethodModel updateMethod) { String methodName = updateMethod.name.toString(); return methodName.substring(0, 1).toUpperCase(Locale.ROOT) + methodName.substring(1) + STATE_UPDATE_IMPL_NAME_SUFFIX; } private static String getMemberName(MethodParamModel methodParamModel) { return "m" + methodParamModel.getName().substring(0, 1).toUpperCase() + methodParamModel.getName().substring(1); } private static String getParamsForSpecUpdateMethodCall(UpdateStateMethodModel updateStateMethod) { StringBuilder sb = new StringBuilder(); for (int i = 0, size = updateStateMethod.methodParams.size(); i < size; i++) { MethodParamModel methodParam = updateStateMethod.methodParams.get(i); if (MethodParamModelUtils.isAnnotatedWith(methodParam, Param.class)) { sb.append(getMemberName(methodParam)); } else { sb.append(methodParam.getName()); } if (i < size - 1) { sb.append(','); } } return sb.toString(); } }