All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.shaishavgandhi.navigator.FileWriter Maven / Gradle / Ivy

The newest version!
package com.shaishavgandhi.navigator;

import androidx.annotation.CheckResult;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.processing.Messager;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;

import static com.shaishavgandhi.navigator.StringUtils.capitalize;
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;

final class FileWriter {

    private static final ClassName CONTEXT_CLASSNAME = ClassName.get("android.content", "Context");
    private static final ClassName INTENT_CLASSNAME = ClassName.get("android.content", "Intent");
    private static final ClassName BUNDLE_CLASSNAME = ClassName.get("android.os", "Bundle");
    private static final ClassName ACTIVITY_CLASSNAME = ClassName.get("android.app", "Activity");
    private static final ClassName FRAGMENT_CLASSNAME = ClassName.get("androidx.fragment.app", "Fragment");
    private static final ClassName STRING_CLASS = ClassName.bestGuess("java.lang.String");

    private static final String FLAGS = "flags";
    private static final String ACTION = "action";

    private List constructorParams = new ArrayList<>();
    private List classBuilders = new ArrayList<>();

    private HashMap typeMapper = new HashMap(){{
        put("java.lang.String", "String");
        put("java.lang.String[]", "StringArray");
        put("java.util.ArrayList", "StringArrayList");
        put("java.lang.Integer", "Int");
        put("int", "Int");
        put("int[]", "IntArray");
        put("java.util.ArrayList", "IntegerArrayList");
        put("java.lang.Long","Long");
        put("long", "Long");
        put("long[]", "LongArray");
        put("double", "Double");
        put("java.lang.Double", "Double");
        put("double[]", "DoubleArray");
        put("float", "Float");
        put("java.lang.Float","Float");
        put("float[]", "FloatArray");
        put("byte", "Byte");
        put("java.lang.Byte", "Byte");
        put("byte[]", "ByteArray");
        put("short", "Short");
        put("java.lang.Short", "Short");
        put("short[]", "ShortArray");
        put("char", "Char");
        put("java.lang.Character", "Char");
        put("char[]", "CharArray");
        put("java.lang.CharSequence", "CharSequence");
        put("java.lang.CharSequence[]", "CharSequenceArray");
        put("java.util.ArrayList", "CharSequenceArrayList");
        put("android.util.Size", "Size");
        put("android.util.SizeF", "SizeF");
        put("boolean", "Boolean");
        put("boolean[]", "BooleanArray");
        put("java.lang.Boolean", "Boolean");
        put("android.os.Parcelable", "Parcelable");
        put("android.os.Parcelable[]", "ParcelableArray");
        put("java.util.ArrayList", "ParcelableArrayList");
    }};

    private LinkedHashMap> annotationsPerClass;
    private Types typeUtils;
    private Elements elementUtils;
    private List files = new ArrayList<>();
    private Messager messager;

    FileWriter(Types typeUtils, Elements elementUtils, LinkedHashMap> annotationsPerClass, Messager messager) {
        this.typeUtils = typeUtils;
        this.elementUtils = elementUtils;
        this.annotationsPerClass = annotationsPerClass;
        this.messager = messager;
    }

    protected void writeFiles() {
        for (Map.Entry> item : annotationsPerClass.entrySet()) {
            ClassName className = item.getKey().getJavaClass();
            LinkedHashSet annotations = item.getValue();

            writeBinder(item.getKey(), annotations);
            writeBuilder(item.getKey(), annotations);
        }
    }

    private void addBindingVariables(TypeSpec.Builder binder, ClassName activity, LinkedHashSet annotations) {
        MethodSpec.Builder builder = MethodSpec.methodBuilder("bind")
                .addJavadoc(CodeBlock.builder()
                        .add("Binds the fields in {@link $T} annotated with {@link $T}\n", activity, Extra.class)
                        .add("\n")
                        .add("This requires that the fields in {@link $T} be at least package-private" +
                                " or if they \nare private, they have a defined setter in the class.\n", activity)
                        .add("You should call this method in an early part of the activity lifecycle like " +
                                "onCreate on onStart.\n\n")
                        .add("@param binder the activity/fragment whose variables are being bound\n")
                        .build())
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC)
                .addParameter(activity, "binder");

