ModelClass.java revision d7adbdab48fb8f894cb59073b864da8f5bafb8a9
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 this ModelClass type extends ViewStub. 135 */ 136 public boolean extendsViewStub() { 137 return ModelAnalyzer.getInstance().getViewStubType().isAssignableFrom(this); 138 } 139 140 /** 141 * @return whether or not this is an Observable type such as ObservableMap, ObservableList, 142 * or Observable. 143 */ 144 public boolean isObservable() { 145 ModelAnalyzer modelAnalyzer = ModelAnalyzer.getInstance(); 146 return modelAnalyzer.getObservableType().isAssignableFrom(this) || 147 modelAnalyzer.getObservableListType().isAssignableFrom(this) || 148 modelAnalyzer.getObservableMapType().isAssignableFrom(this); 149 150 } 151 152 /** 153 * @return whether or not this is an ObservableField, or any of the primitive versions 154 * such as ObservableBoolean and ObservableInt 155 */ 156 public boolean isObservableField() { 157 ModelClass erasure = erasure(); 158 for (ModelClass observableField : ModelAnalyzer.getInstance().getObservableFieldTypes()) { 159 if (observableField.isAssignableFrom(erasure)) { 160 return true; 161 } 162 } 163 return false; 164 } 165 166 /** 167 * @return whether or not this ModelClass represents a void 168 */ 169 public abstract boolean isVoid(); 170 171 /** 172 * When this is a boxed type, such as Integer, this will return the unboxed value, 173 * such as int. If this is not a boxed type, this is returned. 174 * 175 * @return The unboxed type of the class that this ModelClass represents or this if it isn't a 176 * boxed type. 177 */ 178 public abstract ModelClass unbox(); 179 180 /** 181 * When this is a primitive type, such as boolean, this will return the boxed value, 182 * such as Boolean. If this is not a primitive type, this is returned. 183 * 184 * @return The boxed type of the class that this ModelClass represents or this if it isn't a 185 * primitive type. 186 */ 187 public abstract ModelClass box(); 188 189 /** 190 * Returns whether or not the type associated with <code>that</code> can be assigned to 191 * the type associated with this ModelClass. If this and that only require boxing or unboxing 192 * then true is returned. 193 * 194 * @param that the ModelClass to compare. 195 * @return true if <code>that</code> requires only boxing or if <code>that</code> is an 196 * implementation of or subclass of <code>this</code>. 197 */ 198 public abstract boolean isAssignableFrom(ModelClass that); 199 200 /** 201 * Returns an array containing all public methods on the type represented by this ModelClass 202 * with the name <code>name</code> and can take the passed-in types as arguments. This will 203 * also work if the arguments match VarArgs parameter. 204 * 205 * @param name The name of the method to find. 206 * @param args The types that the method should accept. 207 * @param isStatic Whether only static methods should be returned or instance methods. 208 * @return An array containing all public methods with the name <code>name</code> and taking 209 * <code>args</code> parameters. 210 */ 211 public ModelMethod[] getMethods(String name, List<ModelClass> args, boolean isStatic) { 212 ModelMethod[] methods = getDeclaredMethods(); 213 ArrayList<ModelMethod> matching = new ArrayList<ModelMethod>(); 214 for (ModelMethod method : methods) { 215 if (method.isPublic() && method.isStatic() == isStatic && 216 name.equals(method.getName()) && method.acceptsArguments(args)) { 217 matching.add(method); 218 } 219 } 220 return matching.toArray(new ModelMethod[matching.size()]); 221 } 222 223 /** 224 * Returns all public instance methods with the given name and number of parameters. 225 * 226 * @param name The name of the method to find. 227 * @param numParameters The number of parameters that the method should take 228 * @return An array containing all public methods with the given name and number of parameters. 229 */ 230 public ModelMethod[] getMethods(String name, int numParameters) { 231 ModelMethod[] methods = getDeclaredMethods(); 232 ArrayList<ModelMethod> matching = new ArrayList<ModelMethod>(); 233 for (ModelMethod method : methods) { 234 if (method.isPublic() && !method.isStatic() && 235 name.equals(method.getName()) && 236 method.getParameterTypes().length == numParameters) { 237 matching.add(method); 238 } 239 } 240 return matching.toArray(new ModelMethod[matching.size()]); 241 } 242 243 /** 244 * Returns the public method with the name <code>name</code> with the parameters that 245 * best match args. <code>staticAccess</code> governs whether a static or instance method 246 * will be returned. If no matching method was found, null is returned. 247 * 248 * @param name The method name to find 249 * @param args The arguments that the method should accept 250 * @param staticAccess true if the returned method should be static or false if it should 251 * be an instance method. 252 */ 253 public ModelMethod getMethod(String name, List<ModelClass> args, boolean staticAccess) { 254 ModelMethod[] methods = getMethods(name, args, staticAccess); 255 if (methods.length == 0) { 256 return null; 257 } 258 ModelMethod bestMethod = methods[0]; 259 for (int i = 1; i < methods.length; i++) { 260 if (methods[i].isBetterArgMatchThan(bestMethod, args)) { 261 bestMethod = methods[i]; 262 } 263 } 264 return bestMethod; 265 } 266 267 /** 268 * If this represents a class, the super class that it extends is returned. If this 269 * represents an interface, the interface that this extends is returned. 270 * <code>null</code> is returned if this is not a class or interface, such as an int, or 271 * if it is java.lang.Object or an interface that does not extend any other type. 272 * 273 * @return The class or interface that this ModelClass extends or null. 274 */ 275 public abstract ModelClass getSuperclass(); 276 277 /** 278 * @return A String representation of the class or interface that this represents, not 279 * including any type arguments. 280 */ 281 public String getCanonicalName() { 282 return erasure().toJavaCode(); 283 } 284 285 /** 286 * Returns this class type without any generic type arguments. 287 * @return this class type without any generic type arguments. 288 */ 289 public abstract ModelClass erasure(); 290 291 /** 292 * Since when this class is available. Important for Binding expressions so that we don't 293 * call non-existing APIs when setting UI. 294 * 295 * @return The SDK_INT where this method was added. If it is not a framework method, should 296 * return 1. 297 */ 298 public int getMinApi() { 299 return SdkUtil.getMinApi(this); 300 } 301 302 /** 303 * Returns the JNI description of the method which can be used to lookup it in SDK. 304 * @see TypeUtil 305 */ 306 public abstract String getJniDescription(); 307 308 /** 309 * Returns the getter method or field that the name refers to. 310 * @param name The name of the field or the body of the method name -- can be name(), 311 * getName(), or isName(). 312 * @param staticAccess Whether this should look for static methods and fields or instance 313 * versions 314 * @return the getter method or field that the name refers to. 315 * @throws IllegalArgumentException if there is no such method or field available. 316 */ 317 public Callable findGetterOrField(String name, boolean staticAccess) { 318 if ("length".equals(name) && isArray()) { 319 return new Callable(Type.FIELD, name, ModelAnalyzer.getInstance().loadPrimitive("int"), 320 0); 321 } 322 String capitalized = StringUtils.capitalize(name); 323 String[] methodNames = { 324 "get" + capitalized, 325 "is" + capitalized, 326 name 327 }; 328 ModelField backingField = getField(name, true, staticAccess); 329 L.d("Finding getter or field for %s, field = %s", name, backingField == null ? null : backingField.getName()); 330 for (String methodName : methodNames) { 331 ModelMethod[] methods = getMethods(methodName, 0); 332 for (ModelMethod method : methods) { 333 if (method.isPublic() && (!staticAccess || method.isStatic())) { 334 int flags = DYNAMIC; 335 if (method.isStatic()) { 336 flags |= STATIC; 337 } 338 if (method.isBindable() || 339 (backingField != null && backingField.isBindable() 340 && backingField.isStatic() == method.isStatic())) { 341 flags |= CAN_BE_INVALIDATED; 342 } 343 final Callable result = new Callable(Callable.Type.METHOD, methodName, 344 method.getReturnType(null), flags); 345 L.d("backing field for %s is %s", result, backingField); 346 return result; 347 } 348 } 349 } 350 351 if (backingField == null && !staticAccess) { 352 // if we could not find an instance field, we should search for static fields since 353 // we are not accessing through an instance method 354 backingField = getField(name, false, true); 355 } 356 357 if (backingField != null && backingField.isPublic()) { 358 ModelClass fieldType = backingField.getFieldType(); 359 int flags = 0; 360 if (!backingField.isFinal()) { 361 flags |= DYNAMIC; 362 } 363 if (backingField.isBindable()) { 364 flags |= CAN_BE_INVALIDATED; 365 } 366 if (backingField.isStatic()) { 367 flags |= STATIC; 368 } 369 return new Callable(Callable.Type.FIELD, name, fieldType, flags); 370 } 371 if (staticAccess) { 372 // can be an inner class. allow it as well. 373 } 374 throw new IllegalArgumentException( 375 "cannot find " + name + " in " + toJavaCode()); 376 377 } 378 379 public ModelField getField(String name, boolean allowPrivate, boolean staticAccess) { 380 ModelField[] fields = getDeclaredFields(); 381 for (ModelField field : fields) { 382 if (name.equals(stripFieldName(field.getName())) && field.isStatic() == staticAccess && 383 (allowPrivate || field.isPublic())) { 384 return field; 385 } 386 } 387 return null; 388 } 389 390 protected abstract ModelField[] getDeclaredFields(); 391 392 protected abstract ModelMethod[] getDeclaredMethods(); 393 394 private static String stripFieldName(String fieldName) { 395 // TODO: Make this configurable through IntelliJ 396 if (fieldName.length() > 2) { 397 final char start = fieldName.charAt(2); 398 if (fieldName.startsWith("m_") && Character.isJavaIdentifierStart(start)) { 399 return Character.toLowerCase(start) + fieldName.substring(3); 400 } 401 } 402 if (fieldName.length() > 1) { 403 final char start = fieldName.charAt(1); 404 final char fieldIdentifier = fieldName.charAt(0); 405 final boolean strip; 406 if (fieldIdentifier == '_') { 407 strip = true; 408 } else if (fieldIdentifier == 'm' && Character.isJavaIdentifierStart(start) && 409 !Character.isLowerCase(start)) { 410 strip = true; 411 } else { 412 strip = false; // not mUppercase format 413 } 414 if (strip) { 415 return Character.toLowerCase(start) + fieldName.substring(2); 416 } 417 } 418 return fieldName; 419 } 420} 421