You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by oh...@apache.org on 2013/11/09 19:00:18 UTC

svn commit: r1540350 - in /commons/proper/beanutils/trunk/src: main/java/org/apache/commons/beanutils/ test/java/org/apache/commons/beanutils/

Author: oheger
Date: Sat Nov  9 18:00:17 2013
New Revision: 1540350

URL: http://svn.apache.org/r1540350
Log:
[BEANUTILS-425] Externalized introspection mechanism.

A new BeanIntrospector interface was added. Implementations are now responsible
for obtaining property descriptors. This makes it possible to support bean
properties that are not compliant to the Java Beans specification.

Added:
    commons/proper/beanutils/trunk/src/main/java/org/apache/commons/beanutils/BeanIntrospector.java
    commons/proper/beanutils/trunk/src/main/java/org/apache/commons/beanutils/DefaultBeanIntrospector.java
    commons/proper/beanutils/trunk/src/main/java/org/apache/commons/beanutils/DefaultIntrospectionContext.java
    commons/proper/beanutils/trunk/src/main/java/org/apache/commons/beanutils/IntrospectionContext.java
    commons/proper/beanutils/trunk/src/test/java/org/apache/commons/beanutils/DefaultIntrospectionContextTestCase.java
Modified:
    commons/proper/beanutils/trunk/src/main/java/org/apache/commons/beanutils/PropertyUtils.java
    commons/proper/beanutils/trunk/src/main/java/org/apache/commons/beanutils/PropertyUtilsBean.java
    commons/proper/beanutils/trunk/src/test/java/org/apache/commons/beanutils/PropertyUtilsTestCase.java

Added: commons/proper/beanutils/trunk/src/main/java/org/apache/commons/beanutils/BeanIntrospector.java
URL: http://svn.apache.org/viewvc/commons/proper/beanutils/trunk/src/main/java/org/apache/commons/beanutils/BeanIntrospector.java?rev=1540350&view=auto
==============================================================================
--- commons/proper/beanutils/trunk/src/main/java/org/apache/commons/beanutils/BeanIntrospector.java (added)
+++ commons/proper/beanutils/trunk/src/main/java/org/apache/commons/beanutils/BeanIntrospector.java Sat Nov  9 18:00:17 2013
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.beanutils;
+
+import java.beans.IntrospectionException;
+
+/**
+ * <p>
+ * Definition of an interface for components that can perform introspection on
+ * bean classes.
+ * </p>
+ * <p>
+ * Before {@link PropertyUtils} can be used for interaction with a specific Java
+ * class, the class's properties have to be determined. This is called
+ * <em>introspection</em> and is initiated automatically on demand.
+ * <code>PropertyUtils</code> does not perform introspection on its own, but
+ * delegates this task to one or more objects implementing this interface. This
+ * makes it possible to customize introspection which may be useful for certain
+ * code bases using non-standard conventions for accessing properties.
+ * </p>
+ *
+ * @version $Id: $
+ * @since 1.9
+ */
+public interface BeanIntrospector {
+    /**
+     * Performs introspection on a Java class. The current class to be inspected
+     * can be queried from the passed in <code>IntrospectionContext</code>
+     * object. A typical implementation has to obtain this class, determine its
+     * properties according to the rules it implements, and add them to the
+     * passed in context object.
+     *
+     * @param icontext the context object for interaction with the initiator of
+     *        the introspection request
+     * @throws IntrospectionException if an error occurs during introspection
+     */
+    void introspect(IntrospectionContext icontext)
+            throws IntrospectionException;
+}

