FromGenericDocumentCodeGenerator.java

/*
 * 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 androidx.annotation.Nullable;

import com.squareup.javapoet.ClassName;
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 java.util.ArrayList;
import java.util.Arrays;
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.ElementKind;
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 a {@code androidx.appsearch.app.GenericDocument} to
 * an instance of a class annotated with {@code androidx.appsearch.annotation.Document}.
 */
class FromGenericDocumentCodeGenerator {
    private final ProcessingEnvironment mEnv;
    private final IntrospectionHelper mHelper;
    private final DocumentModel mModel;

    private FromGenericDocumentCodeGenerator(
            @NonNull ProcessingEnvironment env, @NonNull DocumentModel model) {
        mEnv = env;
        mHelper = new IntrospectionHelper(env);
        mModel = model;
    }

    public static void generate(
            @NonNull ProcessingEnvironment env,
            @NonNull DocumentModel model,
            @NonNull TypeSpec.Builder classBuilder) throws ProcessingException {
        new FromGenericDocumentCodeGenerator(env, model).generate(classBuilder);
    }

    private void generate(TypeSpec.Builder classBuilder) throws ProcessingException {
        classBuilder.addMethod(createFromGenericDocumentMethod());
    }

    private MethodSpec createFromGenericDocumentMethod() throws ProcessingException {
        // Method header
        TypeName classType = TypeName.get(mModel.getClassElement().asType());
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("fromGenericDocument")
                .addModifiers(Modifier.PUBLIC)
                .returns(classType)
                .addAnnotation(Override.class)
                .addParameter(mHelper.getAppSearchClass("GenericDocument"), "genericDoc")
                .addException(mHelper.getAppSearchExceptionClass());

        unpackSpecialFields(methodBuilder);

        // Unpack properties from the GenericDocument into the format desired by the document class
        for (Map.Entry<String, VariableElement> entry : mModel.getPropertyFields().entrySet()) {
            fieldFromGenericDoc(methodBuilder, entry.getKey(), entry.getValue());
        }

        // Create an instance of the document class via the chosen create method.
        if (mModel.getChosenCreationMethod().getKind() == ElementKind.CONSTRUCTOR) {
            methodBuilder.addStatement(
                    "$T document = new $T($L)", classType, classType, getCreationMethodParams());
        } else {
            methodBuilder.addStatement(
                    "$T document = $T.$L($L)", classType, classType,
                    mModel.getChosenCreationMethod().getSimpleName().toString(),
                    getCreationMethodParams());
        }

        // Assign all fields which weren't set in the constructor
        for (String field : mModel.getAllFields().keySet()) {
            CodeBlock fieldWrite = createAppSearchFieldWrite(field);
            if (fieldWrite != null) {
                methodBuilder.addStatement(fieldWrite);
            }
        }

        methodBuilder.addStatement("return document");
        return methodBuilder.build();
    }

    /**
     * Converts a field from a {@code androidx.appsearch.app.GenericDocument} into a format suitable
     * for the document class.
     */
    private void fieldFromGenericDoc(
            @NonNull MethodSpec.Builder builder,
            @NonNull String fieldName,
            @NonNull VariableElement property) throws ProcessingException {
        // Scenario 1: field is assignable from List
        //   1a: ListForLoopAssign
        //       List contains boxed Long, Integer, Double, Float, Boolean or byte[]. We have to
        //       unpack it from 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: ListCallArraysAsList
        //       List contains String. We have to convert this from an array of String[], but no
        //       conversion of the collection elements is needed. We can use Arrays#asList for this.
        //
        //   1c: ListForLoopCallFromGenericDocument
        //       List contains a class which is annotated with @Document.
        //       We have to convert this from an array of GenericDocument[], by reading each element
        //       one-by-one and converting it through the standard conversion machinery.
        //
        //   1x: List contains any other kind of class. This unsupported and compilation fails.
        //       Note: List<Byte[]>, List<Byte>, List<List<Byte>>, Set<String> 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 unpack it from 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[][].
        //       We can directly use this field with no conversion.
        //
        //   2c: ArrayForLoopCallFromGenericDocument
        //       Array is of a class which is annotated with @Document.
        //       We have to convert this from 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[].
        //       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. Since we cannot assign null, we must assign the
        //       default value if the field is not specified. The java compiler will box or unbox as
        //       needed
        //
        //   3c: FieldCallFromGenericDocument
        //       Field is of a class which is annotated with @Document.
        //       We have to convert this from a GenericDocument through the standard conversion
        //       machinery.

        String propertyName = mModel.getPropertyName(property);
        if (tryConvertToList(builder, fieldName, propertyName, property)) {
            return;
        }
        if (tryConvertToArray(builder, fieldName, propertyName, property)) {
            return;
        }
        convertToField(builder, fieldName, propertyName, property);
    }

