1 /*
2  * Copyright 2020 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 
17 package androidx.appsearch.compiler;
18 
19 import static androidx.appsearch.compiler.CodegenUtils.createNewArrayExpr;
20 import static androidx.appsearch.compiler.IntrospectionHelper.APPSEARCH_EXCEPTION_CLASS;
21 import static androidx.appsearch.compiler.IntrospectionHelper.DOCUMENT_CLASS_MAPPING_CONTEXT_CLASS;
22 import static androidx.appsearch.compiler.IntrospectionHelper.GENERIC_DOCUMENT_CLASS;
23 import static androidx.appsearch.compiler.IntrospectionHelper.isNonNullKotlinField;
24 
25 import androidx.appsearch.compiler.AnnotatedGetterOrField.ElementTypeCategory;
26 import androidx.appsearch.compiler.annotationwrapper.DataPropertyAnnotation;
27 import androidx.appsearch.compiler.annotationwrapper.LongPropertyAnnotation;
28 import androidx.appsearch.compiler.annotationwrapper.MetadataPropertyAnnotation;
29 import androidx.appsearch.compiler.annotationwrapper.PropertyAnnotation;
30 import androidx.appsearch.compiler.annotationwrapper.SerializerClass;
31 import androidx.appsearch.compiler.annotationwrapper.StringPropertyAnnotation;
32 
33 import com.squareup.javapoet.CodeBlock;
34 import com.squareup.javapoet.MethodSpec;
35 import com.squareup.javapoet.TypeName;
36 import com.squareup.javapoet.TypeSpec;
37 
38 import org.jspecify.annotations.NonNull;
39 
40 import java.util.ArrayList;
41 import java.util.Arrays;
42 import java.util.Collections;
43 import java.util.List;
44 import java.util.Map;
45 import java.util.Objects;
46 
47 import javax.annotation.processing.ProcessingEnvironment;
48 import javax.lang.model.element.Modifier;
49 import javax.lang.model.type.PrimitiveType;
50 import javax.lang.model.type.TypeMirror;
51 
52 /**
53  * Generates java code for a translator from a {@code androidx.appsearch.app.GenericDocument} to
54  * an instance of a class annotated with {@code androidx.appsearch.annotation.Document}.
55  */
56 class FromGenericDocumentCodeGenerator {
57     private final ProcessingEnvironment mEnv;
58     private final IntrospectionHelper mHelper;
59     private final DocumentModel mModel;
60 
FromGenericDocumentCodeGenerator( @onNull ProcessingEnvironment env, @NonNull DocumentModel model)61     private FromGenericDocumentCodeGenerator(
62             @NonNull ProcessingEnvironment env, @NonNull DocumentModel model) {
63         mEnv = env;
64         mHelper = new IntrospectionHelper(env);
65         mModel = model;
66     }
67 
generate( @onNull ProcessingEnvironment env, @NonNull DocumentModel model, TypeSpec.@NonNull Builder classBuilder)68     public static void generate(
69             @NonNull ProcessingEnvironment env,
70             @NonNull DocumentModel model,
71             TypeSpec.@NonNull Builder classBuilder) {
72         new FromGenericDocumentCodeGenerator(env, model).generate(classBuilder);
73     }
74 
generate(TypeSpec.Builder classBuilder)75     private void generate(TypeSpec.Builder classBuilder) {
76         classBuilder.addMethod(createFromGenericDocumentMethod());
77     }
78 
createFromGenericDocumentMethod()79     private MethodSpec createFromGenericDocumentMethod() {
80         // Method header
81         TypeName documentClass = TypeName.get(mModel.getClassElement().asType());
82         MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("fromGenericDocument")
83                 .addModifiers(Modifier.PUBLIC)
84                 .returns(documentClass)
85                 .addAnnotation(Override.class)
86                 .addParameter(GENERIC_DOCUMENT_CLASS, "genericDoc")
87                 .addParameter(DOCUMENT_CLASS_MAPPING_CONTEXT_CLASS, "documentClassMappingContext")
88                 .addException(APPSEARCH_EXCEPTION_CLASS);
89 
90         // Unpack properties from the GenericDocument into the format desired by the document class.
91         // Unpack metadata properties first, then data properties.
92         for (AnnotatedGetterOrField getterOrField : mModel.getAnnotatedGettersAndFields()) {
93             if (getterOrField.getAnnotation().getPropertyKind()
94                     != PropertyAnnotation.Kind.METADATA_PROPERTY) {
95                 continue;
96             }
97             methodBuilder.addCode(createCodeToExtractFromGenericDoc(
98                     (MetadataPropertyAnnotation) getterOrField.getAnnotation(), getterOrField));
99         }
100         for (AnnotatedGetterOrField getterOrField : mModel.getAnnotatedGettersAndFields()) {
101             if (getterOrField.getAnnotation().getPropertyKind()
102                     != PropertyAnnotation.Kind.DATA_PROPERTY) {
103                 continue;
104             }
105             methodBuilder.addCode(createCodeToExtractFromGenericDoc(
106                     (DataPropertyAnnotation) getterOrField.getAnnotation(), getterOrField));
107         }
108 
109         // Create an instance of the document class/builder via the chosen create method.
110         DocumentClassCreationInfo documentClassCreationInfo = mModel.getDocumentClassCreationInfo();
111         CreationMethod creationMethod = documentClassCreationInfo.getCreationMethod();
112         String variableName = creationMethod.getReturnsBuilder() ? "builder" : "document";
113         List<CodeBlock> params = creationMethod.getParamAssociations().stream()
114                 .map(annotatedGetterOrField ->
115                         CodeBlock.of("$NConv", annotatedGetterOrField.getJvmName()))
116                 .toList();
117         if (creationMethod.isConstructor()) {
118             methodBuilder.addStatement("$T $N = new $T($L)",
119                     creationMethod.getReturnType(),
120                     variableName,
121                     creationMethod.getReturnType(),
122                     CodeBlock.join(params, /* separator= */", "));
123         } else {
124             // static method
125             methodBuilder.addStatement("$T $N = $T.$N($L)",
126                     creationMethod.getReturnType(),
127                     variableName,
128                     creationMethod.getEnclosingClass(),
129                     creationMethod.getJvmName(),
130                     CodeBlock.join(params, /* separator= */", "));
131         }
132 
133         // Assign all fields which weren't set in the creation method
134         for (Map.Entry<AnnotatedGetterOrField, SetterOrField> entry :
135                 documentClassCreationInfo.getSettersAndFields().entrySet()) {
136             AnnotatedGetterOrField getterOrField = entry.getKey();
137             SetterOrField setterOrField = entry.getValue();
138             if (setterOrField.isSetter()) {
139                 methodBuilder.addStatement("$N.$N($NConv)",
140                         variableName, setterOrField.getJvmName(), getterOrField.getJvmName());
141             } else {
142                 // field
143                 methodBuilder.addStatement("$N.$N = $NConv",
144                         variableName, setterOrField.getJvmName(), getterOrField.getJvmName());
145             }
146         }
147 
148         if (creationMethod.getReturnsBuilder()) {
149             methodBuilder.addStatement("return $N.build()", variableName);
150         } else {
151             methodBuilder.addStatement("return $N", variableName);
152         }
153 
154         return methodBuilder.build();
155     }
156 
157     /**
158      * Returns code that copies the metadata property out of a generic document document.
159      *
160      * <p>Assumes there is a generic document var in-scope called {@code genericDoc}.
161      *
162      * <p>Leaves a variable in the scope with the name {@code {JVM_NAME}Conv} with the final result.
163      * This variable is guaranteed to be of the same type as the {@link AnnotatedGetterOrField}'s
164      * JVM type.
165      */
createCodeToExtractFromGenericDoc( @onNull MetadataPropertyAnnotation annotation, @NonNull AnnotatedGetterOrField getterOrField)166     private CodeBlock createCodeToExtractFromGenericDoc(
167             @NonNull MetadataPropertyAnnotation annotation,
168             @NonNull AnnotatedGetterOrField getterOrField) {
169         // All metadata properties in a GenericDocument either use primitives or Strings and
170         // their getters always return non-null values
171         // e.g. genericDoc.getId() -> String, genericDoc.getTtlMillis() -> long
172         return CodeBlock.builder()
173                 .addStatement("$T $NConv = $L",
174                         getterOrField.getJvmType(),
175                         getterOrField.getJvmName(),
176                         maybeApplyNarrowingCast(
177                                 CodeBlock.of(
178                                         "genericDoc.$N()", annotation.getGenericDocGetterName()),
179                                 /* exprType= */
180                                 annotation.getUnderlyingTypeWithinGenericDoc(mHelper),
181                                 /* targetType= */getterOrField.getJvmType()))
182                 .build();
183     }
184 
185     /**
186      * Returns code that copies the data property out of a generic document document.
187      *
188      * <p>Assumes there is a generic document var in-scope called {@code genericDoc}.
189      *
190      * <p>Leaves a variable in the scope with the name {@code {JVM_NAME}Conv} with the final result.
191      * This variable is guaranteed to be of the same type as the {@link AnnotatedGetterOrField}'s
192      * JVM type.
193      */
createCodeToExtractFromGenericDoc( @onNull DataPropertyAnnotation annotation, @NonNull AnnotatedGetterOrField getterOrField)194     private CodeBlock createCodeToExtractFromGenericDoc(
195             @NonNull DataPropertyAnnotation annotation,
196             @NonNull AnnotatedGetterOrField getterOrField) {
197         // Scenario 1: field is assignable from List
198         //   1a: ListForLoopAssign
199         //       List contains boxed Long, Integer, Double, Float, Boolean or byte[]. We have to
200         //       unpack it from a primitive array of type long[], double[], boolean[], or byte[][]
201         //       by reading each element one-by-one and assigning it. The compiler takes care of
202         //       unboxing.
203         //
204         //   1b: ListCallArraysAsList
205         //       List contains String, EmbeddingVector or AppSearchBlobHandle. We have to convert
206         //       this from an array of String[], EmbeddingVector[] or AppSearchBlobHandle[], but no
207         //       conversion of the collection elements is needed. We can use Arrays#asList for this.
208         //
209         //   1c: ListForLoopCallFromGenericDocument
210         //       List contains a class which is annotated with @Document.
211         //       We have to convert this from an array of GenericDocument[], by reading each element
212         //       one-by-one and converting it through the standard conversion machinery.
213         //
214         //   1d: ListForLoopCallDeserialize
215         //       List contains a custom type for which we have a serializer.
216         //       We have to convert this from an array of String[]|long[], by reading each element
217         //       one-by-one and calling serializerClass.deserialize(element).
218 
219         // Scenario 2: field is an Array
220         //   2a: ArrayForLoopAssign
221         //       Array is of type Long[], Integer[], int[], Double[], Float[], float[], Boolean[],
222         //       or Byte[].
223         //       We have to unpack it from a primitive array of type long[], double[], boolean[] or
224         //       byte[] by reading each element one-by-one and assigning it. The compiler takes care
225         //       of unboxing.
226         //
227         //   2b: ArrayUseDirectly
228         //       Array is of type String[], long[], double[], boolean[], byte[][],
229         //       EmbeddingVector[] or AppSearchBlobHandle[]
230         //       We can directly use this field with no conversion.
231         //
232         //   2c: ArrayForLoopCallFromGenericDocument
233         //       Array is of a class which is annotated with @Document.
234         //       We have to convert this from an array of GenericDocument[], by reading each element
235         //       one-by-one and converting it through the standard conversion machinery.
236         //
237         //   2d: ArrayForLoopCallDeserialize
238         //       Array is of a custom type for which we have a serializer.
239         //       We have to convert this from an array of String[]|long[], by reading each element
240         //       one-by-one and calling serializerClass.deserialize(element).
241         //
242         //   2e: Array is of class byte[]. This is actually a single-valued field as byte arrays are
243         //       natively supported by Icing, and is handled as Scenario 3a.
244 
245         // Scenario 3: Single valued fields
246         //   3a: FieldUseDirectlyWithNullCheck
247         //       Field is of type String, Long, Integer, Double, Float, Boolean, byte[],
248         //       EmbeddingVector or AppSearchBlobHandle.
249         //       We can use this field directly, after testing for null. The java compiler will box
250         //       or unbox as needed.
251         //
252         //   3b: FieldUseDirectlyWithoutNullCheck
253         //       Field is of type long, int, double, float, or boolean.
254         //       We can use this field directly. Since we cannot assign null, we must assign the
255         //       default value if the field is not specified. The java compiler will box or unbox as
256         //       needed
257         //
258         //   3c: FieldCallFromGenericDocument
259         //       Field is of a class which is annotated with @Document.
260         //       We have to convert this from a GenericDocument through the standard conversion
261         //       machinery.
262         //
263         //   3d: FieldCallDeserialize
264         //       Field is of a custom type for which we have a serializer.
265         //       We have to convert this from a String|long by calling
266         //       serializerClass.deserialize(value).
267         ElementTypeCategory typeCategory = getterOrField.getElementTypeCategory();
268         switch (annotation.getDataPropertyKind()) {
269             case STRING_PROPERTY:
270                 SerializerClass stringSerializer =
271                         ((StringPropertyAnnotation) annotation).getCustomSerializer();
272                 switch (typeCategory) {
273                     case COLLECTION:
274                         if (stringSerializer != null) { // List<CustomType>: 1d
275                             return listForLoopCallDeserialize(
276                                     annotation, getterOrField, stringSerializer);
277                         } else { // List<String>: 1b
278                             return listCallArraysAsList(annotation, getterOrField);
279                         }
280                     case ARRAY:
281                         if (stringSerializer != null) { // CustomType[]: 2d
282                             return arrayForLoopCallDeserialize(
283                                     annotation, getterOrField, stringSerializer);
284                         } else { // String[]: 2b
285                             return arrayUseDirectly(annotation, getterOrField);
286                         }
287                     case SINGLE:
288                         if (stringSerializer != null) { // CustomType: 3d
289                             return fieldCallDeserialize(
290                                     annotation, getterOrField, stringSerializer);
291                         } else { // String: 3a
292                             return fieldUseDirectlyWithNullCheck(annotation, getterOrField);
293                         }
294                     default:
295                         throw new IllegalStateException("Unhandled type-category: " + typeCategory);
296                 }
297             case DOCUMENT_PROPERTY:
298                 switch (typeCategory) {
299                     case COLLECTION: // List<Person>: 1c
300                         return listForLoopCallFromGenericDocument(annotation, getterOrField);
301                     case ARRAY: // Person[]: 2c
302                         return arrayForLoopCallFromGenericDocument(annotation, getterOrField);
303                     case SINGLE: // Person: 3c
304                         return fieldCallFromGenericDocument(annotation, getterOrField);
305                     default:
306                         throw new IllegalStateException("Unhandled type-category: " + typeCategory);
307                 }
308             case LONG_PROPERTY:
309                 SerializerClass longSerializer =
310                         ((LongPropertyAnnotation) annotation).getCustomSerializer();
311                 switch (typeCategory) {
312                     case COLLECTION:
313                         if (longSerializer != null) { // List<CustomType>: 1d
314                             return listForLoopCallDeserialize(
315                                     annotation, getterOrField, longSerializer);
316                         } else { // List<Long>|List<Integer>: 1a
317                             return listForLoopAssign(annotation, getterOrField);
318                         }
319                     case ARRAY:
320                         if (longSerializer != null) { // CustomType[]: 2d
321                             return arrayForLoopCallDeserialize(
322                                     annotation, getterOrField, longSerializer);
323                         } else if (mHelper.isPrimitiveLongArray(getterOrField.getJvmType())) {
324                             // long[]: 2b
325                             return arrayUseDirectly(annotation, getterOrField);
326                         } else { // int[]|Integer[]|Long[]: 2a
327                             return arrayForLoopAssign(annotation, getterOrField);
328                         }
329                     case SINGLE:
330                         if (longSerializer != null) { // CustomType: 3d
331                             return fieldCallDeserialize(annotation, getterOrField, longSerializer);
332                         } else if (getterOrField.getJvmType() instanceof PrimitiveType) {
333                             // long|int: 3b
334                             return fieldUseDirectlyWithoutNullCheck(annotation, getterOrField);
335                         } else { // Long|Integer: 3a
336                             return fieldUseDirectlyWithNullCheck(annotation, getterOrField);
337                         }
338                     default:
339                         throw new IllegalStateException("Unhandled type-category: " + typeCategory);
340                 }
341             case DOUBLE_PROPERTY:
342                 switch (typeCategory) {
343                     case COLLECTION: // List<Double>|List<Float>: 1a
344                         return listForLoopAssign(annotation, getterOrField);
345                     case ARRAY:
346                         if (mHelper.isPrimitiveDoubleArray(getterOrField.getJvmType())) {
347                             // double[]: 2b
348                             return arrayUseDirectly(annotation, getterOrField);
349                         } else {
350                             // float[]|Float[]|Double[]: 2a
351                             return arrayForLoopAssign(annotation, getterOrField);
352                         }
353                     case SINGLE:
354                         if (getterOrField.getJvmType() instanceof PrimitiveType) {
355                             // double|float: 3b
356                             return fieldUseDirectlyWithoutNullCheck(annotation, getterOrField);
357                         } else {
358                             // Double|Float: 3a
359                             return fieldUseDirectlyWithNullCheck(annotation, getterOrField);
360                         }
361                     default:
362                         throw new IllegalStateException("Unhandled type-category: " + typeCategory);
363                 }
364             case BOOLEAN_PROPERTY:
365                 switch (typeCategory) {
366                     case COLLECTION: // List<Boolean>: 1a
367                         return listForLoopAssign(annotation, getterOrField);
368                     case ARRAY:
369                         if (mHelper.isPrimitiveBooleanArray(getterOrField.getJvmType())) {
370                             // boolean[]: 2b
371                             return arrayUseDirectly(annotation, getterOrField);
372                         } else {
373                             // Boolean[]
374                             return arrayForLoopAssign(annotation, getterOrField);
375                         }
376                     case SINGLE:
377                         if (getterOrField.getJvmType() instanceof PrimitiveType) {
378                             // boolean: 3b
379                             return fieldUseDirectlyWithoutNullCheck(annotation, getterOrField);
380                         } else {
381                             // Boolean: 3a
382                             return fieldUseDirectlyWithNullCheck(annotation, getterOrField);
383                         }
384                     default:
385                         throw new IllegalStateException("Unhandled type-category: " + typeCategory);
386                 }
387             case BYTES_PROPERTY:
388                 switch (typeCategory) {
389                     case COLLECTION: // List<byte[]>: 1a
390                         return listForLoopAssign(annotation, getterOrField);
391                     case ARRAY: // byte[][]: 2b
392                         return arrayUseDirectly(annotation, getterOrField);
393                     case SINGLE: // byte[]: 2e/3a
394                         return fieldUseDirectlyWithNullCheck(annotation, getterOrField);
395                     default:
396                         throw new IllegalStateException("Unhandled type-category: " + typeCategory);
397                 }
398             case EMBEDDING_PROPERTY:
399                 switch (typeCategory) {
400                     case COLLECTION: // List<EmbeddingVector>: 1b
401                         return listCallArraysAsList(annotation, getterOrField);
402                     case ARRAY:
403                         // EmbeddingVector[]: 2b
404                         return arrayUseDirectly(annotation, getterOrField);
405                     case SINGLE:
406                         // EmbeddingVector: 3a
407                         return fieldUseDirectlyWithNullCheck(annotation, getterOrField);
408                     default:
409                         throw new IllegalStateException("Unhandled type-category: " + typeCategory);
410                 }
411             case BLOB_HANDLE_PROPERTY:
412                 switch (typeCategory) {
413                     case COLLECTION: // List<AppSearchBlobHandle>: 1b
414                         return listCallArraysAsList(annotation, getterOrField);
415                     case ARRAY:
416                         // AppSearchBlobHandle[]: 2b
417                         return arrayUseDirectly(annotation, getterOrField);
418                     case SINGLE:
419                         // AppSearchBlobHandle: 3a
420                         return fieldUseDirectlyWithNullCheck(annotation, getterOrField);
421                     default:
422                         throw new IllegalStateException("Unhandled type-category: " + typeCategory);
423                 }
424             default:
425                 throw new IllegalStateException("Unhandled annotation: " + annotation);
426         }
427     }
428 
429     // TODO(b/378122240): Determine if this should be done for other types of collections
430     /**
431      * Writes the assignment of a default list value to codeBlockBuilder.
432      *
433      * <p>If the list is a non-null Kotlin list, it will be initialized to an empty list. Otherwise,
434      * if it is a nullable Kotlin list or Java list, it will be initialized to null.
435      */
addDefaultValueForList(CodeBlock.@onNull Builder codeBlockBuilder, @NonNull AnnotatedGetterOrField getterOrField)436     private void addDefaultValueForList(CodeBlock.@NonNull Builder codeBlockBuilder,
437             @NonNull AnnotatedGetterOrField getterOrField) {
438         Objects.requireNonNull(codeBlockBuilder);
439         Objects.requireNonNull(getterOrField);
440 
441         if (isNonNullKotlinField(getterOrField)) {
442             codeBlockBuilder.addStatement("$T<$T> $NConv = $T.emptyList()",
443                     List.class, getterOrField.getComponentType(), getterOrField.getJvmName(),
444                     Collections.class);
445         } else {
446             codeBlockBuilder.addStatement("$T<$T> $NConv = null",
447                     List.class, getterOrField.getComponentType(), getterOrField.getJvmName());
448         }
449     }
450 
451     // 1a: ListForLoopAssign
452     //     List contains boxed Long, Integer, Double, Float, Boolean or byte[]. We have to
453     //     unpack it from a primitive array of type long[], double[], boolean[], or byte[][]
454     //     by reading each element one-by-one and assigning it. The compiler takes care of
455     //     unboxing.
listForLoopAssign( @onNull DataPropertyAnnotation annotation, @NonNull AnnotatedGetterOrField getterOrField)456     private @NonNull CodeBlock listForLoopAssign(
457             @NonNull DataPropertyAnnotation annotation,
458             @NonNull AnnotatedGetterOrField getterOrField) {
459         TypeMirror serializedType = annotation.getUnderlyingTypeWithinGenericDoc(mHelper);
460         CodeBlock.Builder codeBlockBuilder = CodeBlock.builder()
461                 .addStatement("$T[] $NCopy = genericDoc.$N($S)",
462                         serializedType,
463                         getterOrField.getJvmName(),
464                         annotation.getGenericDocArrayGetterName(),
465                         annotation.getName());
466 
467         addDefaultValueForList(codeBlockBuilder, getterOrField);
468 
469         return codeBlockBuilder.beginControlFlow("if ($NCopy != null)", getterOrField.getJvmName())
470                 .addStatement("$NConv = new $T<>($NCopy.length)",
471                         getterOrField.getJvmName(), ArrayList.class, getterOrField.getJvmName())
472                 .beginControlFlow("for (int i = 0; i < $NCopy.length; i++)",
473                         getterOrField.getJvmName())
474                 .addStatement("$NConv.add($L)",
475                         getterOrField.getJvmName(),
476                         maybeApplyNarrowingCast(
477                                 CodeBlock.of("$NCopy[i]", getterOrField.getJvmName()),
478                                 /* exprType= */serializedType,
479                                 /* targetType= */getterOrField.getComponentType()))
480                 .endControlFlow() // for (...)
481                 .endControlFlow() // if (...)
482                 .build();
483     }
484 
485     // 1b: ListCallArraysAsList
486     //     List contains String, EmbeddingVector or AppSearchBlobHandle. We have to convert this
487     //     from an array of String[], EmbeddingVector[] or AppSearchBlobHandle[], but no conversion
488     //     of the collection elements is needed. We can use Arrays#asList for this.
listCallArraysAsList( @onNull DataPropertyAnnotation annotation, @NonNull AnnotatedGetterOrField getterOrField)489     private @NonNull CodeBlock listCallArraysAsList(
490             @NonNull DataPropertyAnnotation annotation,
491             @NonNull AnnotatedGetterOrField getterOrField) {
492         CodeBlock.Builder builder = CodeBlock.builder()
493                 .addStatement("$T[] $NCopy = genericDoc.$N($S)",
494                         annotation.getUnderlyingTypeWithinGenericDoc(mHelper),
495                         getterOrField.getJvmName(),
496                         annotation.getGenericDocArrayGetterName(),
497                         annotation.getName());
498         addDefaultValueForList(builder, getterOrField);
499         return builder
500                 .beginControlFlow("if ($NCopy != null)", getterOrField.getJvmName())
501                 .addStatement("$NConv = $T.asList($NCopy)",
502                         getterOrField.getJvmName(), Arrays.class, getterOrField.getJvmName())
503                 .endControlFlow() // if (...)
504                 .build();
505     }
506 
507     // 1c: ListForLoopCallFromGenericDocument
508     //     List contains a class which is annotated with @Document.
509     //     We have to convert this from an array of GenericDocument[], by reading each element
510     //     one-by-one and converting it through the standard conversion machinery.
listForLoopCallFromGenericDocument( @onNull DataPropertyAnnotation annotation, @NonNull AnnotatedGetterOrField getterOrField)511     private @NonNull CodeBlock listForLoopCallFromGenericDocument(
512             @NonNull DataPropertyAnnotation annotation,
513             @NonNull AnnotatedGetterOrField getterOrField) {
514         CodeBlock.Builder codeBlockBuilder = CodeBlock.builder()
515                 .addStatement("$T[] $NCopy = genericDoc.getPropertyDocumentArray($S)",
516                         GENERIC_DOCUMENT_CLASS, getterOrField.getJvmName(), annotation.getName());
517         addDefaultValueForList(codeBlockBuilder, getterOrField);
518         return codeBlockBuilder.beginControlFlow("if ($NCopy != null)", getterOrField.getJvmName())
519                 .addStatement("$NConv = new $T<>($NCopy.length)",
520                         getterOrField.getJvmName(), ArrayList.class, getterOrField.getJvmName())
521                 .beginControlFlow("for (int i = 0; i < $NCopy.length; i++)",
522                         getterOrField.getJvmName())
523                 .addStatement(
524                         "$NConv.add($NCopy[i].toDocumentClass($T.class, "
525                                 + "documentClassMappingContext))",
526                         getterOrField.getJvmName(),
527                         getterOrField.getJvmName(),
528                         getterOrField.getComponentType())
529                 .endControlFlow() // for (...)
530                 .endControlFlow() // if (...)
531                 .build();
532     }
533 
534     // 1d: ListForLoopCallDeserialize
535     //     List contains a custom type for which we have a serializer.
536     //     We have to convert this from an array of String[]|long[], by reading each element
537     //     one-by-one and calling serializerClass.deserialize(element).
listForLoopCallDeserialize( @onNull DataPropertyAnnotation annotation, @NonNull AnnotatedGetterOrField getterOrField, @NonNull SerializerClass serializerClass)538     private @NonNull CodeBlock listForLoopCallDeserialize(
539             @NonNull DataPropertyAnnotation annotation,
540             @NonNull AnnotatedGetterOrField getterOrField,
541             @NonNull SerializerClass serializerClass) {
542         TypeMirror customType = getterOrField.getComponentType();
543         String jvmName = getterOrField.getJvmName(); // e.g. mProp|prop
544         CodeBlock.Builder codeBlockBuilder = CodeBlock.builder()
545                 .addStatement("$T[] $NCopy = genericDoc.$N($S)",
546                         annotation.getUnderlyingTypeWithinGenericDoc(mHelper),
547                         jvmName,
548                         annotation.getGenericDocArrayGetterName(),
549                         annotation.getName());
550         addDefaultValueForList(codeBlockBuilder, getterOrField);
551         return codeBlockBuilder.beginControlFlow("if ($NCopy != null)", jvmName)
552                 .addStatement("$NConv = new $T<>($NCopy.length)", jvmName, ArrayList.class, jvmName)
553                 .addStatement("$T serializer = new $T()",
554                         serializerClass.getElement(), serializerClass.getElement())
555                 .beginControlFlow("for (int i = 0; i < $NCopy.length; i++)", jvmName)
556                 .addStatement("$T elem = serializer.deserialize($NCopy[i])", customType, jvmName)
557                 .beginControlFlow("if (elem == null)")
558                 // Deserialization failed
559                 // Abort the whole transaction since we cannot preserve the same element indices
560                 // as the underlying data.
561                 .addStatement("$NConv = null", jvmName)
562                 .addStatement("break")
563                 .endControlFlow() // if (elem == null)
564                 .addStatement("$NConv.add(elem)", jvmName)
565                 .endControlFlow() // for (...)
566                 .endControlFlow() // if ($NCopy != null)
567                 .build();
568     }
569 
570     // 2a: ArrayForLoopAssign
571     //     Array is of type Long[], Integer[], int[], Double[], Float[], float[], Boolean[],
572     //     or Byte[].
573     //     We have to unpack it from a primitive array of type long[], double[], boolean[] or
574     //     byte[] by reading each element one-by-one and assigning it. The compiler takes care
575     //     of unboxing.
arrayForLoopAssign( @onNull DataPropertyAnnotation annotation, @NonNull AnnotatedGetterOrField getterOrField)576     private @NonNull CodeBlock arrayForLoopAssign(
577             @NonNull DataPropertyAnnotation annotation,
578             @NonNull AnnotatedGetterOrField getterOrField) {
579         TypeMirror serializedType = annotation.getUnderlyingTypeWithinGenericDoc(mHelper);
580         return CodeBlock.builder()
581                 .addStatement("$T[] $NCopy = genericDoc.$N($S)",
582                         serializedType,
583                         getterOrField.getJvmName(),
584                         annotation.getGenericDocArrayGetterName(),
585                         annotation.getName())
586                 .addStatement("$T[] $NConv = null",
587                         getterOrField.getComponentType(), getterOrField.getJvmName())
588                 .beginControlFlow("if ($NCopy != null)", getterOrField.getJvmName())
589                 .addStatement("$NConv = $L",
590                         getterOrField.getJvmName(),
591                         createNewArrayExpr(
592                                 getterOrField.getComponentType(),
593                                 /* size= */
594                                 CodeBlock.of("$NCopy.length", getterOrField.getJvmName()),
595                                 mEnv))
596                 .beginControlFlow("for (int i = 0; i < $NCopy.length; i++)",
597                         getterOrField.getJvmName())
598                 .addStatement("$NConv[i] = $L",
599                         getterOrField.getJvmName(),
600                         maybeApplyNarrowingCast(
601                                 CodeBlock.of("$NCopy[i]", getterOrField.getJvmName()),
602                                 /* exprType= */serializedType,
603                                 /* targetType= */getterOrField.getComponentType()))
604                 .endControlFlow() // for (...)
605                 .endControlFlow() // if (...)
606                 .build();
607     }
608 
609     // 2b: ArrayUseDirectly
610     //     Array is of type String[], long[], double[], boolean[], byte[][], EmbeddingVector[]
611     //     or AppSearchBlobHandle[].
arrayUseDirectly( @onNull DataPropertyAnnotation annotation, @NonNull AnnotatedGetterOrField getterOrField)612     private @NonNull CodeBlock arrayUseDirectly(
613             @NonNull DataPropertyAnnotation annotation,
614             @NonNull AnnotatedGetterOrField getterOrField) {
615         return CodeBlock.builder()
616                 .addStatement("$T[] $NConv = genericDoc.$N($S)",
617                         annotation.getUnderlyingTypeWithinGenericDoc(mHelper),
618                         getterOrField.getJvmName(),
619                         annotation.getGenericDocArrayGetterName(),
620                         annotation.getName())
621                 .build();
622     }
623 
624     // 2c: ArrayForLoopCallFromGenericDocument
625     //     Array is of a class which is annotated with @Document.
626     //     We have to convert this from an array of GenericDocument[], by reading each element
627     //     one-by-one and converting it through the standard conversion machinery.
arrayForLoopCallFromGenericDocument( @onNull DataPropertyAnnotation annotation, @NonNull AnnotatedGetterOrField getterOrField)628     private @NonNull CodeBlock arrayForLoopCallFromGenericDocument(
629             @NonNull DataPropertyAnnotation annotation,
630             @NonNull AnnotatedGetterOrField getterOrField) {
631         return CodeBlock.builder()
632                 .addStatement("$T[] $NCopy = genericDoc.getPropertyDocumentArray($S)",
633                         GENERIC_DOCUMENT_CLASS, getterOrField.getJvmName(), annotation.getName())
634                 .addStatement("$T[] $NConv = null",
635                         getterOrField.getComponentType(), getterOrField.getJvmName())
636                 .beginControlFlow("if ($NCopy != null)", getterOrField.getJvmName())
637                 .addStatement("$NConv = new $T[$NCopy.length]",
638                         getterOrField.getJvmName(),
639                         getterOrField.getComponentType(),
640                         getterOrField.getJvmName())
641                 .beginControlFlow("for (int i = 0; i < $NCopy.length; i++)",
642                         getterOrField.getJvmName())
643                 .addStatement(
644                         "$NConv[i] = $NCopy[i].toDocumentClass($T.class, "
645                                 + "documentClassMappingContext)",
646                         getterOrField.getJvmName(),
647                         getterOrField.getJvmName(),
648                         getterOrField.getComponentType())
649                 .endControlFlow() // for (...)
650                 .endControlFlow() // if (...)
651                 .build();
652     }
653 
654     // 2d: ArrayForLoopCallDeserialize
655     //     Array is of a custom type for which we have a serializer.
656     //     We have to convert this from an array of String[]|long[], by reading each element
657     //     one-by-one and calling serializerClass.deserialize(element).
arrayForLoopCallDeserialize( @onNull DataPropertyAnnotation annotation, @NonNull AnnotatedGetterOrField getterOrField, @NonNull SerializerClass serializerClass)658     private @NonNull CodeBlock arrayForLoopCallDeserialize(
659             @NonNull DataPropertyAnnotation annotation,
660             @NonNull AnnotatedGetterOrField getterOrField,
661             @NonNull SerializerClass serializerClass) {
662         TypeMirror customType = getterOrField.getComponentType();
663         String jvmName = getterOrField.getJvmName(); // e.g. mProp|prop
664         return CodeBlock.builder()
665                 .addStatement("$T[] $NCopy = genericDoc.$N($S)",
666                         annotation.getUnderlyingTypeWithinGenericDoc(mHelper),
667                         jvmName,
668                         annotation.getGenericDocArrayGetterName(),
669                         annotation.getName())
670                 .addStatement("$T[] $NConv = null", customType, jvmName)
671                 .beginControlFlow("if ($NCopy != null)", jvmName)
672                 .addStatement("$NConv = $L",
673                         jvmName,
674                         createNewArrayExpr(
675                                 customType,
676                                 /* size= */CodeBlock.of("$NCopy.length", jvmName),
677                                 mEnv))
678                 .addStatement("$T serializer = new $T()",
679                         serializerClass.getElement(), serializerClass.getElement())
680                 .beginControlFlow("for (int i = 0; i < $NCopy.length; i++)", jvmName)
681                 .addStatement("$T elem = serializer.deserialize($NCopy[i])", customType, jvmName)
682                 .beginControlFlow("if (elem == null)")
683                 // Deserialization failed
684                 // Abort the whole transaction since we cannot preserve the same element indices
685                 // as the underlying data.
686                 .addStatement("$NConv = null", jvmName)
687                 .addStatement("break")
688                 .endControlFlow() // if (elem == null)
689                 .addStatement("$NConv[i] = elem", jvmName)
690                 .endControlFlow() // for (...)
691                 .endControlFlow() // if ($NCopy != null)
692                 .build();
693     }
694 
695     // 3a: FieldUseDirectlyWithNullCheck
696     //     Field is of type String, Long, Integer, Double, Float, Boolean, byte[], EmbeddingVector
697     //     or AppSearchBlobHandle.
698     //     We can use this field directly, after testing for null. The java compiler will box
699     //     or unbox as needed.
fieldUseDirectlyWithNullCheck( @onNull DataPropertyAnnotation annotation, @NonNull AnnotatedGetterOrField getterOrField)700     private @NonNull CodeBlock fieldUseDirectlyWithNullCheck(
701             @NonNull DataPropertyAnnotation annotation,
702             @NonNull AnnotatedGetterOrField getterOrField) {
703         TypeMirror serializedType = annotation.getUnderlyingTypeWithinGenericDoc(mHelper);
704         return CodeBlock.builder()
705                 .addStatement("$T[] $NCopy = genericDoc.$N($S)",
706                         serializedType,
707                         getterOrField.getJvmName(),
708                         annotation.getGenericDocArrayGetterName(),
709                         annotation.getName())
710                 .addStatement("$T $NConv = null",
711                         getterOrField.getJvmType(), getterOrField.getJvmName())
712                 .beginControlFlow("if ($NCopy != null && $NCopy.length != 0)",
713                         getterOrField.getJvmName(), getterOrField.getJvmName())
714                 .addStatement("$NConv = $L",
715                         getterOrField.getJvmName(),
716                         maybeApplyNarrowingCast(
717                                 CodeBlock.of("$NCopy[0]", getterOrField.getJvmName()),
718                                 /* exprType= */serializedType,
719                                 /* targetType= */getterOrField.getJvmType()))
720                 .endControlFlow() // if (...)
721                 .build();
722     }
723 
724     // 3b: FieldUseDirectlyWithoutNullCheck
725     //     Field is of type long, int, double, float, or boolean.
726     //     We can use this field directly. Since we cannot assign null, we must assign the
727     //     default value if the field is not specified. The java compiler will box or unbox as
728     //     needed
fieldUseDirectlyWithoutNullCheck( @onNull DataPropertyAnnotation annotation, @NonNull AnnotatedGetterOrField getterOrField)729     private @NonNull CodeBlock fieldUseDirectlyWithoutNullCheck(
730             @NonNull DataPropertyAnnotation annotation,
731             @NonNull AnnotatedGetterOrField getterOrField) {
732         return CodeBlock.builder()
733                 .addStatement("$T $NConv = $L",
734                         getterOrField.getJvmType(),
735                         getterOrField.getJvmName(),
736                         maybeApplyNarrowingCast(
737                                 CodeBlock.of("genericDoc.$N($S)",
738                                         annotation.getGenericDocGetterName(), annotation.getName()),
739                                 /* exprType= */
740                                 annotation.getUnderlyingTypeWithinGenericDoc(mHelper),
741                                 /* targetType= */getterOrField.getJvmType()))
742                 .build();
743     }
744 
745     // 3c: FieldCallFromGenericDocument
746     //     Field is of a class which is annotated with @Document.
747     //     We have to convert this from a GenericDocument through the standard conversion
748     //     machinery.
fieldCallFromGenericDocument( @onNull DataPropertyAnnotation annotation, @NonNull AnnotatedGetterOrField getterOrField)749     private @NonNull CodeBlock fieldCallFromGenericDocument(
750             @NonNull DataPropertyAnnotation annotation,
751             @NonNull AnnotatedGetterOrField getterOrField) {
752         return CodeBlock.builder()
753                 .addStatement("$T $NCopy = genericDoc.getPropertyDocument($S)",
754                         GENERIC_DOCUMENT_CLASS, getterOrField.getJvmName(), annotation.getName())
755                 .addStatement("$T $NConv = null",
756                         getterOrField.getJvmType(), getterOrField.getJvmName())
757                 .beginControlFlow("if ($NCopy != null)", getterOrField.getJvmName())
758                 .addStatement(
759                         "$NConv = $NCopy.toDocumentClass($T.class, documentClassMappingContext)",
760                         getterOrField.getJvmName(),
761                         getterOrField.getJvmName(),
762                         getterOrField.getJvmType())
763                 .endControlFlow() // if (...)
764                 .build();
765     }
766 
767     // 3d: FieldCallDeserialize
768     //     Field is of a custom type for which we have a serializer.
769     //     We have to convert this from a String|long by calling
770     //     serializerClass.deserialize(value).
fieldCallDeserialize( @onNull DataPropertyAnnotation annotation, @NonNull AnnotatedGetterOrField getterOrField, @NonNull SerializerClass serializerClass)771     private @NonNull CodeBlock fieldCallDeserialize(
772             @NonNull DataPropertyAnnotation annotation,
773             @NonNull AnnotatedGetterOrField getterOrField,
774             @NonNull SerializerClass serializerClass) {
775         TypeMirror customType = getterOrField.getJvmType();
776         String jvmName = getterOrField.getJvmName(); // e.g. mProp|prop
777         TypeMirror propType = annotation.getUnderlyingTypeWithinGenericDoc(mHelper); // e.g. long
778         CodeBlock.Builder codeBlock = CodeBlock.builder()
779                 .addStatement("$T $NCopy = genericDoc.$N($S)",
780                         propType,
781                         jvmName,
782                         annotation.getGenericDocGetterName(),
783                         annotation.getName())
784                 .addStatement("$T $NConv = null", customType, jvmName);
785         boolean nullCheckRequired = !(propType instanceof PrimitiveType);
786         if (nullCheckRequired) {
787             codeBlock.beginControlFlow("if ($NCopy != null)", jvmName);
788         }
789         codeBlock.addStatement("$NConv = new $T().deserialize($NCopy)",
790                 jvmName, serializerClass.getElement(), jvmName);
791         if (nullCheckRequired) {
792             codeBlock.endControlFlow();
793         }
794         return codeBlock.build();
795     }
796 
797     /**
798      * Prepends the expr with a cast so it may be coerced to the target type. For example,
799      *
800      * <pre>
801      * {@code
802      * // Given expr.ofTypeLong() and target type = int; returns:
803      * (int) expr.ofTypeLong()
804      * }
805      * </pre>
806      */
maybeApplyNarrowingCast( @onNull CodeBlock expr, @NonNull TypeMirror exprType, @NonNull TypeMirror targetType)807     private @NonNull CodeBlock maybeApplyNarrowingCast(
808             @NonNull CodeBlock expr,
809             @NonNull TypeMirror exprType,
810             @NonNull TypeMirror targetType) {
811         TypeMirror castType =
812                 mHelper.getNarrowingCastType(/* sourceType= */exprType, targetType);
813         if (castType == null) {
814             return expr;
815         }
816         return CodeBlock.of("($T) $L", castType, expr);
817     }
818 }
819