IntrospectionHelper.java

/*
 * Copyright 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package androidx.appsearch.compiler;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;

import com.google.auto.common.MoreTypes;
import com.google.auto.value.AutoValue;
import com.squareup.javapoet.ClassName;

import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;

/**
 * Utilities for working with data structures representing parsed Java code.
 *
 * @hide
 */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class IntrospectionHelper {
    @VisibleForTesting
    static final String GEN_CLASS_PREFIX = "$$__AppSearch__";
    static final String APPSEARCH_PKG = "androidx.appsearch.app";
    static final String APPSEARCH_EXCEPTION_PKG = "androidx.appsearch.exceptions";
    static final String APPSEARCH_EXCEPTION_SIMPLE_NAME = "AppSearchException";
    static final String DOCUMENT_ANNOTATION_CLASS = "androidx.appsearch.annotation.Document";
    static final String ID_CLASS = "androidx.appsearch.annotation.Document.Id";
    static final String NAMESPACE_CLASS = "androidx.appsearch.annotation.Document.Namespace";
    static final String CREATION_TIMESTAMP_MILLIS_CLASS =
            "androidx.appsearch.annotation.Document.CreationTimestampMillis";
    static final String TTL_MILLIS_CLASS = "androidx.appsearch.annotation.Document.TtlMillis";
    static final String SCORE_CLASS = "androidx.appsearch.annotation.Document.Score";
    final TypeMirror mCollectionType;
    final TypeMirror mListType;
    final TypeMirror mStringType;
    final TypeMirror mIntegerBoxType;
    final TypeMirror mIntPrimitiveType;
    final TypeMirror mLongBoxType;
    final TypeMirror mLongPrimitiveType;
    final TypeMirror mFloatBoxType;
    final TypeMirror mFloatPrimitiveType;
    final TypeMirror mDoubleBoxType;
    final TypeMirror mDoublePrimitiveType;
    final TypeMirror mBooleanBoxType;
    final TypeMirror mBooleanPrimitiveType;
    final TypeMirror mByteBoxType;
    final TypeMirror mByteBoxArrayType;
    final TypeMirror mBytePrimitiveType;
    final TypeMirror mBytePrimitiveArrayType;
    private final ProcessingEnvironment mEnv;
    private final Types mTypeUtils;

    IntrospectionHelper(ProcessingEnvironment env) {
        mEnv = env;

        Elements elementUtil = env.getElementUtils();
        mTypeUtils = env.getTypeUtils();
        mCollectionType = elementUtil.getTypeElement(Collection.class.getName()).asType();
        mListType = elementUtil.getTypeElement(List.class.getName()).asType();
        mStringType = elementUtil.getTypeElement(String.class.getName()).asType();
        mIntegerBoxType = elementUtil.getTypeElement(Integer.class.getName()).asType();
        mIntPrimitiveType = mTypeUtils.unboxedType(mIntegerBoxType);
        mLongBoxType = elementUtil.getTypeElement(Long.class.getName()).asType();
        mLongPrimitiveType = mTypeUtils.unboxedType(mLongBoxType);
        mFloatBoxType = elementUtil.getTypeElement(Float.class.getName()).asType();
        mFloatPrimitiveType = mTypeUtils.unboxedType(mFloatBoxType);
        mDoubleBoxType = elementUtil.getTypeElement(Double.class.getName()).asType();
        mDoublePrimitiveType = mTypeUtils.unboxedType(mDoubleBoxType);
        mBooleanBoxType = elementUtil.getTypeElement(Boolean.class.getName()).asType();
        mBooleanPrimitiveType = mTypeUtils.unboxedType(mBooleanBoxType);
        mByteBoxType = elementUtil.getTypeElement(Byte.class.getName()).asType();
        mByteBoxArrayType = mTypeUtils.getArrayType(mByteBoxType);
        mBytePrimitiveType = mTypeUtils.unboxedType(mByteBoxType);
        mBytePrimitiveArrayType = mTypeUtils.getArrayType(mBytePrimitiveType);
    }

    /**
     * Returns {@code androidx.appsearch.annotation.Document} annotation element from the input
     * element's annotations. Returns null if no such annotation is found.
     */
    @Nullable
    public static AnnotationMirror getDocumentAnnotation(@NonNull Element element) {
        Objects.requireNonNull(element);
        for (AnnotationMirror annotation : element.getAnnotationMirrors()) {
            String annotationFq = annotation.getAnnotationType().toString();
            if (IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS.equals(annotationFq)) {
                return annotation;
            }
        }
        return null;
    }

    /** Checks whether the property data type is one of the valid types. */
    public boolean isFieldOfExactType(VariableElement property, TypeMirror... validTypes) {
        TypeMirror propertyType = property.asType();
        for (TypeMirror validType : validTypes) {
            if (propertyType.getKind() == TypeKind.ARRAY) {
                if (mTypeUtils.isSameType(
                        ((ArrayType) propertyType).getComponentType(), validType)) {
                    return true;
                }
            } else if (mTypeUtils.isAssignable(mTypeUtils.erasure(propertyType), mCollectionType)) {
                if (mTypeUtils.isSameType(
                        ((DeclaredType) propertyType).getTypeArguments().get(0), validType)) {
                    return true;
                }
            } else if (mTypeUtils.isSameType(property.asType(), validType)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Checks whether the property data class has {@code androidx.appsearch.annotation.Document
     * .DocumentProperty} annotation.
     */
    public boolean isFieldOfDocumentType(VariableElement property) {
        TypeMirror propertyType = property.asType();
        AnnotationMirror documentAnnotation = null;

        if (propertyType.getKind() == TypeKind.ARRAY) {
            documentAnnotation = getDocumentAnnotation(
                    mTypeUtils.asElement(((ArrayType) property.asType()).getComponentType()));
        } else if (mTypeUtils.isAssignable(mTypeUtils.erasure(propertyType), mCollectionType)) {
            documentAnnotation = getDocumentAnnotation(mTypeUtils.asElement(
                    ((DeclaredType) propertyType).getTypeArguments().get(0)));
        } else {
            documentAnnotation = getDocumentAnnotation(mTypeUtils.asElement(propertyType));
        }
        return documentAnnotation != null;
    }

    public Map<String, Object> getAnnotationParams(@NonNull AnnotationMirror annotation) {
        Map<? extends ExecutableElement, ? extends AnnotationValue> values =
                mEnv.getElementUtils().getElementValuesWithDefaults(annotation);
        Map<String, Object> ret = new HashMap<>();
        for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry :
                values.entrySet()) {
            String key = entry.getKey().getSimpleName().toString();
            ret.put(key, entry.getValue().getValue());
        }
        return ret;
    }

    /**
     * Creates the name of output class. $$__AppSearch__Foo for Foo, $$__AppSearch__Foo$$__Bar
     * for inner class Foo.Bar.
     */
    public ClassName getDocumentClassFactoryForClass(String pkg, String className) {
        String genClassName = GEN_CLASS_PREFIX + className.replace(".", "$$__");
        return ClassName.get(pkg, genClassName);
    }

    /**
     * Creates the name of output class. $$__AppSearch__Foo for Foo, $$__AppSearch__Foo$$__Bar
     * for inner class Foo.Bar.
     */
    public ClassName getDocumentClassFactoryForClass(ClassName clazz) {
        String className = clazz.canonicalName().substring(clazz.packageName().length() + 1);
        return getDocumentClassFactoryForClass(clazz.packageName(), className);
    }

    public ClassName getAppSearchClass(String clazz, String... nested) {
        return ClassName.get(APPSEARCH_PKG, clazz, nested);
    }

    public ClassName getAppSearchExceptionClass() {
        return ClassName.get(APPSEARCH_EXCEPTION_PKG, APPSEARCH_EXCEPTION_SIMPLE_NAME);
    }

    /**
     * Get a list of super classes of element annotated with @Document, in order starting with the
     * class at the top of the hierarchy and descending down the class hierarchy
     */
    @NonNull
    public static Collection<TypeElement> generateClassHierarchy(@NonNull TypeElement element,
            boolean isAutoValueDocument)
            throws ProcessingException {
        Deque<TypeElement> hierarchy = new ArrayDeque<>();
        if (isAutoValueDocument) {
            // We don't allow classes annotated with both Document and AutoValue to extend classes.
            // Because of how AutoValue is set up, there is no way to add a constructor to
            // populate fields of super classes.
            // There should just be the generated class and the original annotated class
            TypeElement superClass = MoreTypes.asTypeElement(
                    MoreTypes.asTypeElement(element.getSuperclass()).getSuperclass());

            if (!superClass.getQualifiedName().contentEquals(Object.class.getCanonicalName())) {
                throw new ProcessingException(
                        "A class annotated with AutoValue and Document cannot have a superclass",
                        element);
            }
            hierarchy.add(element);
        } else {
            TypeElement currentClass = element;
            while (!currentClass.getQualifiedName()
                    .contentEquals(Object.class.getCanonicalName())) {
                // If you inherit from an AutoValue class, you have to implement the static methods.
                // That defeats the purpose of AutoValue
                if (currentClass.getAnnotation(AutoValue.class) != null) {
                    throw new ProcessingException(
                            "A class annotated with Document cannot inherit from a class "
                                    + "annotated with AutoValue", element);
                }

                if (getDocumentAnnotation(currentClass) != null) {
                    hierarchy.addFirst(currentClass);
                }

                currentClass = MoreTypes.asTypeElement(currentClass.getSuperclass());
            }
        }
        return hierarchy;
    }

    enum PropertyClass {
        BOOLEAN_PROPERTY_CLASS("androidx.appsearch.annotation.Document.BooleanProperty"),
        BYTES_PROPERTY_CLASS("androidx.appsearch.annotation.Document.BytesProperty"),
        DOCUMENT_PROPERTY_CLASS("androidx.appsearch.annotation.Document.DocumentProperty"),
        DOUBLE_PROPERTY_CLASS("androidx.appsearch.annotation.Document.DoubleProperty"),
        LONG_PROPERTY_CLASS("androidx.appsearch.annotation.Document.LongProperty"),
        STRING_PROPERTY_CLASS("androidx.appsearch.annotation.Document.StringProperty");

        private final String mClassFullPath;

        PropertyClass(String classFullPath) {
            mClassFullPath = classFullPath;
        }

        String getClassFullPath() {
            return mClassFullPath;
        }

        boolean isPropertyClass(String annotationFq) {
            return mClassFullPath.equals(annotationFq);
        }
    }
}