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