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