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