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