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

com.evernote.android.state.StateProcessor Maven / Gradle / Ivy

There is a newer version: 1.4.1
Show newest version
/* *****************************************************************************
 * Copyright (c) 2017 Evernote Corporation.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Ralf Wondratschek - initial version
 *******************************************************************************/
package com.evernote.android.state;

import android.os.Bundle;
import android.os.Parcelable;
import android.util.SparseArray;
import android.view.View;

import com.google.auto.service.AutoService;
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.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeVariableName;
import com.squareup.javapoet.WildcardTypeName;

import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.io.Writer;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;

/**
 * @author rwondratschek
 */
@AutoService(Processor.class)
public class StateProcessor extends AbstractProcessor {

    private static final Map TYPE_MAPPING = new HashMap() {{
        put("boolean", "Boolean");
        put("boolean[]", "BooleanArray");
        put("java.lang.Boolean", "BoxedBoolean");
        put("byte", "Byte");
        put("byte[]", "ByteArray");
        put("java.lang.Byte", "BoxedByte");
        put("char", "Char");
        put("char[]", "CharArray");
        put("java.lang.Character", "BoxedChar");
        put("double", "Double");
        put("double[]", "DoubleArray");
        put("java.lang.Double", "BoxedDouble");
        put("float", "Float");
        put("float[]", "FloatArray");
        put("java.lang.Float", "BoxedFloat");
        put("int", "Int");
        put("int[]", "IntArray");
        put("java.lang.Integer", "BoxedInt");
        put("long", "Long");
        put("long[]", "LongArray");
        put("java.lang.Long", "BoxedLong");
        put("short", "Short");
        put("short[]", "ShortArray");
        put("java.lang.Short", "BoxedShort");
        put("java.lang.CharSequence", "CharSequence");
        put("java.lang.CharSequence[]", "CharSequenceArray");
        put("java.lang.String", "String");
        put("java.lang.String[]", "StringArray");
        put("java.util.ArrayList", "CharSequenceArrayList");
        put("java.util.ArrayList", "IntegerArrayList");
        put("java.util.ArrayList", "StringArrayList");
        put("android.os.Bundle", "Bundle");
        put("android.os.Parcelable[]", "ParcelableArray");
    }};

    private static final Comparator COMPARATOR = new Comparator() {
        @Override
        public int compare(Element o1, Element o2) {
            return o1.asType().toString().compareTo(o2.asType().toString());
        }
    };

    private static final String STATE_CLASS_NAME = State.class.getName();
    private static final String STATE_REFLECTION_CLASS_NAME = StateReflection.class.getName();
    private static final String OBJECT_CLASS_NAME = Object.class.getName();
    private static final String PARCELABLE_CLASS_NAME = Parcelable.class.getName();
    private static final String SERIALIZABLE_CLASS_NAME = Serializable.class.getName();

    private static final Set GENERIC_SUPER_TYPES = Collections.unmodifiableSet(new HashSet() {{
        add(PARCELABLE_CLASS_NAME);
        add(SERIALIZABLE_CLASS_NAME);
    }});

    private static final Set IGNORED_TYPE_DECLARATIONS = Collections.unmodifiableSet(new HashSet() {{
        add(Bundle.class.getName());
        add(String.class.getName());
        add(Byte.class.getName());
        add(Short.class.getName());
        add(Integer.class.getName());
        add(Long.class.getName());
        add(Float.class.getName());
        add(Double.class.getName());
        add(Character.class.getName());
        add(Boolean.class.getName());
    }});

    private Types mTypeUtils;
    private Elements mElementUtils;
    private Filer mFiler;
    private Messager mMessager;

