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