    /**
     * If the given field is a List, generates code to read it from a repeated GenericDocument
     * property and returns true. If the field is not a List, returns false.
     */
    private boolean tryConvertToList(
            @NonNull MethodSpec.Builder method,
            @NonNull String fieldName,
            @NonNull String propertyName,
            @NonNull VariableElement property) throws ProcessingException {
        Types typeUtil = mEnv.getTypeUtils();
        if (!typeUtil.isAssignable(mHelper.mListType, typeUtil.erasure(property.asType()))) {
            return false;  // This is not a scenario 1 list
        }

        List<? extends TypeMirror> genericTypes =
                ((DeclaredType) property.asType()).getTypeArguments();
        TypeMirror propertyType = genericTypes.get(0);
        ParameterizedTypeName listTypeName = ParameterizedTypeName.get(ClassName.get(List.class),
                TypeName.get(propertyType));

        CodeBlock.Builder builder = CodeBlock.builder();
        if (!tryListForLoopAssign(builder, fieldName, propertyName, propertyType, listTypeName)// 1a
                && !tryListCallArraysAsList(
                builder, fieldName, propertyName, propertyType, listTypeName)          // 1b
                && !tryListForLoopCallFromGenericDocument(
                builder, fieldName, propertyName, propertyType, listTypeName)) {       // 1c
            // Scenario 1x
            throw new ProcessingException(
                    "Unhandled in property type (1x): " + property.asType().toString(), property);
        }

        method.addCode(builder.build());
        return true;
    }

