001package arez.processor;
002
003import com.squareup.javapoet.ParameterizedTypeName;
004import com.squareup.javapoet.TypeName;
005import java.io.IOException;
006import java.util.ArrayList;
007import java.util.Arrays;
008import java.util.Collection;
009import java.util.Collections;
010import java.util.HashMap;
011import java.util.HashSet;
012import java.util.List;
013import java.util.Map;
014import java.util.Objects;
015import java.util.Set;
016import java.util.function.Function;
017import java.util.regex.Matcher;
018import java.util.regex.Pattern;
019import java.util.regex.PatternSyntaxException;
020import javax.annotation.Nonnull;
021import javax.annotation.Nullable;
022import javax.annotation.processing.ProcessingEnvironment;
023import javax.annotation.processing.RoundEnvironment;
024import javax.annotation.processing.SupportedAnnotationTypes;
025import javax.annotation.processing.SupportedOptions;
026import javax.annotation.processing.SupportedSourceVersion;
027import javax.lang.model.SourceVersion;
028import javax.lang.model.element.AnnotationMirror;
029import javax.lang.model.element.AnnotationValue;
030import javax.lang.model.element.Element;
031import javax.lang.model.element.ElementKind;
032import javax.lang.model.element.ExecutableElement;
033import javax.lang.model.element.Modifier;
034import javax.lang.model.element.TypeElement;
035import javax.lang.model.element.VariableElement;
036import javax.lang.model.type.DeclaredType;
037import javax.lang.model.type.ExecutableType;
038import javax.lang.model.type.TypeKind;
039import javax.lang.model.type.TypeMirror;
040import javax.lang.model.util.Elements;
041import javax.lang.model.util.Types;
042import javax.tools.Diagnostic;
043import org.realityforge.proton.AbstractStandardProcessor;
044import org.realityforge.proton.AnnotationsUtil;
045import org.realityforge.proton.DeferredElementSet;
046import org.realityforge.proton.ElementsUtil;
047import org.realityforge.proton.MemberChecks;
048import org.realityforge.proton.ProcessorException;
049import org.realityforge.proton.StopWatch;
050import org.realityforge.proton.SuperficialValidation;
051import org.realityforge.proton.TypesUtil;
052import static javax.tools.Diagnostic.Kind.*;
053
054/**
055 * Annotation processor that analyzes Arez annotated source and generates models from the annotations.
056 */
057@SupportedAnnotationTypes( Constants.COMPONENT_CLASSNAME )
058@SupportedSourceVersion( SourceVersion.RELEASE_17 )
059@SupportedOptions( { "arez.defer.unresolved",
060                     "arez.defer.errors",
061                     "arez.debug",
062                     "arez.profile",
063                     "arez.verbose_out_of_round.errors" } )
064public final class ArezProcessor
065  extends AbstractStandardProcessor
066{
067  @Nonnull
068  static final Pattern GETTER_PATTERN = Pattern.compile( "^get([A-Z].*)$" );
069  @Nonnull
070  private static final Pattern ON_ACTIVATE_PATTERN = Pattern.compile( "^on([A-Z].*)Activate$" );
071  @Nonnull
072  private static final Pattern ON_DEACTIVATE_PATTERN = Pattern.compile( "^on([A-Z].*)Deactivate$" );
073  @Nonnull
074  private static final Pattern SETTER_PATTERN = Pattern.compile( "^set([A-Z].*)$" );
075  @Nonnull
076  private static final Pattern ISSER_PATTERN = Pattern.compile( "^is([A-Z].*)$" );
077  @Nonnull
078  private static final List<String> OBJECT_METHODS =
079    Arrays.asList( "hashCode", "equals", "clone", "toString", "finalize", "getClass", "wait", "notifyAll", "notify" );
080  @Nonnull
081  private static final List<String> AREZ_SPECIAL_METHODS =
082    Arrays.asList( "observe", "dispose", "isDisposed", "getArezId" );
083  @Nonnull
084  private static final Pattern ID_GETTER_PATTERN = Pattern.compile( "^get([A-Z].*)Id$" );
085  @Nonnull
086  private static final Pattern RAW_ID_GETTER_PATTERN = Pattern.compile( "^(.*)Id$" );
087  @Nonnull
088  private static final Pattern OBSERVABLE_REF_PATTERN = Pattern.compile( "^get([A-Z].*)ObservableValue$" );
089  @Nonnull
090  private static final Pattern COMPUTABLE_VALUE_REF_PATTERN = Pattern.compile( "^get([A-Z].*)ComputableValue$" );
091  @Nonnull
092  private static final Pattern OBSERVER_REF_PATTERN = Pattern.compile( "^get([A-Z].*)Observer$" );
093  @Nonnull
094  private static final Pattern ON_DEPS_CHANGE_PATTERN = Pattern.compile( "^on([A-Z].*)DepsChange" );
095  @Nonnull
096  private static final Pattern PRE_INVERSE_REMOVE_PATTERN = Pattern.compile( "^pre([A-Z].*)Remove" );
097  @Nonnull
098  private static final Pattern POST_INVERSE_ADD_PATTERN = Pattern.compile( "^post([A-Z].*)Add" );
099  @Nonnull
100  private static final Pattern CAPTURE_PATTERN = Pattern.compile( "^capture([A-Z].*)" );
101  @Nonnull
102  private static final Pattern POP_PATTERN = Pattern.compile( "^pop([A-Z].*)" );
103  @Nonnull
104  private static final Pattern PUSH_PATTERN = Pattern.compile( "^push([A-Z].*)" );
105  @Nonnull
106  private final DeferredElementSet _deferredTypes = new DeferredElementSet();
107  @Nonnull
108  private final StopWatch _analyzeComponentStopWatch = new StopWatch( "Analyze Component" );
109
110  @Override
111  @Nonnull
112  protected String getIssueTrackerURL()
113  {
114    return "https://github.com/arez/arez/issues";
115  }
116
117  @Nonnull
118  @Override
119  protected String getOptionPrefix()
120  {
121    return "arez";
122  }
123
124  @Override
125  protected void collectStopWatches( @Nonnull final Collection<StopWatch> stopWatches )
126  {
127    stopWatches.add( _analyzeComponentStopWatch );
128  }
129
130  @Override
131  public boolean process( @Nonnull final Set<? extends TypeElement> annotations, @Nonnull final RoundEnvironment env )
132  {
133    debugAnnotationProcessingRootElements( env );
134    collectRootTypeNames( env );
135    processTypeElements( annotations,
136                         env,
137                         Constants.COMPONENT_CLASSNAME,
138                         _deferredTypes,
139                         _analyzeComponentStopWatch.getName(),
140                         this::process,
141                         _analyzeComponentStopWatch );
142    errorIfProcessingOverAndInvalidTypesDetected( env );
143    clearRootTypeNamesIfProcessingOver( env );
144    return true;
145  }
146
147  private void process( @Nonnull final TypeElement element )
148    throws IOException, ProcessorException
149  {
150    final ComponentDescriptor descriptor = parse( element );
151    emitTypeSpec( descriptor.getPackageName(), ComponentGenerator.buildType( processingEnv, descriptor ) );
152  }
153
154  @Nonnull
155  private ObservableDescriptor addObservable( @Nonnull final ComponentDescriptor component,
156                                              @Nonnull final AnnotationMirror annotation,
157                                              @Nonnull final ExecutableElement method,
158                                              @Nonnull final ExecutableType methodType )
159    throws ProcessorException
160  {
161    MemberChecks.mustBeOverridable( component.getElement(),
162                                    Constants.COMPONENT_CLASSNAME,
163                                    Constants.OBSERVABLE_CLASSNAME,
164                                    method );
165
166    final String declaredName = AnnotationsUtil.getAnnotationValueValue( annotation, "name" );
167    final boolean expectSetter = AnnotationsUtil.getAnnotationValueValue( annotation, "expectSetter" );
168    final VariableElement readOutsideTransaction =
169      AnnotationsUtil.getAnnotationValueValue( annotation, "readOutsideTransaction" );
170    final VariableElement writeOutsideTransaction =
171      AnnotationsUtil.getAnnotationValueValue( annotation, "writeOutsideTransaction" );
172    final boolean setterAlwaysMutates = AnnotationsUtil.getAnnotationValueValue( annotation, "setterAlwaysMutates" );
173    final Boolean requireInitializer = isInitializerRequired( method );
174
175    final TypeMirror returnType = method.getReturnType();
176    final String methodName = method.getSimpleName().toString();
177    String name;
178    final boolean setter;
179    if ( TypeKind.VOID == returnType.getKind() )
180    {
181      setter = true;
182      //Should be a setter
183      if ( 1 != method.getParameters().size() )
184      {
185        throw new ProcessorException( "@Observable target should be a setter or getter", method );
186      }
187
188      name = deriveName( method, SETTER_PATTERN, declaredName );
189      if ( null == name )
190      {
191        name = methodName;
192      }
193    }
194    else
195    {
196      setter = false;
197      //Must be a getter
198      if ( !method.getParameters().isEmpty() )
199      {
200        throw new ProcessorException( "@Observable target should be a setter or getter", method );
201      }
202      name = getPropertyAccessorName( method, declaredName );
203    }
204    // Override name if supplied by user
205    if ( !Constants.SENTINEL.equals( declaredName ) )
206    {
207      name = declaredName;
208      if ( !SourceVersion.isIdentifier( name ) )
209      {
210        throw new ProcessorException( "@Observable target specified an invalid name '" + name + "'. The " +
211                                      "name must be a valid java identifier.", method );
212      }
213      else if ( SourceVersion.isKeyword( name ) )
214      {
215        throw new ProcessorException( "@Observable target specified an invalid name '" + name + "'. The " +
216                                      "name must not be a java keyword.", method );
217      }
218    }
219    checkNameUnique( component, name, method, Constants.OBSERVABLE_CLASSNAME );
220
221    if ( setter && !expectSetter )
222    {
223      throw new ProcessorException( "Method annotated with @Observable is a setter but defines " +
224                                    "expectSetter = false for observable named " + name, method );
225    }
226
227    final ObservableDescriptor observable = component.findOrCreateObservable( name );
228    observable.setReadOutsideTransaction( readOutsideTransaction.getSimpleName().toString() );
229    observable.setWriteOutsideTransaction( writeOutsideTransaction.getSimpleName().toString() );
230    if ( !setterAlwaysMutates )
231    {
232      observable.setSetterAlwaysMutates( false );
233    }
234    if ( !expectSetter )
235    {
236      observable.setExpectSetter( false );
237    }
238    if ( !observable.expectSetter() )
239    {
240      if ( observable.hasSetter() )
241      {
242        throw new ProcessorException( "Method annotated with @Observable defines expectSetter = false but a " +
243                                      "setter exists named " + observable.getSetter().getSimpleName() +
244                                      "for observable named " + name, method );
245      }
246    }
247    if ( setter )
248    {
249      if ( observable.hasSetter() )
250      {
251        throw new ProcessorException( "Method annotated with @Observable defines duplicate setter for " +
252                                      "observable named " + name, method );
253      }
254      if ( !observable.expectSetter() )
255      {
256        throw new ProcessorException( "Method annotated with @Observable defines expectSetter = false but a " +
257                                      "setter exists for observable named " + name, method );
258      }
259      observable.setSetter( method, methodType );
260    }
261    else
262    {
263      if ( observable.hasGetter() )
264      {
265        throw new ProcessorException( "Method annotated with @Observable defines duplicate getter for " +
266                                      "observable named " + name, method );
267      }
268      observable.setGetter( method, methodType );
269    }
270    if ( null != requireInitializer )
271    {
272      if ( !method.getModifiers().contains( Modifier.ABSTRACT ) )
273      {
274        throw new ProcessorException( "@Observable target set initializer parameter to ENABLED but " +
275                                      "method is not abstract.", method );
276      }
277      final Boolean existing = observable.getInitializer();
278      if ( null == existing )
279      {
280        observable.setInitializer( requireInitializer );
281      }
282      else if ( existing != requireInitializer )
283      {
284        throw new ProcessorException( "@Observable target set initializer parameter to value that differs from " +
285                                      "the paired observable method.", method );
286      }
287    }
288    return observable;
289  }
290
291  private void addObservableValueRef( @Nonnull final ComponentDescriptor component,
292                                      @Nonnull final AnnotationMirror annotation,
293                                      @Nonnull final ExecutableElement method,
294                                      @Nonnull final ExecutableType methodType )
295    throws ProcessorException
296  {
297    mustBeStandardRefMethod( processingEnv,
298                             component,
299                             method,
300                             Constants.OBSERVABLE_VALUE_REF_CLASSNAME );
301
302    final TypeMirror returnType = methodType.getReturnType();
303    if ( TypeKind.DECLARED != returnType.getKind() ||
304         !ElementsUtil.toRawType( returnType ).toString().equals( "arez.ObservableValue" ) )
305    {
306      throw new ProcessorException( "Method annotated with @ObservableValueRef must return an instance of " +
307                                    "arez.ObservableValue", method );
308    }
309
310    final String declaredName = AnnotationsUtil.getAnnotationValueValue( annotation, "name" );
311    final String name;
312    if ( Constants.SENTINEL.equals( declaredName ) )
313    {
314      name = deriveName( method, OBSERVABLE_REF_PATTERN, declaredName );
315      if ( null == name )
316      {
317        throw new ProcessorException( "Method annotated with @ObservableValueRef should specify name or be " +
318                                      "named according to the convention get[Name]ObservableValue", method );
319      }
320    }
321    else
322    {
323      name = declaredName;
324      if ( !SourceVersion.isIdentifier( name ) )
325      {
326        throw new ProcessorException( "@ObservableValueRef target specified an invalid name '" + name + "'. The " +
327                                      "name must be a valid java identifier.", method );
328      }
329      else if ( SourceVersion.isKeyword( name ) )
330      {
331        throw new ProcessorException( "@ObservableValueRef target specified an invalid name '" + name + "'. The " +
332                                      "name must not be a java keyword.", method );
333      }
334    }
335
336    component.findOrCreateObservable( name ).addRefMethod( method, methodType );
337  }
338
339  private void addComputableValueRef( @Nonnull final ComponentDescriptor component,
340                                      @Nonnull final AnnotationMirror annotation,
341                                      @Nonnull final ExecutableElement method,
342                                      @Nonnull final ExecutableType methodType )
343    throws ProcessorException
344  {
345    mustBeRefMethod( component, method, Constants.COMPUTABLE_VALUE_REF_CLASSNAME );
346    shouldBeInternalRefMethod( processingEnv,
347                               component,
348                               method,
349                               Constants.COMPUTABLE_VALUE_REF_CLASSNAME );
350
351    final TypeMirror returnType = methodType.getReturnType();
352    if ( TypeKind.DECLARED != returnType.getKind() ||
353         !ElementsUtil.toRawType( returnType ).toString().equals( "arez.ComputableValue" ) )
354    {
355      throw new ProcessorException( "Method annotated with @ComputableValueRef must return an instance of " +
356                                    "arez.ComputableValue", method );
357    }
358
359    final String declaredName = AnnotationsUtil.getAnnotationValueValue( annotation, "name" );
360    final String name;
361    if ( Constants.SENTINEL.equals( declaredName ) )
362    {
363      name = deriveName( method, COMPUTABLE_VALUE_REF_PATTERN, declaredName );
364      if ( null == name )
365      {
366        throw new ProcessorException( "Method annotated with @ComputableValueRef should specify name or be " +
367                                      "named according to the convention get[Name]ComputableValue", method );
368      }
369    }
370    else
371    {
372      name = declaredName;
373      if ( !SourceVersion.isIdentifier( name ) )
374      {
375        throw new ProcessorException( "@ComputableValueRef target specified an invalid name '" + name + "'. The " +
376                                      "name must be a valid java identifier.", method );
377      }
378      else if ( SourceVersion.isKeyword( name ) )
379      {
380        throw new ProcessorException( "@ComputableValueRef target specified an invalid name '" + name + "'. The " +
381                                      "name must not be a java keyword.", method );
382      }
383    }
384
385    MemberChecks.mustBeSubclassCallable( component.getElement(),
386                                         Constants.COMPONENT_CLASSNAME,
387                                         Constants.COMPUTABLE_VALUE_REF_CLASSNAME,
388                                         method );
389    MemberChecks.mustNotThrowAnyExceptions( Constants.COMPUTABLE_VALUE_REF_CLASSNAME, method );
390    component.findOrCreateMemoize( name ).addRefMethod( method, methodType );
391  }
392
393  @Nonnull
394  private String deriveMemoizeName( @Nonnull final ExecutableElement method,
395                                    @Nonnull final AnnotationMirror annotation )
396    throws ProcessorException
397  {
398    final String name = AnnotationsUtil.getAnnotationValueValue( annotation, "name" );
399    if ( Constants.SENTINEL.equals( name ) )
400    {
401      return getPropertyAccessorName( method, name );
402    }
403    else
404    {
405      if ( !SourceVersion.isIdentifier( name ) )
406      {
407        throw new ProcessorException( "@Memoize target specified an invalid name '" + name + "'. The " +
408                                      "name must be a valid java identifier.", method );
409      }
410      else if ( SourceVersion.isKeyword( name ) )
411      {
412        throw new ProcessorException( "@Memoize target specified an invalid name '" + name + "'. The " +
413                                      "name must not be a java keyword.", method );
414      }
415      return name;
416    }
417  }
418
419  private void addOnActivate( @Nonnull final ComponentDescriptor component,
420                              @Nonnull final AnnotationMirror annotation,
421                              @Nonnull final ExecutableElement method )
422    throws ProcessorException
423  {
424    final String name = deriveHookName( component, method,
425                                        ON_ACTIVATE_PATTERN,
426                                        "Activate",
427                                        AnnotationsUtil.getAnnotationValueValue( annotation, "name" ) );
428    MemberChecks.mustNotBeAbstract( Constants.ON_ACTIVATE_CLASSNAME, method );
429    MemberChecks.mustBeSubclassCallable( component.getElement(),
430                                         Constants.COMPONENT_CLASSNAME,
431                                         Constants.ON_ACTIVATE_CLASSNAME,
432                                         method );
433    final List<? extends VariableElement> parameters = method.getParameters();
434
435    if (
436      !(
437        parameters.isEmpty() ||
438        ( 1 == parameters.size() &&
439          Constants.COMPUTABLE_VALUE_CLASSNAME.equals( ElementsUtil.toRawType( parameters.get( 0 ).asType() )
440                                                         .toString() ) )
441      )
442    )
443    {
444      throw new ProcessorException( "@OnActivate target must not have any parameters or must have a single " +
445                                    "parameter of type arez.ComputableValue", method );
446    }
447
448    MemberChecks.mustNotReturnAnyValue( Constants.ON_ACTIVATE_CLASSNAME, method );
449    MemberChecks.mustNotThrowAnyExceptions( Constants.ON_ACTIVATE_CLASSNAME, method );
450    shouldBeInternalHookMethod( processingEnv,
451                                component,
452                                method,
453                                Constants.ON_ACTIVATE_CLASSNAME );
454
455    component.findOrCreateMemoize( name ).setOnActivate( method );
456  }
457
458  private void addOnDeactivate( @Nonnull final ComponentDescriptor component,
459                                @Nonnull final AnnotationMirror annotation,
460                                @Nonnull final ExecutableElement method )
461    throws ProcessorException
462  {
463    final String name =
464      deriveHookName( component, method,
465                      ON_DEACTIVATE_PATTERN,
466                      "Deactivate",
467                      AnnotationsUtil.getAnnotationValueValue( annotation, "name" ) );
468    MemberChecks.mustBeLifecycleHook( component.getElement(),
469                                      Constants.COMPONENT_CLASSNAME,
470                                      Constants.ON_DEACTIVATE_CLASSNAME,
471                                      method );
472    shouldBeInternalHookMethod( processingEnv,
473                                component,
474                                method,
475                                Constants.ON_DEACTIVATE_CLASSNAME );
476    component.findOrCreateMemoize( name ).setOnDeactivate( method );
477  }
478
479  @Nonnull
480  private String deriveHookName( @Nonnull final ComponentDescriptor component,
481                                 @Nonnull final ExecutableElement method,
482                                 @Nonnull final Pattern pattern,
483                                 @Nonnull final String type,
484                                 @Nonnull final String name )
485    throws ProcessorException
486  {
487    final String value = deriveName( method, pattern, name );
488    if ( null == value )
489    {
490      throw new ProcessorException( "Unable to derive name for @On" + type + " as does not match " +
491                                    "on[Name]" + type + " pattern. Please specify name.", method );
492    }
493    else if ( !SourceVersion.isIdentifier( value ) )
494    {
495      throw new ProcessorException( "@On" + type + " target specified an invalid name '" + value + "'. The " +
496                                    "name must be a valid java identifier.", component.getElement() );
497    }
498    else if ( SourceVersion.isKeyword( value ) )
499    {
500      throw new ProcessorException( "@On" + type + " target specified an invalid name '" + value + "'. The " +
501                                    "name must not be a java keyword.", component.getElement() );
502    }
503    else
504    {
505      return value;
506    }
507  }
508
509  private void addComponentStateRef( @Nonnull final ComponentDescriptor component,
510                                     @Nonnull final AnnotationMirror annotation,
511                                     @Nonnull final ExecutableElement method )
512    throws ProcessorException
513  {
514    mustBeStandardRefMethod( processingEnv,
515                             component,
516                             method,
517                             Constants.COMPONENT_STATE_REF_CLASSNAME );
518
519    final TypeMirror returnType = method.getReturnType();
520    if ( TypeKind.BOOLEAN != returnType.getKind() )
521    {
522      throw new ProcessorException( "@ComponentStateRef target must return a boolean", method );
523    }
524    final VariableElement variableElement = AnnotationsUtil.getAnnotationValueValue( annotation, "value" );
525    final ComponentStateRefDescriptor.State state =
526      ComponentStateRefDescriptor.State.valueOf( variableElement.getSimpleName().toString() );
527
528    component.getComponentStateRefs().add( new ComponentStateRefDescriptor( method, state ) );
529  }
530
531  private void addContextRef( @Nonnull final ComponentDescriptor component, @Nonnull final ExecutableElement method )
532    throws ProcessorException
533  {
534    mustBeStandardRefMethod( processingEnv,
535                             component,
536                             method,
537                             Constants.CONTEXT_REF_CLASSNAME );
538    MemberChecks.mustReturnAnInstanceOf( processingEnv,
539                                         method,
540                                         Constants.OBSERVER_REF_CLASSNAME,
541                                         "arez.ArezContext" );
542    component.getContextRefs().add( method );
543  }
544
545  private void addComponentIdRef( @Nonnull final ComponentDescriptor component,
546                                  @Nonnull final ExecutableElement method )
547  {
548    mustBeRefMethod( component, method, Constants.COMPONENT_ID_REF_CLASSNAME );
549    MemberChecks.mustNotHaveAnyParameters( Constants.COMPONENT_ID_REF_CLASSNAME, method );
550    component.getComponentIdRefs().add( method );
551  }
552
553  private void addComponentRef( @Nonnull final ComponentDescriptor component, @Nonnull final ExecutableElement method )
554    throws ProcessorException
555  {
556    mustBeStandardRefMethod( processingEnv,
557                             component,
558                             method,
559                             Constants.COMPONENT_REF_CLASSNAME );
560    MemberChecks.mustReturnAnInstanceOf( processingEnv,
561                                         method,
562                                         Constants.COMPONENT_REF_CLASSNAME,
563                                         "arez.Component" );
564    component.getComponentRefs().add( method );
565  }
566
567  private void setComponentId( @Nonnull final ComponentDescriptor component,
568                               @Nonnull final ExecutableElement componentId,
569                               @Nonnull final ExecutableType componentIdMethodType )
570    throws ProcessorException
571  {
572    MemberChecks.mustNotBeAbstract( Constants.COMPONENT_ID_CLASSNAME, componentId );
573    MemberChecks.mustBeSubclassCallable( component.getElement(),
574                                         Constants.COMPONENT_CLASSNAME,
575                                         Constants.COMPONENT_ID_CLASSNAME,
576                                         componentId );
577    MemberChecks.mustNotHaveAnyParameters( Constants.COMPONENT_ID_CLASSNAME, componentId );
578    MemberChecks.mustReturnAValue( Constants.COMPONENT_ID_CLASSNAME, componentId );
579    MemberChecks.mustNotThrowAnyExceptions( Constants.COMPONENT_ID_CLASSNAME, componentId );
580
581    if ( null != component.getComponentId() )
582    {
583      throw new ProcessorException( "@ComponentId target duplicates existing method named " +
584                                    component.getComponentId().getSimpleName(), componentId );
585    }
586    else
587    {
588      component.setComponentId( Objects.requireNonNull( componentId ) );
589      component.setComponentIdMethodType( componentIdMethodType );
590    }
591  }
592
593  private void setComponentTypeNameRef( @Nonnull final ComponentDescriptor component,
594                                        @Nonnull final ExecutableElement method )
595    throws ProcessorException
596  {
597    mustBeStandardRefMethod( processingEnv,
598                             component,
599                             method,
600                             Constants.COMPONENT_TYPE_NAME_REF_CLASSNAME );
601    MemberChecks.mustReturnAnInstanceOf( processingEnv,
602                                         method,
603                                         Constants.COMPONENT_TYPE_NAME_REF_CLASSNAME,
604                                         String.class.getName() );
605    component.getComponentTypeNameRefs().add( method );
606  }
607
608  private void addComponentNameRef( @Nonnull final ComponentDescriptor component,
609                                    @Nonnull final ExecutableElement method )
610    throws ProcessorException
611  {
612    mustBeStandardRefMethod( processingEnv,
613                             component,
614                             method,
615                             Constants.COMPONENT_NAME_REF_CLASSNAME );
616    MemberChecks.mustReturnAnInstanceOf( processingEnv,
617                                         method,
618                                         Constants.COMPONENT_NAME_REF_CLASSNAME,
619                                         String.class.getName() );
620    component.getComponentNameRefs().add( method );
621  }
622
623  private void addPostConstruct( @Nonnull final ComponentDescriptor component, @Nonnull final ExecutableElement method )
624    throws ProcessorException
625  {
626    MemberChecks.mustBeLifecycleHook( component.getElement(),
627                                      Constants.COMPONENT_CLASSNAME,
628                                      Constants.POST_CONSTRUCT_CLASSNAME,
629                                      method );
630    shouldBeInternalLifecycleMethod( processingEnv,
631                                     component,
632                                     method,
633                                     Constants.POST_CONSTRUCT_CLASSNAME );
634    component.getPostConstructs().add( method );
635  }
636
637  private void addPreDispose( @Nonnull final ComponentDescriptor component, @Nonnull final ExecutableElement method )
638    throws ProcessorException
639  {
640    MemberChecks.mustBeLifecycleHook( component.getElement(),
641                                      Constants.COMPONENT_CLASSNAME,
642                                      Constants.PRE_DISPOSE_CLASSNAME,
643                                      method );
644    shouldBeInternalLifecycleMethod( processingEnv,
645                                     component,
646                                     method,
647                                     Constants.PRE_DISPOSE_CLASSNAME );
648    component.getPreDisposes().add( method );
649  }
650
651  private void addPostDispose( @Nonnull final ComponentDescriptor component, @Nonnull final ExecutableElement method )
652    throws ProcessorException
653  {
654    MemberChecks.mustBeLifecycleHook( component.getElement(),
655                                      Constants.COMPONENT_CLASSNAME,
656                                      Constants.POST_DISPOSE_CLASSNAME,
657                                      method );
658    shouldBeInternalLifecycleMethod( processingEnv,
659                                     component,
660                                     method,
661                                     Constants.POST_DISPOSE_CLASSNAME );
662    component.getPostDisposes().add( method );
663  }
664
665  private void linkUnAnnotatedObservables( @Nonnull final ComponentDescriptor component,
666                                           @Nonnull final Map<String, CandidateMethod> getters,
667                                           @Nonnull final Map<String, CandidateMethod> setters )
668    throws ProcessorException
669  {
670    for ( final ObservableDescriptor observable : component.getObservables().values() )
671    {
672      if ( !observable.hasSetter() && !observable.hasGetter() )
673      {
674        throw new ProcessorException( "@ObservableValueRef target unable to be associated with an " +
675                                      "Observable property", observable.getRefMethods().get( 0 ).getMethod() );
676      }
677      else if ( !observable.hasSetter() && observable.expectSetter() )
678      {
679        final CandidateMethod candidate = setters.remove( observable.getName() );
680        if ( null != candidate )
681        {
682          MemberChecks.mustBeOverridable( component.getElement(),
683                                          Constants.COMPONENT_CLASSNAME,
684                                          Constants.OBSERVABLE_CLASSNAME,
685                                          candidate.getMethod() );
686          observable.setSetter( candidate.getMethod(), candidate.getMethodType() );
687        }
688        else if ( observable.hasGetter() )
689        {
690          throw new ProcessorException( "@Observable target defined getter but no setter was defined and no " +
691                                        "setter could be automatically determined", observable.getGetter() );
692        }
693      }
694      else if ( !observable.hasGetter() )
695      {
696        final CandidateMethod candidate = getters.remove( observable.getName() );
697        if ( null != candidate )
698        {
699          MemberChecks.mustBeOverridable( component.getElement(),
700                                          Constants.COMPONENT_CLASSNAME,
701                                          Constants.OBSERVABLE_CLASSNAME,
702                                          candidate.getMethod() );
703          observable.setGetter( candidate.getMethod(), candidate.getMethodType() );
704        }
705        else
706        {
707          throw new ProcessorException( "@Observable target defined setter but no getter was defined and no " +
708                                        "getter could be automatically determined", observable.getSetter() );
709        }
710      }
711    }
712
713    // Find pairs of un-annotated abstract setter/getter pairs and treat them as if they
714    // are annotated with @Observable
715    for ( final Map.Entry<String, CandidateMethod> entry : new ArrayList<>( getters.entrySet() ) )
716    {
717      final CandidateMethod getter = entry.getValue();
718      if ( getter.getMethod().getModifiers().contains( Modifier.ABSTRACT ) )
719      {
720        final String name = entry.getKey();
721        final CandidateMethod setter = setters.remove( name );
722        if ( null != setter && setter.getMethod().getModifiers().contains( Modifier.ABSTRACT ) )
723        {
724          final ObservableDescriptor observable = component.findOrCreateObservable( name );
725          observable.setGetter( getter.getMethod(), getter.getMethodType() );
726          observable.setSetter( setter.getMethod(), setter.getMethodType() );
727          getters.remove( name );
728        }
729      }
730    }
731  }
732
733  private void linkUnAnnotatedObserves( @Nonnull final ComponentDescriptor component,
734                                        @Nonnull final Map<String, CandidateMethod> observes,
735                                        @Nonnull final Map<String, CandidateMethod> onDepsChanges )
736    throws ProcessorException
737  {
738    for ( final ObserveDescriptor observe : component.getObserves().values() )
739    {
740      if ( !observe.hasObserve() )
741      {
742        final CandidateMethod candidate = observes.remove( observe.getName() );
743        if ( null != candidate )
744        {
745          observe.setObserveMethod( false,
746                                    Priority.NORMAL,
747                                    true,
748                                    true,
749                                    true,
750                                    "AREZ",
751                                    false,
752                                    false,
753                                    candidate.getMethod(),
754                                    candidate.getMethodType() );
755        }
756        else
757        {
758          throw new ProcessorException( "@OnDepsChange target has no corresponding @Observe that could " +
759                                        "be automatically determined", observe.getOnDepsChange() );
760        }
761      }
762      else if ( !observe.hasOnDepsChange() )
763      {
764        final CandidateMethod candidate = onDepsChanges.remove( observe.getName() );
765        if ( null != candidate )
766        {
767          setOnDepsChange( component, observe, candidate.getMethod() );
768        }
769      }
770    }
771  }
772
773  private void setOnDepsChange( @Nonnull final ComponentDescriptor component,
774                                @Nonnull final ObserveDescriptor observe,
775                                @Nonnull final ExecutableElement method )
776  {
777    MemberChecks.mustNotBeAbstract( Constants.ON_DEPS_CHANGE_CLASSNAME, method );
778    MemberChecks.mustBeSubclassCallable( component.getElement(),
779                                         Constants.COMPONENT_CLASSNAME,
780                                         Constants.ON_DEPS_CHANGE_CLASSNAME,
781                                         method );
782    final List<? extends VariableElement> parameters = method.getParameters();
783    if (
784      !(
785        parameters.isEmpty() ||
786        ( 1 == parameters.size() && Constants.OBSERVER_CLASSNAME.equals( parameters.get( 0 ).asType().toString() ) )
787      )
788    )
789    {
790      throw new ProcessorException( "@OnDepsChange target must not have any parameters or must have a single " +
791                                    "parameter of type arez.Observer", method );
792    }
793
794    MemberChecks.mustNotReturnAnyValue( Constants.ON_DEPS_CHANGE_CLASSNAME, method );
795    MemberChecks.mustNotThrowAnyExceptions( Constants.ON_DEPS_CHANGE_CLASSNAME, method );
796    shouldBeInternalHookMethod( processingEnv,
797                                component,
798                                method,
799                                Constants.ON_DEPS_CHANGE_CLASSNAME );
800    observe.setOnDepsChange( method );
801  }
802
803  private void verifyNoDuplicateAnnotations( @Nonnull final ExecutableElement method )
804    throws ProcessorException
805  {
806    final List<String> annotations =
807      Arrays.asList( Constants.ACTION_CLASSNAME,
808                     Constants.OBSERVE_CLASSNAME,
809                     Constants.ON_DEPS_CHANGE_CLASSNAME,
810                     Constants.OBSERVER_REF_CLASSNAME,
811                     Constants.OBSERVABLE_CLASSNAME,
812                     Constants.OBSERVABLE_VALUE_REF_CLASSNAME,
813                     Constants.MEMOIZE_CLASSNAME,
814                     Constants.MEMOIZE_CONTEXT_PARAMETER_CLASSNAME,
815                     Constants.COMPUTABLE_VALUE_REF_CLASSNAME,
816                     Constants.COMPONENT_REF_CLASSNAME,
817                     Constants.COMPONENT_ID_CLASSNAME,
818                     Constants.COMPONENT_NAME_REF_CLASSNAME,
819                     Constants.COMPONENT_TYPE_NAME_REF_CLASSNAME,
820                     Constants.CASCADE_DISPOSE_CLASSNAME,
821                     Constants.CONTEXT_REF_CLASSNAME,
822                     Constants.POST_CONSTRUCT_CLASSNAME,
823                     Constants.PRE_DISPOSE_CLASSNAME,
824                     Constants.POST_DISPOSE_CLASSNAME,
825                     Constants.REFERENCE_CLASSNAME,
826                     Constants.REFERENCE_ID_CLASSNAME,
827                     Constants.ON_ACTIVATE_CLASSNAME,
828                     Constants.ON_DEACTIVATE_CLASSNAME,
829                     Constants.COMPONENT_DEPENDENCY_CLASSNAME );
830    final Map<String, Collection<String>> exceptions = new HashMap<>();
831    exceptions.put( Constants.OBSERVABLE_CLASSNAME,
832                    Arrays.asList( Constants.COMPONENT_DEPENDENCY_CLASSNAME,
833                                   Constants.CASCADE_DISPOSE_CLASSNAME,
834                                   Constants.REFERENCE_ID_CLASSNAME ) );
835    exceptions.put( Constants.REFERENCE_CLASSNAME,
836                    Collections.singletonList( Constants.CASCADE_DISPOSE_CLASSNAME ) );
837    exceptions.put( Constants.POST_CONSTRUCT_CLASSNAME,
838                    Collections.singletonList( Constants.ACTION_CLASSNAME ) );
839
840    MemberChecks.verifyNoOverlappingAnnotations( method, annotations, exceptions );
841  }
842
843  private void verifyNoDuplicateAnnotations( @Nonnull final VariableElement field )
844    throws ProcessorException
845  {
846    MemberChecks.verifyNoOverlappingAnnotations( field,
847                                                 Arrays.asList( Constants.COMPONENT_DEPENDENCY_CLASSNAME,
848                                                                Constants.CASCADE_DISPOSE_CLASSNAME ),
849                                                 Collections.emptyMap() );
850  }
851
852  @Nonnull
853  private String getPropertyAccessorName( @Nonnull final ExecutableElement method, @Nonnull final String specifiedName )
854    throws ProcessorException
855  {
856    String name = deriveName( method, GETTER_PATTERN, specifiedName );
857    if ( null != name )
858    {
859      return name;
860    }
861    if ( method.getReturnType().getKind() == TypeKind.BOOLEAN )
862    {
863      name = deriveName( method, ISSER_PATTERN, specifiedName );
864      if ( null != name )
865      {
866        return name;
867      }
868    }
869    return method.getSimpleName().toString();
870  }
871
872  private void validate( final boolean allowEmpty, @Nonnull final ComponentDescriptor component )
873    throws ProcessorException
874  {
875    component.getCascadeDisposes().values().forEach( CascadeDisposeDescriptor::validate );
876    component.getObservables().values().forEach( ObservableDescriptor::validate );
877    component.getMemoizes().values().forEach( e -> e.validate( processingEnv ) );
878    component.getMemoizeContextParameters().values().forEach( p -> p.validate( processingEnv ) );
879    component.getObserves().values().forEach( ObserveDescriptor::validate );
880    component.getDependencies().values().forEach( DependencyDescriptor::validate );
881    component.getReferences().values().forEach( ReferenceDescriptor::validate );
882    component.getInverses().values().forEach( e -> e.validate( processingEnv ) );
883
884    final boolean hasZeroReactiveElements =
885      component.getObservables().isEmpty() &&
886      component.getActions().isEmpty() &&
887      component.getMemoizes().isEmpty() &&
888      component.getDependencies().isEmpty() &&
889      component.getCascadeDisposes().isEmpty() &&
890      component.getReferences().isEmpty() &&
891      component.getInverses().isEmpty() &&
892      component.getObserves().isEmpty();
893
894    final TypeElement element = component.getElement();
895    if ( null != component.getDefaultPriority() &&
896         component.getMemoizes().isEmpty() &&
897         component.getObserves().isEmpty() &&
898         isWarningNotSuppressed( element, Constants.WARNING_UNNECESSARY_DEFAULT_PRIORITY ) )
899    {
900      final String message =
901        MemberChecks.toSimpleName( Constants.COMPONENT_CLASSNAME ) + " target should not specify " +
902        "the defaultPriority parameter unless it contains methods annotated with either the " +
903        MemberChecks.toSimpleName( Constants.MEMOIZE_CLASSNAME ) + " annotation or the " +
904        MemberChecks.toSimpleName( Constants.OBSERVE_CLASSNAME ) + " annotation. " +
905        suppressedBy( Constants.WARNING_UNNECESSARY_DEFAULT_PRIORITY );
906      processingEnv.getMessager().printMessage( WARNING, message, element );
907    }
908    if ( !allowEmpty && hasZeroReactiveElements )
909    {
910      throw new ProcessorException( "@ArezComponent target has no methods annotated with @Action, " +
911                                    "@CascadeDispose, @Memoize, @Observable, @Inverse, " +
912                                    "@Reference, @ComponentDependency or @Observe", element );
913    }
914    else if ( allowEmpty &&
915              !hasZeroReactiveElements &&
916              isWarningNotSuppressed( element, Constants.WARNING_UNNECESSARY_ALLOW_EMPTY ) )
917    {
918      final String message =
919        "@ArezComponent target has specified allowEmpty = true but has methods " +
920        "annotated with @Action, @CascadeDispose, @Memoize, @Observable, @Inverse, " +
921        "@Reference, @ComponentDependency or @Observe. " +
922        suppressedBy( Constants.WARNING_UNNECESSARY_ALLOW_EMPTY );
923      processingEnv.getMessager().printMessage( WARNING, message, element );
924    }
925
926    for ( final ExecutableElement componentIdRef : component.getComponentIdRefs() )
927    {
928      if ( null != component.getComponentId() &&
929           !processingEnv.getTypeUtils()
930             .isSameType( component.getComponentId().getReturnType(), componentIdRef.getReturnType() ) )
931      {
932        throw new ProcessorException( "@ComponentIdRef target has a return type " + componentIdRef.getReturnType() +
933                                      " and a @ComponentId annotated method with a return type " +
934                                      componentIdRef.getReturnType() + ". The types must match.",
935                                      element );
936      }
937      else if ( null == component.getComponentId() &&
938                !processingEnv.getTypeUtils()
939                  .isSameType( processingEnv.getTypeUtils().getPrimitiveType( TypeKind.INT ),
940                               componentIdRef.getReturnType() ) )
941      {
942        throw new ProcessorException( "@ComponentIdRef target has a return type " + componentIdRef.getReturnType() +
943                                      " but no @ComponentId annotated method. The type is expected to be of " +
944                                      "type int.", element );
945      }
946    }
947    for ( final ExecutableElement constructor : ElementsUtil.getConstructors( element ) )
948    {
949      if ( Elements.Origin.EXPLICIT == processingEnv.getElementUtils().getOrigin( constructor ) &&
950           constructor.getModifiers().contains( Modifier.PUBLIC ) &&
951           ElementsUtil.isWarningNotSuppressed( constructor, Constants.WARNING_PUBLIC_CONSTRUCTOR ) )
952      {
953        final String instruction =
954          component.isStingEnabled() ?
955          "The type is instantiated by the sting injection framework and should have a package-access constructor. " :
956          "It is recommended that a static create method be added to the component that is responsible " +
957          "for instantiating the arez implementation class. ";
958
959        final String message =
960          MemberChecks.shouldNot( Constants.COMPONENT_CLASSNAME,
961                                  "have a public constructor. " + instruction +
962                                  MemberChecks.suppressedBy( Constants.WARNING_PUBLIC_CONSTRUCTOR,
963                                                             Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ) );
964        processingEnv.getMessager().printMessage( Diagnostic.Kind.WARNING, message, constructor );
965      }
966    }
967    if ( null != component.getDeclaredDefaultReadOutsideTransaction() &&
968         component.getObservables().isEmpty() &&
969         component.getMemoizes().isEmpty() &&
970         isWarningNotSuppressed( element, Constants.WARNING_UNNECESSARY_DEFAULT ) )
971    {
972      final String message =
973        "@ArezComponent target has specified a value for the defaultReadOutsideTransaction parameter but does not " +
974        "contain any methods annotated with either @Memoize or @Observable. " +
975        suppressedBy( Constants.WARNING_UNNECESSARY_DEFAULT );
976      processingEnv.getMessager().printMessage( WARNING, message, element );
977    }
978    if ( null != component.getDeclaredDefaultWriteOutsideTransaction() &&
979         component.getObservables().isEmpty() &&
980         isWarningNotSuppressed( element, Constants.WARNING_UNNECESSARY_DEFAULT ) )
981    {
982      final String message =
983        "@ArezComponent target has specified a value for the defaultWriteOutsideTransaction parameter but does not " +
984        "contain any methods annotated with @Observable. " +
985        suppressedBy( Constants.WARNING_UNNECESSARY_DEFAULT );
986      processingEnv.getMessager().printMessage( WARNING, message, element );
987    }
988  }
989
990  private void processCascadeDisposeFields( @Nonnull final ComponentDescriptor component )
991  {
992    ElementsUtil.getFields( component.getElement() )
993      .stream()
994      .filter( f -> AnnotationsUtil.hasAnnotationOfType( f, Constants.CASCADE_DISPOSE_CLASSNAME ) )
995      .forEach( field -> processCascadeDisposeField( component, field ) );
996  }
997
998  private void processCascadeDisposeField( @Nonnull final ComponentDescriptor component,
999                                           @Nonnull final VariableElement field )
1000  {
1001    verifyNoDuplicateAnnotations( field );
1002    MemberChecks.mustBeSubclassCallable( component.getElement(),
1003                                         Constants.COMPONENT_CLASSNAME,
1004                                         Constants.CASCADE_DISPOSE_CLASSNAME,
1005                                         field );
1006    mustBeCascadeDisposeTypeCompatible( field );
1007    component.addCascadeDispose( new CascadeDisposeDescriptor( field ) );
1008  }
1009
1010  @Nonnull
1011  private String suppressedBy( @Nonnull final String warning )
1012  {
1013    return MemberChecks.suppressedBy( warning, Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME );
1014  }
1015
1016  private boolean isWarningNotSuppressed( @Nonnull final Element element, @Nonnull final String warning )
1017  {
1018    return !ElementsUtil.isWarningSuppressed( element,
1019                                              warning,
1020                                              Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME );
1021  }
1022
1023  @SuppressWarnings( "SameParameterValue" )
1024  @Nonnull
1025  private String extractName( @Nonnull final ExecutableElement method,
1026                              @Nonnull final Function<ExecutableElement, String> function,
1027                              @Nonnull final String annotationClassname )
1028  {
1029    return AnnotationsUtil.extractName( method, function, annotationClassname, "name", Constants.SENTINEL );
1030  }
1031
1032  private void mustBeCascadeDisposeTypeCompatible( @Nonnull final VariableElement field )
1033  {
1034    final TypeMirror typeMirror = field.asType();
1035    if ( !isAssignable( typeMirror, getDisposableTypeElement() ) )
1036    {
1037      final TypeElement typeElement = (TypeElement) processingEnv.getTypeUtils().asElement( typeMirror );
1038      final AnnotationMirror value =
1039        null != typeElement ?
1040        AnnotationsUtil.findAnnotationByType( typeElement, Constants.COMPONENT_CLASSNAME ) :
1041        null;
1042      if ( null == value || !isDisposableTrackableRequired( typeElement ) )
1043      {
1044        throw new ProcessorException( "@CascadeDispose target must be assignable to " +
1045                                      Constants.DISPOSABLE_CLASSNAME + " or a type annotated with the @ArezComponent " +
1046                                      "annotation where the disposeNotifier does not resolve to DISABLE",
1047                                      field );
1048      }
1049    }
1050  }
1051
1052  private void addCascadeDisposeMethod( @Nonnull final ComponentDescriptor component,
1053                                        @Nonnull final ExecutableElement method,
1054                                        @Nullable final ObservableDescriptor observable )
1055  {
1056    MemberChecks.mustNotHaveAnyParameters( Constants.CASCADE_DISPOSE_CLASSNAME, method );
1057    MemberChecks.mustNotThrowAnyExceptions( Constants.CASCADE_DISPOSE_CLASSNAME, method );
1058    MemberChecks.mustBeSubclassCallable( component.getElement(),
1059                                         Constants.COMPONENT_CLASSNAME,
1060                                         Constants.CASCADE_DISPOSE_CLASSNAME,
1061                                         method );
1062    mustBeCascadeDisposeTypeCompatible( method );
1063    component.addCascadeDispose( new CascadeDisposeDescriptor( method, observable ) );
1064  }
1065
1066  private void mustBeCascadeDisposeTypeCompatible( @Nonnull final ExecutableElement method )
1067  {
1068    final TypeMirror typeMirror = method.getReturnType();
1069    if ( !isAssignable( typeMirror, getDisposableTypeElement() ) )
1070    {
1071      final TypeElement typeElement = (TypeElement) processingEnv.getTypeUtils().asElement( typeMirror );
1072      final AnnotationMirror value =
1073        null != typeElement ?
1074        AnnotationsUtil.findAnnotationByType( typeElement, Constants.COMPONENT_CLASSNAME ) :
1075        null;
1076      if ( null == value || !isDisposableTrackableRequired( typeElement ) )
1077      {
1078        //The type of the field must implement {@link arez.Disposable} or must be annotated by {@link ArezComponent}
1079        throw new ProcessorException( "@CascadeDispose target must return a type assignable to " +
1080                                      Constants.DISPOSABLE_CLASSNAME + " or a type annotated with @ArezComponent",
1081                                      method );
1082      }
1083    }
1084  }
1085
1086  private void addOrUpdateDependency( @Nonnull final ComponentDescriptor component,
1087                                      @Nonnull final ExecutableElement method,
1088                                      @Nonnull final ObservableDescriptor observable )
1089  {
1090    final DependencyDescriptor dependencyDescriptor =
1091      component.getDependencies().computeIfAbsent( method, m -> createMethodDependencyDescriptor( component, method ) );
1092    dependencyDescriptor.setObservable( observable );
1093  }
1094
1095  private void addAction( @Nonnull final ComponentDescriptor component,
1096                          @Nonnull final AnnotationMirror annotation,
1097                          @Nonnull final ExecutableElement method,
1098                          @Nonnull final ExecutableType methodType )
1099    throws ProcessorException
1100  {
1101    MemberChecks.mustBeWrappable( component.getElement(),
1102                                  Constants.COMPONENT_CLASSNAME,
1103                                  Constants.ACTION_CLASSNAME,
1104                                  method );
1105
1106    final String name =
1107      extractName( method, m -> m.getSimpleName().toString(), Constants.ACTION_CLASSNAME );
1108    checkNameUnique( component, name, method, Constants.ACTION_CLASSNAME );
1109    final boolean mutation = AnnotationsUtil.getAnnotationValueValue( annotation, "mutation" );
1110    final boolean requireNewTransaction =
1111      AnnotationsUtil.getAnnotationValueValue( annotation, "requireNewTransaction" );
1112    final boolean reportParameters = AnnotationsUtil.getAnnotationValueValue( annotation, "reportParameters" );
1113    final boolean reportResult = AnnotationsUtil.getAnnotationValueValue( annotation, "reportResult" );
1114    final boolean verifyRequired = AnnotationsUtil.getAnnotationValueValue( annotation, "verifyRequired" );
1115    final ActionDescriptor action =
1116      new ActionDescriptor( component,
1117                            name,
1118                            requireNewTransaction,
1119                            mutation,
1120                            verifyRequired,
1121                            reportParameters,
1122                            reportResult,
1123                            method,
1124                            methodType );
1125    component.getActions().put( action.getName(), action );
1126  }
1127
1128  private void addObserve( @Nonnull final ComponentDescriptor component,
1129                           @Nonnull final AnnotationMirror annotation,
1130                           @Nonnull final ExecutableElement method,
1131                           @Nonnull final ExecutableType methodType )
1132    throws ProcessorException
1133  {
1134    final String name = deriveObserveName( method, annotation );
1135    checkNameUnique( component, name, method, Constants.OBSERVE_CLASSNAME );
1136    final boolean mutation = AnnotationsUtil.getAnnotationValueValue( annotation, "mutation" );
1137    final boolean observeLowerPriorityDependencies =
1138      AnnotationsUtil.getAnnotationValueValue( annotation, "observeLowerPriorityDependencies" );
1139    final boolean nestedActionsAllowed = AnnotationsUtil.getAnnotationValueValue( annotation, "nestedActionsAllowed" );
1140    final VariableElement priority = AnnotationsUtil.getAnnotationValueValue( annotation, "priority" );
1141    final boolean reportParameters = AnnotationsUtil.getAnnotationValueValue( annotation, "reportParameters" );
1142    final boolean reportResult = AnnotationsUtil.getAnnotationValueValue( annotation, "reportResult" );
1143    final VariableElement executor = AnnotationsUtil.getAnnotationValueValue( annotation, "executor" );
1144    final VariableElement depType = AnnotationsUtil.getAnnotationValueValue( annotation, "depType" );
1145
1146    component
1147      .findOrCreateObserve( name )
1148      .setObserveMethod( mutation,
1149                         toPriority( component.getDefaultPriority(), priority ),
1150                         executor.getSimpleName().toString().equals( "INTERNAL" ),
1151                         reportParameters,
1152                         reportResult,
1153                         depType.getSimpleName().toString(),
1154                         observeLowerPriorityDependencies,
1155                         nestedActionsAllowed,
1156                         method,
1157                         methodType );
1158  }
1159
1160  @Nonnull
1161  private String deriveObserveName( @Nonnull final ExecutableElement method,
1162                                    @Nonnull final AnnotationMirror annotation )
1163    throws ProcessorException
1164  {
1165    final String name = AnnotationsUtil.getAnnotationValueValue( annotation, "name" );
1166    if ( Constants.SENTINEL.equals( name ) )
1167    {
1168      return method.getSimpleName().toString();
1169    }
1170    else
1171    {
1172      if ( !SourceVersion.isIdentifier( name ) )
1173      {
1174        throw new ProcessorException( "@Observe target specified an invalid name '" + name + "'. The " +
1175                                      "name must be a valid java identifier.", method );
1176      }
1177      else if ( SourceVersion.isKeyword( name ) )
1178      {
1179        throw new ProcessorException( "@Observe target specified an invalid name '" + name + "'. The " +
1180                                      "name must not be a java keyword.", method );
1181      }
1182      return name;
1183    }
1184  }
1185
1186  private void addOnDepsChange( @Nonnull final ComponentDescriptor component,
1187                                @Nonnull final AnnotationMirror annotation,
1188                                @Nonnull final ExecutableElement method )
1189    throws ProcessorException
1190  {
1191    final String name =
1192      deriveHookName( component, method,
1193                      ON_DEPS_CHANGE_PATTERN,
1194                      "DepsChange",
1195                      AnnotationsUtil.getAnnotationValueValue( annotation, "name" ) );
1196    setOnDepsChange( component, component.findOrCreateObserve( name ), method );
1197  }
1198
1199  private void addObserverRef( @Nonnull final ComponentDescriptor component,
1200                               @Nonnull final AnnotationMirror annotation,
1201                               @Nonnull final ExecutableElement method,
1202                               @Nonnull final ExecutableType methodType )
1203    throws ProcessorException
1204  {
1205    mustBeStandardRefMethod( processingEnv,
1206                             component,
1207                             method,
1208                             Constants.OBSERVER_REF_CLASSNAME );
1209    MemberChecks.mustReturnAnInstanceOf( processingEnv,
1210                                         method,
1211                                         Constants.OBSERVER_REF_CLASSNAME,
1212                                         Constants.OBSERVER_CLASSNAME );
1213
1214    final String declaredName = AnnotationsUtil.getAnnotationValueValue( annotation, "name" );
1215    final String name;
1216    if ( Constants.SENTINEL.equals( declaredName ) )
1217    {
1218      name = deriveName( method, OBSERVER_REF_PATTERN, declaredName );
1219      if ( null == name )
1220      {
1221        throw new ProcessorException( "Method annotated with @ObserverRef should specify name or be " +
1222                                      "named according to the convention get[Name]Observer", method );
1223      }
1224    }
1225    else
1226    {
1227      name = declaredName;
1228      if ( !SourceVersion.isIdentifier( name ) )
1229      {
1230        throw new ProcessorException( "@ObserverRef target specified an invalid name '" + name + "'. The " +
1231                                      "name must be a valid java identifier.", method );
1232      }
1233      else if ( SourceVersion.isKeyword( name ) )
1234      {
1235        throw new ProcessorException( "@ObserverRef target specified an invalid name '" + name + "'. The " +
1236                                      "name must not be a java keyword.", method );
1237      }
1238    }
1239    component.getObserverRefs().computeIfAbsent( name, s -> new ArrayList<>() )
1240      .add( new CandidateMethod( method, methodType ) );
1241  }
1242
1243  private void addMemoizeContextParameter( @Nonnull final ComponentDescriptor component,
1244                                           @Nonnull final AnnotationMirror annotation,
1245                                           @Nonnull final ExecutableElement method,
1246                                           @Nonnull final ExecutableType methodType )
1247    throws ProcessorException
1248  {
1249    final String methodName = method.getSimpleName().toString();
1250    final MemoizeContextParameterMethodType mcpMethodType =
1251      PUSH_PATTERN.matcher( methodName ).matches() ? MemoizeContextParameterMethodType.Push :
1252      POP_PATTERN.matcher( methodName ).matches() ? MemoizeContextParameterMethodType.Pop :
1253      MemoizeContextParameterMethodType.Capture;
1254    final String name = deriveMemoizeContextParameterName( method, annotation, mcpMethodType );
1255
1256    checkNameUnique( component, name, method, Constants.MEMOIZE_CONTEXT_PARAMETER_CLASSNAME );
1257    final boolean allowEmpty = AnnotationsUtil.getAnnotationValueValue( annotation, "allowEmpty" );
1258    final String pattern = AnnotationsUtil.getAnnotationValueValue( annotation, "pattern" );
1259    final MemoizeContextParameterDescriptor descriptor = component.findOrCreateMemoizeContextParameter( name );
1260
1261    final Pattern compiledPattern;
1262    try
1263    {
1264      compiledPattern = Pattern.compile( pattern );
1265    }
1266    catch ( final PatternSyntaxException e )
1267    {
1268      throw new ProcessorException( "@MemoizeContextParameter target specified a pattern parameter " +
1269                                    "that is not a valid regular expression.", method );
1270    }
1271
1272    if ( MemoizeContextParameterMethodType.Capture == mcpMethodType )
1273    {
1274      descriptor.setCapture( method, methodType, allowEmpty, pattern, compiledPattern );
1275    }
1276    else if ( MemoizeContextParameterMethodType.Push == mcpMethodType )
1277    {
1278      descriptor.setPush( method, methodType, allowEmpty, pattern, compiledPattern );
1279    }
1280    else // MemoizeContextParameterMethodType.Pop == mcpMethodType
1281    {
1282      descriptor.setPop( method, methodType, allowEmpty, pattern, compiledPattern );
1283    }
1284  }
1285
1286  private String deriveMemoizeContextParameterName( @Nonnull final ExecutableElement method,
1287                                                    @Nonnull final AnnotationMirror annotation,
1288                                                    @Nonnull final MemoizeContextParameterMethodType mcpMethodType )
1289    throws ProcessorException
1290  {
1291    final String name = AnnotationsUtil.getAnnotationValueValue( annotation, "name" );
1292    if ( Constants.SENTINEL.equals( name ) )
1293    {
1294      final Pattern pattern =
1295        MemoizeContextParameterMethodType.Push == mcpMethodType ? PUSH_PATTERN :
1296        MemoizeContextParameterMethodType.Pop == mcpMethodType ? POP_PATTERN :
1297        CAPTURE_PATTERN;
1298      final String methodName = method.getSimpleName().toString();
1299      final Matcher matcher = pattern.matcher( methodName );
1300      if ( matcher.find() )
1301      {
1302        final String candidate = matcher.group( 1 );
1303        return firstCharacterToLowerCase( candidate );
1304      }
1305      else
1306      {
1307        // we get here for a capture method that does not start with capture
1308        return methodName;
1309      }
1310    }
1311    else
1312    {
1313      if ( !SourceVersion.isIdentifier( name ) )
1314      {
1315        throw new ProcessorException( "@MemoizeContextParameter target specified an invalid name '" + name +
1316                                      "'. The name must be a valid java identifier.", method );
1317      }
1318      else if ( SourceVersion.isKeyword( name ) )
1319      {
1320        throw new ProcessorException( "@MemoizeContextParameter target specified an invalid name '" + name +
1321                                      "'. The name must not be a java keyword.", method );
1322      }
1323      return name;
1324    }
1325  }
1326
1327  private void addMemoize( @Nonnull final ComponentDescriptor component,
1328                           @Nonnull final AnnotationMirror annotation,
1329                           @Nonnull final ExecutableElement method,
1330                           @Nonnull final ExecutableType methodType )
1331    throws ProcessorException
1332  {
1333    final String name = deriveMemoizeName( method, annotation );
1334    checkNameUnique( component, name, method, Constants.MEMOIZE_CLASSNAME );
1335    final boolean keepAlive = AnnotationsUtil.getAnnotationValueValue( annotation, "keepAlive" );
1336    final boolean reportResult = AnnotationsUtil.getAnnotationValueValue( annotation, "reportResult" );
1337    final boolean observeLowerPriorityDependencies =
1338      AnnotationsUtil.getAnnotationValueValue( annotation, "observeLowerPriorityDependencies" );
1339    final VariableElement readOutsideTransaction =
1340      AnnotationsUtil.getAnnotationValueValue( annotation, "readOutsideTransaction" );
1341    final VariableElement priority = AnnotationsUtil.getAnnotationValueValue( annotation, "priority" );
1342    final VariableElement depType = AnnotationsUtil.getAnnotationValueValue( annotation, "depType" );
1343    final String depTypeAsString = depType.getSimpleName().toString();
1344    component.findOrCreateMemoize( name ).setMemoize( method,
1345                                                      methodType,
1346                                                      keepAlive,
1347                                                      toPriority( component.getDefaultPriority(),
1348                                                                  priority ),
1349                                                      reportResult,
1350                                                      observeLowerPriorityDependencies,
1351                                                      readOutsideTransaction.getSimpleName().toString(),
1352                                                      depTypeAsString );
1353  }
1354
1355  @Nonnull
1356  private Priority toPriority( @Nullable final Priority defaultPriority,
1357                               @Nonnull final VariableElement priorityElement )
1358  {
1359    final String priorityName = priorityElement.getSimpleName().toString();
1360    return "DEFAULT".equals( priorityName ) ?
1361           null != defaultPriority ? defaultPriority : Priority.NORMAL :
1362           Priority.valueOf( priorityName );
1363  }
1364
1365  private void autodetectObservableInitializers( @Nonnull final ComponentDescriptor component )
1366  {
1367    for ( final ObservableDescriptor observable : component.getObservables().values() )
1368    {
1369      if ( null == observable.getInitializer() && observable.hasGetter() )
1370      {
1371        if ( observable.hasSetter() )
1372        {
1373          final boolean initializer =
1374            autodetectInitializer( observable.getGetter() ) && autodetectInitializer( observable.getSetter() );
1375          observable.setInitializer( initializer );
1376        }
1377        else
1378        {
1379          observable.setInitializer( autodetectInitializer( observable.getGetter() ) );
1380        }
1381      }
1382    }
1383  }
1384
1385  private boolean hasDependencyAnnotation( @Nonnull final ExecutableElement method )
1386  {
1387    return AnnotationsUtil.hasAnnotationOfType( method, Constants.COMPONENT_DEPENDENCY_CLASSNAME );
1388  }
1389
1390  private void ensureTargetTypeAligns( @Nonnull final ComponentDescriptor component,
1391                                       @Nonnull final InverseDescriptor descriptor,
1392                                       @Nonnull final TypeMirror target )
1393  {
1394    if ( !processingEnv.getTypeUtils().isSameType( target, component.getElement().asType() ) )
1395    {
1396      throw new ProcessorException( "@Inverse target expected to find an associated @Reference annotation with " +
1397                                    "a target type equal to " + component.getElement().asType() + " but the actual " +
1398                                    "target type is " + target, descriptor.getObservable().getGetter() );
1399    }
1400  }
1401
1402  @Nullable
1403  private TypeElement getInverseManyTypeTarget( @Nonnull final ExecutableElement method )
1404  {
1405    final TypeName typeName = TypeName.get( method.getReturnType() );
1406    if ( typeName instanceof final ParameterizedTypeName type )
1407    {
1408      if ( isSupportedInverseCollectionType( type.rawType.toString() ) && !type.typeArguments.isEmpty() )
1409      {
1410        final TypeElement typeElement = getTypeElement( type.typeArguments.get( 0 ).toString() );
1411        if ( AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.COMPONENT_CLASSNAME ) )
1412        {
1413          return typeElement;
1414        }
1415        else
1416        {
1417          throw new ProcessorException( "@Inverse target expected to return a type annotated with " +
1418                                        Constants.COMPONENT_CLASSNAME, method );
1419        }
1420      }
1421    }
1422    return null;
1423  }
1424
1425  private boolean isSupportedInverseCollectionType( @Nonnull final String typeClassname )
1426  {
1427    return Collection.class.getName().equals( typeClassname ) ||
1428           Set.class.getName().equals( typeClassname ) ||
1429           List.class.getName().equals( typeClassname );
1430  }
1431
1432  @Nonnull
1433  private String getInverseReferenceNameParameter( @Nonnull final ComponentDescriptor component,
1434                                                   @Nonnull final ExecutableElement method )
1435  {
1436    final String declaredName =
1437      (String) AnnotationsUtil.getAnnotationValue( method,
1438                                                   Constants.INVERSE_CLASSNAME,
1439                                                   "referenceName" ).getValue();
1440    final String name;
1441    if ( Constants.SENTINEL.equals( declaredName ) )
1442    {
1443      name = firstCharacterToLowerCase( component.getElement().getSimpleName().toString() );
1444    }
1445    else
1446    {
1447      name = declaredName;
1448      if ( !SourceVersion.isIdentifier( name ) )
1449      {
1450        throw new ProcessorException( "@Inverse target specified an invalid referenceName '" + name + "'. The " +
1451                                      "name must be a valid java identifier.", method );
1452      }
1453      else if ( SourceVersion.isKeyword( name ) )
1454      {
1455        throw new ProcessorException( "@Inverse target specified an invalid referenceName '" + name + "'. The " +
1456                                      "name must not be a java keyword.", method );
1457      }
1458    }
1459    return name;
1460  }
1461
1462  private void linkDependencies( @Nonnull final ComponentDescriptor component,
1463                                 @Nonnull final Collection<CandidateMethod> candidates )
1464  {
1465    component.getObservables()
1466      .values()
1467      .stream()
1468      .filter( ObservableDescriptor::hasGetter )
1469      .filter( o -> hasDependencyAnnotation( o.getGetter() ) )
1470      .forEach( o -> addOrUpdateDependency( component, o.getGetter(), o ) );
1471
1472    component.getMemoizes()
1473      .values()
1474      .stream()
1475      .filter( MemoizeDescriptor::hasMemoize )
1476      .map( MemoizeDescriptor::getMethod )
1477      .filter( this::hasDependencyAnnotation )
1478      .forEach( method1 -> component.addDependency( createMethodDependencyDescriptor( component, method1 ) ) );
1479
1480    candidates
1481      .stream()
1482      .map( CandidateMethod::getMethod )
1483      .filter( this::hasDependencyAnnotation )
1484      .forEach( method -> component.addDependency( createMethodDependencyDescriptor( component, method ) ) );
1485  }
1486
1487  private void linkCascadeDisposeObservables( @Nonnull final ComponentDescriptor component )
1488  {
1489    for ( final ObservableDescriptor observable : component.getObservables().values() )
1490    {
1491      final CascadeDisposeDescriptor cascadeDisposeDescriptor = observable.getCascadeDisposeDescriptor();
1492      if ( null == cascadeDisposeDescriptor )
1493      {
1494        //@CascadeDisposable can only occur on getter so if we don't have it then we look in
1495        // cascadeDisposableDescriptor list to see if we can match getter
1496        final CascadeDisposeDescriptor descriptor = component.getCascadeDisposes().get( observable.getGetter() );
1497        if ( null != descriptor )
1498        {
1499          descriptor.setObservable( observable );
1500        }
1501      }
1502    }
1503  }
1504
1505  private void linkCascadeDisposeReferences( @Nonnull final ComponentDescriptor component )
1506  {
1507    for ( final ReferenceDescriptor reference : component.getReferences().values() )
1508    {
1509      final CascadeDisposeDescriptor cascadeDisposeDescriptor = reference.getCascadeDisposeDescriptor();
1510      if ( null == cascadeDisposeDescriptor && reference.hasMethod() )
1511      {
1512        final CascadeDisposeDescriptor descriptor = component.getCascadeDisposes().get( reference.getMethod() );
1513        if ( null != descriptor )
1514        {
1515          descriptor.setReference( reference );
1516        }
1517      }
1518    }
1519  }
1520
1521  private void linkObserverRefs( @Nonnull final ComponentDescriptor component )
1522  {
1523    for ( final Map.Entry<String, List<CandidateMethod>> entry : component.getObserverRefs().entrySet() )
1524    {
1525      final String key = entry.getKey();
1526      final List<CandidateMethod> methods = entry.getValue();
1527      final ObserveDescriptor observed = component.getObserves().get( key );
1528      if ( null != observed )
1529      {
1530        methods.stream().map( CandidateMethod::getMethod ).forEach( observed::addRefMethod );
1531      }
1532      else
1533      {
1534        throw new ProcessorException( "@ObserverRef target defined observer named '" + key + "' but no " +
1535                                      "@Observe method with that name exists", methods.get( 0 ).getMethod() );
1536      }
1537    }
1538  }
1539
1540  @Nullable
1541  private Boolean isInitializerRequired( @Nonnull final ExecutableElement element )
1542  {
1543    final AnnotationMirror annotation =
1544      AnnotationsUtil.findAnnotationByType( element, Constants.OBSERVABLE_CLASSNAME );
1545    final AnnotationValue v =
1546      null == annotation ? null : AnnotationsUtil.findAnnotationValueNoDefaults( annotation, "initializer" );
1547    final String value = null == v ? "AUTODETECT" : ( (VariableElement) v.getValue() ).getSimpleName().toString();
1548    return switch ( value )
1549    {
1550      case "ENABLE" -> Boolean.TRUE;
1551      case "DISABLE" -> Boolean.FALSE;
1552      default -> null;
1553    };
1554  }
1555
1556  private boolean autodetectInitializer( @Nonnull final ExecutableElement element )
1557  {
1558    return element.getModifiers().contains( Modifier.ABSTRACT ) &&
1559           (
1560             (
1561               // Getter
1562               element.getReturnType().getKind() != TypeKind.VOID &&
1563               AnnotationsUtil.hasNonnullAnnotation( element ) &&
1564               !AnnotationsUtil.hasAnnotationOfType( element, Constants.INVERSE_CLASSNAME )
1565             ) ||
1566             (
1567               // Setter
1568               1 == element.getParameters().size() &&
1569               AnnotationsUtil.hasNonnullAnnotation( element.getParameters().get( 0 ) )
1570             )
1571           );
1572  }
1573
1574  private void checkNameUnique( @Nonnull final ComponentDescriptor component, @Nonnull final String name,
1575                                @Nonnull final ExecutableElement sourceMethod,
1576                                @Nonnull final String sourceAnnotationName )
1577    throws ProcessorException
1578  {
1579    final ActionDescriptor action = component.getActions().get( name );
1580    if ( null != action )
1581    {
1582      throw toException( name,
1583                         sourceAnnotationName,
1584                         sourceMethod,
1585                         Constants.ACTION_CLASSNAME,
1586                         action.getAction() );
1587    }
1588    final MemoizeDescriptor memoize = component.getMemoizes().get( name );
1589    if ( null != memoize && memoize.hasMemoize() )
1590    {
1591      throw toException( name,
1592                         sourceAnnotationName,
1593                         sourceMethod,
1594                         Constants.MEMOIZE_CLASSNAME,
1595                         memoize.getMethod() );
1596    }
1597    // Observe have pairs so let the caller determine whether a duplicate occurs in that scenario
1598    if ( !sourceAnnotationName.equals( Constants.OBSERVE_CLASSNAME ) )
1599    {
1600      final ObserveDescriptor observed = component.getObserves().get( name );
1601      if ( null != observed )
1602      {
1603        throw toException( name,
1604                           sourceAnnotationName,
1605                           sourceMethod,
1606                           Constants.OBSERVE_CLASSNAME,
1607                           observed.getMethod() );
1608      }
1609    }
1610    // Observables have pairs so let the caller determine whether a duplicate occurs in that scenario
1611    if ( !sourceAnnotationName.equals( Constants.OBSERVABLE_CLASSNAME ) )
1612    {
1613      final ObservableDescriptor observable = component.getObservables().get( name );
1614      if ( null != observable )
1615      {
1616        throw toException( name,
1617                           sourceAnnotationName,
1618                           sourceMethod,
1619                           Constants.OBSERVABLE_CLASSNAME,
1620                           observable.getDefiner() );
1621      }
1622    }
1623  }
1624
1625  @Nonnull
1626  private ProcessorException toException( @Nonnull final String name,
1627                                          @Nonnull final String sourceAnnotationName,
1628                                          @Nonnull final ExecutableElement sourceMethod,
1629                                          @Nonnull final String targetAnnotationName,
1630                                          @Nonnull final ExecutableElement targetElement )
1631  {
1632    return new ProcessorException( "Method annotated with " + MemberChecks.toSimpleName( sourceAnnotationName ) +
1633                                   " specified name " + name + " that duplicates " +
1634                                   MemberChecks.toSimpleName( targetAnnotationName ) + " defined by method " +
1635                                   targetElement.getSimpleName(), sourceMethod );
1636  }
1637
1638  private void processComponentDependencyFields( @Nonnull final ComponentDescriptor component )
1639  {
1640    ElementsUtil.getFields( component.getElement() )
1641      .stream()
1642      .filter( f -> AnnotationsUtil.hasAnnotationOfType( f, Constants.COMPONENT_DEPENDENCY_CLASSNAME ) )
1643      .forEach( field -> processComponentDependencyField( component, field ) );
1644  }
1645
1646  private void processComponentDependencyField( @Nonnull final ComponentDescriptor component,
1647                                                @Nonnull final VariableElement field )
1648  {
1649    verifyNoDuplicateAnnotations( field );
1650    MemberChecks.mustBeSubclassCallable( component.getElement(),
1651                                         Constants.COMPONENT_CLASSNAME,
1652                                         Constants.COMPONENT_DEPENDENCY_CLASSNAME,
1653                                         field );
1654    component.addDependency( createFieldDependencyDescriptor( component, field ) );
1655  }
1656
1657  private void addReference( @Nonnull final ComponentDescriptor component,
1658                             @Nonnull final AnnotationMirror annotation,
1659                             @Nonnull final ExecutableElement method,
1660                             @Nonnull final ExecutableType methodType )
1661  {
1662    MemberChecks.mustNotHaveAnyParameters( Constants.REFERENCE_CLASSNAME, method );
1663    MemberChecks.mustBeSubclassCallable( component.getElement(),
1664                                         Constants.COMPONENT_CLASSNAME,
1665                                         Constants.REFERENCE_CLASSNAME,
1666                                         method );
1667    MemberChecks.mustNotThrowAnyExceptions( Constants.REFERENCE_CLASSNAME, method );
1668    MemberChecks.mustReturnAValue( Constants.REFERENCE_CLASSNAME, method );
1669    MemberChecks.mustBeAbstract( Constants.REFERENCE_CLASSNAME, method );
1670
1671    final String name = getReferenceName( annotation, method );
1672    final String linkType = getLinkType( method );
1673    final String inverseName;
1674    final Multiplicity inverseMultiplicity;
1675    if ( hasInverse( annotation ) )
1676    {
1677      inverseMultiplicity = getReferenceInverseMultiplicity( annotation );
1678      inverseName = getReferenceInverseName( component, annotation, method, inverseMultiplicity );
1679      final TypeMirror returnType = method.getReturnType();
1680      if ( !( returnType instanceof DeclaredType ) ||
1681           !AnnotationsUtil.hasAnnotationOfType( ( (DeclaredType) returnType ).asElement(),
1682                                                 Constants.COMPONENT_CLASSNAME ) )
1683      {
1684        throw new ProcessorException( "@Reference target expected to return a type annotated with " +
1685                                      MemberChecks.toSimpleName( Constants.COMPONENT_CLASSNAME ) +
1686                                      " if there is an inverse reference", method );
1687      }
1688    }
1689    else
1690    {
1691      inverseName = null;
1692      inverseMultiplicity = null;
1693    }
1694    final ReferenceDescriptor descriptor = component.findOrCreateReference( name );
1695    descriptor.setMethod( method, methodType, linkType, inverseName, inverseMultiplicity );
1696    verifyMultiplicityOfAssociatedInverseMethod( component, descriptor );
1697  }
1698
1699  private boolean hasInverse( @Nonnull final AnnotationMirror annotation )
1700  {
1701    final VariableElement variableElement = AnnotationsUtil.getAnnotationValueValue( annotation, "inverse" );
1702    return switch ( variableElement.getSimpleName().toString() )
1703    {
1704      case "ENABLE" -> true;
1705      case "DISABLE" -> false;
1706      default -> null != AnnotationsUtil.findAnnotationValueNoDefaults( annotation, "inverseName" ) ||
1707                 null != AnnotationsUtil.findAnnotationValueNoDefaults( annotation, "inverseMultiplicity" );
1708    };
1709  }
1710
1711  private void verifyMultiplicityOfAssociatedReferenceMethod( @Nonnull final ComponentDescriptor component,
1712                                                              @Nonnull final InverseDescriptor descriptor )
1713  {
1714    final Multiplicity multiplicity =
1715      ElementsUtil
1716        .getMethods( descriptor.getTargetType(),
1717                     processingEnv.getElementUtils(),
1718                     processingEnv.getTypeUtils() )
1719        .stream()
1720        .map( m -> {
1721          final AnnotationMirror a =
1722            AnnotationsUtil.findAnnotationByType( m, Constants.REFERENCE_CLASSNAME );
1723          if ( null != a && getReferenceName( a, m ).equals( descriptor.getReferenceName() ) )
1724          {
1725            if ( null == AnnotationsUtil.findAnnotationValueNoDefaults( a, "inverse" ) &&
1726                 null == AnnotationsUtil.findAnnotationValueNoDefaults( a, "inverseName" ) &&
1727                 null == AnnotationsUtil.findAnnotationValueNoDefaults( a, "inverseMultiplicity" ) )
1728            {
1729              throw new ProcessorException( "@Inverse target found an associated @Reference on the method '" +
1730                                            m.getSimpleName() + "' on type '" +
1731                                            descriptor.getTargetType().getQualifiedName() + "' but the " +
1732                                            "annotation has not configured an inverse.",
1733                                            descriptor.getObservable().getGetter() );
1734            }
1735            ensureTargetTypeAligns( component, descriptor, m.getReturnType() );
1736            return getReferenceInverseMultiplicity( a );
1737          }
1738          else
1739          {
1740            return null;
1741          }
1742        } )
1743        .filter( Objects::nonNull )
1744        .findAny()
1745        .orElse( null );
1746    if ( null == multiplicity )
1747    {
1748      throw new ProcessorException( "@Inverse target expected to find an associated @Reference annotation with " +
1749                                    "a name parameter equal to '" + descriptor.getReferenceName() + "' on class " +
1750                                    descriptor.getTargetType().getQualifiedName() + " but is unable to " +
1751                                    "locate a matching method.", descriptor.getObservable().getGetter() );
1752    }
1753
1754    if ( descriptor.getMultiplicity() != multiplicity )
1755    {
1756      throw new ProcessorException( "@Inverse target has a multiplicity of " + descriptor.getMultiplicity() +
1757                                    " but that associated @Reference has a multiplicity of " + multiplicity +
1758                                    ". The multiplicity must align.", descriptor.getObservable().getGetter() );
1759    }
1760  }
1761
1762  @Nonnull
1763  private String getLinkType( @Nonnull final ExecutableElement method )
1764  {
1765    return AnnotationsUtil.getEnumAnnotationParameter( method, Constants.REFERENCE_CLASSNAME, "load" );
1766  }
1767
1768  @Nonnull
1769  private String getReferenceName( @Nonnull final AnnotationMirror annotation,
1770                                   @Nonnull final ExecutableElement method )
1771  {
1772    final String declaredName = AnnotationsUtil.getAnnotationValueValue( annotation, "name" );
1773    final String name;
1774    if ( Constants.SENTINEL.equals( declaredName ) )
1775    {
1776      final String candidate = deriveName( method, GETTER_PATTERN, declaredName );
1777      if ( null == candidate )
1778      {
1779        name = method.getSimpleName().toString();
1780      }
1781      else
1782      {
1783        name = candidate;
1784      }
1785    }
1786    else
1787    {
1788      name = declaredName;
1789      if ( !SourceVersion.isIdentifier( name ) )
1790      {
1791        throw new ProcessorException( "@Reference target specified an invalid name '" + name + "'. The " +
1792                                      "name must be a valid java identifier.", method );
1793      }
1794      else if ( SourceVersion.isKeyword( name ) )
1795      {
1796        throw new ProcessorException( "@Reference target specified an invalid name '" + name + "'. The " +
1797                                      "name must not be a java keyword.", method );
1798      }
1799    }
1800    return name;
1801  }
1802
1803  @Nonnull
1804  private Multiplicity getReferenceInverseMultiplicity( @Nonnull final AnnotationMirror annotation )
1805  {
1806    final VariableElement variableElement =
1807      AnnotationsUtil.getAnnotationValueValue( annotation, "inverseMultiplicity" );
1808    return switch ( variableElement.getSimpleName().toString() )
1809    {
1810      case "MANY" -> Multiplicity.MANY;
1811      case "ONE" -> Multiplicity.ONE;
1812      default -> Multiplicity.ZERO_OR_ONE;
1813    };
1814  }
1815
1816  @Nonnull
1817  private String getReferenceInverseName( @Nonnull final ComponentDescriptor component,
1818                                          @Nonnull final AnnotationMirror annotation,
1819                                          @Nonnull final ExecutableElement method,
1820                                          @Nonnull final Multiplicity multiplicity )
1821  {
1822    final String declaredName = AnnotationsUtil.getAnnotationValueValue( annotation, "inverseName" );
1823    final String name;
1824    if ( Constants.SENTINEL.equals( declaredName ) )
1825    {
1826      final String baseName = component.getElement().getSimpleName().toString();
1827      return firstCharacterToLowerCase( baseName ) + ( Multiplicity.MANY == multiplicity ? "s" : "" );
1828    }
1829    else
1830    {
1831      name = declaredName;
1832      if ( !SourceVersion.isIdentifier( name ) )
1833      {
1834        throw new ProcessorException( "@Reference target specified an invalid inverseName '" + name + "'. The " +
1835                                      "inverseName must be a valid java identifier.", method );
1836      }
1837      else if ( SourceVersion.isKeyword( name ) )
1838      {
1839        throw new ProcessorException( "@Reference target specified an invalid inverseName '" + name + "'. The " +
1840                                      "inverseName must not be a java keyword.", method );
1841      }
1842    }
1843    return name;
1844  }
1845
1846  private void ensureTargetTypeAligns( @Nonnull final ComponentDescriptor component,
1847                                       @Nonnull final ReferenceDescriptor descriptor,
1848                                       @Nonnull final TypeMirror target )
1849  {
1850    if ( !processingEnv.getTypeUtils().isSameType( target, component.getElement().asType() ) )
1851    {
1852      throw new ProcessorException( "@Reference target expected to find an associated @Inverse annotation with " +
1853                                    "a target type equal to " + component.getElement().getQualifiedName() + " but " +
1854                                    "the actual target type is " + target, descriptor.getMethod() );
1855    }
1856  }
1857
1858  private void verifyMultiplicityOfAssociatedInverseMethod( @Nonnull final ComponentDescriptor component,
1859                                                            @Nonnull final ReferenceDescriptor descriptor )
1860  {
1861    final TypeElement element =
1862      (TypeElement) processingEnv.getTypeUtils().asElement( descriptor.getMethod().getReturnType() );
1863    final String defaultInverseName =
1864      descriptor.hasInverse() ?
1865      null :
1866      firstCharacterToLowerCase( component.getElement().getSimpleName().toString() ) + "s";
1867    final Multiplicity multiplicity =
1868      ElementsUtil
1869        .getMethods( element, processingEnv.getElementUtils(), processingEnv.getTypeUtils() )
1870        .stream()
1871        .map( m -> {
1872          final AnnotationMirror a = AnnotationsUtil.findAnnotationByType( m, Constants.INVERSE_CLASSNAME );
1873          if ( null == a )
1874          {
1875            return null;
1876          }
1877          final String inverseName = getInverseName( a, m );
1878          if ( !descriptor.hasInverse() && inverseName.equals( defaultInverseName ) )
1879          {
1880            throw new ProcessorException( "@Reference target has not configured an inverse but there is an " +
1881                                          "associated @Inverse annotated method named '" + m.getSimpleName() +
1882                                          "' on type '" + element.getQualifiedName() + "'.",
1883                                          descriptor.getMethod() );
1884          }
1885          if ( descriptor.hasInverse() && inverseName.equals( descriptor.getInverseName() ) )
1886          {
1887            final TypeElement target = getInverseManyTypeTarget( m );
1888            if ( null != target )
1889            {
1890              ensureTargetTypeAligns( component, descriptor, target.asType() );
1891              return Multiplicity.MANY;
1892            }
1893            else
1894            {
1895              ensureTargetTypeAligns( component, descriptor, m.getReturnType() );
1896              return AnnotationsUtil.hasNonnullAnnotation( m ) ? Multiplicity.ONE : Multiplicity.ZERO_OR_ONE;
1897            }
1898          }
1899          else
1900          {
1901            return null;
1902          }
1903        } )
1904        .filter( Objects::nonNull )
1905        .findAny()
1906        .orElse( null );
1907
1908    if ( descriptor.hasInverse() )
1909    {
1910      if ( null == multiplicity )
1911      {
1912        throw new ProcessorException( "@Reference target expected to find an associated @Inverse annotation " +
1913                                      "with a name parameter equal to '" + descriptor.getInverseName() + "' on " +
1914                                      "class " + descriptor.getMethod().getReturnType() + " but is unable to " +
1915                                      "locate a matching method.", descriptor.getMethod() );
1916      }
1917
1918      final Multiplicity inverseMultiplicity = descriptor.getInverseMultiplicity();
1919      if ( inverseMultiplicity != multiplicity )
1920      {
1921        throw new ProcessorException( "@Reference target has an inverseMultiplicity of " + inverseMultiplicity +
1922                                      " but that associated @Inverse has a multiplicity of " + multiplicity +
1923                                      ". The multiplicity must align.", descriptor.getMethod() );
1924      }
1925    }
1926  }
1927
1928  @Nonnull
1929  private DependencyDescriptor createMethodDependencyDescriptor( @Nonnull final ComponentDescriptor descriptor,
1930                                                                 @Nonnull final ExecutableElement method )
1931  {
1932    MemberChecks.mustNotHaveAnyParameters( Constants.COMPONENT_DEPENDENCY_CLASSNAME, method );
1933    MemberChecks.mustBeSubclassCallable( descriptor.getElement(),
1934                                         Constants.COMPONENT_CLASSNAME,
1935                                         Constants.COMPONENT_DEPENDENCY_CLASSNAME,
1936                                         method );
1937    MemberChecks.mustNotThrowAnyExceptions( Constants.COMPONENT_DEPENDENCY_CLASSNAME, method );
1938    MemberChecks.mustReturnAValue( Constants.COMPONENT_DEPENDENCY_CLASSNAME, method );
1939
1940    final boolean validateTypeAtRuntime =
1941      (Boolean) AnnotationsUtil.getAnnotationValue( method,
1942                                                    Constants.COMPONENT_DEPENDENCY_CLASSNAME,
1943                                                    "validateTypeAtRuntime" ).getValue();
1944
1945    final TypeMirror type = method.getReturnType();
1946    if ( TypeKind.DECLARED != type.getKind() )
1947    {
1948      throw new ProcessorException( "@ComponentDependency target must return a non-primitive value", method );
1949    }
1950    if ( !validateTypeAtRuntime )
1951    {
1952      final TypeElement disposeNotifier = getTypeElement( Constants.DISPOSE_NOTIFIER_CLASSNAME );
1953      if ( !processingEnv.getTypeUtils().isAssignable( type, disposeNotifier.asType() ) )
1954      {
1955        final TypeElement typeElement = (TypeElement) processingEnv.getTypeUtils().asElement( type );
1956        if ( !isActAsComponentAnnotated( typeElement ) && !isDisposeTrackableComponent( typeElement ) )
1957        {
1958          throw new ProcessorException( "@ComponentDependency target must return an instance compatible with " +
1959                                        Constants.DISPOSE_NOTIFIER_CLASSNAME + " or a type annotated " +
1960                                        "with @ArezComponent(disposeNotifier=ENABLE) or @ActAsComponent", method );
1961        }
1962      }
1963    }
1964
1965    final boolean cascade = isActionCascade( method );
1966    return new DependencyDescriptor( descriptor, method, cascade );
1967  }
1968
1969  @Nonnull
1970  private DependencyDescriptor createFieldDependencyDescriptor( @Nonnull final ComponentDescriptor descriptor,
1971                                                                @Nonnull final VariableElement field )
1972  {
1973    MemberChecks.mustBeSubclassCallable( descriptor.getElement(),
1974                                         Constants.COMPONENT_CLASSNAME,
1975                                         Constants.COMPONENT_DEPENDENCY_CLASSNAME,
1976                                         field );
1977    MemberChecks.mustBeFinal( Constants.COMPONENT_DEPENDENCY_CLASSNAME, field );
1978
1979    final boolean validateTypeAtRuntime =
1980      (Boolean) AnnotationsUtil.getAnnotationValue( field,
1981                                                    Constants.COMPONENT_DEPENDENCY_CLASSNAME,
1982                                                    "validateTypeAtRuntime" ).getValue();
1983
1984    final TypeMirror type = processingEnv.getTypeUtils().asMemberOf( descriptor.asDeclaredType(), field );
1985    if ( TypeKind.TYPEVAR != type.getKind() && TypeKind.DECLARED != type.getKind() )
1986    {
1987      throw new ProcessorException( "@ComponentDependency target must be a non-primitive value", field );
1988    }
1989    if ( !validateTypeAtRuntime )
1990    {
1991      final TypeElement disposeNotifier = getTypeElement( Constants.DISPOSE_NOTIFIER_CLASSNAME );
1992      if ( !processingEnv.getTypeUtils().isAssignable( type, disposeNotifier.asType() ) )
1993      {
1994        final Element element = processingEnv.getTypeUtils().asElement( type );
1995        if ( !( element instanceof TypeElement ) ||
1996             !isActAsComponentAnnotated( (TypeElement) element ) &&
1997             !isDisposeTrackableComponent( (TypeElement) element ) )
1998        {
1999          throw new ProcessorException( "@ComponentDependency target must be an instance compatible with " +
2000                                        Constants.DISPOSE_NOTIFIER_CLASSNAME + " or a type annotated " +
2001                                        "with @ArezComponent(disposeNotifier=ENABLE) or @ActAsComponent", field );
2002        }
2003      }
2004    }
2005
2006    if ( !isActionCascade( field ) )
2007    {
2008      throw new ProcessorException( "@ComponentDependency target defined an action of 'SET_NULL' but the " +
2009                                    "dependency is on a final field and can not be set to null.", field );
2010
2011    }
2012
2013    return new DependencyDescriptor( descriptor, field );
2014  }
2015
2016  private boolean isActionCascade( @Nonnull final Element method )
2017  {
2018    final String value =
2019      AnnotationsUtil.getEnumAnnotationParameter( method,
2020                                                  Constants.COMPONENT_DEPENDENCY_CLASSNAME,
2021                                                  "action" );
2022    return "CASCADE".equals( value );
2023  }
2024
2025  @SuppressWarnings( "BooleanMethodIsAlwaysInverted" )
2026  private boolean isActAsComponentAnnotated( @Nonnull final TypeElement typeElement )
2027  {
2028    return AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.ACT_AS_COMPONENT_CLASSNAME );
2029  }
2030
2031  @SuppressWarnings( "BooleanMethodIsAlwaysInverted" )
2032  private boolean isDisposeTrackableComponent( @Nonnull final TypeElement typeElement )
2033  {
2034    return AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.COMPONENT_CLASSNAME ) &&
2035           isDisposableTrackableRequired( typeElement );
2036  }
2037
2038  @Nonnull
2039  private ComponentDescriptor parse( @Nonnull final TypeElement typeElement )
2040    throws ProcessorException
2041  {
2042    if ( ElementKind.CLASS != typeElement.getKind() && ElementKind.INTERFACE != typeElement.getKind() )
2043    {
2044      throw new ProcessorException( "@ArezComponent target must be a class or an interface", typeElement );
2045    }
2046    else if ( typeElement.getModifiers().contains( Modifier.FINAL ) )
2047    {
2048      throw new ProcessorException( "@ArezComponent target must not be final", typeElement );
2049    }
2050    else if ( ElementsUtil.isNonStaticNestedClass( typeElement ) )
2051    {
2052      throw new ProcessorException( "@ArezComponent target must not be a non-static nested class", typeElement );
2053    }
2054    final AnnotationMirror arezComponent =
2055      AnnotationsUtil.getAnnotationByType( typeElement, Constants.COMPONENT_CLASSNAME );
2056    final String declaredName = getAnnotationParameter( arezComponent, "name" );
2057    final boolean disposeOnDeactivate = getAnnotationParameter( arezComponent, "disposeOnDeactivate" );
2058    final boolean observableFlag = isComponentObservableRequired( arezComponent, disposeOnDeactivate );
2059    final boolean service = isService( typeElement );
2060    final boolean disposeNotifierFlag = isDisposableTrackableRequired( typeElement );
2061    final boolean allowEmpty = getAnnotationParameter( arezComponent, "allowEmpty" );
2062    final List<VariableElement> fields = ElementsUtil.getFields( typeElement );
2063    ensureNoFieldInjections( fields );
2064    ensureNoMethodInjections( typeElement );
2065    final boolean sting = isStingIntegrationEnabled( arezComponent, service );
2066
2067    final AnnotationValue defaultReadOutsideTransaction =
2068      AnnotationsUtil.findAnnotationValueNoDefaults( arezComponent, "defaultReadOutsideTransaction" );
2069    final AnnotationValue defaultWriteOutsideTransaction =
2070      AnnotationsUtil.findAnnotationValueNoDefaults( arezComponent, "defaultWriteOutsideTransaction" );
2071
2072    final boolean requireEquals = isEqualsRequired( arezComponent );
2073    final boolean requireVerify = isVerifyRequired( arezComponent, typeElement );
2074
2075    if ( !typeElement.getModifiers().contains( Modifier.ABSTRACT ) )
2076    {
2077      throw new ProcessorException( "@ArezComponent target must be abstract", typeElement );
2078    }
2079
2080    final String name =
2081      Constants.SENTINEL.equals( declaredName ) ?
2082      typeElement.getQualifiedName().toString().replace( ".", "_" ) :
2083      declaredName;
2084
2085    if ( !SourceVersion.isIdentifier( name ) )
2086    {
2087      throw new ProcessorException( "@ArezComponent target specified an invalid name '" + name + "'. The " +
2088                                    "name must be a valid java identifier.", typeElement );
2089    }
2090    else if ( SourceVersion.isKeyword( name ) )
2091    {
2092      throw new ProcessorException( "@ArezComponent target specified an invalid name '" + name + "'. The " +
2093                                    "name must not be a java keyword.", typeElement );
2094    }
2095
2096    verifyConstructors( typeElement, sting );
2097
2098    if ( sting && !( (DeclaredType) typeElement.asType() ).getTypeArguments().isEmpty() )
2099    {
2100      throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME,
2101                                                          "enable sting integration and be a parameterized type" ),
2102                                    typeElement );
2103    }
2104    if ( !sting && AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.STING_EAGER ) )
2105    {
2106      throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME,
2107                                                          "disable sting integration and be annotated with " +
2108                                                          Constants.STING_EAGER ),
2109                                    typeElement );
2110    }
2111    if ( !sting && AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.STING_TYPED ) )
2112    {
2113      throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME,
2114                                                          "disable sting integration and be annotated with " +
2115                                                          Constants.STING_TYPED ),
2116                                    typeElement );
2117    }
2118    if ( !sting && AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.STING_NAMED ) )
2119    {
2120      throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME,
2121                                                          "disable sting integration and be annotated with " +
2122                                                          Constants.STING_NAMED ),
2123                                    typeElement );
2124    }
2125    if ( !sting && AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.STING_CONTRIBUTE_TO ) )
2126    {
2127      throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME,
2128                                                          "disable sting integration and be annotated with " +
2129                                                          Constants.STING_CONTRIBUTE_TO ),
2130                                    typeElement );
2131    }
2132
2133    if ( !observableFlag && disposeOnDeactivate )
2134    {
2135      throw new ProcessorException( "@ArezComponent target has specified observable = DISABLE and " +
2136                                    "disposeOnDeactivate = true which is not a valid combination", typeElement );
2137    }
2138
2139    if ( isWarningNotSuppressed( typeElement, Constants.WARNING_EXTENDS_COMPONENT ) )
2140    {
2141      TypeMirror parent = typeElement.getSuperclass();
2142      while ( null != parent )
2143      {
2144        final Element parentElement = processingEnv.getTypeUtils().asElement( parent );
2145        final TypeElement parentTypeElement =
2146          null != parentElement && ElementKind.CLASS == parentElement.getKind() ? (TypeElement) parentElement : null;
2147
2148        if ( null != parentTypeElement &&
2149             AnnotationsUtil.hasAnnotationOfType( parentTypeElement, Constants.COMPONENT_CLASSNAME ) )
2150        {
2151          final String message =
2152            MemberChecks.shouldNot( Constants.COMPONENT_CLASSNAME,
2153                                    "extend a class annotated with the " + Constants.COMPONENT_CLASSNAME +
2154                                    " annotation. " + suppressedBy( Constants.WARNING_EXTENDS_COMPONENT ) );
2155          processingEnv.getMessager().printMessage( WARNING, message, typeElement );
2156        }
2157        parent = null != parentTypeElement ? parentTypeElement.getSuperclass() : null;
2158      }
2159    }
2160
2161    final List<ExecutableElement> methods =
2162      ElementsUtil.getMethods( typeElement, processingEnv.getElementUtils(), processingEnv.getTypeUtils(), true );
2163    final boolean generateToString = methods.stream().
2164      noneMatch( m -> m.getSimpleName().toString().equals( "toString" ) &&
2165                      m.getParameters().isEmpty() &&
2166                      !( m.getEnclosingElement().getSimpleName().toString().equals( "Object" ) &&
2167                         "java.lang".equals( processingEnv
2168                                               .getElementUtils()
2169                                               .getPackageOf( m.getEnclosingElement() )
2170                                               .getQualifiedName()
2171                                               .toString() ) ) );
2172
2173    final String priority = getDefaultPriority( arezComponent );
2174    final Priority defaultPriority =
2175      null == priority ? null : "DEFAULT".equals( priority ) ? Priority.NORMAL : Priority.valueOf( priority );
2176
2177    final String defaultReadOutsideTransactionValue =
2178      null == defaultReadOutsideTransaction ?
2179      null :
2180      ( (VariableElement) defaultReadOutsideTransaction.getValue() ).getSimpleName().toString();
2181    final String defaultWriteOutsideTransactionValue =
2182      null == defaultWriteOutsideTransaction ?
2183      null :
2184      ( (VariableElement) defaultWriteOutsideTransaction.getValue() ).getSimpleName().toString();
2185
2186    final ComponentDescriptor descriptor =
2187      new ComponentDescriptor( name,
2188                               defaultPriority,
2189                               observableFlag,
2190                               disposeNotifierFlag,
2191                               disposeOnDeactivate,
2192                               sting,
2193                               requireEquals,
2194                               requireVerify,
2195                               generateToString,
2196                               typeElement,
2197                               defaultReadOutsideTransactionValue,
2198                               defaultWriteOutsideTransactionValue );
2199
2200    analyzeCandidateMethods( descriptor, methods, processingEnv.getTypeUtils() );
2201    validate( allowEmpty, descriptor );
2202
2203    for ( final ObservableDescriptor observable : descriptor.getObservables().values() )
2204    {
2205      if ( observable.expectSetter() )
2206      {
2207        final TypeMirror returnType = observable.getGetterType().getReturnType();
2208        final TypeMirror parameterType = observable.getSetterType().getParameterTypes().get( 0 );
2209        if ( !processingEnv.getTypeUtils().isSameType( parameterType, returnType ) &&
2210             !parameterType.toString().equals( returnType.toString() ) )
2211        {
2212          throw new ProcessorException( "@Observable property defines a setter and getter with different types." +
2213                                        " Getter type: " + returnType + " Setter type: " + parameterType + ".",
2214                                        observable.getGetter() );
2215        }
2216      }
2217    }
2218
2219    final boolean idRequired = isIdRequired( arezComponent );
2220    descriptor.setIdRequired( idRequired );
2221    if ( !idRequired )
2222    {
2223      if ( descriptor.hasComponentIdMethod() )
2224      {
2225        throw new ProcessorException( "@ArezComponent target has specified the idRequired = DISABLE " +
2226                                      "annotation parameter but also has annotated a method with @ComponentId " +
2227                                      "that requires idRequired = ENABLE.", typeElement );
2228      }
2229      if ( !descriptor.getComponentIdRefs().isEmpty() )
2230      {
2231        throw new ProcessorException( "@ArezComponent target has specified the idRequired = DISABLE " +
2232                                      "annotation parameter but also has annotated a method with @ComponentIdRef " +
2233                                      "that requires idRequired = ENABLE.", typeElement );
2234      }
2235      if ( !descriptor.getInverses().isEmpty() )
2236      {
2237        throw new ProcessorException( "@ArezComponent target has specified the idRequired = DISABLE " +
2238                                      "annotation parameter but also has annotated a method with @Inverse " +
2239                                      "that requires idRequired = ENABLE.", typeElement );
2240      }
2241    }
2242
2243    warnOnUnmanagedComponentReferences( descriptor, fields );
2244
2245    return descriptor;
2246  }
2247
2248  private boolean isStingIntegrationEnabled( @Nonnull final AnnotationMirror arezComponent, final boolean service )
2249  {
2250    final VariableElement parameter = getAnnotationParameter( arezComponent, "sting" );
2251    final String value = parameter.getSimpleName().toString();
2252    return "ENABLE".equals( value ) ||
2253           ( "AUTODETECT".equals( value ) &&
2254             service &&
2255             null != findTypeElement( Constants.STING_INJECTOR ) );
2256  }
2257
2258  private void verifyConstructors( @Nonnull final TypeElement typeElement, final boolean sting )
2259  {
2260    final List<ExecutableElement> constructors = ElementsUtil.getConstructors( typeElement );
2261    if ( constructors.size() > 1 )
2262    {
2263      if ( sting )
2264      {
2265        throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME,
2266                                                            "enable sting integration and have multiple constructors" ),
2267                                      typeElement );
2268      }
2269    }
2270
2271    for ( final ExecutableElement constructor : constructors )
2272    {
2273      if ( constructor.getModifiers().contains( Modifier.PROTECTED ) &&
2274           isWarningNotSuppressed( constructor, Constants.WARNING_PROTECTED_CONSTRUCTOR ) )
2275      {
2276        final String message =
2277          MemberChecks.should( Constants.COMPONENT_CLASSNAME,
2278                               "have a package access constructor. " +
2279                               suppressedBy( Constants.WARNING_PROTECTED_CONSTRUCTOR ) );
2280        processingEnv.getMessager().printMessage( WARNING, message, constructor );
2281      }
2282      verifyConstructorParameters( constructor, sting );
2283    }
2284  }
2285
2286  private void verifyConstructorParameters( @Nonnull final ExecutableElement constructor, final boolean sting )
2287  {
2288    for ( final VariableElement parameter : constructor.getParameters() )
2289    {
2290      final TypeMirror type = parameter.asType();
2291      if ( sting && TypesUtil.containsArrayType( type ) )
2292      {
2293        throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME,
2294                                                            "enable sting integration and contain a constructor with a parameter that contains an array type" ),
2295                                      parameter );
2296      }
2297      else if ( sting && TypesUtil.containsWildcard( type ) )
2298      {
2299        throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME,
2300                                                            "enable sting integration and contain a constructor with a parameter that contains a wildcard type parameter" ),
2301                                      parameter );
2302      }
2303      else if ( sting && TypesUtil.containsRawType( type ) )
2304      {
2305        throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME,
2306                                                            "enable sting integration and contain a constructor with a parameter that contains a raw type" ),
2307                                      parameter );
2308      }
2309      else if ( !sting && AnnotationsUtil.hasAnnotationOfType( parameter, Constants.STING_NAMED ) )
2310      {
2311        throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME,
2312                                                            "disable sting integration and contain a constructor with a parameter that is annotated with the " +
2313                                                            Constants.STING_NAMED + " annotation" ),
2314                                      parameter );
2315      }
2316      else if ( sting && TypeKind.DECLARED == type.getKind() && !( (DeclaredType) type ).getTypeArguments().isEmpty() )
2317      {
2318        throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME,
2319                                                            "enable sting integration and contain a constructor with a parameter that contains a parameterized type" ),
2320                                      parameter );
2321      }
2322    }
2323  }
2324
2325  private void ensureNoFieldInjections( @Nonnull final List<VariableElement> fields )
2326  {
2327    for ( final VariableElement field : fields )
2328    {
2329      if ( hasInjectAnnotation( field ) )
2330      {
2331        throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME,
2332                                                            "contain fields annotated by the " +
2333                                                            Constants.INJECT_CLASSNAME +
2334                                                            " annotation. Use constructor injection instead" ),
2335                                      field );
2336      }
2337    }
2338  }
2339
2340  private void ensureNoMethodInjections( @Nonnull final TypeElement typeElement )
2341  {
2342    final List<ExecutableElement> methods =
2343      ElementsUtil.getMethods( typeElement, processingEnv.getElementUtils(), processingEnv.getTypeUtils() );
2344    for ( final ExecutableElement method : methods )
2345    {
2346      if ( hasInjectAnnotation( method ) )
2347      {
2348        throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME,
2349                                                            "contain methods annotated by the " +
2350                                                            Constants.INJECT_CLASSNAME +
2351                                                            " annotation. Use constructor injection instead" ),
2352                                      method );
2353      }
2354    }
2355  }
2356
2357  private void analyzeCandidateMethods( @Nonnull final ComponentDescriptor componentDescriptor,
2358                                        @Nonnull final List<ExecutableElement> methods,
2359                                        @Nonnull final Types typeUtils )
2360    throws ProcessorException
2361  {
2362    for ( final ExecutableElement method : methods )
2363    {
2364      final String methodName = method.getSimpleName().toString();
2365      if ( AREZ_SPECIAL_METHODS.contains( methodName ) && method.getParameters().isEmpty() )
2366      {
2367        throw new ProcessorException( "Method defined on a class annotated by @ArezComponent uses a name " +
2368                                      "reserved by Arez", method );
2369      }
2370      else if ( methodName.startsWith( ComponentGenerator.FIELD_PREFIX ) ||
2371                methodName.startsWith( ComponentGenerator.OBSERVABLE_DATA_FIELD_PREFIX ) ||
2372                methodName.startsWith( ComponentGenerator.REFERENCE_FIELD_PREFIX ) ||
2373                methodName.startsWith( ComponentGenerator.FRAMEWORK_PREFIX ) )
2374      {
2375        throw new ProcessorException( "Method defined on a class annotated by @ArezComponent uses a name " +
2376                                      "with a prefix reserved by Arez", method );
2377      }
2378    }
2379    final Map<String, CandidateMethod> getters = new HashMap<>();
2380    final Map<String, CandidateMethod> captures = new HashMap<>();
2381    final Map<String, CandidateMethod> pushes = new HashMap<>();
2382    final Map<String, CandidateMethod> pops = new HashMap<>();
2383    final Map<String, CandidateMethod> setters = new HashMap<>();
2384    final Map<String, CandidateMethod> observes = new HashMap<>();
2385    final Map<String, CandidateMethod> onDepsChanges = new HashMap<>();
2386    for ( final ExecutableElement method : methods )
2387    {
2388      final ExecutableType methodType =
2389        (ExecutableType) typeUtils.asMemberOf( (DeclaredType) componentDescriptor.getElement().asType(), method );
2390      if ( !analyzeMethod( componentDescriptor, method, methodType ) )
2391      {
2392        /*
2393         * If we get here the method was not annotated so we can try to detect if it is a
2394         * candidate arez method in case some arez annotations are implied via naming conventions.
2395         */
2396        if ( method.getModifiers().contains( Modifier.STATIC ) )
2397        {
2398          continue;
2399        }
2400
2401        final CandidateMethod candidateMethod = new CandidateMethod( method, methodType );
2402        final boolean voidReturn = method.getReturnType().getKind() == TypeKind.VOID;
2403        final int parameterCount = method.getParameters().size();
2404        String name;
2405
2406        name = deriveName( method, PUSH_PATTERN, Constants.SENTINEL );
2407        if ( voidReturn && 1 == parameterCount && null != name )
2408        {
2409          pushes.put( name, candidateMethod );
2410          continue;
2411        }
2412        name = deriveName( method, POP_PATTERN, Constants.SENTINEL );
2413        if ( voidReturn && 1 == parameterCount && null != name )
2414        {
2415          pops.put( name, candidateMethod );
2416          continue;
2417        }
2418        name = deriveName( method, CAPTURE_PATTERN, Constants.SENTINEL );
2419        if ( !voidReturn && 0 == parameterCount && null != name )
2420        {
2421          captures.put( name, candidateMethod );
2422          continue;
2423        }
2424
2425        if ( !method.getModifiers().contains( Modifier.FINAL ) )
2426        {
2427          name = deriveName( method, SETTER_PATTERN, Constants.SENTINEL );
2428          if ( voidReturn && 1 == parameterCount && null != name )
2429          {
2430            setters.put( name, candidateMethod );
2431            continue;
2432          }
2433          name = deriveName( method, ISSER_PATTERN, Constants.SENTINEL );
2434          if ( !voidReturn && 0 == parameterCount && null != name )
2435          {
2436            getters.put( name, candidateMethod );
2437            continue;
2438          }
2439          name = deriveName( method, GETTER_PATTERN, Constants.SENTINEL );
2440          if ( !voidReturn && 0 == parameterCount && null != name )
2441          {
2442            getters.put( name, candidateMethod );
2443            continue;
2444          }
2445        }
2446        name = deriveName( method, ON_DEPS_CHANGE_PATTERN, Constants.SENTINEL );
2447        if ( voidReturn && null != name )
2448        {
2449          if ( 0 == parameterCount ||
2450               (
2451                 1 == parameterCount &&
2452                 Constants.OBSERVER_CLASSNAME.equals( method.getParameters().get( 0 ).asType().toString() )
2453               )
2454          )
2455          {
2456            onDepsChanges.put( name, candidateMethod );
2457            continue;
2458          }
2459        }
2460
2461        final String methodName = method.getSimpleName().toString();
2462        if ( !OBJECT_METHODS.contains( methodName ) )
2463        {
2464          observes.put( methodName, candidateMethod );
2465        }
2466      }
2467    }
2468
2469    linkUnAnnotatedObservables( componentDescriptor, getters, setters );
2470    linkUnAnnotatedObserves( componentDescriptor, observes, onDepsChanges );
2471    linkUnMemoizeContextParameters( componentDescriptor, captures, pushes, pops );
2472    linkObserverRefs( componentDescriptor );
2473    linkCascadeDisposeObservables( componentDescriptor );
2474    linkCascadeDisposeReferences( componentDescriptor );
2475
2476    // CascadeDispose returned false but it was actually processed so lets remove them from getters set
2477
2478    componentDescriptor.getCascadeDisposes().keySet().forEach( method -> {
2479      for ( final Map.Entry<String, CandidateMethod> entry : new HashMap<>( getters ).entrySet() )
2480      {
2481        if ( method.equals( entry.getValue().getMethod() ) )
2482        {
2483          getters.remove( entry.getKey() );
2484        }
2485      }
2486    } );
2487
2488    linkMemoizeContextParametersToMemoizes( componentDescriptor );
2489
2490    linkDependencies( componentDescriptor, getters.values() );
2491
2492    autodetectObservableInitializers( componentDescriptor );
2493
2494    /*
2495     * All of the maps will have called remove() for all matching candidates.
2496     * Thus any left are the non-arez methods.
2497     */
2498
2499    ensureNoAbstractMethods( componentDescriptor, getters.values() );
2500    ensureNoAbstractMethods( componentDescriptor, setters.values() );
2501    ensureNoAbstractMethods( componentDescriptor, observes.values() );
2502    ensureNoAbstractMethods( componentDescriptor, onDepsChanges.values() );
2503
2504    processCascadeDisposeFields( componentDescriptor );
2505    processComponentDependencyFields( componentDescriptor );
2506  }
2507
2508  private static void linkMemoizeContextParametersToMemoizes( final @Nonnull ComponentDescriptor componentDescriptor )
2509  {
2510    // Link MemoizeContextParameters to associated Memoize descriptors
2511    componentDescriptor
2512      .getMemoizes()
2513      .values()
2514      .forEach( m ->
2515                  componentDescriptor
2516                    .getMemoizeContextParameters()
2517                    .values()
2518                    .forEach( p -> p.tryMatchMemoizeDescriptor( m ) ) );
2519  }
2520
2521  private void linkUnMemoizeContextParameters( @Nonnull final ComponentDescriptor componentDescriptor,
2522                                               @Nonnull final Map<String, CandidateMethod> captures,
2523                                               @Nonnull final Map<String, CandidateMethod> pushes,
2524                                               @Nonnull final Map<String, CandidateMethod> pops )
2525  {
2526    final var parameters = componentDescriptor.getMemoizeContextParameters().values();
2527    for ( final var parameter : parameters )
2528    {
2529      if ( !parameter.hasCapture() )
2530      {
2531        final CandidateMethod capture = captures.remove( parameter.getName() );
2532        if ( null != capture )
2533        {
2534          parameter.linkUnAnnotatedCapture( capture.getMethod(), capture.getMethodType() );
2535        }
2536      }
2537      if ( !parameter.hasPop() )
2538      {
2539        final CandidateMethod pop = pops.remove( parameter.getName() );
2540        if ( null != pop )
2541        {
2542          parameter.linkUnAnnotatedPop( pop.getMethod(), pop.getMethodType() );
2543        }
2544      }
2545      if ( !parameter.hasPush() )
2546      {
2547        final CandidateMethod push = pushes.remove( parameter.getName() );
2548        if ( null != push )
2549        {
2550          parameter.linkUnAnnotatedPush( push.getMethod(), push.getMethodType() );
2551        }
2552      }
2553    }
2554  }
2555
2556  private void ensureNoAbstractMethods( @Nonnull final ComponentDescriptor componentDescriptor,
2557                                        @Nonnull final Collection<CandidateMethod> candidateMethods )
2558  {
2559    candidateMethods
2560      .stream()
2561      .map( CandidateMethod::getMethod )
2562      .filter( m -> m.getModifiers().contains( Modifier.ABSTRACT ) )
2563      .forEach( m -> {
2564        throw new ProcessorException( "@ArezComponent target has an abstract method not implemented by " +
2565                                      "framework. The method is named " + m.getSimpleName(),
2566                                      componentDescriptor.getElement() );
2567      } );
2568  }
2569
2570  private boolean analyzeMethod( @Nonnull final ComponentDescriptor descriptor,
2571                                 @Nonnull final ExecutableElement method,
2572                                 @Nonnull final ExecutableType methodType )
2573    throws ProcessorException
2574  {
2575    emitWarningForUnnecessaryProtectedMethod( descriptor, method );
2576    emitWarningForUnnecessaryFinalMethod( descriptor, method );
2577    verifyNoDuplicateAnnotations( method );
2578
2579    final AnnotationMirror action =
2580      AnnotationsUtil.findAnnotationByType( method, Constants.ACTION_CLASSNAME );
2581    final AnnotationMirror jaxWsAction =
2582      AnnotationsUtil.findAnnotationByType( method, Constants.JAX_WS_ACTION_CLASSNAME );
2583    final AnnotationMirror observed =
2584      AnnotationsUtil.findAnnotationByType( method, Constants.OBSERVE_CLASSNAME );
2585    final AnnotationMirror observable =
2586      AnnotationsUtil.findAnnotationByType( method, Constants.OBSERVABLE_CLASSNAME );
2587    final AnnotationMirror observableValueRef =
2588      AnnotationsUtil.findAnnotationByType( method, Constants.OBSERVABLE_VALUE_REF_CLASSNAME );
2589    final AnnotationMirror memoize =
2590      AnnotationsUtil.findAnnotationByType( method, Constants.MEMOIZE_CLASSNAME );
2591    final AnnotationMirror memoizeContextParameter =
2592      AnnotationsUtil.findAnnotationByType( method, Constants.MEMOIZE_CONTEXT_PARAMETER_CLASSNAME );
2593    final AnnotationMirror computableValueRef =
2594      AnnotationsUtil.findAnnotationByType( method, Constants.COMPUTABLE_VALUE_REF_CLASSNAME );
2595    final AnnotationMirror contextRef =
2596      AnnotationsUtil.findAnnotationByType( method, Constants.CONTEXT_REF_CLASSNAME );
2597    final AnnotationMirror stateRef =
2598      AnnotationsUtil.findAnnotationByType( method, Constants.COMPONENT_STATE_REF_CLASSNAME );
2599    final AnnotationMirror componentRef =
2600      AnnotationsUtil.findAnnotationByType( method, Constants.COMPONENT_REF_CLASSNAME );
2601    final AnnotationMirror componentId =
2602      AnnotationsUtil.findAnnotationByType( method, Constants.COMPONENT_ID_CLASSNAME );
2603    final AnnotationMirror componentIdRef =
2604      AnnotationsUtil.findAnnotationByType( method, Constants.COMPONENT_ID_REF_CLASSNAME );
2605    final AnnotationMirror componentTypeName =
2606      AnnotationsUtil.findAnnotationByType( method, Constants.COMPONENT_TYPE_NAME_REF_CLASSNAME );
2607    final AnnotationMirror componentNameRef =
2608      AnnotationsUtil.findAnnotationByType( method, Constants.COMPONENT_NAME_REF_CLASSNAME );
2609    final AnnotationMirror postConstruct =
2610      AnnotationsUtil.findAnnotationByType( method, Constants.POST_CONSTRUCT_CLASSNAME );
2611    final AnnotationMirror ejbPostConstruct =
2612      AnnotationsUtil.findAnnotationByType( method, Constants.EJB_POST_CONSTRUCT_CLASSNAME );
2613    final AnnotationMirror preDispose =
2614      AnnotationsUtil.findAnnotationByType( method, Constants.PRE_DISPOSE_CLASSNAME );
2615    final AnnotationMirror postDispose =
2616      AnnotationsUtil.findAnnotationByType( method, Constants.POST_DISPOSE_CLASSNAME );
2617    final AnnotationMirror onActivate =
2618      AnnotationsUtil.findAnnotationByType( method, Constants.ON_ACTIVATE_CLASSNAME );
2619    final AnnotationMirror onDeactivate =
2620      AnnotationsUtil.findAnnotationByType( method, Constants.ON_DEACTIVATE_CLASSNAME );
2621    final AnnotationMirror onDepsChange =
2622      AnnotationsUtil.findAnnotationByType( method, Constants.ON_DEPS_CHANGE_CLASSNAME );
2623    final AnnotationMirror observerRef =
2624      AnnotationsUtil.findAnnotationByType( method, Constants.OBSERVER_REF_CLASSNAME );
2625    final AnnotationMirror dependency =
2626      AnnotationsUtil.findAnnotationByType( method, Constants.COMPONENT_DEPENDENCY_CLASSNAME );
2627    final AnnotationMirror reference =
2628      AnnotationsUtil.findAnnotationByType( method, Constants.REFERENCE_CLASSNAME );
2629    final AnnotationMirror referenceId =
2630      AnnotationsUtil.findAnnotationByType( method, Constants.REFERENCE_ID_CLASSNAME );
2631    final AnnotationMirror inverse =
2632      AnnotationsUtil.findAnnotationByType( method, Constants.INVERSE_CLASSNAME );
2633    final AnnotationMirror preInverseRemove =
2634      AnnotationsUtil.findAnnotationByType( method, Constants.PRE_INVERSE_REMOVE_CLASSNAME );
2635    final AnnotationMirror postInverseAdd =
2636      AnnotationsUtil.findAnnotationByType( method, Constants.POST_INVERSE_ADD_CLASSNAME );
2637    final AnnotationMirror cascadeDispose =
2638      AnnotationsUtil.findAnnotationByType( method, Constants.CASCADE_DISPOSE_CLASSNAME );
2639
2640    if ( null != observable )
2641    {
2642      final ObservableDescriptor observableDescriptor = addObservable( descriptor,
2643                                                                       observable, method, methodType );
2644      if ( null != referenceId )
2645      {
2646        addReferenceId( descriptor, referenceId, observableDescriptor, method );
2647      }
2648      if ( null != inverse )
2649      {
2650        addInverse( descriptor, inverse, observableDescriptor, method );
2651      }
2652      if ( null != cascadeDispose )
2653      {
2654        addCascadeDisposeMethod( descriptor, method, observableDescriptor );
2655      }
2656      return true;
2657    }
2658    else if ( null != observableValueRef )
2659    {
2660      addObservableValueRef( descriptor, observableValueRef, method, methodType );
2661      return true;
2662    }
2663    else if ( null != action )
2664    {
2665      if ( null != postConstruct )
2666      {
2667        addPostConstruct( descriptor, method );
2668      }
2669      addAction( descriptor, action, method, methodType );
2670      return true;
2671    }
2672    else if ( null != observed )
2673    {
2674      addObserve( descriptor, observed, method, methodType );
2675      return true;
2676    }
2677    else if ( null != onDepsChange )
2678    {
2679      addOnDepsChange( descriptor, onDepsChange, method );
2680      return true;
2681    }
2682    else if ( null != observerRef )
2683    {
2684      addObserverRef( descriptor, observerRef, method, methodType );
2685      return true;
2686    }
2687    else if ( null != contextRef )
2688    {
2689      addContextRef( descriptor, method );
2690      return true;
2691    }
2692    else if ( null != stateRef )
2693    {
2694      addComponentStateRef( descriptor, stateRef, method );
2695      return true;
2696    }
2697    else if ( null != memoizeContextParameter )
2698    {
2699      addMemoizeContextParameter( descriptor, memoizeContextParameter, method, methodType );
2700      return true;
2701    }
2702    else if ( null != memoize )
2703    {
2704      addMemoize( descriptor, memoize, method, methodType );
2705      return true;
2706    }
2707    else if ( null != computableValueRef )
2708    {
2709      addComputableValueRef( descriptor, computableValueRef, method, methodType );
2710      return true;
2711    }
2712    else if ( null != reference )
2713    {
2714      if ( null != cascadeDispose )
2715      {
2716        addCascadeDisposeMethod( descriptor, method, null );
2717      }
2718      addReference( descriptor, reference, method, methodType );
2719      return true;
2720    }
2721    else if ( null != cascadeDispose )
2722    {
2723      addCascadeDisposeMethod( descriptor, method, null );
2724      // Return false so that it can be picked as the getter of an @Observable or linked to a @Reference
2725      return false;
2726    }
2727    else if ( null != componentIdRef )
2728    {
2729      addComponentIdRef( descriptor, method );
2730      return true;
2731    }
2732    else if ( null != componentRef )
2733    {
2734      addComponentRef( descriptor, method );
2735      return true;
2736    }
2737    else if ( null != componentId )
2738    {
2739      setComponentId( descriptor, method, methodType );
2740      return true;
2741    }
2742    else if ( null != componentNameRef )
2743    {
2744      addComponentNameRef( descriptor, method );
2745      return true;
2746    }
2747    else if ( null != componentTypeName )
2748    {
2749      setComponentTypeNameRef( descriptor, method );
2750      return true;
2751    }
2752    else if ( null != jaxWsAction )
2753    {
2754      throw new ProcessorException( "@" + Constants.JAX_WS_ACTION_CLASSNAME + " annotation " +
2755                                    "not supported in components annotated with @ArezComponent, use the @" +
2756                                    Constants.ACTION_CLASSNAME + " annotation instead.",
2757                                    method );
2758    }
2759    else if ( null != ejbPostConstruct )
2760    {
2761      throw new ProcessorException( "@" + Constants.EJB_POST_CONSTRUCT_CLASSNAME + " annotation " +
2762                                    "not supported in components annotated with @ArezComponent, use the @" +
2763                                    Constants.POST_CONSTRUCT_CLASSNAME + " annotation instead.",
2764                                    method );
2765    }
2766    else if ( null != postConstruct )
2767    {
2768      addPostConstruct( descriptor, method );
2769      return true;
2770    }
2771    else if ( null != preDispose )
2772    {
2773      addPreDispose( descriptor, method );
2774      return true;
2775    }
2776    else if ( null != postDispose )
2777    {
2778      addPostDispose( descriptor, method );
2779      return true;
2780    }
2781    else if ( null != onActivate )
2782    {
2783      addOnActivate( descriptor, onActivate, method );
2784      return true;
2785    }
2786    else if ( null != onDeactivate )
2787    {
2788      addOnDeactivate( descriptor, onDeactivate, method );
2789      return true;
2790    }
2791    else if ( null != dependency )
2792    {
2793      descriptor.addDependency( createMethodDependencyDescriptor( descriptor, method ) );
2794      return false;
2795    }
2796    else if ( null != referenceId )
2797    {
2798      addReferenceId( descriptor, referenceId, method );
2799      return true;
2800    }
2801    else if ( null != inverse )
2802    {
2803      addInverse( descriptor, inverse, method, methodType );
2804      return true;
2805    }
2806    else if ( null != preInverseRemove )
2807    {
2808      addPreInverseRemove( descriptor, preInverseRemove, method );
2809      return true;
2810    }
2811    else if ( null != postInverseAdd )
2812    {
2813      addPostInverseAdd( descriptor, postInverseAdd, method );
2814      return true;
2815    }
2816    else
2817    {
2818      return false;
2819    }
2820  }
2821
2822  private void emitWarningForUnnecessaryProtectedMethod( @Nonnull final ComponentDescriptor descriptor,
2823                                                         @Nonnull final ExecutableElement method )
2824  {
2825    if ( method.getModifiers().contains( Modifier.PROTECTED ) &&
2826         Objects.equals( method.getEnclosingElement(), descriptor.getElement() ) &&
2827         ElementsUtil.isWarningNotSuppressed( method,
2828                                              Constants.WARNING_PROTECTED_METHOD,
2829                                              Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ) &&
2830         !isMethodAProtectedOverride( descriptor.getElement(), method ) )
2831    {
2832      final String message =
2833        MemberChecks.shouldNot( Constants.COMPONENT_CLASSNAME,
2834                                "declare a protected method. " +
2835                                MemberChecks.suppressedBy( Constants.WARNING_PROTECTED_METHOD,
2836                                                           Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ) );
2837      processingEnv.getMessager().printMessage( Diagnostic.Kind.WARNING, message, method );
2838    }
2839  }
2840
2841  private void emitWarningForUnnecessaryFinalMethod( @Nonnull final ComponentDescriptor descriptor,
2842                                                     @Nonnull final ExecutableElement method )
2843  {
2844    if ( method.getModifiers().contains( Modifier.FINAL ) &&
2845         Objects.equals( method.getEnclosingElement(), descriptor.getElement() ) &&
2846         ElementsUtil.isWarningNotSuppressed( method,
2847                                              Constants.WARNING_FINAL_METHOD,
2848                                              Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ) )
2849    {
2850      final String message =
2851        MemberChecks.shouldNot( Constants.COMPONENT_CLASSNAME,
2852                                "declare a final method. " +
2853                                MemberChecks.suppressedBy( Constants.WARNING_FINAL_METHOD,
2854                                                           Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ) );
2855      processingEnv.getMessager().printMessage( Diagnostic.Kind.WARNING, message, method );
2856    }
2857  }
2858
2859  private void addReferenceId( @Nonnull final ComponentDescriptor descriptor,
2860                               @Nonnull final AnnotationMirror annotation,
2861                               @Nonnull final ObservableDescriptor observable,
2862                               @Nonnull final ExecutableElement method )
2863  {
2864    MemberChecks.mustNotHaveAnyParameters( Constants.REFERENCE_ID_CLASSNAME, method );
2865    MemberChecks.mustBeSubclassCallable( descriptor.getElement(),
2866                                         Constants.COMPONENT_CLASSNAME,
2867                                         Constants.REFERENCE_ID_CLASSNAME,
2868                                         method );
2869    MemberChecks.mustNotThrowAnyExceptions( Constants.REFERENCE_ID_CLASSNAME, method );
2870    MemberChecks.mustReturnAValue( Constants.REFERENCE_ID_CLASSNAME, method );
2871
2872    final String name = getReferenceIdName( annotation, method );
2873    descriptor.findOrCreateReference( name ).setObservable( observable );
2874  }
2875
2876  private void addReferenceId( @Nonnull final ComponentDescriptor descriptor,
2877                               @Nonnull final AnnotationMirror annotation,
2878                               @Nonnull final ExecutableElement method )
2879  {
2880    MemberChecks.mustNotHaveAnyParameters( Constants.REFERENCE_ID_CLASSNAME, method );
2881    MemberChecks.mustBeSubclassCallable( descriptor.getElement(),
2882                                         Constants.COMPONENT_CLASSNAME,
2883                                         Constants.REFERENCE_ID_CLASSNAME,
2884                                         method );
2885    MemberChecks.mustNotThrowAnyExceptions( Constants.REFERENCE_ID_CLASSNAME, method );
2886    MemberChecks.mustReturnAValue( Constants.REFERENCE_ID_CLASSNAME, method );
2887
2888    final String name = getReferenceIdName( annotation, method );
2889    descriptor.findOrCreateReference( name ).setIdMethod( method );
2890  }
2891
2892  @Nonnull
2893  private String getReferenceIdName( @Nonnull final AnnotationMirror annotation,
2894                                     @Nonnull final ExecutableElement method )
2895  {
2896    final String declaredName = AnnotationsUtil.getAnnotationValueValue( annotation, "name" );
2897    final String name;
2898    if ( Constants.SENTINEL.equals( declaredName ) )
2899    {
2900      final String candidate = deriveName( method, ID_GETTER_PATTERN, declaredName );
2901      if ( null == candidate )
2902      {
2903        final String candidate2 = deriveName( method, RAW_ID_GETTER_PATTERN, declaredName );
2904        if ( null == candidate2 )
2905        {
2906          throw new ProcessorException( "@ReferenceId target has not specified a name and does not follow " +
2907                                        "the convention \"get[Name]Id\" or \"[name]Id\"", method );
2908        }
2909        else
2910        {
2911          name = candidate2;
2912        }
2913      }
2914      else
2915      {
2916        name = candidate;
2917      }
2918    }
2919    else
2920    {
2921      name = declaredName;
2922      if ( !SourceVersion.isIdentifier( name ) )
2923      {
2924        throw new ProcessorException( "@ReferenceId target specified an invalid name '" + name + "'. The " +
2925                                      "name must be a valid java identifier.", method );
2926      }
2927      else if ( SourceVersion.isKeyword( name ) )
2928      {
2929        throw new ProcessorException( "@ReferenceId target specified an invalid name '" + name + "'. The " +
2930                                      "name must not be a java keyword.", method );
2931      }
2932    }
2933    return name;
2934  }
2935
2936  private void addPreInverseRemove( @Nonnull final ComponentDescriptor component,
2937                                    @Nonnull final AnnotationMirror annotation,
2938                                    @Nonnull final ExecutableElement method )
2939    throws ProcessorException
2940  {
2941    mustBeHookHook( component.getElement(),
2942                    Constants.PRE_INVERSE_REMOVE_CLASSNAME,
2943                    method );
2944    shouldBeInternalHookMethod( processingEnv,
2945                                component,
2946                                method,
2947                                Constants.PRE_INVERSE_REMOVE_CLASSNAME );
2948    if ( 1 != method.getParameters().size() )
2949    {
2950      throw new ProcessorException( MemberChecks.must( Constants.PRE_INVERSE_REMOVE_CLASSNAME,
2951                                                       "have exactly 1 parameter" ), method );
2952    }
2953
2954    final String name = getPreInverseRemoveName( annotation, method );
2955    findOrCreateInverseDescriptor( component, name ).addPreInverseRemoveHook( method );
2956  }
2957
2958  @Nonnull
2959  private String getPreInverseRemoveName( @Nonnull final AnnotationMirror annotation,
2960                                          @Nonnull final ExecutableElement method )
2961  {
2962    final String name = AnnotationsUtil.getAnnotationValueValue( annotation, "name" );
2963    if ( Constants.SENTINEL.equals( name ) )
2964    {
2965      final String candidate = deriveName( method, PRE_INVERSE_REMOVE_PATTERN, name );
2966      if ( null == candidate )
2967      {
2968        throw new ProcessorException( "@PreInverseRemove target has not specified a name and does not follow " +
2969                                      "the convention \"pre[Name]Remove\"", method );
2970      }
2971      else
2972      {
2973        return candidate;
2974      }
2975    }
2976    else
2977    {
2978      if ( !SourceVersion.isIdentifier( name ) )
2979      {
2980        throw new ProcessorException( "@PreInverseRemove target specified an invalid name '" + name + "'. The " +
2981                                      "name must be a valid java identifier", method );
2982      }
2983      else if ( SourceVersion.isKeyword( name ) )
2984      {
2985        throw new ProcessorException( "@PreInverseRemove target specified an invalid name '" + name + "'. The " +
2986                                      "name must not be a java keyword", method );
2987      }
2988      return name;
2989    }
2990  }
2991
2992  private void addPostInverseAdd( @Nonnull final ComponentDescriptor component,
2993                                  @Nonnull final AnnotationMirror annotation,
2994                                  @Nonnull final ExecutableElement method )
2995    throws ProcessorException
2996  {
2997    mustBeHookHook( component.getElement(),
2998                    Constants.POST_INVERSE_ADD_CLASSNAME,
2999                    method );
3000    shouldBeInternalHookMethod( processingEnv,
3001                                component,
3002                                method,
3003                                Constants.POST_INVERSE_ADD_CLASSNAME );
3004    if ( 1 != method.getParameters().size() )
3005    {
3006      throw new ProcessorException( MemberChecks.must( Constants.POST_INVERSE_ADD_CLASSNAME,
3007                                                       "have exactly 1 parameter" ), method );
3008    }
3009    final String name = getPostInverseAddName( annotation, method );
3010    findOrCreateInverseDescriptor( component, name ).addPostInverseAddHook( method );
3011  }
3012
3013  @Nonnull
3014  private String getPostInverseAddName( @Nonnull final AnnotationMirror annotation,
3015                                        @Nonnull final ExecutableElement method )
3016  {
3017    final String name = AnnotationsUtil.getAnnotationValueValue( annotation, "name" );
3018    if ( Constants.SENTINEL.equals( name ) )
3019    {
3020      final String candidate = deriveName( method, POST_INVERSE_ADD_PATTERN, name );
3021      if ( null == candidate )
3022      {
3023        throw new ProcessorException( "@PostInverseAdd target has not specified a name and does not follow " +
3024                                      "the convention \"post[Name]Add\"", method );
3025      }
3026      else
3027      {
3028        return candidate;
3029      }
3030    }
3031    else
3032    {
3033      if ( !SourceVersion.isIdentifier( name ) )
3034      {
3035        throw new ProcessorException( "@PostInverseAdd target specified an invalid name '" + name + "'. The " +
3036                                      "name must be a valid java identifier", method );
3037      }
3038      else if ( SourceVersion.isKeyword( name ) )
3039      {
3040        throw new ProcessorException( "@PostInverseAdd target specified an invalid name '" + name + "'. The " +
3041                                      "name must not be a java keyword", method );
3042      }
3043      return name;
3044    }
3045  }
3046
3047  private void addInverse( @Nonnull final ComponentDescriptor descriptor,
3048                           @Nonnull final AnnotationMirror annotation,
3049                           @Nonnull final ExecutableElement method,
3050                           @Nonnull final ExecutableType methodType )
3051  {
3052    MemberChecks.mustNotHaveAnyParameters( Constants.INVERSE_CLASSNAME, method );
3053    MemberChecks.mustBeSubclassCallable( descriptor.getElement(),
3054                                         Constants.COMPONENT_CLASSNAME,
3055                                         Constants.INVERSE_CLASSNAME,
3056                                         method );
3057    MemberChecks.mustNotThrowAnyExceptions( Constants.INVERSE_CLASSNAME, method );
3058    MemberChecks.mustReturnAValue( Constants.INVERSE_CLASSNAME, method );
3059    MemberChecks.mustBeAbstract( Constants.INVERSE_CLASSNAME, method );
3060
3061    final String name = getInverseName( annotation, method );
3062    final ObservableDescriptor observable = descriptor.findOrCreateObservable( name );
3063    observable.setGetter( method, methodType );
3064
3065    addInverse( descriptor, annotation, observable, method );
3066  }
3067
3068  private void addInverse( @Nonnull final ComponentDescriptor descriptor,
3069                           @Nonnull final AnnotationMirror annotation,
3070                           @Nonnull final ObservableDescriptor observable,
3071                           @Nonnull final ExecutableElement method )
3072  {
3073    MemberChecks.mustNotHaveAnyParameters( Constants.INVERSE_CLASSNAME, method );
3074    MemberChecks.mustBeSubclassCallable( descriptor.getElement(),
3075                                         Constants.COMPONENT_CLASSNAME,
3076                                         Constants.INVERSE_CLASSNAME,
3077                                         method );
3078    MemberChecks.mustNotThrowAnyExceptions( Constants.INVERSE_CLASSNAME, method );
3079    MemberChecks.mustReturnAValue( Constants.INVERSE_CLASSNAME, method );
3080    MemberChecks.mustBeAbstract( Constants.INVERSE_CLASSNAME, method );
3081
3082    final String name = getInverseName( annotation, method );
3083    final InverseDescriptor existing = descriptor.getInverses().get( name );
3084    if ( null != existing && existing.hasObservable() )
3085    {
3086      throw new ProcessorException( "@Inverse target defines duplicate inverse for name '" + name +
3087                                    "'. The other inverse is " + existing.getObservable().getGetter(),
3088                                    method );
3089    }
3090    else
3091    {
3092      final TypeMirror type = method.getReturnType();
3093
3094      final Multiplicity multiplicity;
3095      TypeElement targetType = getInverseManyTypeTarget( method );
3096      if ( null != targetType )
3097      {
3098        multiplicity = Multiplicity.MANY;
3099      }
3100      else
3101      {
3102        if ( !( type instanceof DeclaredType ) ||
3103             !AnnotationsUtil.hasAnnotationOfType( ( (DeclaredType) type ).asElement(),
3104                                                   Constants.COMPONENT_CLASSNAME ) )
3105        {
3106          throw new ProcessorException( "@Inverse target expected to return a type annotated with " +
3107                                        Constants.COMPONENT_CLASSNAME, method );
3108        }
3109        targetType = (TypeElement) ( (DeclaredType) type ).asElement();
3110        if ( AnnotationsUtil.hasNonnullAnnotation( method ) )
3111        {
3112          multiplicity = Multiplicity.ONE;
3113        }
3114        else if ( AnnotationsUtil.hasNullableAnnotation( method ) )
3115        {
3116          multiplicity = Multiplicity.ZERO_OR_ONE;
3117        }
3118        else
3119        {
3120          throw new ProcessorException( "@Inverse target expected to be annotated with either " +
3121                                        AnnotationsUtil.NULLABLE_CLASSNAME + " or " +
3122                                        AnnotationsUtil.NONNULL_CLASSNAME, method );
3123        }
3124      }
3125      final String referenceName = getInverseReferenceNameParameter( descriptor, method );
3126
3127      final InverseDescriptor inverse = findOrCreateInverseDescriptor( descriptor, name );
3128      final String otherName = firstCharacterToLowerCase( targetType.getSimpleName().toString() );
3129      inverse.setInverse( observable, referenceName, multiplicity, targetType, otherName );
3130      verifyMultiplicityOfAssociatedReferenceMethod( descriptor, inverse );
3131    }
3132  }
3133
3134  @Nonnull
3135  private InverseDescriptor findOrCreateInverseDescriptor( @Nonnull final ComponentDescriptor descriptor,
3136                                                           @Nonnull final String name )
3137  {
3138    return descriptor.getInverses().computeIfAbsent( name, n -> new InverseDescriptor( descriptor, name ) );
3139  }
3140
3141  @Nonnull
3142  private String getInverseName( @Nonnull final AnnotationMirror annotation,
3143                                 @Nonnull final ExecutableElement method )
3144  {
3145    final String declaredName = AnnotationsUtil.getAnnotationValueValue( annotation, "name" );
3146    final String name;
3147    if ( Constants.SENTINEL.equals( declaredName ) )
3148    {
3149      final String candidate = deriveName( method, GETTER_PATTERN, declaredName );
3150      name = null == candidate ? method.getSimpleName().toString() : candidate;
3151    }
3152    else
3153    {
3154      name = declaredName;
3155      if ( !SourceVersion.isIdentifier( name ) )
3156      {
3157        throw new ProcessorException( "@Inverse target specified an invalid name '" + name + "'. The " +
3158                                      "name must be a valid java identifier.", method );
3159      }
3160      else if ( SourceVersion.isKeyword( name ) )
3161      {
3162        throw new ProcessorException( "@Inverse target specified an invalid name '" + name + "'. The " +
3163                                      "name must not be a java keyword.", method );
3164      }
3165    }
3166    return name;
3167  }
3168
3169  private void warnOnUnmanagedComponentReferences( @Nonnull final ComponentDescriptor descriptor,
3170                                                   @Nonnull final List<VariableElement> fields )
3171  {
3172    final TypeElement disposeNotifier = getTypeElement( Constants.DISPOSE_NOTIFIER_CLASSNAME );
3173
3174    final Set<String> injectedTypes = new HashSet<>();
3175    if ( descriptor.isStingEnabled() )
3176    {
3177      for ( final ExecutableElement constructor : ElementsUtil.getConstructors( descriptor.getElement() ) )
3178      {
3179        final List<? extends VariableElement> parameters = constructor.getParameters();
3180        for ( final VariableElement parameter : parameters )
3181        {
3182          final boolean isDisposeNotifier = isAssignable( parameter.asType(), disposeNotifier );
3183          final boolean isTypeAnnotatedByComponentAnnotation =
3184            !isDisposeNotifier && isTypeAnnotatedByComponentAnnotation( parameter );
3185          final boolean isTypeAnnotatedActAsComponent =
3186            !isDisposeNotifier &&
3187            !isTypeAnnotatedByComponentAnnotation &&
3188            isTypeAnnotatedByActAsComponentAnnotation( parameter );
3189          if ( isDisposeNotifier || isTypeAnnotatedByComponentAnnotation || isTypeAnnotatedActAsComponent )
3190          {
3191            injectedTypes.add( parameter.asType().toString() );
3192          }
3193        }
3194      }
3195    }
3196
3197    for ( final VariableElement field : fields )
3198    {
3199      if ( !field.getModifiers().contains( Modifier.STATIC ) &&
3200           SuperficialValidation.validateElement( processingEnv, field ) )
3201      {
3202        final boolean isDisposeNotifier = isAssignable( field.asType(), disposeNotifier );
3203        final boolean isTypeAnnotatedByComponentAnnotation =
3204          !isDisposeNotifier && isTypeAnnotatedByComponentAnnotation( field );
3205        final boolean isTypeAnnotatedActAsComponent =
3206          !isDisposeNotifier &&
3207          !isTypeAnnotatedByComponentAnnotation &&
3208          isTypeAnnotatedByActAsComponentAnnotation( field );
3209        if ( isDisposeNotifier || isTypeAnnotatedByComponentAnnotation || isTypeAnnotatedActAsComponent )
3210        {
3211          if ( !descriptor.isDependencyDefined( field ) &&
3212               !descriptor.isCascadeDisposeDefined( field ) &&
3213               ( isDisposeNotifier || isTypeAnnotatedActAsComponent || verifyReferencesToComponent( field ) ) &&
3214               isUnmanagedComponentReferenceNotSuppressed( field ) &&
3215               !injectedTypes.contains( field.asType().toString() ) )
3216          {
3217            final String label =
3218              isDisposeNotifier ? "an implementation of DisposeNotifier" :
3219              isTypeAnnotatedByComponentAnnotation ? "an Arez component" :
3220              "annotated with @ActAsComponent";
3221            final String message =
3222              "Field named '" + field.getSimpleName().toString() + "' has a type that is " + label +
3223              " but is not annotated with @" + Constants.CASCADE_DISPOSE_CLASSNAME + " or " +
3224              "@" + Constants.COMPONENT_DEPENDENCY_CLASSNAME + " and was not injected into the " +
3225              "constructor. This scenario can cause errors if the value is disposed. Please " +
3226              "annotate the field as appropriate or suppress the warning by annotating the field with " +
3227              "@SuppressWarnings( \"" + Constants.WARNING_UNMANAGED_COMPONENT_REFERENCE + "\" ) or " +
3228              "@SuppressArezWarnings( \"" + Constants.WARNING_UNMANAGED_COMPONENT_REFERENCE + "\" )";
3229            processingEnv.getMessager().printMessage( WARNING, message, field );
3230          }
3231        }
3232      }
3233    }
3234
3235    for ( final ObservableDescriptor observable : descriptor.getObservables().values() )
3236    {
3237      if ( observable.isAbstract() )
3238      {
3239        final ExecutableElement getter = observable.getGetter();
3240        if ( SuperficialValidation.validateElement( processingEnv, getter ) )
3241        {
3242          final TypeMirror returnType = getter.getReturnType();
3243          final Element returnElement = processingEnv.getTypeUtils().asElement( returnType );
3244          final boolean isDisposeNotifier = isAssignable( returnType, disposeNotifier );
3245          final boolean isTypeAnnotatedByComponentAnnotation =
3246            !isDisposeNotifier && isElementAnnotatedBy( returnElement, Constants.COMPONENT_CLASSNAME );
3247          final boolean isTypeAnnotatedActAsComponent =
3248            !isDisposeNotifier &&
3249            !isTypeAnnotatedByComponentAnnotation &&
3250            isElementAnnotatedBy( returnElement, Constants.ACT_AS_COMPONENT_CLASSNAME );
3251          if ( isDisposeNotifier || isTypeAnnotatedByComponentAnnotation || isTypeAnnotatedActAsComponent )
3252          {
3253            if ( !descriptor.isDependencyDefined( getter ) &&
3254                 !descriptor.isCascadeDisposeDefined( getter ) &&
3255                 ( isDisposeNotifier ||
3256                   isTypeAnnotatedActAsComponent ||
3257                   verifyReferencesToComponent( (TypeElement) returnElement ) ) &&
3258                 isUnmanagedComponentReferenceNotSuppressed( getter ) &&
3259                 ( observable.hasSetter() && isUnmanagedComponentReferenceNotSuppressed( observable.getSetter() ) ) )
3260            {
3261              final String label =
3262                isDisposeNotifier ? "an implementation of DisposeNotifier" :
3263                isTypeAnnotatedByComponentAnnotation ? "an Arez component" :
3264                "annotated with @ActAsComponent";
3265              final String message =
3266                "Method named '" + getter.getSimpleName().toString() + "' has a return type that is " + label +
3267                " but is not annotated with @" + Constants.CASCADE_DISPOSE_CLASSNAME + " or " +
3268                "@" + Constants.COMPONENT_DEPENDENCY_CLASSNAME + ". This scenario can cause errors. " +
3269                "Please annotate the method as appropriate or suppress the warning by annotating the method with " +
3270                "@SuppressWarnings( \"" + Constants.WARNING_UNMANAGED_COMPONENT_REFERENCE + "\" ) or " +
3271                "@SuppressArezWarnings( \"" + Constants.WARNING_UNMANAGED_COMPONENT_REFERENCE + "\" )";
3272              processingEnv.getMessager().printMessage( WARNING, message, getter );
3273            }
3274          }
3275        }
3276      }
3277    }
3278  }
3279
3280  private boolean verifyReferencesToComponent( @Nonnull final VariableElement field )
3281  {
3282    return verifyReferencesToComponent( (TypeElement) processingEnv.getTypeUtils().asElement( field.asType() ) );
3283  }
3284
3285  private boolean verifyReferencesToComponent( @Nonnull final TypeElement element )
3286  {
3287    assert SuperficialValidation.validateElement( processingEnv, element );
3288
3289    final String verifyReferencesToComponent =
3290      AnnotationsUtil.getEnumAnnotationParameter( element,
3291                                                  Constants.COMPONENT_CLASSNAME,
3292                                                  "verifyReferencesToComponent" );
3293    return switch ( verifyReferencesToComponent )
3294    {
3295      case "ENABLE" -> true;
3296      case "DISABLE" -> false;
3297      default -> isDisposableTrackableRequired( element );
3298    };
3299  }
3300
3301  private boolean isUnmanagedComponentReferenceNotSuppressed( @Nonnull final Element element )
3302  {
3303    return !ElementsUtil.isWarningSuppressed( element,
3304                                              Constants.WARNING_UNMANAGED_COMPONENT_REFERENCE,
3305                                              Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME );
3306  }
3307
3308  private boolean isTypeAnnotatedByActAsComponentAnnotation( @Nonnull final VariableElement field )
3309  {
3310    final Element element = processingEnv.getTypeUtils().asElement( field.asType() );
3311    return isElementAnnotatedBy( element, Constants.ACT_AS_COMPONENT_CLASSNAME );
3312  }
3313
3314  private boolean isTypeAnnotatedByComponentAnnotation( @Nonnull final VariableElement field )
3315  {
3316    final Element element = processingEnv.getTypeUtils().asElement( field.asType() );
3317    return isElementAnnotatedBy( element, Constants.COMPONENT_CLASSNAME );
3318  }
3319
3320  private boolean isElementAnnotatedBy( @Nullable final Element element, @Nonnull final String annotation )
3321  {
3322    return null != element &&
3323           SuperficialValidation.validateElement( processingEnv, element ) &&
3324           AnnotationsUtil.hasAnnotationOfType( element, annotation );
3325  }
3326
3327  private boolean isService( @Nonnull final TypeElement typeElement )
3328  {
3329    final String service =
3330      AnnotationsUtil.getEnumAnnotationParameter( typeElement, Constants.COMPONENT_CLASSNAME, "service" );
3331    return switch ( service )
3332    {
3333      case "ENABLE" -> true;
3334      case "DISABLE" -> false;
3335      default -> AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.STING_CONTRIBUTE_TO ) ||
3336                 AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.STING_TYPED ) ||
3337                 AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.STING_NAMED ) ||
3338                 AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.STING_EAGER );
3339    };
3340  }
3341
3342  private boolean isComponentObservableRequired( @Nonnull final AnnotationMirror arezComponent,
3343                                                 final boolean disposeOnDeactivate )
3344  {
3345    final VariableElement variableElement = getAnnotationParameter( arezComponent, "observable" );
3346    return switch ( variableElement.getSimpleName().toString() )
3347    {
3348      case "ENABLE" -> true;
3349      case "DISABLE" -> false;
3350      default -> disposeOnDeactivate;
3351    };
3352  }
3353
3354  private boolean isVerifyRequired( @Nonnull final AnnotationMirror arezComponent,
3355                                    @Nonnull final TypeElement typeElement )
3356  {
3357    final VariableElement parameter = getAnnotationParameter( arezComponent, "verify" );
3358    return switch ( parameter.getSimpleName().toString() )
3359    {
3360      case "ENABLE" -> true;
3361      case "DISABLE" -> false;
3362      default -> ElementsUtil.getMethods( typeElement, processingEnv.getElementUtils(), processingEnv.getTypeUtils() ).
3363        stream().anyMatch( this::hasReferenceAnnotations );
3364    };
3365  }
3366
3367  private boolean hasReferenceAnnotations( @Nonnull final Element method )
3368  {
3369    return AnnotationsUtil.hasAnnotationOfType( method, Constants.REFERENCE_CLASSNAME ) ||
3370           AnnotationsUtil.hasAnnotationOfType( method, Constants.REFERENCE_ID_CLASSNAME ) ||
3371           AnnotationsUtil.hasAnnotationOfType( method, Constants.INVERSE_CLASSNAME );
3372  }
3373
3374  private boolean isEqualsRequired( @Nonnull final AnnotationMirror arezComponent )
3375  {
3376    final VariableElement injectParameter = getAnnotationParameter( arezComponent, "requireEquals" );
3377    return "ENABLE".equals( injectParameter.getSimpleName().toString() );
3378  }
3379
3380  @Nullable
3381  private String getDefaultPriority( @Nonnull final AnnotationMirror arezComponent )
3382  {
3383    final AnnotationValue value =
3384      AnnotationsUtil.findAnnotationValueNoDefaults( arezComponent, "defaultPriority" );
3385    return null == value ? null : ( (VariableElement) value.getValue() ).getSimpleName().toString();
3386  }
3387
3388  private boolean isIdRequired( @Nonnull final AnnotationMirror arezComponent )
3389  {
3390    final VariableElement injectParameter = getAnnotationParameter( arezComponent, "requireId" );
3391    return !"DISABLE".equals( injectParameter.getSimpleName().toString() );
3392  }
3393
3394  private boolean hasInjectAnnotation( @Nonnull final Element method )
3395  {
3396    return AnnotationsUtil.hasAnnotationOfType( method, Constants.INJECT_CLASSNAME );
3397  }
3398
3399  @Nonnull
3400  private <T> T getAnnotationParameter( @Nonnull final AnnotationMirror annotation,
3401                                        @Nonnull final String parameterName )
3402  {
3403    return AnnotationsUtil.getAnnotationValueValue( annotation, parameterName );
3404  }
3405
3406  @Nonnull
3407  private TypeElement getDisposableTypeElement()
3408  {
3409    return getTypeElement( Constants.DISPOSABLE_CLASSNAME );
3410  }
3411
3412  private boolean isDisposableTrackableRequired( @Nonnull final TypeElement element )
3413  {
3414    final String disposeNotifier =
3415      AnnotationsUtil.getEnumAnnotationParameter( element, Constants.COMPONENT_CLASSNAME, "disposeNotifier" );
3416    return switch ( disposeNotifier )
3417    {
3418      case "ENABLE" -> true;
3419      case "DISABLE" -> false;
3420      default -> null == AnnotationsUtil.findAnnotationByType( element, Constants.COMPONENT_CLASSNAME ) ||
3421                 !isService( element );
3422    };
3423  }
3424
3425  @Nonnull
3426  private TypeElement getTypeElement( @Nonnull final String classname )
3427  {
3428    final TypeElement typeElement = findTypeElement( classname );
3429    assert null != typeElement;
3430    return typeElement;
3431  }
3432
3433  @Nullable
3434  private TypeElement findTypeElement( @Nonnull final String classname )
3435  {
3436    return processingEnv.getElementUtils().getTypeElement( classname );
3437  }
3438
3439  private boolean isAssignable( @Nonnull final TypeMirror type, @Nonnull final TypeElement typeElement )
3440  {
3441    return processingEnv.getTypeUtils().isAssignable( type, typeElement.asType() );
3442  }
3443
3444  private boolean isMethodAProtectedOverride( @Nonnull final TypeElement typeElement,
3445                                              @Nonnull final ExecutableElement method )
3446  {
3447    final ExecutableElement overriddenMethod = ElementsUtil.getOverriddenMethod( processingEnv, typeElement, method );
3448    return null != overriddenMethod && overriddenMethod.getModifiers().contains( Modifier.PROTECTED );
3449  }
3450
3451  private void mustBeStandardRefMethod( @Nonnull final ProcessingEnvironment processingEnv,
3452                                        @Nonnull final ComponentDescriptor descriptor,
3453                                        @Nonnull final ExecutableElement method,
3454                                        @Nonnull final String annotationClassname )
3455  {
3456    mustBeRefMethod( descriptor, method, annotationClassname );
3457    MemberChecks.mustNotHaveAnyParameters( annotationClassname, method );
3458    shouldBeInternalRefMethod( processingEnv, descriptor, method, annotationClassname );
3459  }
3460
3461  private void mustBeRefMethod( @Nonnull final ComponentDescriptor descriptor,
3462                                @Nonnull final ExecutableElement method,
3463                                @Nonnull final String annotationClassname )
3464  {
3465    MemberChecks.mustBeAbstract( annotationClassname, method );
3466    final TypeElement typeElement = descriptor.getElement();
3467    MemberChecks.mustNotBePackageAccessInDifferentPackage( typeElement,
3468                                                           Constants.COMPONENT_CLASSNAME,
3469                                                           annotationClassname,
3470                                                           method );
3471    MemberChecks.mustReturnAValue( annotationClassname, method );
3472    MemberChecks.mustNotThrowAnyExceptions( annotationClassname, method );
3473  }
3474
3475  private void mustBeHookHook( @Nonnull final TypeElement targetType,
3476                               @Nonnull final String annotationName,
3477                               @Nonnull final ExecutableElement method )
3478    throws ProcessorException
3479  {
3480    MemberChecks.mustNotBeAbstract( annotationName, method );
3481    MemberChecks.mustBeSubclassCallable( targetType, Constants.COMPONENT_CLASSNAME, annotationName, method );
3482    MemberChecks.mustNotReturnAnyValue( annotationName, method );
3483    MemberChecks.mustNotThrowAnyExceptions( annotationName, method );
3484  }
3485
3486  private void shouldBeInternalRefMethod( @Nonnull final ProcessingEnvironment processingEnv,
3487                                          @Nonnull final ComponentDescriptor descriptor,
3488                                          @Nonnull final ExecutableElement method,
3489                                          @Nonnull final String annotationClassname )
3490  {
3491    if ( MemberChecks.doesMethodNotOverrideInterfaceMethod( processingEnv, descriptor.getElement(), method ) )
3492    {
3493      MemberChecks.shouldNotBePublic( processingEnv,
3494                                      method,
3495                                      annotationClassname,
3496                                      Constants.WARNING_PUBLIC_REF_METHOD,
3497                                      Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME );
3498    }
3499  }
3500
3501  private void shouldBeInternalLifecycleMethod( @Nonnull final ProcessingEnvironment processingEnv,
3502                                                @Nonnull final ComponentDescriptor descriptor,
3503                                                @Nonnull final ExecutableElement method,
3504                                                @Nonnull final String annotationClassname )
3505  {
3506    if ( MemberChecks.doesMethodNotOverrideInterfaceMethod( processingEnv, descriptor.getElement(), method ) )
3507    {
3508      MemberChecks.shouldNotBePublic( processingEnv,
3509                                      method,
3510                                      annotationClassname,
3511                                      Constants.WARNING_PUBLIC_LIFECYCLE_METHOD,
3512                                      Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME );
3513    }
3514  }
3515
3516  private void shouldBeInternalHookMethod( @Nonnull final ProcessingEnvironment processingEnv,
3517                                           @Nonnull final ComponentDescriptor descriptor,
3518                                           @Nonnull final ExecutableElement method,
3519                                           @Nonnull final String annotationClassname )
3520  {
3521    if ( MemberChecks.doesMethodNotOverrideInterfaceMethod( processingEnv, descriptor.getElement(), method ) )
3522    {
3523      MemberChecks.shouldNotBePublic( processingEnv,
3524                                      method,
3525                                      annotationClassname,
3526                                      Constants.WARNING_PUBLIC_HOOK_METHOD,
3527                                      Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME );
3528    }
3529  }
3530
3531  @Nullable
3532  private String deriveName( @Nonnull final ExecutableElement method,
3533                             @Nonnull final Pattern pattern,
3534                             @Nonnull final String name )
3535    throws ProcessorException
3536  {
3537    if ( Constants.SENTINEL.equals( name ) )
3538    {
3539      final String methodName = method.getSimpleName().toString();
3540      final Matcher matcher = pattern.matcher( methodName );
3541      if ( matcher.find() )
3542      {
3543        final String candidate = matcher.group( 1 );
3544        return firstCharacterToLowerCase( candidate );
3545      }
3546      else
3547      {
3548        return null;
3549      }
3550    }
3551    else
3552    {
3553      return name;
3554    }
3555  }
3556
3557  @Nonnull
3558  private String firstCharacterToLowerCase( @Nonnull final String name )
3559  {
3560    return Character.toLowerCase( name.charAt( 0 ) ) + name.substring( 1 );
3561  }
3562}