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: MappedPropertyDescriptor.java 1540504 2013-11-10 18:22:57Z niallp $
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     * @exception IntrospectionException if an exception occurs during
084     *              introspection.
085     */
086    public MappedPropertyDescriptor(String propertyName, 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        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 (IntrospectionException e) {
107                mappedReadMethod = getMethod(beanClass, "is" + base,
108                        STRING_CLASS_PARAMETER);
109            }
110            Class<?>[] params = { String.class, mappedReadMethod.getReturnType() };
111            mappedWriteMethod = getMethod(beanClass, "set" + base, params);
112        } catch (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     * @exception IntrospectionException if an exception occurs during
150     *              introspection.
151     */
152    public MappedPropertyDescriptor(String propertyName, Class<?> beanClass,
153                                    String mappedGetterName, 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            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     * @exception IntrospectionException if an exception occurs during
196     *              introspection.
197     */
198    public MappedPropertyDescriptor(String propertyName,
199                                    Method mappedGetter, 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(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(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            Method mappedReadMethod  = getMappedReadMethod();
286            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                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 (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(String s) {
327        if (s.length() == 0) {
328            return s;
329        }
330
331        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(Class<?> initial, String methodName,
340                                            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            Method[] methods = clazz.getDeclaredMethods();
345            for (int i = 0; i < methods.length; i++) {
346                Method method = methods[i];
347                if (method == null) {
348                    continue;
349                }
350                // skip static methods.
351                int mods = method.getModifiers();
352                if (!Modifier.isPublic(mods) ||
353                    Modifier.isStatic(mods)) {
354                    continue;
355                }
356                if (method.getName().equals(methodName) &&
357                        method.getParameterTypes().length == parameterCount) {
358                    return method;
359                }
360            }
361        }
362
363        // Now check any inherited interfaces.  This is necessary both when
364        // the argument class is itself an interface, and when the argument
365        // class is an abstract class.
366        Class<?>[] interfaces = initial.getInterfaces();
367        for (int i = 0; i < interfaces.length; i++) {
368            Method method = internalGetMethod(interfaces[i], methodName, parameterCount);
369            if (method != null) {
370                return method;
371            }
372        }
373
374        return null;
375    }
376
377    /**
378     * Find a method on a class with a specified number of parameters.
379     */
380    private static Method getMethod(Class<?> clazz, String methodName, int parameterCount)
381            throws IntrospectionException {
382        if (methodName == null) {
383            return null;
384        }
385
386        Method method = internalGetMethod(clazz, methodName, parameterCount);
387        if (method != null) {
388            return method;
389        }
390
391        // No Method found
392        throw new IntrospectionException("No method \"" + methodName +
393                "\" with " + parameterCount + " parameter(s)");
394    }
395
396    /**
397     * Find a method on a class with a specified parameter list.
398     */
399    private static Method getMethod(Class<?> clazz, String methodName, Class<?>[] parameterTypes)
400                                           throws IntrospectionException {
401        if (methodName == null) {
402            return null;
403        }
404
405        Method method = MethodUtils.getMatchingAccessibleMethod(clazz, methodName, parameterTypes);
406        if (method != null) {
407            return method;
408        }
409
410        int parameterCount = (parameterTypes == null) ? 0 : parameterTypes.length;
411
412        // No Method found
413        throw new IntrospectionException("No method \"" + methodName +
414                "\" with " + parameterCount + " parameter(s) of matching types.");
415    }
416
417    /**
418     * Holds a {@link Method} in a {@link SoftReference} so that it
419     * it doesn't prevent any ClassLoader being garbage collected, but
420     * tries to re-create the method if the method reference has been
421     * released.
422     *
423     * See http://issues.apache.org/jira/browse/BEANUTILS-291
424     */
425    private static class MappedMethodReference {
426        private String className;
427        private String methodName;
428        private Reference<Method> methodRef;
429        private Reference<Class<?>> classRef;
430        private Reference<Class<?>> writeParamTypeRef0;
431        private Reference<Class<?>> writeParamTypeRef1;
432        private String[] writeParamClassNames;
433        MappedMethodReference(Method m) {
434            if (m != null) {
435                className = m.getDeclaringClass().getName();
436                methodName = m.getName();
437                methodRef = new SoftReference<Method>(m);
438                classRef = new WeakReference<Class<?>>(m.getDeclaringClass());
439                Class<?>[] types = m.getParameterTypes();
440                if (types.length == 2) {
441                    writeParamTypeRef0 = new WeakReference<Class<?>>(types[0]);
442                    writeParamTypeRef1 = new WeakReference<Class<?>>(types[1]);
443                    writeParamClassNames = new String[2];
444                    writeParamClassNames[0] = types[0].getName();
445                    writeParamClassNames[1] = types[1].getName();
446                }
447            }
448        }
449        private Method get() {
450            if (methodRef == null) {
451                return null;
452            }
453            Method m = methodRef.get();
454            if (m == null) {
455                Class<?> clazz = classRef.get();
456                if (clazz == null) {
457                    clazz = reLoadClass();
458                    if (clazz != null) {
459                        classRef = new WeakReference<Class<?>>(clazz);
460                    }
461                }
462                if (clazz == null) {
463                    throw new RuntimeException("Method " + methodName + " for " +
464                            className + " could not be reconstructed - class reference has gone");
465                }
466                Class<?>[] paramTypes = null;
467                if (writeParamClassNames != null) {
468                    paramTypes = new Class[2];
469                    paramTypes[0] = writeParamTypeRef0.get();
470                    if (paramTypes[0] == null) {
471                        paramTypes[0] = reLoadClass(writeParamClassNames[0]);
472                        if (paramTypes[0] != null) {
473                            writeParamTypeRef0 = new WeakReference<Class<?>>(paramTypes[0]);
474                        }
475                    }
476                    paramTypes[1] = writeParamTypeRef1.get();
477                    if (paramTypes[1] == null) {
478                        paramTypes[1] = reLoadClass(writeParamClassNames[1]);
479                        if (paramTypes[1] != null) {
480                            writeParamTypeRef1 = new WeakReference<Class<?>>(paramTypes[1]);
481                        }
482                    }
483                } else {
484                    paramTypes = STRING_CLASS_PARAMETER;
485                }
486                try {
487                    m = clazz.getMethod(methodName, paramTypes);
488                    // Un-comment following line for testing
489                    // System.out.println("Recreated Method " + methodName + " for " + className);
490                } catch (NoSuchMethodException e) {
491                    throw new RuntimeException("Method " + methodName + " for " +
492                            className + " could not be reconstructed - method not found");
493                }
494                methodRef = new SoftReference<Method>(m);
495            }
496            return m;
497        }
498
499        /**
500         * Try to re-load the class
501         */
502        private Class<?> reLoadClass() {
503            return reLoadClass(className);
504        }
505
506        /**
507         * Try to re-load the class
508         */
509        private Class<?> reLoadClass(String name) {
510
511            ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
512
513            // Try the context class loader
514            if (classLoader != null) {
515                try {
516                    return classLoader.loadClass(name);
517                } catch (ClassNotFoundException e) {
518                    // ignore
519                }
520            }
521
522            // Try this class's class loader
523            classLoader = MappedPropertyDescriptor.class.getClassLoader();
524            try {
525                return classLoader.loadClass(name);
526            } catch (ClassNotFoundException e) {
527                return null;
528            }
529        }
530    }
531}