react4j.processor.BuilderGenerator Maven / Gradle / Ivy
The newest version!
package react4j.processor;
import com.squareup.javapoet.ArrayTypeName;
import com.squareup.javapoet.ClassName;
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 com.squareup.javapoet.TypeVariableName;
import com.squareup.javapoet.WildcardTypeName;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import org.realityforge.proton.AnnotationsUtil;
import org.realityforge.proton.GeneratorUtil;
import org.realityforge.proton.SuppressWarningsUtil;
final class BuilderGenerator
{
private static final ClassName JS_ARRAY_CLASSNAME = ClassName.get( "akasha.lang", "JsArray" );
private static final ClassName IDENTIFIABLE_CLASSNAME = ClassName.get( "arez.component", "Identifiable" );
private static final ClassName REACT_ELEMENT_CLASSNAME = ClassName.get( "react4j", "ReactElement" );
private static final ClassName CONTEXT_CLASSNAME = ClassName.get( "react4j", "Context" );
private static final ClassName CONTEXTS_CLASSNAME = ClassName.get( "react4j", "Contexts" );
private static final ClassName REACT_CLASSNAME = ClassName.get( "react4j", "React" );
private static final ClassName KEYED_CLASSNAME = ClassName.get( "react4j", "Keyed" );
private static final ClassName REACT_NODE_CLASSNAME = ClassName.get( "react4j", "ReactNode" );
private static final ClassName JS_PROPERTY_MAP_CLASSNAME = ClassName.get( "jsinterop.base", "JsPropertyMap" );
private static final ParameterizedTypeName JS_PROPERTY_MAP_T_OBJECT_CLASSNAME =
ParameterizedTypeName.get( JS_PROPERTY_MAP_CLASSNAME, TypeName.OBJECT );
private BuilderGenerator()
{
}
@Nonnull
static TypeSpec buildType( @Nonnull final ProcessingEnvironment processingEnv,
@Nonnull final ViewDescriptor descriptor )
{
final TypeSpec.Builder builder = TypeSpec.classBuilder( descriptor.getBuilderClassName() );
GeneratorUtil.addOriginatingTypes( descriptor.getElement(), builder );
GeneratorUtil.addGeneratedAnnotation( processingEnv, builder, React4jProcessor.class.getName() );
builder.addModifiers( Modifier.FINAL );
GeneratorUtil.copyAccessModifiers( descriptor.getElement(), builder );
GeneratorUtil.copyWhitelistedAnnotations( descriptor.getElement(), builder,
Collections.singletonList( Deprecated.class.getName() ) );
if ( descriptor.builderAccessesDeprecatedElements() )
{
builder.addAnnotation( SuppressWarningsUtil.suppressWarningsAnnotation( "deprecation" ) );
}
// Private constructor so can not instantiate
builder.addMethod( MethodSpec.constructorBuilder().addModifiers( Modifier.PRIVATE ).build() );
final BuilderDescriptor builderDescriptor = buildBuilderDescriptor( descriptor );
final List steps = builderDescriptor.getSteps();
builder.addMethod( buildStaticNewBuilderMethod( descriptor ) );
for ( final Step step : steps )
{
builder.addType( buildBuilderStepInterface( processingEnv, descriptor, step ) );
}
// first step which may be required input, optional inputs, or build terminal step.
buildStaticStepMethodMethods( processingEnv, descriptor, builder, steps.get( 0 ) );
if ( descriptor.getInputs().stream().anyMatch( InputDescriptor::isContextSource ) )
{
builder.addType( buildContextHolder( descriptor ) );
}
builder.addType( buildBuilder( processingEnv, descriptor, builderDescriptor ) );
return builder.build();
}
private static void buildStaticStepMethodMethods( @Nonnull final ProcessingEnvironment processingEnv,
@Nonnull final ViewDescriptor descriptor,
@Nonnull final TypeSpec.Builder builder,
@Nonnull final Step step )
{
for ( final StepMethod method : step.getMethods() )
{
builder.addMethod( buildStaticStepMethodMethod( processingEnv, descriptor, step, method ) );
}
}
@Nonnull
private static MethodSpec buildStaticNewBuilderMethod( @Nonnull final ViewDescriptor descriptor )
{
final String infix = descriptor.getDeclaredType().getTypeArguments().isEmpty() ? "" : "<>";
final MethodSpec.Builder method = MethodSpec
.methodBuilder( "newBuilder" )
.addModifiers( Modifier.PRIVATE, Modifier.STATIC )
.addAnnotation( GeneratorUtil.NONNULL_CLASSNAME )
.returns( parameterizeIfRequired( descriptor, ClassName.bestGuess( "Step1" ) ) )
.addStatement( "return new $T" + infix + "()", ClassName.bestGuess( "Builder" ) );
GeneratorUtil.copyTypeParameters( descriptor.getElement(), method );
return method.build();
}
@Nonnull
private static MethodSpec buildStaticStepMethodMethod( @Nonnull final ProcessingEnvironment processingEnv,
@Nonnull final ViewDescriptor descriptor,
@Nonnull final Step step,
@Nonnull final StepMethod stepMethod )
{
final MethodSpec.Builder method =
MethodSpec.methodBuilder( stepMethod.getName() ).
addAnnotation( GeneratorUtil.NONNULL_CLASSNAME );
method.addModifiers( Modifier.STATIC );
if ( descriptor.getDeclaredType().asElement().getModifiers().contains( Modifier.PUBLIC ) )
{
method.addModifiers( Modifier.PUBLIC );
}
GeneratorUtil.copyTypeParameters( descriptor.getElement(), method );
if ( stepMethod.isBuildIntrinsic() )
{
method.addStatement( "return newBuilder().build()" );
}
else
{
final TypeName type = stepMethod.getType();
final ParameterSpec.Builder parameter =
ParameterSpec.builder( type, stepMethod.getName(), Modifier.FINAL );
final ExecutableElement inputMethod = stepMethod.getMethod();
if ( null != inputMethod )
{
GeneratorUtil.copyWhitelistedAnnotations( inputMethod, parameter );
final ExecutableType methodType = stepMethod.getMethodType();
assert null != methodType;
SuppressWarningsUtil.addSuppressWarningsIfRequired( processingEnv, parameter, methodType.getReturnType() );
}
else if ( stepMethod.isChildrenStreamIntrinsic() )
{
parameter.addAnnotation( GeneratorUtil.NONNULL_CLASSNAME );
}
if ( type instanceof ArrayTypeName )
{
method.varargs();
}
method.addParameter( parameter.build() );
final String infix = asTypeArgumentsInfix( descriptor.getDeclaredType() );
if ( infix.isEmpty() )
{
// No type parameters
method.addStatement( "return newBuilder().$N( $N )", stepMethod.getName(), stepMethod.getName() );
}
else
{
method.addStatement( "return $T." + infix + "newBuilder().$N( $N )",
descriptor.getBuilderClassName(),
stepMethod.getName(),
stepMethod.getName() );
}
}
configureStepMethodReturns( descriptor, method, step, stepMethod.getStepMethodType() );
return method.build();
}
@Nonnull
private static MethodSpec.Builder buildStepInterfaceMethod( @Nonnull final ViewDescriptor descriptor,
@Nonnull final String name,
@Nonnull final Step step,
@Nonnull final StepMethodType stepMethodType,
@Nonnull final Consumer action )
{
final MethodSpec.Builder method = MethodSpec.methodBuilder( name );
method.addModifiers( Modifier.PUBLIC, Modifier.ABSTRACT );
method.addAnnotation( GeneratorUtil.NONNULL_CLASSNAME );
action.accept( method );
configureStepMethodReturns( descriptor, method, step, stepMethodType );
return method;
}
private static void configureStepMethodReturns( @Nonnull final ViewDescriptor descriptor,
@Nonnull final MethodSpec.Builder method,
@Nonnull final Step step,
@Nonnull final StepMethodType stepMethodType )
{
if ( StepMethodType.TERMINATE == stepMethodType )
{
method.returns( REACT_NODE_CLASSNAME );
}
else
{
final int returnIndex = step.getIndex() + ( StepMethodType.STAY == stepMethodType ? 0 : 1 );
method.returns( parameterizeIfRequired( descriptor, ClassName.bestGuess( "Step" + returnIndex ) ) );
}
}
@Nonnull
private static TypeName parameterizeIfRequired( @Nonnull final ViewDescriptor descriptor,
@Nonnull final ClassName className )
{
final List variableNames =
GeneratorUtil.getTypeArgumentsAsNames( descriptor.getDeclaredType() );
if ( variableNames.isEmpty() )
{
return className;
}
else
{
return ParameterizedTypeName.get( className, variableNames.toArray( new TypeName[ 0 ] ) );
}
}
@Nonnull
private static TypeSpec buildBuilderStepInterface( @Nonnull final ProcessingEnvironment processingEnv,
@Nonnull final ViewDescriptor descriptor,
@Nonnull final Step step )
{
final int stepIndex = step.getIndex();
final TypeSpec.Builder builder = TypeSpec.interfaceBuilder( "Step" + stepIndex );
builder.addModifiers( Modifier.PUBLIC, Modifier.STATIC );
builder.addTypeVariables( GeneratorUtil.getTypeArgumentsAsNames( descriptor.getDeclaredType() ) );
if ( !descriptor.getDeclaredType().getTypeArguments().isEmpty() )
{
builder.addAnnotation( SuppressWarningsUtil.suppressWarningsAnnotation( "unused" ) );
}
for ( final StepMethod stepMethod : step.getMethods() )
{
final StepMethodType stepMethodType = stepMethod.getStepMethodType();
// Magically handle the step method named build
if ( stepMethod.isBuildIntrinsic() )
{
builder.addMethod( buildStepInterfaceMethod( descriptor, "build", step, stepMethodType, m -> {
} ).build() );
}
else
{
builder.addMethod( buildStepInterfaceMethod( descriptor, stepMethod.getName(), step, stepMethodType, m -> {
final ExecutableType inputMethodType = stepMethod.getMethodType();
if ( null != inputMethodType )
{
GeneratorUtil.copyTypeParameters( inputMethodType, m );
}
if ( stepMethod.isChildrenIntrinsic() )
{
m.varargs();
}
final TypeName type = stepMethod.getType();
if ( type instanceof ArrayTypeName )
{
m.varargs();
}
final ParameterSpec.Builder parameter = ParameterSpec.builder( type, stepMethod.getName() );
final ExecutableElement inputMethod = stepMethod.getMethod();
if ( null != inputMethod )
{
GeneratorUtil.copyWhitelistedAnnotations( inputMethod, parameter );
final ExecutableType methodType = stepMethod.getMethodType();
assert null != methodType;
SuppressWarningsUtil.addSuppressWarningsIfRequired( processingEnv, parameter, methodType.getReturnType() );
}
else if ( stepMethod.isChildrenStreamIntrinsic() )
{
parameter.addAnnotation( GeneratorUtil.NONNULL_CLASSNAME );
}
m.addParameter( parameter.build() );
} ).build() );
}
}
return builder.build();
}
@Nonnull
private static MethodSpec buildBuilderStepImpl( @Nonnull final ProcessingEnvironment processingEnv,
@Nonnull final ViewDescriptor descriptor,
@Nonnull final Step step,
@Nonnull final StepMethod stepMethod )
{
final MethodSpec.Builder method = MethodSpec.methodBuilder( stepMethod.getName() );
method.addModifiers( Modifier.PUBLIC, Modifier.FINAL );
method.addAnnotation( Override.class );
method.addAnnotation( GeneratorUtil.NONNULL_CLASSNAME );
final InputDescriptor input = stepMethod.getInput();
final ExecutableType inputMethodType = stepMethod.getMethodType();
if ( null != inputMethodType )
{
GeneratorUtil.copyTypeParameters( inputMethodType, method );
}
final TypeName type = stepMethod.getType();
final ParameterSpec.Builder parameter =
ParameterSpec.builder( type, stepMethod.getName(), Modifier.FINAL );
if ( type instanceof ArrayTypeName )
{
method.varargs();
}
final ExecutableElement inputMethod = stepMethod.getMethod();
if ( null != inputMethod )
{
GeneratorUtil.copyWhitelistedAnnotations( inputMethod, parameter );
final ExecutableType methodType = stepMethod.getMethodType();
assert null != methodType;
SuppressWarningsUtil.addSuppressWarningsIfRequired( processingEnv, parameter, methodType.getReturnType() );
}
else if ( stepMethod.isChildrenStreamIntrinsic() )
{
parameter.addAnnotation( GeneratorUtil.NONNULL_CLASSNAME );
}
method.addParameter( parameter.build() );
if ( null != input && input.isImmutable() && 1 == descriptor.syntheticKeyParts() )
{
final ImmutableInputKeyStrategy strategy = input.getImmutableInputKeyStrategy();
if ( ImmutableInputKeyStrategy.KEYED == strategy )
{
method.addStatement( "_element.setKey( $T.getKey( $N ) + " +
"( $T.enableViewNames() ? $S : $T.class.getName() ) )",
KEYED_CLASSNAME,
stepMethod.getName(),
REACT_CLASSNAME,
descriptor.keySuffix(),
descriptor.getClassName() );
}
else if ( ImmutableInputKeyStrategy.IS_STRING == strategy ||
ImmutableInputKeyStrategy.TO_STRING == strategy ||
ImmutableInputKeyStrategy.ENUM == strategy )
{
method.addStatement( "_element.setKey( $N + ( $T.enableViewNames() ? $S : $T.class.getName() ) )",
stepMethod.getName(),
REACT_CLASSNAME,
descriptor.keySuffix(),
descriptor.getClassName() );
}
else if ( ImmutableInputKeyStrategy.DYNAMIC == strategy )
{
method.addStatement( "_element.setKey( ( $N instanceof $T ? $T.getKey( $N ) : " +
"$N instanceof $T ? $T.<$T>getArezId( $N ) : $T.valueOf( $N ) ) + " +
"( $T.enableViewNames() ? $S : $T.class.getName() ) )",
stepMethod.getName(),
KEYED_CLASSNAME,
KEYED_CLASSNAME,
stepMethod.getName(),
stepMethod.getName(),
IDENTIFIABLE_CLASSNAME,
IDENTIFIABLE_CLASSNAME,
Object.class,
stepMethod.getName(),
String.class,
stepMethod.getName(),
REACT_CLASSNAME,
descriptor.keySuffix(),
descriptor.getClassName() );
}
else
{
assert ImmutableInputKeyStrategy.AREZ_IDENTIFIABLE == strategy;
method.addStatement( "_element.setKey( $T.