ModelClass.java revision 88ce44ccc65e74a8553244ca246cc9f4c48483e0
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 */
16package android.databinding.tool.reflection;
17
18import org.apache.commons.lang3.StringUtils;
19
20import android.databinding.tool.reflection.Callable.Type;
21import android.databinding.tool.util.L;
22
23import java.util.ArrayList;
24import java.util.Arrays;
25import java.util.List;
26
27import static android.databinding.tool.reflection.Callable.CAN_BE_INVALIDATED;
28import static android.databinding.tool.reflection.Callable.DYNAMIC;
29import static android.databinding.tool.reflection.Callable.STATIC;
30
31public 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 this is a wildcard type argument or not.
147     */
148    public abstract boolean isWildcard();
149
150    /**
151     * @return whether or not this ModelClass is java.lang.Object and not a primitive or subclass.
152     */
153    public boolean isObject() {
154        return ModelAnalyzer.getInstance().getObjectType().equals(this);
155    }
156
157    /**
158     * @return whether or not this ModelClass is an interface
159     */
160    public abstract boolean isInterface();
161
162    /**
163     * @return whether or not his is a ViewDataBinding subclass.
164     */
165    public boolean isViewDataBinding() {
166        return ModelAnalyzer.getInstance().getViewDataBindingType().isAssignableFrom(this);
167    }
168
169    /**
170     * @return whether or not this ModelClass type extends ViewStub.
171     */
172    public boolean extendsViewStub() {
173        return ModelAnalyzer.getInstance().getViewStubType().isAssignableFrom(this);
174    }
175
176    /**
177     * @return whether or not this is an Observable type such as ObservableMap, ObservableList,
178     * or Observable.
179     */
180    public boolean isObservable() {
181        ModelAnalyzer modelAnalyzer = ModelAnalyzer.getInstance();
182        return modelAnalyzer.getObservableType().isAssignableFrom(this) ||
183                modelAnalyzer.getObservableListType().isAssignableFrom(this) ||
184                modelAnalyzer.getObservableMapType().isAssignableFrom(this);
185
186    }
187
188    /**
189     * @return whether or not this is an ObservableField, or any of the primitive versions
190     * such as ObservableBoolean and ObservableInt
191     */
192    public boolean isObservableField() {
193        ModelClass erasure = erasure();
194        for (ModelClass observableField : ModelAnalyzer.getInstance().getObservableFieldTypes()) {
195            if (observableField.isAssignableFrom(erasure)) {
196                return true;
197            }
198        }
199        return false;
200    }
201
202    /**
203     * @return whether or not this ModelClass represents a void
204     */
205    public abstract boolean isVoid();
206
207    /**
208     * When this is a boxed type, such as Integer, this will return the unboxed value,
209     * such as int. If this is not a boxed type, this is returned.
210     *
211     * @return The unboxed type of the class that this ModelClass represents or this if it isn't a
212     * boxed type.
213     */
214    public abstract ModelClass unbox();
215
216    /**
217     * When this is a primitive type, such as boolean, this will return the boxed value,
218     * such as Boolean. If this is not a primitive type, this is returned.
219     *
220     * @return The boxed type of the class that this ModelClass represents or this if it isn't a
221     * primitive type.
222     */
223    public abstract ModelClass box();
224
225    /**
226     * Returns whether or not the type associated with <code>that</code> can be assigned to
227     * the type associated with this ModelClass. If this and that only require boxing or unboxing
228     * then true is returned.
229     *
230     * @param that the ModelClass to compare.
231     * @return true if <code>that</code> requires only boxing or if <code>that</code> is an
232     * implementation of or subclass of <code>this</code>.
233     */
234    public abstract boolean isAssignableFrom(ModelClass that);
235
236    /**
237     * Returns an array containing all public methods on the type represented by this ModelClass
238     * with the name <code>name</code> and can take the passed-in types as arguments. This will
239     * also work if the arguments match VarArgs parameter.
240     *
241     * @param name The name of the method to find.
242     * @param args The types that the method should accept.
243     * @param staticOnly Whether only static methods should be returned or both instance methods
244     *                 and static methods are valid.
245     *
246     * @return An array containing all public methods with the name <code>name</code> and taking
247     * <code>args</code> parameters.
248     */
249    public ModelMethod[] getMethods(String name, List<ModelClass> args, boolean staticOnly) {
250        ModelMethod[] methods = getDeclaredMethods();
251        ArrayList<ModelMethod> matching = new ArrayList<ModelMethod>();
252        for (ModelMethod method : methods) {
253            if (method.isPublic() && (!staticOnly || method.isStatic()) &&
254                    name.equals(method.getName()) && method.acceptsArguments(args)) {
255                matching.add(method);
256            }
257        }
258        return matching.toArray(new ModelMethod[matching.size()]);
259    }
260
261    /**
262     * Returns all public instance methods with the given name and number of parameters.
263     *
264     * @param name The name of the method to find.
265     * @param numParameters The number of parameters that the method should take
266     * @return An array containing all public methods with the given name and number of parameters.
267     */
268    public ModelMethod[] getMethods(String name, int numParameters) {
269        ModelMethod[] methods = getDeclaredMethods();
270        ArrayList<ModelMethod> matching = new ArrayList<ModelMethod>();
271        for (ModelMethod method : methods) {
272            if (method.isPublic() && !method.isStatic() &&
273                    name.equals(method.getName()) &&
274                    method.getParameterTypes().length == numParameters) {
275                matching.add(method);
276            }
277        }
278        return matching.toArray(new ModelMethod[matching.size()]);
279    }
280
281    /**
282     * Returns the public method with the name <code>name</code> with the parameters that
283     * best match args. <code>staticOnly</code> governs whether a static or instance method
284     * will be returned. If no matching method was found, null is returned.
285     *
286     * @param name The method name to find
287     * @param args The arguments that the method should accept
288     * @param staticOnly true if the returned method must be static or false if it does not
289     *                     matter.
290     */
291    public ModelMethod getMethod(String name, List<ModelClass> args, boolean staticOnly) {
292        ModelMethod[] methods = getMethods(name, args, staticOnly);
293        L.d("looking methods for %s. static only ? %s . method count: %d", name, staticOnly,
294                methods.length);
295        for (ModelMethod method : methods) {
296            L.d("method: %s, %s", method.getName(), method.isStatic());
297        }
298        if (methods.length == 0) {
299            return null;
300        }
301        ModelMethod bestMethod = methods[0];
302        for (int i = 1; i < methods.length; i++) {
303            if (methods[i].isBetterArgMatchThan(bestMethod, args)) {
304                bestMethod = methods[i];
305            }
306        }
307        return bestMethod;
308    }
309
310    /**
311     * If this represents a class, the super class that it extends is returned. If this
312     * represents an interface, the interface that this extends is returned.
313     * <code>null</code> is returned if this is not a class or interface, such as an int, or
314     * if it is java.lang.Object or an interface that does not extend any other type.
315     *
316     * @return The class or interface that this ModelClass extends or null.
317     */
318    public abstract ModelClass getSuperclass();
319
320    /**
321     * @return A String representation of the class or interface that this represents, not
322     * including any type arguments.
323     */
324    public String getCanonicalName() {
325        return erasure().toJavaCode();
326    }
327
328    /**
329     * @return The class or interface name of this type or the primitive type if it isn't a
330     * reference type.
331     */
332    public String getSimpleName() {
333        final String canonicalName = getCanonicalName();
334        final int dotIndex = canonicalName.lastIndexOf('.');
335        if (dotIndex >= 0) {
336            return canonicalName.substring(dotIndex + 1);
337        }
338        return canonicalName;
339    }
340
341    /**
342     * Returns this class type without any generic type arguments.
343     * @return this class type without any generic type arguments.
344     */
345    public abstract ModelClass erasure();
346
347    /**
348     * Since when this class is available. Important for Binding expressions so that we don't
349     * call non-existing APIs when setting UI.
350     *
351     * @return The SDK_INT where this method was added. If it is not a framework method, should
352     * return 1.
353     */
354    public int getMinApi() {
355        return SdkUtil.getMinApi(this);
356    }
357
358    /**
359     * Returns the JNI description of the method which can be used to lookup it in SDK.
360     * @see TypeUtil
361     */
362    public abstract String getJniDescription();
363
364    /**
365     * Returns a list of all abstract methods in the type.
366     */
367    public List<ModelMethod> getAbstractMethods() {
368        ArrayList<ModelMethod> abstractMethods = new ArrayList<ModelMethod>();
369        ModelMethod[] methods = getDeclaredMethods();
370        for (ModelMethod method : methods) {
371            if (method.isAbstract()) {
372                abstractMethods.add(method);
373            }
374        }
375        return abstractMethods;
376    }
377
378    /**
379     * Returns the getter method or field that the name refers to.
380     * @param name The name of the field or the body of the method name -- can be name(),
381     *             getName(), or isName().
382     * @param staticOnly Whether this should look for static methods and fields or instance
383     *                     versions
384     * @return the getter method or field that the name refers to or null if none can be found.
385     */
386    public Callable findGetterOrField(String name, boolean staticOnly) {
387        if ("length".equals(name) && isArray()) {
388            return new Callable(Type.FIELD, name, ModelAnalyzer.getInstance().loadPrimitive("int"),
389                    0, 0);
390        }
391        String capitalized = StringUtils.capitalize(name);
392        String[] methodNames = {
393                "get" + capitalized,
394                "is" + capitalized,
395                name
396        };
397        for (String methodName : methodNames) {
398            ModelMethod[] methods = getMethods(methodName, new ArrayList<ModelClass>(), staticOnly);
399            for (ModelMethod method : methods) {
400                if (method.isPublic() && (!staticOnly || method.isStatic()) &&
401                        !method.getReturnType(Arrays.asList(method.getParameterTypes())).isVoid()) {
402                    int flags = DYNAMIC;
403                    if (method.isStatic()) {
404                        flags |= STATIC;
405                    }
406                    if (method.isBindable()) {
407                        flags |= CAN_BE_INVALIDATED;
408                    } else {
409                        // if method is not bindable, look for a backing field
410                        final ModelField backingField = getField(name, true, method.isStatic());
411                        L.d("backing field for method %s is %s", method.getName(),
412                                backingField == null ? "NOT FOUND" : backingField.getName());
413                        if (backingField != null && backingField.isBindable()) {
414                            flags |= CAN_BE_INVALIDATED;
415                        }
416                    }
417                    final Callable result = new Callable(Callable.Type.METHOD, methodName,
418                            method.getReturnType(null), method.getParameterTypes().length,
419                            flags);
420                    return result;
421                }
422            }
423        }
424
425        // could not find a method. Look for a public field
426        ModelField publicField = null;
427        if (staticOnly) {
428            publicField = getField(name, false, true);
429        } else {
430            // first check non-static
431            publicField = getField(name, false, false);
432            if (publicField == null) {
433                // check for static
434                publicField = getField(name, false, true);
435            }
436        }
437        if (publicField == null) {
438            return null;
439        }
440        ModelClass fieldType = publicField.getFieldType();
441        int flags = 0;
442        if (!publicField.isFinal()) {
443            flags |= DYNAMIC;
444        }
445        if (publicField.isBindable()) {
446            flags |= CAN_BE_INVALIDATED;
447        }
448        if (publicField.isStatic()) {
449            flags |= STATIC;
450        }
451        return new Callable(Callable.Type.FIELD, name, fieldType, 0, flags);
452    }
453
454    private ModelField getField(String name, boolean allowPrivate, boolean isStatic) {
455        ModelField[] fields = getDeclaredFields();
456        for (ModelField field : fields) {
457            boolean nameMatch = name.equals(field.getName()) ||
458                    name.equals(stripFieldName(field.getName()));
459            if (nameMatch && field.isStatic() == isStatic &&
460                    (allowPrivate || field.isPublic())) {
461                return field;
462            }
463        }
464        return null;
465    }
466
467    /**
468     * Finds public methods that matches the given name exactly. These may be resolved into
469     * listener methods during Expr.resolveListeners.
470     */
471    public List<ModelMethod> findMethods(String name, boolean staticOnly) {
472        ModelMethod[] methods = getDeclaredMethods();
473        ArrayList<ModelMethod> matching = new ArrayList<ModelMethod>();
474        for (ModelMethod method : methods) {
475            if (method.getName().equals(name) && (!staticOnly || method.isStatic()) &&
476                    method.isPublic()) {
477                matching.add(method);
478            }
479        }
480        if (matching.isEmpty()) {
481            return null;
482        }
483        return matching;
484    }
485
486    public boolean isIncomplete() {
487        if (isTypeVar() || isWildcard()) {
488            return true;
489        }
490        List<ModelClass> typeArgs = getTypeArguments();
491        if (typeArgs != null) {
492            for (ModelClass typeArg : typeArgs) {
493                if (typeArg.isIncomplete()) {
494                    return true;
495                }
496            }
497        }
498        return false;
499    }
500
501    protected abstract ModelField[] getDeclaredFields();
502
503    protected abstract ModelMethod[] getDeclaredMethods();
504
505    private static String stripFieldName(String fieldName) {
506        // TODO: Make this configurable through IntelliJ
507        if (fieldName.length() > 2) {
508            final char start = fieldName.charAt(2);
509            if (fieldName.startsWith("m_") && Character.isJavaIdentifierStart(start)) {
510                return Character.toLowerCase(start) + fieldName.substring(3);
511            }
512        }
513        if (fieldName.length() > 1) {
514            final char start = fieldName.charAt(1);
515            final char fieldIdentifier = fieldName.charAt(0);
516            final boolean strip;
517            if (fieldIdentifier == '_') {
518                strip = true;
519            } else if (fieldIdentifier == 'm' && Character.isJavaIdentifierStart(start) &&
520                    !Character.isLowerCase(start)) {
521                strip = true;
522            } else {
523                strip = false; // not mUppercase format
524            }
525            if (strip) {
526                return Character.toLowerCase(start) + fieldName.substring(2);
527            }
528        }
529        return fieldName;
530    }
531}
532