        if (isActivity(activity)) {
            builder.addStatement("$T bundle = $L.getIntent().getExtras()", BUNDLE_CLASSNAME,
                    "binder");
        } else if (isFragment(activity)) {
            builder.addStatement("$T bundle = $L.getArguments()", BUNDLE_CLASSNAME,
                    "binder");
        } else {
            messager.printMessage(Diagnostic.Kind.ERROR, "@Extra can only be applied to fields in" +
                    " Activities or Fragments");
        }

        builder.beginControlFlow("if (bundle != null)");
        for (Element element: annotations) {
            Set modifiers = element.getModifiers();

            TypeName name = TypeName.get(element.asType());
            String varKey = getQualifiedExtraFieldName(activity, element);
            String varName = element.getSimpleName().toString();

            MethodSpec.Builder nullableGetter = MethodSpec.methodBuilder("get" + capitalize(varName))
                    .addModifiers(PUBLIC)
                    .addJavadoc(CodeBlock.builder()
                            .add("Nullable getter for $L\n\n", varName)
                            .add("If you want a non-null instance of the type, see {@link #$L($T)}\n", "get" + capitalize(varName), name)
                            .add("\n")
                            .add("@return the $L\n", varName)
                            .build())
                    .returns(name);

            MethodSpec.Builder nonNullGetter = MethodSpec.methodBuilder("get" + capitalize(varName))
                    .addModifiers(PUBLIC)
                    .addAnnotation(NonNull.class)
                   .addParameter(ParameterSpec.builder(name, "defaultValue")
                           .addModifiers(FINAL)
                           .addAnnotation(NonNull.class)
                           .build())
                    .addJavadoc(CodeBlock.builder()
                            .add("Non-null getter for $L\n", varName)
                            .add("\n")
                            .add("@param defaultValue the default value in case the key isn't present in the Bundle.\n")
                            .add("@return the $L\n", varName)
                            .build())
                    .returns(name);

            builder.beginControlFlow("if ($L.containsKey($L))", "bundle", varKey);
            nonNullGetter.beginControlFlow("if ($L.containsKey($L)\n" +
                    "    && $L.get($L) != null)", "bundle", varKey, "bundle", varKey);

            if (!name.isPrimitive()) {
                nullableGetter.addAnnotation(Nullable.class);
                nullableGetter.beginControlFlow("if ($L.containsKey($L))", "bundle", varKey);
            }

            String extraName = getExtraTypeName(element.asType());
            if (extraName == null) {
                if (isSerializable(typeUtils, elementUtils, element.asType())) {
                    // Add casting for serializable
                    builder.addStatement("$T $L = ($T) bundle.getSerializable($L)", name,
                            varName, name, varKey);
                    nullableGetter.addStatement("return ($T) bundle.getSerializable($L)", name, varKey);
                    nonNullGetter.addStatement("return ($T) bundle.getSerializable($L)", name, varKey);
                } else {
                    messager.printMessage(Diagnostic.Kind.ERROR, element.getSimpleName().toString() + " cannot be put in Bundle");
                }
            } else {
                if (extraName.equals("ParcelableArray") || extraName.equals("Serializable")) {
                    // Add extra casting. TODO: Refactor this to be more generic
                    builder.addStatement("$T $L = ($T) bundle.get" + extraName + "($L)", name,
                            varName, name, varKey);
                    nullableGetter.addStatement("return ($T) bundle.get" + extraName + "($L)", name, varKey);
                    nonNullGetter.addStatement("return ($T) bundle.get" + extraName + "($L)", name, varKey);
                } else {
                    builder.addStatement("$T $L = bundle.get" + extraName + "($L)", name, varName,
                            varKey);
                    nullableGetter.addStatement("return bundle.get" + extraName + "($L)", varKey);
                    nonNullGetter.addStatement("return bundle.get" + extraName + "($L)", varKey);
                }
            }

            if (modifiers.contains(PRIVATE)) {
                // Use getter and setter
                builder.addStatement("$L.set$L($L)", "binder", capitalize(varName), varName);

            } else {
                builder.addStatement("$L.$L = $L", "binder", varName, varName);
            }
            builder.endControlFlow();

            if (!name.isPrimitive()) {
                nullableGetter.endControlFlow();
                nullableGetter.addStatement("return null");
            }
            nonNullGetter.endControlFlow();
            nonNullGetter.addStatement("return defaultValue");

            binder.addMethod(nullableGetter.build());
            binder.addMethod(nonNullGetter.build());
        }

