• 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 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 {
toJavaCode()33     public abstract String toJavaCode();
34 
35     /**
36      * @return whether this ModelClass represents an array.
37      */
isArray()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      */
getComponentType()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      */
isList()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      */
isMap()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      */
isString()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      */
isNullable()81     public abstract boolean isNullable();
82 
83     /**
84      * @return whether or not this ModelClass represents a primitive type.
85      */
isPrimitive()86     public abstract boolean isPrimitive();
87 
88     /**
89      * @return whether or not this ModelClass represents a Java boolean
90      */
isBoolean()91     public abstract boolean isBoolean();
92 
93     /**
94      * @return whether or not this ModelClass represents a Java char
95      */
isChar()96     public abstract boolean isChar();
97 
98     /**
99      * @return whether or not this ModelClass represents a Java byte
100      */
isByte()101     public abstract boolean isByte();
102 
103     /**
104      * @return whether or not this ModelClass represents a Java short
105      */
isShort()106     public abstract boolean isShort();
107 
108     /**
109      * @return whether or not this ModelClass represents a Java int
110      */
isInt()111     public abstract boolean isInt();
112 
113     /**
114      * @return whether or not this ModelClass represents a Java long
115      */
isLong()116     public abstract boolean isLong();
117 
118     /**
119      * @return whether or not this ModelClass represents a Java float
120      */
isFloat()121     public abstract boolean isFloat();
122 
123     /**
124      * @return whether or not this ModelClass represents a Java double
125      */
isDouble()126     public abstract boolean isDouble();
127 
128     /**
129      * @return whether or not this has type parameters
130      */
isGeneric()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      */
getTypeArguments()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      */
isTypeVar()144     public abstract boolean isTypeVar();
145 
146     /**
147      * @return whether this is a wildcard type argument or not.
148      */
isWildcard()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      */
isObject()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      */
isInterface()161     public abstract boolean isInterface();
162 
163     /**
164      * @return whether or not his is a ViewDataBinding subclass.
165      */
isViewDataBinding()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      */
extendsViewStub()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      */
isObservable()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      */
isObservableField()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      */
isVoid()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      */
unbox()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      */
box()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      */
isAssignableFrom(ModelClass that)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      */
getMethods(String name, List<ModelClass> args, boolean staticOnly, boolean allowProtected)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      */
getMethods(String name, int numParameters)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      */
getMethod(String name, List<ModelClass> args, boolean staticOnly, boolean allowProtected)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      */
getSuperclass()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      */
getCanonicalName()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      */
getSimpleName()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      */
erasure()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      */
getMinApi()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      */
getJniDescription()369     public abstract String getJniDescription();
370 
371     /**
372      * Returns a list of all abstract methods in the type.
373      */
374     @NotNull
getAbstractMethods()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      */
findGetterOrField(String name, boolean staticOnly)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 
findInstanceGetter(String name)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 
getField(String name, boolean allowPrivate, boolean isStatic)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 
findSetter(ModelMethod getter, String originalName)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
findMethods(String name, boolean staticOnly)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 
isIncomplete()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 
getDeclaredFields()557     protected abstract ModelField[] getDeclaredFields();
558 
getDeclaredMethods()559     protected abstract ModelMethod[] getDeclaredMethods();
560 
stripFieldName(String fieldName)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