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