1 /* <lambda>null2 * Copyright 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package androidx.appsearch.compiler 17 18 import androidx.appsearch.compiler.AnnotatedGetterOrField.ElementTypeCategory 19 import androidx.appsearch.compiler.IntrospectionHelper.MethodTypeAndElement 20 import java.util.Locale 21 import java.util.stream.Collectors 22 import javax.lang.model.element.Element 23 import javax.lang.model.element.ElementKind 24 import javax.lang.model.element.ExecutableElement 25 import javax.lang.model.element.Modifier 26 import javax.lang.model.element.TypeElement 27 import javax.lang.model.type.DeclaredType 28 import javax.lang.model.type.TypeKind 29 import javax.lang.model.type.TypeMirror 30 31 /** 32 * Info about how to construct a class annotated with `@Document`, aka the document class. 33 * 34 * This has two components: 35 * 1. A constructor/static [CreationMethod] that takes in **N** params, each corresponding to an 36 * [AnnotatedGetterOrField] and returns either the document class or a builder. 37 * 2. A set of **M** setters/fields on the object returned by the [CreationMethod]. 38 * 39 * Note: Fields only apply if [CreationMethod.returnsDocumentClass] since it is assumed that 40 * builders cannot have fields. When [CreationMethod.returnsBuilder], this only contains setters. 41 * 42 * **N + M** collectively encompass all of the annotated getters/fields in the document class. 43 * 44 * For example: 45 * <pre> 46 * @Document 47 * class DocumentClass { 48 * public DocumentClass(String id, String namespace, int someProp) {...} 49 * // ^^^^^^^^^^^^^ 50 * // Creation method 51 * 52 * @Document.Id 53 * public String getId() {...} 54 * 55 * @Document.Namespace 56 * public String getNamespace() {...} 57 * 58 * @Document.LongProperty 59 * public int getSomeProp() {...} 60 * 61 * @Document.StringProperty 62 * public String getOtherProp() {...} 63 * public void setOtherProp(String otherProp) {...} 64 * // ^^^^^^^^^^^^ 65 * // setter 66 * 67 * @Document.BooleanProperty 68 * public boolean mYetAnotherProp; 69 * // ^^^^^^^^^^^^^^^ 70 * // field 71 * } 72 * </pre> 73 */ 74 data class DocumentClassCreationInfo( 75 /** The creation method. */ 76 val creationMethod: CreationMethod, 77 78 /** 79 * Maps an annotated getter/field to the corresponding setter/field on the object returned by 80 * the [CreationMethod]. 81 */ 82 val settersAndFields: Map<AnnotatedGetterOrField, SetterOrField> 83 ) { 84 companion object { 85 /** Infers the [DocumentClassCreationInfo] for a specified document class. */ 86 @Throws(ProcessingException::class) 87 @JvmStatic 88 fun infer( 89 documentClass: TypeElement, 90 annotatedGettersAndFields: Set<AnnotatedGetterOrField>, 91 helper: IntrospectionHelper, 92 ): DocumentClassCreationInfo { 93 val builderProducer = BuilderProducer.tryCreate(documentClass, helper) 94 95 val settersAndFields = LinkedHashMap<AnnotatedGetterOrField, SetterOrField>() 96 val setterNotFoundErrors = mutableListOf<ProcessingException>() 97 for (getterOrField in annotatedGettersAndFields) { 98 if ( 99 builderProducer == null && 100 getterOrField.isField && 101 fieldCanBeSetDirectly(getterOrField.element) 102 ) { 103 // annotated field on the document class itself 104 settersAndFields[getterOrField] = SetterOrField(getterOrField.element) 105 } else { 106 // Annotated getter|annotated private field|must use builder pattern 107 try { 108 val targetClass = 109 if (builderProducer == null) { 110 documentClass 111 } else { 112 builderProducer.builderType.asElement() as TypeElement 113 } 114 val setter = findSetter(targetClass, getterOrField, helper) 115 settersAndFields[getterOrField] = SetterOrField(setter) 116 } catch (e: ProcessingException) { 117 setterNotFoundErrors.add(e) 118 } 119 } 120 } 121 122 val potentialCreationMethods = 123 extractPotentialCreationMethods( 124 documentClass, 125 annotatedGettersAndFields, 126 builderProducer, 127 helper 128 ) 129 130 // Start building the exception in case we don't find a suitable creation method 131 val remainingGettersAndFields = annotatedGettersAndFields - settersAndFields.keys 132 val exception = 133 ProcessingException( 134 ("Could not find a suitable %s for \"%s\" that covers properties: [%s]. " + 135 "See the warnings for more details.") 136 .format( 137 if (builderProducer == null) { 138 "constructor/factory method" 139 } else { 140 "builder producer" 141 }, 142 documentClass.qualifiedName, 143 getCommaSeparatedJvmNames(remainingGettersAndFields) 144 ), 145 documentClass 146 ) 147 exception.addWarnings(setterNotFoundErrors) 148 149 // Pick the first creation method that covers the annotated getters/fields that we don't 150 // already have setters/fields for 151 for (creationMethod in potentialCreationMethods) { 152 val missingParams = remainingGettersAndFields - creationMethod.paramAssociations 153 if (missingParams.isNotEmpty()) { 154 exception.addWarning( 155 ProcessingException( 156 ("Cannot use this %s to construct the class: \"%s\". " + 157 "No parameters for the properties: [%s]") 158 .format( 159 if (creationMethod.isConstructor) { 160 "constructor" 161 } else { 162 "creation method" 163 }, 164 documentClass.qualifiedName, 165 getCommaSeparatedJvmNames(missingParams) 166 ), 167 creationMethod.element 168 ) 169 ) 170 continue 171 } 172 // found one! 173 // This creation method may cover properties that we already have setters for. 174 // If so, forget those setters. 175 for (getterOrField in creationMethod.paramAssociations) { 176 settersAndFields.remove(getterOrField) 177 } 178 return DocumentClassCreationInfo(creationMethod, settersAndFields) 179 } 180 181 throw exception 182 } 183 184 /** 185 * Finds a setter corresponding to the getter/field within the specified class. 186 * 187 * @throws ProcessingException if no suitable setter was found within the specified class. 188 */ 189 @Throws(ProcessingException::class) 190 private fun findSetter( 191 clazz: TypeElement, 192 getterOrField: AnnotatedGetterOrField, 193 helper: IntrospectionHelper, 194 ): ExecutableElement { 195 val setterNames = getAcceptableSetterNames(getterOrField) 196 // Start building the exception in case we don't find a suitable setter 197 val setterSignatures = 198 setterNames 199 .stream() 200 .map { "[public] void $it(${getterOrField.jvmType})" } 201 .collect(Collectors.joining("|")) 202 val exception = 203 ProcessingException( 204 "Could not find any of the setter(s): $setterSignatures", 205 getterOrField.element 206 ) 207 208 val potentialSetters = 209 helper 210 .getAllMethods(clazz) 211 .stream() 212 .filter { method: ExecutableElement -> 213 setterNames.contains(method.simpleName.toString()) 214 } 215 .toList() 216 for (method in potentialSetters) { 217 if (method.modifiers.contains(Modifier.PRIVATE)) { 218 exception.addWarning( 219 ProcessingException("Setter cannot be used: private visibility", method) 220 ) 221 continue 222 } 223 if (method.modifiers.contains(Modifier.STATIC)) { 224 exception.addWarning( 225 ProcessingException("Setter cannot be used: static method", method) 226 ) 227 continue 228 } 229 if (method.parameters.size != 1) { 230 exception.addWarning( 231 ProcessingException( 232 ("Setter cannot be used: takes ${method.parameters.size} parameters " + 233 "instead of 1"), 234 method 235 ) 236 ) 237 continue 238 } 239 // found one! 240 return method 241 } 242 243 throw exception 244 } 245 246 private fun getAcceptableSetterNames(getterOrField: AnnotatedGetterOrField): Set<String> { 247 // String mField -> {field(String), setField(String)} 248 // String getProp() -> {prop(String), setProp(String)} 249 // List<String> getProps() -> {props(List), setProps(List), addProps(List)} 250 val setterNames = mutableSetOf<String>() 251 val normalizedName = getterOrField.normalizedName 252 setterNames.add(normalizedName) 253 val pascalCase = 254 normalizedName.substring(0, 1).uppercase(Locale.getDefault()) + 255 normalizedName.substring(1) 256 setterNames.add("set$pascalCase") 257 when (getterOrField.elementTypeCategory) { 258 ElementTypeCategory.SINGLE -> {} 259 ElementTypeCategory.COLLECTION, 260 ElementTypeCategory.ARRAY -> setterNames.add("add$pascalCase") 261 } 262 return setterNames 263 } 264 265 private fun fieldCanBeSetDirectly(field: Element): Boolean { 266 val modifiers = field.modifiers 267 return !modifiers.contains(Modifier.PRIVATE) && !modifiers.contains(Modifier.FINAL) 268 } 269 270 /** 271 * Extracts potential creation methods for the document class. 272 * 273 * Returns creation methods corresponding to the [BuilderProducer], when it is not null. 274 * 275 * @throws ProcessingException if no viable creation methods could be extracted. 276 */ 277 @Throws(ProcessingException::class) 278 private fun extractPotentialCreationMethods( 279 documentClass: TypeElement, 280 annotatedGettersAndFields: Set<AnnotatedGetterOrField>, 281 builderProducer: BuilderProducer?, 282 helper: IntrospectionHelper, 283 ): List<CreationMethod> { 284 val potentialMethods: List<ExecutableElement> = 285 if (builderProducer != null && builderProducer.isStaticMethod) { 286 listOf(builderProducer.element as ExecutableElement) 287 } else { 288 // Use the constructors & factory methods on the document class or builder class 289 // itself 290 val targetClass = 291 if (builderProducer == null) { 292 documentClass 293 } else { 294 builderProducer.element as TypeElement 295 } 296 targetClass.enclosedElements 297 .stream() 298 .filter { element: Element -> 299 element.kind == ElementKind.CONSTRUCTOR || 300 helper.isStaticFactoryMethod(element) 301 } 302 .map { element: Element -> element as ExecutableElement } 303 .toList() 304 } 305 306 // Start building an exception in case none of the candidates are suitable 307 val exception = 308 ProcessingException("Could not find a suitable creation method", documentClass) 309 310 val creationMethods = mutableListOf<CreationMethod>() 311 for (candidate in potentialMethods) { 312 try { 313 creationMethods.add( 314 CreationMethod.inferParamAssociationsAndCreate( 315 candidate, 316 annotatedGettersAndFields, 317 /* returnsDocumentClass= */ builderProducer == null 318 ) 319 ) 320 } catch (e: ProcessingException) { 321 exception.addWarning(e) 322 } 323 } 324 325 if (creationMethods.isEmpty()) { 326 throw exception 327 } 328 329 return creationMethods 330 } 331 332 private fun getCommaSeparatedJvmNames( 333 gettersAndFields: Collection<AnnotatedGetterOrField> 334 ): String { 335 return gettersAndFields 336 .stream() 337 .map(AnnotatedGetterOrField::jvmName) 338 .collect(Collectors.joining(", ")) 339 } 340 } 341 342 /** 343 * Represents a static method/nested class within a document class annotated with 344 * `@Document.BuilderProducer`. For example: 345 * <pre> 346 * @Document 347 * public class MyEntity { 348 * @Document.BuilderProducer 349 * public static Builder newBuilder(); 350 * 351 * // This class may directly be annotated with @Document.BuilderProducer instead 352 * public static class Builder {...} 353 * } 354 * </pre> 355 */ 356 private class BuilderProducer( 357 /** The static method/nested class annotated with `@Document.BuilderProducer`. */ 358 val element: Element, 359 360 /** The return type of the annotated method or the annotated builder class. */ 361 val builderType: DeclaredType, 362 ) { 363 companion object { 364 @Throws(ProcessingException::class) 365 fun tryCreate( 366 documentClass: TypeElement, 367 helper: IntrospectionHelper, 368 ): BuilderProducer? { 369 val annotatedElements: List<Element> = 370 documentClass.enclosedElements 371 .stream() 372 .filter(::isAnnotatedWithBuilderProducer) 373 .toList() 374 if (annotatedElements.isEmpty()) { 375 return null 376 } else if (annotatedElements.size > 1) { 377 throw ProcessingException("Found duplicated builder producer", documentClass) 378 } 379 380 val annotatedElement = annotatedElements[0] 381 requireBuilderProducerAccessible(annotatedElement) 382 // Since @Document.BuilderProducer is configured with 383 // @Target({ElementType.METHOD, ElementType.TYPE}), this should never throw in 384 // practice. 385 requireBuilderProducerIsMethodOrClass(annotatedElement) 386 387 val builderType: DeclaredType = 388 if (annotatedElement.kind == ElementKind.METHOD) { 389 val method = annotatedElement as ExecutableElement 390 requireIsDeclaredTypeWithBuildMethod( 391 method.returnType, 392 documentClass, 393 annotatedElement, 394 helper 395 ) 396 method.returnType as DeclaredType 397 } else { 398 // A class is annotated with @Document.BuilderProducer. Use its constructors 399 // as the creation methods. 400 val builderClass = annotatedElement as TypeElement 401 requireIsDeclaredTypeWithBuildMethod( 402 builderClass.asType(), 403 documentClass, 404 annotatedElement, 405 helper 406 ) 407 annotatedElement.asType() as DeclaredType 408 } 409 410 return BuilderProducer(annotatedElement, builderType) 411 } 412 413 private fun isAnnotatedWithBuilderProducer(element: Element): Boolean { 414 return !IntrospectionHelper.getAnnotations( 415 element, 416 IntrospectionHelper.BUILDER_PRODUCER_CLASS 417 ) 418 .isEmpty() 419 } 420 421 /** Makes sure the annotated element is a builder/class. */ 422 @Throws(ProcessingException::class) 423 private fun requireBuilderProducerIsMethodOrClass(annotatedElement: Element) { 424 if ( 425 annotatedElement.kind != ElementKind.METHOD && 426 annotatedElement.kind != ElementKind.CLASS 427 ) { 428 throw ProcessingException( 429 "Builder producer must be a method or a class", 430 annotatedElement 431 ) 432 } 433 } 434 435 /** Makes sure the annotated element is static and not private. */ 436 @Throws(ProcessingException::class) 437 private fun requireBuilderProducerAccessible(annotatedElement: Element) { 438 if (!annotatedElement.modifiers.contains(Modifier.STATIC)) { 439 throw ProcessingException("Builder producer must be static", annotatedElement) 440 } 441 if (annotatedElement.modifiers.contains(Modifier.PRIVATE)) { 442 throw ProcessingException( 443 "Builder producer cannot be private", 444 annotatedElement 445 ) 446 } 447 } 448 449 /** 450 * Makes sure the builder type is a [DeclaredType] with a non-private & non-static 451 * method of the form `DocumentClass build()`. 452 * 453 * @param annotatedElement The method/class annotated with `@Document.BuilderProducer`. 454 * @throws ProcessingException on the annotated element if the conditions are not met. 455 */ 456 @Throws(ProcessingException::class) 457 private fun requireIsDeclaredTypeWithBuildMethod( 458 builderType: TypeMirror, 459 documentClass: TypeElement, 460 annotatedElement: Element, 461 helper: IntrospectionHelper, 462 ) { 463 val exception = 464 ProcessingException( 465 ("Invalid builder producer: $builderType does not have a method " + 466 "$documentClass build()"), 467 annotatedElement 468 ) 469 if (builderType.kind != TypeKind.DECLARED) { 470 throw exception 471 } 472 val hasBuildMethod = 473 helper.getAllMethods(builderType as DeclaredType).anyMatch { 474 method: MethodTypeAndElement -> 475 method.element.simpleName.contentEquals("build") && 476 !method.element.modifiers.contains(Modifier.STATIC) && 477 !method.element.modifiers.contains(Modifier.PRIVATE) && 478 helper.isReturnTypeMatching(method.type, documentClass.asType()) && 479 method.type.parameterTypes.isEmpty() 480 } 481 if (!hasBuildMethod) { 482 throw exception 483 } 484 } 485 } 486 487 val isStaticMethod: Boolean 488 get() = element.kind == ElementKind.METHOD 489 } 490 } 491