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