paperparcel.PaperParcelWriter.java Source code

Java tutorial

Introduction

Here is the source code for paperparcel.PaperParcelWriter.java

Source

/*
 * Copyright (C) 2016 Bradley Campbell.
 *
 * 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 paperparcel;

import android.support.annotation.NonNull;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.squareup.javapoet.ArrayTypeName;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Name;
import javax.lang.model.type.TypeMirror;
import paperparcel.AdapterDescriptor.ConstructorInfo;

import static javax.lang.model.element.Modifier.FINAL;
import static javax.lang.model.element.Modifier.PRIVATE;
import static javax.lang.model.element.Modifier.PUBLIC;
import static javax.lang.model.element.Modifier.STATIC;

/**
 * Responsible for creating a {@link TypeSpec.Builder} for the Parcelable read and write
 * implementations
 */
final class PaperParcelWriter {
    private static final ClassName PARCEL = ClassName.get("android.os", "Parcel");
    private static final ClassName UTILS = ClassName.get("paperparcel.internal", "Utils");
    private static final ClassName TYPE_ADAPTER = ClassName.get("paperparcel", "TypeAdapter");

    private final AdapterNameGenerator adapterNames = new AdapterNameGenerator();

    private final ClassName name;
    private final PaperParcelDescriptor descriptor;

    PaperParcelWriter(ClassName name, PaperParcelDescriptor descriptor) {
        this.name = name;
        this.descriptor = descriptor;
    }

    final TypeSpec.Builder write() {
        ClassName className = ClassName.get(descriptor.element());
        return TypeSpec.classBuilder(name).addModifiers(FINAL)
                .addFields(adapterDependencies(descriptor.adapters().values())).addField(creator(className))
                .addMethod(writeToParcel(className))
                .addMethod(MethodSpec.constructorBuilder().addModifiers(PRIVATE).build());
    }

    private FieldSpec creator(ClassName className) {
        UniqueNameSet readNames = new UniqueNameSet();

        ClassName creator = ClassName.get("android.os", "Parcelable", "Creator");
        TypeName creatorOfClass = ParameterizedTypeName.get(creator, className);

        ParameterSpec in = ParameterSpec.builder(PARCEL, readNames.getUniqueName("in")).build();
        MethodSpec.Builder createFromParcel = MethodSpec.methodBuilder("createFromParcel")
                .addAnnotation(Override.class).addModifiers(PUBLIC).returns(className).addParameter(in);

        if (descriptor.isSingleton()) {
            createFromParcel.addStatement("return $T.INSTANCE", className);
        } else {
            // Read the fields from the parcel
            ImmutableMap<String, FieldSpec> fieldMap = readFields(in, readNames);
            for (FieldSpec field : fieldMap.values()) {
                createFromParcel.addStatement("$T $N = $L", field.type, field.name, field.initializer);
            }
            // Re-construct the model and return
            FieldSpec model = initModel(className, readNames, fieldMap);
            createFromParcel.addStatement("$T $N = $L", model.type, model.name, model.initializer)
                    .addCode(setFields(model, fieldMap)).addStatement("return $N", model.name);
        }

        MethodSpec.Builder newArray = MethodSpec.methodBuilder("newArray").addAnnotation(Override.class)
                .addModifiers(PUBLIC).addParameter(int.class, "size").returns(ArrayTypeName.of(className))
                .addStatement("return new $T[size]", className);

        TypeSpec initializer = TypeSpec.anonymousClassBuilder("").addSuperinterface(creatorOfClass)
                .addMethod(createFromParcel.build()).addMethod(newArray.build()).build();

        return FieldSpec.builder(creatorOfClass, "CREATOR", STATIC, FINAL).initializer("$L", initializer)
                .addAnnotation(NonNull.class).build();
    }

    private ImmutableMap<String, FieldSpec> readFields(ParameterSpec in, UniqueNameSet readNames) {
        ImmutableMap.Builder<String, FieldSpec> result = ImmutableMap.builder();

        // Read the fields in the exact same order that they were written to the Parcel. Currently
        // directly readable fields first, then all fields that are read via getters, and finally
        // all fields that require reflection.
        ImmutableList<FieldDescriptor> combined = ImmutableList.<FieldDescriptor>builder()
                .addAll(descriptor.readableFields()).addAll(descriptor.getterMethodMap().keySet()).build();

        for (FieldDescriptor field : combined) {
            String fieldName = readNames.getUniqueName(field.name());
            result.put(field.name(), readField(fieldName, field, in));
        }

        return result.build();
    }

