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