Home | History | Annotate | Download | only in reflection
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package android.databinding.tool.reflection;
     17 
     18 import android.databinding.tool.reflection.Callable.Type;
     19 import android.databinding.tool.util.L;
     20 import android.databinding.tool.util.StringUtils;
     21 
     22 import org.jetbrains.annotations.NotNull;
     23 
     24 import java.util.ArrayList;
     25 import java.util.Arrays;
     26 import java.util.List;
     27 
     28 import static android.databinding.tool.reflection.Callable.CAN_BE_INVALIDATED;
     29 import static android.databinding.tool.reflection.Callable.DYNAMIC;
     30 import static android.databinding.tool.reflection.Callable.STATIC;
     31 
     32 public abstract class ModelClass {
     33     public abstract String toJavaCode();
     34 
     35     /**
     36      * @return whether this ModelClass represents an array.
     37      */
     38     public abstract boolean isArray();
     39 
     40     /**
     41      * For arrays, lists, and maps, this returns the contained value. For other types, null
     42      * is returned.
     43      *
     44      * @return The component type for arrays, the value type for maps, and the element type
     45      * for lists.
     46      */
     47     public abstract ModelClass getComponentType();
     48 
     49     /**
     50      * @return Whether or not this ModelClass can be treated as a List. This means
     51      * it is a java.util.List, or one of the Sparse*Array classes.
     52      */
     53     public boolean isList() {
     54         for (ModelClass listType : ModelAnalyzer.getInstance().getListTypes()) {
     55             if (listType != null) {
     56                 if (listType.isAssignableFrom(this)) {
     57                     return true;
     58                 }
     59             }
     60         }
     61         return false;
     62     }
     63 
     64     /**
     65      * @return whether or not this ModelClass can be considered a Map or not.
     66      */
     67     public boolean isMap()  {
     68         return ModelAnalyzer.getInstance().getMapType().isAssignableFrom(erasure());
     69     }
     70 
     71     /**
     72      * @return whether or not this ModelClass is a java.lang.String.
     73      */
     74     public boolean isString() {
     75         return ModelAnalyzer.getInstance().getStringType().equals(this);
     76     }
     77 
     78     /**
     79      * @return whether or not this ModelClass represents a Reference type.
     80      */
     81     public abstract boolean isNullable();
     82 
     83     /**
     84      * @return whether or not this ModelClass represents a primitive type.
     85      */
     86     public abstract boolean isPrimitive();
     87 
     88     /**
     89      * @return whether or not this ModelClass represents a Java boolean
     90      */
     91     public abstract boolean isBoolean();
     92 
     93     /**
     94      * @return whether or not this ModelClass represents a Java char
     95      */
     96     public abstract boolean isChar();
     97 
     98     /**
     99      * @return whether or not this ModelClass represents a Java byte
    100      */
    101     public abstract boolean isByte();
    102 
    103     /**
    104      * @return whether or not this ModelClass represents a Java short
    105      */
    106     public abstract boolean isShort();
    107 
    108     /**
    109      * @return whether or not this ModelClass represents a Java int
    110      */
    111     public abstract boolean isInt();
    112 
    113     /**
    114      * @return whether or not this ModelClass represents a Java long
    115      */
    116     public abstract boolean isLong();
    117 
    118     /**
    119      * @return whether or not this ModelClass represents a Java float
    120      */
    121     public abstract boolean isFloat();
    122 
    123     /**
    124      * @return whether or not this ModelClass represents a Java double
    125      */
    126     public abstract boolean isDouble();
    127 
    128     /**
    129      * @return whether or not this has type parameters
    130      */
    131     public abstract boolean isGeneric();
    132 
    133     /**
    134      * @return a list of Generic type paramters for the class. For example, if the class
    135      * is List<T>, then the return value will be a list containing T. null is returned
    136      * if this is not a generic type
    137      */
    138     public abstract List<ModelClass> getTypeArguments();
    139 
    140     /**
    141      * @return whether this is a type variable. For example, in List&lt;T>, T is a type variable.
    142      * However, List&lt;String>, String is not a type variable.
    143      */
    144     public abstract boolean isTypeVar();
    145 
    146     /**
    147      * @return whether this is a wildcard type argument or not.
    148      */
    149     public abstract boolean isWildcard();
    150 
    151     /**
    152      * @return whether or not this ModelClass is java.lang.Object and not a primitive or subclass.
    153      */
    154     public boolean isObject() {
    155         return ModelAnalyzer.getInstance().getObjectType().equals(this);
    156     }
    157 
    158     /**
    159      * @return whether or not this ModelClass is an interface
    160      */
    161     public abstract boolean isInterface();
    162 
    163     /**
    164      * @return whether or not his is a ViewDataBinding subclass.
    165      */
    166     public boolean isViewDataBinding() {
    167         return ModelAnalyzer.getInstance().getViewDataBindingType().isAssignableFrom(this);
    168     }
    169 
    170     /**
    171      * @return whether or not this ModelClass type extends ViewStub.
    172      */
    173     public boolean extendsViewStub() {
    174         return ModelAnalyzer.getInstance().getViewStubType().isAssignableFrom(this);
    175     }
    176 
    177     /**
    178      * @return whether or not this is an Observable type such as ObservableMap, ObservableList,
    179      * or Observable.
    180      */
    181     public boolean isObservable() {
    182         ModelAnalyzer modelAnalyzer = ModelAnalyzer.getInstance();
    183         return modelAnalyzer.getObservableType().isAssignableFrom(this) ||
    184                 modelAnalyzer.getObservableListType().isAssignableFrom(this) ||
    185                 modelAnalyzer.getObservableMapType().isAssignableFrom(this);
    186 
    187     }
    188 
    189     /**
    190      * @return whether or not this is an ObservableField, or any of the primitive versions
    191      * such as ObservableBoolean and ObservableInt
    192      */
    193     public boolean isObservableField() {
    194         ModelClass erasure = erasure();
    195         for (ModelClass observableField : ModelAnalyzer.getInstance().getObservableFieldTypes()) {
    196             if (observableField.isAssignableFrom(erasure)) {
    197                 return true;
    198             }
    199         }
    200         return false;
    201     }
    202 
    203     /**
    204      * @return whether or not this ModelClass represents a void
    205      */
    206     public abstract boolean isVoid();
    207 
    208     /**
    209      * When this is a boxed type, such as Integer, this will return the unboxed value,
    210      * such as int. If this is not a boxed type, this is returned.
    211      *
    212      * @return The unboxed type of the class that this ModelClass represents or this if it isn't a
    213      * boxed type.
    214      */
    215     public abstract ModelClass unbox();
    216 
    217     /**
    218      * When this is a primitive type, such as boolean, this will return the boxed value,
    219      * such as Boolean. If this is not a primitive type, this is returned.
    220      *
    221      * @return The boxed type of the class that this ModelClass represents or this if it isn't a
    222      * primitive type.
    223      */
    224     public abstract ModelClass box();
    225 
    226     /**
    227      * Returns whether or not the type associated with <code>that</code> can be assigned to
    228      * the type associated with this ModelClass. If this and that only require boxing or unboxing
    229      * then true is returned.
    230      *
    231      * @param that the ModelClass to compare.
    232      * @return true if <code>that</code> requires only boxing or if <code>that</code> is an
    233      * implementation of or subclass of <code>this</code>.
    234      */
    235     public abstract boolean isAssignableFrom(ModelClass that);
    236 
    237     /**
    238      * Returns an array containing all public methods (or protected if allowProtected is true)
    239      * on the type represented by this ModelClass with the name <code>name</code> and can
    240      * take the passed-in types as arguments. This will also work if the arguments match
    241      * VarArgs parameter.
    242      *
    243      * @param name The name of the method to find.
    244      * @param args The types that the method should accept.
    245      * @param staticOnly Whether only static methods should be returned or both instance methods
    246      *                 and static methods are valid.
    247      * @param allowProtected true if the method can be protected as well as public.
    248      *
    249      * @return An array containing all public methods with the name <code>name</code> and taking
    250      * <code>args</code> parameters.
    251      */
    252     public ModelMethod[] getMethods(String name, List<ModelClass> args, boolean staticOnly,
    253             boolean allowProtected) {
    254         ModelMethod[] methods = getDeclaredMethods();
    255         ArrayList<ModelMethod> matching = new ArrayList<ModelMethod>();
    256         for (ModelMethod method : methods) {
    257             if ((method.isPublic() || (allowProtected && method.isProtected())) &&
    258                     (!staticOnly || method.isStatic()) &&
    259                     name.equals(method.getName()) && method.acceptsArguments(args)) {
    260                 matching.add(method);
    261             }
    262         }
    263         return matching.toArray(new ModelMethod[matching.size()]);
    264     }
    265 
    266     /**
    267      * Returns all public instance methods with the given name and number of parameters.
    268      *
    269      * @param name The name of the method to find.
    270      * @param numParameters The number of parameters that the method should take
    271      * @return An array containing all public methods with the given name and number of parameters.
    272      */
    273     public ModelMethod[] getMethods(String name, int numParameters) {
    274         ModelMethod[] methods = getDeclaredMethods();
    275         ArrayList<ModelMethod> matching = new ArrayList<ModelMethod>();
    276         for (ModelMethod method : methods) {
    277             if (method.isPublic() && !method.isStatic() &&
    278                     name.equals(method.getName()) &&
    279                     method.getParameterTypes().length == numParameters) {
    280                 matching.add(method);
    281             }
    282         }
    283         return matching.toArray(new ModelMethod[matching.size()]);
    284     }
    285 
    286     /**
    287      * Returns the public method with the name <code>name</code> with the parameters that
    288      * best match args. <code>staticOnly</code> governs whether a static or instance method
    289      * will be returned. If no matching method was found, null is returned.
    290      *
    291      * @param name The method name to find
    292      * @param args The arguments that the method should accept
    293      * @param staticOnly true if the returned method must be static or false if it does not
    294      *                     matter.
    295      * @param allowProtected true if the method can be protected as well as public.
    296      */
    297     public ModelMethod getMethod(String name, List<ModelClass> args, boolean staticOnly,
    298             boolean allowProtected) {
    299         ModelMethod[] methods = getMethods(name, args, staticOnly, allowProtected);
    300         L.d("looking methods for %s. static only ? %s . method count: %d", name, staticOnly,
    301                 methods.length);
    302         for (ModelMethod method : methods) {
    303             L.d("method: %s, %s", method.getName(), method.isStatic());
    304         }
    305         if (methods.length == 0) {
    306             return null;
    307         }
    308         ModelMethod bestMethod = methods[0];
    309         for (int i = 1; i < methods.length; i++) {
    310             if (methods[i].isBetterArgMatchThan(bestMethod, args)) {
    311                 bestMethod = methods[i];
    312             }
    313         }
    314         return bestMethod;
    315     }
    316 
    317     /**
    318      * If this represents a class, the super class that it extends is returned. If this
    319      * represents an interface, the interface that this extends is returned.
    320      * <code>null</code> is returned if this is not a class or interface, such as an int, or
    321      * if it is java.lang.Object or an interface that does not extend any other type.
    322      *
    323      * @return The class or interface that this ModelClass extends or null.
    324      */
    325     public abstract ModelClass getSuperclass();
    326 
    327     /**
    328      * @return A String representation of the class or interface that this represents, not
    329      * including any type arguments.
    330      */
    331     public String getCanonicalName() {
    332         return erasure().toJavaCode();
    333     }
    334 
    335     /**
    336      * @return The class or interface name of this type or the primitive type if it isn't a
    337      * reference type.
    338      */
    339     public String getSimpleName() {
    340         final String canonicalName = getCanonicalName();
    341         final int dotIndex = canonicalName.lastIndexOf('.');
    342         if (dotIndex >= 0) {
    343             return canonicalName.substring(dotIndex + 1);
    344         }
    345         return canonicalName;
    346     }
    347 
    348     /**
    349      * Returns this class type without any generic type arguments.
    350      * @return this class type without any generic type arguments.
    351      */
    352     public abstract ModelClass erasure();
    353 
    354     /**
    355      * Since when this class is available. Important for Binding expressions so that we don't
    356      * call non-existing APIs when setting UI.
    357      *
    358      * @return The SDK_INT where this method was added. If it is not a framework method, should
    359      * return 1.
    360      */
    361     public int getMinApi() {
    362         return SdkUtil.getMinApi(this);
    363     }
    364 
    365     /**
    366      * Returns the JNI description of the method which can be used to lookup it in SDK.
    367      * @see TypeUtil
    368      */
    369     public abstract String getJniDescription();
    370 
    371     /**
    372      * Returns a list of all abstract methods in the type.
    373      */
    374     @NotNull
    375     public List<ModelMethod> getAbstractMethods() {
    376         ArrayList<ModelMethod> abstractMethods = new ArrayList<ModelMethod>();
    377         ModelMethod[] methods = getDeclaredMethods();
    378         for (ModelMethod method : methods) {
    379             if (method.isAbstract()) {
    380                 abstractMethods.add(method);
    381             }
    382         }
    383         return abstractMethods;
    384     }
    385 
    386     /**
    387      * Returns the getter method or field that the name refers to.
    388      * @param name The name of the field or the body of the method name -- can be name(),
    389      *             getName(), or isName().
    390      * @param staticOnly Whether this should look for static methods and fields or instance
    391      *                     versions
    392      * @return the getter method or field that the name refers to or null if none can be found.
    393      */
    394     public Callable findGetterOrField(String name, boolean staticOnly) {
    395         if ("length".equals(name) && isArray()) {
    396             return new Callable(Type.FIELD, name, null,
    397                     ModelAnalyzer.getInstance().loadPrimitive("int"), 0, 0, null);
    398         }
    399         String capitalized = StringUtils.capitalize(name);
    400         String[] methodNames = {
    401                 "get" + capitalized,
    402                 "is" + capitalized,
    403                 name
    404         };
    405         for (String methodName : methodNames) {
    406             ModelMethod[] methods =
    407                     getMethods(methodName, new ArrayList<ModelClass>(), staticOnly, false);
    408             for (ModelMethod method : methods) {
    409                 if (method.isPublic() && (!staticOnly || method.isStatic()) &&
    410                         !method.getReturnType(Arrays.asList(method.getParameterTypes())).isVoid()) {
    411                     int flags = DYNAMIC;
    412                     if (method.isStatic()) {
    413                         flags |= STATIC;
    414                     }
    415                     if (method.isBindable()) {
    416                         flags |= CAN_BE_INVALIDATED;
    417                     } else {
    418                         // if method is not bindable, look for a backing field
    419                         final ModelField backingField = getField(name, true, method.isStatic());
    420                         L.d("backing field for method %s is %s", method.getName(),
    421                                 backingField == null ? "NOT FOUND" : backingField.getName());
    422                         if (backingField != null && backingField.isBindable()) {
    423                             flags |= CAN_BE_INVALIDATED;
    424                         }
    425                     }
    426                     final ModelMethod setterMethod = findSetter(method, name);
    427                     final String setterName = setterMethod == null ? null : setterMethod.getName();
    428                     final Callable result = new Callable(Callable.Type.METHOD, methodName,
    429                             setterName, method.getReturnType(null), method.getParameterTypes().length,
    430                             flags, method);
    431                     return result;
    432                 }
    433             }
    434         }
    435 
    436         // could not find a method. Look for a public field
    437         ModelField publicField = null;
    438         if (staticOnly) {
    439             publicField = getField(name, false, true);
    440         } else {
    441             // first check non-static
    442             publicField = getField(name, false, false);
    443             if (publicField == null) {
    444                 // check for static
    445                 publicField = getField(name, false, true);
    446             }
    447         }
    448         if (publicField == null) {
    449             return null;
    450         }
    451         ModelClass fieldType = publicField.getFieldType();
    452         int flags = 0;
    453         String setterFieldName = name;
    454         if (publicField.isStatic()) {
    455             flags |= STATIC;
    456         }
    457         if (!publicField.isFinal()) {
    458             setterFieldName = null;
    459             flags |= DYNAMIC;
    460         }
    461         if (publicField.isBindable()) {
    462             flags |= CAN_BE_INVALIDATED;
    463         }
    464         return new Callable(Callable.Type.FIELD, name, setterFieldName, fieldType, 0, flags, null);
    465     }
    466 
    467     public ModelMethod findInstanceGetter(String name) {
    468         String capitalized = StringUtils.capitalize(name);
    469         String[] methodNames = {
    470                 "get" + capitalized,
    471                 "is" + capitalized,
    472                 name
    473         };
    474         for (String methodName : methodNames) {
    475             ModelMethod[] methods =
    476                     getMethods(methodName, new ArrayList<ModelClass>(), false, false);
    477             for (ModelMethod method : methods) {
    478                 if (method.isPublic() && !method.isStatic() &&
    479                         !method.getReturnType(Arrays.asList(method.getParameterTypes())).isVoid()) {
    480                     return method;
    481                 }
    482             }
    483         }
    484         return null;
    485     }
    486 
    487     private ModelField getField(String name, boolean allowPrivate, boolean isStatic) {
    488         ModelField[] fields = getDeclaredFields();
    489         for (ModelField field : fields) {
    490             boolean nameMatch = name.equals(field.getName()) ||
    491                     name.equals(stripFieldName(field.getName()));
    492             if (nameMatch && field.isStatic() == isStatic &&
    493                     (allowPrivate || field.isPublic())) {
    494                 return field;
    495             }
    496         }
    497         return null;
    498     }
    499 
    500     private ModelMethod findSetter(ModelMethod getter, String originalName) {
    501         final String capitalized = StringUtils.capitalize(originalName);
    502         final String[] possibleNames;
    503         if (originalName.equals(getter.getName())) {
    504             possibleNames = new String[] { originalName, "set" + capitalized };
    505         } else if (getter.getName().startsWith("is")){
    506             possibleNames = new String[] { "set" + capitalized, "setIs" + capitalized };
    507         } else {
    508             possibleNames = new String[] { "set" + capitalized };
    509         }
    510         for (String name : possibleNames) {
    511             List<ModelMethod> methods = findMethods(name, getter.isStatic());
    512             ModelClass param = getter.getReturnType(null);
    513             for (ModelMethod method : methods) {
    514                 ModelClass[] parameterTypes = method.getParameterTypes();
    515                 if (parameterTypes != null && parameterTypes.length == 1 &&
    516                         parameterTypes[0].equals(param) &&
    517                         method.isStatic() == getter.isStatic()) {
    518                     return method;
    519                 }
    520             }
    521         }
    522         return null;
    523     }
    524 
    525     /**
    526      * Finds public methods that matches the given name exactly. These may be resolved into
    527      * listener methods during Expr.resolveListeners.
    528      */
    529     @NotNull
    530     public List<ModelMethod> findMethods(String name, boolean staticOnly) {
    531         ModelMethod[] methods = getDeclaredMethods();
    532         ArrayList<ModelMethod> matching = new ArrayList<ModelMethod>();
    533         for (ModelMethod method : methods) {
    534             if (method.getName().equals(name) && (!staticOnly || method.isStatic()) &&
    535                     method.isPublic()) {
    536                 matching.add(method);
    537             }
    538         }
    539         return matching;
    540     }
    541 
    542     public boolean isIncomplete() {
    543         if (isTypeVar() || isWildcard()) {
    544             return true;
    545         }
    546         List<ModelClass> typeArgs = getTypeArguments();
    547         if (typeArgs != null) {
    548             for (ModelClass typeArg : typeArgs) {
    549                 if (typeArg.isIncomplete()) {
    550                     return true;
    551                 }
    552             }
    553         }
    554         return false;
    555     }
    556 
    557     protected abstract ModelField[] getDeclaredFields();
    558 
    559     protected abstract ModelMethod[] getDeclaredMethods();
    560 
    561     private static String stripFieldName(String fieldName) {
    562         // TODO: Make this configurable through IntelliJ
    563         if (fieldName.length() > 2) {
    564             final char start = fieldName.charAt(2);
    565             if (fieldName.startsWith("m_") && Character.isJavaIdentifierStart(start)) {
    566                 return Character.toLowerCase(start) + fieldName.substring(3);
    567             }
    568         }
    569         if (fieldName.length() > 1) {
    570             final char start = fieldName.charAt(1);
    571             final char fieldIdentifier = fieldName.charAt(0);
    572             final boolean strip;
    573             if (fieldIdentifier == '_') {
    574                 strip = true;
    575             } else if (fieldIdentifier == 'm' && Character.isJavaIdentifierStart(start) &&
    576                     !Character.isLowerCase(start)) {
    577                 strip = true;
    578             } else {
    579                 strip = false; // not mUppercase format
    580             }
    581             if (strip) {
    582                 return Character.toLowerCase(start) + fieldName.substring(2);
    583             }
    584         }
    585         return fieldName;
    586     }
    587 }
    588