    //   1a: ListForLoopAssign
    //       List contains boxed Long, Integer, Double, Float, Boolean or byte[]. We have to
    //       unpack it from 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 tryListForLoopAssign(
            @NonNull CodeBlock.Builder method,
            @NonNull String fieldName,
            @NonNull String propertyName,
            @NonNull TypeMirror propertyType,
            @NonNull ParameterizedTypeName listTypeName) {
        Types typeUtil = mEnv.getTypeUtils();
        CodeBlock.Builder body = CodeBlock.builder();

        // Copy the property to refer to it more easily.
        if (typeUtil.isSameType(propertyType, mHelper.mLongBoxType)
                || typeUtil.isSameType(propertyType, mHelper.mIntegerBoxType)) {
            body.addStatement(
                    "long[] $NCopy = genericDoc.getPropertyLongArray($S)",
                    fieldName, propertyName);

        } else if (typeUtil.isSameType(propertyType, mHelper.mDoubleBoxType)
                || typeUtil.isSameType(propertyType, mHelper.mFloatBoxType)) {
            body.addStatement(
                    "double[] $NCopy = genericDoc.getPropertyDoubleArray($S)",
                    fieldName, propertyName);

        } else if (typeUtil.isSameType(propertyType, mHelper.mBooleanBoxType)) {
            body.addStatement(
                    "boolean[] $NCopy = genericDoc.getPropertyBooleanArray($S)",
                    fieldName, propertyName);

        } else if (typeUtil.isSameType(propertyType, mHelper.mBytePrimitiveArrayType)) {
            body.addStatement(
                    "byte[][] $NCopy = genericDoc.getPropertyBytesArray($S)",
                    fieldName, propertyName);

        } else {
            // This is not a type 1a list.
            return false;
        }

        // Create the destination list
        body.addStatement(
                "$T $NConv = null", listTypeName, fieldName);

        // If not null, iterate and assign
        body
                .add("if ($NCopy != null) {\n", fieldName).indent()
                .addStatement(
                        "$NConv = new $T<>($NCopy.length)", fieldName, ArrayList.class, fieldName)
                .add("for (int i = 0; i < $NCopy.length; i++) {\n", fieldName).indent();

        if (typeUtil.isSameType(propertyType, mHelper.mIntegerBoxType)) {
            body.addStatement("$NConv.add((int) $NCopy[i])", fieldName, fieldName);
        } else if (typeUtil.isSameType(propertyType, mHelper.mFloatBoxType)) {
            body.addStatement("$NConv.add((float) $NCopy[i])", fieldName, fieldName);
        } else {
            body.addStatement("$NConv.add($NCopy[i])", fieldName, fieldName);
        }

        body
                .unindent().add("}\n")  // for loop
                .unindent().add("}\n"); // if ($NCopy != null)
        method.add(body.build());
        return true;
    }

    //   1b: ListCallArraysAsList
    //       List contains String. We have to convert this from an array of String[], but no
    //       conversion of the collection elements is needed. We can use Arrays#asList for this.
    private boolean tryListCallArraysAsList(
            @NonNull CodeBlock.Builder method,
            @NonNull String fieldName,
            @NonNull String propertyName,
            @NonNull TypeMirror propertyType,
            @NonNull ParameterizedTypeName listTypeName) {
        Types typeUtil = mEnv.getTypeUtils();
        CodeBlock.Builder body = CodeBlock.builder();

        if (typeUtil.isSameType(propertyType, mHelper.mStringType)) {
            body.addStatement(
                    "String[] $NCopy = genericDoc.getPropertyStringArray($S)",
                    fieldName, propertyName);

        } else {
            // This is not a type 1b list.
            return false;
        }

        // Create the destination list
        body.addStatement("$T $NConv = null", listTypeName, fieldName);

        // If not null, iterate and assign
        body
                .add("if ($NCopy != null) {\n", fieldName).indent()
                .addStatement("$NConv = $T.asList($NCopy)", fieldName, Arrays.class, fieldName)
                .unindent().add("}\n");

        method.add(body.build());
        return true;
    }

    //   1c: ListForLoopCallFromGenericDocument
    //       List contains a class which is annotated with @Document.
    //       We have to convert this from an array of GenericDocument[], by reading each element
    //       one-by-one and converting it through the standard conversion machinery.
    private boolean tryListForLoopCallFromGenericDocument(
            @NonNull CodeBlock.Builder method,
            @NonNull String fieldName,
            @NonNull String propertyName,
            @NonNull TypeMirror propertyType,
            @NonNull ParameterizedTypeName listTypeName) {
        Types typeUtil = mEnv.getTypeUtils();
        CodeBlock.Builder body = CodeBlock.builder();

        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[] $NCopy = genericDoc.getPropertyDocumentArray($S)",
                fieldName, propertyName);

        // Create the destination list
        body.addStatement("$T $NConv = null", listTypeName, fieldName);

        // If not null, iterate and assign
        body.add("if ($NCopy != null) {\n", fieldName).indent();
        body.addStatement(
                "$NConv = new $T<>($NCopy.length)", fieldName, ArrayList.class, fieldName);

        body
                .add("for (int i = 0; i < $NCopy.length; i++) {\n", fieldName).indent()
                .addStatement(
                        "$NConv.add($NCopy[i].toDocumentClass($T.class))",
                        fieldName, fieldName, propertyType)
                .unindent().add("}\n");

        body.unindent().add("}\n");  //  if ($NCopy != null) {
        method.add(body.build());

        return true;
    }

    /**
     * If the given field is an array, generates code to read it from a repeated GenericDocument
     * property and returns true. If the field is not an array, returns false.
     */
    private boolean tryConvertToArray(
            @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
        }

        TypeMirror propertyType = ((ArrayType) property.asType()).getComponentType();

        CodeBlock.Builder builder = CodeBlock.builder();
        if (!tryArrayForLoopAssign(builder, fieldName, propertyName, propertyType)             // 2a
                && !tryArrayUseDirectly(builder, fieldName, propertyName, propertyType)        // 2b
                && !tryArrayForLoopCallFromGenericDocument(
                builder, fieldName, propertyName, propertyType)) {                     // 2c
            // Scenario 2x
            throw new ProcessingException(
                    "Unhandled in property type (2x): " + property.asType().toString(), property);
        }

        method.addCode(builder.build());
        return true;
    }

    //   2a: ArrayForLoopAssign
    //       Array is of type Long[], Integer[], int[], Double[], Float[], float[], Boolean[],
    //       or Byte[].
    //       We have to unpack it from 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();

        // Copy the property to refer to it more easily.
        if (typeUtil.isSameType(propertyType, mHelper.mLongBoxType)
                || typeUtil.isSameType(propertyType, mHelper.mIntegerBoxType)
                || typeUtil.isSameType(propertyType, mHelper.mIntPrimitiveType)) {
            body.addStatement(
                    "long[] $NCopy = genericDoc.getPropertyLongArray($S)",
                    fieldName, propertyName);

        } else if (typeUtil.isSameType(propertyType, mHelper.mDoubleBoxType)
                || typeUtil.isSameType(propertyType, mHelper.mFloatBoxType)
                || typeUtil.isSameType(propertyType, mHelper.mFloatPrimitiveType)) {
            body.addStatement(
                    "double[] $NCopy = genericDoc.getPropertyDoubleArray($S)",
                    fieldName, propertyName);

        } else if (typeUtil.isSameType(propertyType, mHelper.mBooleanBoxType)) {
            body.addStatement(
                    "boolean[] $NCopy = genericDoc.getPropertyBooleanArray($S)",
                    fieldName, propertyName);

        } else if (typeUtil.isSameType(propertyType, mHelper.mByteBoxType)) {
            body.addStatement(
                    "byte[] $NCopy = genericDoc.getPropertyBytes($S)",
                    fieldName, propertyName);

        } else {
            // This is not a type 2a array.
            return false;
        }

        // Create the destination array
        body.addStatement("$T[] $NConv = null", propertyType, fieldName);

        // If not null, iterate and assign
        body
                .add("if ($NCopy != null) {\n", fieldName).indent()
                .addStatement("$NConv = new $T[$NCopy.length]", fieldName, propertyType, fieldName)
                .add("for (int i = 0; i < $NCopy.length; i++) {\n", fieldName).indent();

        if (typeUtil.isSameType(propertyType, mHelper.mIntegerBoxType)
                || typeUtil.isSameType(propertyType, mHelper.mIntPrimitiveType)) {
            body.addStatement("$NConv[i] = (int) $NCopy[i]", fieldName, fieldName);
        } else if (typeUtil.isSameType(propertyType, mHelper.mFloatBoxType)
                || typeUtil.isSameType(propertyType, mHelper.mFloatPrimitiveType)) {
            body.addStatement("$NConv[i] = (float) $NCopy[i]", fieldName, fieldName);
        } else {
            body.addStatement("$NConv[i] = $NCopy[i]", fieldName, fieldName);
        }

        body
                .unindent().add("}\n")  // for loop
                .unindent().add("}\n"); // if ($NCopy != null)

        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();

        // Copy the field to a local variable to make it easier to refer to repeatedly
        if (typeUtil.isSameType(propertyType, mHelper.mStringType)) {
            body.addStatement(
                    "String[] $NConv = genericDoc.getPropertyStringArray($S)",
                    fieldName, propertyName);

        } else if (typeUtil.isSameType(propertyType, mHelper.mLongPrimitiveType)) {
            body.addStatement(
                    "long[] $NConv = genericDoc.getPropertyLongArray($S)",
                    fieldName, propertyName);

        } else if (typeUtil.isSameType(propertyType, mHelper.mDoublePrimitiveType)) {
            body.addStatement(
                    "double[] $NConv = genericDoc.getPropertyDoubleArray($S)",
                    fieldName, propertyName);

        } else if (typeUtil.isSameType(propertyType, mHelper.mBooleanPrimitiveType)) {
            body.addStatement(
                    "boolean[] $NConv = genericDoc.getPropertyBooleanArray($S)",
                    fieldName, propertyName);

        } else if (typeUtil.isSameType(propertyType, mHelper.mBytePrimitiveArrayType)) {
            body.addStatement(
                    "byte[][] $NConv = genericDoc.getPropertyBytesArray($S)",
                    fieldName, propertyName);

        } else {
            // This is not a type 2b array.
            return false;
        }

        method.add(body.build());
        return true;
    }

    //   2c: ArrayForLoopCallFromGenericDocument
    //       Array is of a class which is annotated with @Document.
    //       We have to convert this from an array of GenericDocument[], by reading each element
    //       one-by-one and converting it through the standard conversion machinery.
    private boolean tryArrayForLoopCallFromGenericDocument(
            @NonNull CodeBlock.Builder method,
            @NonNull String fieldName,
            @NonNull String propertyName,
            @NonNull TypeMirror propertyType) {
        Types typeUtil = mEnv.getTypeUtils();
        CodeBlock.Builder body = CodeBlock.builder();

        Element element = typeUtil.asElement(propertyType);
        if (element == null) {
            // The propertyType is not an element, this is not a type 2c array.
            return false;
        }
        try {
            getDocumentAnnotation(element);
        } catch (ProcessingException e) {
            // The propertyType doesn't have @Document annotation, this is not a type 2c
            // array.
            return false;
        }

        // Copy the field to a local variable to make it easier to refer to repeatedly

        body.addStatement(
                "GenericDocument[] $NCopy = genericDoc.getPropertyDocumentArray($S)",
                fieldName, propertyName);

        // Create the destination array
        body.addStatement(
                "$T[] $NConv = null", propertyType, fieldName);

        // If not null, iterate and assign
        body.add("if ($NCopy != null) {\n", fieldName).indent();
        body.addStatement("$NConv = new $T[$NCopy.length]", fieldName, propertyType, fieldName);

        body
                .add("for (int i = 0; i < $NCopy.length; i++) {\n", fieldName).indent()
                .addStatement(
                        "$NConv[i] = $NCopy[i].toDocumentClass($T.class)",
                        fieldName, fieldName, propertyType)
                .unindent().add("}\n");

        body.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 from a
     * repeated GenericDocument property.
     */
    private void convertToField(
            @NonNull MethodSpec.Builder method,
            @NonNull String fieldName,
            @NonNull String propertyName,
            @NonNull VariableElement property) throws ProcessingException {
        // TODO(b/156296904): Handle scenario 3c (FieldCallToGenericDocument)
        CodeBlock.Builder builder = CodeBlock.builder();
        if (!tryFieldUseDirectlyWithNullCheck(
                builder, fieldName, propertyName, property.asType())  // 3a
                && !tryFieldUseDirectlyWithoutNullCheck(
                builder, fieldName, propertyName, property.asType()) // 3b
                && !tryFieldCallFromGenericDocument(
                builder, fieldName, propertyName, property.asType())) {   // 3c
            throw new ProcessingException("Unhandled property type.", property);
        }
        method.addCode(builder.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.
        // Even though we want a single field, we can't use genericDoc.getPropertyString() (and
        // relatives) because we need to be able to check for null.
        CodeBlock.Builder body = CodeBlock.builder();
        if (typeUtil.isSameType(propertyType, mHelper.mStringType)) {
            body.addStatement(
                    "String[] $NCopy = genericDoc.getPropertyStringArray($S)",
                    fieldName, propertyName);

        } else if (typeUtil.isSameType(propertyType, mHelper.mLongBoxType)
                || typeUtil.isSameType(propertyType, mHelper.mIntegerBoxType)) {
            body.addStatement(
                    "long[] $NCopy = genericDoc.getPropertyLongArray($S)",
                    fieldName, propertyName);

        } else if (typeUtil.isSameType(propertyType, mHelper.mDoubleBoxType)
                || typeUtil.isSameType(propertyType, mHelper.mFloatBoxType)) {
            body.addStatement(
                    "double[] $NCopy = genericDoc.getPropertyDoubleArray($S)",
                    fieldName, propertyName);

        } else if (typeUtil.isSameType(propertyType, mHelper.mBooleanBoxType)) {
            body.addStatement(
                    "boolean[] $NCopy = genericDoc.getPropertyBooleanArray($S)",
                    fieldName, propertyName);

        } else if (typeUtil.isSameType(propertyType, mHelper.mBytePrimitiveArrayType)) {
            body.addStatement(
                    "byte[][] $NCopy = genericDoc.getPropertyBytesArray($S)",
                    fieldName, propertyName);

        } else {
            // This is not a type 3a field
            return false;
        }

        // Create the destination field
        body.addStatement("$T $NConv = null", propertyType, fieldName);

        // If not null, assign
        body
                .add("if ($NCopy != null && $NCopy.length != 0) {\n", fieldName, fieldName)
                .indent();

        if (typeUtil.isSameType(propertyType, mHelper.mIntegerBoxType)) {
            body.addStatement("$NConv = (int) $NCopy[0]", fieldName, fieldName);
        } else if (typeUtil.isSameType(propertyType, mHelper.mFloatBoxType)) {
            body.addStatement("$NConv = (float) $NCopy[0]", fieldName, fieldName);
        } else {
            body.addStatement("$NConv = $NCopy[0]", fieldName, fieldName);
        }

        body.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. Since we cannot assign null, we must assign the
    //       default value if the field is not specified. The java compiler will box or unbox as
    //       needed
    private boolean tryFieldUseDirectlyWithoutNullCheck(
            @NonNull CodeBlock.Builder method,
            @NonNull String fieldName,
            @NonNull String propertyName,
            @NonNull TypeMirror propertyType) {
        Types typeUtil = mEnv.getTypeUtils();
        if (typeUtil.isSameType(propertyType, mHelper.mLongPrimitiveType)) {
            method.addStatement(
                    "$T $NConv = genericDoc.getPropertyLong($S)",
                    propertyType, fieldName, propertyName);

        } else if (typeUtil.isSameType(propertyType, mHelper.mIntPrimitiveType)) {
            method.addStatement(
                    "$T $NConv = (int) genericDoc.getPropertyLong($S)",
                    propertyType, fieldName, propertyName);

        } else if (typeUtil.isSameType(propertyType, mHelper.mDoublePrimitiveType)) {
            method.addStatement(
                    "$T $NConv = genericDoc.getPropertyDouble($S)",
                    propertyType, fieldName, propertyName);

        } else if (typeUtil.isSameType(propertyType, mHelper.mFloatPrimitiveType)) {
            method.addStatement(
                    "$T $NConv = (float) genericDoc.getPropertyDouble($S)",
                    propertyType, fieldName, propertyName);

        } else if (typeUtil.isSameType(propertyType, mHelper.mBooleanPrimitiveType)) {
            method.addStatement(
                    "$T $NConv = genericDoc.getPropertyBoolean($S)",
                    propertyType, fieldName, propertyName);

        } else {
            // This is not a type 3b field
            return false;
        }

        return true;
    }

    //   3c: FieldCallFromGenericDocument
    //       Field is of a class which is annotated with @Document.
    //       We have to convert this from a GenericDocument through the standard conversion
    //       machinery.
    private boolean tryFieldCallFromGenericDocument(
            @NonNull CodeBlock.Builder method,
            @NonNull String fieldName,
            @NonNull String propertyName,
            @NonNull TypeMirror propertyType) {
        Types typeUtil = mEnv.getTypeUtils();
        CodeBlock.Builder body = CodeBlock.builder();

        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;
        }

        body.addStatement("GenericDocument $NCopy = genericDoc.getPropertyDocument($S)",
                fieldName, propertyName);

        body.addStatement("$T $NConv = null", propertyType, fieldName);
        // If not null, assign
        body
                .add("if ($NCopy != null) {\n", fieldName).indent()
                .addStatement(
                        "$NConv = $NCopy.toDocumentClass($T.class)",
                        fieldName, fieldName, propertyType)
                .unindent().add("}\n");

        method.add(body.build());

        return true;
    }

    private CodeBlock getCreationMethodParams() {
        CodeBlock.Builder builder = CodeBlock.builder();
        List<String> params = mModel.getChosenCreationMethodParams();
        if (params.size() > 0) {
            builder.add("$NConv", params.get(0));
        }
        for (int i = 1; i < params.size(); i++) {
            builder.add(", $NConv", params.get(i));
        }
        return builder.build();
    }

    private void unpackSpecialFields(@NonNull 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 unpack it.
            }
            switch (specialField) {
                case ID:
                    method.addStatement("String $NConv = genericDoc.getId()", fieldName);
                    break;
                case NAMESPACE:
                    method.addStatement("String $NConv = genericDoc.getNamespace()", fieldName);
                    break;
                case CREATION_TIMESTAMP_MILLIS:
                    method.addStatement(
                            "long $NConv = genericDoc.getCreationTimestampMillis()", fieldName);
                    break;
                case TTL_MILLIS:
                    method.addStatement("long $NConv = genericDoc.getTtlMillis()", fieldName);
                    break;
                case SCORE:
                    method.addStatement("int $NConv = genericDoc.getScore()", fieldName);
                    break;
            }
        }
    }

    @Nullable
    private CodeBlock createAppSearchFieldWrite(@NonNull String fieldName) {
        switch (Objects.requireNonNull(mModel.getFieldWriteKind(fieldName))) {
            case FIELD:
                return CodeBlock.of("document.$N = $NConv", fieldName, fieldName);
            case SETTER:
                String setter = mModel.getSetterForField(fieldName).getSimpleName().toString();
                return CodeBlock.of("document.$N($NConv)", setter, fieldName);
            default:
                return null;  // Constructor params should already have been set
        }
    }
}