/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.appsearch.compiler;
import static androidx.appsearch.compiler.IntrospectionHelper.getDocumentAnnotation;
import androidx.annotation.NonNull;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.WildcardTypeName;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Types;
/**
* Generates java code for a translator from an instance of a class annotated with
* {@link androidx.appsearch.annotation.Document} into a
* {@link androidx.appsearch.app.GenericDocument}.
*/
class ToGenericDocumentCodeGenerator {
private final ProcessingEnvironment mEnv;
private final IntrospectionHelper mHelper;
private final DocumentModel mModel;
public static void generate(
@NonNull ProcessingEnvironment env,
@NonNull DocumentModel model,
@NonNull TypeSpec.Builder classBuilder) throws ProcessingException {
new ToGenericDocumentCodeGenerator(env, model).generate(classBuilder);
}
private ToGenericDocumentCodeGenerator(
@NonNull ProcessingEnvironment env, @NonNull DocumentModel model) {
mEnv = env;
mHelper = new IntrospectionHelper(env);
mModel = model;
}
private void generate(TypeSpec.Builder classBuilder) throws ProcessingException {
classBuilder.addMethod(createToGenericDocumentMethod());
}
private MethodSpec createToGenericDocumentMethod() throws ProcessingException {
// Method header
TypeName classType = TypeName.get(mModel.getClassElement().asType());
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("toGenericDocument")
.addModifiers(Modifier.PUBLIC)
.returns(mHelper.getAppSearchClass("GenericDocument"))
.addAnnotation(Override.class)
.addParameter(classType, "document")
.addException(mHelper.getAppSearchExceptionClass());
// Construct a new GenericDocument.Builder with the namespace, id, and schema type
methodBuilder.addStatement("$T builder =\nnew $T<>($L, $L, SCHEMA_NAME)",
ParameterizedTypeName.get(
mHelper.getAppSearchClass("GenericDocument", "Builder"),
WildcardTypeName.subtypeOf(Object.class)),
mHelper.getAppSearchClass("GenericDocument", "Builder"),
createAppSearchFieldRead(
mModel.getSpecialFieldName(DocumentModel.SpecialField.NAMESPACE)),
createAppSearchFieldRead(
mModel.getSpecialFieldName(DocumentModel.SpecialField.ID)));
setSpecialFields(methodBuilder);
// Set properties
for (Map.Entry<String, VariableElement> entry : mModel.getPropertyFields().entrySet()) {
fieldToGenericDoc(methodBuilder, entry.getKey(), entry.getValue());
}
methodBuilder.addStatement("return builder.build()");
return methodBuilder.build();
}
/**
* Converts a field from a document class into a format suitable for one of the
* {@link androidx.appsearch.app.GenericDocument.Builder#setProperty} methods.
*/
private void fieldToGenericDoc(
@NonNull MethodSpec.Builder method,
@NonNull String fieldName,
@NonNull VariableElement property) throws ProcessingException {
// Scenario 1: field is a Collection
// 1a: CollectionForLoopAssign
// Collection contains boxed Long, Integer, Double, Float, Boolean or byte[].
// We have to pack it into a primitive array of type long[], double[], boolean[],
// or byte[][] by reading each element one-by-one and assigning it. The compiler takes
// care of unboxing.
//
// 1b: CollectionCallToArray
// Collection contains String or GenericDocument.
// We have to convert this into an array of String[] or GenericDocument[], but no
// conversion of the collection elements is needed. We can use Collection#toArray for
// this.
//
// 1c: CollectionForLoopCallToGenericDocument
// Collection contains a class which is annotated with @Document.
// We have to convert this into an array of GenericDocument[], by reading each element
// one-by-one and converting it through the standard conversion machinery.
//
// 1x: Collection contains any other kind of class. This unsupported and compilation
// fails.
// Note: Set<Byte[]>, Set<Byte>, and Set<Set<Byte>> are in this category. We don't
// support such conversions currently, but in principle they are possible and could
// be implemented.
// Scenario 2: field is an Array
// 2a: ArrayForLoopAssign
// Array is of type Long[], Integer[], int[], Double[], Float[], float[], Boolean[],
// or Byte[].
// We have to pack it into a primitive array of type long[], double[], boolean[] or
// byte[] by reading each element one-by-one and assigning it. The compiler takes care
// of unboxing.
//
// 2b: ArrayUseDirectly
// Array is of type String[], long[], double[], boolean[], byte[][] or
// GenericDocument[].
// We can directly use this field with no conversion.
//
// 2c: ArrayForLoopCallToGenericDocument
// Array is of a class which is annotated with @Document.
// We have to convert this into an array of GenericDocument[], by reading each element
// one-by-one and converting it through the standard conversion machinery.
//
// 2d: Array is of class byte[]. This is actually a single-valued field as byte arrays are
// natively supported by Icing, and is handled as Scenario 3a.
//
// 2x: Array is of any other kind of class. This unsupported and compilation fails.
// Note: Byte[][] is in this category. We don't support such conversions
// currently, but in principle they are possible and could be implemented.
// Scenario 3: Single valued fields
// 3a: FieldUseDirectlyWithNullCheck
// Field is of type String, Long, Integer, Double, Float, Boolean, byte[] or
// GenericDocument.
// We can use this field directly, after testing for null. The java compiler will box
// or unbox as needed.
//
// 3b: FieldUseDirectlyWithoutNullCheck
// Field is of type long, int, double, float, or boolean.
// We can use this field directly without testing for null.
//
// 3c: FieldCallToGenericDocument
// Field is of a class which is annotated with @Document.
// We have to convert this into a GenericDocument through the standard conversion
// machinery.
String propertyName = mModel.getPropertyName(property);
if (tryConvertFromCollection(method, fieldName, propertyName, property)) {
return;
}
if (tryConvertFromArray(method, fieldName, propertyName, property)) {
return;
}
convertFromField(method, fieldName, propertyName, property);
}
/**
* If the given field is a Collection, generates code to read it and convert it into a form
* suitable for GenericDocument and returns true. If the field is not a Collection, returns
* false.
*/
private boolean tryConvertFromCollection(
@NonNull MethodSpec.Builder method,
@NonNull String fieldName,
@NonNull String propertyName,
@NonNull VariableElement property) throws ProcessingException {
Types typeUtil = mEnv.getTypeUtils();
if (!typeUtil.isAssignable(typeUtil.erasure(property.asType()), mHelper.mCollectionType)) {
return false; // This is not a scenario 1 collection
}
// Copy the field into a local variable to make it easier to refer to it repeatedly.
CodeBlock.Builder body = CodeBlock.builder()
.addStatement(
"$T $NCopy = $L",
property.asType(),
fieldName,
createAppSearchFieldRead(fieldName));
List<? extends TypeMirror> genericTypes =
((DeclaredType) property.asType()).getTypeArguments();
TypeMirror propertyType = genericTypes.get(0);
if (!tryCollectionForLoopAssign(body, fieldName, propertyName, propertyType) // 1a
&& !tryCollectionCallToArray(body, fieldName, propertyName, propertyType) // 1b
&& !tryCollectionForLoopCallToGenericDocument(
body, fieldName, propertyName, propertyType)) { // 1c
// Scenario 1x
throw new ProcessingException(
"Unhandled out property type (1x): " + property.asType().toString(), property);
}
method.addCode(body.build());
return true;
}
// 1a: CollectionForLoopAssign
// Collection contains boxed Long, Integer, Double, Float, Boolean or byte[].
// We have to pack it into a primitive array of type long[], double[], boolean[],
// or byte[][] by reading each element one-by-one and assigning it. The compiler takes
// care of unboxing.
private boolean tryCollectionForLoopAssign(
@NonNull CodeBlock.Builder method,
@NonNull String fieldName,
@NonNull String propertyName,
@NonNull TypeMirror propertyType) {
Types typeUtil = mEnv.getTypeUtils();
CodeBlock.Builder body = CodeBlock.builder()
.add("if ($NCopy != null) {\n", fieldName).indent();
String setPropertyMethod;
if (typeUtil.isSameType(propertyType, mHelper.mLongBoxType)
|| typeUtil.isSameType(propertyType, mHelper.mIntegerBoxType)) {
setPropertyMethod = "setPropertyLong";
body.addStatement(
"long[] $NConv = new long[$NCopy.size()]", fieldName, fieldName);
} else if (typeUtil.isSameType(propertyType, mHelper.mDoubleBoxType)
|| typeUtil.isSameType(propertyType, mHelper.mFloatBoxType)) {
setPropertyMethod = "setPropertyDouble";
body.addStatement(
"double[] $NConv = new double[$NCopy.size()]", fieldName, fieldName);
} else if (typeUtil.isSameType(propertyType, mHelper.mBooleanBoxType)) {
setPropertyMethod = "setPropertyBoolean";
body.addStatement(
"boolean[] $NConv = new boolean[$NCopy.size()]", fieldName, fieldName);
} else if (typeUtil.isSameType(propertyType, mHelper.mBytePrimitiveArrayType)) {
setPropertyMethod = "setPropertyBytes";
body.addStatement(
"byte[][] $NConv = new byte[$NCopy.size()][]", fieldName, fieldName);
} else {
// This is not a type 1a collection.
return false;
}
// Iterate over each element of the collection, assigning it to the output array.
body.addStatement("int i = 0")
.add("for ($T item : $NCopy) {\n", propertyType, fieldName).indent()
.addStatement("$NConv[i++] = item", fieldName)
.unindent().add("}\n")
.addStatement("builder.$N($S, $NConv)", setPropertyMethod, propertyName, fieldName)
.unindent().add("}\n");
method.add(body.build());
return true;
}
// 1b: CollectionCallToArray
// Collection contains String. We have to convert this into an array of String[] or but no
// conversion of the collection elements is needed. We can use Collection#toArray for
// this.
private boolean tryCollectionCallToArray(
@NonNull CodeBlock.Builder method,
@NonNull String fieldName,
@NonNull String propertyName,
@NonNull TypeMirror propertyType) {
Types typeUtil = mEnv.getTypeUtils();
CodeBlock.Builder body = CodeBlock.builder()
.add("if ($NCopy != null) {\n", fieldName).indent();
if (typeUtil.isSameType(propertyType, mHelper.mStringType)) {
body.addStatement(
"String[] $NConv = $NCopy.toArray(new String[0])", fieldName, fieldName);
} else {
// This is not a type 1b collection.
return false;
}
body.addStatement(
"builder.setPropertyString($S, $NConv)", propertyName, fieldName)
.unindent().add("}\n");
method.add(body.build());
return true;
}
// 1c: CollectionForLoopCallToGenericDocument
// Collection contains a class which is annotated with @Document.
// We have to convert this into an array of GenericDocument[], by reading each element
// one-by-one and converting it through the standard conversion machinery.
private boolean tryCollectionForLoopCallToGenericDocument(
@NonNull CodeBlock.Builder method,
@NonNull String fieldName,
@NonNull String propertyName,
@NonNull TypeMirror propertyType) {
Types typeUtil = mEnv.getTypeUtils();
CodeBlock.Builder body = CodeBlock.builder()
.add("if ($NCopy != null) {\n", fieldName).indent();
Element element = typeUtil.asElement(propertyType);
if (element == null) {
// The propertyType is not an element, this is not a type 1c list.
return false;
}
try {
getDocumentAnnotation(element);
} catch (ProcessingException e) {
// The propertyType doesn't have @Document annotation, this is not a type 1c
// list.
return false;
}
body.addStatement(
"GenericDocument[] $NConv = new GenericDocument[$NCopy.size()]",
fieldName, fieldName);
body.addStatement("int i = 0");
body
.add("for ($T item : $NCopy) {\n", propertyType, fieldName).indent()
.addStatement(
"$NConv[i++] = $T.fromDocumentClass(item)",
fieldName, mHelper.getAppSearchClass("GenericDocument"))
.unindent().add("}\n");
body
.addStatement("builder.setPropertyDocument($S, $NConv)", propertyName, fieldName)
.unindent()
.add("}\n"); // if ($NCopy != null) {
method.add(body.build());
return true;
}
/**
* If the given field is an array, generates code to read it and convert it into a form suitable
* for GenericDocument and returns true. If the field is not an array, returns false.
*/
private boolean tryConvertFromArray(
@NonNull MethodSpec.Builder method,
@NonNull String fieldName,
@NonNull String propertyName,
@NonNull VariableElement property) throws ProcessingException {
Types typeUtil = mEnv.getTypeUtils();
if (property.asType().getKind() != TypeKind.ARRAY
// Byte arrays have a native representation in Icing, so they are not considered a
// "repeated" type
|| typeUtil.isSameType(property.asType(), mHelper.mBytePrimitiveArrayType)) {
return false; // This is not a scenario 2 array
}
// Copy the field into a local variable to make it easier to refer to it repeatedly.
CodeBlock.Builder body = CodeBlock.builder()
.addStatement(
"$T $NCopy = $L",
property.asType(),
fieldName,
createAppSearchFieldRead(fieldName));
TypeMirror propertyType = ((ArrayType) property.asType()).getComponentType();
if (!tryArrayForLoopAssign(body, fieldName, propertyName, propertyType) // 2a
&& !tryArrayUseDirectly(body, fieldName, propertyName, propertyType) // 2b
&& !tryArrayForLoopCallToGenericDocument(
body, fieldName, propertyName, propertyType)) { // 2c
// Scenario 2x
throw new ProcessingException(
"Unhandled out property type (2x): " + property.asType().toString(), property);
}
method.addCode(body.build());
return true;
}
// 2a: ArrayForLoopAssign
// Array is of type Long[], Integer[], int[], Double[], Float[], float[], Boolean[],
// or Byte[].
// We have to pack it into a primitive array of type long[], double[], boolean[] or
// byte[] by reading each element one-by-one and assigning it. The compiler takes care
// of unboxing.
private boolean tryArrayForLoopAssign(
@NonNull CodeBlock.Builder method,
@NonNull String fieldName,
@NonNull String propertyName,
@NonNull TypeMirror propertyType) {
Types typeUtil = mEnv.getTypeUtils();
CodeBlock.Builder body = CodeBlock.builder()
.add("if ($NCopy != null) {\n", fieldName).indent();
String setPropertyMethod;
if (typeUtil.isSameType(propertyType, mHelper.mLongBoxType)
|| typeUtil.isSameType(propertyType, mHelper.mIntegerBoxType)
|| typeUtil.isSameType(propertyType, mHelper.mIntPrimitiveType)) {
setPropertyMethod = "setPropertyLong";
body.addStatement(
"long[] $NConv = new long[$NCopy.length]", fieldName, fieldName);
} else if (typeUtil.isSameType(propertyType, mHelper.mDoubleBoxType)
|| typeUtil.isSameType(propertyType, mHelper.mFloatBoxType)
|| typeUtil.isSameType(propertyType, mHelper.mFloatPrimitiveType)) {
setPropertyMethod = "setPropertyDouble";
body.addStatement(
"double[] $NConv = new double[$NCopy.length]", fieldName, fieldName);
} else if (typeUtil.isSameType(propertyType, mHelper.mBooleanBoxType)) {
setPropertyMethod = "setPropertyBoolean";
body.addStatement(
"boolean[] $NConv = new boolean[$NCopy.length]", fieldName, fieldName);
} else if (typeUtil.isSameType(propertyType, mHelper.mByteBoxType)) {
setPropertyMethod = "setPropertyBytes";
body.addStatement(
"byte[] $NConv = new byte[$NCopy.length]", fieldName, fieldName);
} else {
// This is not a type 2a array.
return false;
}
// Iterate over each element of the array, assigning it to the output array.
body.add("for (int i = 0 ; i < $NCopy.length ; i++) {\n", fieldName)
.indent()
.addStatement("$NConv[i] = $NCopy[i]", fieldName, fieldName)
.unindent().add("}\n")
.addStatement("builder.$N($S, $NConv)", setPropertyMethod, propertyName, fieldName)
.unindent().add("}\n");
method.add(body.build());
return true;
}
// 2b: ArrayUseDirectly
// Array is of type String[], long[], double[], boolean[], byte[][].
// We can directly use this field with no conversion.
private boolean tryArrayUseDirectly(
@NonNull CodeBlock.Builder method,
@NonNull String fieldName,
@NonNull String propertyName,
@NonNull TypeMirror propertyType) {
Types typeUtil = mEnv.getTypeUtils();
CodeBlock.Builder body = CodeBlock.builder()
.add("if ($NCopy != null) {\n", fieldName).indent();
String setPropertyMethod;
if (typeUtil.isSameType(propertyType, mHelper.mStringType)) {
setPropertyMethod = "setPropertyString";
} else if (typeUtil.isSameType(propertyType, mHelper.mLongPrimitiveType)) {
setPropertyMethod = "setPropertyLong";
} else if (typeUtil.isSameType(propertyType, mHelper.mDoublePrimitiveType)) {
setPropertyMethod = "setPropertyDouble";
} else if (typeUtil.isSameType(propertyType, mHelper.mBooleanPrimitiveType)) {
setPropertyMethod = "setPropertyBoolean";
} else if (typeUtil.isSameType(propertyType, mHelper.mBytePrimitiveArrayType)) {
setPropertyMethod = "setPropertyBytes";
} else {
// This is not a type 2b array.
return false;
}
body.addStatement(
"builder.$N($S, $NCopy)", setPropertyMethod, propertyName, fieldName)
.unindent().add("}\n");
method.add(body.build());
return true;
}
// 2c: ArrayForLoopCallToGenericDocument
// Array is of a class which is annotated with @Document.
// We have to convert this into an array of GenericDocument[], by reading each element
// one-by-one and converting it through the standard conversion machinery.
private boolean tryArrayForLoopCallToGenericDocument(
@NonNull CodeBlock.Builder method,
@NonNull String fieldName,
@NonNull String propertyName,
@NonNull TypeMirror propertyType) {
Types typeUtil = mEnv.getTypeUtils();
CodeBlock.Builder body = CodeBlock.builder()
.add("if ($NCopy != null) {\n", fieldName).indent();
Element element = typeUtil.asElement(propertyType);
if (element == null) {
// The propertyType is not an element, this is not a type 1c list.
return false;
}
try {
getDocumentAnnotation(element);
} catch (ProcessingException e) {
// The propertyType doesn't have @Document annotation, this is not a type 1c
// list.
return false;
}
body.addStatement(
"GenericDocument[] $NConv = new GenericDocument[$NCopy.length]",
fieldName, fieldName);
body
.add("for (int i = 0; i < $NConv.length; i++) {\n", fieldName).indent()
.addStatement(
"$NConv[i] = $T.fromDocumentClass($NCopy[i])",
fieldName, mHelper.getAppSearchClass("GenericDocument"), fieldName)
.unindent().add("}\n");
body.addStatement("builder.setPropertyDocument($S, $NConv)", propertyName, fieldName)
.unindent().add("}\n"); // if ($NCopy != null) {
method.add(body.build());
return true;
}
/**
* Given a field which is a single element (non-collection), generates code to read it and
* convert it into a form suitable for GenericDocument.
*/
private void convertFromField(
@NonNull MethodSpec.Builder method,
@NonNull String fieldName,
@NonNull String propertyName,
@NonNull VariableElement property) throws ProcessingException {
// TODO(b/156296904): Handle scenario 3c (FieldCallToGenericDocument)
CodeBlock.Builder body = CodeBlock.builder();
if (!tryFieldUseDirectlyWithNullCheck(
body, fieldName, propertyName, property.asType()) // 3a
&& !tryFieldUseDirectlyWithoutNullCheck(
body, fieldName, propertyName, property.asType()) // 3b
&& !tryFieldCallToGenericDocument(
body, fieldName, propertyName, property.asType())) { // 3c
throw new ProcessingException("Unhandled property type.", property);
}
method.addCode(body.build());
}
// 3a: FieldUseDirectlyWithNullCheck
// Field is of type String, Long, Integer, Double, Float, Boolean, byte[].
// We can use this field directly, after testing for null. The java compiler will box
// or unbox as needed.
private boolean tryFieldUseDirectlyWithNullCheck(
@NonNull CodeBlock.Builder method,
@NonNull String fieldName,
@NonNull String propertyName,
@NonNull TypeMirror propertyType) {
Types typeUtil = mEnv.getTypeUtils();
// Copy the field into a local variable to make it easier to refer to it repeatedly.
CodeBlock.Builder body = CodeBlock.builder()
.addStatement(
"$T $NCopy = $L",
propertyType,
fieldName,
createAppSearchFieldRead(fieldName))
.add("if ($NCopy != null) {\n", fieldName).indent();
String setPropertyMethod;
if (typeUtil.isSameType(propertyType, mHelper.mStringType)) {
setPropertyMethod = "setPropertyString";
} else if (typeUtil.isSameType(propertyType, mHelper.mLongBoxType)
|| typeUtil.isSameType(propertyType, mHelper.mIntegerBoxType)) {
setPropertyMethod = "setPropertyLong";
} else if (typeUtil.isSameType(propertyType, mHelper.mDoubleBoxType)
|| typeUtil.isSameType(propertyType, mHelper.mFloatBoxType)) {
setPropertyMethod = "setPropertyDouble";
} else if (typeUtil.isSameType(propertyType, mHelper.mBooleanBoxType)) {
setPropertyMethod = "setPropertyBoolean";
} else if (typeUtil.isSameType(propertyType, mHelper.mBytePrimitiveArrayType)) {
setPropertyMethod = "setPropertyBytes";
} else {
// This is not a type 3a field
return false;
}
body.addStatement(
"builder.$N($S, $NCopy)", setPropertyMethod, propertyName, fieldName)
.unindent().add("}\n");
method.add(body.build());
return true;
}
// 3b: FieldUseDirectlyWithoutNullCheck
// Field is of type long, int, double, float, or boolean.
// We can use this field directly without testing for null.
private boolean tryFieldUseDirectlyWithoutNullCheck(
@NonNull CodeBlock.Builder method,
@NonNull String fieldName,
@NonNull String propertyName,
@NonNull TypeMirror propertyType) {
Types typeUtil = mEnv.getTypeUtils();
String setPropertyMethod;
if (typeUtil.isSameType(propertyType, mHelper.mLongPrimitiveType)
|| typeUtil.isSameType(propertyType, mHelper.mIntPrimitiveType)) {
setPropertyMethod = "setPropertyLong";
} else if (typeUtil.isSameType(propertyType, mHelper.mDoublePrimitiveType)
|| typeUtil.isSameType(propertyType, mHelper.mFloatPrimitiveType)) {
setPropertyMethod = "setPropertyDouble";
} else if (typeUtil.isSameType(propertyType, mHelper.mBooleanPrimitiveType)) {
setPropertyMethod = "setPropertyBoolean";
} else {
// This is not a type 3b field
return false;
}
method.addStatement(
"builder.$N($S, $L)",
setPropertyMethod,
propertyName,
createAppSearchFieldRead(fieldName));
return true;
}
// 3c: FieldCallToGenericDocument
// Field is of a class which is annotated with @Document.
// We have to convert this into a GenericDocument through the standard conversion
// machinery.
private boolean tryFieldCallToGenericDocument(
@NonNull CodeBlock.Builder method,
@NonNull String fieldName,
@NonNull String propertyName,
@NonNull TypeMirror propertyType) {
Types typeUtil = mEnv.getTypeUtils();
Element element = typeUtil.asElement(propertyType);
if (element == null) {
// The propertyType is not an element, this is not a type 3c field.
return false;
}
try {
getDocumentAnnotation(element);
} catch (ProcessingException e) {
// The propertyType doesn't have @Document annotation, this is not a type 3c
// field.
return false;
}
method.addStatement(
"$T $NCopy = $L", propertyType, fieldName, createAppSearchFieldRead(fieldName));
method.add("if ($NCopy != null) {\n", fieldName).indent();
method
.addStatement(
"GenericDocument $NConv = $T.fromDocumentClass($NCopy)",
fieldName, mHelper.getAppSearchClass("GenericDocument"), fieldName)
.addStatement("builder.setPropertyDocument($S, $NConv)", propertyName, fieldName);
method.unindent().add("}\n");
return true;
}
private void setSpecialFields(MethodSpec.Builder method) {
for (DocumentModel.SpecialField specialField :
DocumentModel.SpecialField.values()) {
String fieldName = mModel.getSpecialFieldName(specialField);
if (fieldName == null) {
continue; // The document class doesn't have this field, so no need to set it.
}
switch (specialField) {
case ID:
break; // Always provided to builder constructor; cannot be set separately.
case NAMESPACE:
break; // Always provided to builder constructor; cannot be set separately.
case CREATION_TIMESTAMP_MILLIS:
method.addStatement(
"builder.setCreationTimestampMillis($L)",
createAppSearchFieldRead(fieldName));
break;
case TTL_MILLIS:
method.addStatement(
"builder.setTtlMillis($L)", createAppSearchFieldRead(fieldName));
break;
case SCORE:
method.addStatement(
"builder.setScore($L)", createAppSearchFieldRead(fieldName));
break;
}
}
}
private CodeBlock createAppSearchFieldRead(@NonNull String fieldName) {
switch (Objects.requireNonNull(mModel.getFieldReadKind(fieldName))) {
case FIELD:
return CodeBlock.of("document.$N", fieldName);
case GETTER:
String getter = mModel.getGetterForField(fieldName).getSimpleName().toString();
return CodeBlock.of("document.$N()", getter);
}
return null;
}
}