001package react4j.processor;
002
003import com.squareup.javapoet.TypeName;
004import java.io.IOException;
005import java.util.ArrayList;
006import java.util.Arrays;
007import java.util.Collection;
008import java.util.Collections;
009import java.util.HashMap;
010import java.util.HashSet;
011import java.util.List;
012import java.util.Map;
013import java.util.Set;
014import java.util.regex.Matcher;
015import java.util.regex.Pattern;
016import java.util.stream.Collectors;
017import javax.annotation.Nonnull;
018import javax.annotation.Nullable;
019import javax.annotation.processing.RoundEnvironment;
020import javax.annotation.processing.SupportedAnnotationTypes;
021import javax.annotation.processing.SupportedOptions;
022import javax.annotation.processing.SupportedSourceVersion;
023import javax.lang.model.SourceVersion;
024import javax.lang.model.element.AnnotationMirror;
025import javax.lang.model.element.AnnotationValue;
026import javax.lang.model.element.Element;
027import javax.lang.model.element.ElementKind;
028import javax.lang.model.element.ExecutableElement;
029import javax.lang.model.element.Modifier;
030import javax.lang.model.element.TypeElement;
031import javax.lang.model.element.TypeParameterElement;
032import javax.lang.model.element.VariableElement;
033import javax.lang.model.type.ArrayType;
034import javax.lang.model.type.DeclaredType;
035import javax.lang.model.type.ExecutableType;
036import javax.lang.model.type.TypeKind;
037import javax.lang.model.type.TypeMirror;
038import javax.lang.model.type.TypeVariable;
039import javax.lang.model.util.Elements;
040import javax.lang.model.util.Types;
041import javax.tools.Diagnostic;
042import org.realityforge.proton.AbstractStandardProcessor;
043import org.realityforge.proton.AnnotationsUtil;
044import org.realityforge.proton.DeferredElementSet;
045import org.realityforge.proton.ElementsUtil;
046import org.realityforge.proton.MemberChecks;
047import org.realityforge.proton.ProcessorException;
048import org.realityforge.proton.StopWatch;
049
050/**
051 * Annotation processor that analyzes React annotated source and generates models from the annotations.
052 */
053@SuppressWarnings( "Duplicates" )
054@SupportedAnnotationTypes( Constants.VIEW_CLASSNAME )
055@SupportedSourceVersion( SourceVersion.RELEASE_17 )
056@SupportedOptions( { "react4j.defer.unresolved",
057                     "react4j.defer.errors",
058                     "react4j.debug",
059                     "react4j.profile",
060                     "react4j.verbose_out_of_round" } )
061public final class React4jProcessor
062  extends AbstractStandardProcessor
063{
064  private static final String SENTINEL_NAME = "<default>";
065  private static final Pattern DEFAULT_GETTER_PATTERN = Pattern.compile( "^get([A-Z].*)Default$" );
066  private static final Pattern VALIDATE_INPUT_PATTERN = Pattern.compile( "^validate([A-Z].*)$" );
067  private static final Pattern LAST_INPUT_PATTERN = Pattern.compile( "^last([A-Z].*)$" );
068  private static final Pattern PREV_INPUT_PATTERN = Pattern.compile( "^prev([A-Z].*)$" );
069  private static final Pattern INPUT_PATTERN = Pattern.compile( "^([a-z].*)$" );
070  private static final Pattern GETTER_PATTERN = Pattern.compile( "^get([A-Z].*)$" );
071  private static final Pattern ISSER_PATTERN = Pattern.compile( "^is([A-Z].*)$" );
072  @Nonnull
073  private final DeferredElementSet _deferredTypes = new DeferredElementSet();
074  @Nonnull
075  private final StopWatch _analyzeViewStopWatch = new StopWatch( "Analyze View" );
076
077  @Override
078  protected void collectStopWatches( @Nonnull final Collection<StopWatch> stopWatches )
079  {
080    stopWatches.add( _analyzeViewStopWatch );
081  }
082
083  @Override
084  public boolean process( @Nonnull final Set<? extends TypeElement> annotations, @Nonnull final RoundEnvironment env )
085  {
086    debugAnnotationProcessingRootElements( env );
087    collectRootTypeNames( env );
088    processTypeElements( annotations,
089                         env,
090                         Constants.VIEW_CLASSNAME,
091                         _deferredTypes,
092                         _analyzeViewStopWatch.getName(),
093                         this::process,
094                         _analyzeViewStopWatch );
095    errorIfProcessingOverAndInvalidTypesDetected( env );
096    clearRootTypeNamesIfProcessingOver( env );
097    return true;
098  }
099
100  @Override
101  @Nonnull
102  protected String getIssueTrackerURL()
103  {
104    return "https://github.com/react4j/react4j/issues";
105  }
106
107  @Nonnull
108  @Override
109  protected String getOptionPrefix()
110  {
111    return "react4j";
112  }
113
114  private void process( @Nonnull final TypeElement element )
115    throws IOException, ProcessorException
116  {
117    final ViewDescriptor descriptor = parse( element );
118    final String packageName = descriptor.getPackageName();
119    emitTypeSpec( packageName, ViewGenerator.buildType( processingEnv, descriptor ) );
120    emitTypeSpec( packageName, BuilderGenerator.buildType( processingEnv, descriptor ) );
121    if ( descriptor.needsInjection() )
122    {
123      emitTypeSpec( packageName, FactoryGenerator.buildType( processingEnv, descriptor ) );
124    }
125  }
126
127  /**
128   * Return true if there is any method annotated with @PostConstruct.
129   */
130  private boolean hasPostConstruct( @Nonnull final TypeElement typeElement )
131  {
132    return getMethods( typeElement )
133      .stream()
134      .anyMatch( e -> AnnotationsUtil.hasAnnotationOfType( e, Constants.POST_CONSTRUCT_CLASSNAME ) );
135  }
136
137  @Nonnull
138  private ViewDescriptor parse( @Nonnull final TypeElement typeElement )
139  {
140    final String name = deriveViewName( typeElement );
141    final ViewType type = extractViewType( typeElement );
142    final boolean hasPostConstruct = hasPostConstruct( typeElement );
143    final boolean shouldSetDefaultPriority = shouldSetDefaultPriority( typeElement );
144
145    MemberChecks.mustNotBeFinal( Constants.VIEW_CLASSNAME, typeElement );
146    MemberChecks.mustBeAbstract( Constants.VIEW_CLASSNAME, typeElement );
147    if ( ElementKind.CLASS != typeElement.getKind() )
148    {
149      throw new ProcessorException( MemberChecks.must( Constants.VIEW_CLASSNAME, "be a class" ),
150                                    typeElement );
151    }
152    else if ( ElementsUtil.isNonStaticNestedClass( typeElement ) )
153    {
154      throw new ProcessorException( MemberChecks.toSimpleName( Constants.VIEW_CLASSNAME ) +
155                                    " target must not be a non-static nested class",
156                                    typeElement );
157    }
158    final List<ExecutableElement> constructors = ElementsUtil.getConstructors( typeElement );
159    if ( 1 != constructors.size() || !isConstructorValid( constructors.get( 0 ) ) )
160    {
161      throw new ProcessorException( MemberChecks.must( Constants.VIEW_CLASSNAME,
162                                                       "have a single, package-access constructor or the default constructor" ),
163                                    typeElement );
164    }
165    final ExecutableElement constructor = constructors.get( 0 );
166
167    final boolean sting = deriveSting( typeElement, constructor );
168    final boolean notSyntheticConstructor =
169      Elements.Origin.EXPLICIT == processingEnv.getElementUtils().getOrigin( constructor );
170
171    final List<? extends VariableElement> parameters = constructor.getParameters();
172    if ( sting )
173    {
174      if ( parameters.isEmpty() )
175      {
176        throw new ProcessorException( MemberChecks.mustNot( Constants.VIEW_CLASSNAME,
177                                                            "have specified sting=ENABLED if the constructor has no parameters" ),
178                                      typeElement );
179      }
180    }
181    else
182    {
183      final boolean hasNamedAnnotation =
184        parameters.stream().anyMatch( p -> AnnotationsUtil.hasAnnotationOfType( p, Constants.STING_NAMED_CLASSNAME ) );
185      if ( hasNamedAnnotation )
186      {
187        throw new ProcessorException( MemberChecks.mustNot( Constants.VIEW_CLASSNAME,
188                                                            "have specified sting=DISABLED and have a constructor parameter annotated with the " +
189                                                            Constants.STING_NAMED_CLASSNAME + " annotation" ),
190                                      constructor );
191      }
192    }
193
194    final ViewDescriptor descriptor =
195      new ViewDescriptor( name,
196                          typeElement,
197                          constructor,
198                          type,
199                          sting,
200                          notSyntheticConstructor,
201                          hasPostConstruct,
202                          shouldSetDefaultPriority );
203
204    for ( final Element element : descriptor.getElement().getEnclosedElements() )
205    {
206      if ( ElementKind.METHOD == element.getKind() )
207      {
208        final ExecutableElement method = (ExecutableElement) element;
209        if ( method.getModifiers().contains( Modifier.PUBLIC ) &&
210             MemberChecks.doesMethodNotOverrideInterfaceMethod( processingEnv, typeElement, method ) &&
211             ElementsUtil.isWarningNotSuppressed( method,
212                                                  Constants.WARNING_PUBLIC_METHOD,
213                                                  Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) )
214        {
215          final String message =
216            MemberChecks.shouldNot( Constants.VIEW_CLASSNAME,
217                                    "declare a public method. " +
218                                    MemberChecks.suppressedBy( Constants.WARNING_PUBLIC_METHOD,
219                                                               Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) );
220          processingEnv.getMessager().printMessage( Diagnostic.Kind.WARNING, message, method );
221        }
222        if ( method.getModifiers().contains( Modifier.FINAL ) &&
223             ElementsUtil.isWarningNotSuppressed( method,
224                                                  Constants.WARNING_FINAL_METHOD,
225                                                  Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) )
226        {
227          final String message =
228            MemberChecks.shouldNot( Constants.VIEW_CLASSNAME,
229                                    "declare a final method. " +
230                                    MemberChecks.suppressedBy( Constants.WARNING_FINAL_METHOD,
231                                                               Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) );
232          processingEnv.getMessager().printMessage( Diagnostic.Kind.WARNING, message, method );
233        }
234        if ( method.getModifiers().contains( Modifier.PROTECTED ) &&
235             ElementsUtil.isWarningNotSuppressed( method,
236                                                  Constants.WARNING_PROTECTED_METHOD,
237                                                  Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) &&
238             !isMethodAProtectedOverride( typeElement, method ) )
239        {
240          final String message =
241            MemberChecks.shouldNot( Constants.VIEW_CLASSNAME,
242                                    "declare a protected method. " +
243                                    MemberChecks.suppressedBy( Constants.WARNING_PROTECTED_METHOD,
244                                                               Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) );
245          processingEnv.getMessager().printMessage( Diagnostic.Kind.WARNING, message, method );
246        }
247      }
248    }
249
250    determineViewCapabilities( descriptor, typeElement );
251    determineInputs( descriptor );
252    determineInputValidatesMethods( descriptor );
253    determineOnInputChangeMethods( descriptor );
254    determineDefaultInputsMethods( descriptor );
255    determineDefaultInputsFields( descriptor );
256    determinePreUpdateMethod( typeElement, descriptor );
257    determinePostMountOrUpdateMethod( typeElement, descriptor );
258    determinePostUpdateMethod( typeElement, descriptor );
259    determinePostMountMethod( typeElement, descriptor );
260    determineOnErrorMethod( typeElement, descriptor );
261    determineScheduleRenderMethods( typeElement, descriptor );
262    determinePublishMethods( typeElement, descriptor );
263    determineRenderMethod( typeElement, descriptor );
264
265    for ( final InputDescriptor input : descriptor.getInputs() )
266    {
267      if ( !isInputRequired( input ) )
268      {
269        input.markAsOptional();
270      }
271      else
272      {
273        if ( input.isContextSource() )
274        {
275          throw new ProcessorException( MemberChecks.mustNot( Constants.INPUT_CLASSNAME,
276                                                              "specify require=ENABLE parameter when the for source=CONTEXT parameter is specified" ),
277                                        input.getMethod() );
278        }
279      }
280    }
281
282    /*
283     * Sorting must occur after @InputDefault has been processed to ensure the sorting
284     * correctly sorts optional inputs after required inputs.
285     */
286    descriptor.sortInputs();
287
288    verifyInputsNotAnnotatedWithArezAnnotations( descriptor );
289    verifyInputsNotCollectionOfArezComponents( descriptor );
290
291    return descriptor;
292  }
293
294  private boolean isMethodAProtectedOverride( @Nonnull final TypeElement typeElement,
295                                              @Nonnull final ExecutableElement method )
296  {
297    final ExecutableElement overriddenMethod = ElementsUtil.getOverriddenMethod( processingEnv, typeElement, method );
298    return null != overriddenMethod && overriddenMethod.getModifiers().contains( Modifier.PROTECTED );
299  }
300
301  private boolean deriveSting( @Nonnull final TypeElement typeElement, final @Nonnull ExecutableElement constructor )
302  {
303    final String inject =
304      AnnotationsUtil.getEnumAnnotationParameter( typeElement,
305                                                  Constants.VIEW_CLASSNAME,
306                                                  "sting" );
307    if ( "ENABLE".equals( inject ) )
308    {
309      return true;
310    }
311    else if ( "DISABLE".equals( inject ) )
312    {
313      return false;
314    }
315    else
316    {
317      return !constructor.getParameters().isEmpty() &&
318             null != processingEnv.getElementUtils().getTypeElement( Constants.STING_INJECTABLE_CLASSNAME );
319    }
320  }
321
322  private boolean isConstructorValid( @Nonnull final ExecutableElement ctor )
323  {
324    if ( Elements.Origin.EXPLICIT != processingEnv.getElementUtils().getOrigin( ctor ) )
325    {
326      return true;
327    }
328    else
329    {
330      final Set<Modifier> modifiers = ctor.getModifiers();
331      return
332        !modifiers.contains( Modifier.PRIVATE ) &&
333        !modifiers.contains( Modifier.PUBLIC ) &&
334        !modifiers.contains( Modifier.PROTECTED );
335    }
336  }
337
338  private void verifyInputsNotCollectionOfArezComponents( @Nonnull final ViewDescriptor descriptor )
339  {
340    for ( final InputDescriptor input : descriptor.getInputs() )
341    {
342      final ExecutableElement method = input.getMethod();
343      final TypeMirror returnType = method.getReturnType();
344      if ( TypeKind.DECLARED == returnType.getKind() )
345      {
346        final DeclaredType declaredType = (DeclaredType) returnType;
347        final List<? extends TypeMirror> typeArguments = declaredType.getTypeArguments();
348        if ( isCollection( declaredType ) )
349        {
350          if ( 1 == typeArguments.size() && isArezComponent( typeArguments.get( 0 ) ) )
351          {
352            throw new ProcessorException( "@Input target is a collection that contains Arez components. " +
353                                          "This is not a safe pattern when the arez components can be disposed.",
354                                          method );
355          }
356        }
357        else if ( isMap( declaredType ) )
358        {
359          if ( 2 == typeArguments.size() &&
360               ( isArezComponent( typeArguments.get( 0 ) ) ||
361                 isArezComponent( typeArguments.get( 1 ) ) ) )
362          {
363            throw new ProcessorException( "@Input target is a collection that contains Arez components. " +
364                                          "This is not a safe pattern when the arez components can be disposed.",
365                                          method );
366          }
367        }
368      }
369      else if ( TypeKind.ARRAY == returnType.getKind() )
370      {
371        final ArrayType arrayType = (ArrayType) returnType;
372        if ( isArezComponent( arrayType.getComponentType() ) )
373        {
374          throw new ProcessorException( "@Input target is an array that contains Arez components. " +
375                                        "This is not a safe pattern when the arez components can be disposed.",
376                                        method );
377        }
378      }
379    }
380  }
381
382  private boolean isCollection( @Nonnull final DeclaredType declaredType )
383  {
384    final TypeElement returnType = (TypeElement) processingEnv.getTypeUtils().asElement( declaredType );
385    final String classname = returnType.getQualifiedName().toString();
386    /*
387     * For the time being lets just list out a bunch of collections. We can ge more specific when/if
388     * it is ever required
389     */
390    return Collection.class.getName().equals( classname ) ||
391           Set.class.getName().equals( classname ) ||
392           List.class.getName().equals( classname ) ||
393           HashSet.class.getName().equals( classname ) ||
394           ArrayList.class.getName().equals( classname );
395  }
396
397  private boolean isMap( @Nonnull final DeclaredType declaredType )
398  {
399    final TypeElement returnType = (TypeElement) processingEnv.getTypeUtils().asElement( declaredType );
400    final String classname = returnType.getQualifiedName().toString();
401    /*
402     * For the time being lets just list out a bunch of collections. We can ge more specific when/if
403     * it is ever required
404     */
405    return Map.class.getName().equals( classname ) || HashMap.class.getName().equals( classname );
406  }
407
408  private boolean isArezComponent( @Nonnull final TypeMirror typeMirror )
409  {
410    return typeMirror instanceof DeclaredType &&
411           processingEnv.getTypeUtils()
412             .asElement( typeMirror )
413             .getAnnotationMirrors()
414             .stream()
415             .anyMatch( a -> a.getAnnotationType().toString().equals( Constants.AREZ_COMPONENT_CLASSNAME ) );
416  }
417
418  private void verifyInputsNotAnnotatedWithArezAnnotations( @Nonnull final ViewDescriptor descriptor )
419  {
420    for ( final InputDescriptor input : descriptor.getInputs() )
421    {
422      final ExecutableElement method = input.getMethod();
423      for ( final AnnotationMirror mirror : method.getAnnotationMirrors() )
424      {
425        final String classname = mirror.getAnnotationType().toString();
426        if ( classname.startsWith( "arez.annotations." ) )
427        {
428          throw new ProcessorException( "@Input target must not be annotated with any arez annotations but " +
429                                        "is annotated by '" + classname + "'.", method );
430        }
431      }
432    }
433  }
434
435  private void determineOnInputChangeMethods( @Nonnull final ViewDescriptor descriptor )
436  {
437    final List<ExecutableElement> methods =
438      getMethods( descriptor.getElement() ).stream()
439        .filter( m -> AnnotationsUtil.hasAnnotationOfType( m, Constants.ON_INPUT_CHANGE_CLASSNAME ) )
440        .collect( Collectors.toList() );
441
442    final ArrayList<OnInputChangeDescriptor> onInputChangeDescriptors = new ArrayList<>();
443    for ( final ExecutableElement method : methods )
444    {
445      final VariableElement phase = (VariableElement)
446        AnnotationsUtil.getAnnotationValue( method, Constants.ON_INPUT_CHANGE_CLASSNAME, "phase" ).getValue();
447      final boolean preUpdate = phase.getSimpleName().toString().equals( "PRE" );
448
449      final List<? extends VariableElement> parameters = method.getParameters();
450      final ExecutableType methodType = resolveMethodType( descriptor, method );
451      final List<? extends TypeMirror> parameterTypes = methodType.getParameterTypes();
452
453      MemberChecks.mustBeSubclassCallable( descriptor.getElement(),
454                                           Constants.VIEW_CLASSNAME,
455                                           Constants.ON_INPUT_CHANGE_CLASSNAME,
456                                           method );
457      MemberChecks.mustNotThrowAnyExceptions( Constants.ON_INPUT_CHANGE_CLASSNAME, method );
458      MemberChecks.mustNotReturnAnyValue( Constants.ON_INPUT_CHANGE_CLASSNAME, method );
459
460      final int parameterCount = parameters.size();
461      if ( 0 == parameterCount )
462      {
463        throw new ProcessorException( "@OnInputChange target must have at least 1 parameter.", method );
464      }
465      final List<InputDescriptor> inputDescriptors = new ArrayList<>( parameterCount );
466      for ( int i = 0; i < parameterCount; i++ )
467      {
468        final VariableElement parameter = parameters.get( i );
469        final String name = deriveOnInputChangeName( parameter );
470        final InputDescriptor input = descriptor.findInputNamed( name );
471        if ( null == input )
472        {
473          throw new ProcessorException( "@OnInputChange target has a parameter named '" +
474                                        parameter.getSimpleName() + "' and the parameter is associated with a " +
475                                        "@Input named '" + name + "' but there is no corresponding @Input " +
476                                        "annotated method.", parameter );
477        }
478        final Types typeUtils = processingEnv.getTypeUtils();
479        if ( !typeUtils.isAssignable( parameterTypes.get( i ), input.getMethodType().getReturnType() ) )
480        {
481          throw new ProcessorException( "@OnInputChange target has a parameter named '" +
482                                        parameter.getSimpleName() + "' and the parameter type is not " +
483                                        "assignable to the return type of the associated @Input annotated method.",
484                                        method );
485        }
486        final boolean mismatchedNullability =
487          (
488            AnnotationsUtil.hasNonnullAnnotation( parameter ) &&
489            AnnotationsUtil.hasNullableAnnotation( input.getMethod() )
490          ) ||
491          (
492            AnnotationsUtil.hasNullableAnnotation( parameter ) &&
493            input.isNonNull() );
494
495        if ( mismatchedNullability )
496        {
497          throw new ProcessorException( "@OnInputChange target has a parameter named '" +
498                                        parameter.getSimpleName() + "' that has a nullability annotation " +
499                                        "incompatible with the associated @Input method named " +
500                                        method.getSimpleName(), method );
501        }
502        if ( input.isImmutable() )
503        {
504          throw new ProcessorException( "@OnInputChange target has a parameter named '" +
505                                        parameter.getSimpleName() + "' that is associated with a @Input " +
506                                        "annotated method and the input is specified as immutable.", method );
507        }
508        inputDescriptors.add( input );
509      }
510      onInputChangeDescriptors.add( new OnInputChangeDescriptor( method, inputDescriptors, preUpdate ) );
511    }
512    descriptor.setOnInputChangeDescriptors( onInputChangeDescriptors );
513  }
514
515  @Nonnull
516  private String deriveOnInputChangeName( @Nonnull final VariableElement parameter )
517  {
518    final AnnotationValue value =
519      AnnotationsUtil.findAnnotationValue( parameter, Constants.INPUT_REF_CLASSNAME, "value" );
520
521    if ( null != value )
522    {
523      return (String) value.getValue();
524    }
525    else
526    {
527      final String parameterName = parameter.getSimpleName().toString();
528      if ( LAST_INPUT_PATTERN.matcher( parameterName ).matches() ||
529           PREV_INPUT_PATTERN.matcher( parameterName ).matches() )
530      {
531        return Character.toLowerCase( parameterName.charAt( 4 ) ) + parameterName.substring( 5 );
532      }
533      else if ( INPUT_PATTERN.matcher( parameterName ).matches() )
534      {
535        return parameterName;
536      }
537      else
538      {
539        throw new ProcessorException( "@OnInputChange target has a parameter named '" + parameterName +
540                                      "' is not explicitly associated with a input using @InputRef nor does it " +
541                                      "follow required naming conventions 'prev[MyInput]', 'last[MyInput]' or " +
542                                      "'[myInput]'.", parameter );
543      }
544    }
545  }
546
547  private void determineInputValidatesMethods( @Nonnull final ViewDescriptor descriptor )
548  {
549    final List<ExecutableElement> methods =
550      getMethods( descriptor.getElement() ).stream()
551        .filter( m -> AnnotationsUtil.hasAnnotationOfType( m, Constants.INPUT_VALIDATE_CLASSNAME ) )
552        .collect( Collectors.toList() );
553
554    for ( final ExecutableElement method : methods )
555    {
556      final String name = deriveInputValidateName( method );
557      final InputDescriptor input = descriptor.findInputNamed( name );
558      if ( null == input )
559      {
560        throw new ProcessorException( "@InputValidate target for input named '" + name + "' has no corresponding " +
561                                      "@Input annotated method.", method );
562      }
563      if ( 1 != method.getParameters().size() )
564      {
565        throw new ProcessorException( "@InputValidate target must have exactly 1 parameter", method );
566      }
567      final ExecutableType methodType = resolveMethodType( descriptor, method );
568      if ( !processingEnv.getTypeUtils().isAssignable( methodType.getParameterTypes().get( 0 ),
569                                                       input.getMethodType().getReturnType() ) )
570      {
571        throw new ProcessorException( "@InputValidate target has a parameter type that is not assignable to the " +
572                                      "return type of the associated @Input annotated method.", method );
573      }
574      MemberChecks.mustBeSubclassCallable( descriptor.getElement(),
575                                           Constants.VIEW_CLASSNAME,
576                                           Constants.INPUT_VALIDATE_CLASSNAME,
577                                           method );
578      MemberChecks.mustNotThrowAnyExceptions( Constants.INPUT_VALIDATE_CLASSNAME, method );
579      MemberChecks.mustNotReturnAnyValue( Constants.INPUT_VALIDATE_CLASSNAME, method );
580
581      final VariableElement param = method.getParameters().get( 0 );
582      final boolean mismatchedNullability =
583        (
584          AnnotationsUtil.hasNonnullAnnotation( param ) &&
585          AnnotationsUtil.hasNullableAnnotation( input.getMethod() )
586        ) ||
587        (
588          AnnotationsUtil.hasNullableAnnotation( param ) &&
589          input.isNonNull() );
590
591      if ( mismatchedNullability )
592      {
593        throw new ProcessorException( "@InputValidate target has a parameter that has a nullability annotation " +
594                                      "incompatible with the associated @Input method named " +
595                                      input.getMethod().getSimpleName(), method );
596      }
597      input.setValidateMethod( method );
598    }
599  }
600
601  @Nonnull
602  private String deriveInputValidateName( @Nonnull final Element element )
603    throws ProcessorException
604  {
605    final String name =
606      (String) AnnotationsUtil.getAnnotationValue( element, Constants.INPUT_VALIDATE_CLASSNAME, "name" )
607        .getValue();
608
609    if ( isSentinelName( name ) )
610    {
611      final String deriveName = deriveName( element, VALIDATE_INPUT_PATTERN, name );
612      if ( null == deriveName )
613      {
614        throw new ProcessorException( "@InputValidate target has not specified name nor is it named according " +
615                                      "to the convention 'validate[Name]Input'.", element );
616      }
617      return deriveName;
618    }
619    else
620    {
621      if ( !SourceVersion.isIdentifier( name ) )
622      {
623        throw new ProcessorException( "@InputValidate target specified an invalid name '" + name + "'. The " +
624                                      "name must be a valid java identifier.", element );
625      }
626      else if ( SourceVersion.isKeyword( name ) )
627      {
628        throw new ProcessorException( "@InputValidate target specified an invalid name '" + name + "'. The " +
629                                      "name must not be a java keyword.", element );
630      }
631      return name;
632    }
633  }
634
635  private void determineDefaultInputsMethods( @Nonnull final ViewDescriptor descriptor )
636  {
637    final List<ExecutableElement> defaultInputsMethods =
638      getMethods( descriptor.getElement() ).stream()
639        .filter( m -> AnnotationsUtil.hasAnnotationOfType( m, Constants.INPUT_DEFAULT_CLASSNAME ) )
640        .collect( Collectors.toList() );
641
642    for ( final ExecutableElement method : defaultInputsMethods )
643    {
644      final String name = deriveInputDefaultName( method );
645      final InputDescriptor input = descriptor.findInputNamed( name );
646      if ( null == input )
647      {
648        throw new ProcessorException( "@InputDefault target for input named '" + name + "' has no corresponding " +
649                                      "@Input annotated method.", method );
650      }
651      final ExecutableType methodType = resolveMethodType( descriptor, method );
652      if ( !processingEnv.getTypeUtils().isAssignable( methodType.getReturnType(),
653                                                       input.getMethodType().getReturnType() ) )
654      {
655        throw new ProcessorException( "@InputDefault target has a return type that is not assignable to the " +
656                                      "return type of the associated @Input annotated method.", method );
657      }
658      MemberChecks.mustBeStaticallySubclassCallable( descriptor.getElement(),
659                                                     Constants.VIEW_CLASSNAME,
660                                                     Constants.INPUT_DEFAULT_CLASSNAME,
661                                                     method );
662      MemberChecks.mustNotHaveAnyParameters( Constants.INPUT_DEFAULT_CLASSNAME, method );
663      MemberChecks.mustNotThrowAnyExceptions( Constants.INPUT_DEFAULT_CLASSNAME, method );
664      MemberChecks.mustReturnAValue( Constants.INPUT_DEFAULT_CLASSNAME, method );
665
666      input.setDefaultMethod( method );
667    }
668  }
669
670  private void determineDefaultInputsFields( @Nonnull final ViewDescriptor descriptor )
671  {
672    final List<VariableElement> defaultInputsFields =
673      ElementsUtil.getFields( descriptor.getElement() ).stream()
674        .filter( m -> AnnotationsUtil.hasAnnotationOfType( m, Constants.INPUT_DEFAULT_CLASSNAME ) )
675        .collect( Collectors.toList() );
676
677    for ( final VariableElement field : defaultInputsFields )
678    {
679      final String name = deriveInputDefaultName( field );
680      final InputDescriptor input = descriptor.findInputNamed( name );
681      if ( null == input )
682      {
683        throw new ProcessorException( "@InputDefault target for input named '" + name + "' has no corresponding " +
684                                      "@Input annotated method.", field );
685      }
686      if ( !processingEnv.getTypeUtils().isAssignable( field.asType(), input.getMethodType().getReturnType() ) )
687      {
688        throw new ProcessorException( "@InputDefault target has a type that is not assignable to the " +
689                                      "return type of the associated @Input annotated method.", field );
690      }
691      MemberChecks.mustBeStaticallySubclassCallable( descriptor.getElement(),
692                                                     Constants.VIEW_CLASSNAME,
693                                                     Constants.INPUT_DEFAULT_CLASSNAME,
694                                                     field );
695      MemberChecks.mustBeFinal( Constants.INPUT_DEFAULT_CLASSNAME, field );
696      input.setDefaultField( field );
697    }
698  }
699
700  @Nonnull
701  private String deriveInputDefaultName( @Nonnull final Element element )
702    throws ProcessorException
703  {
704    final String name =
705      (String) AnnotationsUtil.getAnnotationValue( element, Constants.INPUT_DEFAULT_CLASSNAME, "name" )
706        .getValue();
707
708    if ( isSentinelName( name ) )
709    {
710      if ( element instanceof ExecutableElement )
711      {
712        final String deriveName = deriveName( element, DEFAULT_GETTER_PATTERN, name );
713        if ( null == deriveName )
714        {
715          throw new ProcessorException( "@InputDefault target has not specified name nor is it named according " +
716                                        "to the convention 'get[Name]Default'.", element );
717        }
718        return deriveName;
719      }
720      else
721      {
722        final String fieldName = element.getSimpleName().toString();
723        boolean matched = true;
724        final int lengthPrefix = "DEFAULT_".length();
725        final int length = fieldName.length();
726        if ( fieldName.startsWith( "DEFAULT_" ) && length > lengthPrefix )
727        {
728          for ( int i = lengthPrefix; i < length; i++ )
729          {
730            final char ch = fieldName.charAt( i );
731            if ( Character.isLowerCase( ch ) ||
732                 (
733                   ( i != lengthPrefix || !Character.isJavaIdentifierStart( ch ) ) &&
734                   ( i == lengthPrefix || !Character.isJavaIdentifierPart( ch ) )
735                 ) )
736            {
737              matched = false;
738              break;
739            }
740          }
741        }
742        else
743        {
744          matched = false;
745        }
746        if ( matched )
747        {
748          return uppercaseConstantToPascalCase( fieldName.substring( lengthPrefix ) );
749        }
750        else
751        {
752          throw new ProcessorException( "@InputDefault target has not specified name nor is it named according " +
753                                        "to the convention 'DEFAULT_[NAME]'.", element );
754        }
755      }
756    }
757    else
758    {
759      if ( !SourceVersion.isIdentifier( name ) )
760      {
761        throw new ProcessorException( "@InputDefault target specified an invalid name '" + name + "'. The " +
762                                      "name must be a valid java identifier.", element );
763      }
764      else if ( SourceVersion.isKeyword( name ) )
765      {
766        throw new ProcessorException( "@InputDefault target specified an invalid name '" + name + "'. The " +
767                                      "name must not be a java keyword.", element );
768      }
769      return name;
770    }
771  }
772
773  @Nonnull
774  private String uppercaseConstantToPascalCase( @Nonnull final String candidate )
775  {
776    final String s = candidate.toLowerCase();
777    final StringBuilder sb = new StringBuilder();
778    boolean uppercase = false;
779    for ( int i = 0; i < s.length(); i++ )
780    {
781      final char ch = s.charAt( i );
782      if ( '_' == ch )
783      {
784        uppercase = true;
785      }
786      else if ( uppercase )
787      {
788        sb.append( Character.toUpperCase( ch ) );
789        uppercase = false;
790      }
791      else
792      {
793        sb.append( ch );
794      }
795    }
796    return sb.toString();
797  }
798
799  private void determineInputs( @Nonnull final ViewDescriptor descriptor )
800  {
801    final List<InputDescriptor> inputs =
802      getMethods( descriptor.getElement() ).stream()
803        .filter( m -> AnnotationsUtil.hasAnnotationOfType( m, Constants.INPUT_CLASSNAME ) )
804        .map( m -> createInputDescriptor( descriptor, m ) )
805        .collect( Collectors.toList() );
806
807    final InputDescriptor childrenInput =
808      inputs.stream().filter( p -> p.getName().equals( "children" ) ).findAny().orElse( null );
809    final InputDescriptor childInput =
810      inputs.stream().filter( p -> p.getName().equals( "child" ) ).findAny().orElse( null );
811    if ( null != childrenInput && null != childInput )
812    {
813      throw new ProcessorException( "Multiple candidate children @Input annotated methods: " +
814                                    childrenInput.getMethod().getSimpleName() + " and " +
815                                    childInput.getMethod().getSimpleName(),
816                                    childrenInput.getMethod() );
817    }
818
819    descriptor.setInputs( inputs );
820  }
821
822  private boolean isInputRequired( @Nonnull final InputDescriptor input )
823  {
824    final String requiredValue = input.getRequiredValue();
825    if ( "ENABLE".equals( requiredValue ) )
826    {
827      return true;
828    }
829    else if ( "DISABLE".equals( requiredValue ) )
830    {
831      return false;
832    }
833    else if ( input.isContextSource() )
834    {
835      return false;
836    }
837    else
838    {
839      return !input.hasDefaultMethod() &&
840             !input.hasDefaultField() &&
841             !AnnotationsUtil.hasNullableAnnotation( input.getMethod() );
842    }
843  }
844
845  @Nonnull
846  private InputDescriptor createInputDescriptor( @Nonnull final ViewDescriptor descriptor,
847                                                 @Nonnull final ExecutableElement method )
848  {
849    final String name = deriveInputName( method );
850    final ExecutableType methodType = resolveMethodType( descriptor, method );
851
852    verifyNoDuplicateAnnotations( method );
853    MemberChecks.mustBeAbstract( Constants.INPUT_CLASSNAME, method );
854    MemberChecks.mustNotHaveAnyParameters( Constants.INPUT_CLASSNAME, method );
855    MemberChecks.mustReturnAValue( Constants.INPUT_CLASSNAME, method );
856    MemberChecks.mustNotThrowAnyExceptions( Constants.INPUT_CLASSNAME, method );
857    MemberChecks.mustNotBePackageAccessInDifferentPackage( descriptor.getElement(),
858                                                           Constants.VIEW_CLASSNAME,
859                                                           Constants.INPUT_CLASSNAME,
860                                                           method );
861    final TypeMirror returnType = method.getReturnType();
862    if ( "build".equals( name ) )
863    {
864      throw new ProcessorException( "@Input named 'build' is invalid as it conflicts with the method named " +
865                                    "build() that is used in the generated Builder classes",
866                                    method );
867    }
868    else if ( "child".equals( name ) &&
869              ( returnType.getKind() != TypeKind.DECLARED && !"react4j.ReactNode".equals( returnType.toString() ) ) )
870    {
871      throw new ProcessorException( "@Input named 'child' should be of type react4j.ReactNode", method );
872    }
873    else if ( "children".equals( name ) &&
874              ( returnType.getKind() != TypeKind.DECLARED && !"react4j.ReactNode[]".equals( returnType.toString() ) ) )
875    {
876      throw new ProcessorException( "@Input named 'children' should be of type react4j.ReactNode[]", method );
877    }
878
879    if ( returnType instanceof TypeVariable )
880    {
881      final TypeVariable typeVariable = (TypeVariable) returnType;
882      final String typeVariableName = typeVariable.asElement().getSimpleName().toString();
883      List<? extends TypeParameterElement> typeParameters = method.getTypeParameters();
884      if ( typeParameters.stream().anyMatch( p -> p.getSimpleName().toString().equals( typeVariableName ) ) )
885      {
886        throw new ProcessorException( "@Input named '" + name + "' is has a type variable as a return type " +
887                                      "that is declared on the method.", method );
888      }
889    }
890    final String qualifier = (String) AnnotationsUtil
891      .getAnnotationValue( method, Constants.INPUT_CLASSNAME, "qualifier" ).getValue();
892    final boolean contextInput = isContextInput( method );
893    final Element inputType = processingEnv.getTypeUtils().asElement( returnType );
894    final boolean immutable = isInputImmutable( method );
895    final boolean observable = isInputObservable( descriptor, method, immutable );
896    final boolean disposable = null != inputType && isInputDisposable( method, inputType );
897    final TypeName typeName = TypeName.get( returnType );
898    if ( typeName.isBoxedPrimitive() && AnnotationsUtil.hasNonnullAnnotation( method ) )
899    {
900      throw new ProcessorException( "@Input named '" + name + "' is a boxed primitive annotated with a " +
901                                    "@Nonnull annotation. The return type should be the primitive type.",
902                                    method );
903    }
904    final ImmutableInputKeyStrategy strategy = immutable ? getImmutableInputKeyStrategy( typeName, inputType ) : null;
905    if ( !"".equals( qualifier ) && !contextInput )
906    {
907      throw new ProcessorException( MemberChecks.mustNot( Constants.INPUT_CLASSNAME,
908                                                          "specify qualifier parameter unless source=CONTEXT is also specified" ),
909                                    method );
910    }
911    final String requiredValue =
912      ( (VariableElement) AnnotationsUtil.getAnnotationValue( method, Constants.INPUT_CLASSNAME, "require" )
913        .getValue() )
914        .getSimpleName().toString();
915
916    final boolean dependency = isInputDependency( method, immutable, disposable );
917
918    final InputDescriptor inputDescriptor =
919      new InputDescriptor( descriptor,
920                           name,
921                           qualifier,
922                           method,
923                           methodType,
924                           inputType,
925                           contextInput,
926                           !immutable,
927                           observable,
928                           disposable,
929                           dependency,
930                           strategy,
931                           requiredValue );
932    if ( inputDescriptor.mayNeedMutableInputAccessedInPostConstructInvariant() )
933    {
934      if ( ElementsUtil.isWarningSuppressed( method,
935                                             Constants.WARNING_MUTABLE_INPUT_ACCESSED_IN_POST_CONSTRUCT,
936                                             Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) )
937      {
938        inputDescriptor.suppressMutableInputAccessedInPostConstruct();
939      }
940    }
941    return inputDescriptor;
942  }
943
944  @Nonnull
945  private ImmutableInputKeyStrategy getImmutableInputKeyStrategy( @Nonnull final TypeName typeName,
946                                                                  @Nullable final Element element )
947  {
948    if ( typeName.toString().equals( "java.lang.String" ) )
949    {
950      return ImmutableInputKeyStrategy.IS_STRING;
951    }
952    else if ( typeName.isBoxedPrimitive() || typeName.isPrimitive() )
953    {
954      return ImmutableInputKeyStrategy.TO_STRING;
955    }
956    else if ( null != element )
957    {
958      if ( ( ElementKind.CLASS == element.getKind() || ElementKind.INTERFACE == element.getKind() ) &&
959           isAssignableToKeyed( element ) )
960      {
961        return ImmutableInputKeyStrategy.KEYED;
962      }
963      else if ( ( ElementKind.CLASS == element.getKind() || ElementKind.INTERFACE == element.getKind() ) &&
964                (
965                  isAssignableToIdentifiable( element ) ||
966                  AnnotationsUtil.hasAnnotationOfType( element, Constants.ACT_AS_COMPONENT_CLASSNAME ) ||
967                  ( AnnotationsUtil.hasAnnotationOfType( element, Constants.AREZ_COMPONENT_CLASSNAME ) &&
968                    isIdRequired( (TypeElement) element ) )
969                ) )
970      {
971        return ImmutableInputKeyStrategy.AREZ_IDENTIFIABLE;
972      }
973      else if ( ElementKind.ENUM == element.getKind() )
974      {
975        return ImmutableInputKeyStrategy.ENUM;
976      }
977    }
978    return ImmutableInputKeyStrategy.DYNAMIC;
979  }
980
981  private boolean isAssignableToKeyed( @Nonnull final Element element )
982  {
983    final TypeElement typeElement = processingEnv.getElementUtils().getTypeElement( Constants.KEYED_CLASSNAME );
984    return processingEnv.getTypeUtils().isAssignable( element.asType(), typeElement.asType() );
985  }
986
987  private boolean isAssignableToIdentifiable( @Nonnull final Element element )
988  {
989    final TypeElement typeElement = processingEnv.getElementUtils().getTypeElement( Constants.IDENTIFIABLE_CLASSNAME );
990    final TypeMirror identifiableErasure = processingEnv.getTypeUtils().erasure( typeElement.asType() );
991    return processingEnv.getTypeUtils().isAssignable( element.asType(), identifiableErasure );
992  }
993
994  /**
995   * The logic from this method has been cloned from Arez.
996   * One day we should consider improving Arez so that this is not required somehow?
997   */
998  private boolean isIdRequired( @Nonnull final TypeElement element )
999  {
1000    final VariableElement requireIdParameter = (VariableElement)
1001      AnnotationsUtil.getAnnotationValue( element, Constants.AREZ_COMPONENT_CLASSNAME, "requireId" )
1002        .getValue();
1003    return !"DISABLE".equals( requireIdParameter.getSimpleName().toString() );
1004  }
1005
1006  @Nonnull
1007  private String deriveInputName( @Nonnull final ExecutableElement method )
1008    throws ProcessorException
1009  {
1010    final String specifiedName =
1011      (String) AnnotationsUtil.getAnnotationValue( method, Constants.INPUT_CLASSNAME, "name" ).getValue();
1012
1013    final String name = getPropertyAccessorName( method, specifiedName );
1014    if ( !SourceVersion.isIdentifier( name ) )
1015    {
1016      throw new ProcessorException( "@Input target specified an invalid name '" + specifiedName + "'. The " +
1017                                    "name must be a valid java identifier.", method );
1018    }
1019    else if ( SourceVersion.isKeyword( name ) )
1020    {
1021      throw new ProcessorException( "@Input target specified an invalid name '" + specifiedName + "'. The " +
1022                                    "name must not be a java keyword.", method );
1023    }
1024    else
1025    {
1026      return name;
1027    }
1028  }
1029
1030  private void determineOnErrorMethod( @Nonnull final TypeElement typeElement,
1031                                       @Nonnull final ViewDescriptor descriptor )
1032  {
1033    for ( final ExecutableElement method : getMethods( typeElement ) )
1034    {
1035      if ( AnnotationsUtil.hasAnnotationOfType( method, Constants.ON_ERROR_CLASSNAME ) )
1036      {
1037        MemberChecks.mustNotBeAbstract( Constants.ON_ERROR_CLASSNAME, method );
1038        MemberChecks.mustBeSubclassCallable( typeElement,
1039                                             Constants.VIEW_CLASSNAME,
1040                                             Constants.ON_ERROR_CLASSNAME,
1041                                             method );
1042        MemberChecks.mustNotReturnAnyValue( Constants.ON_ERROR_CLASSNAME, method );
1043        MemberChecks.mustNotThrowAnyExceptions( Constants.ON_ERROR_CLASSNAME, method );
1044
1045        boolean infoFound = false;
1046        boolean errorFound = false;
1047        for ( final VariableElement parameter : method.getParameters() )
1048        {
1049          final TypeName typeName = TypeName.get( parameter.asType() );
1050          if ( typeName.toString().equals( Constants.ERROR_INFO_CLASSNAME ) )
1051          {
1052            if ( infoFound )
1053            {
1054              throw new ProcessorException( "@OnError target has multiple parameters of type " +
1055                                            Constants.ERROR_INFO_CLASSNAME,
1056                                            method );
1057            }
1058            infoFound = true;
1059          }
1060          else if ( typeName.toString().equals( Constants.JS_ERROR_CLASSNAME ) )
1061          {
1062            if ( errorFound )
1063            {
1064              throw new ProcessorException( "@OnError target has multiple parameters of type " +
1065                                            Constants.JS_ERROR_CLASSNAME,
1066                                            method );
1067            }
1068            errorFound = true;
1069          }
1070          else
1071          {
1072            throw new ProcessorException( "@OnError target has parameter of invalid type named " +
1073                                          parameter.getSimpleName().toString(),
1074                                          parameter );
1075          }
1076        }
1077        descriptor.setOnError( method );
1078      }
1079    }
1080  }
1081
1082  private void determineScheduleRenderMethods( @Nonnull final TypeElement typeElement,
1083                                               @Nonnull final ViewDescriptor descriptor )
1084  {
1085    final List<ScheduleRenderDescriptor> scheduleRenderDescriptors = new ArrayList<>();
1086    for ( final ExecutableElement method : getMethods( typeElement ) )
1087    {
1088      final AnnotationMirror annotation =
1089        AnnotationsUtil.findAnnotationByType( method, Constants.SCHEDULE_RENDER_CLASSNAME );
1090      if ( null != annotation )
1091      {
1092        MemberChecks.mustBeAbstract( Constants.SCHEDULE_RENDER_CLASSNAME, method );
1093        MemberChecks.mustBeSubclassCallable( typeElement,
1094                                             Constants.VIEW_CLASSNAME,
1095                                             Constants.SCHEDULE_RENDER_CLASSNAME,
1096                                             method );
1097        MemberChecks.mustNotReturnAnyValue( Constants.SCHEDULE_RENDER_CLASSNAME, method );
1098        MemberChecks.mustNotThrowAnyExceptions( Constants.SCHEDULE_RENDER_CLASSNAME, method );
1099
1100        final ViewType viewType = descriptor.getType();
1101        if ( ViewType.STATEFUL != viewType )
1102        {
1103          final String message =
1104            MemberChecks.mustNot( Constants.SCHEDULE_RENDER_CLASSNAME,
1105                                  "be enclosed in a type if it is annotated by @View(type=" + viewType +
1106                                  "). The type must be STATEFUL" );
1107          throw new ProcessorException( message, method );
1108        }
1109
1110        final boolean skipShouldViewUpdate =
1111          AnnotationsUtil.getAnnotationValueValue( annotation, "skipShouldViewUpdate" );
1112
1113        scheduleRenderDescriptors.add( new ScheduleRenderDescriptor( method, skipShouldViewUpdate ) );
1114      }
1115    }
1116    descriptor.setScheduleRenderDescriptors( scheduleRenderDescriptors );
1117  }
1118
1119  private void determinePublishMethods( @Nonnull final TypeElement typeElement,
1120                                        @Nonnull final ViewDescriptor descriptor )
1121  {
1122    final List<PublishDescriptor> descriptors = new ArrayList<>();
1123    for ( final ExecutableElement method : getMethods( typeElement ) )
1124    {
1125      final AnnotationMirror annotation = AnnotationsUtil.findAnnotationByType( method, Constants.PUBLISH_CLASSNAME );
1126      if ( null != annotation )
1127      {
1128        MemberChecks.mustBeSubclassCallable( typeElement,
1129                                             Constants.VIEW_CLASSNAME,
1130                                             Constants.PUBLISH_CLASSNAME,
1131                                             method );
1132        MemberChecks.mustNotHaveAnyParameters( Constants.PUBLISH_CLASSNAME, method );
1133        MemberChecks.mustNotHaveAnyTypeParameters( Constants.PUBLISH_CLASSNAME, method );
1134        MemberChecks.mustReturnAValue( Constants.PUBLISH_CLASSNAME, method );
1135        MemberChecks.mustNotThrowAnyExceptions( Constants.PUBLISH_CLASSNAME, method );
1136
1137        final String qualifier = AnnotationsUtil.getAnnotationValueValue( annotation, "qualifier" );
1138        final ExecutableType methodType = resolveMethodType( descriptor, method );
1139
1140        if ( TypeKind.TYPEVAR == methodType.getReturnType().getKind() )
1141        {
1142          throw new ProcessorException( MemberChecks.mustNot( Constants.PUBLISH_CLASSNAME, "return a type variable" ),
1143                                        method );
1144        }
1145
1146        descriptors.add( new PublishDescriptor( qualifier, method, methodType ) );
1147      }
1148    }
1149    descriptor.setPublishDescriptors( descriptors );
1150  }
1151
1152  private void determineRenderMethod( @Nonnull final TypeElement typeElement,
1153                                      @Nonnull final ViewDescriptor descriptor )
1154  {
1155    boolean foundRender = false;
1156    for ( final ExecutableElement method : getMethods( typeElement ) )
1157    {
1158      final AnnotationMirror annotation =
1159        AnnotationsUtil.findAnnotationByType( method, Constants.RENDER_CLASSNAME );
1160      if ( null != annotation )
1161      {
1162        MemberChecks.mustNotBeAbstract( Constants.RENDER_CLASSNAME, method );
1163        MemberChecks.mustBeSubclassCallable( typeElement,
1164                                             Constants.VIEW_CLASSNAME,
1165                                             Constants.RENDER_CLASSNAME,
1166                                             method );
1167        MemberChecks.mustNotHaveAnyParameters( Constants.RENDER_CLASSNAME, method );
1168        MemberChecks.mustReturnAnInstanceOf( processingEnv,
1169                                             method,
1170                                             Constants.RENDER_CLASSNAME,
1171                                             Constants.VNODE_CLASSNAME );
1172        MemberChecks.mustNotThrowAnyExceptions( Constants.RENDER_CLASSNAME, method );
1173        MemberChecks.mustNotHaveAnyTypeParameters( Constants.RENDER_CLASSNAME, method );
1174
1175        descriptor.setRender( method );
1176        foundRender = true;
1177      }
1178    }
1179    final boolean requireRender = descriptor.requireRender();
1180    if ( requireRender && !foundRender )
1181    {
1182      throw new ProcessorException( MemberChecks.must( Constants.VIEW_CLASSNAME,
1183                                                       "contain a method annotated with the " +
1184                                                       MemberChecks.toSimpleName( Constants.RENDER_CLASSNAME ) +
1185                                                       " annotation or must specify type=NO_RENDER" ),
1186                                    typeElement );
1187    }
1188    else if ( !requireRender )
1189    {
1190      if ( foundRender )
1191      {
1192        throw new ProcessorException( MemberChecks.mustNot( Constants.VIEW_CLASSNAME,
1193                                                            "contain a method annotated with the " +
1194                                                            MemberChecks.toSimpleName( Constants.RENDER_CLASSNAME ) +
1195                                                            " annotation or must not specify type=NO_RENDER" ),
1196                                      typeElement );
1197      }
1198      else if ( !descriptor.hasConstructor() &&
1199                !descriptor.hasPostConstruct() &&
1200                null == descriptor.getPostMount() &&
1201                null == descriptor.getPostRender() &&
1202                null == descriptor.getPreUpdate() &&
1203                null == descriptor.getPostUpdate() &&
1204                !descriptor.hasPreUpdateOnInputChange() &&
1205                !descriptor.hasPostUpdateOnInputChange() )
1206      {
1207        throw new ProcessorException( MemberChecks.must( Constants.VIEW_CLASSNAME,
1208                                                         "contain lifecycle methods if the the @View(type=NO_RENDER) parameter is specified" ),
1209                                      typeElement );
1210      }
1211    }
1212  }
1213
1214  private void determinePostMountMethod( @Nonnull final TypeElement typeElement,
1215                                         @Nonnull final ViewDescriptor descriptor )
1216  {
1217    for ( final ExecutableElement method : getMethods( typeElement ) )
1218    {
1219      if ( AnnotationsUtil.hasAnnotationOfType( method, Constants.POST_MOUNT_CLASSNAME ) )
1220      {
1221        MemberChecks.mustBeLifecycleHook( typeElement,
1222                                          Constants.VIEW_CLASSNAME,
1223                                          Constants.POST_MOUNT_CLASSNAME,
1224                                          method );
1225        descriptor.setPostMount( method );
1226      }
1227    }
1228  }
1229
1230  private void determinePostMountOrUpdateMethod( @Nonnull final TypeElement typeElement,
1231                                                 @Nonnull final ViewDescriptor descriptor )
1232  {
1233    for ( final ExecutableElement method : getMethods( typeElement ) )
1234    {
1235      if ( AnnotationsUtil.hasAnnotationOfType( method, Constants.POST_MOUNT_OR_UPDATE_CLASSNAME ) )
1236      {
1237        MemberChecks.mustBeLifecycleHook( typeElement,
1238                                          Constants.VIEW_CLASSNAME,
1239                                          Constants.POST_MOUNT_OR_UPDATE_CLASSNAME,
1240                                          method );
1241        descriptor.setPostRender( method );
1242      }
1243    }
1244  }
1245
1246  private void determinePostUpdateMethod( @Nonnull final TypeElement typeElement,
1247                                          @Nonnull final ViewDescriptor descriptor )
1248  {
1249    for ( final ExecutableElement method : getMethods( typeElement ) )
1250    {
1251      if ( AnnotationsUtil.hasAnnotationOfType( method, Constants.POST_UPDATE_CLASSNAME ) )
1252      {
1253        MemberChecks.mustBeLifecycleHook( typeElement,
1254                                          Constants.VIEW_CLASSNAME,
1255                                          Constants.POST_UPDATE_CLASSNAME,
1256                                          method );
1257        descriptor.setPostUpdate( method );
1258      }
1259    }
1260  }
1261
1262  private void determinePreUpdateMethod( @Nonnull final TypeElement typeElement,
1263                                         @Nonnull final ViewDescriptor descriptor )
1264  {
1265    for ( final ExecutableElement method : getMethods( typeElement ) )
1266    {
1267      if ( AnnotationsUtil.hasAnnotationOfType( method, Constants.PRE_UPDATE_CLASSNAME ) )
1268      {
1269        MemberChecks.mustBeLifecycleHook( typeElement,
1270                                          Constants.VIEW_CLASSNAME,
1271                                          Constants.PRE_UPDATE_CLASSNAME,
1272                                          method );
1273        descriptor.setPreUpdate( method );
1274      }
1275    }
1276  }
1277
1278  private ExecutableType resolveMethodType( @Nonnull final ViewDescriptor descriptor,
1279                                            @Nonnull final ExecutableElement method )
1280  {
1281    return (ExecutableType) processingEnv.getTypeUtils().asMemberOf( descriptor.getDeclaredType(), method );
1282  }
1283
1284  @Nonnull
1285  private String deriveViewName( @Nonnull final TypeElement typeElement )
1286  {
1287    final String name =
1288      (String) AnnotationsUtil.getAnnotationValue( typeElement, Constants.VIEW_CLASSNAME, "name" )
1289        .getValue();
1290
1291    if ( isSentinelName( name ) )
1292    {
1293      return typeElement.getSimpleName().toString();
1294    }
1295    else
1296    {
1297      if ( !SourceVersion.isIdentifier( name ) )
1298      {
1299        throw new ProcessorException( MemberChecks.toSimpleName( Constants.VIEW_CLASSNAME ) +
1300                                      " target specified an invalid name '" + name + "'. The " +
1301                                      "name must be a valid java identifier.", typeElement );
1302      }
1303      else if ( SourceVersion.isKeyword( name ) )
1304      {
1305        throw new ProcessorException( MemberChecks.toSimpleName( Constants.VIEW_CLASSNAME ) +
1306                                      " target specified an invalid name '" + name + "'. The " +
1307                                      "name must not be a java keyword.", typeElement );
1308      }
1309      return name;
1310    }
1311  }
1312
1313  private void determineViewCapabilities( @Nonnull final ViewDescriptor descriptor,
1314                                          @Nonnull final TypeElement typeElement )
1315  {
1316    if ( AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.AREZ_COMPONENT_CLASSNAME ) )
1317    {
1318      throw new ProcessorException( MemberChecks.mustNot( Constants.VIEW_CLASSNAME,
1319                                                          "be annotated with the " +
1320                                                          MemberChecks.toSimpleName( Constants.AREZ_COMPONENT_CLASSNAME ) +
1321                                                          " as React4j will add the annotation." ),
1322                                    typeElement );
1323    }
1324
1325    if ( descriptor.needsInjection() && !descriptor.getDeclaredType().getTypeArguments().isEmpty() )
1326    {
1327      throw new ProcessorException( MemberChecks.toSimpleName( Constants.VIEW_CLASSNAME ) +
1328                                    " target has enabled injection integration but the class " +
1329                                    "has type arguments which is incompatible with injection integration.",
1330                                    typeElement );
1331    }
1332  }
1333
1334  @Nonnull
1335  private ViewType extractViewType( @Nonnull final TypeElement typeElement )
1336  {
1337    final VariableElement declaredTypeEnum = (VariableElement)
1338      AnnotationsUtil
1339        .getAnnotationValue( typeElement, Constants.VIEW_CLASSNAME, "type" )
1340        .getValue();
1341    return ViewType.valueOf( declaredTypeEnum.getSimpleName().toString() );
1342  }
1343
1344  private boolean isInputObservable( @Nonnull final ViewDescriptor descriptor,
1345                                     @Nonnull final ExecutableElement method,
1346                                     final boolean immutable )
1347  {
1348    final VariableElement parameter = (VariableElement)
1349      AnnotationsUtil.getAnnotationValue( method, Constants.INPUT_CLASSNAME, "observable" ).getValue();
1350    switch ( parameter.getSimpleName().toString() )
1351    {
1352      case "ENABLE":
1353        if ( immutable )
1354        {
1355          throw new ProcessorException( "@Input target has specified both immutable=true and " +
1356                                        "observable=ENABLE which is an invalid combination.",
1357                                        method );
1358        }
1359        return true;
1360      case "DISABLE":
1361        return false;
1362      default:
1363        return hasAnyArezObserverMethods( descriptor.getElement() );
1364    }
1365  }
1366
1367  private boolean hasAnyArezObserverMethods( @Nonnull final TypeElement typeElement )
1368  {
1369    return getMethods( typeElement )
1370      .stream()
1371      .anyMatch( m -> AnnotationsUtil.hasAnnotationOfType( m, Constants.MEMOIZE_CLASSNAME ) ||
1372                      ( AnnotationsUtil.hasAnnotationOfType( m, Constants.OBSERVE_CLASSNAME ) &&
1373                        ( !m.getParameters().isEmpty() || !m.getSimpleName().toString().equals( "trackRender" ) ) ) );
1374  }
1375
1376  private boolean isInputImmutable( @Nonnull final ExecutableElement method )
1377  {
1378    return (Boolean) AnnotationsUtil.getAnnotationValue( method, Constants.INPUT_CLASSNAME, "immutable" )
1379      .getValue();
1380  }
1381
1382  private boolean isInputDependency( @Nonnull final ExecutableElement method,
1383                                     final boolean immutable,
1384                                     final boolean disposable )
1385  {
1386    final VariableElement parameter = (VariableElement)
1387      AnnotationsUtil.getAnnotationValue( method, Constants.INPUT_CLASSNAME, "dependency" ).getValue();
1388    switch ( parameter.getSimpleName().toString() )
1389    {
1390      case "ENABLE":
1391        if ( !immutable )
1392        {
1393          throw new ProcessorException( "@Input target must be immutable if dependency=ENABLE is specified",
1394                                        method );
1395        }
1396        else if ( !disposable )
1397        {
1398          throw new ProcessorException( "@Input target must be disposable if dependency=ENABLE is specified",
1399                                        method );
1400        }
1401        return true;
1402      case "DISABLE":
1403        return false;
1404      default:
1405        return immutable && disposable;
1406    }
1407  }
1408
1409  private boolean isInputDisposable( @Nonnull final ExecutableElement method, @Nonnull final Element inputType )
1410  {
1411    final VariableElement parameter = (VariableElement)
1412      AnnotationsUtil.getAnnotationValue( method, Constants.INPUT_CLASSNAME, "disposable" ).getValue();
1413    switch ( parameter.getSimpleName().toString() )
1414    {
1415      case "ENABLE":
1416        return true;
1417      case "DISABLE":
1418        return false;
1419      default:
1420        return
1421          (
1422            ElementKind.CLASS == inputType.getKind() &&
1423            AnnotationsUtil.hasAnnotationOfType( inputType, Constants.AREZ_COMPONENT_CLASSNAME )
1424          ) ||
1425          (
1426            ( ElementKind.CLASS == inputType.getKind() || ElementKind.INTERFACE == inputType.getKind() ) &&
1427            AnnotationsUtil.hasAnnotationOfType( inputType, Constants.ACT_AS_COMPONENT_CLASSNAME )
1428          );
1429    }
1430  }
1431
1432  private boolean isContextInput( @Nonnull final ExecutableElement method )
1433  {
1434    final VariableElement parameter = (VariableElement)
1435      AnnotationsUtil.getAnnotationValue( method, Constants.INPUT_CLASSNAME, "source" ).getValue();
1436    return "CONTEXT".equals( parameter.getSimpleName().toString() );
1437  }
1438
1439  private boolean shouldSetDefaultPriority( @Nonnull final TypeElement typeElement )
1440  {
1441    final List<ExecutableElement> methods = getMethods( typeElement );
1442    return methods
1443      .stream()
1444      .filter( method -> !method.getModifiers().contains( Modifier.PRIVATE ) )
1445      .anyMatch( method -> AnnotationsUtil.hasAnnotationOfType( method, Constants.MEMOIZE_CLASSNAME ) ||
1446                           AnnotationsUtil.hasAnnotationOfType( method, Constants.OBSERVE_CLASSNAME ) );
1447  }
1448
1449  private void verifyNoDuplicateAnnotations( @Nonnull final ExecutableElement method )
1450    throws ProcessorException
1451  {
1452    final List<String> annotations =
1453      Arrays.asList( Constants.INPUT_DEFAULT_CLASSNAME,
1454                     Constants.INPUT_VALIDATE_CLASSNAME,
1455                     Constants.ON_INPUT_CHANGE_CLASSNAME,
1456                     Constants.INPUT_CLASSNAME );
1457    MemberChecks.verifyNoOverlappingAnnotations( method, annotations, Collections.emptyMap() );
1458  }
1459
1460  @Nonnull
1461  private List<ExecutableElement> getMethods( @Nonnull final TypeElement typeElement )
1462  {
1463    return ElementsUtil.getMethods( typeElement, processingEnv.getElementUtils(), processingEnv.getTypeUtils() );
1464  }
1465
1466  private boolean isSentinelName( @Nonnull final String name )
1467  {
1468    return SENTINEL_NAME.equals( name );
1469  }
1470
1471  @Nonnull
1472  private String getPropertyAccessorName( @Nonnull final ExecutableElement method,
1473                                          @Nonnull final String specifiedName )
1474    throws ProcessorException
1475  {
1476    String name = deriveName( method, GETTER_PATTERN, specifiedName );
1477    if ( null != name )
1478    {
1479      return name;
1480    }
1481    else if ( method.getReturnType().getKind() == TypeKind.BOOLEAN )
1482    {
1483      name = deriveName( method, ISSER_PATTERN, specifiedName );
1484      if ( null != name )
1485      {
1486        return name;
1487      }
1488    }
1489    return method.getSimpleName().toString();
1490  }
1491
1492  @Nullable
1493  private String deriveName( @Nonnull final Element method, @Nonnull final Pattern pattern, @Nonnull final String name )
1494    throws ProcessorException
1495  {
1496    if ( isSentinelName( name ) )
1497    {
1498      final String methodName = method.getSimpleName().toString();
1499      final Matcher matcher = pattern.matcher( methodName );
1500      if ( matcher.find() )
1501      {
1502        final String candidate = matcher.group( 1 );
1503        return Character.toLowerCase( candidate.charAt( 0 ) ) + candidate.substring( 1 );
1504      }
1505      else
1506      {
1507        return null;
1508      }
1509    }
1510    else
1511    {
1512      return name;
1513    }
1514  }
1515}