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