View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.beanutils2;
19  
20  import java.beans.IntrospectionException;
21  import java.beans.PropertyDescriptor;
22  import java.lang.ref.Reference;
23  import java.lang.ref.SoftReference;
24  import java.lang.ref.WeakReference;
25  import java.lang.reflect.Method;
26  import java.lang.reflect.Modifier;
27  
28  /**
29   * A MappedPropertyDescriptor describes one mapped property. Mapped properties are multivalued properties like indexed properties but that are accessed with a
30   * 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
31   * getter and setter methods of the form
32   * <p>
33   * {@code get<strong>Property</strong>(String key)} and
34   * <p>
35   * {@code set<strong>Property</strong>(String key, Object value)},
36   * <p>
37   * where {@code <strong>Property</strong>} must be replaced by the name of the property.
38   *
39   * @see java.beans.PropertyDescriptor
40   */
41  public class MappedPropertyDescriptor extends PropertyDescriptor {
42  
43      /**
44       * 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
45       * method if the method reference has been released.
46       *
47       * See https://issues.apache.org/jira/browse/BEANUTILS-291
48       */
49      private static final class MappedMethodReference {
50          private String className;
51          private String methodName;
52          private Reference<Method> methodRef;
53          private Reference<Class<?>> classRef;
54          private Reference<Class<?>> writeParamTypeRef0;
55          private Reference<Class<?>> writeParamTypeRef1;
56          private String[] writeParamClassNames;
57  
58          MappedMethodReference(final Method m) {
59              if (m != null) {
60                  className = m.getDeclaringClass().getName();
61                  methodName = m.getName();
62                  methodRef = new SoftReference<>(m);
63                  classRef = new WeakReference<>(m.getDeclaringClass());
64                  final Class<?>[] types = m.getParameterTypes();
65                  if (types.length == 2) {
66                      writeParamTypeRef0 = new WeakReference<>(types[0]);
67                      writeParamTypeRef1 = new WeakReference<>(types[1]);
68                      writeParamClassNames = new String[2];
69                      writeParamClassNames[0] = types[0].getName();
70                      writeParamClassNames[1] = types[1].getName();
71                  }
72              }
73          }
74  
75          private Method get() {
76              if (methodRef == null) {
77                  return null;
78              }
79              Method m = methodRef.get();
80              if (m == null) {
81                  Class<?> clazz = classRef.get();
82                  if (clazz == null) {
83                      clazz = reLoadClass();
84                      if (clazz != null) {
85                          classRef = new WeakReference<>(clazz);
86                      }
87                  }
88                  if (clazz == null) {
89                      throw new RuntimeException("Method " + methodName + " for " + className + " could not be reconstructed - class reference has gone");
90                  }
91                  Class<?>[] paramTypes = null;
92                  if (writeParamClassNames != null) {
93                      paramTypes = new Class[2];
94                      paramTypes[0] = writeParamTypeRef0.get();
95                      if (paramTypes[0] == null) {
96                          paramTypes[0] = reLoadClass(writeParamClassNames[0]);
97                          if (paramTypes[0] != null) {
98                              writeParamTypeRef0 = new WeakReference<>(paramTypes[0]);
99                          }
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 }