001package arez.persist.processor; 002 003import java.io.IOException; 004import java.util.ArrayList; 005import java.util.Collection; 006import java.util.HashMap; 007import java.util.List; 008import java.util.Map; 009import java.util.Set; 010import java.util.regex.Matcher; 011import java.util.regex.Pattern; 012import javax.annotation.Nonnull; 013import javax.annotation.Nullable; 014import javax.annotation.processing.RoundEnvironment; 015import javax.annotation.processing.SupportedAnnotationTypes; 016import javax.annotation.processing.SupportedOptions; 017import javax.annotation.processing.SupportedSourceVersion; 018import javax.lang.model.SourceVersion; 019import javax.lang.model.element.AnnotationMirror; 020import javax.lang.model.element.Element; 021import javax.lang.model.element.ElementKind; 022import javax.lang.model.element.ExecutableElement; 023import javax.lang.model.element.TypeElement; 024import javax.lang.model.type.TypeKind; 025import javax.lang.model.type.TypeMirror; 026import javax.tools.Diagnostic; 027import org.realityforge.proton.AbstractStandardProcessor; 028import org.realityforge.proton.AnnotationsUtil; 029import org.realityforge.proton.DeferredElementSet; 030import org.realityforge.proton.ElementsUtil; 031import org.realityforge.proton.GeneratorUtil; 032import org.realityforge.proton.MemberChecks; 033import org.realityforge.proton.ProcessorException; 034import org.realityforge.proton.StopWatch; 035 036/** 037 * Annotation processor that analyzes Arez annotated source and generates models from the annotations. 038 */ 039@SupportedAnnotationTypes( { Constants.PERSIST_TYPE_CLASSNAME, 040 Constants.PERSIST_ID_CLASSNAME, 041 Constants.PERSIST_CLASSNAME } ) 042@SupportedSourceVersion( SourceVersion.RELEASE_17 ) 043@SupportedOptions( { "arez.persist.defer.unresolved", 044 "arez.persist.defer.errors", 045 "arez.persist.debug", 046 "arez.persist.profile", 047 "arez.persist.verbose_out_of_round" } ) 048public final class ArezPersistProcessor 049 extends AbstractStandardProcessor 050{ 051 @Nonnull 052 private static final Pattern GETTER_PATTERN = Pattern.compile( "^get([A-Z].*)$" ); 053 @Nonnull 054 private static final Pattern ISSER_PATTERN = Pattern.compile( "^is([A-Z].*)$" ); 055 /** 056 * Sentinel value indicating the default value. 057 */ 058 @Nonnull 059 private static final String DEFAULT_SENTINEL = "<default>"; 060 @Nonnull 061 private final DeferredElementSet _deferredTypes = new DeferredElementSet(); 062 @Nonnull 063 private final StopWatch _analyzePersistTypeStopWatch = new StopWatch( "Analyze PersistType" ); 064 @Nonnull 065 private final StopWatch _analyzePersistIdStopWatch = new StopWatch( "Analyze PersistId" ); 066 067 @Override 068 @Nonnull 069 protected String getIssueTrackerURL() 070 { 071 return "https://github.com/arez/arez/issues"; 072 } 073 074 @Nonnull 075 @Override 076 protected String getOptionPrefix() 077 { 078 return "arez.persist"; 079 } 080 081 @Override 082 protected void collectStopWatches( @Nonnull final Collection<StopWatch> stopWatches ) 083 { 084 stopWatches.add( _analyzePersistTypeStopWatch ); 085 stopWatches.add( _analyzePersistIdStopWatch ); 086 } 087 088 @Override 089 public boolean process( @Nonnull final Set<? extends TypeElement> annotations, @Nonnull final RoundEnvironment env ) 090 { 091 debugAnnotationProcessingRootElements( env ); 092 collectRootTypeNames( env ); 093 // Validate persist first so we don't have to perform validation inside type processing 094 // The framework assumes that Arez is also running the annotation processor and validating 095 // the shape of the same methods as they are marked as @Observable so this annotation processor 096 // performs minimal validation. This can result in this annotation processor producing bad code 097 // and the arez annotation processor failing later in the round. The expectation is that we will 098 // need to get the arez annotation processor passing and once it does then the code generated by 099 // this annotation processor will start to work. 100 annotations 101 .stream() 102 .filter( a -> a.getQualifiedName().toString().equals( Constants.PERSIST_CLASSNAME ) ) 103 .findAny() 104 .ifPresent( a -> verifyPersistElements( env, env.getElementsAnnotatedWith( a ) ) ); 105 106 annotations 107 .stream() 108 .filter( a -> a.getQualifiedName().toString().equals( Constants.PERSIST_ID_CLASSNAME ) ) 109 .findAny() 110 .ifPresent( a -> verifyPersistIdElements( env, env.getElementsAnnotatedWith( a ) ) ); 111 112 processTypeElements( annotations, 113 env, 114 Constants.PERSIST_TYPE_CLASSNAME, 115 _deferredTypes, 116 _analyzePersistTypeStopWatch.getName(), 117 this::process, 118 _analyzePersistTypeStopWatch ); 119 120 errorIfProcessingOverAndInvalidTypesDetected( env ); 121 clearRootTypeNamesIfProcessingOver( env ); 122 return true; 123 } 124 125 private void process( @Nonnull final TypeElement element ) 126 throws Exception 127 { 128 if ( !AnnotationsUtil.hasAnnotationOfType( element, Constants.AREZ_COMPONENT_CLASSNAME ) ) 129 { 130 throw new ProcessorException( MemberChecks.must( Constants.PERSIST_TYPE_CLASSNAME, 131 "be present on a type annotated with the " + 132 MemberChecks.toSimpleName( Constants.AREZ_COMPONENT_CLASSNAME ) + 133 " annotation" ), 134 element ); 135 } 136 137 final AnnotationMirror annotation = 138 AnnotationsUtil.getAnnotationByType( element, Constants.PERSIST_TYPE_CLASSNAME ); 139 final String name = extractPersistTypeName( element, annotation ); 140 final boolean persistOnDispose = AnnotationsUtil.getAnnotationValueValue( annotation, "persistOnDispose" ); 141 final String defaultStore = extractDefaultStore( element, annotation ); 142 final List<ExecutableElement> methods = 143 ElementsUtil.getMethods( element, processingEnv.getElementUtils(), processingEnv.getTypeUtils() ); 144 ExecutableElement idMethod = null; 145 final Map<String, PropertyDescriptor> properties = new HashMap<>(); 146 for ( final ExecutableElement method : methods ) 147 { 148 final AnnotationMirror persistAnnotation = 149 AnnotationsUtil.findAnnotationByType( method, Constants.PERSIST_CLASSNAME ); 150 final AnnotationMirror persistIdAnnotation = 151 AnnotationsUtil.findAnnotationByType( method, Constants.PERSIST_ID_CLASSNAME ); 152 if ( null != persistAnnotation && null != persistIdAnnotation ) 153 { 154 throw new ProcessorException( MemberChecks.mustNot( Constants.PERSIST_ID_CLASSNAME, 155 "also be annotated with the " + 156 MemberChecks.toSimpleName( Constants.PERSIST_CLASSNAME ) + 157 " annotation" ), 158 element ); 159 } 160 else if ( null != persistAnnotation ) 161 { 162 processPersistAnnotation( element, method, persistAnnotation, methods, defaultStore, properties ); 163 } 164 else if ( null != persistIdAnnotation ) 165 { 166 // These validations have already been performed by an earlier step in this annotation processor 167 // if and only if the type declaring the @PersistId is also compiled in this compilation round. 168 // If the type that encloses @PersistId annotated method is part of a library or compiled earlier 169 // we re-run the checks just in case to avoid scenarios where weird behaviour slips through in code 170 // generation step. 171 validatePersisIdMethod( method ); 172 MemberChecks.mustNotBePackageAccessInDifferentPackage( element, 173 Constants.PERSIST_CLASSNAME, 174 Constants.PERSIST_ID_CLASSNAME, 175 method ); 176 if ( null != idMethod ) 177 { 178 throw new ProcessorException( MemberChecks.mustNot( Constants.PERSIST_ID_CLASSNAME, 179 "be present multiple times within a " + 180 MemberChecks.toSimpleName( Constants.PERSIST_TYPE_CLASSNAME ) + 181 " annotated type but another method annotated with " + 182 MemberChecks.toSimpleName( Constants.PERSIST_ID_CLASSNAME ) + 183 " exists and is named " + 184 idMethod.getSimpleName() ), 185 element ); 186 } 187 idMethod = method; 188 } 189 } 190 191 if ( properties.isEmpty() ) 192 { 193 throw new ProcessorException( MemberChecks.must( Constants.PERSIST_TYPE_CLASSNAME, 194 "contain one or more " + 195 MemberChecks.toSimpleName( Constants.OBSERVABLE_CLASSNAME ) + 196 " properties annotated with " + 197 MemberChecks.toSimpleName( Constants.PERSIST_CLASSNAME ) ), 198 element ); 199 } 200 201 emitSidecar( new TypeDescriptor( name, 202 persistOnDispose, 203 element, 204 idMethod, 205 new ArrayList<>( properties.values() ) ) ); 206 } 207 208 private void processPersistAnnotation( @Nonnull final TypeElement element, 209 @Nonnull final ExecutableElement method, 210 @Nonnull final AnnotationMirror persistAnnotation, 211 @Nonnull final List<ExecutableElement> methods, 212 @Nonnull final String defaultStore, 213 @Nonnull final Map<String, PropertyDescriptor> properties ) 214 { 215 final TypeMirror returnType = method.getReturnType(); 216 if ( TypeKind.VOID == returnType.getKind() ) 217 { 218 throw new ProcessorException( MemberChecks.must( Constants.PERSIST_CLASSNAME, 219 "be present on the accessor method of the " + 220 MemberChecks.toSimpleName( Constants.OBSERVABLE_CLASSNAME ) + 221 " property" ), 222 method ); 223 } 224 225 final String persistName = extractPersistName( method, persistAnnotation ); 226 final String persistStore = extractStore( element, method, persistAnnotation, defaultStore ); 227 final String setterName = extractSetterName( persistAnnotation, persistName ); 228 229 final ExecutableElement setter = methods 230 .stream() 231 .filter( m -> TypeKind.VOID == m.getReturnType().getKind() && 232 setterName.equals( m.getSimpleName().toString() ) && 233 1 == m.getParameters().size() && 234 processingEnv.getTypeUtils().isSameType( m.getParameters().get( 0 ).asType(), returnType ) ) 235 .findAny() 236 .orElse( null ); 237 if ( null == setter ) 238 { 239 throw new ProcessorException( MemberChecks.must( Constants.PERSIST_CLASSNAME, 240 "be paired with a setter named " + setterName ), 241 method ); 242 } 243 244 final PropertyDescriptor existing = properties.get( persistName ); 245 if ( null != existing ) 246 { 247 throw new ProcessorException( MemberChecks.must( Constants.PERSIST_CLASSNAME, 248 "has the same name '" + persistName + 249 "' as another persistent property declared by the name. " + 250 "The other property is accessed by the method named " + 251 existing.getGetter().getSimpleName() ), 252 method ); 253 } 254 else 255 { 256 properties.put( persistName, new PropertyDescriptor( persistName, persistStore, method, setter ) ); 257 } 258 } 259 260 @Nonnull 261 private String extractSetterName( @Nonnull final AnnotationMirror annotation, @Nonnull final String persistName ) 262 { 263 final String declaredValue = AnnotationsUtil.getAnnotationValueValue( annotation, "setterName" ); 264 return "<default>".equals( declaredValue ) ? "set" + firstCharacterToUpperCase( persistName ) : declaredValue; 265 } 266 267 @Nonnull 268 private String extractPersistTypeName( @Nonnull final TypeElement element, 269 @Nonnull final AnnotationMirror annotation ) 270 { 271 final String declaredValue = AnnotationsUtil.getAnnotationValueValue( annotation, "name" ); 272 if ( DEFAULT_SENTINEL.equals( declaredValue ) ) 273 { 274 return element.getSimpleName().toString(); 275 } 276 else if ( SourceVersion.isIdentifier( declaredValue ) ) 277 { 278 return declaredValue; 279 } 280 else 281 { 282 throw new ProcessorException( MemberChecks.mustNot( Constants.PERSIST_TYPE_CLASSNAME, 283 "specify a name parameter that is not a valid java identifier" ), 284 element, 285 annotation ); 286 } 287 } 288 289 @Nonnull 290 private String extractDefaultStore( @Nonnull final TypeElement element, @Nonnull final AnnotationMirror annotation ) 291 { 292 final String defaultStore = AnnotationsUtil.getAnnotationValueValue( annotation, "defaultStore" ); 293 if ( !SourceVersion.isIdentifier( defaultStore ) ) 294 { 295 throw new ProcessorException( MemberChecks.mustNot( Constants.PERSIST_TYPE_CLASSNAME, 296 "specify a defaultStore parameter that is not a valid java identifier" ), 297 element, 298 annotation ); 299 } 300 return defaultStore; 301 } 302 303 @Nonnull 304 private String extractPersistName( @Nonnull final ExecutableElement method, 305 @Nonnull final AnnotationMirror annotation ) 306 { 307 final String declaredValue = AnnotationsUtil.getAnnotationValueValue( annotation, "name" ); 308 final String value = getPropertyAccessorName( method, declaredValue ); 309 if ( SourceVersion.isIdentifier( value ) ) 310 { 311 return value; 312 } 313 else 314 { 315 throw new ProcessorException( MemberChecks.mustNot( Constants.PERSIST_CLASSNAME, 316 "specify a name parameter that is not a valid java identifier" ), 317 method, 318 annotation ); 319 } 320 } 321 322 @Nonnull 323 private String extractStore( @Nonnull final TypeElement typeElement, 324 @Nonnull final ExecutableElement element, 325 @Nonnull final AnnotationMirror annotation, 326 @Nonnull final String defaultStore ) 327 { 328 final String store = AnnotationsUtil.getAnnotationValueValue( annotation, "store" ); 329 if ( DEFAULT_SENTINEL.equals( store ) ) 330 { 331 return defaultStore; 332 } 333 else if ( SourceVersion.isIdentifier( store ) ) 334 { 335 if ( defaultStore.equals( store ) && 336 typeElement == element.getEnclosingElement() && 337 ElementsUtil.isWarningNotSuppressed( element, Constants.WARNING_UNNECESSARY_STORE ) ) 338 { 339 final String message = 340 MemberChecks.shouldNot( Constants.PERSIST_CLASSNAME, 341 "specify the store parameter when it is the same as the defaultStore " + 342 "parameter in the specified by the " + 343 MemberChecks.toSimpleName( Constants.PERSIST_TYPE_CLASSNAME ) + 344 " annotation on the enclosing type. " + 345 MemberChecks.suppressedBy( Constants.WARNING_UNNECESSARY_STORE ) ); 346 processingEnv.getMessager().printMessage( Diagnostic.Kind.WARNING, message, element ); 347 } 348 return store; 349 } 350 else 351 352 { 353 throw new ProcessorException( MemberChecks.mustNot( Constants.PERSIST_CLASSNAME, 354 "specify a store parameter that is not a valid java identifier" ), 355 element, 356 annotation ); 357 } 358 } 359 360 private void verifyPersistElements( @Nonnull final RoundEnvironment env, 361 @Nonnull final Set<? extends Element> elements ) 362 { 363 for ( final Element element : elements ) 364 { 365 assert ElementKind.METHOD == element.getKind(); 366 if ( !AnnotationsUtil.hasAnnotationOfType( element, Constants.OBSERVABLE_CLASSNAME ) ) 367 { 368 reportError( env, 369 MemberChecks.must( Constants.PERSIST_CLASSNAME, 370 "be also be annotated with the " + Constants.OBSERVABLE_CLASSNAME + 371 " annotation" ), 372 element ); 373 } 374 } 375 } 376 377 private void verifyPersistIdElements( @Nonnull final RoundEnvironment env, 378 @Nonnull final Set<? extends Element> elements ) 379 { 380 for ( final Element element : elements ) 381 { 382 performAction( env, 383 _analyzePersistIdStopWatch.getName(), e -> verifyPersistIdElement( env, e ), element, 384 _analyzePersistIdStopWatch ); 385 } 386 } 387 388 private void verifyPersistIdElement( @Nonnull final RoundEnvironment env, @Nonnull final Element element ) 389 { 390 assert ElementKind.METHOD == element.getKind(); 391 final Element type = element.getEnclosingElement(); 392 if ( !AnnotationsUtil.hasAnnotationOfType( type, Constants.AREZ_COMPONENT_CLASSNAME ) && 393 !AnnotationsUtil.hasAnnotationOfType( type, Constants.AREZ_COMPONENT_LIKE_CLASSNAME ) ) 394 { 395 reportError( env, 396 MemberChecks.must( Constants.PERSIST_ID_CLASSNAME, 397 "be enclosed within a type annotated by either the " + 398 Constants.AREZ_COMPONENT_CLASSNAME + " annotation or the " + 399 Constants.AREZ_COMPONENT_LIKE_CLASSNAME + " annotation" ), 400 element ); 401 } 402 403 validatePersisIdMethod( (ExecutableElement) element ); 404 } 405 406 private void validatePersisIdMethod( @Nonnull final ExecutableElement method ) 407 { 408 MemberChecks.mustNotBeAbstract( Constants.PERSIST_ID_CLASSNAME, method ); 409 MemberChecks.mustNotThrowAnyExceptions( Constants.PERSIST_ID_CLASSNAME, method ); 410 MemberChecks.mustNotHaveAnyTypeParameters( Constants.PERSIST_ID_CLASSNAME, method ); 411 MemberChecks.mustNotHaveAnyParameters( Constants.PERSIST_ID_CLASSNAME, method ); 412 MemberChecks.mustReturnAValue( Constants.PERSIST_ID_CLASSNAME, method ); 413 MemberChecks.mustNotBeStatic( Constants.PERSIST_ID_CLASSNAME, method ); 414 MemberChecks.mustNotBePrivate( Constants.PERSIST_ID_CLASSNAME, method ); 415 MemberChecks.mustNotBeProtected( Constants.PERSIST_ID_CLASSNAME, method ); 416 } 417 418 private void emitSidecar( @Nonnull final TypeDescriptor type ) 419 throws IOException 420 { 421 final String packageName = GeneratorUtil.getQualifiedPackageName( type.getElement() ); 422 emitTypeSpec( packageName, SidecarGenerator.buildType( processingEnv, type ) ); 423 } 424 425 @SuppressWarnings( "SameParameterValue" ) 426 @Nonnull 427 private String getPropertyAccessorName( @Nonnull final ExecutableElement method, @Nonnull final String specifiedName ) 428 throws ProcessorException 429 { 430 String name = deriveName( method, GETTER_PATTERN, specifiedName ); 431 if ( null != name ) 432 { 433 return name; 434 } 435 if ( method.getReturnType().getKind() == TypeKind.BOOLEAN ) 436 { 437 name = deriveName( method, ISSER_PATTERN, specifiedName ); 438 if ( null != name ) 439 { 440 return name; 441 } 442 } 443 return method.getSimpleName().toString(); 444 } 445 446 @Nullable 447 private String deriveName( @Nonnull final ExecutableElement method, 448 @Nonnull final Pattern pattern, 449 @Nonnull final String name ) 450 throws ProcessorException 451 { 452 if ( DEFAULT_SENTINEL.equals( name ) ) 453 { 454 final String methodName = method.getSimpleName().toString(); 455 final Matcher matcher = pattern.matcher( methodName ); 456 if ( matcher.find() ) 457 { 458 final String candidate = matcher.group( 1 ); 459 return firstCharacterToLowerCase( candidate ); 460 } 461 else 462 { 463 return null; 464 } 465 } 466 else 467 { 468 return name; 469 } 470 } 471 472 @Nonnull 473 private String firstCharacterToLowerCase( @Nonnull final String name ) 474 { 475 return Character.toLowerCase( name.charAt( 0 ) ) + name.substring( 1 ); 476 } 477 478 @Nonnull 479 private String firstCharacterToUpperCase( @Nonnull final String name ) 480 { 481 return Character.toUpperCase( name.charAt( 0 ) ) + name.substring( 1 ); 482 } 483}