    private volatile String mLicenseHeader;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mTypeUtils = processingEnv.getTypeUtils();
        mElementUtils = processingEnv.getElementUtils();
        mFiler = processingEnv.getFiler();
        mMessager = processingEnv.getMessager();
    }

    @Override
    public Set getSupportedAnnotationTypes() {
        Set annotations = new HashSet<>();
        annotations.add(State.class.getName());
        annotations.add(StateReflection.class.getName());
        return Collections.unmodifiableSet(annotations);
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set annotations, RoundEnvironment env) {
        final Set annotatedFields = new HashSet<>();
        annotatedFields.addAll(env.getElementsAnnotatedWith(State.class));
        annotatedFields.addAll(env.getElementsAnnotatedWith(StateReflection.class));

        final Map bundlers = new HashMap<>();

        for (Element field : annotatedFields) {
            if (field.getModifiers().contains(Modifier.STATIC)) {
                mMessager.printMessage(Diagnostic.Kind.ERROR, "Field must not be static", field);
                return true;
            }
            if (field.getModifiers().contains(Modifier.FINAL)) {
                mMessager.printMessage(Diagnostic.Kind.ERROR, "Field must not be final", field);
                return true;
            }

            BundlerWrapper bundlerWrapper = getBundlerWrapper(field);
            if (bundlerWrapper == null && TYPE_MAPPING.get(field.asType().toString()) == null) {
                CompatibilityType compatibilityType = getCompatibilityType(field);
                if (compatibilityType == null) {
                    mMessager.printMessage(Diagnostic.Kind.ERROR, "Don't know how to put " + field.asType() + " into a bundle", field);
                    return true;
                }

                /*
                 * Something really weird happens here. In Kotlin inner classes use a '$' sign instead of a '.'as delimiter,
                 * e.g. field.asType().toString() returns "com.evernote.android.state.test.YNABFormats$Currency"
                 *
                 * After the getCompatibilityType() method the same method call on the same object returns
                 * "com.evernote.android.state.test.YNABFormats.Currency", what is actually expected.
                 *
                 * I don't get why that's the case. But definitely use the new value as key in the map.
                 */
                TYPE_MAPPING.put(field.asType().toString(), compatibilityType.mMapping); // this caches the class
            }
            if (bundlerWrapper != null) {
                Element privateClass = getPrivateClass(mElementUtils.getTypeElement(bundlerWrapper.mGenericName.toString()));
                if (privateClass != null) {
                    mMessager.printMessage(Diagnostic.Kind.ERROR, "Class must not be private", privateClass);
                    return true;
                }
                privateClass = getPrivateClass(mElementUtils.getTypeElement(bundlerWrapper.mBundlerName.toString()));
                if (privateClass != null) {
                    mMessager.printMessage(Diagnostic.Kind.ERROR, "Class must not be private", privateClass);
                    return true;
                }

                bundlers.put(field, bundlerWrapper);
            }
        }

        Map> classes = getClassElements(annotatedFields);
        Set allClassElements = classes.keySet();

        for (Element classElement : allClassElements) {
            Element privateClass = getPrivateClass(classElement);
            if (privateClass != null) {
                mMessager.printMessage(Diagnostic.Kind.ERROR, "Class must not be private", privateClass);
                return true;
            }
        }

        for (Element classElement : allClassElements) {
            final List fields = sorted(classes.get(classElement));
            final Element packageElement = findElement(classElement, ElementKind.PACKAGE);

            String packageName = packageElement.asType().toString();
            if ("package".equals(packageName)) {
                // that's a weird bug in Jack where the package name is always "package"
                String packageJackCompiler = findPackageJackCompiler(classElement);
                packageName = packageJackCompiler == null ? packageName : packageJackCompiler;
            }

            final String className = getClassName(classElement);
            final boolean isView = isAssignable(classElement, View.class);

            final TypeVariableName genericType = TypeVariableName.get("T", TypeName.get(eraseGenericIfNecessary(classElement.asType())));

            final TypeName superTypeName;
            final TypeMirror superType = getSuperType(classElement.asType(), allClassElements);
            if (superType == null) {
                superTypeName = ParameterizedTypeName.get(ClassName.get(isView ? Injector.View.class : Injector.Object.class), genericType);
            } else {
                superTypeName = ParameterizedTypeName.get(ClassName.bestGuess(eraseGenericIfNecessary(superType).toString() + StateSaver.SUFFIX), genericType);
            }

            MethodSpec.Builder saveMethodBuilder = MethodSpec.methodBuilder("save")
                    .addAnnotation(Override.class)
                    .addAnnotation(
                            AnnotationSpec
                                    .builder(SuppressWarnings.class)
                                    .addMember("value", "$S", "unchecked")
                                    .build()
                    )
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(genericType, "target");

            MethodSpec.Builder restoreMethodBuilder = MethodSpec.methodBuilder("restore")
                    .addAnnotation(Override.class)
                    .addAnnotation(
                            AnnotationSpec
                                    .builder(SuppressWarnings.class)
                                    .addMember("value", "$S", "unchecked")
                                    .build()
                    )
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(genericType, "target");

            if (isView) {
                saveMethodBuilder = saveMethodBuilder.returns(Parcelable.class).addParameter(Parcelable.class, "p");
                restoreMethodBuilder = restoreMethodBuilder.returns(Parcelable.class).addParameter(Parcelable.class, "p");

                if (superType != null) {
                    saveMethodBuilder = saveMethodBuilder.addStatement("$T state = HELPER.putParent(super.save(target, p))", Bundle.class);
                } else {
                    saveMethodBuilder = saveMethodBuilder.addStatement("$T state = HELPER.putParent(p)", Bundle.class);
                }
                restoreMethodBuilder = restoreMethodBuilder.addStatement("$T state = ($T) p", Bundle.class, Bundle.class);

            } else {
                saveMethodBuilder = saveMethodBuilder.returns(void.class).addParameter(Bundle.class, "state");
                restoreMethodBuilder = restoreMethodBuilder.returns(void.class).addParameter(Bundle.class, "state");

                if (superType != null) {
                    saveMethodBuilder = saveMethodBuilder.addStatement("super.save(target, state)");
                    restoreMethodBuilder = restoreMethodBuilder.addStatement("super.restore(target, state)");
                }
            }

            CodeBlock.Builder staticInitBlock = CodeBlock.builder();

            for (Element field : fields) {
                String fieldTypeString = field.asType().toString();
                String mapping = TYPE_MAPPING.get(fieldTypeString);

                FieldType fieldType = getFieldType(field);
                String fieldName = fieldType.getFieldName(field);

                BundlerWrapper bundler = bundlers.get(field);
                if (bundler != null) {
                    staticInitBlock = staticInitBlock.addStatement("BUNDLERS.put($S, new $T())", fieldName, bundler.mBundlerName);
                    mapping = "WithBundler";
                }

                switch (fieldType) {
                    case FIELD:
                        saveMethodBuilder = saveMethodBuilder.addStatement("HELPER.put$N(state, $S, target.$N)", mapping, fieldName, fieldName);
                        restoreMethodBuilder = restoreMethodBuilder.addStatement("target.$N = HELPER.get$N(state, $S)", fieldName, mapping, fieldName);
                        break;

                    case BOOLEAN_PROPERTY:
                    case BOOLEAN_PROPERTY_KOTLIN:
                    case PROPERTY:
                        if (fieldType == FieldType.BOOLEAN_PROPERTY || fieldType == FieldType.BOOLEAN_PROPERTY_KOTLIN) {
                            saveMethodBuilder.addStatement("HELPER.put$N(state, $S, target.$N$N())", mapping, fieldName, "is", fieldName);
                        } else {
                            saveMethodBuilder.addStatement("HELPER.put$N(state, $S, target.$N$N())", mapping, fieldName, "get", fieldName);
                        }

                        if (bundler != null) {
                            restoreMethodBuilder = restoreMethodBuilder.addStatement("target.set$N(HELPER.<$T>get$N(state, $S))", fieldName,
                                    bundler.mGenericName, mapping, fieldName);
                        } else {
                            TypeMirror insertedType = getInsertedType(field, true);
                            if (insertedType != null) {
                                restoreMethodBuilder = restoreMethodBuilder.addStatement("target.set$N(HELPER.<$T>get$N(state, $S))",
                                        fieldName, ClassName.get(insertedType), mapping, fieldName);
                            } else {
                                restoreMethodBuilder = restoreMethodBuilder.addStatement("target.set$N(HELPER.get$N(state, $S))",
                                        fieldName, mapping, fieldName);
                            }
                        }
                        break;

                    case FIELD_REFLECTION:
                        String reflectionMapping = isPrimitiveMapping(mapping) ? mapping : "";

                        saveMethodBuilder = saveMethodBuilder
                                .beginControlFlow("try")
                                .addStatement("$T field = target.getClass().getDeclaredField($S)", Field.class, fieldName)
                                .addStatement("boolean accessible = field.isAccessible()")
                                .beginControlFlow("if (!accessible)")
                                .addStatement("field.setAccessible(true)")
                                .endControlFlow()
                                .addStatement("HELPER.put$N(state, $S, ($N) field.get$N(target))", mapping, fieldName, fieldTypeString, reflectionMapping)
                                .beginControlFlow("if (!accessible)")
                                .addStatement("field.setAccessible(false)")
                                .endControlFlow()
                                .nextControlFlow("catch (Exception e)")
                                .addStatement("throw new $T(e)", RuntimeException.class)
                                .endControlFlow();

                        restoreMethodBuilder = restoreMethodBuilder
                                .beginControlFlow("try")
                                .addStatement("$T field = target.getClass().getDeclaredField($S)", Field.class, fieldName)
                                .addStatement("boolean accessible = field.isAccessible()")
                                .beginControlFlow("if (!accessible)")
                                .addStatement("field.setAccessible(true)")
                                .endControlFlow()
                                .addStatement("field.set$N(target, HELPER.get$N(state, $S))", reflectionMapping, mapping, fieldName)
                                .beginControlFlow("if (!accessible)")
                                .addStatement("field.setAccessible(false)")
                                .endControlFlow()
                                .nextControlFlow("catch (Exception e)")
                                .addStatement("throw new $T(e)", RuntimeException.class)
                                .endControlFlow();
                        break;

                    case NOT_SUPPORTED:
                    default:
                        mMessager.printMessage(Diagnostic.Kind.ERROR, "Field must be either non-private or provide a getter and setter method", field);
                        return true;
                }
            }

            if (isView) {
                saveMethodBuilder = saveMethodBuilder.addStatement("return state");
                if (superType != null) {
                    restoreMethodBuilder = restoreMethodBuilder.addStatement("return super.restore(target, HELPER.getParent(state))");
                } else {
                    restoreMethodBuilder = restoreMethodBuilder.addStatement("return HELPER.getParent(state)");
                }
            }

            TypeName bundlerType = ParameterizedTypeName.get(ClassName.get(Bundler.class), WildcardTypeName.subtypeOf(Object.class));
            TypeName bundlerMap = ParameterizedTypeName.get(ClassName.get(HashMap.class), ClassName.get(String.class), bundlerType);

            TypeSpec classBuilder = TypeSpec.classBuilder(className + StateSaver.SUFFIX)
                    .addModifiers(Modifier.PUBLIC)
                    .superclass(superTypeName)
                    .addTypeVariable(genericType)
                    .addField(
                            FieldSpec.builder(bundlerMap, "BUNDLERS")
                                    .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
                                    .initializer("new $T()", bundlerMap)
                                    .build()
                    )
                    .addStaticBlock(staticInitBlock.build())
                    .addField(
                            FieldSpec.builder(InjectionHelper.class, "HELPER")
                                    .addModifiers(Modifier.FINAL, Modifier.STATIC, Modifier.PRIVATE)
                                    .initializer("new $T($S, $N)", InjectionHelper.class, packageName + '.' + className + StateSaver.SUFFIX, "BUNDLERS")
                                    .build()
                    )
                    .addMethod(saveMethodBuilder.build())
                    .addMethod(restoreMethodBuilder.build())
                    .build();

            JavaFile javaFile = JavaFile.builder(packageName, classBuilder).build();
            if (!writeJavaFile(javaFile)) {
                return true; // failed, stop processor here
            }
        }

        return true;
    }

    private boolean writeJavaFile(JavaFile javaFile) {
        StringBuilder builder = new StringBuilder();

        JavaFileObject filerSourceFile = null;

        try {
            builder.append(getLicenseHeader());
            javaFile.writeTo(builder);

            String fileName = javaFile.packageName.isEmpty() ? javaFile.typeSpec.name : javaFile.packageName + "." + javaFile.typeSpec.name;
            List originatingElements = javaFile.typeSpec.originatingElements;
            filerSourceFile = mFiler.createSourceFile(fileName, originatingElements.toArray(new Element[originatingElements.size()]));

            try (Writer writer = filerSourceFile.openWriter()) {
                writer.write(builder.toString());
            }
            return true;

        } catch (Exception e) {
            mMessager.printMessage(Diagnostic.Kind.ERROR, "Couldn't generate classes");
            if (filerSourceFile != null) {
                filerSourceFile.delete();
            }

            return false;
        }
    }

    private Map> getClassElements(Collection annotatedFields) {
        Map> result = new HashMap<>();
        for (Element field : annotatedFields) {
            Element classElement = findElement(field, ElementKind.CLASS);
            Set elements = result.get(classElement);
            if (elements == null) {
                elements = new HashSet<>();
            }
            elements.add(field);
            result.put(classElement, elements);
        }
        return result;
    }

    private Element findElement(Element element, ElementKind kind) {
        Element enclosingElement = element.getEnclosingElement();
        if (enclosingElement == null) {
            return element;
        }
        return element.getKind() == kind ? element : findElement(enclosingElement, kind);
    }

    private String findPackageJackCompiler(Element element) {
        Element enclosingElement = element.getEnclosingElement();
        if (enclosingElement != null && enclosingElement.getKind() == ElementKind.PACKAGE && element.getKind() == ElementKind.CLASS) {
            String className = element.asType().toString();
            int index = className.lastIndexOf('.');
            if (index <= 0) {
                mMessager.printMessage(Diagnostic.Kind.WARNING, "Couldn't find package name with Jack compiler");
                return null;
            }
            return className.substring(0, index);
        }

        return enclosingElement != null ? findPackageJackCompiler(enclosingElement) : null;
    }

    private Element getPrivateClass(Element classElement) {
        if (classElement == null || classElement.getKind() != ElementKind.CLASS) {
            return null;
        } else if (classElement.getModifiers().contains(Modifier.PRIVATE)) {
            return classElement;
        } else {
            return getPrivateClass(classElement.getEnclosingElement());
        }
    }

    private String getClassName(Element classElement) {
        StringBuilder className = new StringBuilder(classElement.getSimpleName().toString());
        Element enclosingElement = classElement.getEnclosingElement();
        while (enclosingElement != null && enclosingElement.getKind() == ElementKind.CLASS) {
            className.insert(0, enclosingElement.getSimpleName() + "$");
            enclosingElement = enclosingElement.getEnclosingElement();
        }
        return className.toString();
    }

    private static String getPropertyFieldName(Element field) {
        String fieldName = field.getSimpleName().toString();
        if (fieldName.length() >= 2 && fieldName.startsWith("m") && Character.isUpperCase(fieldName.charAt(1))) {
            fieldName = fieldName.substring(1);
        }
        return Character.toUpperCase(fieldName.charAt(0)) + (fieldName.length() > 1 ? fieldName.substring(1) : "");
    }

    private FieldType getFieldType(Element field) {
        if (!field.getModifiers().contains(Modifier.PRIVATE)) {
            return FieldType.FIELD;
        }

        if (useReflection(field)) {
            return FieldType.FIELD_REFLECTION;
        }

        String fieldName = getPropertyFieldName(field);
        String getterName = "get" + fieldName;
        String isGetterName = "is" + fieldName;
        String setterName = "set" + fieldName;

        String isGetterNameKotlin = null;
        String setterNameKotlin = null;

        if (fieldName.length() > 2 && fieldName.startsWith("Is") && Character.isUpperCase(fieldName.charAt(2))) {
            // cut off the "is"
            String substring = fieldName.substring(2);
            isGetterNameKotlin = "is" + substring;
            setterNameKotlin = "set" + substring;
        }


        List elements = field.getEnclosingElement().getEnclosedElements();
        boolean hasGetter = false;
        boolean hasIsGetter = false;
        boolean hasSetter = false;
        boolean isKotlinBooleanProperty = false;

        for (Element element : elements) {
            if (element.getKind() != ElementKind.METHOD) {
                continue;
            }
            String elementName = element.getSimpleName().toString();
            if (!hasGetter && getterName.equals(elementName) && !element.getModifiers().contains(Modifier.PRIVATE)) {
                hasGetter = true;
                if (hasSetter) {
                    break;
                }
            }
            if (!hasIsGetter && isGetterName.equals(elementName) && !element.getModifiers().contains(Modifier.PRIVATE)) {
                hasIsGetter = true;
                if (hasSetter) {
                    break;
                }
            }
            if (!hasGetter && isGetterNameKotlin != null && isGetterNameKotlin.equals(elementName) && !element.getModifiers().contains(Modifier.PRIVATE)) {
                hasIsGetter = true;
                isKotlinBooleanProperty = true;
                if (hasSetter) {
                    break;
                }
            }
            if (!hasSetter && setterName.equals(elementName) && !element.getModifiers().contains(Modifier.PRIVATE)) {
                hasSetter = true;
                if (hasGetter) {
                    break;
                }
            }
            if (!hasSetter && setterNameKotlin != null && setterNameKotlin.equals(elementName) && !element.getModifiers().contains(Modifier.PRIVATE)) {
                hasSetter = true;
                if (hasGetter) {
                    break;
                }
            }
        }

        if (isKotlinBooleanProperty && hasSetter) {
            return FieldType.BOOLEAN_PROPERTY_KOTLIN;
        } else if (hasIsGetter && hasSetter) {
            return FieldType.BOOLEAN_PROPERTY;
        } else if (hasGetter && hasSetter) {
            return FieldType.PROPERTY;
        } else {
            return FieldType.NOT_SUPPORTED;
        }
    }

    private enum FieldType {
        FIELD, FIELD_REFLECTION, PROPERTY, BOOLEAN_PROPERTY, BOOLEAN_PROPERTY_KOTLIN, NOT_SUPPORTED;

        public String getFieldName(Element field) {
            switch (this) {
                case FIELD:
                case FIELD_REFLECTION:
                    return field.getSimpleName().toString();
                case PROPERTY:
                case BOOLEAN_PROPERTY:
                    return getPropertyFieldName(field);
                case BOOLEAN_PROPERTY_KOTLIN:
                    return field.getSimpleName().toString().substring(2);
                case NOT_SUPPORTED:
                default:
                    return null;
            }
        }
    }

    private BundlerWrapper getBundlerWrapper(Element field) {
        for (AnnotationMirror annotationMirror : field.getAnnotationMirrors()) {
            if (!isStateAnnotation(annotationMirror)) {
                continue;
            }

            Map elementValues = annotationMirror.getElementValues();
            for (ExecutableElement executableElement : elementValues.keySet()) {
                if ("value".equals(executableElement.getSimpleName().toString())) {
                    Object value = elementValues.get(executableElement).getValue(); // bundler class
                    if (value == null) {
                        continue;
                    }
                    TypeName bundlerName = ClassName.get(mElementUtils.getTypeElement(value.toString()));
                    TypeName genericName = null;

                    try {
                        // gets the generic type Data from `class MyBundler implements Bundler {}`
                        List interfaces = mElementUtils.getTypeElement(value.toString()).getInterfaces();
                        for (TypeMirror anInterface : interfaces) {
                            if (isAssignable(mTypeUtils.erasure(anInterface), Bundler.class)) {
                                List typeArguments = ((DeclaredType) anInterface).getTypeArguments();
                                if (typeArguments != null && typeArguments.size() >= 1) {
                                    TypeMirror genericTypeMirror = typeArguments.get(0);
                                    String genericString = genericTypeMirror.toString();

                                    // this check is necessary for returned types like: List -> remove "? extends"
                                    if (genericString.contains(" for parcelable list bundler
                                        if (PARCELABLE_CLASS_NAME.equals(innerType)) {
                                            TypeMirror insertedType = getInsertedType(field, true);
                                            if (insertedType != null) {
                                                innerType = insertedType.toString();
                                            }
                                        }

                                        ClassName erasureClassName = ClassName.bestGuess(mTypeUtils.erasure(genericTypeMirror).toString());
                                        genericName = ParameterizedTypeName.get(erasureClassName, ClassName.bestGuess(innerType));
                                    } else {
                                        genericName = ClassName.get(genericTypeMirror);
                                    }
                                }
                            }
                        }
                    } catch (Exception ignored) {
                    }
                    return new BundlerWrapper(bundlerName, genericName == null ? ClassName.get(Object.class) : genericName);
                }
            }
        }
        return null;
    }

    private boolean useReflection(Element field) {
        for (AnnotationMirror annotationMirror : field.getAnnotationMirrors()) {
            if (STATE_REFLECTION_CLASS_NAME.equals(annotationMirror.getAnnotationType().toString())) {
                return true;
            }
        }
        return false;
    }

    private boolean isPrimitiveMapping(String mapping) {
        switch (mapping) {
            case "Int":
            case "Short":
            case "Byte":
            case "Long":
            case "Char":
            case "Boolean":
            case "Double":
            case "Float":
                return true;
            default:
                return false;
        }
    }

    private static final class BundlerWrapper {
        final TypeName mBundlerName;
        final TypeName mGenericName;

        private BundlerWrapper(TypeName bundlerName, TypeName genericName) {
            mBundlerName = bundlerName;
            mGenericName = genericName;
        }
    }

    @SuppressWarnings("unused")
    private enum CompatibilityType {
        PARCELABLE("Parcelable", Parcelable.class, null),
        PARCELABLE_LIST("ParcelableArrayList", ArrayList.class, Parcelable.class),
        SPARSE_PARCELABLE_ARRAY("SparseParcelableArray", SparseArray.class, Parcelable.class),
        SERIALIZABLE("Serializable", Serializable.class, null);

        final String mMapping;
        final Class mClass;
        final Class mGenericClass;

        CompatibilityType(String mapping, Class clazz, Class genericClass) {
            mMapping = mapping;
            mClass = clazz;
            mGenericClass = genericClass;
        }
    }

    private CompatibilityType getCompatibilityType(Element field) {
        TypeMirror typeMirror = field.asType();
        for (CompatibilityType compatibilityType : CompatibilityType.values()) {
            if (compatibilityType.mGenericClass == null) {
                if (isAssignable(typeMirror, compatibilityType.mClass)) {
                    return compatibilityType;
                }

            } else if (typeMirror.getKind() != TypeKind.WILDCARD) {
                if (isAssignable(mTypeUtils.erasure(typeMirror), compatibilityType.mClass)) {
                    List typeArguments = ((DeclaredType) typeMirror).getTypeArguments();
                    if (typeArguments != null && typeArguments.size() >= 1 && isAssignable(typeArguments.get(0), compatibilityType.mGenericClass)) {
                        return compatibilityType;
                    }
                }
            }
        }
        return null;
    }

    private boolean isAssignable(Element element, Class clazz) {
        return isAssignable(element.asType(), clazz);
    }

    private boolean isAssignable(TypeMirror typeMirror, Class clazz) {
        return mTypeUtils.isAssignable(typeMirror, mElementUtils.getTypeElement(clazz.getName()).asType());
    }

    private static List sorted(Collection collection) {
        List result = new ArrayList<>(collection);
        Collections.sort(result, COMPARATOR);
        return result;
    }

    private TypeMirror eraseGenericIfNecessary(TypeMirror typeMirror) {
        // is there a better way to detect a generic type?
        return (typeMirror.toString().endsWith(">")) ? mTypeUtils.erasure(typeMirror) : typeMirror;
    }

    private TypeMirror eraseCovarianceAndInvariance(TypeMirror typeMirror) {
        String string = typeMirror.toString();
        if (string.startsWith("? extends") || string.startsWith("? super")) {
            return mTypeUtils.erasure(typeMirror);
        } else {
            return typeMirror;
        }
    }

    private TypeMirror getSuperType(TypeMirror classElement, Set allClassElements) {
        List typeMirrors = mTypeUtils.directSupertypes(classElement);
        while (typeMirrors != null && !typeMirrors.isEmpty()) {
            TypeMirror superClass = typeMirrors.get(0); // interfaces are at the end
            if (OBJECT_CLASS_NAME.equals(superClass.toString())) {
                break;
            }
            if (allClassElements.contains(mTypeUtils.asElement(superClass))) {
                return superClass;
            }
            typeMirrors = mTypeUtils.directSupertypes(superClass);
        }
        return null;
    }

    private TypeMirror getInsertedType(Element field, @SuppressWarnings("SameParameterValue") boolean checkIgnoredTypes) {
        if (field == null) {
            return null;
        }
        return getInsertedType(field.asType(), checkIgnoredTypes);
    }

    private TypeMirror getInsertedType(TypeMirror fieldType, boolean checkIgnoredTypes) {
        fieldType = eraseCovarianceAndInvariance(fieldType);

        TypeElement classElement = mElementUtils.getTypeElement(eraseGenericIfNecessary(fieldType).toString());
        List superTypes = classElement == null ? null : mTypeUtils.directSupertypes(classElement.asType());

        if (fieldType instanceof DeclaredType) {
            List typeArguments = ((DeclaredType) fieldType).getTypeArguments();
            if (typeArguments != null && !typeArguments.isEmpty()) {
                // generic type, return the generic value

                if (superTypes != null && isSuperType(superTypes, GENERIC_SUPER_TYPES) && !fieldType.toString().startsWith(ArrayList.class.getName())) {
                    // if this is generic Parcelable or Serializable, then use the type, ignore ArrayList, which also implements Serializable
                    return fieldType;
                }

                return getInsertedType(typeArguments.get(0), false);
            }
        }

        if (classElement == null || OBJECT_CLASS_NAME.equals(classElement.toString())) {
            return null;
        }

        if (checkIgnoredTypes && IGNORED_TYPE_DECLARATIONS.contains(classElement.toString())) {
            return null;
        }

        if (superTypes != null) {
            if (isSuperType(superTypes, GENERIC_SUPER_TYPES)) {
                // either instance of Serializable or Parcelable
                return fieldType;
            }

            for (TypeMirror superType : superTypes) {
                TypeMirror result = getInsertedType(eraseGenericIfNecessary(superType), checkIgnoredTypes);
                if (result != null) {
                    // always return the passed in type and not any super type
                    return fieldType;
                }
            }
        }
        return null;
    }

    private boolean isSuperType(List typeMirrors, Collection superTypes) {
        for (TypeMirror superType : typeMirrors) {
            if (superTypes.contains(superType.toString())) {
                return true;
            }
        }
        return false;
    }

    private String getLicenseHeader() throws IOException {
        if (mLicenseHeader == null) {
            synchronized (this) {
                if (mLicenseHeader == null) {
                    try (InputStream inputStream = getClass().getResourceAsStream("/license.txt");
                         Scanner scanner = new Scanner(inputStream)) {

                        scanner.useDelimiter("\\A");
                        mLicenseHeader = scanner.next();
                    }
                }
            }
        }
        return mLicenseHeader;
    }

    private boolean isStateAnnotation(AnnotationMirror annotationMirror) {
        String string = annotationMirror.getAnnotationType().toString();
        return STATE_CLASS_NAME.equals(string) || STATE_REFLECTION_CLASS_NAME.equals(string);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy