From 4a8be690998f43a9253305e09f96ad83058ef540 Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Sat, 24 Nov 2012 12:22:31 +0100 Subject: [PATCH] Overhaul non-void JavaBean write method support This change revisits the implementation of ExtendedBeanInfo, simplifying the overall approach while also ensuring that ExtendedBeanInfo is fully isolated from the BeanInfo instance it wraps. This includes any existing PropertyDescriptors in the wrapped BeanInfo - along with being copied locally into ExtendedBeanInfo, each property descriptor is now also wrapped with our own new "simple" PropertyDescriptor variants that bypass the soft/weak reference management that goes on in both java.beans.PropertyDescriptor and java.beans.IndexedPropertyDescriptor, maintaining hard references to methods and bean classes instead. This ensures that changes we make to property descriptors, e.g. adding write methods, do not cause subtle conflicts during garbage collection (as was reported and reproduced in SPR-9702). Eliminating soft/weak reference management means that we must take extra care to ensure that we do not cause ClassLoader leaks by maintaining hard references to methods, and therefore transitively to the ClassLoader in which the bean class was loaded. The forthcoming SPR-10028 addresses this aspect. See the updated ExtendedBeanInfo Javadoc for further details. Issue: SPR-8079, SPR-8175, SPR-8347, SPR-8432, SPR-8491, SPR-8522, SPR-8806, SPR-8931, SPR-8937, SPR-8949, SPR-9007, SPR-9059, SPR-9414, SPR-9453, SPR-9542, SPR-9584, SPR-9677, SPR-9702, SPR-9723, SPR-9943, SPR-9978, SPR-10028, SPR-10029 --- .../beans/ExtendedBeanInfo.java | 856 ++++++++++++------ .../beans/ExtendedBeanInfoFactory.java | 10 +- .../beans/ExtendedBeanInfoTests.java | 146 ++- .../beans/SimplePropertyDescriptorTests.java | 150 +++ 4 files changed, 824 insertions(+), 338 deletions(-) create mode 100644 spring-beans/src/test/java/org/springframework/beans/SimplePropertyDescriptorTests.java diff --git a/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.java b/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.java index 1e2b3c398b..c383a52b5b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.java +++ b/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.java @@ -16,9 +16,8 @@ package org.springframework.beans; -import static java.lang.String.format; - import java.awt.Image; + import java.beans.BeanDescriptor; import java.beans.BeanInfo; import java.beans.EventSetDescriptor; @@ -27,310 +26,191 @@ import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.MethodDescriptor; import java.beans.PropertyDescriptor; + import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import java.util.ArrayList; import java.util.Comparator; -import java.util.SortedSet; +import java.util.Enumeration; +import java.util.List; +import java.util.Set; import java.util.TreeSet; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.core.BridgeMethodResolver; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; -import org.springframework.util.ReflectionUtils; -import org.springframework.util.StringUtils; +import static org.springframework.beans.PropertyDescriptorUtils.*; /** - * Decorates a standard {@link BeanInfo} object (likely created created by - * {@link Introspector#getBeanInfo(Class)}) by including non-void returning setter - * methods in the collection of {@link #getPropertyDescriptors() property descriptors}. - * Both regular and + * Decorator for a standard {@link BeanInfo} object, e.g. as created by + * {@link Introspector#getBeanInfo(Class)}, designed to discover and register non-void + * returning setter methods. For example: + *
{@code
+ * public class Bean {
+ *     private Foo foo;
+ *
+ *     public Foo getFoo() {
+ *         return this.foo;
+ *     }
+ *
+ *     public Bean setFoo(Foo foo) {
+ *         this.foo = foo;
+ *         return this;
+ *     }
+ * }
+ * The standard JavaBeans {@code Introspector} will discover the {@code getFoo} read + * method, but will bypass the {@code #setFoo(Foo)} write method, because its non-void + * returning signature does not comply with the JavaBeans specification. + * {@code ExtendedBeanInfo}, on the other hand, will recognize and include it. This is + * designed to allow APIs with "builder" or method-chaining style setter signatures to be + * used within Spring {@code } XML. {@link #getPropertyDescriptors()} returns all + * existing property descriptors from the wrapped {@code BeanInfo} as well any added for + * non-void returning setters. Both standard ("non-indexed") and * * indexed properties are fully supported. * - *

The wrapped {@code BeanInfo} object is not modified in any way. - * * @author Chris Beams * @since 3.1 + * @see #ExtendedBeanInfo(BeanInfo) + * @see ExtendedBeanInfoFactory * @see CachedIntrospectionResults */ class ExtendedBeanInfo implements BeanInfo { - private final Log logger = LogFactory.getLog(getClass()); - private final BeanInfo delegate; - private final SortedSet propertyDescriptors = - new TreeSet(new PropertyDescriptorComparator()); + private final Set propertyDescriptors = + new TreeSet(new PropertyDescriptorComparator()); /** - * Wrap the given delegate {@link BeanInfo} instance and find any non-void returning - * setter methods, creating and adding a {@link PropertyDescriptor} for each. - * - *

Note that the wrapped {@code BeanInfo} is modified by this process. - * + * Wrap the given {@link BeanInfo} instance; copy all its existing property descriptors + * locally, wrapping each in a custom {@link SimpleIndexedPropertyDescriptor indexed} or + * {@link SimpleNonIndexedPropertyDescriptor non-indexed} {@code PropertyDescriptor} + * variant that bypasses default JDK weak/soft reference management; then search + * through its method descriptors to find any non-void returning write methods and + * update or create the corresponding {@link PropertyDescriptor} for each one found. + * @param delegate the wrapped {@code BeanInfo}, which is never modified + * @throws IntrospectionException if any problems occur creating and adding new + * property descriptors * @see #getPropertyDescriptors() - * @throws IntrospectionException if any problems occur creating and adding new {@code PropertyDescriptors} */ public ExtendedBeanInfo(BeanInfo delegate) throws IntrospectionException { this.delegate = delegate; - ALL_METHODS: - for (MethodDescriptor md : delegate.getMethodDescriptors()) { - Method method = resolveMethod(md.getMethod()); + for (PropertyDescriptor pd : delegate.getPropertyDescriptors()) { + this.propertyDescriptors.add(pd instanceof IndexedPropertyDescriptor ? + new SimpleIndexedPropertyDescriptor((IndexedPropertyDescriptor) pd) : + new SimpleNonIndexedPropertyDescriptor(pd)); + } - // bypass non-getter java.lang.Class methods for efficiency - if (ReflectionUtils.isObjectMethod(method) && !method.getName().startsWith("get")) { - continue ALL_METHODS; - } - - // is the method a NON-INDEXED setter? ignore return type in order to capture non-void signatures - if (method.getName().startsWith("set") && method.getParameterTypes().length == 1) { - String propertyName = propertyNameFor(method); - if(propertyName.length() == 0) { - continue ALL_METHODS; - } - for (PropertyDescriptor pd : delegate.getPropertyDescriptors()) { - Method readMethod = readMethodFor(pd); - Method writeMethod = writeMethodFor(pd); - // has the setter already been found by the wrapped BeanInfo? - if (writeMethod != null - && writeMethod.getName().equals(method.getName())) { - // yes -> copy it, including corresponding getter method (if any -- may be null) - this.addOrUpdatePropertyDescriptor(pd, propertyName, readMethod, writeMethod); - continue ALL_METHODS; - } - // has a getter corresponding to this setter already been found by the wrapped BeanInfo? - if (readMethod != null - && readMethod.getName().equals(getterMethodNameFor(propertyName)) - && readMethod.getReturnType().equals(method.getParameterTypes()[0])) { - this.addOrUpdatePropertyDescriptor(pd, propertyName, readMethod, method); - continue ALL_METHODS; - } - } - // the setter method was not found by the wrapped BeanInfo -> add a new PropertyDescriptor for it - // no corresponding getter was detected, so the 'read method' parameter is null. - this.addOrUpdatePropertyDescriptor(null, propertyName, null, method); - continue ALL_METHODS; - } - - // is the method an INDEXED setter? ignore return type in order to capture non-void signatures - if (method.getName().startsWith("set") && method.getParameterTypes().length == 2 && method.getParameterTypes()[0].equals(int.class)) { - String propertyName = propertyNameFor(method); - if(propertyName.length() == 0) { - continue ALL_METHODS; - } - DELEGATE_PD: - for (PropertyDescriptor pd : delegate.getPropertyDescriptors()) { - if (!(pd instanceof IndexedPropertyDescriptor)) { - continue DELEGATE_PD; - } - IndexedPropertyDescriptor ipd = (IndexedPropertyDescriptor) pd; - Method readMethod = readMethodFor(ipd); - Method writeMethod = writeMethodFor(ipd); - Method indexedReadMethod = indexedReadMethodFor(ipd); - Method indexedWriteMethod = indexedWriteMethodFor(ipd); - // has the setter already been found by the wrapped BeanInfo? - if (!(indexedWriteMethod != null - && indexedWriteMethod.getName().equals(method.getName()))) { - indexedWriteMethod = method; - } - // yes -> copy it, including corresponding getter method (if any -- may be null) - this.addOrUpdatePropertyDescriptor(pd, propertyName, readMethod, writeMethod, indexedReadMethod, indexedWriteMethod); - continue ALL_METHODS; - } - // the INDEXED setter method was not found by the wrapped BeanInfo -> add a new PropertyDescriptor - // for it. no corresponding INDEXED getter was detected, so the 'indexed read method' parameter is null. - this.addOrUpdatePropertyDescriptor(null, propertyName, null, null, null, method); - continue ALL_METHODS; - } - - // the method is not a setter, but is it a getter? - for (PropertyDescriptor pd : delegate.getPropertyDescriptors()) { - // have we already copied this read method to a property descriptor locally? - String propertyName = pd.getName(); - Method readMethod = readMethodFor(pd); - Method mostSpecificReadMethod = ClassUtils.getMostSpecificMethod(readMethod, method.getDeclaringClass()); - for (PropertyDescriptor existingPD : this.propertyDescriptors) { - if (method.equals(mostSpecificReadMethod) - && existingPD.getName().equals(propertyName)) { - if (readMethodFor(existingPD) == null) { - // no -> add it now - this.addOrUpdatePropertyDescriptor(pd, propertyName, method, writeMethodFor(pd)); - } - // yes -> do not add a duplicate - continue ALL_METHODS; - } - } - if (method.equals(mostSpecificReadMethod) - || (pd instanceof IndexedPropertyDescriptor && method.equals(indexedReadMethodFor((IndexedPropertyDescriptor) pd)))) { - // yes -> copy it, including corresponding setter method (if any -- may be null) - if (pd instanceof IndexedPropertyDescriptor) { - this.addOrUpdatePropertyDescriptor(pd, propertyName, readMethod, writeMethodFor(pd), indexedReadMethodFor((IndexedPropertyDescriptor)pd), indexedWriteMethodFor((IndexedPropertyDescriptor)pd)); - } else { - this.addOrUpdatePropertyDescriptor(pd, propertyName, readMethod, writeMethodFor(pd)); - } - continue ALL_METHODS; - } - } + for (Method method : findNonVoidWriteMethods(delegate.getMethodDescriptors())) { + handleNonVoidWriteMethod(method); } } - private static Method resolveMethod(Method method) { - return BridgeMethodResolver.findBridgedMethod(method); - } - - private static Method readMethodFor(PropertyDescriptor pd) { - return resolveMethod(pd.getReadMethod()); - } - - private static Method writeMethodFor(PropertyDescriptor pd) { - return resolveMethod(pd.getWriteMethod()); - } - - private static Method indexedReadMethodFor(IndexedPropertyDescriptor ipd) { - return resolveMethod(ipd.getIndexedReadMethod()); - } - - private static Method indexedWriteMethodFor(IndexedPropertyDescriptor ipd) { - return resolveMethod(ipd.getIndexedWriteMethod()); - } - - private void addOrUpdatePropertyDescriptor(PropertyDescriptor pd, String propertyName, Method readMethod, Method writeMethod) throws IntrospectionException { - addOrUpdatePropertyDescriptor(pd, propertyName, readMethod, writeMethod, null, null); - } - - private void addOrUpdatePropertyDescriptor(PropertyDescriptor pd, String propertyName, Method readMethod, Method writeMethod, Method indexedReadMethod, Method indexedWriteMethod) throws IntrospectionException { - Assert.notNull(propertyName, "propertyName may not be null"); - propertyName = pd == null ? propertyName : pd.getName(); - for (PropertyDescriptor existingPD : this.propertyDescriptors) { - if (existingPD.getName().equals(propertyName)) { - // is there already a descriptor that captures this read method or its corresponding write method? - if (readMethodFor(existingPD) != null) { - if (readMethod != null && readMethodFor(existingPD).getReturnType() != readMethod.getReturnType() - || writeMethod != null && readMethodFor(existingPD).getReturnType() != writeMethod.getParameterTypes()[0]) { - // no -> add a new descriptor for it below - break; - } - } - // update the existing descriptor's read method - if (readMethod != null) { - try { - existingPD.setReadMethod(readMethod); - } catch (IntrospectionException ex) { - // there is a conflicting setter method present -> null it out and try again - existingPD.setWriteMethod(null); - existingPD.setReadMethod(readMethod); - } - } - - // is there already a descriptor that captures this write method or its corresponding read method? - if (writeMethodFor(existingPD) != null) { - if (readMethod != null && writeMethodFor(existingPD).getParameterTypes()[0] != readMethod.getReturnType() - || writeMethod != null && writeMethodFor(existingPD).getParameterTypes()[0] != writeMethod.getParameterTypes()[0]) { - // no -> add a new descriptor for it below - break; - } - } - // update the existing descriptor's write method - if (writeMethod != null - && !(existingPD instanceof IndexedPropertyDescriptor && - !writeMethod.getParameterTypes()[0].isArray())) { - existingPD.setWriteMethod(writeMethod); - } - - // is this descriptor indexed? - if (existingPD instanceof IndexedPropertyDescriptor) { - IndexedPropertyDescriptor existingIPD = (IndexedPropertyDescriptor) existingPD; - - // is there already a descriptor that captures this indexed read method or its corresponding indexed write method? - if (indexedReadMethodFor(existingIPD) != null) { - if (indexedReadMethod != null && indexedReadMethodFor(existingIPD).getReturnType() != indexedReadMethod.getReturnType() - || indexedWriteMethod != null && indexedReadMethodFor(existingIPD).getReturnType() != indexedWriteMethod.getParameterTypes()[1]) { - // no -> add a new descriptor for it below - break; - } - } - // update the existing descriptor's indexed read method - try { - if (indexedReadMethod != null) { - existingIPD.setIndexedReadMethod(indexedReadMethod); - } - } catch (IntrospectionException ex) { - // there is a conflicting indexed setter method present -> null it out and try again - existingIPD.setIndexedWriteMethod(null); - existingIPD.setIndexedReadMethod(indexedReadMethod); - } - - // is there already a descriptor that captures this indexed write method or its corresponding indexed read method? - if (indexedWriteMethodFor(existingIPD) != null) { - if (indexedReadMethod != null && indexedWriteMethodFor(existingIPD).getParameterTypes()[1] != indexedReadMethod.getReturnType() - || indexedWriteMethod != null && indexedWriteMethodFor(existingIPD).getParameterTypes()[1] != indexedWriteMethod.getParameterTypes()[1]) { - // no -> add a new descriptor for it below - break; - } - } - // update the existing descriptor's indexed write method - if (indexedWriteMethod != null) { - existingIPD.setIndexedWriteMethod(indexedWriteMethod); - } - } - - // the descriptor has been updated -> return immediately - return; + private List findNonVoidWriteMethods(MethodDescriptor[] methodDescriptors) { + List matches = new ArrayList(); + for (MethodDescriptor methodDescriptor : methodDescriptors) { + Method method = methodDescriptor.getMethod(); + if (isNonVoidWriteMethod(method)) { + matches.add(method); } } + return matches; + } - // we haven't yet seen read or write methods for this property -> add a new descriptor - if (pd == null) { - try { - if (indexedReadMethod == null && indexedWriteMethod == null) { - pd = new PropertyDescriptor(propertyName, readMethod, writeMethod); - } - else { - pd = new IndexedPropertyDescriptor(propertyName, readMethod, writeMethod, indexedReadMethod, indexedWriteMethod); - } - this.propertyDescriptors.add(pd); - } catch (IntrospectionException ex) { - logger.debug(format("Could not create new PropertyDescriptor for readMethod [%s] writeMethod [%s] " + - "indexedReadMethod [%s] indexedWriteMethod [%s] for property [%s]. Reason: %s", - readMethod, writeMethod, indexedReadMethod, indexedWriteMethod, propertyName, ex.getMessage())); - // suppress exception and attempt to continue + public static boolean isNonVoidWriteMethod(Method method) { + String methodName = method.getName(); + Class[] parameterTypes = method.getParameterTypes(); + int nParams = parameterTypes.length; + if (methodName.length() > 3 && methodName.startsWith("set") && + Modifier.isPublic(method.getModifiers()) && + !void.class.isAssignableFrom(method.getReturnType()) && + (nParams == 1 || (nParams == 2 && parameterTypes[0].equals(int.class)))) { + return true; + } + return false; + } + + private void handleNonVoidWriteMethod(Method method) throws IntrospectionException { + int nParams = method.getParameterTypes().length; + String propertyName = propertyNameFor(method); + Class propertyType = method.getParameterTypes()[nParams-1]; + PropertyDescriptor existingPD = findExistingPropertyDescriptor(propertyName, propertyType); + if (nParams == 1) { + if (existingPD == null) { + this.propertyDescriptors.add( + new SimpleNonIndexedPropertyDescriptor(propertyName, null, method)); + } + else { + existingPD.setWriteMethod(method); + } + } + else if (nParams == 2) { + if (existingPD == null) { + this.propertyDescriptors.add( + new SimpleIndexedPropertyDescriptor( + propertyName, null, null, null, method)); + } + else if (existingPD instanceof IndexedPropertyDescriptor) { + ((IndexedPropertyDescriptor)existingPD).setIndexedWriteMethod(method); + } + else { + this.propertyDescriptors.remove(existingPD); + this.propertyDescriptors.add( + new SimpleIndexedPropertyDescriptor( + propertyName, existingPD.getReadMethod(), + existingPD.getWriteMethod(), null, method)); } } else { - pd.setWriteMethod(null); - pd.setReadMethod(readMethod); - try { - pd.setWriteMethod(writeMethod); - } catch (IntrospectionException ex) { - logger.debug(format("Could not add write method [%s] for property [%s]. Reason: %s", - writeMethod, propertyName, ex.getMessage())); - // fall through -> add property descriptor as best we can - } - if (pd instanceof IndexedPropertyDescriptor) { - ((IndexedPropertyDescriptor)pd).setIndexedWriteMethod(null); - ((IndexedPropertyDescriptor)pd).setIndexedReadMethod(indexedReadMethod); - try { - ((IndexedPropertyDescriptor)pd).setIndexedWriteMethod(indexedWriteMethod); - } catch (IntrospectionException ex) { - logger.debug(format("Could not add indexed write method [%s] for property [%s]. Reason: %s", - indexedWriteMethod, propertyName, ex.getMessage())); - // fall through -> add property descriptor as best we can - } - } - this.propertyDescriptors.add(pd); + throw new IllegalArgumentException( + "write method must have exactly 1 or 2 parameters: " + method); } } - private String propertyNameFor(Method method) { - return Introspector.decapitalize(method.getName().substring(3,method.getName().length())); + private PropertyDescriptor findExistingPropertyDescriptor( + String propertyName, Class propertyType) { + + for (PropertyDescriptor pd : this.propertyDescriptors) { + final Class candidateType; + final String candidateName = pd.getName(); + if (pd instanceof IndexedPropertyDescriptor) { + IndexedPropertyDescriptor ipd = (IndexedPropertyDescriptor) pd; + candidateType = ipd.getIndexedPropertyType(); + if (candidateName.equals(propertyName) && + (candidateType.equals(propertyType) || + candidateType.equals(propertyType.getComponentType()))) { + return pd; + } + } + else { + candidateType = pd.getPropertyType(); + if (candidateName.equals(propertyName) && + (candidateType.equals(propertyType) || + propertyType.equals(candidateType.getComponentType()))) { + return pd; + } + } + } + return null; } - private Object getterMethodNameFor(String name) { - return "get" + StringUtils.capitalize(name); + private String propertyNameFor(Method method) { + return Introspector.decapitalize( + method.getName().substring(3, method.getName().length())); + } + + + /** + * Return the set of {@link PropertyDescriptor}s from the wrapped {@link BeanInfo} + * object as well as {@code PropertyDescriptor}s for each non-void returning setter + * method found during construction. + * @see #ExtendedBeanInfo(BeanInfo) + */ + public PropertyDescriptor[] getPropertyDescriptors() { + return this.propertyDescriptors.toArray( + new PropertyDescriptor[this.propertyDescriptors.size()]); } public BeanInfo[] getAdditionalBeanInfo() { @@ -353,45 +233,431 @@ class ExtendedBeanInfo implements BeanInfo { return delegate.getEventSetDescriptors(); } - public Image getIcon(int arg0) { - return delegate.getIcon(arg0); + public Image getIcon(int iconKind) { + return delegate.getIcon(iconKind); } public MethodDescriptor[] getMethodDescriptors() { return delegate.getMethodDescriptors(); } +} - /** - * Return the set of {@link PropertyDescriptor}s from the wrapped {@link BeanInfo} - * object as well as {@code PropertyDescriptor}s for each non-void returning setter - * method found during construction. - * @see #ExtendedBeanInfo(BeanInfo) - */ - public PropertyDescriptor[] getPropertyDescriptors() { - return this.propertyDescriptors.toArray(new PropertyDescriptor[this.propertyDescriptors.size()]); + +class SimpleNonIndexedPropertyDescriptor extends PropertyDescriptor { + + private Method readMethod; + private Method writeMethod; + private Class propertyType; + private Class propertyEditorClass; + + + public SimpleNonIndexedPropertyDescriptor(PropertyDescriptor original) + throws IntrospectionException { + + this(original.getName(), original.getReadMethod(), original.getWriteMethod()); + copyNonMethodProperties(original, this); + } + + public SimpleNonIndexedPropertyDescriptor(String propertyName, + Method readMethod, Method writeMethod) throws IntrospectionException { + + super(propertyName, readMethod, writeMethod); + this.setReadMethod(readMethod); + this.setWriteMethod(writeMethod); + this.propertyType = findPropertyType(readMethod, writeMethod); } - /** - * Sorts PropertyDescriptor instances alpha-numerically to emulate the behavior of - * {@link java.beans.BeanInfo#getPropertyDescriptors()}. - * - * @see ExtendedBeanInfo#propertyDescriptors - */ - static class PropertyDescriptorComparator implements Comparator { - public int compare(PropertyDescriptor desc1, PropertyDescriptor desc2) { - String left = desc1.getName(); - String right = desc2.getName(); - for (int i = 0; i < left.length(); i++) { - if (right.length() == i) { - return 1; - } - int result = left.getBytes()[i] - right.getBytes()[i]; - if (result != 0) { - return result; - } + @Override + public Method getReadMethod() { + return this.readMethod; + } + + @Override + public void setReadMethod(Method readMethod) { + this.readMethod = readMethod; + } + + @Override + public Method getWriteMethod() { + return this.writeMethod; + } + + @Override + public void setWriteMethod(Method writeMethod) { + this.writeMethod = writeMethod; + } + + @Override + public Class getPropertyType() { + if (this.propertyType == null) { + try { + this.propertyType = findPropertyType(this.readMethod, this.writeMethod); + } catch (IntrospectionException ex) { + // ignore, as does PropertyDescriptor#getPropertyType } - return left.length() - right.length(); } + return this.propertyType; } -} \ No newline at end of file + + @Override + public Class getPropertyEditorClass() { + return this.propertyEditorClass; + } + + @Override + public void setPropertyEditorClass(Class propertyEditorClass) { + this.propertyEditorClass = propertyEditorClass; + } + + + @Override + public boolean equals(Object obj) { + return PropertyDescriptorUtils.equals(this, obj); + } + + @Override + public String toString() { + return String.format("%s[name=%s, propertyType=%s, readMethod=%s, writeMethod=%s]", + this.getClass().getSimpleName(), this.getName(), this.getPropertyType(), + this.readMethod, this.writeMethod); + } +} + + +class SimpleIndexedPropertyDescriptor extends IndexedPropertyDescriptor { + + private Method readMethod; + private Method writeMethod; + private Class propertyType; + private Class propertyEditorClass; + + private Method indexedReadMethod; + private Method indexedWriteMethod; + private Class indexedPropertyType; + + + public SimpleIndexedPropertyDescriptor(IndexedPropertyDescriptor original) + throws IntrospectionException { + + this(original.getName(), original.getReadMethod(), original.getWriteMethod(), + original.getIndexedReadMethod(), original.getIndexedWriteMethod()); + copyNonMethodProperties(original, this); + } + + public SimpleIndexedPropertyDescriptor(String propertyName, + Method readMethod, Method writeMethod, + Method indexedReadMethod, Method indexedWriteMethod) + throws IntrospectionException { + + super(propertyName, readMethod, writeMethod, indexedReadMethod, indexedWriteMethod); + this.setReadMethod(readMethod); + this.setWriteMethod(writeMethod); + this.propertyType = findPropertyType(readMethod, writeMethod); + + this.setIndexedReadMethod(indexedReadMethod); + this.setIndexedWriteMethod(indexedWriteMethod); + this.indexedPropertyType = findIndexedPropertyType( + this.getName(), this.propertyType, indexedReadMethod, indexedWriteMethod); + } + + + @Override + public Method getReadMethod() { + return this.readMethod; + } + + @Override + public void setReadMethod(Method readMethod) { + this.readMethod = readMethod; + } + + @Override + public Method getWriteMethod() { + return this.writeMethod; + } + + @Override + public void setWriteMethod(Method writeMethod) { + this.writeMethod = writeMethod; + } + + @Override + public Class getPropertyType() { + if (this.propertyType == null) { + try { + this.propertyType = findPropertyType(this.readMethod, this.writeMethod); + } catch (IntrospectionException ex) { + // ignore, as does IndexedPropertyDescriptor#getPropertyType + } + } + return this.propertyType; + } + + @Override + public Method getIndexedReadMethod() { + return this.indexedReadMethod; + } + + @Override + public void setIndexedReadMethod(Method indexedReadMethod) throws IntrospectionException { + this.indexedReadMethod = indexedReadMethod; + } + + @Override + public Method getIndexedWriteMethod() { + return this.indexedWriteMethod; + } + + @Override + public void setIndexedWriteMethod(Method indexedWriteMethod) throws IntrospectionException { + this.indexedWriteMethod = indexedWriteMethod; + } + + @Override + public Class getIndexedPropertyType() { + if (this.indexedPropertyType == null) { + try { + this.indexedPropertyType = findIndexedPropertyType( + this.getName(), this.getPropertyType(), + this.indexedReadMethod, this.indexedWriteMethod); + } catch (IntrospectionException ex) { + // ignore, as does IndexedPropertyDescriptor#getIndexedPropertyType + } + } + return this.indexedPropertyType; + } + + @Override + public Class getPropertyEditorClass() { + return this.propertyEditorClass; + } + + @Override + public void setPropertyEditorClass(Class propertyEditorClass) { + this.propertyEditorClass = propertyEditorClass; + } + + + /* + * @see java.beans.IndexedPropertyDescriptor#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj != null && obj instanceof IndexedPropertyDescriptor) { + IndexedPropertyDescriptor other = (IndexedPropertyDescriptor) obj; + if (!compareMethods(getIndexedReadMethod(), other.getIndexedReadMethod())) { + return false; + } + + if (!compareMethods(getIndexedWriteMethod(), other.getIndexedWriteMethod())) { + return false; + } + + if (getIndexedPropertyType() != other.getIndexedPropertyType()) { + return false; + } + return PropertyDescriptorUtils.equals(this, obj); + } + return false; + } + + @Override + public String toString() { + return String.format("%s[name=%s, propertyType=%s, indexedPropertyType=%s, " + + "readMethod=%s, writeMethod=%s, indexedReadMethod=%s, indexedWriteMethod=%s]", + this.getClass().getSimpleName(), this.getName(), this.getPropertyType(), + this.getIndexedPropertyType(), this.readMethod, this.writeMethod, + this.indexedReadMethod, this.indexedWriteMethod); + } +} + + +class PropertyDescriptorUtils { + + /* + * see java.beans.FeatureDescriptor#FeatureDescriptor(FeatureDescriptor) + */ + public static void copyNonMethodProperties(PropertyDescriptor source, PropertyDescriptor target) + throws IntrospectionException { + + target.setExpert(source.isExpert()); + target.setHidden(source.isHidden()); + target.setPreferred(source.isPreferred()); + target.setName(source.getName()); + target.setShortDescription(source.getShortDescription()); + target.setDisplayName(source.getDisplayName()); + + // copy all attributes (emulating behavior of private FeatureDescriptor#addTable) + Enumeration keys = source.attributeNames(); + while (keys.hasMoreElements()) { + String key = (String)keys.nextElement(); + target.setValue(key, source.getValue(key)); + } + + // see java.beans.PropertyDescriptor#PropertyDescriptor(PropertyDescriptor) + target.setPropertyEditorClass(source.getPropertyEditorClass()); + target.setBound(source.isBound()); + target.setConstrained(source.isConstrained()); + } + + /* + * See PropertyDescriptor#findPropertyType + */ + public static Class findPropertyType(Method readMethod, Method writeMethod) + throws IntrospectionException { + + Class propertyType = null; + + if (readMethod != null) { + Class[] params = readMethod.getParameterTypes(); + if (params.length != 0) { + throw new IntrospectionException("bad read method arg count: " + readMethod); + } + propertyType = readMethod.getReturnType(); + if (propertyType == Void.TYPE) { + throw new IntrospectionException("read method " + + readMethod.getName() + " returns void"); + } + } + if (writeMethod != null) { + Class params[] = writeMethod.getParameterTypes(); + if (params.length != 1) { + throw new IntrospectionException("bad write method arg count: " + writeMethod); + } + if (propertyType != null + && !params[0].isAssignableFrom(propertyType)) { + throw new IntrospectionException("type mismatch between read and write methods"); + } + propertyType = params[0]; + } + return propertyType; + } + + /* + * See IndexedPropertyDescriptor#findIndexedPropertyType + */ + public static Class findIndexedPropertyType(String name, Class propertyType, + Method indexedReadMethod, Method indexedWriteMethod) + throws IntrospectionException { + + Class indexedPropertyType = null; + + if (indexedReadMethod != null) { + Class params[] = indexedReadMethod.getParameterTypes(); + if (params.length != 1) { + throw new IntrospectionException( + "bad indexed read method arg count"); + } + if (params[0] != Integer.TYPE) { + throw new IntrospectionException( + "non int index to indexed read method"); + } + indexedPropertyType = indexedReadMethod.getReturnType(); + if (indexedPropertyType == Void.TYPE) { + throw new IntrospectionException( + "indexed read method returns void"); + } + } + if (indexedWriteMethod != null) { + Class params[] = indexedWriteMethod.getParameterTypes(); + if (params.length != 2) { + throw new IntrospectionException( + "bad indexed write method arg count"); + } + if (params[0] != Integer.TYPE) { + throw new IntrospectionException( + "non int index to indexed write method"); + } + if (indexedPropertyType != null && indexedPropertyType != params[1]) { + throw new IntrospectionException( + "type mismatch between indexed read and indexed write methods: " + name); + } + indexedPropertyType = params[1]; + } + if (propertyType != null + && (!propertyType.isArray() || + propertyType.getComponentType() != indexedPropertyType)) { + throw new IntrospectionException( + "type mismatch between indexed and non-indexed methods: " + name); + } + return indexedPropertyType; + } + + /** + * Compare the given {@link PropertyDescriptor} against the given {@link Object} and + * return {@code true} if they are objects are equivalent, i.e. both are {@code + * PropertyDescriptor}s whose read method, write method, property types, property + * editor and flags are equivalent. + * + * @see PropertyDescriptor#equals(Object) + */ + public static boolean equals(PropertyDescriptor pd1, Object obj) { + if (pd1 == obj) { + return true; + } + if (obj != null && obj instanceof PropertyDescriptor) { + PropertyDescriptor pd2 = (PropertyDescriptor) obj; + if (!compareMethods(pd1.getReadMethod(), pd2.getReadMethod())) { + return false; + } + + if (!compareMethods(pd1.getWriteMethod(), pd2.getWriteMethod())) { + return false; + } + + if (pd1.getPropertyType() == pd2.getPropertyType() + && pd1.getPropertyEditorClass() == pd2.getPropertyEditorClass() + && pd1.isBound() == pd2.isBound() + && pd1.isConstrained() == pd2.isConstrained()) { + return true; + } + } + return false; + } + + /* + * see PropertyDescriptor#compareMethods + */ + public static boolean compareMethods(Method a, Method b) { + if ((a == null) != (b == null)) { + return false; + } + + if (a != null && b != null) { + if (!a.equals(b)) { + return false; + } + } + return true; + } +} + + +/** + * Sorts PropertyDescriptor instances alpha-numerically to emulate the behavior of + * {@link java.beans.BeanInfo#getPropertyDescriptors()}. + * + * @see ExtendedBeanInfo#propertyDescriptors + */ +class PropertyDescriptorComparator implements Comparator { + + public int compare(PropertyDescriptor desc1, PropertyDescriptor desc2) { + String left = desc1.getName(); + String right = desc2.getName(); + for (int i = 0; i < left.length(); i++) { + if (right.length() == i) { + return 1; + } + int result = left.getBytes()[i] - right.getBytes()[i]; + if (result != 0) { + return result; + } + } + return left.length() - right.length(); + } +} diff --git a/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfoFactory.java b/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfoFactory.java index 2eafdea317..7e46211922 100644 --- a/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfoFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfoFactory.java @@ -20,7 +20,6 @@ import java.beans.BeanInfo; import java.beans.IntrospectionException; import java.beans.Introspector; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; import org.springframework.core.Ordered; @@ -52,14 +51,7 @@ public class ExtendedBeanInfoFactory implements Ordered, BeanInfoFactory { */ private boolean supports(Class beanClass) { for (Method method : beanClass.getMethods()) { - String methodName = method.getName(); - Class[] parameterTypes = method.getParameterTypes(); - if (Modifier.isPublic(method.getModifiers()) - && methodName.length() > 3 - && methodName.startsWith("set") - && (parameterTypes.length == 1 - || (parameterTypes.length == 2 && parameterTypes[0].equals(int.class))) - && !void.class.isAssignableFrom(method.getReturnType())) { + if (ExtendedBeanInfo.isNonVoidWriteMethod(method)) { return true; } } diff --git a/spring-beans/src/test/java/org/springframework/beans/ExtendedBeanInfoTests.java b/spring-beans/src/test/java/org/springframework/beans/ExtendedBeanInfoTests.java index 5187ae2d9e..5e05fc91db 100644 --- a/spring-beans/src/test/java/org/springframework/beans/ExtendedBeanInfoTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/ExtendedBeanInfoTests.java @@ -24,22 +24,22 @@ import java.beans.PropertyDescriptor; import java.lang.reflect.Method; -import org.junit.Ignore; import org.junit.Test; -import org.springframework.beans.ExtendedBeanInfo.PropertyDescriptorComparator; import org.springframework.core.JdkVersion; import org.springframework.util.ClassUtils; import test.beans.TestBean; -import static org.junit.Assert.*; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.instanceOf; -import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.lessThan; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; + +import static org.junit.Assert.*; + /** * Unit tests for {@link ExtendedBeanInfo}. * @@ -128,12 +128,32 @@ public class ExtendedBeanInfoTests { ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi); assertThat(hasReadMethodForProperty(bi, "foo"), is(true)); - assertThat(hasWriteMethodForProperty(bi, "foo"), is(true)); + assertThat(hasWriteMethodForProperty(bi, "foo"), is(false)); assertThat(hasReadMethodForProperty(ebi, "foo"), is(true)); assertThat(hasWriteMethodForProperty(ebi, "foo"), is(true)); } + @Test + public void standardReadAndNonStandardIndexedWriteMethod() throws IntrospectionException { + @SuppressWarnings("unused") class C { + public String[] getFoo() { return null; } + public C setFoo(int i, String foo) { return this; } + } + + BeanInfo bi = Introspector.getBeanInfo(C.class); + + assertThat(hasReadMethodForProperty(bi, "foo"), is(true)); + assertThat(hasWriteMethodForProperty(bi, "foo"), is(false)); + assertThat(hasIndexedWriteMethodForProperty(bi, "foo"), is(trueUntilJdk17())); + + BeanInfo ebi = new ExtendedBeanInfo(bi); + + assertThat(hasReadMethodForProperty(ebi, "foo"), is(true)); + assertThat(hasWriteMethodForProperty(ebi, "foo"), is(false)); + assertThat(hasIndexedWriteMethodForProperty(ebi, "foo"), is(true)); + } + @Test public void standardReadMethodsAndOverloadedNonStandardWriteMethods() throws Exception { @SuppressWarnings("unused") class C { @@ -150,7 +170,7 @@ public class ExtendedBeanInfoTests { ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi); assertThat(hasReadMethodForProperty(bi, "foo"), is(true)); - assertThat(hasWriteMethodForProperty(bi, "foo"), is(trueUntilJdk17())); + assertThat(hasWriteMethodForProperty(bi, "foo"), is(false)); assertThat(hasReadMethodForProperty(ebi, "foo"), is(true)); assertThat(hasWriteMethodForProperty(ebi, "foo"), is(true)); @@ -225,7 +245,7 @@ public class ExtendedBeanInfoTests { ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi); assertThat(hasReadMethodForProperty(bi, "foo"), is(true)); - assertThat(hasWriteMethodForProperty(bi, "foo"), is(true)); + assertThat(hasWriteMethodForProperty(bi, "foo"), is(false)); assertThat(hasReadMethodForProperty(ebi, "foo"), is(true)); assertThat(hasWriteMethodForProperty(ebi, "foo"), is(true)); @@ -268,13 +288,13 @@ public class ExtendedBeanInfoTests { assertThat(hasReadMethodForProperty(bi, "bar"), is(true)); assertThat(hasWriteMethodForProperty(bi, "bar"), is(false)); - ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi); + BeanInfo ebi = new ExtendedBeanInfo(bi); assertThat(hasReadMethodForProperty(bi, "foo"), is(true)); - assertThat(hasWriteMethodForProperty(bi, "foo"), is(true)); + assertThat(hasWriteMethodForProperty(bi, "foo"), is(false)); assertThat(hasReadMethodForProperty(bi, "bar"), is(true)); - assertThat(hasWriteMethodForProperty(bi, "bar"), is(true)); + assertThat(hasWriteMethodForProperty(bi, "bar"), is(false)); assertThat(hasReadMethodForProperty(ebi, "foo"), is(true)); assertThat(hasWriteMethodForProperty(ebi, "foo"), is(true)); @@ -291,7 +311,7 @@ public class ExtendedBeanInfoTests { } BeanInfo bi = Introspector.getBeanInfo(C.class); - ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi); + BeanInfo ebi = new ExtendedBeanInfo(bi); assertThat(hasReadMethodForProperty(bi, "foo"), is(false)); assertThat(hasWriteMethodForProperty(bi, "foo"), is(false)); @@ -312,7 +332,7 @@ public class ExtendedBeanInfoTests { } BeanInfo bi = Introspector.getBeanInfo(C.class); - ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi); + BeanInfo ebi = new ExtendedBeanInfo(bi); assertThat(hasReadMethodForProperty(bi, "foo"), is(true)); assertThat(hasWriteMethodForProperty(bi, "foo"), is(false)); @@ -329,7 +349,7 @@ public class ExtendedBeanInfoTests { } BeanInfo bi = Introspector.getBeanInfo(C.class); - ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi); + BeanInfo ebi = new ExtendedBeanInfo(bi); assertThat(hasIndexedReadMethodForProperty(bi, "foos"), is(true)); assertThat(hasIndexedWriteMethodForProperty(bi, "foos"), is(false)); @@ -350,7 +370,7 @@ public class ExtendedBeanInfoTests { } BeanInfo bi = Introspector.getBeanInfo(C.class); - ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi); + BeanInfo ebi = new ExtendedBeanInfo(bi); assertThat(hasReadMethodForProperty(bi, "foo"), is(true)); assertThat(hasWriteMethodForProperty(bi, "foo"), is(false)); @@ -367,7 +387,7 @@ public class ExtendedBeanInfoTests { } BeanInfo bi = Introspector.getBeanInfo(C.class); - ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi); + BeanInfo ebi = new ExtendedBeanInfo(bi); assertThat(hasIndexedReadMethodForProperty(bi, "foos"), is(true)); assertThat(hasIndexedWriteMethodForProperty(bi, "foos"), is(false)); @@ -508,15 +528,14 @@ public class ExtendedBeanInfoTests { BeanInfo ebi = new ExtendedBeanInfo(Introspector.getBeanInfo(C.class)); assertThat(hasIndexedReadMethodForProperty(bi, "foos"), is(true)); - assertThat(hasWriteMethodForProperty(bi, "foos"), is(true)); - assertThat(hasIndexedWriteMethodForProperty(bi, "foos"), is(true)); + assertThat(hasWriteMethodForProperty(bi, "foos"), is(false)); + assertThat(hasIndexedWriteMethodForProperty(bi, "foos"), is(trueUntilJdk17())); assertThat(hasIndexedReadMethodForProperty(ebi, "foos"), is(true)); assertThat(hasWriteMethodForProperty(ebi, "foos"), is(true)); assertThat(hasIndexedWriteMethodForProperty(ebi, "foos"), is(true)); } - @Ignore // see comments at SPR-9702 @Test public void cornerSpr9702() throws IntrospectionException { { // baseline with standard write method @@ -579,10 +598,10 @@ public class ExtendedBeanInfoTests { assertThat(hasReadMethodForProperty(bi, "foo"), is(true)); assertThat(hasWriteMethodForProperty(bi, "foo"), is(false)); - ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi); + BeanInfo ebi = new ExtendedBeanInfo(bi); assertThat(hasReadMethodForProperty(bi, "foo"), is(true)); - assertThat(hasWriteMethodForProperty(bi, "foo"), is(true)); + assertThat(hasWriteMethodForProperty(bi, "foo"), is(false)); assertThat(hasReadMethodForProperty(ebi, "foo"), is(true)); assertThat(hasWriteMethodForProperty(ebi, "foo"), is(true)); @@ -598,7 +617,7 @@ public class ExtendedBeanInfoTests { } BeanInfo bi = Introspector.getBeanInfo(C.class); - ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi); + BeanInfo ebi = new ExtendedBeanInfo(bi); assertThat(hasReadMethodForProperty(bi, "foo"), is(false)); assertThat(hasWriteMethodForProperty(bi, "foo"), is(true)); @@ -619,11 +638,67 @@ public class ExtendedBeanInfoTests { } BeanInfo bi = Introspector.getBeanInfo(C.class); - ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi); + BeanInfo ebi = new ExtendedBeanInfo(bi); assertThat(ebi.getPropertyDescriptors(), equalTo(bi.getPropertyDescriptors())); } + @Test + public void overloadedNonStandardWriteMethodsOnly_orderA() throws IntrospectionException, SecurityException, NoSuchMethodException { + @SuppressWarnings("unused") class C { + public Object setFoo(String p) { return new Object(); } + public Object setFoo(int p) { return new Object(); } + } + BeanInfo bi = Introspector.getBeanInfo(C.class); + + assertThat(hasReadMethodForProperty(bi, "foo"), is(false)); + assertThat(hasWriteMethodForProperty(bi, "foo"), is(false)); + + BeanInfo ebi = new ExtendedBeanInfo(bi); + + assertThat(hasReadMethodForProperty(bi, "foo"), is(false)); + assertThat(hasWriteMethodForProperty(bi, "foo"), is(false)); + + assertThat(hasReadMethodForProperty(ebi, "foo"), is(false)); + assertThat(hasWriteMethodForProperty(ebi, "foo"), is(true)); + + for (PropertyDescriptor pd : ebi.getPropertyDescriptors()) { + if (pd.getName().equals("foo")) { + assertThat(pd.getWriteMethod(), is(C.class.getMethod("setFoo", String.class))); + return; + } + } + fail("never matched write method"); + } + + @Test + public void overloadedNonStandardWriteMethodsOnly_orderB() throws IntrospectionException, SecurityException, NoSuchMethodException { + @SuppressWarnings("unused") class C { + public Object setFoo(int p) { return new Object(); } + public Object setFoo(String p) { return new Object(); } + } + BeanInfo bi = Introspector.getBeanInfo(C.class); + + assertThat(hasReadMethodForProperty(bi, "foo"), is(false)); + assertThat(hasWriteMethodForProperty(bi, "foo"), is(false)); + + BeanInfo ebi = new ExtendedBeanInfo(bi); + + assertThat(hasReadMethodForProperty(bi, "foo"), is(false)); + assertThat(hasWriteMethodForProperty(bi, "foo"), is(false)); + + assertThat(hasReadMethodForProperty(ebi, "foo"), is(false)); + assertThat(hasWriteMethodForProperty(ebi, "foo"), is(true)); + + for (PropertyDescriptor pd : ebi.getPropertyDescriptors()) { + if (pd.getName().equals("foo")) { + assertThat(pd.getWriteMethod(), is(C.class.getMethod("setFoo", int.class))); + return; + } + } + fail("never matched write method"); + } + /** * Corners the bug revealed by SPR-8522, in which an (apparently) indexed write method * without a corresponding indexed read method would fail to be processed correctly by @@ -645,7 +720,7 @@ public class ExtendedBeanInfoTests { assertThat(hasIndexedReadMethodForProperty(bi, "dateFormat"), is(false)); assertThat(hasIndexedWriteMethodForProperty(bi, "dateFormat"), is(trueUntilJdk17())); - ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi); + BeanInfo ebi = new ExtendedBeanInfo(bi); assertThat(hasReadMethodForProperty(bi, "dateFormat"), is(false)); assertThat(hasWriteMethodForProperty(bi, "dateFormat"), is(false)); @@ -653,7 +728,7 @@ public class ExtendedBeanInfoTests { assertThat(hasIndexedWriteMethodForProperty(bi, "dateFormat"), is(trueUntilJdk17())); assertThat(hasReadMethodForProperty(ebi, "dateFormat"), is(false)); - assertThat(hasWriteMethodForProperty(ebi, "dateFormat"), is(false)); + assertThat(hasWriteMethodForProperty(ebi, "dateFormat"), is(true)); assertThat(hasIndexedReadMethodForProperty(ebi, "dateFormat"), is(false)); assertThat(hasIndexedWriteMethodForProperty(ebi, "dateFormat"), is(true)); } @@ -661,7 +736,7 @@ public class ExtendedBeanInfoTests { @Test public void propertyCountsMatch() throws IntrospectionException { BeanInfo bi = Introspector.getBeanInfo(TestBean.class); - ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi); + BeanInfo ebi = new ExtendedBeanInfo(bi); assertThat(ebi.getPropertyDescriptors().length, equalTo(bi.getPropertyDescriptors().length)); } @@ -673,7 +748,7 @@ public class ExtendedBeanInfoTests { public ExtendedTestBean setFoo(String s) { return this; } } BeanInfo bi = Introspector.getBeanInfo(ExtendedTestBean.class); - ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi); + BeanInfo ebi = new ExtendedBeanInfo(bi); boolean found = false; for (PropertyDescriptor pd : ebi.getPropertyDescriptors()) { @@ -692,7 +767,7 @@ public class ExtendedBeanInfoTests { @Test public void propertyDescriptorOrderIsEqual() throws IntrospectionException { BeanInfo bi = Introspector.getBeanInfo(TestBean.class); - ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi); + BeanInfo ebi = new ExtendedBeanInfo(bi); for (int i = 0; i < bi.getPropertyDescriptors().length; i++) { assertThat("element " + i + " in BeanInfo and ExtendedBeanInfo propertyDescriptor arrays do not match", @@ -739,7 +814,9 @@ public class ExtendedBeanInfoTests { private boolean hasIndexedWriteMethodForProperty(BeanInfo beanInfo, String propertyName) { for (PropertyDescriptor pd : beanInfo.getPropertyDescriptors()) { if (pd.getName().equals(propertyName)) { - assertThat(propertyName + " property is not indexed", pd, instanceOf(IndexedPropertyDescriptor.class)); + if (!(pd instanceof IndexedPropertyDescriptor)) { + return false; + } return ((IndexedPropertyDescriptor)pd).getIndexedWriteMethod() != null; } } @@ -749,7 +826,9 @@ public class ExtendedBeanInfoTests { private boolean hasIndexedReadMethodForProperty(BeanInfo beanInfo, String propertyName) { for (PropertyDescriptor pd : beanInfo.getPropertyDescriptors()) { if (pd.getName().equals(propertyName)) { - assertThat(propertyName + " property is not indexed", pd, instanceOf(IndexedPropertyDescriptor.class)); + if (!(pd instanceof IndexedPropertyDescriptor)) { + return false; + } return ((IndexedPropertyDescriptor)pd).getIndexedReadMethod() != null; } } @@ -830,7 +909,7 @@ public class ExtendedBeanInfoTests { } // and now demonstrate that we've indeed fixed the problem - ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi); + BeanInfo ebi = new ExtendedBeanInfo(bi); assertThat(hasReadMethodForProperty(bi, "targetMethod"), is(true)); assertThat(hasWriteMethodForProperty(bi, "targetMethod"), is(false)); @@ -855,12 +934,11 @@ public class ExtendedBeanInfoTests { assertThat(hasIndexedWriteMethodForProperty(bi, "address"), is(true)); } { - ExtendedBeanInfo bi = new ExtendedBeanInfo(Introspector.getBeanInfo(A.class)); + BeanInfo bi = new ExtendedBeanInfo(Introspector.getBeanInfo(A.class)); assertThat(hasReadMethodForProperty(bi, "address"), is(false)); assertThat(hasWriteMethodForProperty(bi, "address"), is(false)); assertThat(hasIndexedReadMethodForProperty(bi, "address"), is(true)); assertThat(hasIndexedWriteMethodForProperty(bi, "address"), is(true)); } } - } diff --git a/spring-beans/src/test/java/org/springframework/beans/SimplePropertyDescriptorTests.java b/spring-beans/src/test/java/org/springframework/beans/SimplePropertyDescriptorTests.java new file mode 100644 index 0000000000..8627083279 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/SimplePropertyDescriptorTests.java @@ -0,0 +1,150 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed 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.springframework.beans; + +import java.beans.IndexedPropertyDescriptor; +import java.beans.IntrospectionException; +import java.beans.PropertyDescriptor; + +import java.lang.reflect.Method; + +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +/** + * Unit tests for {@link SimpleNonIndexedPropertyDescriptor} and + * {@link SimpleIndexedPropertyDescriptor}. + * + * @author Chris Beams + * @see ExtendedBeanInfoTests + */ +public class SimplePropertyDescriptorTests { + + @Test + public void toStringOutput() throws IntrospectionException, SecurityException, NoSuchMethodException { + { + Object pd = new SimpleNonIndexedPropertyDescriptor("foo", null, null); + assertThat(pd.toString(), containsString( + "PropertyDescriptor[name=foo, propertyType=null, readMethod=null")); + } + { + class C { + @SuppressWarnings("unused") + public Object setFoo(String foo) { return null; } + } + Method m = C.class.getMethod("setFoo", String.class); + Object pd = new SimpleNonIndexedPropertyDescriptor("foo", null, m); + assertThat(pd.toString(), allOf( + containsString("PropertyDescriptor[name=foo"), + containsString("propertyType=class java.lang.String"), + containsString("readMethod=null, writeMethod=public java.lang.Object"))); + } + { + Object pd = new SimpleIndexedPropertyDescriptor("foo", null, null, null, null); + assertThat(pd.toString(), containsString( + "PropertyDescriptor[name=foo, propertyType=null, indexedPropertyType=null")); + } + { + class C { + @SuppressWarnings("unused") + public Object setFoo(int i, String foo) { return null; } + } + Method m = C.class.getMethod("setFoo", int.class, String.class); + Object pd = new SimpleIndexedPropertyDescriptor("foo", null, null, null, m); + assertThat(pd.toString(), allOf( + containsString("PropertyDescriptor[name=foo, propertyType=null"), + containsString("indexedPropertyType=class java.lang.String"), + containsString("indexedWriteMethod=public java.lang.Object"))); + } + } + + @Test + public void nonIndexedEquality() throws IntrospectionException, SecurityException, NoSuchMethodException { + Object pd1 = new SimpleNonIndexedPropertyDescriptor("foo", null, null); + assertThat(pd1, equalTo(pd1)); + + Object pd2 = new SimpleNonIndexedPropertyDescriptor("foo", null, null); + assertThat(pd1, equalTo(pd2)); + assertThat(pd2, equalTo(pd1)); + + @SuppressWarnings("unused") + class C { + public Object setFoo(String foo) { return null; } + public String getFoo() { return null; } + } + Method wm1 = C.class.getMethod("setFoo", String.class); + Object pd3 = new SimpleNonIndexedPropertyDescriptor("foo", null, wm1); + assertThat(pd1, not(equalTo(pd3))); + assertThat(pd3, not(equalTo(pd1))); + + Method rm1 = C.class.getMethod("getFoo"); + Object pd4 = new SimpleNonIndexedPropertyDescriptor("foo", rm1, null); + assertThat(pd1, not(equalTo(pd4))); + assertThat(pd4, not(equalTo(pd1))); + + Object pd5 = new PropertyDescriptor("foo", null, null); + assertThat(pd1, equalTo(pd5)); + assertThat(pd5, equalTo(pd1)); + + Object pd6 = "not a PD"; + assertThat(pd1, not(equalTo(pd6))); + assertThat(pd6, not(equalTo(pd1))); + + Object pd7 = null; + assertThat(pd1, not(equalTo(pd7))); + assertThat(pd7, not(equalTo(pd1))); + } + + @Test + public void indexedEquality() throws IntrospectionException, SecurityException, NoSuchMethodException { + Object pd1 = new SimpleIndexedPropertyDescriptor("foo", null, null, null, null); + assertThat(pd1, equalTo(pd1)); + + Object pd2 = new SimpleIndexedPropertyDescriptor("foo", null, null, null, null); + assertThat(pd1, equalTo(pd2)); + assertThat(pd2, equalTo(pd1)); + + @SuppressWarnings("unused") + class C { + public Object setFoo(int i, String foo) { return null; } + public String getFoo(int i) { return null; } + } + Method wm1 = C.class.getMethod("setFoo", int.class, String.class); + Object pd3 = new SimpleIndexedPropertyDescriptor("foo", null, null, null, wm1); + assertThat(pd1, not(equalTo(pd3))); + assertThat(pd3, not(equalTo(pd1))); + + Method rm1 = C.class.getMethod("getFoo", int.class); + Object pd4 = new SimpleIndexedPropertyDescriptor("foo", null, null, rm1, null); + assertThat(pd1, not(equalTo(pd4))); + assertThat(pd4, not(equalTo(pd1))); + + Object pd5 = new IndexedPropertyDescriptor("foo", null, null, null, null); + assertThat(pd1, equalTo(pd5)); + assertThat(pd5, equalTo(pd1)); + + Object pd6 = "not a PD"; + assertThat(pd1, not(equalTo(pd6))); + assertThat(pd6, not(equalTo(pd1))); + + Object pd7 = null; + assertThat(pd1, not(equalTo(pd7))); + assertThat(pd7, not(equalTo(pd1))); + } +}