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