    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}