Revision: 1181
          http://stripes.svn.sourceforge.net/stripes/?rev=1181&view=rev
Author:   bengunter
Date:     2009-10-21 19:17:10 +0000 (Wed, 21 Oct 2009)

Log Message:
-----------
A more complete fix for STS-664: JDK bug in bean introspection prevents Stripes 
validation. I added several useful methods to help deal with this problem in 
ReflectUtil and updated several classes to get the property descriptors from 
ReflectUtil instead of directly from Introspector. ReflectUtil transparently 
handles bridge methods in PropertyDescriptors so that calling code need not be 
concerned with them.

Modified Paths:
--------------
    
branches/1.5.x/stripes/src/net/sourceforge/stripes/ajax/JavaScriptBuilder.java
    
branches/1.5.x/stripes/src/net/sourceforge/stripes/exception/DefaultExceptionHandler.java
    branches/1.5.x/stripes/src/net/sourceforge/stripes/util/ReflectUtil.java
    
branches/1.5.x/stripes/src/net/sourceforge/stripes/util/bean/PropertyExpressionEvaluation.java
    
branches/1.5.x/stripes/src/net/sourceforge/stripes/validation/DefaultValidationMetadataProvider.java
    
branches/1.5.x/tests/src/net/sourceforge/stripes/validation/ValidationWithGenericsTest.java

Modified: 
branches/1.5.x/stripes/src/net/sourceforge/stripes/ajax/JavaScriptBuilder.java
===================================================================
--- 
branches/1.5.x/stripes/src/net/sourceforge/stripes/ajax/JavaScriptBuilder.java  
    2009-10-20 19:47:29 UTC (rev 1180)
+++ 
branches/1.5.x/stripes/src/net/sourceforge/stripes/ajax/JavaScriptBuilder.java  
    2009-10-21 19:17:10 UTC (rev 1181)
@@ -16,8 +16,8 @@
 
 import net.sourceforge.stripes.exception.StripesRuntimeException;
 import net.sourceforge.stripes.util.Log;
+import net.sourceforge.stripes.util.ReflectUtil;
 
-import java.beans.Introspector;
 import java.beans.PropertyDescriptor;
 import java.io.StringWriter;
 import java.io.Writer;
@@ -389,7 +389,7 @@
     void buildObjectNode(String targetName, Object in, String propertyPrefix) 
