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