Added: commons/proper/beanutils/trunk/src/main/java/org/apache/commons/beanutils/DefaultBeanIntrospector.java
URL: http://svn.apache.org/viewvc/commons/proper/beanutils/trunk/src/main/java/org/apache/commons/beanutils/DefaultBeanIntrospector.java?rev=1540350&view=auto
==============================================================================
--- commons/proper/beanutils/trunk/src/main/java/org/apache/commons/beanutils/DefaultBeanIntrospector.java (added)
+++ commons/proper/beanutils/trunk/src/main/java/org/apache/commons/beanutils/DefaultBeanIntrospector.java Sat Nov  9 18:00:17 2013
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.beanutils;
+
+import java.beans.BeanInfo;
+import java.beans.IndexedPropertyDescriptor;
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.Method;
+import java.util.List;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * <p>
+ * The default {@link BeanIntrospector} implementation.
+ * </p>
+ * <p>
+ * This class implements a default bean introspection algorithm based on the JDK
+ * classes in the <code>java.beans</code> package. It discovers properties
+ * conforming to the Java Beans specification.
+ * </p>
+ * <p>
+ * This class is a singleton. The single instance can be obtained using the
+ * {@code INSTANCE} field. It does not define any state and thus can be
+ * shared by arbitrary clients. {@link PropertyUtils} per default uses this
+ * instance as its only {@code BeanIntrospector} object.
+ * </p>
+ *
+ * @version $Id: $
+ * @since 1.9
+ */
+public class DefaultBeanIntrospector implements BeanIntrospector {
+    /** The singleton instance of this class. */
+    public static final BeanIntrospector INSTANCE = new DefaultBeanIntrospector();
+
+    /** Constant for argument types of a method that expects no arguments. */
+    private static final Class<?>[] EMPTY_CLASS_PARAMETERS = new Class[0];
+
+    /** Constant for arguments types of a method that expects a list argument. */
+    private static final Class<?>[] LIST_CLASS_PARAMETER = new Class[] { java.util.List.class };
+
+    /** Log instance */
+    private final Log log = LogFactory.getLog(getClass());
+
+    /**
+     * Private constructor so that no instances can be created.
+     */
+    private DefaultBeanIntrospector() {
+    }
+
+    /**
+     * Performs introspection of a specific Java class. This implementation uses
+     * the {@code java.beans.Introspector.getBeanInfo()} method to obtain
+     * all property descriptors for the current class and adds them to the
+     * passed in introspection context.
+     *
+     * @param icontext the introspection context
+     */
+    public void introspect(IntrospectionContext icontext) {
+        BeanInfo beanInfo = null;
+        try {
+            beanInfo = Introspector.getBeanInfo(icontext.getTargetClass());
+        } catch (IntrospectionException e) {
+            // no descriptors are added to the context
+            log.error(
+                    "Error when inspecting class " + icontext.getTargetClass(),
+                    e);
+            return;
+        }
+
+        PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors();
+        if (descriptors == null) {
+            descriptors = new PropertyDescriptor[0];
+        }
+
+        handleIndexedPropertyDescriptors(icontext.getTargetClass(),
+                descriptors);
+        icontext.addPropertyDescriptors(descriptors);
+    }
+
+    /**
+     * This method fixes an issue where IndexedPropertyDescriptor behaves
+     * differently in different versions of the JDK for 'indexed' properties
+     * which use java.util.List (rather than an array). It implements a
+     * workaround for Bug 28358. If you have a Bean with the following
+     * getters/setters for an indexed property:
+     *
+     * <pre>
+     * public List getFoo()
+     * public Object getFoo(int index)
+     * public void setFoo(List foo)
+     * public void setFoo(int index, Object foo)
+     * </pre>
+     *
+     * then the IndexedPropertyDescriptor's getReadMethod() and getWriteMethod()
+     * behave as follows:
+     * <ul>
+     * <li>JDK 1.3.1_04: returns valid Method objects from these methods.</li>
+     * <li>JDK 1.4.2_05: returns null from these methods.</li>
+     * </ul>
+     *
+     * @param beanClass the current class to be inspected
+     * @param descriptors the array with property descriptors
+     */
+    private void handleIndexedPropertyDescriptors(Class<?> beanClass,
+            PropertyDescriptor[] descriptors) {
+        for (PropertyDescriptor pd : descriptors) {
+            if (pd instanceof IndexedPropertyDescriptor) {
+                IndexedPropertyDescriptor descriptor = (IndexedPropertyDescriptor) pd;
+                String propName = descriptor.getName().substring(0, 1)
+                        .toUpperCase()
+                        + descriptor.getName().substring(1);
+
+                if (descriptor.getReadMethod() == null) {
+                    String methodName = descriptor.getIndexedReadMethod() != null ? descriptor
+                            .getIndexedReadMethod().getName() : "get"
+                            + propName;
+                    Method readMethod = MethodUtils
+                            .getMatchingAccessibleMethod(beanClass, methodName,
+                                    EMPTY_CLASS_PARAMETERS);
+                    if (readMethod != null) {
+                        try {
+                            descriptor.setReadMethod(readMethod);
+                        } catch (Exception e) {
+                            log.error(
+                                    "Error setting indexed property read method",
+                                    e);
+                        }
+                    }
+                }
+                if (descriptor.getWriteMethod() == null) {
+                    String methodName = descriptor.getIndexedWriteMethod() != null ? descriptor
+                            .getIndexedWriteMethod().getName() : "set"
+                            + propName;
+                    Method writeMethod = MethodUtils
+                            .getMatchingAccessibleMethod(beanClass, methodName,
+                                    LIST_CLASS_PARAMETER);
+                    if (writeMethod == null) {
+                        for (Method m : beanClass.getMethods()) {
+                            if (m.getName().equals(methodName)) {
+                                Class<?>[] parameterTypes = m.getParameterTypes();
+                                if (parameterTypes.length == 1
+                                        && List.class
+                                                .isAssignableFrom(parameterTypes[0])) {
+                                    writeMethod = m;
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                    if (writeMethod != null) {
+                        try {
+                            descriptor.setWriteMethod(writeMethod);
+                        } catch (Exception e) {
+                            log.error(
+                                    "Error setting indexed property write method",
+                                    e);
+                        }
+                    }
+                }
+            }
+        }
+    }
+}

Added: commons/proper/beanutils/trunk/src/main/java/org/apache/commons/beanutils/DefaultIntrospectionContext.java
URL: http://svn.apache.org/viewvc/commons/proper/beanutils/trunk/src/main/java/org/apache/commons/beanutils/DefaultIntrospectionContext.java?rev=1540350&view=auto
==============================================================================
--- commons/proper/beanutils/trunk/src/main/java/org/apache/commons/beanutils/DefaultIntrospectionContext.java (added)
+++ commons/proper/beanutils/trunk/src/main/java/org/apache/commons/beanutils/DefaultIntrospectionContext.java Sat Nov  9 18:00:17 2013
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.beanutils;
+
+import java.beans.PropertyDescriptor;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * <p>
+ * An implementation of the {@code IntrospectionContext} interface used by
+ * {@link PropertyUtilsBean} when doing introspection of a bean class.
+ * </p>
+ * <p>
+ * This class implements the methods required by the
+ * {@code IntrospectionContext} interface in a straight-forward manner
+ * based on a map. It is used internally only. It is not thread-safe.
+ * </p>
+ *
+ * @version $Id: $
+ * @since 1.9
+ */
+class DefaultIntrospectionContext implements IntrospectionContext {
+    /** Constant for an empty array of property descriptors. */
+    private static final PropertyDescriptor[] EMPTY_DESCRIPTORS = new PropertyDescriptor[0];
+
+    /** The current class for introspection. */
+    private final Class<?> currentClass;
+
+    /** A map for storing the already added property descriptors. */
+    private final Map<String, PropertyDescriptor> descriptors;
+
+    /**
+     *
+     * Creates a new instance of <code>DefaultIntrospectionContext</code> and sets
+     * the current class for introspection.
+     *
+     * @param cls the current class
+     */
+    public DefaultIntrospectionContext(Class<?> cls) {
+        currentClass = cls;
+        descriptors = new HashMap<String, PropertyDescriptor>();
+    }
+
+    public Class<?> getTargetClass() {
+        return currentClass;
+    }
+
+    public void addPropertyDescriptor(PropertyDescriptor desc) {
+        if (desc == null) {
+            throw new IllegalArgumentException(
+                    "Property descriptor must not be null!");
+        }
+        descriptors.put(desc.getName(), desc);
+    }
+
+    public void addPropertyDescriptors(PropertyDescriptor[] descs) {
+        if (descs == null) {
+            throw new IllegalArgumentException(
+                    "Array with descriptors must not be null!");
+        }
+
+        for (int i = 0; i < descs.length; i++) {
+            addPropertyDescriptor(descs[i]);
+        }
+    }
+
+    public boolean hasProperty(String name) {
+        return descriptors.containsKey(name);
+    }
+
+    public PropertyDescriptor getPropertyDescriptor(String name) {
+        return descriptors.get(name);
+    }
+
+    public void removePropertyDescriptor(String name) {
+        descriptors.remove(name);
+    }
+
+    public Set<String> propertyNames() {
+        return descriptors.keySet();
+    }
+
+    /**
+     * Returns an array with all descriptors added to this context. This method
+     * is used to obtain the results of introspection.
+     *
+     * @return an array with all known property descriptors
+     */
+    public PropertyDescriptor[] getPropertyDescriptors() {
+        return descriptors.values().toArray(EMPTY_DESCRIPTORS);
+    }
+}

Added: commons/proper/beanutils/trunk/src/main/java/org/apache/commons/beanutils/IntrospectionContext.java
URL: http://svn.apache.org/viewvc/commons/proper/beanutils/trunk/src/main/java/org/apache/commons/beanutils/IntrospectionContext.java?rev=1540350&view=auto
==============================================================================
--- commons/proper/beanutils/trunk/src/main/java/org/apache/commons/beanutils/IntrospectionContext.java (added)
+++ commons/proper/beanutils/trunk/src/main/java/org/apache/commons/beanutils/IntrospectionContext.java Sat Nov  9 18:00:17 2013
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.beanutils;
+
+import java.beans.PropertyDescriptor;
+import java.util.Set;
+
+/**
+ * <p>
+ * A context interface used during introspection for querying and setting
+ * property descriptors.
+ * </p>
+ * <p>
+ * An implementation of this interface is passed to {@link BeanIntrospector}
+ * objects during processing of a bean class. It allows the
+ * {@code BeanIntrospector} to deliver descriptors for properties it has
+ * detected. It is also possible to find out which properties have already been
+ * found by another {@code BeanIntrospector}; this allows multiple
+ * {@code BeanIntrospector} instances to collaborate.
+ * </p>
+ *
+ * @version $Id: $
+ * @since 1.9
+ */
+public interface IntrospectionContext {
+    /**
+     * Returns the class that is subject of introspection.
+     *
+     * @return the current class
+     */
+    Class<?> getTargetClass();
+
+    /**
+     * Adds the given property descriptor to this context. This method is called
+     * by a {@code BeanIntrospector} during introspection for each detected
+     * property. If this context already contains a descriptor for the affected
+     * property, it is overridden.
+     *
+     * @param desc the property descriptor
+     */
+    void addPropertyDescriptor(PropertyDescriptor desc);
+
+    /**
+     * Adds an array of property descriptors to this context. Using this method
+     * multiple descriptors can be added at once.
+     *
+     * @param descriptors the array of descriptors to be added
+     */
+    void addPropertyDescriptors(PropertyDescriptor[] descriptors);
+
+    /**
+     * Tests whether a descriptor for the property with the given name is
+     * already contained in this context. This method can be used for instance
+     * to prevent that an already existing property descriptor is overridden.
+     *
+     * @param name the name of the property in question
+     * @return <b>true</b> if a descriptor for this property has already been
+     *         added, <b>false</b> otherwise
+     */
+    boolean hasProperty(String name);
+
+    /**
+     * Returns the descriptor for the property with the given name or
+     * <b>null</b> if this property is unknown.
+     *
+     * @param name the name of the property in question
+     * @return the descriptor for this property or <b>null</b> if this property
+     *         is unknown
+     */
+    PropertyDescriptor getPropertyDescriptor(String name);
+
+    /**
+     * Removes the descriptor for the property with the given name.
+     *
+     * @param name the name of the affected property
+     */
+    void removePropertyDescriptor(String name);
+
+    /**
+     * Returns a set with the names of all properties known to this context.
+     *
+     * @return a set with the known property names
+     */
+    Set<String> propertyNames();
+}

Modified: commons/proper/beanutils/trunk/src/main/java/org/apache/commons/beanutils/PropertyUtils.java
URL: http://svn.apache.org/viewvc/commons/proper/beanutils/trunk/src/main/java/org/apache/commons/beanutils/PropertyUtils.java?rev=1540350&r1=1540349&r2=1540350&view=diff
==============================================================================
--- commons/proper/beanutils/trunk/src/main/java/org/apache/commons/beanutils/PropertyUtils.java (original)
+++ commons/proper/beanutils/trunk/src/main/java/org/apache/commons/beanutils/PropertyUtils.java Sat Nov  9 18:00:17 2013
@@ -155,6 +155,31 @@ public class PropertyUtils {
 
     }
 
+    /**
+     * Adds a <code>BeanIntrospector</code>. This object is invoked when the
+     * property descriptors of a class need to be obtained.
+     *
+     * @param introspector the <code>BeanIntrospector</code> to be added (must
+     *        not be <b>null</b>
+     * @throws IllegalArgumentException if the argument is <b>null</b>
+     * @since 1.9
+     */
+    public static void addBeanIntrospector(BeanIntrospector introspector) {
+        PropertyUtilsBean.getInstance().addBeanIntrospector(introspector);
+    }
+
+    /**
+     * Removes the specified <code>BeanIntrospector</code>.
+     *
+     * @param introspector the <code>BeanIntrospector</code> to be removed
+     * @return <b>true</b> if the <code>BeanIntrospector</code> existed and
+     *         could be removed, <b>false</b> otherwise
+     * @since 1.9
+     */
+    public static boolean removeBeanIntrospector(BeanIntrospector introspector) {
+        return PropertyUtilsBean.getInstance().removeBeanIntrospector(
+                introspector);
+    }
 
     /**
      * <p>Copy property values from the "origin" bean to the "destination" bean

Modified: commons/proper/beanutils/trunk/src/main/java/org/apache/commons/beanutils/PropertyUtilsBean.java
URL: http://svn.apache.org/viewvc/commons/proper/beanutils/trunk/src/main/java/org/apache/commons/beanutils/PropertyUtilsBean.java?rev=1540350&r1=1540349&r2=1540350&view=diff
==============================================================================
--- commons/proper/beanutils/trunk/src/main/java/org/apache/commons/beanutils/PropertyUtilsBean.java (original)
+++ commons/proper/beanutils/trunk/src/main/java/org/apache/commons/beanutils/PropertyUtilsBean.java Sat Nov  9 18:00:17 2013
@@ -18,7 +18,6 @@
 package org.apache.commons.beanutils;
 
 
-import java.beans.BeanInfo;
 import java.beans.IndexedPropertyDescriptor;
 import java.beans.IntrospectionException;
 import java.beans.Introspector;
@@ -31,6 +30,7 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.concurrent.CopyOnWriteArrayList;
 
 import org.apache.commons.beanutils.expression.DefaultResolver;
 import org.apache.commons.beanutils.expression.Resolver;
@@ -115,8 +115,6 @@ public class PropertyUtilsBean {
      */
     private WeakFastHashMap<Class<?>, PropertyDescriptor[]> descriptorsCache = null;
     private WeakFastHashMap<Class<?>, FastHashMap> mappedDescriptorsCache = null;
-    private static final Class<?>[] EMPTY_CLASS_PARAMETERS = new Class[0];
-    private static final Class<?>[] LIST_CLASS_PARAMETER = new Class[] {java.util.List.class};
 
     /** An empty object array */
     private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
@@ -124,6 +122,9 @@ public class PropertyUtilsBean {
     /** Log instance */
     private final Log log = LogFactory.getLog(PropertyUtils.class);
 
+    /** The list with BeanIntrospector objects. */
+    private final List<BeanIntrospector> introspectors;
+
     // ---------------------------------------------------------- Constructors
 
     /** Base constructor */
@@ -132,6 +133,8 @@ public class PropertyUtilsBean {
         descriptorsCache.setFast(true);
         mappedDescriptorsCache = new WeakFastHashMap<Class<?>, FastHashMap>();
         mappedDescriptorsCache.setFast(true);
+        introspectors = new CopyOnWriteArrayList<BeanIntrospector>();
+        introspectors.add(DefaultBeanIntrospector.INSTANCE);
     }
 
 
@@ -177,6 +180,35 @@ public class PropertyUtilsBean {
     }
 
     /**
+     * Adds a <code>BeanIntrospector</code>. This object is invoked when the
+     * property descriptors of a class need to be obtained.
+     *
+     * @param introspector the <code>BeanIntrospector</code> to be added (must
+     *        not be <b>null</b>
+     * @throws IllegalArgumentException if the argument is <b>null</b>
+     * @since 1.9
+     */
+    public void addBeanIntrospector(BeanIntrospector introspector) {
+        if (introspector == null) {
+            throw new IllegalArgumentException(
+                    "BeanIntrospector must not be null!");
+        }
+        introspectors.add(introspector);
+    }
+
+    /**
+     * Removes the specified <code>BeanIntrospector</code>.
+     *
+     * @param introspector the <code>BeanIntrospector</code> to be removed
+     * @return <b>true</b> if the <code>BeanIntrospector</code> existed and
+     *         could be removed, <b>false</b> otherwise
+     * @since 1.9
+     */
+    public boolean removeBeanIntrospector(BeanIntrospector introspector) {
+        return introspectors.remove(introspector);
+    }
+
+    /**
      * Clear any cached property descriptors information for all classes
      * loaded by any class loaders.  This is useful in cases where class
      * loaders are thrown away to implement class reloading.
@@ -969,90 +1001,7 @@ public class PropertyUtilsBean {
             return (descriptors);
         }
 
-        // Introspect the bean and cache the generated descriptors
-        BeanInfo beanInfo = null;
-        try {
-            beanInfo = Introspector.getBeanInfo(beanClass);
-        } catch (IntrospectionException e) {
-            return (new PropertyDescriptor[0]);
-        }
-        descriptors = beanInfo.getPropertyDescriptors();
-        if (descriptors == null) {
-            descriptors = new PropertyDescriptor[0];
-        }
-
-        // ----------------- Workaround for Bug 28358 --------- START ------------------
-        //
-        // The following code fixes an issue where IndexedPropertyDescriptor behaves
-        // Differently in different versions of the JDK for 'indexed' properties which
-        // use java.util.List (rather than an array).
-        //
-        // If you have a Bean with the following getters/setters for an indexed property:
-        //
-        //     public List getFoo()
-        //     public Object getFoo(int index)
-        //     public void setFoo(List foo)
-        //     public void setFoo(int index, Object foo)
-        //
-        // then the IndexedPropertyDescriptor's getReadMethod() and getWriteMethod()
-        // behave as follows:
-        //
-        //     JDK 1.3.1_04: returns valid Method objects from these methods.
-        //     JDK 1.4.2_05: returns null from these methods.
-        //
-        for (int i = 0; i < descriptors.length; i++) {
-            if (descriptors[i] instanceof IndexedPropertyDescriptor) {
-                IndexedPropertyDescriptor descriptor =  (IndexedPropertyDescriptor)descriptors[i];
-                String propName = descriptor.getName().substring(0, 1).toUpperCase() +
-                                  descriptor.getName().substring(1);
-
-                if (descriptor.getReadMethod() == null) {
-                    String methodName = descriptor.getIndexedReadMethod() != null
-                                        ? descriptor.getIndexedReadMethod().getName()
-                                        : "get" + propName;
-                    Method readMethod = MethodUtils.getMatchingAccessibleMethod(beanClass,
-                                                            methodName,
-                                                            EMPTY_CLASS_PARAMETERS);
-                    if (readMethod != null) {
-                        try {
-                            descriptor.setReadMethod(readMethod);
-                        } catch(Exception e) {
-                            log.error("Error setting indexed property read method", e);
-                        }
-                    }
-                }
-                if (descriptor.getWriteMethod() == null) {
-                    String methodName = descriptor.getIndexedWriteMethod() != null
-                                      ? descriptor.getIndexedWriteMethod().getName()
-                                      : "set" + propName;
-                    Method writeMethod = MethodUtils.getMatchingAccessibleMethod(beanClass,
-                                                            methodName,
-                                                            LIST_CLASS_PARAMETER);
-                    if (writeMethod == null) {
-                        Method[] methods = beanClass.getMethods();
-                        for (int j = 0; j < methods.length; j++) {
-                            if (methods[j].getName().equals(methodName)) {
-                                Class<?>[] parameterTypes = methods[j].getParameterTypes();
-                                if (parameterTypes.length == 1 &&
-                                    List.class.isAssignableFrom(parameterTypes[0])) {
-                                    writeMethod = methods[j];
-                                    break;
-                                }
-                            }
-                        }
-                    }
-                    if (writeMethod != null) {
-                        try {
-                            descriptor.setWriteMethod(writeMethod);
-                        } catch(Exception e) {
-                            log.error("Error setting indexed property write method", e);
-                        }
-                    }
-                }
-            }
-        }
-        // ----------------- Workaround for Bug 28358 ---------- END -------------------
-
+        descriptors = fetchPropertyDescriptors(beanClass);
         descriptorsCache.put(beanClass, descriptors);
         return (descriptors);
 
@@ -2248,6 +2197,27 @@ public class PropertyUtilsBean {
     }
 
     /**
+     * Performs introspection on the specified class. This method invokes all {@code BeanIntrospector} objects that were
+     * added to this instance.
+     *
+     * @param beanClass the class to be inspected
+     * @return an array with all property descriptors found
+     */
+    private PropertyDescriptor[] fetchPropertyDescriptors(Class<?> beanClass) {
+        DefaultIntrospectionContext ictx = new DefaultIntrospectionContext(beanClass);
+
+        for (BeanIntrospector bi : introspectors) {
+            try {
+                bi.introspect(ictx);
+            } catch (IntrospectionException iex) {
+                log.error("Exception during introspection", iex);
+            }
+        }
+
+        return ictx.getPropertyDescriptors();
+    }
+
+    /**
      * Converts an object to a list of objects. This method is used when dealing
      * with indexed properties. It assumes that indexed properties are stored as
      * lists of objects.

Added: commons/proper/beanutils/trunk/src/test/java/org/apache/commons/beanutils/DefaultIntrospectionContextTestCase.java
URL: http://svn.apache.org/viewvc/commons/proper/beanutils/trunk/src/test/java/org/apache/commons/beanutils/DefaultIntrospectionContextTestCase.java?rev=1540350&view=auto
==============================================================================
--- commons/proper/beanutils/trunk/src/test/java/org/apache/commons/beanutils/DefaultIntrospectionContextTestCase.java (added)
+++ commons/proper/beanutils/trunk/src/test/java/org/apache/commons/beanutils/DefaultIntrospectionContextTestCase.java Sat Nov  9 18:00:17 2013
@@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.beanutils;
+
+import java.beans.IntrospectionException;
+import java.beans.PropertyDescriptor;
+import java.util.HashSet;
+import java.util.Set;
+
+import junit.framework.TestCase;
+
+/**
+ * Test class for {@code IntrospectionContext}.
+ *
+ * @version $Id: $
+ */
+public class DefaultIntrospectionContextTestCase extends TestCase {
+    /** Constant for the name of a property. */
+    private static final String PROP = "foo";
+
+    /** The context to be tested. */
+    private DefaultIntrospectionContext context;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        context = new DefaultIntrospectionContext(getClass());
+    }
+
+    /**
+     * Creates a property descriptor object for a property with the given name.
+     *
+     * @param propName the property name
+     * @return the descriptor for this property
+     */
+    private static PropertyDescriptor createDescriptor(String propName) {
+        try {
+            return new PropertyDescriptor(propName,
+                    DefaultIntrospectionContextTestCase.class, null, null);
+        } catch (IntrospectionException e) {
+            throw new IllegalStateException("Unexpected exception: " + e);
+        }
+    }
+
+    /**
+     * Tests a newly created instance.
+     */
+    public void testInit() {
+        assertEquals("Wrong current class", getClass(),
+                context.getTargetClass());
+        assertTrue("Got property names", context.propertyNames().isEmpty());
+    }
+
+    /**
+     * Tests whether a property descriptor can be added.
+     */
+    public void testAddPropertyDescriptor() {
+        PropertyDescriptor desc = createDescriptor(PROP);
+        context.addPropertyDescriptor(desc);
+        assertTrue("Property not found", context.hasProperty(PROP));
+        assertSame("Wrong descriptor", desc,
+                context.getPropertyDescriptor(PROP));
+    }
+
+    /**
+     * Tries to add a null descriptor.
+     */
+    public void testAddPropertyDescriptorNull() {
+        try {
+            context.addPropertyDescriptor(null);
+            fail("Could add null descriptor!");
+        } catch (IllegalArgumentException iex) {
+            // ok
+        }
+    }
+
+    /**
+     * Tests whether multiple descriptors can be added.
+     */
+    public void testAddPropertyDescriptors() {
+        final int count = 4;
+        PropertyDescriptor[] descs = new PropertyDescriptor[count];
+        Set<PropertyDescriptor> descSet = new HashSet<PropertyDescriptor>();
+        for (int i = 0; i < count; i++) {
+            descs[i] = createDescriptor(PROP + i);
+            descSet.add(descs[i]);
+        }
+        context.addPropertyDescriptors(descs);
+        PropertyDescriptor d = createDescriptor(PROP);
+        context.addPropertyDescriptor(d);
+        descSet.add(d);
+        Set<String> names = context.propertyNames();
+        assertEquals("Wrong number of property names", count + 1, names.size());
+        assertTrue("Property not found: " + PROP, names.contains(PROP));
+        for (int i = 0; i < count; i++) {
+            assertTrue("Property not found: " + (PROP + i),
+                    names.contains(PROP + i));
+        }
+        PropertyDescriptor[] addedDescs = context.getPropertyDescriptors();
+        assertEquals("Wrong number of added descriptors", count + 1,
+                addedDescs.length);
+        for (PropertyDescriptor pd : addedDescs) {
+            assertTrue("Unexpected descriptor: " + pd, descSet.remove(pd));
+        }
+    }
+
+    /**
+     * Tries to add a null array with property descriptors.
+     */
+    public void testAddPropertyDescriptorsNull() {
+        try {
+            context.addPropertyDescriptors(null);
+            fail("Could add a null array with descriptors!");
+        } catch (IllegalArgumentException iex) {
+            // ok
+        }
+    }
+
+    /**
+     * Tests hasProperty() if the expected result is false.
+     */
+    public void testHasPropertyFalse() {
+        assertFalse("Wrong result (1)", context.hasProperty(PROP));
+        context.addPropertyDescriptor(createDescriptor(PROP));
+        assertFalse("Wrong result (2)", context.hasProperty("other"));
+    }
+
+    /**
+     * Tests getPropertyDescriptor() if the property name is unknown.
+     */
+    public void testGetPropertyDescriptorUnknown() {
+        assertNull("Got a property (1)", context.getPropertyDescriptor(PROP));
+        context.addPropertyDescriptor(createDescriptor(PROP));
+        assertNull("Got a property (2)", context.getPropertyDescriptor("other"));
+    }
+
+    /**
+     * Tests that the set with property names cannot be changed.
+     */
+    public void testPropertyNamesModify() {
+        Set<String> names = context.propertyNames();
+        try {
+            names.add(PROP);
+            fail("Could modify property names set!");
+        } catch (UnsupportedOperationException uex) {
+            // ok
+        }
+    }
+
+    /**
+     * Tests whether a descriptor can be removed.
+     */
+    public void testRemovePropertyDescriptor() {
+        context.addPropertyDescriptor(createDescriptor(PROP));
+        context.removePropertyDescriptor(PROP);
+        assertTrue("Got property names", context.propertyNames().isEmpty());
+        assertEquals("Got descriptors", 0,
+                context.getPropertyDescriptors().length);
+    }
+}

Modified: commons/proper/beanutils/trunk/src/test/java/org/apache/commons/beanutils/PropertyUtilsTestCase.java
URL: http://svn.apache.org/viewvc/commons/proper/beanutils/trunk/src/test/java/org/apache/commons/beanutils/PropertyUtilsTestCase.java?rev=1540350&r1=1540349&r2=1540350&view=diff
==============================================================================
--- commons/proper/beanutils/trunk/src/test/java/org/apache/commons/beanutils/PropertyUtilsTestCase.java (original)
+++ commons/proper/beanutils/trunk/src/test/java/org/apache/commons/beanutils/PropertyUtilsTestCase.java Sat Nov  9 18:00:17 2013
@@ -19,14 +19,17 @@
 package org.apache.commons.beanutils;
 
 
+import java.beans.IntrospectionException;
 import java.beans.PropertyDescriptor;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import junit.framework.Test;
 import junit.framework.TestCase;
@@ -4438,4 +4441,104 @@ public class PropertyUtilsTestCase exten
             fail("Expected IllegalArgumentException, but threw " + t);
         }
     }
+
+    /**
+     * Tests whether the default introspection mechanism can be replaced by a
+     * custom BeanIntrospector.
+     */
+    public void testCustomIntrospection() {
+        PropertyDescriptor[] desc1 = PropertyUtils
+                .getPropertyDescriptors(AlphaBean.class);
+        PropertyDescriptor nameDescriptor = findNameDescriptor(desc1);
+        assertNotNull("No write method", nameDescriptor.getWriteMethod());
+
+        BeanIntrospector bi = new BeanIntrospector() {
+            // Only produce read-only property descriptors
+            public void introspect(IntrospectionContext icontext)
+                    throws IntrospectionException {
+                Set names = icontext.propertyNames();
+                PropertyDescriptor[] newDescs = new PropertyDescriptor[names
+                        .size()];
+                int idx = 0;
+                for (Iterator it = names.iterator(); it.hasNext(); idx++) {
+                    String propName = (String) it.next();
+                    PropertyDescriptor pd = icontext
+                            .getPropertyDescriptor(propName);
+                    newDescs[idx] = new PropertyDescriptor(pd.getName(),
+                            pd.getReadMethod(), null);
+                }
+                icontext.addPropertyDescriptors(newDescs);
+            }
+        };
+        PropertyUtils.clearDescriptors();
+        PropertyUtils.addBeanIntrospector(bi);
+        PropertyDescriptor[] desc2 = PropertyUtils
+                .getPropertyDescriptors(AlphaBean.class);
+        assertEquals("Different number of properties", desc1.length,
+                desc2.length);
+        nameDescriptor = findNameDescriptor(desc2);
+        assertNull("Got a write method", nameDescriptor.getWriteMethod());
+        PropertyUtils.removeBeanIntrospector(bi);
+    }
+
+    /**
+     * Finds the descriptor of the name property.
+     *
+     * @param desc the array with descriptors
+     * @return the found descriptor or null
+     */
+    private static PropertyDescriptor findNameDescriptor(
+            PropertyDescriptor[] desc) {
+        for (int i = 0; i < desc.length; i++) {
+            if (desc[i].getName().equals("name")) {
+                return desc[i];
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Tests whether exceptions during custom introspection are handled.
+     */
+    public void testCustomIntrospectionEx() {
+        BeanIntrospector bi = new BeanIntrospector() {
+            public void introspect(IntrospectionContext icontext)
+                    throws IntrospectionException {
+                throw new IntrospectionException("TestException");
+            }
+        };
+        PropertyUtils.clearDescriptors();
+        PropertyUtils.addBeanIntrospector(bi);
+        PropertyDescriptor[] desc = PropertyUtils
+                .getPropertyDescriptors(AlphaBean.class);
+        assertNotNull("Introspection did not work", findNameDescriptor(desc));
+        PropertyUtils.removeBeanIntrospector(bi);
+    }
+
+    /**
+     * Tests whether a BeanIntrospector can be removed.
+     */
+    public void testRemoveBeanIntrospector() {
+        PropertyUtils.clearDescriptors();
+        assertTrue(
+                "Wrong result",
+                PropertyUtils
+                        .removeBeanIntrospector(DefaultBeanIntrospector.INSTANCE));
+        PropertyDescriptor[] desc = PropertyUtils
+                .getPropertyDescriptors(AlphaBean.class);
+        assertEquals("Got descriptors", 0, desc.length);
+        PropertyUtils.addBeanIntrospector(DefaultBeanIntrospector.INSTANCE);
+    }
+
+    /**
+     * Tries to add a null BeanIntrospector.
+     */
+    public void testAddBeanIntrospectorNull() {
+        try {
+            PropertyUtils.addBeanIntrospector(null);
+            fail("Could add null BeanIntrospector!");
+        } catch (IllegalArgumentException iex) {
+            // ok
+        }
+    }
 }