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