    @SuppressWarnings("ConstantConditions")
    private FieldSpec readField(String fieldName, FieldDescriptor field, ParameterSpec in) {
        TypeName fieldTypeName = TypeName.get(field.type().get());

        FieldSpec.Builder builder = FieldSpec.builder(fieldTypeName, fieldName);

        if (fieldTypeName.isPrimitive()) {
            if (TypeName.BOOLEAN.equals(fieldTypeName)) {
                builder.initializer("$N.readInt() == 1", in);
            } else if (TypeName.INT.equals(fieldTypeName)) {
                builder.initializer("$N.readInt()", in);
            } else if (TypeName.LONG.equals(fieldTypeName)) {
                builder.initializer("$N.readLong()", in);
            } else if (TypeName.DOUBLE.equals(fieldTypeName)) {
                builder.initializer("$N.readDouble()", in);
            } else if (TypeName.FLOAT.equals(fieldTypeName)) {
                builder.initializer("$N.readFloat()", in);
            } else if (TypeName.CHAR.equals(fieldTypeName)) {
                builder.initializer("(char) $N.readInt()", in);
            } else if (TypeName.BYTE.equals(fieldTypeName)) {
                builder.initializer("$N.readByte()", in);
            } else if (TypeName.SHORT.equals(fieldTypeName)) {
                builder.initializer("(short) $N.readInt()", in);
            } else {
                throw new IllegalArgumentException("Unknown primitive type: " + fieldTypeName);
            }
        } else {
            AdapterDescriptor adapter = descriptor.adapters().get(field);
            CodeBlock adapterInstance = adapterInstance(adapter);
            if (field.isNullable() && !adapter.nullSafe()) {
                builder.initializer("$T.readNullable($N, $L)", UTILS, in, adapterInstance);
            } else {
                builder.initializer("$L.readFromParcel($N)", adapterInstance, in);
            }
        }

        return builder.build();
    }

    private FieldSpec initModel(final ClassName className, final UniqueNameSet readNames,
            final ImmutableMap<String, FieldSpec> fieldMap) {

        ImmutableList<FieldDescriptor> constructorFields = descriptor.constructorFields();
        CodeBlock constructorParameterList = CodeBlocks
                .join(FluentIterable.from(constructorFields).transform(new Function<FieldDescriptor, CodeBlock>() {
                    @Override
                    public CodeBlock apply(FieldDescriptor field) {
                        return CodeBlock.of("$N", fieldMap.get(field.name()));
                    }
                }), ", ");

        CodeBlock initializer;
        if (descriptor.isConstructorVisible()) {
            initializer = CodeBlock.of("new $T($L)", className, constructorParameterList);
        } else {
            // Constructor is private, init via reflection
            CodeBlock constructorArgClassList = CodeBlocks.join(
                    FluentIterable.from(constructorFields).transform(new Function<FieldDescriptor, CodeBlock>() {
                        @Override
                        public CodeBlock apply(FieldDescriptor field) {
                            return CodeBlock.of("$T.class", rawTypeFrom(field.type().get()));
                        }
                    }), ", ");
            initializer = CodeBlock.of("$T.init($T.class, new Class[] { $L }, new Object[] { $L })", UTILS,
                    className, constructorArgClassList, constructorParameterList);
        }

        return FieldSpec.builder(className, readNames.getUniqueName("data")).initializer(initializer).build();
    }

    private CodeBlock setFields(FieldSpec model, ImmutableMap<String, FieldSpec> fieldMap) {
        CodeBlock.Builder block = CodeBlock.builder();

        // Write directly
        for (FieldDescriptor field : descriptor.writableFields()) {
            if (field.isVisible()) {
                block.addStatement("$N.$N = $N", model.name, field.name(), fieldMap.get(field.name()));
            } else {
                // Field isn't visible, write via reflection
                TypeName enclosingClass = rawTypeFrom(field.element().getEnclosingElement().asType());
                block.addStatement("$T.writeField($N, $T.class, $N, $S)", UTILS, fieldMap.get(field.name()),
                        enclosingClass, model.name, field.name());
            }
        }

        // Write via setters
        ImmutableSet<Map.Entry<FieldDescriptor, ExecutableElement>> fieldSetterEntries = descriptor
                .setterMethodMap().entrySet();
        for (Map.Entry<FieldDescriptor, ExecutableElement> fieldSetterEntry : fieldSetterEntries) {
            Name setterName = fieldSetterEntry.getValue().getSimpleName();
            FieldDescriptor field = fieldSetterEntry.getKey();
            block.addStatement("$N.$N($N)", model.name, setterName, fieldMap.get(field.name()));
        }

        return block.build();
    }

