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