• 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 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 {
toJavaCode()32     public abstract String toJavaCode();
33 
34     /**
35      * @return whether this ModelClass represents an array.
36      */
isArray()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      */
getComponentType()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      */
isList()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      */
isMap()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      */
isString()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      */
isNullable()80     public abstract boolean isNullable();
81 
82     /**
83      * @return whether or not this ModelClass represents a primitive type.
84      */
isPrimitive()85     public abstract boolean isPrimitive();
86 
87     /**
88      * @return whether or not this ModelClass represents a Java boolean
89      */
isBoolean()90     public abstract boolean isBoolean();
91 
92     /**
93      * @return whether or not this ModelClass represents a Java char
94      */
isChar()95     public abstract boolean isChar();
96 
97     /**
98      * @return whether or not this ModelClass represents a Java byte
99      */
isByte()100     public abstract boolean isByte();
101 
102     /**
103      * @return whether or not this ModelClass represents a Java short
104      */
isShort()105     public abstract boolean isShort();
106 
107     /**
108      * @return whether or not this ModelClass represents a Java int
109      */
isInt()110     public abstract boolean isInt();
111 
112     /**
113      * @return whether or not this ModelClass represents a Java long
114      */
isLong()115     public abstract boolean isLong();
116 
117     /**
118      * @return whether or not this ModelClass represents a Java float
119      */
isFloat()120     public abstract boolean isFloat();
121 
122     /**
123      * @return whether or not this ModelClass represents a Java double
124      */
isDouble()125     public abstract boolean isDouble();
126 
127     /**
128      * @return whether or not this has type parameters
129      */
isGeneric()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      */
getTypeArguments()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      */
isTypeVar()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      */
isObject()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      */
isInterface()155     public abstract boolean isInterface();
156 
157     /**
158      * @return whether or not his is a ViewDataBinding subclass.
159      */
isViewDataBinding()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      */
extendsViewStub()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      */
isObservable()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      */
isObservableField()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      */
isVoid()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      */
unbox()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      */
box()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      */
isAssignableFrom(ModelClass that)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      */
getMethods(String name, List<ModelClass> args, boolean staticOnly)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      */
getMethods(String name, int numParameters)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      */
getMethod(String name, List<ModelClass> args, boolean staticOnly)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      */
getSuperclass()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      */
getCanonicalName()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      */
erasure()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      */
getMinApi()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      */
getJniDescription()344     public abstract String getJniDescription();
345 
346     /**
347      * Returns a list of all abstract methods in the type.
348      */
getAbstractMethods()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      */
findGetterOrField(String name, boolean staticOnly)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 
getField(String name, boolean allowPrivate, boolean isStatic)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      */
findMethods(String name, boolean staticOnly)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 
getDeclaredFields()467     protected abstract ModelField[] getDeclaredFields();
468 
getDeclaredMethods()469     protected abstract ModelMethod[] getDeclaredMethods();
470 
stripFieldName(String fieldName)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