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