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