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