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