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    *      https://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  import java.util.Objects;
28  
29  /**
30   * A MappedPropertyDescriptor describes one mapped property. Mapped properties are multivalued properties like indexed properties but that are accessed with a
31   * 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
32   * getter and setter methods of the form
33   * <p>
34   * {@code get<strong>Property</strong>(String key)} and
35   * <p>
36   * {@code set<strong>Property</strong>(String key, Object value)},
37   * <p>
38   * where {@code <strong>Property</strong>} must be replaced by the name of the property.
39   *
40   * @see java.beans.PropertyDescriptor
41   */
42  public class MappedPropertyDescriptor extends PropertyDescriptor {
43  
44      /**
45       * 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
46       * method if the method reference has been released.
47       *
48       * See https://issues.apache.org/jira/browse/BEANUTILS-291
49       */
50      private static final class MappedMethodReference {
51          private String className;
52          private String methodName;
53          private Reference<Method> methodRef;
54          private Reference<Class<?>> classRef;
55          private Reference<Class<?>> writeParamTypeRef0;
56          private Reference<Class<?>> writeParamTypeRef1;
57          private String[] writeParamClassNames;
58  
59          MappedMethodReference(final Method m) {
60              if (m != null) {
61                  className = m.getDeclaringClass().getName();
62                  methodName = m.getName();
63                  methodRef = new SoftReference<>(m);
64                  classRef = new WeakReference<>(m.getDeclaringClass());
65                  final Class<?>[] types = m.getParameterTypes();
66                  if (types.length == 2) {
67                      writeParamTypeRef0 = new WeakReference<>(types[0]);
68                      writeParamTypeRef1 = new WeakReference<>(types[1]);
69                      writeParamClassNames = new String[2];
70                      writeParamClassNames[0] = types[0].getName();
71                      writeParamClassNames[1] = types[1].getName();
72                  }
73              }
74          }
75  
76          private Method get() {
77              if (methodRef == null) {
78                  return null;
79              }
80              Method m = methodRef.get();
81              if (m == null) {
82                  Class<?> clazz = classRef.get();
83                  if (clazz == null) {
84                      clazz = reLoadClass();
85                      if (clazz != null) {
86                          classRef = new WeakReference<>(clazz);
87                      }
88                  }
89                  Objects.requireNonNull(clazz, () -> "Method " + methodName + " for " + className + " could not be reconstructed - class reference has gone");
90                  Class<?>[] paramTypes = null;
91                  if (writeParamClassNames != null) {
92                      paramTypes = new Class[2];
93                      paramTypes[0] = writeParamTypeRef0.get();
94                      if (paramTypes[0] == null) {
95                          paramTypes[0] = reLoadClass(writeParamClassNames[0]);
96                          if (paramTypes[0] != null) {
97                              writeParamTypeRef0 = new WeakReference<>(paramTypes[0]);
98                          }
99                      }
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 }