Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5b9cab18 authored by George Mount's avatar George Mount
Browse files

Move common parts of AnnotationAnalyzer methods to ModelAnalyzer.

Bug 19643846
Bug 19627630

Also made it so that setter methods will auto-cast from Object
when necessary. This is useful for heterogenous map objects
where .get(id) may return an Integer or a String or a Drawable.

Change-Id: Iacfd739ea4938f38b584a8eab9193f1fd4071df1
parent 243a1e3e
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -145,7 +145,7 @@ abstract public class Expr {
    }

    public boolean isObservable() {
        return ModelAnalyzer.getInstance().isObservable(getResolvedType());
        return getResolvedType().isObservable();
    }

    public BitSet getShouldReadFlags() {
+3 −3
Original line number Diff line number Diff line
@@ -101,8 +101,8 @@ public class FieldAccessExpr extends Expr {
            ModelClass resolvedType = child.getResolvedType();
            L.d("resolving %s. Resolved type: %s", this, resolvedType);

            mGetter = modelAnalyzer.findMethodOrField(resolvedType, mName, isStatic);
            if (modelAnalyzer.isObservableField(mGetter.resolvedType)) {
            mGetter = resolvedType.findGetterOrField(mName, isStatic);
            if (mGetter.resolvedType.isObservableField()) {
                // Make this the ".get()" and add an extra field access for the observable field
                child.getParents().remove(this);
                getChildren().remove(child);
@@ -112,7 +112,7 @@ public class FieldAccessExpr extends Expr {

                getChildren().add(observableField);
                observableField.getParents().add(this);
                mGetter = modelAnalyzer.findMethodOrField(mGetter.resolvedType, "get", false);
                mGetter = mGetter.resolvedType.findGetterOrField("get", false);
                mName = "";
            }
        }
+14 −1
Original line number Diff line number Diff line
@@ -18,9 +18,12 @@ package com.android.databinding.expr;

import com.google.common.collect.Iterables;

import com.android.databinding.reflection.Callable.Type;
import com.android.databinding.reflection.ModelAnalyzer;
import com.android.databinding.reflection.Callable;
import com.android.databinding.reflection.ModelClass;
import com.android.databinding.reflection.ModelMethod;
import com.android.databinding.util.L;

import java.util.ArrayList;
import java.util.Arrays;
@@ -52,7 +55,17 @@ public class MethodCallExpr extends Expr {

            Expr target = getTarget();
            boolean isStatic = target instanceof StaticIdentifierExpr;
            mGetter = modelAnalyzer.findMethod(target.getResolvedType(), mName, args, isStatic);
            ModelMethod method = target.getResolvedType().getMethod(mName, args, isStatic);
            if (method == null) {
                String message = "cannot find method '" + mName + "' in class " +
                        target.getResolvedType().toJavaCode();
                IllegalArgumentException e = new IllegalArgumentException(message);
                L.e(e, "cannot find method %s in class %s", mName,
                        target.getResolvedType().toJavaCode());
                throw e;
            }
            mGetter = new Callable(Type.METHOD, method.getName(), method.getReturnType(args), true,
                    false);
        }
        return mGetter.resolvedType;
    }
+95 −18
Original line number Diff line number Diff line
@@ -19,11 +19,20 @@ import com.google.common.base.Preconditions;
import com.android.databinding.reflection.annotation.AnnotationAnalyzer;
import com.android.databinding.util.L;

import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.util.Types;

/**
 * This is the base class for several implementations of something that
 * acts like a ClassLoader. Different implementations work with the Annotation
 * Processor, ClassLoader, and an Android Studio plugin.
 */
public abstract class ModelAnalyzer {

    public static final String[] LIST_CLASS_NAMES = {
@@ -42,8 +51,6 @@ public abstract class ModelAnalyzer {

    public static final String OBJECT_CLASS_NAME = "java.lang.Object";

    static ModelAnalyzer instance;

    public static final String OBSERVABLE_CLASS_NAME = "android.binding.Observable";

    public static final String OBSERVABLE_LIST_CLASS_NAME = "android.binding.ObservableList";
@@ -65,28 +72,22 @@ public abstract class ModelAnalyzer {
    public static final String VIEW_DATA_BINDING =
            "com.android.databinding.library.ViewDataBinding";

    private ModelClass[] mListTypes;
    private ModelClass mMapType;
    private ModelClass mStringType;
    private ModelClass mObjectType;
    private ModelClass mObservableType;
    private ModelClass mObservableListType;
    private ModelClass mObservableMapType;
    private ModelClass[] mObservableFieldTypes;
    private ModelClass mViewBindingType;

    private static ModelAnalyzer sAnalyzer;

    protected void setInstance(ModelAnalyzer analyzer) {
        sAnalyzer = analyzer;
    }

    public abstract boolean isDataBinder(ModelClass modelClass);

    public abstract Callable findMethod(ModelClass modelClass, String name,
            List<ModelClass> args, boolean staticAccess);

    public abstract boolean isObservable(ModelClass modelClass);

    public abstract boolean isObservableField(ModelClass modelClass);

    public abstract boolean isBindable(ModelField field);

    public abstract boolean isBindable(ModelMethod method);

    public abstract Callable findMethodOrField(ModelClass modelClass, String name,
            boolean staticAccess);

    public ModelClass findCommonParentOf(ModelClass modelClass1,
            ModelClass modelClass2) {
        ModelClass curr = modelClass1;
@@ -212,4 +213,80 @@ public abstract class ModelAnalyzer {
    public abstract ModelClass findClass(Class classType);

    public abstract TypeUtil createTypeUtil();

    ModelClass[] getListTypes() {
        if (mListTypes == null) {
            mListTypes = new ModelClass[LIST_CLASS_NAMES.length];
            for (int i = 0; i < mListTypes.length; i++) {
                final ModelClass modelClass = findClass(LIST_CLASS_NAMES[i], null);
                if (modelClass != null) {
                    mListTypes[i] = modelClass.erasure();
                }
            }
        }
        return mListTypes;
    }

    public ModelClass getMapType() {
        if (mMapType == null) {
            mMapType = loadClassErasure(MAP_CLASS_NAME);
        }
        return mMapType;
    }

    ModelClass getStringType() {
        if (mStringType == null) {
            mStringType = findClass(STRING_CLASS_NAME, null);
        }
        return mStringType;
    }

    ModelClass getObjectType() {
        if (mObjectType == null) {
            mObjectType = findClass(OBJECT_CLASS_NAME, null);
        }
        return mObjectType;
    }

    ModelClass getObservableType() {
        if (mObservableType == null) {
            mObservableType = findClass(OBSERVABLE_CLASS_NAME, null);
        }
        return mObservableType;
    }

    ModelClass getObservableListType() {
        if (mObservableListType == null) {
            mObservableListType = loadClassErasure(OBSERVABLE_LIST_CLASS_NAME);
        }
        return mObservableListType;
    }

    ModelClass getObservableMapType() {
        if (mObservableMapType == null) {
            mObservableMapType = loadClassErasure(OBSERVABLE_MAP_CLASS_NAME);
        }
        return mObservableMapType;
    }

    ModelClass getViewDataBindingType() {
        if (mViewBindingType == null) {
            mViewBindingType = findClass(VIEW_DATA_BINDING, null);
        }
        return mViewBindingType;
    }

    ModelClass[] getObservableFieldTypes() {
        if (mObservableFieldTypes == null) {
            mObservableFieldTypes = new ModelClass[OBSERVABLE_FIELDS.length];
            for (int i = 0; i < OBSERVABLE_FIELDS.length; i++) {
                mObservableFieldTypes[i] = loadClassErasure(OBSERVABLE_FIELDS[i]);
            }
        }
        return mObservableFieldTypes;
    }

    private ModelClass loadClassErasure(String className) {
        return findClass(className, null).erasure();
    }
}
+324 −27
Original line number Diff line number Diff line
@@ -15,55 +15,267 @@
 */
package com.android.databinding.reflection;

public interface ModelClass {
import com.android.databinding.util.L;

    String toJavaCode();
import org.apache.commons.lang3.StringUtils;

    boolean isArray();
import java.util.ArrayList;
import java.util.List;

    ModelClass getComponentType();
public abstract class ModelClass {

    boolean isList();
    public abstract String toJavaCode();

    boolean isMap();
    /**
     * @return whether this ModelClass represents an array.
     */
    public abstract boolean isArray();

    /**
     * For arrays, lists, and maps, this returns the contained value. For other types, null
     * is returned.
     *
     * @return The component type for arrays, the value type for maps, and the element type
     * for lists.
     */
    public abstract ModelClass getComponentType();

    boolean isString();
    /**
     * @return Whether or not this ModelClass can be treated as a List. This means
     * it is a java.util.List, or one of the Sparse*Array classes.
     */
    public boolean isList() {
        for (ModelClass listType : ModelAnalyzer.getInstance().getListTypes()) {
            if (listType != null) {
                if (listType.isAssignableFrom(this)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * @return whether or not this ModelClass can be considered a Map or not.
     */
    public boolean isMap()  {
        return ModelAnalyzer.getInstance().getMapType().isAssignableFrom(erasure());
    }

    boolean isNullable();
    /**
     * @return whether or not this ModelClass is a java.lang.String.
     */
    public boolean isString() {
        return ModelAnalyzer.getInstance().getStringType().equals(this);
    }

    boolean isPrimitive();
    /**
     * @return whether or not this ModelClass represents a Reference type.
     */
    public abstract boolean isNullable();

    boolean isBoolean();
    /**
     * @return whether or not this ModelClass represents a primitive type.
     */
    public abstract boolean isPrimitive();

    boolean isChar();
    /**
     * @return whether or not this ModelClass represents a Java boolean
     */
    public abstract boolean isBoolean();

    boolean isByte();
    /**
     * @return whether or not this ModelClass represents a Java char
     */
    public abstract boolean isChar();

    boolean isShort();
    /**
     * @return whether or not this ModelClass represents a Java byte
     */
    public abstract boolean isByte();

    boolean isInt();
    /**
     * @return whether or not this ModelClass represents a Java short
     */
    public abstract boolean isShort();

    boolean isLong();
    /**
     * @return whether or not this ModelClass represents a Java int
     */
    public abstract boolean isInt();

    boolean isFloat();
    /**
     * @return whether or not this ModelClass represents a Java long
     */
    public abstract boolean isLong();

    boolean isDouble();
    /**
     * @return whether or not this ModelClass represents a Java float
     */
    public abstract boolean isFloat();

    boolean isObject();
    /**
     * @return whether or not this ModelClass represents a Java double
     */
    public abstract boolean isDouble();

    boolean isVoid();
    /**
     * @return whether or not this ModelClass is java.lang.Object and not a primitive or subclass.
     */
    public boolean isObject() {
        return ModelAnalyzer.getInstance().getObjectType().equals(this);
    }

    ModelClass unbox();
    /**
     * @return whether or not this is an Observable type such as ObservableMap, ObservableList,
     * or Observable.
     */
    public boolean isObservable() {
        ModelAnalyzer modelAnalyzer = ModelAnalyzer.getInstance();
        return modelAnalyzer.getObservableType().isAssignableFrom(this) ||
                modelAnalyzer.getObservableListType().isAssignableFrom(this) ||
                modelAnalyzer.getObservableMapType().isAssignableFrom(this);

    ModelClass box();
    }

    boolean isAssignableFrom(ModelClass that);
    /**
     * @return whether or not this is an ObservableField, or any of the primitive versions
     * such as ObservableBoolean and ObservableInt
     */
    public boolean isObservableField() {
        ModelClass erasure = erasure();
        for (ModelClass observableField : ModelAnalyzer.getInstance().getObservableFieldTypes()) {
            if (observableField.isAssignableFrom(erasure)) {
                return true;
            }
        }
        return false;
    }

    ModelMethod[] getMethods(String name, int numParameters);
    /**
     * @return whether or not this ModelClass represents a void
     */
    public abstract boolean isVoid();

    ModelClass getSuperclass();
    /**
     * When this is a boxed type, such as Integer, this will return the unboxed value,
     * such as int. If this is not a boxed type, this is returned.
     *
     * @return The unboxed type of the class that this ModelClass represents or this if it isn't a
     * boxed type.
     */
    public abstract ModelClass unbox();

    String getCanonicalName();
    /**
     * When this is a primitive type, such as boolean, this will return the boxed value,
     * such as Boolean. If this is not a primitive type, this is returned.
     *
     * @return The boxed type of the class that this ModelClass represents or this if it isn't a
     * primitive type.
     */
    public abstract ModelClass box();

    /**
     * Returns whether or not the type associated with <code>that</code> can be assigned to
     * the type associated with this ModelClass. If this and that only require boxing or unboxing
     * then true is returned.
     *
     * @param that the ModelClass to compare.
     * @return true if <code>that</code> requires only boxing or if <code>that</code> is an
     * implementation of or subclass of <code>this</code>.
     */
    public abstract boolean isAssignableFrom(ModelClass that);

    /**
     * Returns an array containing all public methods on the type represented by this ModelClass
     * with the name <code>name</code> and can take the passed-in types as arguments. This will
     * also work if the arguments match VarArgs parameter.
     *
     * @param name The name of the method to find.
     * @param args The types that the method should accept.
     * @param isStatic Whether only static methods should be returned or instance methods.
     * @return An array containing all public methods with the name <code>name</code> and taking
     * <code>args</code> parameters.
     */
    public ModelMethod[] getMethods(String name, List<ModelClass> args, boolean isStatic) {
        ModelMethod[] methods = getDeclaredMethods();
        ArrayList<ModelMethod> matching = new ArrayList<ModelMethod>();
        for (ModelMethod method : methods) {
            if (method.isPublic() && method.isStatic() == isStatic &&
                    name.equals(method.getName()) && method.acceptsArguments(args)) {
                matching.add(method);
            }
        }
        return matching.toArray(new ModelMethod[matching.size()]);
    }

    /**
     * Returns all public instance methods with the given name and number of parameters.
     *
     * @param name The name of the method to find.
     * @param numParameters The number of parameters that the method should take
     * @return An array containing all public methods with the given name and number of parameters.
     */
    public ModelMethod[] getMethods(String name, int numParameters) {
        ModelMethod[] methods = getDeclaredMethods();
        ArrayList<ModelMethod> matching = new ArrayList<ModelMethod>();
        for (ModelMethod method : methods) {
            if (method.isPublic() && !method.isStatic() &&
                    name.equals(method.getName()) &&
                    method.getParameterTypes().length == numParameters) {
                matching.add(method);
            }
        }
        return matching.toArray(new ModelMethod[matching.size()]);
    }

    /**
     * Returns the public method with the name <code>name</code> with the parameters that
     * best match args. <code>staticAccess</code> governs whether a static or instance method
     * will be returned. If no matching method was found, null is returned.
     *
     * @param name The method name to find
     * @param args The arguments that the method should accept
     * @param staticAccess true if the returned method should be static or false if it should
     *                     be an instance method.
     */
    public ModelMethod getMethod(String name, List<ModelClass> args, boolean staticAccess) {
        ModelMethod[] methods = getMethods(name, args, staticAccess);
        if (methods.length == 0) {
            return null;
        }
        ModelMethod bestMethod = methods[0];
        for (int i = 1; i < methods.length; i++) {
            if (methods[i].isBetterArgMatchThan(bestMethod, args)) {
                bestMethod = methods[i];
            }
        }
        return bestMethod;
    }

    /**
     * If this represents a class, the super class that it extends is returned. If this
     * represents an interface, the interface that this extends is returned.
     * <code>null</code> is returned if this is not a class or interface, such as an int, or
     * if it is java.lang.Object or an interface that does not extend any other type.
     *
     * @return The class or interface that this ModelClass extends or null.
     */
    public abstract ModelClass getSuperclass();

    /**
     * @return A String representation of the class or interface that this represents, not
     * including any type arguments.
     */
    public String getCanonicalName() {
        return erasure().toJavaCode();
    }

    /**
     * Returns this class type without any generic type arguments.
     * @return this class type without any generic type arguments.
     */
    public abstract ModelClass erasure();

    /**
     * Since when this class is available. Important for Binding expressions so that we don't
@@ -72,11 +284,96 @@ public interface ModelClass {
     * @return The SDK_INT where this method was added. If it is not a framework method, should
     * return 1.
     */
    int getMinApi();
    public int getMinApi() {
        return SdkUtil.getMinApi(this);
    }

    /**
     * Returns the JNI description of the method which can be used to lookup it in SDK.
     * @see com.android.databinding.reflection.TypeUtil
     */
    String getJniDescription();
    public abstract String getJniDescription();

    /**
     * Returns the getter method or field that the name refers to.
     * @param name The name of the field or the body of the method name -- can be name(),
     *             getName(), or isName().
     * @param staticAccess Whether this should look for static methods and fields or instance
     *                     versions
     * @return the getter method or field that the name refers to.
     * @throws IllegalArgumentException if there is no such method or field available.
     */
    public Callable findGetterOrField(String name, boolean staticAccess) {
        String capitalized = StringUtils.capitalize(name);
        String[] methodNames = {
                "get" + capitalized,
                "is" + capitalized,
                name
        };
        final ModelField backingField = getField(name, true, staticAccess);
        L.d("Finding getter or field for %s, field = %s", name, backingField == null ? null : backingField.getName());
        for (String methodName : methodNames) {
            ModelMethod[] methods = getMethods(methodName, 0);
            for (ModelMethod method : methods) {
                if (method.isPublic() && method.isStatic() == staticAccess) {
                    final Callable result = new Callable(Callable.Type.METHOD, methodName,
                            method.getReturnType(null), true, method.isBindable() ||
                            (backingField != null && backingField.isBindable()));
                    L.d("backing field for %s is %s", result, backingField);
                    return result;
                }
            }
        }

        if (backingField != null && backingField.isPublic()) {
            ModelClass fieldType = backingField.getFieldType();
            return new Callable(Callable.Type.FIELD, name, fieldType,
                    !backingField.isFinal() || fieldType.isObservable(), backingField.isBindable());
        }
        throw new IllegalArgumentException(
                "cannot find " + name + " in " + toJavaCode());

    }

    public ModelField getField(String name, boolean allowPrivate, boolean staticAccess) {
        ModelField[] fields = getDeclaredFields();
        for (ModelField field : fields) {
            if (name.equals(stripFieldName(field.getName())) && field.isStatic() == staticAccess &&
                    (allowPrivate || !field.isPublic())) {
                return field;
            }
        }
        return null;
    }

    protected abstract ModelField[] getDeclaredFields();

    protected abstract ModelMethod[] getDeclaredMethods();

    private static String stripFieldName(String fieldName) {
        // TODO: Make this configurable through IntelliJ
        if (fieldName.length() > 2) {
            final char start = fieldName.charAt(2);
            if (fieldName.startsWith("m_") && Character.isJavaIdentifierStart(start)) {
                return Character.toLowerCase(start) + fieldName.substring(3);
            }
        }
        if (fieldName.length() > 1) {
            final char start = fieldName.charAt(1);
            final char fieldIdentifier = fieldName.charAt(0);
            final boolean strip;
            if (fieldIdentifier == '_') {
                strip = true;
            } else if (fieldIdentifier == 'm' && Character.isJavaIdentifierStart(start) &&
                    !Character.isLowerCase(start)) {
                strip = true;
            } else {
                strip = false; // not mUppercase format
            }
            if (strip) {
                return Character.toLowerCase(start) + fieldName.substring(2);
            }
        }
        return fieldName;
    }
}
Loading