    private MethodSpec writeToParcel(TypeName className) {
        ParameterSpec data = ParameterSpec.builder(className, "data").addAnnotation(NonNull.class).build();

        ParameterSpec dest = ParameterSpec.builder(PARCEL, "dest").addAnnotation(NonNull.class).build();

        ParameterSpec flags = ParameterSpec.builder(int.class, "flags").build();

        MethodSpec.Builder builder = MethodSpec.methodBuilder("writeToParcel").addModifiers(STATIC)
                .addParameter(data).addParameter(dest).addParameter(flags);

        if (!descriptor.isSingleton()) {
            ImmutableList<FieldDescriptor> readableFields = descriptor.readableFields();
            for (FieldDescriptor field : readableFields) {
                if (field.isVisible()) {
                    CodeBlock accessorBlock = CodeBlock.of("$N.$N", data, field.name());
                    writeField(builder, field, accessorBlock, dest, flags);
                } else {
                    // Field isn't visible, read via reflection.
                    TypeName type = rawTypeFrom(field.type().get());
                    TypeName enclosingClass = rawTypeFrom(field.element().getEnclosingElement().asType());
                    CodeBlock accessorBlock = CodeBlock.of("$T.readField($T.class, $T.class, $N, $S)", UTILS, type,
                            enclosingClass, data, field.name());
                    writeField(builder, field, accessorBlock, dest, flags);
                }
            }

            ImmutableSet<Map.Entry<FieldDescriptor, ExecutableElement>> fieldGetterEntries = descriptor
                    .getterMethodMap().entrySet();
            for (Map.Entry<FieldDescriptor, ExecutableElement> fieldGetterEntry : fieldGetterEntries) {
                FieldDescriptor field = fieldGetterEntry.getKey();
                Name accessorMethodName = fieldGetterEntry.getValue().getSimpleName();
                CodeBlock accessorBlock = CodeBlock.of("$N.$N()", data, accessorMethodName);
                writeField(builder, field, accessorBlock, dest, flags);
            }
        }

        return builder.build();
    }

    @SuppressWarnings("ConstantConditions")
    private void writeField(MethodSpec.Builder builder, FieldDescriptor field, CodeBlock accessorBlock,
            ParameterSpec dest, ParameterSpec flags) {
        TypeName fieldTypeName = TypeName.get(field.type().get());
        if (fieldTypeName.isPrimitive()) {
            if (TypeName.BOOLEAN.equals(fieldTypeName)) {
                builder.addStatement("$N.writeInt($L ? 1 : 0)", dest, accessorBlock);
            } else if (TypeName.INT.equals(fieldTypeName)) {
                builder.addStatement("$N.writeInt($L)", dest, accessorBlock);
            } else if (TypeName.LONG.equals(fieldTypeName)) {
                builder.addStatement("$N.writeLong($L)", dest, accessorBlock);
            } else if (TypeName.DOUBLE.equals(fieldTypeName)) {
                builder.addStatement("$N.writeDouble($L)", dest, accessorBlock);
            } else if (TypeName.FLOAT.equals(fieldTypeName)) {
                builder.addStatement("$N.writeFloat($L)", dest, accessorBlock);
            } else if (TypeName.CHAR.equals(fieldTypeName)) {
                builder.addStatement("$N.writeInt($L)", dest, accessorBlock);
            } else if (TypeName.BYTE.equals(fieldTypeName)) {
                builder.addStatement("$N.writeByte($L)", dest, accessorBlock);
            } else if (TypeName.SHORT.equals(fieldTypeName)) {
                builder.addStatement("$N.writeInt($L)", dest, accessorBlock);
            } else {
                throw new IllegalArgumentException("Unknown primitive type: " + fieldTypeName);
            }
        } else {
            AdapterDescriptor adapter = descriptor.adapters().get(field);
            CodeBlock adapterInstance = adapterInstance(adapter);
            if (field.isNullable() && !adapter.nullSafe()) {
                builder.addStatement("$T.writeNullable($L, $N, $N, $L)", UTILS, accessorBlock, dest, flags,
                        adapterInstance);
            } else {
                builder.addStatement("$L.writeToParcel($L, $N, $N)", adapterInstance, accessorBlock, dest, flags);
            }
        }
    }

    private CodeBlock adapterInstance(AdapterDescriptor adapter) {
        CodeBlock adapterInstance;
        Optional<String> singletonInstance = adapter.singletonInstance();
        if (singletonInstance.isPresent()) {
            adapterInstance = CodeBlock.of("$T.$N", adapter.typeName(), singletonInstance.get());
        } else {
            adapterInstance = CodeBlock.of("$T.$N", name, adapterNames.getName(adapter.typeName()));
        }
        return adapterInstance;
    }

