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
018package org.apache.commons.beanutils;
019
020
021import java.beans.IntrospectionException;
022import java.beans.PropertyDescriptor;
023import java.lang.ref.Reference;
024import java.lang.ref.SoftReference;
025import java.lang.ref.WeakReference;
026import java.lang.reflect.Method;
027import 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
049public 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}