View Javadoc
1   package org.apache.commons.beanutils2;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.beans.IntrospectionException;
23  import java.beans.PropertyDescriptor;
24  import java.lang.ref.Reference;
25  import java.lang.ref.SoftReference;
26  import java.lang.ref.WeakReference;
27  import java.lang.reflect.Method;
28  import java.lang.reflect.Modifier;
29  
30  /**
31   * EXTRACTED FROM BeanUtils1
32   *
33   * A MappedPropertyDescriptor describes one mapped property. Mapped properties are multivalued properties like indexed
34   * properties but that are accessed with a String key instead of an index. Such property values are typically stored in
35   * a Map collection. For this class to work properly, a mapped value must have getter and setter methods of the form
36   * <p>
37   * <code>get<strong>Property</strong>(String key)<code> and
38   * <p><code>set<strong>Property</strong>(String key, Object value)<code>,
39   * <p>where <code><strong>Property</strong></code> must be replaced by the name of the property.
40   *
41   * @see java.beans.PropertyDescriptor
42   * @author Rey Francois
43   * @author Gregor Rayman
44   * @version $Revision: 806915 $ $Date: 2009-08-23 02:50:23 +0200 (Sun, 23 Aug 2009) $
45   */
46  
47  public class MappedPropertyDescriptor
48      extends PropertyDescriptor
49  {
50      // ----------------------------------------------------- Instance Variables
51  
52      /**
53       * The underlying data type of the property we are describing.
54       */
55      private Reference<Class<?>> mappedPropertyTypeRef;
56  
57      /**
58       * The reader method for this property (if any).
59       */
60      private MappedMethodReference mappedReadMethodRef;
61  
62      /**
63       * The writer method for this property (if any).
64       */
65      private MappedMethodReference mappedWriteMethodRef;
66  
67      /**
68       * The parameter types array for the reader method signature.
69       */
70      @SuppressWarnings( "unchecked" )
71      // we can not create a generic array
72      private static final Class<String>[] STRING_CLASS_PARAMETER = new Class[] { String.class };
73  
74      // ----------------------------------------------------------- Constructors
75  
76      /**
77       * Constructs a MappedPropertyDescriptor for a property that follows the standard Java convention by having getFoo
78       * and setFoo accessor methods, with the addition of a String parameter (the key). Thus if the argument name is
79       * "fred", it will assume that the writer method is "setFred" and the reader method is "getFred". Note that the
80       * property name should start with a lower case character, which will be capitalized in the method names.
81       *
82       * @param propertyName The programmatic name of the property.
83       * @param beanClass The Class object for the target bean. For example sun.beans.OurButton.class.
84       * @exception IntrospectionException if an exception occurs during introspection.
85       */
86      public MappedPropertyDescriptor( String propertyName, Class<?> beanClass )
87          throws IntrospectionException
88      {
89  
90          super( propertyName, null, null );
91  
92          if ( propertyName == null || propertyName.length() == 0 )
93          {
94              throw new IntrospectionException( "bad property name: " + propertyName + " on class: "
95                  + beanClass.getClass().getName() );
96          }
97  
98          setName( propertyName );
99          String base = capitalizePropertyName( propertyName );
100 
101         // Look for mapped read method and matching write method
102         Method mappedReadMethod = null;
103         Method mappedWriteMethod = null;
104         try
105         {
106             try
107             {
108                 mappedReadMethod = getMethod( beanClass, "get" + base, STRING_CLASS_PARAMETER );
109             }
110             catch ( IntrospectionException e )
111             {
112                 mappedReadMethod = getMethod( beanClass, "is" + base, STRING_CLASS_PARAMETER );
113             }
114             Class<?>[] params = { String.class, mappedReadMethod.getReturnType() };
115             mappedWriteMethod = getMethod( beanClass, "set" + base, params );
116         }
117         catch ( IntrospectionException e )
118         {
119             /*
120              * Swallow IntrospectionException TODO: Why?
121              */
122         }
123 
124         // If there's no read method, then look for just a write method
125         if ( mappedReadMethod == null )
126         {
127             mappedWriteMethod = getMethod( beanClass, "set" + base, 2 );
128         }
129 
130         if ( ( mappedReadMethod == null ) && ( mappedWriteMethod == null ) )
131         {
132             throw new IntrospectionException( "Property '" + propertyName + "' not found on " + beanClass.getName() );
133         }
134         mappedReadMethodRef = new MappedMethodReference( mappedReadMethod );
135         mappedWriteMethodRef = new MappedMethodReference( mappedWriteMethod );
136 
137         findMappedPropertyType();
138     }
139 
140     /**
141      * This constructor takes the name of a mapped property, and method names for reading and writing the property.
142      *
143      * @param propertyName The programmatic name of the property.
144      * @param beanClass The Class object for the target bean. For example sun.beans.OurButton.class.
145      * @param mappedGetterName The name of the method used for reading one of the property values. May be null if the
146      *            property is write-only.
147      * @param mappedSetterName The name of the method used for writing one of the property values. May be null if the
148      *            property is read-only.
149      * @exception IntrospectionException if an exception occurs during introspection.
150      */
151     public MappedPropertyDescriptor( String propertyName, Class<?> beanClass, String mappedGetterName,
152                                      String mappedSetterName )
153         throws IntrospectionException
154     {
155 
156         super( propertyName, null, null );
157 
158         if ( propertyName == null || propertyName.length() == 0 )
159         {
160             throw new IntrospectionException( "bad property name: " + propertyName );
161         }
162         setName( propertyName );
163 
164         // search the mapped get and set methods
165         Method mappedReadMethod = null;
166         Method mappedWriteMethod = null;
167         mappedReadMethod = getMethod( beanClass, mappedGetterName, STRING_CLASS_PARAMETER );
168 
169         if ( mappedReadMethod != null )
170         {
171             Class<?>[] params = { String.class, mappedReadMethod.getReturnType() };
172             mappedWriteMethod = getMethod( beanClass, mappedSetterName, params );
173         }
174         else
175         {
176             mappedWriteMethod = getMethod( beanClass, mappedSetterName, 2 );
177         }
178         mappedReadMethodRef = new MappedMethodReference( mappedReadMethod );
179         mappedWriteMethodRef = new MappedMethodReference( mappedWriteMethod );
180 
181         findMappedPropertyType();
182     }
183 
184     /**
185      * This constructor takes the name of a mapped property, and Method objects for reading and writing the property.
186      *
187      * @param propertyName The programmatic name of the property.
188      * @param mappedGetter The method used for reading one of the property values. May be be null if the property is
189      *            write-only.
190      * @param mappedSetter The method used for writing one the property values. May be null if the property is
191      *            read-only.
192      * @exception IntrospectionException if an exception occurs during introspection.
193      */
194     public MappedPropertyDescriptor( String propertyName, Method mappedGetter, Method mappedSetter )
195         throws IntrospectionException
196     {
197 
198         super( propertyName, mappedGetter, mappedSetter );
199 
200         if ( propertyName == null || propertyName.length() == 0 )
201         {
202             throw new IntrospectionException( "bad property name: " + propertyName );
203         }
204 
205         setName( propertyName );
206         mappedReadMethodRef = new MappedMethodReference( mappedGetter );
207         mappedWriteMethodRef = new MappedMethodReference( mappedSetter );
208         findMappedPropertyType();
209     }
210 
211     // -------------------------------------------------------- Public Methods
212 
213     /**
214      * Gets the Class object for the property values.
215      *
216      * @return The Java type info for the property values. Note that the "Class" object may describe a built-in Java
217      *         type such as "int". The result may be "null" if this is a mapped property that does not support non-keyed
218      *         access.
219      *         <p>
220      *         This is the type that will be returned by the mappedReadMethod.
221      */
222     public Class<?> getMappedPropertyType()
223     {
224         return mappedPropertyTypeRef.get();
225     }
226 
227     /**
228      * Gets the method that should be used to read one of the property value.
229      *
230      * @return The method that should be used to read the property value. May return null if the property can't be read.
231      */
232     public Method getMappedReadMethod()
233     {
234         return mappedReadMethodRef.get();
235     }
236 
237     /**
238      * Sets the method that should be used to read one of the property value.
239      *
240      * @param mappedGetter The mapped getter method.
241      * @throws IntrospectionException If an error occurs finding the mapped property
242      */
243     public void setMappedReadMethod( Method mappedGetter )
244         throws IntrospectionException
245     {
246         mappedReadMethodRef = new MappedMethodReference( mappedGetter );
247         findMappedPropertyType();
248     }
249 
250     /**
251      * Gets the method that should be used to write one of the property value.
252      *
253      * @return The method that should be used to write one of the property value. May return null if the property can't
254      *         be written.
255      */
256     public Method getMappedWriteMethod()
257     {
258         return mappedWriteMethodRef.get();
259     }
260 
261     /**
262      * Sets the method that should be used to write the property value.
263      *
264      * @param mappedSetter The mapped setter method.
265      * @throws IntrospectionException If an error occurs finding the mapped property
266      */
267     public void setMappedWriteMethod( Method mappedSetter )
268         throws IntrospectionException
269     {
270         mappedWriteMethodRef = new MappedMethodReference( mappedSetter );
271         findMappedPropertyType();
272     }
273 
274     // ------------------------------------------------------- Private Methods
275 
276     /**
277      * Introspect our bean class to identify the corresponding getter and setter methods.
278      */
279     private void findMappedPropertyType()
280         throws IntrospectionException
281     {
282         try
283         {
284             Method mappedReadMethod = getMappedReadMethod();
285             Method mappedWriteMethod = getMappedWriteMethod();
286             Class<?> mappedPropertyType = null;
287             if ( mappedReadMethod != null )
288             {
289                 if ( mappedReadMethod.getParameterTypes().length != 1 )
290                 {
291                     throw new IntrospectionException( "bad mapped read method arg count" );
292                 }
293                 mappedPropertyType = mappedReadMethod.getReturnType();
294                 if ( mappedPropertyType == Void.TYPE )
295                 {
296                     throw new IntrospectionException( "mapped read method " + mappedReadMethod.getName()
297                         + " returns void" );
298                 }
299             }
300 
301             if ( mappedWriteMethod != null )
302             {
303                 Class<?>[] params = mappedWriteMethod.getParameterTypes();
304                 if ( params.length != 2 )
305                 {
306                     throw new IntrospectionException( "bad mapped write method arg count" );
307                 }
308                 if ( mappedPropertyType != null && mappedPropertyType != params[1] )
309                 {
310                     throw new IntrospectionException( "type mismatch between mapped read and write methods" );
311                 }
312                 mappedPropertyType = params[1];
313             }
314             mappedPropertyTypeRef = new SoftReference<Class<?>>( mappedPropertyType );
315         }
316         catch ( IntrospectionException ex )
317         {
318             throw ex;
319         }
320     }
321 
322     /**
323      * Return a capitalized version of the specified property name.
324      *
325      * @param s The property name
326      */
327     private static String capitalizePropertyName( String s )
328     {
329         if ( s.length() == 0 )
330         {
331             return s;
332         }
333 
334         char[] chars = s.toCharArray();
335         chars[0] = Character.toUpperCase( chars[0] );
336         return new String( chars );
337     }
338 
339     /**
340      * Find a method on a class with a specified number of parameters.
341      */
342     private static Method internalGetMethod( Class<?> initial, String methodName, int parameterCount )
343     {
344         // For overridden methods we need to find the most derived version.
345         // So we start with the given class and walk up the superclass chain.
346         for ( Class<?> clazz = initial; clazz != null; clazz = clazz.getSuperclass() )
347         {
348             Method[] methods = clazz.getDeclaredMethods();
349             for ( int i = 0; i < methods.length; i++ )
350             {
351                 Method method = methods[i];
352                 if ( method == null )
353                 {
354                     continue;
355                 }
356                 // skip static methods.
357                 int mods = method.getModifiers();
358                 if ( !Modifier.isPublic( mods ) || Modifier.isStatic( mods ) )
359                 {
360                     continue;
361                 }
362                 if ( method.getName().equals( methodName ) && method.getParameterTypes().length == parameterCount )
363                 {
364                     return method;
365                 }
366             }
367         }
368 
369         // Now check any inherited interfaces. This is necessary both when
370         // the argument class is itself an interface, and when the argument
371         // class is an abstract class.
372         Class<?>[] interfaces = initial.getInterfaces();
373         for ( int i = 0; i < interfaces.length; i++ )
374         {
375             Method method = internalGetMethod( interfaces[i], methodName, parameterCount );
376             if ( method != null )
377             {
378                 return method;
379             }
380         }
381 
382         return null;
383     }
384 
385     /**
386      * Find a method on a class with a specified number of parameters.
387      */
388     private static Method getMethod( Class<?> clazz, String methodName, int parameterCount )
389         throws IntrospectionException
390     {
391         if ( methodName == null )
392         {
393             return null;
394         }
395 
396         Method method = internalGetMethod( clazz, methodName, parameterCount );
397         if ( method != null )
398         {
399             return method;
400         }
401 
402         // No Method found
403         throw new IntrospectionException( "No method \"" + methodName + "\" with " + parameterCount + " parameter(s)" );
404     }
405 
406     /**
407      * Find a method on a class with a specified parameter list.
408      */
409     private static Method getMethod( Class<?> clazz, String methodName, Class<?>[] parameterTypes )
410         throws IntrospectionException
411     {
412         if ( methodName == null )
413         {
414             return null;
415         }
416 
417         Method method = AccessibleObjectsRegistry.getMethodsRegistry().get( true, clazz, methodName, parameterTypes );
418         if ( method != null )
419         {
420             return method;
421         }
422 
423         int parameterCount = ( parameterTypes == null ) ? 0 : parameterTypes.length;
424 
425         // No Method found
426         throw new IntrospectionException( "No method \"" + methodName + "\" with " + parameterCount
427             + " parameter(s) of matching types." );
428     }
429 
430     /**
431      * Holds a {@link Method} in a {@link SoftReference} so that it it doesn't prevent any ClassLoader being garbage
432      * collected, but tries to re-create the method if the method reference has been released. See
433      * http://issues.apache.org/jira/browse/BEANUTILS-291
434      */
435     private static class MappedMethodReference
436     {
437         private String className;
438 
439         private String methodName;
440 
441         private Reference<Method> methodRef;
442 
443         private Reference<Class<?>> classRef;
444 
445         private Reference<Class<?>> writeParamTypeRef0;
446 
447         private Reference<Class<?>> writeParamTypeRef1;
448 
449         private String[] writeParamClassNames;
450 
451         MappedMethodReference( Method m )
452         {
453             if ( m != null )
454             {
455                 className = m.getDeclaringClass().getName();
456                 methodName = m.getName();
457                 methodRef = new SoftReference<Method>( m );
458                 classRef = new WeakReference<Class<?>>( m.getDeclaringClass() );
459                 Class<?>[] types = m.getParameterTypes();
460                 if ( types.length == 2 )
461                 {
462                     writeParamTypeRef0 = new WeakReference<Class<?>>( types[0] );
463                     writeParamTypeRef1 = new WeakReference<Class<?>>( types[1] );
464                     writeParamClassNames = new String[2];
465                     writeParamClassNames[0] = types[0].getName();
466                     writeParamClassNames[1] = types[1].getName();
467                 }
468             }
469         }
470 
471         private Method get()
472         {
473             if ( methodRef == null )
474             {
475                 return null;
476             }
477             Method m = methodRef.get();
478             if ( m == null )
479             {
480                 Class<?> clazz = classRef.get();
481                 if ( clazz == null )
482                 {
483                     clazz = reLoadClass();
484                     if ( clazz != null )
485                     {
486                         classRef = new WeakReference<Class<?>>( clazz );
487                     }
488                 }
489                 if ( clazz == null )
490                 {
491                     throw new RuntimeException( "Method " + methodName + " for " + className
492                         + " could not be reconstructed - class reference has gone" );
493                 }
494                 Class<?>[] paramTypes = null;
495                 if ( writeParamClassNames != null )
496                 {
497                     paramTypes = new Class[2];
498                     paramTypes[0] = writeParamTypeRef0.get();
499                     if ( paramTypes[0] == null )
500                     {
501                         paramTypes[0] = reLoadClass( writeParamClassNames[0] );
502                         if ( paramTypes[0] != null )
503                         {
504                             writeParamTypeRef0 = new WeakReference<Class<?>>( paramTypes[0] );
505                         }
506                     }
507                     paramTypes[1] = writeParamTypeRef1.get();
508                     if ( paramTypes[1] == null )
509                     {
510                         paramTypes[1] = reLoadClass( writeParamClassNames[1] );
511                         if ( paramTypes[1] != null )
512                         {
513                             writeParamTypeRef1 = new WeakReference<Class<?>>( paramTypes[1] );
514                         }
515                     }
516                 }
517                 else
518                 {
519                     paramTypes = STRING_CLASS_PARAMETER;
520                 }
521                 try
522                 {
523                     m = clazz.getMethod( methodName, paramTypes );
524                     // Un-comment following line for testing
525                     // System.out.println("Recreated Method " + methodName + " for " + className);
526                 }
527                 catch ( NoSuchMethodException e )
528                 {
529                     throw new RuntimeException( "Method " + methodName + " for " + className
530                         + " could not be reconstructed - method not found" );
531                 }
532                 methodRef = new SoftReference<Method>( m );
533             }
534             return m;
535         }
536 
537         /**
538          * Try to re-load the class
539          */
540         private Class<?> reLoadClass()
541         {
542             return reLoadClass( className );
543         }
544 
545         /**
546          * Try to re-load the class
547          */
548         private Class<?> reLoadClass( String name )
549         {
550 
551             ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
552 
553             // Try the context class loader
554             if ( classLoader != null )
555             {
556                 try
557                 {
558                     return classLoader.loadClass( name );
559                 }
560                 catch ( ClassNotFoundException e )
561                 {
562                     // ignore
563                 }
564             }
565 
566             // Try this class's class loader
567             classLoader = MappedPropertyDescriptor.class.getClassLoader();
568             try
569             {
570                 return classLoader.loadClass( name );
571             }
572             catch ( ClassNotFoundException e )
573             {
574                 return null;
575             }
576         }
577     }
578 }