    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}