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