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