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