        builder.endControlFlow();
        binder.addMethod(builder.build());
    }

    /**
     * Returns whether the given class is a Fragment or not.
     *
     * @param className Class of element annotated with {@link Extra}
     * @return boolean
     */
    private boolean isFragment(ClassName className) {
        TypeMirror currentClass = elementUtils.getTypeElement(className.toString()).asType();
        boolean isFragment = false;
        if (elementUtils.getTypeElement("android.support.v4.app.Fragment") != null) {
            TypeMirror supportFragment = elementUtils.getTypeElement("android.support.v4.app.Fragment").asType();
            isFragment = typeUtils.isSubtype(currentClass, supportFragment);
        }
        if (elementUtils.getTypeElement("android.app.Fragment") != null && !isFragment) {
            TypeMirror fragment = elementUtils.getTypeElement("android.app.Fragment").asType();
            isFragment = typeUtils.isSubtype(currentClass, fragment);
        }
        if (elementUtils.getTypeElement("androidx.fragment.app.Fragment") != null && !isFragment) {
            TypeMirror fragment = elementUtils.getTypeElement("androidx.fragment.app.Fragment").asType();
            isFragment = typeUtils.isSubtype(currentClass, fragment);
        }
        return isFragment;
    }

    /**
     * Returns whether the given class is an Activity or not.
     *
     * @param className Class of element annotated with {@link Extra}
     * @return boolean
     */
    private boolean isActivity(ClassName className) {
        if (elementUtils.getTypeElement("android.app.Activity") != null) {
            TypeMirror activity = elementUtils.getTypeElement("android.app.Activity").asType();
            TypeMirror currentClass = elementUtils.getTypeElement(className.toString()).asType();
            return typeUtils.isSubtype(currentClass, activity);
        }
        return false;
    }

    private void writeBinder(QualifiedClassName qualifiedClassName, LinkedHashSet annotations) {
        ClassName className = qualifiedClassName.getJavaClass();
        ClassName binderClass = getBinderClass(className);

        TypeSpec.Builder binder = TypeSpec.classBuilder(binderClass.simpleName())
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL);

        addBindingVariables(binder, className, annotations);

        // Add a private empty constructor
        binder.addMethod(MethodSpec.constructorBuilder()
                .addModifiers(PRIVATE)
                .build());

        // Add private constructor with arg as Bundle
        binder.addMethod(MethodSpec.constructorBuilder()
                .addParameter(ParameterSpec.builder(BUNDLE_CLASSNAME, "bundle")
                        .addAnnotation(NonNull.class)
                        .build())
                .addModifiers(PRIVATE)
                .addStatement("this.bundle = bundle")
                .build());

        // Field for storing the Bundle
        binder.addField(FieldSpec.builder(BUNDLE_CLASSNAME, "bundle")
                .addModifiers(PRIVATE)
                .addAnnotation(NonNull.class)
                .build());

        // Static method to get the binder
        binder.addMethod(MethodSpec.methodBuilder("from")
                .addModifiers(PUBLIC, STATIC)
                .addParameter(ParameterSpec.builder(BUNDLE_CLASSNAME, "bundle")
                        .addAnnotation(NonNull.class)
                        .addModifiers(FINAL)
                        .build())
                .addJavadoc(CodeBlock.builder()
                        .add("Static factory method to instantiate {@link $T}\n", binderClass)
                        .add("\n")
                        .add("@param bundle non null bundle that will be used to unwrap the data.\n")
                        .add("@return the binder that will expose getters.\n")
                        .build())
                .returns(binderClass)
                .addStatement("return new $T(bundle)", binderClass)
                .build());


        TypeSpec binderResolved = binder.build();
        files.add(JavaFile.builder(className.packageName(), binderResolved).build());
    }

    /**
     * Get the `Binder` class of the given Activity/Fragment which is responsible
     * for the binding logic of the `Bundle` to the `@Extra` . For example:
     * MainActivity -> MainActivityBinder
     *
     * @param className Class of element annotated with {@link Extra}
     * @return binder class
     */
    private ClassName getBinderClass(ClassName className) {
        return ClassName.bestGuess(className.packageName() + "." + className.simpleName() +
                "Binder");
    }

    /**
     * Get the `Builder` class of the given Activity/Fragment which is responsible
     * for the builder logic and starting of the Activity. Given class `MainActivity`,
     * builder would be `MainActivityBuilder`.
     *
     * @param className Class of element annotated with {@link Extra}
     * @return builder class
     */
    private ClassName getBuilderClass(ClassName className) {
        return ClassName.bestGuess(className.packageName() + "." + className.simpleName() +
                "Builder");
    }

    private void writeBuilder(QualifiedClassName qualifiedClassName, LinkedHashSet elements) {
        ClassName activity = qualifiedClassName.getJavaClass();
        String activityName = activity.simpleName();
        TypeSpec.Builder builder = TypeSpec.classBuilder(activityName + "Builder")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL);

        if (isActivity(activity)) {
            builder.addField(FieldSpec.builder(TypeName.INT, FLAGS)
                    .addModifiers(PRIVATE)
                    .initializer("$L", -1)
                    .build());

            builder.addField(FieldSpec.builder(STRING_CLASS, ACTION)
                    .addModifiers(PRIVATE)
                    .build());
        }

        builder.addField(FieldSpec.builder(BUNDLE_CLASSNAME, "extras")
                .addModifiers(PRIVATE).build());

        ClassName builderClass = getBuilderClass(activity);

        // Constructor
        MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
                .addModifiers(PRIVATE);

        // Set intent flags
        MethodSpec setFlagsMethod = setFlagsMethod(builderClass);

        // Set action
        MethodSpec setActionMethod = setActionMethod(builderClass);

        // Static method to prepare activity
        MethodSpec.Builder prepareMethodBuilder = getPrepareActivityMethod(builderClass);

        // Bundle builder
        MethodSpec.Builder bundleBuilder = getExtrasBundle();

        // TODO: There must be a better way for this with JavaPoet. Right now
        // I manually append each parameter and remove commas and close the bracket
        StringBuilder returnStatement = new StringBuilder("return new $T(");

        for (Element element: elements) {
            TypeMirror typeMirror = element.asType();
            if (typeMirror == null) {
                continue;
            }
            // Add as field parameter
            String name = element.getSimpleName().toString();
            AnnotationSpec nullability = getNullabilityFor(element);
            List annotations = getAnnotationsForElement(element);
            annotations.add(nullability);
            builder.addField(FieldSpec.builder(TypeName.get(typeMirror), name, PRIVATE)
                    .addAnnotations(annotations)
                    .build());

            // Add the extra name as public static variable
            addKeyToClass(element, builder);

            // Add to constructor
            ParameterSpec parameter = getParameter(element, annotations);

            AnnotationSpec nullable = AnnotationSpec.builder(ClassName.get(Nullable.class)).build();
            if (!parameter.annotations.contains(nullable)) {
                constructorBuilder.addParameter(parameter);
                constructorBuilder.addStatement("this.$L = $L", parameter.name, parameter.name);

                // Add to static prepare method
                prepareMethodBuilder.addParameter(parameter);
                constructorParams.add(parameter);

                // Append to return statement
                returnStatement.append(parameter.name);
                returnStatement.append(", ");
            } else {
                addFieldToBuilder(builder, element, builderClass, annotations);
            }

            String extraName = getExtraTypeName(element.asType());

            if (extraName == null) {
                // Put to bundle
                bundleBuilder.addStatement("bundle.putSerializable($L, $L)", getExtraFieldName(element),
                        parameter.name);
            } else {
                // Put to bundle
                bundleBuilder.addStatement("bundle.put" + extraName + "($L, $L)", getExtraFieldName(element),
                        parameter.name);
            }
        }

        classBuilders.add(new ClassBuilder(qualifiedClassName, constructorParams));
        constructorParams = new ArrayList<>();

        // Sanitize return statement
        if (returnStatement.charAt(returnStatement.length() - 1) == ' ') {
            returnStatement.deleteCharAt(returnStatement.length() - 2);
            returnStatement.deleteCharAt(returnStatement.length() - 1);
        }
        returnStatement.append(")");
        prepareMethodBuilder.addStatement(returnStatement.toString(), builderClass);

        bundleBuilder.beginControlFlow("if ($L != null)", "extras");
        bundleBuilder.addStatement("bundle.putAll($L)", "extras");
        bundleBuilder.endControlFlow();
        bundleBuilder.addStatement("return bundle");
        MethodSpec bundle  = bundleBuilder.build();

        // Destination intent
        MethodSpec destinationIntentMethod = getDestinationIntentMethod(activityName, bundle);

        // Start activity
        MethodSpec startActivityMethod = getStartActivityMethod(destinationIntentMethod, CONTEXT_CLASSNAME);

        // Start activity from fragment
        MethodSpec startActivityFromFragmentMethod = getStartActivityMethod(destinationIntentMethod, FRAGMENT_CLASSNAME);

        // Start activity with extras
        MethodSpec startActivityExtrasMethod = getStartActivityWithExtras(destinationIntentMethod, CONTEXT_CLASSNAME);

        // Start activity from fragment with extras
        MethodSpec startActivityFromFragmentExtrasMethod = getStartActivityWithExtras(destinationIntentMethod, FRAGMENT_CLASSNAME);

        // Start for result
        MethodSpec startForResultMethod = getStartForResultMethod(destinationIntentMethod, ACTIVITY_CLASSNAME);

        // Start from fragment with result
        MethodSpec startForResultFromFragmentMethod = getStartForResultMethod(destinationIntentMethod, FRAGMENT_CLASSNAME);

        // Start result with extras
        MethodSpec startResultExtrasMethod = getStartForResultWithExtras(destinationIntentMethod, ACTIVITY_CLASSNAME);

        // Start result from fragment with extras
        MethodSpec startResultFromFragmentExtrasMethod = getStartForResultWithExtras(destinationIntentMethod, FRAGMENT_CLASSNAME);

        MethodSpec setExtrasMethod = getExtrasSetterMethod(builderClass);

        builder.addMethod(prepareMethodBuilder.build());
        if (isActivity(activity)) {
            // Add activity specific methods
            builder.addMethod(startActivityMethod);
            builder.addMethod(startActivityFromFragmentMethod);
            builder.addMethod(startForResultMethod);
            builder.addMethod(startForResultFromFragmentMethod);
            builder.addMethod(destinationIntentMethod);
            builder.addMethod(startResultExtrasMethod);
            builder.addMethod(startResultFromFragmentExtrasMethod);
            builder.addMethod(startActivityExtrasMethod);
            builder.addMethod(startActivityFromFragmentExtrasMethod);
            builder.addMethod(setFlagsMethod);
            builder.addMethod(setActionMethod);
        }
        builder.addMethod(bundle);
        builder.addMethod(setExtrasMethod);
        TypeSpec builderInnerClass = builder.addMethod(constructorBuilder.build()).build();

        JavaFile file = JavaFile.builder(activity.packageName(), builderInnerClass).build();
        files.add(file);
    }

    /**
     * Add static final key that is used to bind each extra
     * from the bundle.
     *
     * @param element to be added
     * @param builder `Builder` class
     */
    private void addKeyToClass(Element element, TypeSpec.Builder builder) {
        NParameter extraName = getVariableKey(element);

        if (!extraName.getCustomKey()) {
            builder.addField(FieldSpec.builder(STRING_CLASS, getExtraFieldName(extraName),
                    Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
                    .initializer("\"$L\"", extraName.getName())
                    .build());
        }
    }

    private String getExtraFieldName(Element element) {
        return getExtraFieldName(getVariableKey(element));
    }

    private String getQualifiedExtraFieldName(ClassName bindingClass, Element element) {
        NParameter param = getVariableKey(element);
        if (!param.getCustomKey()) {
            return bindingClass.simpleName() + "Builder." + getExtraFieldName(getVariableKey(element));
        }
        return getExtraFieldName(param);
    }

    private String getExtraFieldName(NParameter parameter) {
        StringBuilder builder = new StringBuilder("EXTRA");
        if (!parameter.getCustomKey()) {
            for (String word : splitByCasing(parameter.getName())) {
                builder.append("_");
                builder.append(word.toUpperCase());
            }
            return builder.toString();
        } else {
            return "\""+ parameter.getName() +"\"";
        }
    }

    private String[] splitByCasing(String variable) {
        return variable.split("(? annotations) {
        String variableName = element.getSimpleName().toString();
        MethodSpec.Builder setter = MethodSpec.methodBuilder("set" + capitalize(variableName))
                .addModifiers(Modifier.FINAL, Modifier.PUBLIC)
                .addParameter(ParameterSpec.builder(TypeName.get(element.asType()), variableName)
                        .addAnnotations(annotations)
                        .build())
                .addAnnotation(CheckResult.class)
                .addStatement("this.$L = $L", variableName, variableName)
                .returns(builderClass)
                .addStatement("return this");

        builder.addMethod(setter.build());
    }

    /**
     * Generates the `startForResult` method which is added to
     * the `Builder` class.
     *
     * @param destinationIntent the intent to start the Activity.
     * @return `startForResult` method.
     */
    private MethodSpec getStartForResultMethod(MethodSpec destinationIntent, ClassName originClass) {
        final String parameterName = originClass.simpleName().toLowerCase();
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("startForResult")
                .addModifiers(Modifier.PUBLIC)
                .addParameter(ParameterSpec.builder(originClass, parameterName)
                        .addAnnotation(NonNull.class)
                        .build())
                .addParameter(TypeName.INT, "requestCode");
        String context = resolveContext(originClass);
        methodBuilder.addStatement("$T intent = $N($L)", INTENT_CLASSNAME,
                destinationIntent, context);

        // Add JavaDoc
        methodBuilder.addJavadoc(CodeBlock.builder()
                .add("Terminating method in builder. Passes the built bundle, sets any \n")
                .add("{@link android.content.Intent} flags if any and starts the activity with \n")
                .add("the provided requestCode.\n")
                .add("\n")
                .add("@param $L\n", parameterName)
                .add("@param $L\n", "requestCode")
                .build());
        // Start for result
        methodBuilder.addStatement("$L.startActivityForResult($L, $L)", parameterName,
                "intent", "requestCode");
        return methodBuilder.build();
    }

    private MethodSpec getDestinationIntentMethod(String activityName, MethodSpec bundle) {
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("getDestinationIntent")
                .addModifiers(PRIVATE)
                .addAnnotation(NonNull.class)
                .addParameter(ParameterSpec.builder(CONTEXT_CLASSNAME, "context")
                        .addAnnotation(NonNull.class)
                        .build()
                ).returns(INTENT_CLASSNAME);

        methodBuilder.addStatement("$T intent = new $T($L, $L)", INTENT_CLASSNAME,
                INTENT_CLASSNAME, "context", activityName + ".class");

        methodBuilder.addJavadoc(CodeBlock.builder()
                .add("Returns the {@link android.content.Intent} that will be used to start the Activity.\n")
                .add("\n")
                .add("Sets optional fields like {@link flags}, {@link action} if they are supplied by\n")
                .add("you in the builder methods.\n")
                .add("\n")
                .add("@param $L the context used in Intent.\n", "context")
                .add("@return the constructed Intent\n")
                .build());

        // Put extras
        methodBuilder.addStatement("intent.putExtras($N())", bundle);
        //Add flags if any
        addOptionalAttributes(methodBuilder);
        methodBuilder.addStatement("return intent");
        return methodBuilder.build();
    }

    /**
     * Generates the `startForResult` method with overload for a `Bundle`. This
     * is added to the `Builder` class to start an Activity with a result.
     *
     * @param destinationIntent the Intent to be started.
     * @return `startForResult` method
     */
    private MethodSpec getStartForResultWithExtras(MethodSpec destinationIntent, ClassName originClass) {
        final String parameterName = originClass.simpleName().toLowerCase();
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("startForResult")
                .addModifiers(Modifier.PUBLIC)
                .addParameter(ParameterSpec.builder(originClass, parameterName)
                        .addAnnotation(NonNull.class)
                        .build()
                )
                .addParameter(TypeName.INT, "requestCode")
                .addParameter(ParameterSpec.builder(BUNDLE_CLASSNAME, "extras", Modifier.FINAL)
                .addAnnotation(Nullable.class).build());
        String context = resolveContext(originClass);
        methodBuilder.addStatement("$T intent = $N($L)", INTENT_CLASSNAME,
                destinationIntent, context);

        // Add JavaDoc
        methodBuilder.addJavadoc(CodeBlock.builder()
                .add("Terminating method in builder. Passes the built bundle, sets any \n")
                .add("{@link android.content.Intent} flags if any and starts the activity with \n")
                .add("the provided requestCode and {@link android.os.Bundle extras}.\n")
                .add("\n")
                .add("@param $L\n", parameterName)
                .add("@param $L\n", "requestCode")
                .build());

        // Start activity for result
        methodBuilder.addStatement("$L.startActivityForResult($L, $L, $L)", parameterName,
                "intent", "requestCode", "extras");
        return methodBuilder.build();
    }

    /**
     * Returns a partially generated static factory method that is added to the
     * `Builder` for easy access. i.e `MainActivityBuilder.builder(args)`
     *
     * @param builderClass `Builder` returned for a chaining API
     * @return Partially generated method that is appended later.
     */
    private MethodSpec.Builder getPrepareActivityMethod(ClassName builderClass) {
        MethodSpec.Builder prepareMethodBuilder = MethodSpec.methodBuilder("builder");
        prepareMethodBuilder.addModifiers(Modifier.STATIC, Modifier.FINAL, Modifier.PUBLIC);
        prepareMethodBuilder.returns(builderClass);
        prepareMethodBuilder.addAnnotation(CheckResult.class);
        return prepareMethodBuilder;
    }

    /**
     * Adds optional attributes like `setFlags` and `setAction`
     * to the given builder.
     *
     * @param builder `Builder` class
     */
    private void addOptionalAttributes(MethodSpec.Builder builder) {
        builder.beginControlFlow("if ($L != -1)", FLAGS);
        builder.addStatement("$L.setFlags($L)", "intent", FLAGS);
        builder.endControlFlow();

        builder.beginControlFlow("if ($L != null)", ACTION);
        builder.addStatement("$L.setAction($L)", "intent", ACTION);
        builder.endControlFlow();
    }

    /**
     * Get {@link ParameterSpec} from given element.
     * Checks for nullability, types etc and returns
     * the parameter.
     *
     * @param element to be converted to Parameter
     * @return {@link ParameterSpec}
     */
    private ParameterSpec getParameter(Element element, List annotations) {
        TypeMirror typeMirror = element.asType();
        String name = element.getSimpleName().toString();
        ParameterSpec.Builder parameterBuilder = ParameterSpec.builder(TypeName.get(typeMirror), name);
        parameterBuilder.addModifiers(Modifier.FINAL);

        parameterBuilder.addAnnotations(annotations);

        return parameterBuilder.build();
    }

    private List getAnnotationsForElement(Element element) {
        List annotations = new ArrayList<>();

        for (AnnotationMirror annotation: element.getAnnotationMirrors()) {
            if (isAnnotationWhitelisted(annotation)) {
                continue;
            }
            annotations.add(AnnotationSpec.builder(ClassName.bestGuess(annotation.getAnnotationType().toString()))
                    .build());
        }
        return annotations;
    }

    public static boolean isAnnotationWhitelisted(AnnotationMirror annotation) {
        return annotation.getAnnotationType().toString().contains("Nullable")
                || annotation.getAnnotationType().toString().contains("NonNull")
                || annotation.getAnnotationType().toString().contains("NotNull")
                || annotation.getAnnotationType().toString().contains("Extra")
                || annotation.getAnnotationType().toString().contains("Optional");
    }

    /**
     * Returns nullability annotation for given element.
     * Checks for Jetbrains' {@link org.jetbrains.annotations.Nullable}
     * as well as Android's {@link Nullable}.
     *
     * @param element annotated with {@link Extra}
     * @return Nullability annotation
     */
    @NonNull private AnnotationSpec getNullabilityFor(Element element) {
        // Check both Jetbrains and Android nullable annotations since
        // Kotlin nulls are annotated with Jetbrains @Nullable
        if (element.getAnnotation(Nullable.class) == null
                && element.getAnnotation(org.jetbrains.annotations.Nullable.class) == null
                && element.getAnnotation(Optional.class) == null) {
            return AnnotationSpec.builder(ClassName.get(NonNull.class)).build();
        } else {
            return AnnotationSpec.builder(ClassName.get(Nullable.class)).build();
        }
    }

    /**
     * Returns the extra type name for given type.
     * This method is useful in mapping from types
     * like Parcelable, ParcelableArrayList etc. to
     * the appropriate bundle type.
     *
     * @param typeMirror of the element
     * @return type
     */
    private String getExtraTypeName(TypeMirror typeMirror) {
        String result = typeMapper.get(typeMirror.toString());
        if (result == null) {
            if (isParcelable(typeUtils, elementUtils, typeMirror)) {
                result = "Parcelable";
            } else if (isParcelableList(typeUtils, elementUtils, typeMirror)) {
                result = "ParcelableArrayList";
            } else if (isSparseParcelableArrayList(typeUtils, elementUtils, typeMirror)) {
                result = "SparseParcelableArray";
            } else if (isParcelableArray(typeUtils, elementUtils, typeMirror)) {
                result = "ParcelableArray";
            }
        }
        return result;
    }

    public static boolean isParcelable(Types typeUtils, Elements elementUtils, TypeMirror typeMirror) {
        return typeUtils.isAssignable(typeMirror, elementUtils.getTypeElement("android.os.Parcelable")
                .asType());
    }

    public static boolean isParcelableArray(Types typeUtils, Elements elementUtils, TypeMirror typeMirror) {
        return typeUtils.isAssignable(typeMirror, typeUtils.getArrayType(elementUtils
                .getTypeElement("android.os.Parcelable").asType()));
    }

    public static boolean isParcelableList(Types typeUtils, Elements elementUtils, TypeMirror typeMirror) {
        DeclaredType type = typeUtils.getDeclaredType(elementUtils.getTypeElement("java.util" +
                        ".ArrayList"), elementUtils.getTypeElement("android.os.Parcelable").asType());
        if (typeUtils.isAssignable(typeUtils.erasure(typeMirror), type)) {
            List typeArguments = ((DeclaredType) typeMirror).getTypeArguments();
            return typeArguments != null && typeArguments.size() >= 1 &&
                    typeUtils.isAssignable(typeArguments.get(0), elementUtils.getTypeElement
                            ("android.os.Parcelable").asType());
        }
        return false;
    }

    public static boolean isSparseParcelableArrayList(Types typeUtils, Elements elementUtils, TypeMirror typeMirror) {
        DeclaredType type = typeUtils.getDeclaredType(elementUtils.getTypeElement("android.util.SparseArray"),
                elementUtils.getTypeElement("android.os.Parcelable").asType());
        if (typeUtils.isAssignable(typeUtils.erasure(typeMirror), type)) {
            List typeArguments = ((DeclaredType) typeMirror).getTypeArguments();
            return typeArguments != null && typeArguments.size() >= 1 &&
                    typeUtils.isAssignable(typeArguments.get(0), elementUtils.getTypeElement
                            ("android.os.Parcelable").asType());
        }
        return false;
    }

    public static boolean isSerializable(Types typeUtils, Elements elementUtils, TypeMirror typeMirror) {
        return typeUtils.isAssignable(typeMirror, elementUtils.getTypeElement("java.io.Serializable")
                .asType());
    }


    NParameter getVariableKey(Element element) {
        if (element.getAnnotation(Extra.class).key().isEmpty()) {
            return new NParameter(element.asType(), element.getSimpleName().toString(), false);
        } else {
            return new NParameter(element.asType(), element.getAnnotation(Extra.class).key(), true);
        }
    }

    protected List getFiles() {
        return files;
    }

    public List getClassBuilders() {
        return classBuilders;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy