View Javadoc

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