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.annotationwrapper.DataPropertyAnnotation
19 import androidx.appsearch.compiler.annotationwrapper.DataPropertyAnnotation.Companion.tryParse
20 import androidx.appsearch.compiler.annotationwrapper.LongPropertyAnnotation
21 import androidx.appsearch.compiler.annotationwrapper.MetadataPropertyAnnotation
22 import androidx.appsearch.compiler.annotationwrapper.PropertyAnnotation
23 import androidx.appsearch.compiler.annotationwrapper.SerializerClass
24 import androidx.appsearch.compiler.annotationwrapper.StringPropertyAnnotation
25 import java.util.Locale
26 import java.util.stream.Collectors
27 import javax.annotation.processing.ProcessingEnvironment
28 import javax.lang.model.element.AnnotationMirror
29 import javax.lang.model.element.Element
30 import javax.lang.model.element.ElementKind
31 import javax.lang.model.element.ExecutableElement
32 import javax.lang.model.type.ArrayType
33 import javax.lang.model.type.DeclaredType
34 import javax.lang.model.type.TypeKind
35 import javax.lang.model.type.TypeMirror
36 
37 /**
38  * A getter or field annotated with a [PropertyAnnotation] annotation. For example,
39  * <pre>
40  * @Document("MyEntity")
41  * public final class Entity {
42  *     @Document.Id
43  *     public String mId;
44  * //                ^^^
45  *
46  * // OR
47  *
48  *     @Document.StringProperty
49  *     public String getName() {...}
50  * //                ^^^^^^^
51  * }
52  * </pre>
53  */
54 data class AnnotatedGetterOrField(
55     /** The annotation that the getter or field is annotated with. */
56     val annotation: PropertyAnnotation,
57 
58     /** The annotated getter or field. */
59     val element: Element,
60 
61     /**
62      * The type-category of the getter or field.
63      *
64      * Note: `byte[]` as treated specially as documented in [ElementTypeCategory].
65      */
66     val elementTypeCategory: ElementTypeCategory,
67 
68     /**
69      * The field/getter's return type if non-repeated, else the underlying element type.
70      *
71      * For example, `String` for a field `String mName` and `int` for a field `int[] mNums`.
72      *
73      * The one exception to this is `byte[]` where:
74      * <pre>
75      * @BytesProperty bytes[] mField; // componentType: byte[]
76      * @BytesProperty bytes[][] mField; // componentType: byte[]
77      * @BytesProperty List<bytes[]> mField; // componentType: byte[]
78      * </pre>
79      */
80     val componentType: TypeMirror,
81 
82     /**
83      * The normalized/stemmed [.getJvmName].
84      *
85      * For example,
86      * <pre>
87      * getName -> name
88      * mName -> name
89      * _name -> name
90      * name_ -> name
91      * isAwesome -> awesome
92      * </pre>
93      */
94     val normalizedName: String,
95 ) {
96     /**
97      * Specifies whether the getter/field is assigned a collection or array or a single type.
98      *
99      * Note: `byte[]` are treated specially such that
100      * * `byte[]` is a primitive in icing and is treated as [.SINGLE].
101      * * `Collection<byte[]>` is treated as a [.COLLECTION].
102      * * `byte[][]` is treated as an [.ARRAY].
103      *
104      * The boxed [Byte] type is not supported by the AppSearch compiler at all.
105      */
106     enum class ElementTypeCategory {
107         SINGLE,
108         COLLECTION,
109         ARRAY
110     }
111 
112     companion object {
113         /**
114          * Creates a [AnnotatedGetterOrField] if the element is annotated with some
115          * [PropertyAnnotation]. Otherwise returns null.
116          */
117         @Throws(ProcessingException::class)
118         @JvmStatic
119         fun tryCreateFor(element: Element, env: ProcessingEnvironment): AnnotatedGetterOrField? {
120             val annotation: AnnotationMirror = getSingleAppSearchAnnotation(element) ?: return null
121 
122             val metadataPropertyAnnotation = MetadataPropertyAnnotation.tryParse(annotation)
123             if (metadataPropertyAnnotation != null) {
124                 return create(metadataPropertyAnnotation, element, env)
125             }
126 
127             val normalizedName: String = inferNormalizedName(element, env) // e.g. mField -> field
128             val dataPropertyAnnotation =
129                 tryParse(annotation, /* defaultName= */ normalizedName, IntrospectionHelper(env))
130             if (dataPropertyAnnotation != null) {
131                 return create(dataPropertyAnnotation, element, env)
132             }
133 
134             return null
135         }
136 
137         /**
138          * Creates a [AnnotatedGetterOrField] for a `getterOrField` annotated with the specified
139          * `annotation`.
140          */
141         @Throws(ProcessingException::class)
142         private fun create(
143             annotation: PropertyAnnotation,
144             getterOrField: Element,
145             env: ProcessingEnvironment
146         ): AnnotatedGetterOrField {
147             requireIsGetterOrField(getterOrField)
148 
149             val typeCategory: ElementTypeCategory = inferTypeCategory(getterOrField, env)
150             val annotatedGetterOrField =
151                 AnnotatedGetterOrField(
152                     annotation = annotation,
153                     element = getterOrField,
154                     elementTypeCategory = typeCategory,
155                     componentType = inferComponentType(getterOrField, typeCategory),
156                     normalizedName = inferNormalizedName(getterOrField, env),
157                 )
158 
159             requireTypeMatchesAnnotation(annotatedGetterOrField, env)
160 
161             return annotatedGetterOrField
162         }
163 
164         @Throws(ProcessingException::class)
165         private fun requireIsGetterOrField(element: Element) {
166             when (element.kind) {
167                 ElementKind.FIELD -> return
168                 ElementKind.METHOD -> {
169                     val method = element as ExecutableElement
170                     val errors = IntrospectionHelper.validateIsGetter(method)
171                     if (errors.isNotEmpty()) {
172                         val err =
173                             ProcessingException(
174                                 "Failed to find a suitable getter for element " +
175                                     "\"${method.simpleName}\"",
176                                 method
177                             )
178                         err.addWarnings(errors)
179                         throw err
180                     }
181                 }
182                 else -> {}
183             }
184         }
185 
186         /**
187          * Infers whether the getter/field returns a collection or array or neither.
188          *
189          * Note: `byte[]` are treated specially as documented in [ElementTypeCategory].
190          */
191         private fun inferTypeCategory(
192             getterOrField: Element,
193             env: ProcessingEnvironment
194         ): ElementTypeCategory {
195             val jvmType = IntrospectionHelper.getPropertyType(getterOrField)
196             val typeUtils = env.typeUtils
197             val helper = IntrospectionHelper(env)
198             return if (typeUtils.isAssignable(typeUtils.erasure(jvmType), helper.collectionType)) {
199                 ElementTypeCategory.COLLECTION
200             } else if (
201                 jvmType.kind == TypeKind.ARRAY &&
202                     !typeUtils.isSameType(jvmType, helper.bytePrimitiveArrayType) &&
203                     !typeUtils.isSameType(jvmType, helper.byteBoxArrayType)
204             ) {
205                 // byte[] has a native representation in Icing and should be considered a
206                 // primitive by itself.
207                 //
208                 // byte[][], however, is considered repeated (ARRAY).
209                 ElementTypeCategory.ARRAY
210             } else {
211                 ElementTypeCategory.SINGLE
212             }
213         }
214 
215         /**
216          * Infers the getter/field's return type if non-repeated, else the underlying element type.
217          *
218          * For example, `String mField -> String` and `List<String> mField -> String`.
219          */
220         @Throws(ProcessingException::class)
221         private fun inferComponentType(
222             getterOrField: Element,
223             typeCategory: ElementTypeCategory
224         ): TypeMirror {
225             val jvmType = IntrospectionHelper.getPropertyType(getterOrField)
226             return when (typeCategory) {
227                 ElementTypeCategory.SINGLE -> jvmType
228                 ElementTypeCategory.COLLECTION -> {
229                     // e.g. List<T>
230                     //           ^
231                     val typeArguments = (jvmType as DeclaredType).typeArguments
232                     if (typeArguments.isEmpty()) {
233                         throw ProcessingException(
234                             "Property is repeated but has no generic rawType",
235                             getterOrField
236                         )
237                     }
238                     typeArguments.first()
239                 }
240                 ElementTypeCategory.ARRAY -> (jvmType as ArrayType).componentType
241             }
242         }
243 
244         private fun inferNormalizedName(element: Element, env: ProcessingEnvironment): String {
245             return if (element.kind == ElementKind.METHOD) {
246                 inferNormalizedMethodName(element, env)
247             } else {
248                 inferNormalizedFieldName(element)
249             }
250         }
251 
252         /**
253          * Makes sure the getter/field's JVM type matches the type expected by the
254          * [PropertyAnnotation].
255          */
256         @Throws(ProcessingException::class)
257         private fun requireTypeMatchesAnnotation(
258             getterOrField: AnnotatedGetterOrField,
259             env: ProcessingEnvironment
260         ) {
261             val annotation = getterOrField.annotation
262             when (annotation.propertyKind) {
263                 PropertyAnnotation.Kind.METADATA_PROPERTY ->
264                     requireTypeMatchesMetadataPropertyAnnotation(
265                         getterOrField,
266                         annotation as MetadataPropertyAnnotation,
267                         env
268                     )
269                 PropertyAnnotation.Kind.DATA_PROPERTY ->
270                     requireTypeMatchesDataPropertyAnnotation(
271                         getterOrField,
272                         annotation as DataPropertyAnnotation,
273                         env
274                     )
275             }
276         }
277 
278         /**
279          * Returns the only `@Document.*` annotation on the element e.g. `@Document.StringProperty`.
280          *
281          * Returns null if no such annotation exists on the element.
282          *
283          * @throws ProcessingException If the element is annotated with more than one of such
284          *   annotations.
285          */
286         @Throws(ProcessingException::class)
287         private fun getSingleAppSearchAnnotation(element: Element): AnnotationMirror? {
288             // @Document.* annotation
289             val annotations =
290                 element.annotationMirrors
291                     .stream()
292                     .filter {
293                         it.annotationType
294                             .toString()
295                             .startsWith(
296                                 IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS.canonicalName()
297                             )
298                     }
299                     .toList()
300             if (annotations.isEmpty()) {
301                 return null
302             }
303             if (annotations.size > 1) {
304                 throw ProcessingException("Cannot use multiple @Document.* annotations", element)
305             }
306             return annotations.first()
307         }
308 
309         private fun inferNormalizedMethodName(method: Element, env: ProcessingEnvironment): String {
310             val methodName = method.simpleName.toString()
311             val helper = IntrospectionHelper(env)
312             // String getName() -> name
313             if (
314                 methodName.startsWith("get") &&
315                     methodName.length > 3 &&
316                     Character.isUpperCase(methodName[3])
317             ) {
318                 return methodName.substring(3, 4).lowercase(Locale.getDefault()) +
319                     methodName.substring(4)
320             }
321 
322             // String isAwesome() -> awesome
323             if (
324                 helper.isFieldOfBooleanType(method) &&
325                     methodName.startsWith("is") &&
326                     methodName.length > 2
327             ) {
328                 return methodName.substring(2, 3).lowercase(Locale.getDefault()) +
329                     methodName.substring(3)
330             }
331 
332             // Assume the method's name is the normalized name as well: String name() -> name
333             return methodName
334         }
335 
336         private fun inferNormalizedFieldName(field: Element): String {
337             val fieldName = field.simpleName.toString()
338             if (fieldName.length < 2) {
339                 return fieldName
340             }
341 
342             // String mName -> name
343             if (fieldName[0] == 'm' && Character.isUpperCase(fieldName[1])) {
344                 return fieldName.substring(1, 2).lowercase(Locale.getDefault()) +
345                     fieldName.substring(2)
346             }
347 
348             // String _name -> name
349             if (fieldName[0] == '_' && fieldName[1] != '_' && Character.isLowerCase(fieldName[1])) {
350                 return fieldName.substring(1)
351             }
352 
353             // String name_ -> name
354             if (fieldName[fieldName.length - 1] == '_' && fieldName[fieldName.length - 2] != '_') {
355                 return fieldName.substring(0, fieldName.length - 1)
356             }
357 
358             // Assume the field's name is the normalize name as well: String name -> name
359             return fieldName
360         }
361 
362         /**
363          * Makes sure the getter/field's JVM type matches the type expected by the
364          * [MetadataPropertyAnnotation].
365          *
366          * For example, fields annotated with `@Document.Score` must be of type `int` or [Integer].
367          */
368         @Throws(ProcessingException::class)
369         private fun requireTypeMatchesMetadataPropertyAnnotation(
370             getterOrField: AnnotatedGetterOrField,
371             annotation: MetadataPropertyAnnotation,
372             env: ProcessingEnvironment
373         ) {
374             val helper = IntrospectionHelper(env)
375             when (annotation) {
376                 MetadataPropertyAnnotation.ID,
377                 MetadataPropertyAnnotation.NAMESPACE ->
378                     requireTypeIsOneOf(
379                         getterOrField,
380                         listOf(helper.stringType),
381                         env,
382                         allowRepeated = false
383                     )
384                 MetadataPropertyAnnotation.TTL_MILLIS,
385                 MetadataPropertyAnnotation.CREATION_TIMESTAMP_MILLIS ->
386                     requireTypeIsOneOf(
387                         getterOrField,
388                         listOf(
389                             helper.longPrimitiveType,
390                             helper.intPrimitiveType,
391                             helper.longBoxType,
392                             helper.integerBoxType
393                         ),
394                         env,
395                         allowRepeated = false
396                     )
397                 MetadataPropertyAnnotation.SCORE ->
398                     requireTypeIsOneOf(
399                         getterOrField,
400                         listOf(helper.intPrimitiveType, helper.integerBoxType),
401                         env,
402                         allowRepeated = false
403                     )
404             }
405         }
406 
407         /**
408          * Makes sure the getter/field's JVM type matches the type expected by the
409          * [DataPropertyAnnotation].
410          *
411          * For example, fields annotated with [StringPropertyAnnotation] must be of type [String] or
412          * a collection or array of [String]s.
413          */
414         @Throws(ProcessingException::class)
415         private fun requireTypeMatchesDataPropertyAnnotation(
416             getterOrField: AnnotatedGetterOrField,
417             annotation: DataPropertyAnnotation,
418             env: ProcessingEnvironment
419         ) {
420             val helper = IntrospectionHelper(env)
421             when (annotation.dataPropertyKind) {
422                 DataPropertyAnnotation.Kind.STRING_PROPERTY -> {
423                     val stringSerializer = (annotation as StringPropertyAnnotation).customSerializer
424                     if (stringSerializer != null) {
425                         requireComponentTypeMatchesWithSerializer(
426                             getterOrField,
427                             stringSerializer,
428                             env
429                         )
430                     } else {
431                         requireTypeIsOneOf(
432                             getterOrField,
433                             listOf(helper.stringType),
434                             env,
435                             allowRepeated = true
436                         )
437                     }
438                 }
439                 DataPropertyAnnotation.Kind.DOCUMENT_PROPERTY ->
440                     requireTypeIsSomeDocumentClass(getterOrField, env)
441                 DataPropertyAnnotation.Kind.LONG_PROPERTY -> {
442                     val longSerializer = (annotation as LongPropertyAnnotation).customSerializer
443                     if (longSerializer != null) {
444                         requireComponentTypeMatchesWithSerializer(
445                             getterOrField,
446                             longSerializer,
447                             env
448                         )
449                     } else {
450                         requireTypeIsOneOf(
451                             getterOrField,
452                             listOf(
453                                 helper.longPrimitiveType,
454                                 helper.intPrimitiveType,
455                                 helper.longBoxType,
456                                 helper.integerBoxType
457                             ),
458                             env,
459                             allowRepeated = true
460                         )
461                     }
462                 }
463                 DataPropertyAnnotation.Kind.DOUBLE_PROPERTY ->
464                     requireTypeIsOneOf(
465                         getterOrField,
466                         listOf(
467                             helper.doublePrimitiveType,
468                             helper.floatPrimitiveType,
469                             helper.doubleBoxType,
470                             helper.floatBoxType
471                         ),
472                         env,
473                         allowRepeated = true
474                     )
475                 DataPropertyAnnotation.Kind.BOOLEAN_PROPERTY ->
476                     requireTypeIsOneOf(
477                         getterOrField,
478                         listOf(helper.booleanPrimitiveType, helper.booleanBoxType),
479                         env,
480                         allowRepeated = true
481                     )
482                 DataPropertyAnnotation.Kind.BYTES_PROPERTY ->
483                     requireTypeIsOneOf(
484                         getterOrField,
485                         listOf(helper.bytePrimitiveArrayType),
486                         env,
487                         allowRepeated = true
488                     )
489                 DataPropertyAnnotation.Kind.EMBEDDING_PROPERTY ->
490                     requireTypeIsOneOf(
491                         getterOrField,
492                         listOf(helper.embeddingType),
493                         env,
494                         allowRepeated = true
495                     )
496                 DataPropertyAnnotation.Kind.BLOB_HANDLE_PROPERTY ->
497                     requireTypeIsOneOf(
498                         getterOrField,
499                         listOf(helper.blobHandleType),
500                         env,
501                         allowRepeated = true
502                     )
503             }
504         }
505 
506         /**
507          * Makes sure the getter/field's type is one of the expected types.
508          *
509          * If `allowRepeated` is true, also allows the getter/field's type to be an array or
510          * collection of any of the expected types.
511          */
512         @Throws(ProcessingException::class)
513         private fun requireTypeIsOneOf(
514             getterOrField: AnnotatedGetterOrField,
515             expectedTypes: Collection<TypeMirror>,
516             env: ProcessingEnvironment,
517             allowRepeated: Boolean
518         ) {
519             val typeUtils = env.typeUtils
520             val target = if (allowRepeated) getterOrField.componentType else getterOrField.jvmType
521             val isValid =
522                 expectedTypes.stream().anyMatch { expectedType: TypeMirror ->
523                     typeUtils.isSameType(expectedType, target)
524                 }
525             if (!isValid) {
526                 val error =
527                     ("@${getterOrField.annotation.className.simpleName()}" +
528                         " must only be placed on a getter/field of type " +
529                         (if (allowRepeated) "or array or collection of " else "") +
530                         expectedTypes
531                             .stream()
532                             .map(TypeMirror::toString)
533                             .collect(Collectors.joining("|")))
534                 throw ProcessingException(error, getterOrField.element)
535             }
536         }
537 
538         /**
539          * Makes sure the getter/field's component type is consistent with the serializer class.
540          *
541          * @throws ProcessingException If the getter/field is of a different type than what the
542          *   serializer class serializes to/from.
543          */
544         @Throws(ProcessingException::class)
545         private fun requireComponentTypeMatchesWithSerializer(
546             getterOrField: AnnotatedGetterOrField,
547             serializerClass: SerializerClass,
548             env: ProcessingEnvironment
549         ) {
550             // The component type must exactly match the type for which we have a serializer.
551             // Subtypes do not work e.g.
552             // @StringProperty(serializer = ParentSerializer.class) Child mField;
553             // because ParentSerializer.deserialize(String) would return a Parent, which we won't be
554             // able to assign to mField.
555             if (
556                 !env.typeUtils.isSameType(getterOrField.componentType, serializerClass.customType)
557             ) {
558                 throw ProcessingException(
559                     ("@%s with serializer = %s must only be placed on a getter/field of type or " +
560                             "array or collection of %s")
561                         .format(
562                             getterOrField.annotation.className.simpleName(),
563                             serializerClass.element.simpleName,
564                             serializerClass.customType
565                         ),
566                     getterOrField.element
567                 )
568             }
569         }
570 
571         /**
572          * Makes sure the getter/field is assigned a type annotated with `@Document`.
573          *
574          * Allows for arrays and collections of such a type as well.
575          */
576         @Throws(ProcessingException::class)
577         private fun requireTypeIsSomeDocumentClass(
578             annotatedGetterOrField: AnnotatedGetterOrField,
579             env: ProcessingEnvironment
580         ) {
581             val componentType = annotatedGetterOrField.componentType
582             if (componentType.kind == TypeKind.DECLARED) {
583                 val element = env.typeUtils.asElement(componentType)
584                 if (IntrospectionHelper.getDocumentAnnotation(element) != null) {
585                     return
586                 }
587             }
588             throw ProcessingException(
589                 "Invalid type for @DocumentProperty. Must be another class " +
590                     "annotated with @Document (or collection or array of)",
591                 annotatedGetterOrField.element
592             )
593         }
594     }
595 
596     /** The field/getter's return type. */
597     val jvmType: TypeMirror
598         get() =
599             if (isGetter) {
600                 (element as ExecutableElement).returnType
601             } else {
602                 element.asType()
603             }
604 
605     /** The getter/field's jvm name e.g. `mId` or `getName`. */
606     val jvmName: String
607         get() = element.simpleName.toString()
608 
609     /** Whether the [.getElement] is a getter. */
610     val isGetter: Boolean
611         get() = element.kind == ElementKind.METHOD
612 
613     /** Whether the [.getElement] is a field. */
614     val isField: Boolean
615         get() = element.kind == ElementKind.FIELD
616 }
617