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