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