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