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