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