throws Exception {
         StringBuilder out = new StringBuilder();
         out.append("{");
-        PropertyDescriptor[] props = 
Introspector.getBeanInfo(in.getClass()).getPropertyDescriptors();
+        PropertyDescriptor[] props = 
ReflectUtil.getPropertyDescriptors(in.getClass());
 
         for (PropertyDescriptor property : props) {
             try {

Modified: 
branches/1.5.x/stripes/src/net/sourceforge/stripes/exception/DefaultExceptionHandler.java
===================================================================
--- 
branches/1.5.x/stripes/src/net/sourceforge/stripes/exception/DefaultExceptionHandler.java
   2009-10-20 19:47:29 UTC (rev 1180)
+++ 
branches/1.5.x/stripes/src/net/sourceforge/stripes/exception/DefaultExceptionHandler.java
   2009-10-21 19:17:10 UTC (rev 1181)
@@ -14,8 +14,7 @@
  */
 package net.sourceforge.stripes.exception;
 
-import java.beans.BeanInfo;
-import java.beans.Introspector;
+
 import java.beans.PropertyDescriptor;
 import java.io.IOException;
 import java.lang.reflect.Method;
@@ -41,6 +40,7 @@
 import net.sourceforge.stripes.controller.StripesConstants;
 import net.sourceforge.stripes.controller.StripesRequestWrapper;
 import net.sourceforge.stripes.util.Log;
+import net.sourceforge.stripes.util.ReflectUtil;
 import net.sourceforge.stripes.validation.LocalizableError;
 
 /**
@@ -237,8 +237,8 @@
         // Try to guess the field name by finding exactly one FileBean field
         String fieldName = null;
         try {
-            BeanInfo beanInfo = 
Introspector.getBeanInfo(actionBean.getClass());
-            for (PropertyDescriptor pd : beanInfo.getPropertyDescriptors()) {
+            PropertyDescriptor[] pds = 
ReflectUtil.getPropertyDescriptors(actionBean.getClass());
+            for (PropertyDescriptor pd : pds) {
                 if (FileBean.class.isAssignableFrom(pd.getPropertyType())) {
                     if (fieldName == null) {
                         // First FileBean field found so set the field name

Modified: 
branches/1.5.x/stripes/src/net/sourceforge/stripes/util/ReflectUtil.java
===================================================================
--- branches/1.5.x/stripes/src/net/sourceforge/stripes/util/ReflectUtil.java    
2009-10-20 19:47:29 UTC (rev 1180)
+++ branches/1.5.x/stripes/src/net/sourceforge/stripes/util/ReflectUtil.java    
2009-10-21 19:17:10 UTC (rev 1181)
@@ -16,6 +16,8 @@
 
 import net.sourceforge.stripes.exception.StripesRuntimeException;
 
+import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.HashMap;
 import java.util.Collection;
@@ -50,6 +52,8 @@
  * @author Tim Fennell
  */
 public class ReflectUtil {
+    private static final Log log = Log.getInstance(ReflectUtil.class);
+
     /** A cache of property descriptors by class and property name */
     private static Map<Class<?>, Map<String, PropertyDescriptor>> 
propertyDescriptors =
             new ConcurrentHashMap<Class<?>, Map<String, PropertyDescriptor>>();
@@ -289,26 +293,9 @@
      * @return the PropertyDescriptor or null if none is found with a matching 
name
      */
     public static PropertyDescriptor getPropertyDescriptor(Class<?> clazz, 
String property) {
-        Map<String,PropertyDescriptor> pds = propertyDescriptors.get(clazz);
-        if (pds == null) {
-            try {
-                BeanInfo info = Introspector.getBeanInfo(clazz);
-                PropertyDescriptor[] descriptors = 
info.getPropertyDescriptors();
-                pds = new HashMap<String, PropertyDescriptor>();
-
-                for (PropertyDescriptor descriptor : descriptors) {
-                    pds.put(descriptor.getName(), descriptor);
-                }
-
-                propertyDescriptors.put(clazz, pds);
-            }
-            catch (IntrospectionException ie) {
-                throw new StripesRuntimeException("Could not examine class '" 
+ clazz.getName() +
-                        "' using Introspector.getBeanInfo() to determine 
property information.", ie);
-            }
-        }
-
-        return pds.get(property);
+        if (!propertyDescriptors.containsKey(clazz))
+            getPropertyDescriptors(clazz);
+        return propertyDescriptors.get(clazz).get(property);
     }
 
     /**
@@ -463,4 +450,212 @@
 
         return null;
     }
+
+    /**
+     * Get the {...@link PropertyDescriptor}s for a bean class. This is 
normally easy enough to do
+     * except that Java versions 6 and earlier have a bug that can return 
bridge methods for
+     * property getters and/or setters. That can mess up validation and 
binding and possibly other
+     * areas. This method accounts for that bug and attempts to work around 
it, ensuring the
+     * property descriptors contain the true getter and setter methods.
+     * 
+     * @param clazz The bean class to introspect
+     * @return The property descriptors for the bean class, as returned by
+     *         {...@link BeanInfo#getPropertyDescriptors()}.
+     */
+    public static PropertyDescriptor[] getPropertyDescriptors(Class<?> clazz) {
+        // Look in the cache first
+        if (propertyDescriptors.containsKey(clazz)) {
+            Collection<PropertyDescriptor> pds = 
propertyDescriptors.get(clazz).values();
+            return pds.toArray(new PropertyDescriptor[pds.size()]);
+        }
+
+        // A subclass that is aware of bridge methods
+        class BridgedPropertyDescriptor extends PropertyDescriptor {
+            private Method readMethod, writeMethod;
+            private Class<?> propertyType;
+
+            public BridgedPropertyDescriptor(PropertyDescriptor pd) throws 
IntrospectionException {
+                super(pd.getName(), pd.getReadMethod(), pd.getWriteMethod());
+                readMethod = resolveBridgedReadMethod(pd);
+                writeMethod = resolveBridgedWriteMethod(pd);
+                propertyType = resolvePropertyType(pd);
+            }
+
+            @Override
+            public synchronized Class<?> getPropertyType() {
+                return propertyType;
+            }
+
+            @Override
+            public synchronized Method getReadMethod() {
+                return readMethod;
+            }
+
+            @Override
+            public synchronized Method getWriteMethod() {
+                return writeMethod;
+            }
+        }
+
+        // Not cached yet. Look it up the normal way.
+        try {
+            PropertyDescriptor[] pds = 
Introspector.getBeanInfo(clazz).getPropertyDescriptors();
+            Map<String, PropertyDescriptor> map = new LinkedHashMap<String, 
PropertyDescriptor>();
+            for (int i = 0; i < pds.length; i++) {
+                PropertyDescriptor pd = pds[i];
+                if ((pd.getReadMethod() != null && 
pd.getReadMethod().isBridge())
+                        || (pd.getWriteMethod() != null && 
pd.getWriteMethod().isBridge())) {
+                    log.debug("Working around JVM bug involving 
PropertyDescriptors ",
+                            "and bridge methods for ", clazz);
+                    pd = new BridgedPropertyDescriptor(pd);
+                    pds[i] = pd;
+                }
+                map.put(pd.getName(), pd);
+            }
+
+            // Put cache entry
+            propertyDescriptors.put(clazz, map);
+
+            return pds;
+        }
+        catch (IntrospectionException ie) {
+            throw new StripesRuntimeException("Could not examine class '" + 
clazz.getName()
+                    + "' using Introspector.getBeanInfo() to determine 
property information.", ie);
+        }
+    }
+
+    /**
+     * Locate and return the bridged read method for a bean property.
+     * 
+     * @param pd The bean property descriptor
+     * @return The bridged method or the property descriptor's read method, if 
it is not a bridge
+     *         method.
+     */
+    public static Method resolveBridgedReadMethod(PropertyDescriptor pd) {
+        Method getter = pd.getReadMethod();
+
+        if (getter != null && getter.isBridge()) {
+            try {
+                getter = 
getter.getDeclaringClass().getMethod(getter.getName());
+            }
+            catch (SecurityException e) {
+                // Ignore exception and keep whatever was in the property 
descriptor
+            }
+            catch (NoSuchMethodException e) {
+                // Ignore exception and keep whatever was in the property 
descriptor
+            }
+        }
+
+        return getter;
+    }
+
+    /**
+     * Locate and return the bridged write method for a bean property.
+     * 
+     * @param pd The bean property descriptor
+     * @return The bridged method or the property descriptor's write method, 
if it is not a bridge
+     *         method.
+     */
+    public static Method resolveBridgedWriteMethod(PropertyDescriptor pd) {
+        Method setter = pd.getWriteMethod();
+
+        if (setter != null && setter.isBridge()) {
+            // Make a list of methods with the same name that take a single 
parameter
+            List<Method> candidates = new ArrayList<Method>();
+            Method[] methods = setter.getDeclaringClass().getMethods();
+            for (Method method : methods) {
+                if (!method.isBridge() && 
method.getName().equals(setter.getName())
+                        && method.getParameterTypes().length == 1
+                        && 
pd.getPropertyType().isAssignableFrom(method.getParameterTypes()[0])) {
+                    candidates.add(method);
+                }
+            }
+
+            if (candidates.size() == 1) {
+                setter = candidates.get(0);
+            }
+            else if (candidates.isEmpty()) {
+                setter = null;
+                log.error("Something has gone awry! I have a bridge to 
nowhere: ", setter);
+            }
+            else {
+                // Clear the setter and try to find a best guess
+                setter = null;
+
+                // Create a set of all type arguments for all classes 
declaring the matching methods
+                Set<Type> typeArgs = new HashSet<Type>();
+                for (Method method : candidates) {
+                    Class<?> declarer = method.getDeclaringClass();
+
+                    // Add type arguments for interfaces
+                    for (Class<?> iface : getImplementedInterfaces(declarer)) {
+                        Type[] types = getActualTypeArguments(declarer, iface);
+                        if (types != null)
+                            typeArgs.addAll(Arrays.asList(types));
+                    }
+
+                    // Add type arguments for superclasses
+                    for (Class<?> c = declarer.getSuperclass(); c != null; c = 
c.getSuperclass()) {
+                        Type[] types = getActualTypeArguments(declarer, c);
+                        if (types != null)
+                            typeArgs.addAll(Arrays.asList(types));
+                    }
+                }
+
+                // Now cycle through, collecting only those methods whose 
return type is a type arg
+                List<Method> primeCandidates = new 
ArrayList<Method>(candidates);
+                Iterator<Method> iterator = primeCandidates.iterator();
+                while (iterator.hasNext()) {
+                    if 
(!typeArgs.contains(iterator.next().getParameterTypes()[0]))
+                        iterator.remove();
+                }
+
+                // If we are left with exactly one match, then go with it
+                if (primeCandidates.size() == 1)
+                    setter = primeCandidates.get(0);
+
+                // FAIL
+                if (setter == null) {
+                    log.warn("Unable to locate a bridged setter for ", 
pd.getName(),
+                            " due to a JVM bug and an overloaded method with ",
+                            "the same name as the property setter. This could 
be a problem. ",
+                            "The offending overloaded methods are: ", 
candidates);
+                }
+            }
+        }
+
+        return setter;
+    }
+
+    /**
+     * Under normal circumstances, a property's getter will return exactly the 
same type as its
+     * setter accepts as a parameter. However, because we have to hack around 
the JVM bug dealing
+     * with bridge methods this might not always be the case. This method 
resolves the actual type
+     * of the property. In the case where the two types (return type and 
parameter type) are not
+     * identical, the property type is whichever of the two is lower in the 
class hierarchy.
+     * 
+     * @param pd The property descriptor
+     * @return The type of the property
+     */
+    public static Class<?> resolvePropertyType(PropertyDescriptor pd) {
+        Method readMethod = pd.getReadMethod();
+        Method writeMethod = pd.getWriteMethod();
+        Class<?> returnType = readMethod == null ? null : 
readMethod.getReturnType();
+        Class<?> paramType = writeMethod == null ? null : 
writeMethod.getParameterTypes()[0];
+
+        // For a read-only property, use the getter's return type
+        if (readMethod != null && writeMethod == null)
+            return returnType;
+
+        // For a write-only property, use the setter's parameter type
+        if (writeMethod != null && readMethod == null)
+            return paramType;
+
+        // If the two types are identical (generally the case), then this is 
easy
+        if (returnType == paramType)
+            return returnType;
+
+        // Otherwise, take the type that is *lower* in the class hierarchy
+        return returnType.isAssignableFrom(paramType) ? paramType : returnType;
+    }
 }

Modified: 
branches/1.5.x/stripes/src/net/sourceforge/stripes/util/bean/PropertyExpressionEvaluation.java
===================================================================
--- 
branches/1.5.x/stripes/src/net/sourceforge/stripes/util/bean/PropertyExpressionEvaluation.java
      2009-10-20 19:47:29 UTC (rev 1180)
+++ 
branches/1.5.x/stripes/src/net/sourceforge/stripes/util/bean/PropertyExpressionEvaluation.java
      2009-10-21 19:17:10 UTC (rev 1181)
@@ -206,10 +206,10 @@
         PropertyDescriptor pd = ReflectUtil.getPropertyDescriptor(beanClass, 
property);
         if (pd != null) {
             if (pd.getReadMethod() != null) {
-                return 
untangleBridgeMethod(pd.getReadMethod()).getGenericReturnType();
+                return pd.getReadMethod().getGenericReturnType();
             }
             else {
-                return 
untangleBridgeMethod(pd.getWriteMethod()).getGenericParameterTypes()[0];
+                return pd.getWriteMethod().getGenericParameterTypes()[0];
             }
         }
         else {
@@ -236,7 +236,10 @@
      *
      * @param m a Method instance, potentially a bridge method
      * @return a non-bridge method instance if one is locatable, otherwise the 
method passed in
+     * @deprecated This method is no longer used. Bridge methods are handled 
transparently by
+     *             {...@link ReflectUtil#getPropertyDescriptors(Class)}.
      */
+    @Deprecated
     protected Method untangleBridgeMethod(Method m) {
         if (!m.isBridge()) return m;
 

Modified: 
branches/1.5.x/stripes/src/net/sourceforge/stripes/validation/DefaultValidationMetadataProvider.java
===================================================================
--- 
branches/1.5.x/stripes/src/net/sourceforge/stripes/validation/DefaultValidationMetadataProvider.java
        2009-10-20 19:47:29 UTC (rev 1180)
+++ 
branches/1.5.x/stripes/src/net/sourceforge/stripes/validation/DefaultValidationMetadataProvider.java
        2009-10-21 19:17:10 UTC (rev 1181)
@@ -14,7 +14,6 @@
  */
 package net.sourceforge.stripes.validation;
 
-import java.beans.Introspector;
 import java.beans.PropertyDescriptor;
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
@@ -33,6 +32,7 @@
 import net.sourceforge.stripes.controller.ParameterName;
 import net.sourceforge.stripes.exception.StripesRuntimeException;
 import net.sourceforge.stripes.util.Log;
+import net.sourceforge.stripes.util.ReflectUtil;
 
 /**
  * An implementation of {...@link ValidationMetadataProvider} that scans 
classes and their superclasses
@@ -93,7 +93,7 @@
         try {
             for (Class<?> clazz = beanType; clazz != null; clazz = 
clazz.getSuperclass()) {
                 List<PropertyDescriptor> pds = new 
ArrayList<PropertyDescriptor>(
-                    
Arrays.asList(Introspector.getBeanInfo(clazz).getPropertyDescriptors()));
+                        
Arrays.asList(ReflectUtil.getPropertyDescriptors(beanType)));
 
                 // Also look at public fields
                 Field[] publicFields = clazz.getFields();
@@ -112,10 +112,6 @@
                     catch (NoSuchFieldException e) {
                     }
 
-                    // Work around a bug in JDK6 and earlier. See STS-664 for 
more information.
-                    if (accessor != null && accessor.isBridge())
-                        accessor = 
accessor.getDeclaringClass().getMethod(accessor.getName());
-
                     boolean onAccessor = accessor != null
                             && Modifier.isPublic(accessor.getModifiers())
                             && accessor.getDeclaringClass().equals(clazz)

Modified: 
branches/1.5.x/tests/src/net/sourceforge/stripes/validation/ValidationWithGenericsTest.java
===================================================================
--- 
branches/1.5.x/tests/src/net/sourceforge/stripes/validation/ValidationWithGenericsTest.java
 2009-10-20 19:47:29 UTC (rev 1180)
+++ 
branches/1.5.x/tests/src/net/sourceforge/stripes/validation/ValidationWithGenericsTest.java
 2009-10-21 19:17:10 UTC (rev 1181)
@@ -23,6 +23,9 @@
         public void setPassword(String password) { this.password = password; }
     }
 
+    public static class AdminUser extends User {}
+    public static class SuperUser extends AdminUser {}
+
     public static class BaseActionBean<T> implements ActionBean {
         private ActionBeanContext context;
         private T model;
@@ -32,16 +35,63 @@
         public void setContext(ActionBeanContext context) { this.context = 
context; }
     }
 
-    public static class TestActionBean extends BaseActionBean<User> {
+    public static class OverrideGetterAndSetterActionBean extends 
BaseActionBean<User> {
         @Override
         @ValidateNestedProperties( {
                 @Validate(field = "username", required = true),
                 @Validate(field = "password", required = true)
         })
         public User getModel() { return super.getModel(); }
+        public void setModel(User user) { super.setModel(user); }
         public Resolution login() { return null; }
     }
 
+    public static class OverrideGetterActionBean extends BaseActionBean<User> {
+        @Override
+        @ValidateNestedProperties( {
+                @Validate(field = "username", required = true),
+                @Validate(field = "password", required = true)
+        })
+        public User getModel() { return super.getModel(); }
+        public Resolution login() { return null; }
+    }
+
+    public static class OverrideSetterActionBean extends BaseActionBean<User> {
+        @Override
+        @ValidateNestedProperties( {
+                @Validate(field = "username", required = true),
+                @Validate(field = "password", required = true)
+        })
+        public void setModel(User user) { super.setModel(user); }
+        public Resolution login() { return null; }
+    }
+
+    public static class OverloadSetterActionBean extends BaseActionBean<User> {
+        @Override
+        @ValidateNestedProperties( {
+                @Validate(field = "username", required = true),
+                @Validate(field = "password", required = true)
+        })
+        public void setModel(User user) { super.setModel(user); }
+        public void setModel(AdminUser user) {}
+        public void setModel(SuperUser user) {}
+        public void setModel(String string) {}
+        public void setModel(Integer integer) {}
+        public Resolution login() { return null; }
+    }
+
+    public static class ExtendOverloadSetterActionBean extends 
OverloadSetterActionBean {
+    }
+
+    public static class ExtendOverloadSetterAgainActionBean extends 
ExtendOverloadSetterActionBean {
+        @Override
+        @ValidateNestedProperties( {
+                @Validate(field = "username", required = true),
+                @Validate(field = "password", required = true)
+        })
+        public void setModel(User user) { super.setModel(user); }
+    }
+
     /**
      * Attempts to trigger validation errors on an ActionBean declared with a 
type parameter.
      * Validation was crippled by a bug in JDK6 and earlier.
@@ -50,15 +100,22 @@
      */
     @Test(groups = "fast")
     public void testActionBeanWithTypeParameter() throws Exception {
+        runValidationTests(OverrideGetterAndSetterActionBean.class);
+        runValidationTests(OverrideGetterActionBean.class);
+        runValidationTests(OverrideSetterActionBean.class);
+        runValidationTests(OverloadSetterActionBean.class);
+        runValidationTests(ExtendOverloadSetterActionBean.class);
+        runValidationTests(ExtendOverloadSetterAgainActionBean.class);
+    }
+
+    protected void runValidationTests(Class<? extends BaseActionBean<User>> 
type) throws Exception {
         // Trigger the validation errors
-        MockRoundtrip trip = new 
MockRoundtrip(StripesTestFixture.getServletContext(),
-                TestActionBean.class);
+        MockRoundtrip trip = new 
MockRoundtrip(StripesTestFixture.getServletContext(), type);
         trip.execute("login");
         ValidationErrors errors = trip.getValidationErrors();
         Assert.assertNotNull(errors, "Expected validation errors but got 
none");
         Assert.assertFalse(errors.isEmpty(), "Expected validation errors but 
got none");
-        Assert.assertEquals(errors.size(), 2, "Expected two validation errors 
but got "
-                + errors.size());
+        Assert.assertEquals(errors.size(), 2, "Expected two validation errors 
but got " + errors.size());
 
         // Now add the required parameters and make sure the validation errors 
don't happen
         trip.addParameter("model.username", "Scooby");
@@ -67,8 +124,9 @@
         errors = trip.getValidationErrors();
         Assert.assertTrue(errors == null || errors.isEmpty(), "Got unexpected 
validation errors");
 
-        TestActionBean bean = trip.getActionBean(TestActionBean.class);
+        BaseActionBean<User> bean = trip.getActionBean(type);
         Assert.assertNotNull(bean);
+        Assert.assertNotNull(bean.getModel());
         Assert.assertEquals(bean.getModel().getUsername(), "Scooby");
         Assert.assertEquals(bean.getModel().getPassword(), "Shaggy");
     }


This was sent by the SourceForge.net collaborative development platform, the 
world's largest Open Source development site.

------------------------------------------------------------------------------
Come build with us! The BlackBerry(R) Developer Conference in SF, CA
is the only developer event you need to attend this year. Jumpstart your
developing skills, take BlackBerry mobile applications to market and stay 
ahead of the curve. Join us from November 9 - 12, 2009. Register now!
http://p.sf.net/sfu/devconference
_______________________________________________
Stripes-development mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/stripes-development

Reply via email to