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