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