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