1 /*
2  * 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 javax.lang.model.element.ElementKind
19 import javax.lang.model.element.ExecutableElement
20 import javax.lang.model.element.Modifier
21 import javax.lang.model.type.DeclaredType
22 
23 /**
24  * A constructor or static method used to create a class annotated with `@Document` aka document
25  * class.
26  *
27  * Takes in N input params, each corresponding to a value for an [AnnotatedGetterOrField].
28  *
29  * Moreover, may return the document class itself or a builder. All of the following are examples of
30  * valid creation methods:
31  * <pre>
32  * @Document
33  * class MyEntity {
34  *     static MyEntity create(String id, String namespace, int someProp);
35  * //                  ^^^^^^
36  *
37  *     MyEntity() {...}
38  * //  ^^^^^^^^
39  *
40  *     MyEntity(String id, String namespace, int someProp) {...}
41  * //  ^^^^^^^^
42  *
43  *     @Document.BuilderProducer
44  *     static Builder newBuilder() {...}
45  * //                 ^^^^^^^^^^
46  *
47  *     @Document.BuilderProducer
48  *     static class Builder {
49  *         Builder() {...}
50  * //      ^^^^^^^
51  *
52  *         Builder(String id, String namespace, int someProp) {...}
53  * //      ^^^^^^^
54  *     }
55  * }
56  * </pre>
57  */
58 data class CreationMethod(
59     /** The constructor/static method element. */
60     val element: ExecutableElement,
61 
62     /** The [AnnotatedGetterOrField]s that each input param corresponds to (order sensitive). */
63     val paramAssociations: List<AnnotatedGetterOrField>,
64 
65     /** Whether the creation method returns the document class itself instead of a builder. */
66     val returnsDocumentClass: Boolean,
67 ) {
68     companion object {
69         /**
70          * Infers which annotated getter/field each param corresponds to and creates a
71          * [CreationMethod].
72          *
73          * @param method The creation method element.
74          * @param gettersAndFields The annotated getters/fields of the document class.
75          * @param returnsDocumentClass Whether the `method` returns the document class itself. If
76          *   not, it is assumed that it returns a builder for the document class.
77          * @throws ProcessingException If the method is not invocable or the association for a param
78          *   could not be deduced.
79          */
80         @Throws(ProcessingException::class)
inferParamAssociationsAndCreatenull81         fun inferParamAssociationsAndCreate(
82             method: ExecutableElement,
83             gettersAndFields: Collection<AnnotatedGetterOrField>,
84             returnsDocumentClass: Boolean
85         ): CreationMethod {
86             if (method.modifiers.contains(Modifier.PRIVATE)) {
87                 throw ProcessingException(
88                     ("Method cannot be used to create a " +
89                         (if (returnsDocumentClass) "document class" else "builder") +
90                         ": private visibility"),
91                     method
92                 )
93             }
94 
95             if (
96                 method.kind == ElementKind.CONSTRUCTOR &&
97                     method.enclosingElement.modifiers.contains(Modifier.ABSTRACT)
98             ) {
99                 throw ProcessingException(
100                     ("Method cannot be used to create a " +
101                         (if (returnsDocumentClass) "document class" else "builder") +
102                         ": abstract constructor"),
103                     method
104                 )
105             }
106 
107             val normalizedNameToGetterOrField = mutableMapOf<String, AnnotatedGetterOrField>()
108             for (getterOrField in gettersAndFields) {
109                 normalizedNameToGetterOrField[getterOrField.normalizedName] = getterOrField
110             }
111 
112             val paramAssociations = mutableListOf<AnnotatedGetterOrField>()
113             for (param in method.parameters) {
114                 val paramName = param.simpleName.toString()
115                 val correspondingGetterOrField: AnnotatedGetterOrField =
116                     normalizedNameToGetterOrField[paramName]
117                         ?: throw ProcessingException(
118                             "Parameter \"$paramName\" is not an AppSearch parameter; " +
119                                 "don't know how to supply it.",
120                             method
121                         )
122                 paramAssociations.add(correspondingGetterOrField)
123             }
124 
125             return CreationMethod(method, paramAssociations, returnsDocumentClass)
126         }
127     }
128 
129     /** The enclosing class that the constructor/static method is a part of. */
130     val enclosingClass: DeclaredType
131         get() = element.enclosingElement.asType() as DeclaredType
132 
133     /** The static method's return type/constructor's enclosing class. */
134     val returnType: DeclaredType
135         get() =
136             if (isConstructor) {
137                 element.enclosingElement.asType() as DeclaredType
138             } else {
139                 element.returnType as DeclaredType
140             }
141 
142     /** The static method/constructor element's name. */
143     val jvmName: String
144         get() = element.simpleName.toString()
145 
146     /** Whether the creation method is a constructor. */
147     val isConstructor: Boolean
148         get() = element.kind == ElementKind.CONSTRUCTOR
149 
150     /** Whether the creation method returns a builder instead of the document class itself. */
151     val returnsBuilder: Boolean
152         get() = !returnsDocumentClass
153 }
154