001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.apache.commons.beanutils; 019 020import java.beans.IntrospectionException; 021import java.beans.PropertyDescriptor; 022import java.lang.ref.Reference; 023import java.lang.ref.SoftReference; 024import java.lang.ref.WeakReference; 025import java.lang.reflect.Method; 026import java.lang.reflect.Modifier; 027 028/** 029 * A MappedPropertyDescriptor describes one mapped property. 030 * Mapped properties are multivalued properties like indexed properties 031 * but that are accessed with a String key instead of an index. 032 * Such property values are typically stored in a Map collection. 033 * For this class to work properly, a mapped value must have 034 * getter and setter methods of the form 035 * <p><code>get<strong>Property</strong>(String key)</code> and 036 * <p><code>set<strong>Property</strong>(String key, Object value)</code>, 037 * <p>where <code><strong>Property</strong></code> must be replaced 038 * by the name of the property. 039 * @see java.beans.PropertyDescriptor 040 * 041 */ 042public class MappedPropertyDescriptor extends PropertyDescriptor { 043 044 /** 045 * Holds a {@link Method} in a {@link SoftReference} so that it 046 * it doesn't prevent any ClassLoader being garbage collected, but 047 * tries to re-create the method if the method reference has been 048 * released. 049 * 050 * See http://issues.apache.org/jira/browse/BEANUTILS-291 051 */ 052 private static class MappedMethodReference { 053 private String className; 054 private String methodName; 055 private Reference<Method> methodRef; 056 private Reference<Class<?>> classRef; 057 private Reference<Class<?>> writeParamTypeRef0; 058 private Reference<Class<?>> writeParamTypeRef1; 059 private String[] writeParamClassNames; 060 MappedMethodReference(final Method m) { 061 if (m != null) { 062 className = m.getDeclaringClass().getName(); 063 methodName = m.getName(); 064 // Compiler needs generic. 065 methodRef = new SoftReference<>(m); 066 // Compiler needs generic. 067 classRef = new WeakReference<>(m.getDeclaringClass()); 068 final Class<?>[] types = m.getParameterTypes(); 069 if (types.length == 2) { 070 // Compiler needs generic. 071 writeParamTypeRef0 = new WeakReference<>(types[0]); 072 // Compiler needs generic. 073 writeParamTypeRef1 = new WeakReference<>(types[1]); 074 writeParamClassNames = new String[2]; 075 writeParamClassNames[0] = types[0].getName(); 076 writeParamClassNames[1] = types[1].getName(); 077 } 078 } 079 } 080 private Method get() { 081 if (methodRef == null) { 082 return null; 083 } 084 Method m = methodRef.get(); 085 if (m == null) { 086 Class<?> clazz = classRef.get(); 087 if (clazz == null) { 088 clazz = reLoadClass(); 089 if (clazz != null) { 090 // Compiler needs generic. 091 classRef = new WeakReference<>(clazz); 092 } 093 } 094 if (clazz == null) { 095 throw new RuntimeException("Method " + methodName + " for " + 096 className + " could not be reconstructed - class reference has gone"); 097 } 098 Class<?>[] paramTypes = null; 099 if (writeParamClassNames != null) { 100 paramTypes = new Class[2]; 101 paramTypes[0] = writeParamTypeRef0.get(); 102 if (paramTypes[0] == null) { 103 paramTypes[0] = reLoadClass(writeParamClassNames[0]); 104 if (paramTypes[0] != null) { 105 // Compiler needs generic. 106 writeParamTypeRef0 = new WeakReference<>(paramTypes[0]); 107 } 108 } 109 paramTypes[1] = writeParamTypeRef1.get(); 110 if (paramTypes[1] == null) { 111 paramTypes[1] = reLoadClass(writeParamClassNames[1]); 112 if (paramTypes[1] != null) { 113 // Compiler needs generic. 114 writeParamTypeRef1 = new WeakReference<>(paramTypes[1]); 115 } 116 } 117 } else { 118 paramTypes = STRING_CLASS_PARAMETER; 119 } 120 try { 121 m = clazz.getMethod(methodName, paramTypes); 122 // Un-comment following line for testing 123 // System.out.println("Recreated Method " + methodName + " for " + className); 124 } catch (final NoSuchMethodException e) { 125 throw new RuntimeException("Method " + methodName + " for " + 126 className + " could not be reconstructed - method not found"); 127 } 128 methodRef = new SoftReference<>(m); 129 } 130 return m; 131 } 132 133 /** 134 * Try to re-load the class 135 */ 136 private Class<?> reLoadClass() { 137 return reLoadClass(className); 138 } 139 140 /** 141 * Try to re-load the class 142 */ 143 private Class<?> reLoadClass(final String name) { 144 145 ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); 146 147 // Try the context class loader 148 if (classLoader != null) { 149 try { 150 return classLoader.loadClass(name); 151 } catch (final ClassNotFoundException e) { 152 // ignore 153 } 154 } 155 156 // Try this class's class loader 157 classLoader = MappedPropertyDescriptor.class.getClassLoader(); 158 try { 159 return classLoader.loadClass(name); 160 } catch (final ClassNotFoundException e) { 161 return null; 162 } 163 } 164 } 165 166 /** 167 * The parameter types array for the reader method signature. 168 */ 169 private static final Class<?>[] STRING_CLASS_PARAMETER = new Class[]{String.class}; 170 171 /** 172 * Return a capitalized version of the specified property name. 173 * 174 * @param s The property name 175 */ 176 private static String capitalizePropertyName(final String s) { 177 if (s.length() == 0) { 178 return s; 179 } 180 181 final char[] chars = s.toCharArray(); 182 chars[0] = Character.toUpperCase(chars[0]); 183 return new String(chars); 184 } 185 186 /** 187 * Find a method on a class with a specified parameter list. 188 */ 189 private static Method getMethod(final Class<?> clazz, final String methodName, final Class<?>[] parameterTypes) 190 throws IntrospectionException { 191 if (methodName == null) { 192 return null; 193 } 194 195 final Method method = MethodUtils.getMatchingAccessibleMethod(clazz, methodName, parameterTypes); 196 if (method != null) { 197 return method; 198 } 199 200 final int parameterCount = parameterTypes == null ? 0 : parameterTypes.length; 201 202 // No Method found 203 throw new IntrospectionException("No method \"" + methodName + 204 "\" with " + parameterCount + " parameter(s) of matching types."); 205 } 206 207 /** 208 * Find a method on a class with a specified number of parameters. 209 */ 210 private static Method getMethod(final Class<?> clazz, final String methodName, final int parameterCount) 211 throws IntrospectionException { 212 if (methodName == null) { 213 return null; 214 } 215 216 final Method method = internalGetMethod(clazz, methodName, parameterCount); 217 if (method != null) { 218 return method; 219 } 220 221 // No Method found 222 throw new IntrospectionException("No method \"" + methodName + 223 "\" with " + parameterCount + " parameter(s)"); 224 } 225 226 /** 227 * Find a method on a class with a specified number of parameters. 228 */ 229 private static Method internalGetMethod(final Class<?> initial, final String methodName, 230 final int parameterCount) { 231 // For overridden methods we need to find the most derived version. 232 // So we start with the given class and walk up the superclass chain. 233 for (Class<?> clazz = initial; clazz != null; clazz = clazz.getSuperclass()) { 234 final Method[] methods = clazz.getDeclaredMethods(); 235 for (final Method method : methods) { 236 if (method == null) { 237 continue; 238 } 239 // skip static methods. 240 final int mods = method.getModifiers(); 241 if (!Modifier.isPublic(mods) || 242 Modifier.isStatic(mods)) { 243 continue; 244 } 245 if (method.getName().equals(methodName) && 246 method.getParameterTypes().length == parameterCount) { 247 return method; 248 } 249 } 250 } 251 252 // Now check any inherited interfaces. This is necessary both when 253 // the argument class is itself an interface, and when the argument 254 // class is an abstract class. 255 final Class<?>[] interfaces = initial.getInterfaces(); 256 for (final Class<?> interface1 : interfaces) { 257 final Method method = internalGetMethod(interface1, methodName, parameterCount); 258 if (method != null) { 259 return method; 260 } 261 } 262 263 return null; 264 } 265 266 /** 267 * The underlying data type of the property we are describing. 268 */ 269 private Reference<Class<?>> mappedPropertyTypeRef; 270 271 /** 272 * The reader method for this property (if any). 273 */ 274 private MappedMethodReference mappedReadMethodRef; 275 276 /** 277 * The writer method for this property (if any). 278 */ 279 private MappedMethodReference mappedWriteMethodRef; 280 281 /** 282 * Constructs a MappedPropertyDescriptor for a property that follows 283 * the standard Java convention by having getFoo and setFoo 284 * accessor methods, with the addition of a String parameter (the key). 285 * Thus if the argument name is "fred", it will 286 * assume that the writer method is "setFred" and the reader method 287 * is "getFred". Note that the property name should start with a lower 288 * case character, which will be capitalized in the method names. 289 * 290 * @param propertyName The programmatic name of the property. 291 * @param beanClass The Class object for the target bean. For 292 * example sun.beans.OurButton.class. 293 * 294 * @throws IntrospectionException if an exception occurs during 295 * introspection. 296 */ 297 public MappedPropertyDescriptor(final String propertyName, final Class<?> beanClass) 298 throws IntrospectionException { 299 300 super(propertyName, null, null); 301 302 if (propertyName == null || propertyName.length() == 0) { 303 throw new IntrospectionException("bad property name: " + 304 propertyName + " on class: " + beanClass.getClass().getName()); 305 } 306 307 setName(propertyName); 308 final String base = capitalizePropertyName(propertyName); 309 310 // Look for mapped read method and matching write method 311 Method mappedReadMethod = null; 312 Method mappedWriteMethod = null; 313 try { 314 try { 315 mappedReadMethod = getMethod(beanClass, "get" + base, 316 STRING_CLASS_PARAMETER); 317 } catch (final IntrospectionException e) { 318 mappedReadMethod = getMethod(beanClass, "is" + base, 319 STRING_CLASS_PARAMETER); 320 } 321 final Class<?>[] params = { String.class, mappedReadMethod.getReturnType() }; 322 mappedWriteMethod = getMethod(beanClass, "set" + base, params); 323 } catch (final IntrospectionException e) { 324 /* Swallow IntrospectionException 325 * TODO: Why? 326 */ 327 } 328 329 // If there's no read method, then look for just a write method 330 if (mappedReadMethod == null) { 331 mappedWriteMethod = getMethod(beanClass, "set" + base, 2); 332 } 333 334 if (mappedReadMethod == null && mappedWriteMethod == null) { 335 throw new IntrospectionException("Property '" + propertyName + 336 "' not found on " + 337 beanClass.getName()); 338 } 339 mappedReadMethodRef = new MappedMethodReference(mappedReadMethod); 340 mappedWriteMethodRef = new MappedMethodReference(mappedWriteMethod); 341 342 findMappedPropertyType(); 343 } 344 345 /** 346 * This constructor takes the name of a mapped property, and method 347 * names for reading and writing the property. 348 * 349 * @param propertyName The programmatic name of the property. 350 * @param beanClass The Class object for the target bean. For 351 * example sun.beans.OurButton.class. 352 * @param mappedGetterName The name of the method used for 353 * reading one of the property values. May be null if the 354 * property is write-only. 355 * @param mappedSetterName The name of the method used for writing 356 * one of the property values. May be null if the property is 357 * read-only. 358 * 359 * @throws IntrospectionException if an exception occurs during 360 * introspection. 361 */ 362 public MappedPropertyDescriptor(final String propertyName, final Class<?> beanClass, 363 final String mappedGetterName, final String mappedSetterName) 364 throws IntrospectionException { 365 366 super(propertyName, null, null); 367 368 if (propertyName == null || propertyName.length() == 0) { 369 throw new IntrospectionException("bad property name: " + 370 propertyName); 371 } 372 setName(propertyName); 373 374 // search the mapped get and set methods 375 Method mappedReadMethod = null; 376 Method mappedWriteMethod = null; 377 mappedReadMethod = 378 getMethod(beanClass, mappedGetterName, STRING_CLASS_PARAMETER); 379 380 if (mappedReadMethod != null) { 381 final Class<?>[] params = { String.class, mappedReadMethod.getReturnType() }; 382 mappedWriteMethod = 383 getMethod(beanClass, mappedSetterName, params); 384 } else { 385 mappedWriteMethod = 386 getMethod(beanClass, mappedSetterName, 2); 387 } 388 mappedReadMethodRef = new MappedMethodReference(mappedReadMethod); 389 mappedWriteMethodRef = new MappedMethodReference(mappedWriteMethod); 390 391 findMappedPropertyType(); 392 } 393 394 /** 395 * This constructor takes the name of a mapped property, and Method 396 * objects for reading and writing the property. 397 * 398 * @param propertyName The programmatic name of the property. 399 * @param mappedGetter The method used for reading one of 400 * the property values. May be be null if the property 401 * is write-only. 402 * @param mappedSetter The method used for writing one the 403 * property values. May be null if the property is read-only. 404 * 405 * @throws IntrospectionException if an exception occurs during 406 * introspection. 407 */ 408 public MappedPropertyDescriptor(final String propertyName, 409 final Method mappedGetter, final Method mappedSetter) 410 throws IntrospectionException { 411 412 super(propertyName, mappedGetter, mappedSetter); 413 414 if (propertyName == null || propertyName.length() == 0) { 415 throw new IntrospectionException("bad property name: " + 416 propertyName); 417 } 418 419 setName(propertyName); 420 mappedReadMethodRef = new MappedMethodReference(mappedGetter); 421 mappedWriteMethodRef = new MappedMethodReference(mappedSetter); 422 findMappedPropertyType(); 423 } 424 425 /** 426 * Introspect our bean class to identify the corresponding getter 427 * and setter methods. 428 */ 429 private void findMappedPropertyType() throws IntrospectionException { 430 try { 431 final Method mappedReadMethod = getMappedReadMethod(); 432 final Method mappedWriteMethod = getMappedWriteMethod(); 433 Class<?> mappedPropertyType = null; 434 if (mappedReadMethod != null) { 435 if (mappedReadMethod.getParameterTypes().length != 1) { 436 throw new IntrospectionException 437 ("bad mapped read method arg count"); 438 } 439 mappedPropertyType = mappedReadMethod.getReturnType(); 440 if (mappedPropertyType == Void.TYPE) { 441 throw new IntrospectionException 442 ("mapped read method " + 443 mappedReadMethod.getName() + " returns void"); 444 } 445 } 446 447 if (mappedWriteMethod != null) { 448 final Class<?>[] params = mappedWriteMethod.getParameterTypes(); 449 if (params.length != 2) { 450 throw new IntrospectionException 451 ("bad mapped write method arg count"); 452 } 453 if (mappedPropertyType != null && 454 mappedPropertyType != params[1]) { 455 throw new IntrospectionException 456 ("type mismatch between mapped read and write methods"); 457 } 458 mappedPropertyType = params[1]; 459 } 460 // Compiler needs generic. 461 mappedPropertyTypeRef = new SoftReference<>(mappedPropertyType); 462 } catch (final IntrospectionException ex) { 463 throw ex; 464 } 465 } 466 467 /** 468 * Gets the Class object for the property values. 469 * 470 * @return The Java type info for the property values. Note that 471 * the "Class" object may describe a built-in Java type such as "int". 472 * The result may be "null" if this is a mapped property that 473 * does not support non-keyed access. 474 * <p> 475 * This is the type that will be returned by the mappedReadMethod. 476 */ 477 public Class<?> getMappedPropertyType() { 478 return mappedPropertyTypeRef.get(); 479 } 480 481 /** 482 * Gets the method that should be used to read one of the property value. 483 * 484 * @return The method that should be used to read the property value. 485 * May return null if the property can't be read. 486 */ 487 public Method getMappedReadMethod() { 488 return mappedReadMethodRef.get(); 489 } 490 491 /** 492 * Gets the method that should be used to write one of the property value. 493 * 494 * @return The method that should be used to write one of the property value. 495 * May return null if the property can't be written. 496 */ 497 public Method getMappedWriteMethod() { 498 return mappedWriteMethodRef.get(); 499 } 500 501 /** 502 * Sets the method that should be used to read one of the property value. 503 * 504 * @param mappedGetter The mapped getter method. 505 * @throws IntrospectionException If an error occurs finding the 506 * mapped property 507 */ 508 public void setMappedReadMethod(final Method mappedGetter) 509 throws IntrospectionException { 510 mappedReadMethodRef = new MappedMethodReference(mappedGetter); 511 findMappedPropertyType(); 512 } 513 514 /** 515 * Sets the method that should be used to write the property value. 516 * 517 * @param mappedSetter The mapped setter method. 518 * @throws IntrospectionException If an error occurs finding the 519 * mapped property 520 */ 521 public void setMappedWriteMethod(final Method mappedSetter) 522 throws IntrospectionException { 523 mappedWriteMethodRef = new MappedMethodReference(mappedSetter); 524 findMappedPropertyType(); 525 } 526}