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