/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.databinding.tool.reflection; import android.databinding.tool.reflection.Callable.Type; import android.databinding.tool.util.L; import android.databinding.tool.util.StringUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import static android.databinding.tool.reflection.Callable.CAN_BE_INVALIDATED; import static android.databinding.tool.reflection.Callable.DYNAMIC; import static android.databinding.tool.reflection.Callable.STATIC; public abstract class ModelClass { public abstract String toJavaCode(); /** * @return whether this ModelClass represents an array. */ public abstract boolean isArray(); /** * For arrays, lists, and maps, this returns the contained value. For other types, null * is returned. * * @return The component type for arrays, the value type for maps, and the element type * for lists. */ public abstract ModelClass getComponentType(); /** * @return Whether or not this ModelClass can be treated as a List. This means * it is a java.util.List, or one of the Sparse*Array classes. */ public boolean isList() { for (ModelClass listType : ModelAnalyzer.getInstance().getListTypes()) { if (listType != null) { if (listType.isAssignableFrom(this)) { return true; } } } return false; } /** * @return whether or not this ModelClass can be considered a Map or not. */ public boolean isMap() { return ModelAnalyzer.getInstance().getMapType().isAssignableFrom(erasure()); } /** * @return whether or not this ModelClass is a java.lang.String. */ public boolean isString() { return ModelAnalyzer.getInstance().getStringType().equals(this); } /** * @return whether or not this ModelClass represents a Reference type. */ public abstract boolean isNullable(); /** * @return whether or not this ModelClass represents a primitive type. */ public abstract boolean isPrimitive(); /** * @return whether or not this ModelClass represents a Java boolean */ public abstract boolean isBoolean(); /** * @return whether or not this ModelClass represents a Java char */ public abstract boolean isChar(); /** * @return whether or not this ModelClass represents a Java byte */ public abstract boolean isByte(); /** * @return whether or not this ModelClass represents a Java short */ public abstract boolean isShort(); /** * @return whether or not this ModelClass represents a Java int */ public abstract boolean isInt(); /** * @return whether or not this ModelClass represents a Java long */ public abstract boolean isLong(); /** * @return whether or not this ModelClass represents a Java float */ public abstract boolean isFloat(); /** * @return whether or not this ModelClass represents a Java double */ public abstract boolean isDouble(); /** * @return whether or not this has type parameters */ public abstract boolean isGeneric(); /** * @return a list of Generic type paramters for the class. For example, if the class * is List<T>, then the return value will be a list containing T. null is returned * if this is not a generic type */ public abstract List getTypeArguments(); /** * @return whether this is a type variable. For example, in List<T>, T is a type variable. * However, List<String>, String is not a type variable. */ public abstract boolean isTypeVar(); /** * @return whether this is a wildcard type argument or not. */ public abstract boolean isWildcard(); /** * @return whether or not this ModelClass is java.lang.Object and not a primitive or subclass. */ public boolean isObject() { return ModelAnalyzer.getInstance().getObjectType().equals(this); } /** * @return whether or not this ModelClass is an interface */ public abstract boolean isInterface(); /** * @return whether or not his is a ViewDataBinding subclass. */ public boolean isViewDataBinding() { return ModelAnalyzer.getInstance().getViewDataBindingType().isAssignableFrom(this); } /** * @return whether or not this ModelClass type extends ViewStub. */ public boolean extendsViewStub() { return ModelAnalyzer.getInstance().getViewStubType().isAssignableFrom(this); } /** * @return whether or not this is an Observable type such as ObservableMap, ObservableList, * or Observable. */ public boolean isObservable() { ModelAnalyzer modelAnalyzer = ModelAnalyzer.getInstance(); return modelAnalyzer.getObservableType().isAssignableFrom(this) || modelAnalyzer.getObservableListType().isAssignableFrom(this) || modelAnalyzer.getObservableMapType().isAssignableFrom(this); } /** * @return whether or not this is an ObservableField, or any of the primitive versions * such as ObservableBoolean and ObservableInt */ public boolean isObservableField() { ModelClass erasure = erasure(); for (ModelClass observableField : ModelAnalyzer.getInstance().getObservableFieldTypes()) { if (observableField.isAssignableFrom(erasure)) { return true; } } return false; } /** * @return whether or not this ModelClass represents a void */ public abstract boolean isVoid(); /** * When this is a boxed type, such as Integer, this will return the unboxed value, * such as int. If this is not a boxed type, this is returned. * * @return The unboxed type of the class that this ModelClass represents or this if it isn't a * boxed type. */ public abstract ModelClass unbox(); /** * When this is a primitive type, such as boolean, this will return the boxed value, * such as Boolean. If this is not a primitive type, this is returned. * * @return The boxed type of the class that this ModelClass represents or this if it isn't a * primitive type. */ public abstract ModelClass box(); /** * Returns whether or not the type associated with that can be assigned to * the type associated with this ModelClass. If this and that only require boxing or unboxing * then true is returned. * * @param that the ModelClass to compare. * @return true if that requires only boxing or if that is an * implementation of or subclass of this. */ public abstract boolean isAssignableFrom(ModelClass that); /** * Returns an array containing all public methods on the type represented by this ModelClass * with the name name and can take the passed-in types as arguments. This will * also work if the arguments match VarArgs parameter. * * @param name The name of the method to find. * @param args The types that the method should accept. * @param staticOnly Whether only static methods should be returned or both instance methods * and static methods are valid. * * @return An array containing all public methods with the name name and taking * args parameters. */ public ModelMethod[] getMethods(String name, List args, boolean staticOnly) { ModelMethod[] methods = getDeclaredMethods(); ArrayList matching = new ArrayList(); for (ModelMethod method : methods) { if (method.isPublic() && (!staticOnly || method.isStatic()) && name.equals(method.getName()) && method.acceptsArguments(args)) { matching.add(method); } } return matching.toArray(new ModelMethod[matching.size()]); } /** * Returns all public instance methods with the given name and number of parameters. * * @param name The name of the method to find. * @param numParameters The number of parameters that the method should take * @return An array containing all public methods with the given name and number of parameters. */ public ModelMethod[] getMethods(String name, int numParameters) { ModelMethod[] methods = getDeclaredMethods(); ArrayList matching = new ArrayList(); for (ModelMethod method : methods) { if (method.isPublic() && !method.isStatic() && name.equals(method.getName()) && method.getParameterTypes().length == numParameters) { matching.add(method); } } return matching.toArray(new ModelMethod[matching.size()]); } /** * Returns the public method with the name name with the parameters that * best match args. staticOnly governs whether a static or instance method * will be returned. If no matching method was found, null is returned. * * @param name The method name to find * @param args The arguments that the method should accept * @param staticOnly true if the returned method must be static or false if it does not * matter. */ public ModelMethod getMethod(String name, List args, boolean staticOnly) { ModelMethod[] methods = getMethods(name, args, staticOnly); L.d("looking methods for %s. static only ? %s . method count: %d", name, staticOnly, methods.length); for (ModelMethod method : methods) { L.d("method: %s, %s", method.getName(), method.isStatic()); } if (methods.length == 0) { return null; } ModelMethod bestMethod = methods[0]; for (int i = 1; i < methods.length; i++) { if (methods[i].isBetterArgMatchThan(bestMethod, args)) { bestMethod = methods[i]; } } return bestMethod; } /** * If this represents a class, the super class that it extends is returned. If this * represents an interface, the interface that this extends is returned. * null is returned if this is not a class or interface, such as an int, or * if it is java.lang.Object or an interface that does not extend any other type. * * @return The class or interface that this ModelClass extends or null. */ public abstract ModelClass getSuperclass(); /** * @return A String representation of the class or interface that this represents, not * including any type arguments. */ public String getCanonicalName() { return erasure().toJavaCode(); } /** * @return The class or interface name of this type or the primitive type if it isn't a * reference type. */ public String getSimpleName() { final String canonicalName = getCanonicalName(); final int dotIndex = canonicalName.lastIndexOf('.'); if (dotIndex >= 0) { return canonicalName.substring(dotIndex + 1); } return canonicalName; } /** * Returns this class type without any generic type arguments. * @return this class type without any generic type arguments. */ public abstract ModelClass erasure(); /** * Since when this class is available. Important for Binding expressions so that we don't * call non-existing APIs when setting UI. * * @return The SDK_INT where this method was added. If it is not a framework method, should * return 1. */ public int getMinApi() { return SdkUtil.getMinApi(this); } /** * Returns the JNI description of the method which can be used to lookup it in SDK. * @see TypeUtil */ public abstract String getJniDescription(); /** * Returns a list of all abstract methods in the type. */ public List getAbstractMethods() { ArrayList abstractMethods = new ArrayList(); ModelMethod[] methods = getDeclaredMethods(); for (ModelMethod method : methods) { if (method.isAbstract()) { abstractMethods.add(method); } } return abstractMethods; } /** * Returns the getter method or field that the name refers to. * @param name The name of the field or the body of the method name -- can be name(), * getName(), or isName(). * @param staticOnly Whether this should look for static methods and fields or instance * versions * @return the getter method or field that the name refers to or null if none can be found. */ public Callable findGetterOrField(String name, boolean staticOnly) { if ("length".equals(name) && isArray()) { return new Callable(Type.FIELD, name, null, ModelAnalyzer.getInstance().loadPrimitive("int"), 0, 0); } String capitalized = StringUtils.capitalize(name); String[] methodNames = { "get" + capitalized, "is" + capitalized, name }; for (String methodName : methodNames) { ModelMethod[] methods = getMethods(methodName, new ArrayList(), staticOnly); for (ModelMethod method : methods) { if (method.isPublic() && (!staticOnly || method.isStatic()) && !method.getReturnType(Arrays.asList(method.getParameterTypes())).isVoid()) { int flags = DYNAMIC; if (method.isStatic()) { flags |= STATIC; } if (method.isBindable()) { flags |= CAN_BE_INVALIDATED; } else { // if method is not bindable, look for a backing field final ModelField backingField = getField(name, true, method.isStatic()); L.d("backing field for method %s is %s", method.getName(), backingField == null ? "NOT FOUND" : backingField.getName()); if (backingField != null && backingField.isBindable()) { flags |= CAN_BE_INVALIDATED; } } final ModelMethod setterMethod = findSetter(method, name); final String setterName = setterMethod == null ? null : setterMethod.getName(); final Callable result = new Callable(Callable.Type.METHOD, methodName, setterName, method.getReturnType(null), method.getParameterTypes().length, flags); return result; } } } // could not find a method. Look for a public field ModelField publicField = null; if (staticOnly) { publicField = getField(name, false, true); } else { // first check non-static publicField = getField(name, false, false); if (publicField == null) { // check for static publicField = getField(name, false, true); } } if (publicField == null) { return null; } ModelClass fieldType = publicField.getFieldType(); int flags = 0; String setterFieldName = name; if (publicField.isStatic()) { flags |= STATIC; } if (!publicField.isFinal()) { setterFieldName = null; flags |= DYNAMIC; } if (publicField.isBindable()) { flags |= CAN_BE_INVALIDATED; } return new Callable(Callable.Type.FIELD, name, setterFieldName, fieldType, 0, flags); } public ModelMethod findInstanceGetter(String name) { String capitalized = StringUtils.capitalize(name); String[] methodNames = { "get" + capitalized, "is" + capitalized, name }; for (String methodName : methodNames) { ModelMethod[] methods = getMethods(methodName, new ArrayList(), false); for (ModelMethod method : methods) { if (method.isPublic() && !method.isStatic() && !method.getReturnType(Arrays.asList(method.getParameterTypes())).isVoid()) { return method; } } } return null; } private ModelField getField(String name, boolean allowPrivate, boolean isStatic) { ModelField[] fields = getDeclaredFields(); for (ModelField field : fields) { boolean nameMatch = name.equals(field.getName()) || name.equals(stripFieldName(field.getName())); if (nameMatch && field.isStatic() == isStatic && (allowPrivate || field.isPublic())) { return field; } } return null; } private ModelMethod findSetter(ModelMethod getter, String originalName) { final String capitalized = StringUtils.capitalize(originalName); final String[] possibleNames; if (originalName.equals(getter.getName())) { possibleNames = new String[] { originalName, "set" + capitalized }; } else if (getter.getName().startsWith("is")){ possibleNames = new String[] { "set" + capitalized, "setIs" + capitalized }; } else { possibleNames = new String[] { "set" + capitalized }; } for (String name : possibleNames) { List methods = findMethods(name, getter.isStatic()); if (methods != null) { ModelClass param = getter.getReturnType(null); for (ModelMethod method : methods) { ModelClass[] parameterTypes = method.getParameterTypes(); if (parameterTypes != null && parameterTypes.length == 1 && parameterTypes[0].equals(param) && method.isStatic() == getter.isStatic()) { return method; } } } } return null; } /** * Finds public methods that matches the given name exactly. These may be resolved into * listener methods during Expr.resolveListeners. */ public List findMethods(String name, boolean staticOnly) { ModelMethod[] methods = getDeclaredMethods(); ArrayList matching = new ArrayList(); for (ModelMethod method : methods) { if (method.getName().equals(name) && (!staticOnly || method.isStatic()) && method.isPublic()) { matching.add(method); } } if (matching.isEmpty()) { return null; } return matching; } public boolean isIncomplete() { if (isTypeVar() || isWildcard()) { return true; } List typeArgs = getTypeArguments(); if (typeArgs != null) { for (ModelClass typeArg : typeArgs) { if (typeArg.isIncomplete()) { return true; } } } return false; } protected abstract ModelField[] getDeclaredFields(); protected abstract ModelMethod[] getDeclaredMethods(); private static String stripFieldName(String fieldName) { // TODO: Make this configurable through IntelliJ if (fieldName.length() > 2) { final char start = fieldName.charAt(2); if (fieldName.startsWith("m_") && Character.isJavaIdentifierStart(start)) { return Character.toLowerCase(start) + fieldName.substring(3); } } if (fieldName.length() > 1) { final char start = fieldName.charAt(1); final char fieldIdentifier = fieldName.charAt(0); final boolean strip; if (fieldIdentifier == '_') { strip = true; } else if (fieldIdentifier == 'm' && Character.isJavaIdentifierStart(start) && !Character.isLowerCase(start)) { strip = true; } else { strip = false; // not mUppercase format } if (strip) { return Character.toLowerCase(start) + fieldName.substring(2); } } return fieldName; } }