/**
* 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 java.lang.annotation.Annotation;
import java.util.Map;
import com.facebook.litho.specmodels.model.DelegateMethodDescription;
import com.facebook.litho.specmodels.model.DelegateMethodModel;
import com.facebook.litho.specmodels.model.MethodParamModel;
import com.facebook.litho.specmodels.model.SpecModel;
import com.facebook.litho.specmodels.model.SpecModelUtils;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import static com.facebook.litho.specmodels.generator.ComponentImplGenerator.getImplAccessor;
import static com.facebook.litho.specmodels.generator.GeneratorConstants.ABSTRACT_IMPL_PARAM_NAME;
import static com.facebook.litho.specmodels.generator.GeneratorConstants.IMPL_VARIABLE_NAME;
import static com.facebook.litho.specmodels.model.ClassNames.COMPONENT;
import static com.facebook.litho.specmodels.model.ClassNames.OUTPUT;
import static com.facebook.litho.specmodels.model.ClassNames.STATE_VALUE;
/**
* Class that generates delegate methods for a component.
*/
public class DelegateMethodGenerator {
private DelegateMethodGenerator() {
}
/**
* Generate all delegates defined on this {@link SpecModel}.
*/
public static TypeSpecDataHolder generateDelegates(
SpecModel specModel,
Map<Class<? extends Annotation>, DelegateMethodDescription> delegateMethodsMap) {
TypeSpecDataHolder.Builder typeSpecDataHolder = TypeSpecDataHolder.newBuilder();
for (DelegateMethodModel delegateMethodModel : specModel.getDelegateMethods()) {
for (Annotation annotation : delegateMethodModel.annotations) {
if (delegateMethodsMap.containsKey(annotation.annotationType())) {
final DelegateMethodDescription delegateMethodDescription =
delegateMethodsMap.get(annotation.annotationType());
typeSpecDataHolder.addMethod(
generateDelegate(
specModel,
delegateMethodDescription,
delegateMethodModel));
for (MethodSpec methodSpec : delegateMethodDescription.extraMethods) {
typeSpecDataHolder.addMethod(methodSpec);
}
break;
}
}
}
return typeSpecDataHolder.build();
}
/**
* Generate a delegate to the Spec that defines this component.
*/
private static MethodSpec generateDelegate(
SpecModel specModel,
DelegateMethodDescription methodDescription,
DelegateMethodModel delegateMethod) {
final MethodSpec.Builder methodSpec = MethodSpec.methodBuilder(methodDescription.name)
.addModifiers(methodDescription.accessType)
.returns(methodDescription.returnType);
for (AnnotationSpec annotation : methodDescription.annotations) {
methodSpec.addAnnotation(annotation);
}
for (int i = 0, size = methodDescription.definedParameterTypes.size(); i < size; i++) {
methodSpec.addParameter(
methodDescription.definedParameterTypes.get(i),
delegateMethod.methodParams.get(i).getName());
}
if (!methodDescription.optionalParameterTypes.isEmpty()) {
methodSpec.addParameter(COMPONENT, ABSTRACT_IMPL_PARAM_NAME);
}
for (TypeName exception : methodDescription.exceptions) {
methodSpec.addException(exception);
}
if (!methodDescription.optionalParameterTypes.isEmpty()) {
final String implName = specModel.getComponentName() + "Impl";
methodSpec.addStatement(
implName + " " + IMPL_VARIABLE_NAME + " = (" + implName + ") " +
ABSTRACT_IMPL_PARAM_NAME);
}
final CodeBlock.Builder acquireOutputs = CodeBlock.builder();
final CodeBlock.Builder delegation = CodeBlock.builder();
final CodeBlock.Builder releaseOutputs = CodeBlock.builder();
final String sourceDelegateAccessor = SpecModelUtils.getSpecAccessor(specModel);
if (methodDescription.returnType.equals(TypeName.VOID)) {
delegation.add("$L.$L(\n", sourceDelegateAccessor, delegateMethod.name);
} else {
delegation.add(
"$T _result = ($T) $L.$L(\n",
methodDescription.returnType,
methodDescription.returnType,
sourceDelegateAccessor,
delegateMethod.name);
}
delegation.indent();
for (int i = 0, size = delegateMethod.methodParams.size(); i < size; i++) {
final MethodParamModel methodParamModel = delegateMethod.methodParams.get(i);
if (i < methodDescription.definedParameterTypes.size()) {
delegation.add("($T) $L", methodParamModel.getType(), methodParamModel.getName());
} else if (isOutputType(methodParamModel.getType())) {
acquireOutputs.add(
"$T $L = acquireOutput();\n", methodParamModel.getType(), methodParamModel.getName());
delegation.add("$L", methodParamModel.getName());
final boolean isPropOutput = SpecModelUtils.isPropOutput(specModel, methodParamModel);
if (isPropOutput) {
releaseOutputs.beginControlFlow("if ($L.get() != null)", methodParamModel.getName());
}
releaseOutputs.addStatement(
"$L.$L = $L.get()",
IMPL_VARIABLE_NAME,
getImplAccessor(specModel, methodParamModel),
methodParamModel.getName());
if (isPropOutput) {
releaseOutputs.endControlFlow();
}
releaseOutputs.addStatement("releaseOutput($L)", methodParamModel.getName());
} else if (isStateValueType(methodParamModel.getType())) {
acquireOutputs.add(
"$T $L = new StateValue<>();\n",
methodParamModel.getType(),
methodParamModel.getName());
delegation.add("$L", methodParamModel.getName());
releaseOutputs.addStatement(
"$L.$L = $L.get()",
IMPL_VARIABLE_NAME,
getImplAccessor(specModel, methodParamModel),
methodParamModel.getName());
} else {
delegation.add(
"($T) $L.$L",
methodParamModel.getType(),
IMPL_VARIABLE_NAME,
getImplAccessor(specModel, methodParamModel));
}
if (i < delegateMethod.methodParams.size() - 1) {
delegation.add(",\n");
} else {
delegation.add(");\n");
}
}
delegation.unindent();
methodSpec.addCode(acquireOutputs.build());
methodSpec.addCode(delegation.build());
methodSpec.addCode(releaseOutputs.build());
if (!methodDescription.returnType.equals(TypeName.VOID)) {
methodSpec.addStatement("return _result");
}
return methodSpec.build();
}
private static boolean isOutputType(TypeName type) {
return type.equals(OUTPUT) ||
(type instanceof ParameterizedTypeName &&
((ParameterizedTypeName) type).rawType.equals(OUTPUT));
}
private static boolean isStateValueType(TypeName type) {
return type.equals(STATE_VALUE) ||
(type instanceof ParameterizedTypeName &&
((ParameterizedTypeName) type).rawType.equals(STATE_VALUE));
}
}