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