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.annotationwrapper
17 
18 import androidx.appsearch.compiler.ProcessingException
19 import com.squareup.javapoet.ClassName
20 import com.squareup.javapoet.TypeName
21 import javax.lang.model.element.ElementKind
22 import javax.lang.model.element.ExecutableElement
23 import javax.lang.model.element.Modifier
24 import javax.lang.model.element.TypeElement
25 import javax.lang.model.type.TypeMirror
26 
27 /**
28  * Represents a class that can convert between some custom type and a property's actual type.
29  *
30  * @see androidx.appsearch.app.StringSerializer
31  * @see androidx.appsearch.app.LongSerializer
32  */
33 data class SerializerClass(
34     /** The kind of serializer. */
35     val kind: Kind,
36 
37     /** The serializer class element. */
38     val element: TypeElement,
39 
40     /** The zero-param constructor. Present on every serializer class. */
41     val defaultConstructor: ExecutableElement,
42 
43     /** The custom type that can be serialized using the serializer class. */
44     val customType: TypeMirror,
45 ) {
46     enum class Kind(
47         /**
48          * The actual type of the corresponding property within a `GenericDocument`.
49          *
50          * For example, a [.STRING_SERIALIZER] may only be used with a `@Document.StringProperty`
51          * which, in turn, boils down to a [String] within a `GenericDocument`.
52          */
53         // TypeName is an immutable 3P type
54         val actualTypeInGenericDoc: TypeName
55     ) {
56         STRING_SERIALIZER(actualTypeInGenericDoc = ClassName.get(String::class.java)),
57         LONG_SERIALIZER(actualTypeInGenericDoc = TypeName.LONG),
58     }
59 
60     companion object {
61         /**
62          * Creates a serializer class given its [TypeElement].
63          *
64          * @throws ProcessingException If the `clazz` does not have a zero-param constructor.
65          */
66         @Throws(ProcessingException::class)
createnull67         fun create(clazz: TypeElement, kind: Kind): SerializerClass {
68             val deserializeMethod: ExecutableElement = findDeserializeMethod(clazz, kind)
69             return SerializerClass(
70                 kind = kind,
71                 element = clazz,
72                 defaultConstructor = findDefaultConstructor(clazz),
73                 customType = deserializeMethod.returnType,
74             )
75         }
76 
77         /**
78          * Returns the zero-param constructor in the `clazz`.
79          *
80          * @throws ProcessingException If no such constructor exists or it's private.
81          */
82         @Throws(ProcessingException::class)
findDefaultConstructornull83         private fun findDefaultConstructor(clazz: TypeElement): ExecutableElement {
84             val constructor: ExecutableElement =
85                 clazz.enclosedElements
86                     .stream()
87                     .filter { it.kind == ElementKind.CONSTRUCTOR }
88                     .map { it as ExecutableElement }
89                     .filter { it.parameters.isEmpty() }
90                     .findFirst()
91                     .orElseThrow<ProcessingException> {
92                         ProcessingException(
93                             "Serializer ${clazz.qualifiedName} must have a zero-param " +
94                                 "constructor",
95                             clazz
96                         )
97                     }
98             if (constructor.modifiers.contains(Modifier.PRIVATE)) {
99                 throw ProcessingException(
100                     "The zero-param constructor of serializer ${clazz.qualifiedName} must not " +
101                         "be private",
102                     constructor
103                 )
104             }
105             return constructor
106         }
107 
108         /** Returns the `T deserialize(PropertyType)` method. */
findDeserializeMethodnull109         private fun findDeserializeMethod(clazz: TypeElement, kind: Kind): ExecutableElement {
110             return clazz.enclosedElements
111                 .stream()
112                 .filter { it.kind == ElementKind.METHOD }
113                 .map { it as ExecutableElement }
114                 // The type-system enforces there is one method satisfying these constraints
115                 .filter {
116                     it.simpleName.contentEquals("deserialize") &&
117                         !it.modifiers.contains(Modifier.STATIC) &&
118                         // Direct equality check with the param's type should be sufficient.
119                         // Don't need to allow for subtypes because mActualTypeInGenericDoc can
120                         // only be a primitive type or String which is a final class.
121                         hasSingleParamOfExactType(it, kind.actualTypeInGenericDoc)
122                 }
123                 .findFirst()
124                 // Should never throw because param type is enforced by the type-system
125                 .get()
126         }
127 
hasSingleParamOfExactTypenull128         private fun hasSingleParamOfExactType(
129             method: ExecutableElement,
130             expectedType: TypeName
131         ): Boolean {
132             if (method.parameters.size != 1) {
133                 return false
134             }
135             val firstParamType = TypeName.get(method.parameters.first().asType())
136             return firstParamType == expectedType
137         }
138     }
139 }
140