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