    private ImmutableList<FieldSpec> adapterDependencies(ImmutableCollection<AdapterDescriptor> adapters) {
        Set<TypeName> emptySet = Sets.newLinkedHashSet();
        return adapterDependenciesInternal(adapters, emptySet);
    }

    /** Returns a list of all of the {@link FieldSpec}s that define the required TypeAdapters */
    @SuppressWarnings("OptionalGetWithoutIsPresent") // Previous validation ensures this is fine.
    private ImmutableList<FieldSpec> adapterDependenciesInternal(Collection<AdapterDescriptor> adapters,
            Set<TypeName> scoped) {

        ImmutableList.Builder<FieldSpec> adapterFields = new ImmutableList.Builder<>();
        for (AdapterDescriptor adapter : adapters) {
            // Don't define the same adapter twice
            if (scoped.contains(adapter.typeName())) {
                continue;
            }
            scoped.add(adapter.typeName());

            if (!adapter.singletonInstance().isPresent()) {
                ConstructorInfo constructorInfo = adapter.constructorInfo().get();

                // Add dependencies, then create and add the current adapter
                List<AdapterDescriptor> adapterDependencies = new ArrayList<>();
                for (ConstructorInfo.Param param : constructorInfo.constructorParameters()) {
                    if (param instanceof ConstructorInfo.AdapterParam) {
                        adapterDependencies.add(((ConstructorInfo.AdapterParam) param).adapter);
                    }
                }
                if (adapterDependencies.size() > 0) {
                    adapterFields.addAll(adapterDependenciesInternal(adapterDependencies, scoped));
                }

                // Construct the single instance of this type adapter
                String adapterName = adapterNames.getName(adapter.typeName());
                CodeBlock parameters = getAdapterParameterList(constructorInfo);
                ParameterizedTypeName adapterInterfaceType = ParameterizedTypeName.get(TYPE_ADAPTER,
                        adapter.adaptedTypeName());

                adapterFields.add(FieldSpec.builder(adapterInterfaceType, adapterName, STATIC, FINAL)
                        .initializer(CodeBlock.of("new $T($L)", adapter.typeName(), parameters)).build());
            }
        }

        return adapterFields.build();
    }

    /**
     * Returns a comma-separated {@link CodeBlock} for all of the constructor parameter
     * {@code dependencies} of an adapter.
     */
    private CodeBlock getAdapterParameterList(ConstructorInfo constructorInfo) {
        List<CodeBlock> blocks = new ArrayList<>();
        for (ConstructorInfo.Param param : constructorInfo.constructorParameters()) {

            if (param instanceof ConstructorInfo.AdapterParam) {
                ConstructorInfo.AdapterParam adapterParam = (ConstructorInfo.AdapterParam) param;
                if (adapterParam.adapter.nullSafe()) {
                    blocks.add(adapterInstance(adapterParam.adapter));
                } else {
                    blocks.add(CodeBlock.of("$T.nullSafeClone($L)", UTILS, adapterInstance(adapterParam.adapter)));
                }

            } else if (param instanceof ConstructorInfo.ClassParam) {
                ConstructorInfo.ClassParam classParam = (ConstructorInfo.ClassParam) param;
                if (classParam.className instanceof ParameterizedTypeName) {
                    ParameterizedTypeName parameterizedName = (ParameterizedTypeName) classParam.className;
                    blocks.add(CodeBlock.of("($1T<$2T>)($1T<?>) $3T.class", Class.class, parameterizedName,
                            parameterizedName.rawType));
                } else {
                    blocks.add(CodeBlock.of("$T.class", classParam.className));
                }

            } else if (param instanceof ConstructorInfo.CreatorParam) {
                ConstructorInfo.CreatorParam creatorParam = (ConstructorInfo.CreatorParam) param;
                if (creatorParam.creatorOwner == null) {
                    blocks.add(CodeBlock.of("null"));
                } else {
                    blocks.add(CodeBlock.of("$T.$N", creatorParam.creatorOwner, "CREATOR"));
                }
            }
        }

        return CodeBlocks.join(blocks, ", ");
    }

    private TypeName rawTypeFrom(TypeMirror typeMirror) {
        TypeName typeName = TypeName.get(typeMirror);
        if (typeName instanceof ParameterizedTypeName) {
            return ((ParameterizedTypeName) typeName).rawType;
        }
        return typeName;
    }
}