diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java b/org.springframework.beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java
index 569c317b304..fc6438434e7 100644
--- a/org.springframework.beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java
+++ b/org.springframework.beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java
@@ -221,7 +221,7 @@ public class CachedIntrospectionResults {
if (logger.isTraceEnabled()) {
logger.trace("Getting BeanInfo for class [" + beanClass.getName() + "]");
}
- this.beanInfo = Introspector.getBeanInfo(beanClass);
+ this.beanInfo = new ExtendedBeanInfo(Introspector.getBeanInfo(beanClass));
// Immediately remove class from Introspector cache, to allow for proper
// garbage collection on class loader shutdown - we cache it here anyway,
diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.java b/org.springframework.beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.java
new file mode 100644
index 00000000000..32709cb427c
--- /dev/null
+++ b/org.springframework.beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright 2002-2011 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.awt.Image;
+import java.beans.BeanDescriptor;
+import java.beans.BeanInfo;
+import java.beans.EventSetDescriptor;
+import java.beans.IndexedPropertyDescriptor;
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.MethodDescriptor;
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.Method;
+import java.util.Comparator;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import org.springframework.util.ReflectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * 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
+ *
+ * indexed properties are fully supported.
+ *
+ *
The wrapped {@code BeanInfo} object is not modified in any way.
+ *
+ * @author Chris Beams
+ * @since 3.1
+ * @see CachedIntrospectionResults
+ */
+public class ExtendedBeanInfo implements BeanInfo {
+ private final BeanInfo delegate;
+ private final SortedSet 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.
+ *
+ * The wrapped {@code BeanInfo} is not modified in any way by this process.
+ *
+ * @see #getPropertyDescriptors()
+ * @throws IntrospectionException if any problems occur creating and adding new {@code PropertyDescriptors}
+ */
+ public ExtendedBeanInfo(BeanInfo delegate) throws IntrospectionException {
+ this.delegate = delegate;
+
+ // PropertyDescriptor instances from the delegate object are never added directly, but always
+ // copied to the local collection of #propertyDescriptors and returned by calls to
+ // #getPropertyDescriptors(). this algorithm iterates through all methods (method descriptors)
+ // in the wrapped BeanInfo object, copying any existing PropertyDescriptor or creating a new
+ // one for any non-standard setter methods found.
+
+ ALL_METHODS:
+ for (MethodDescriptor md : delegate.getMethodDescriptors()) {
+ Method method = md.getMethod();
+
+ // 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);
+ for (PropertyDescriptor pd : delegate.getPropertyDescriptors()) {
+ Method readMethod = pd.getReadMethod();
+ Method writeMethod = pd.getWriteMethod();
+ // 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(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(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(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);
+ DELEGATE_PD:
+ for (PropertyDescriptor pd : delegate.getPropertyDescriptors()) {
+ if (!(pd instanceof IndexedPropertyDescriptor)) {
+ continue DELEGATE_PD;
+ }
+ IndexedPropertyDescriptor ipd = (IndexedPropertyDescriptor) pd;
+ Method readMethod = ipd.getReadMethod();
+ Method writeMethod = ipd.getWriteMethod();
+ Method indexedReadMethod = ipd.getIndexedReadMethod();
+ Method indexedWriteMethod = ipd.getIndexedWriteMethod();
+ // has the setter already been found by the wrapped BeanInfo?
+ if (indexedWriteMethod != null
+ && indexedWriteMethod.getName().equals(method.getName())) {
+ // yes -> copy it, including corresponding getter method (if any -- may be null)
+ this.addOrUpdatePropertyDescriptor(propertyName, readMethod, writeMethod, indexedReadMethod, indexedWriteMethod);
+ continue ALL_METHODS;
+ }
+ // has a getter corresponding to this setter already been found by the wrapped BeanInfo?
+ if (indexedReadMethod != null
+ && indexedReadMethod.getName().equals(getterMethodNameFor(propertyName))
+ && indexedReadMethod.getReturnType().equals(method.getParameterTypes()[1])) {
+ this.addOrUpdatePropertyDescriptor(propertyName, readMethod, writeMethod, indexedReadMethod, method);
+ 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(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?
+ for (PropertyDescriptor existingPD : this.propertyDescriptors) {
+ if (method.equals(pd.getReadMethod())
+ && existingPD.getName().equals(pd.getName())) {
+ if (existingPD.getReadMethod() == null) {
+ // no -> add it now
+ this.addOrUpdatePropertyDescriptor(pd.getName(), method, pd.getWriteMethod());
+ }
+ // yes -> do not add a duplicate
+ continue ALL_METHODS;
+ }
+ }
+ if (method == pd.getReadMethod()
+ || (pd instanceof IndexedPropertyDescriptor && method == ((IndexedPropertyDescriptor) pd).getIndexedReadMethod())) {
+ // yes -> copy it, including corresponding setter method (if any -- may be null)
+ if (pd instanceof IndexedPropertyDescriptor) {
+ this.addOrUpdatePropertyDescriptor(pd.getName(), pd.getReadMethod(), pd.getWriteMethod(), ((IndexedPropertyDescriptor)pd).getIndexedReadMethod(), ((IndexedPropertyDescriptor)pd).getIndexedWriteMethod());
+ } else {
+ this.addOrUpdatePropertyDescriptor(pd.getName(), pd.getReadMethod(), pd.getWriteMethod());
+ }
+ continue ALL_METHODS;
+ }
+ }
+ }
+ }
+
+ private void addOrUpdatePropertyDescriptor(String propertyName, Method readMethod, Method writeMethod) throws IntrospectionException {
+ addOrUpdatePropertyDescriptor(propertyName, readMethod, writeMethod, null, null);
+ }
+
+ private void addOrUpdatePropertyDescriptor(String propertyName, Method readMethod, Method writeMethod, Method indexedReadMethod, Method indexedWriteMethod) throws IntrospectionException {
+ 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 (existingPD.getReadMethod() != null) {
+ if (readMethod != null && existingPD.getReadMethod().getReturnType() != readMethod.getReturnType()
+ || writeMethod != null && existingPD.getReadMethod().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 (existingPD.getWriteMethod() != null) {
+ if (readMethod != null && existingPD.getWriteMethod().getParameterTypes()[0] != readMethod.getReturnType()
+ || writeMethod != null && existingPD.getWriteMethod().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.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 (existingIPD.getIndexedReadMethod() != null) {
+ if (indexedReadMethod != null && existingIPD.getIndexedReadMethod().getReturnType() != indexedReadMethod.getReturnType()
+ || indexedWriteMethod != null && existingIPD.getIndexedReadMethod().getReturnType() != indexedWriteMethod.getParameterTypes()[1]) {
+ // no -> add a new descriptor for it below
+ break;
+ }
+ }
+ // update the existing descriptor's indexed read method
+ try {
+ 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 (existingIPD.getIndexedWriteMethod() != null) {
+ if (indexedReadMethod != null && existingIPD.getIndexedWriteMethod().getParameterTypes()[1] != indexedReadMethod.getReturnType()
+ || indexedWriteMethod != null && existingIPD.getIndexedWriteMethod().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;
+ }
+ }
+
+ // we haven't yet seen read or write methods for this property -> add a new descriptor
+ if (indexedReadMethod == null && indexedWriteMethod == null) {
+ this.propertyDescriptors.add(new PropertyDescriptor(propertyName, readMethod, writeMethod));
+ } else {
+ this.propertyDescriptors.add(new IndexedPropertyDescriptor(propertyName, readMethod, writeMethod, indexedReadMethod, indexedWriteMethod));
+ }
+ }
+
+ private String propertyNameFor(Method method) {
+ return Introspector.decapitalize(method.getName().substring(3,method.getName().length()));
+ }
+
+ private Object getterMethodNameFor(String name) {
+ return "get" + StringUtils.capitalize(name);
+ }
+
+ public BeanInfo[] getAdditionalBeanInfo() {
+ return delegate.getAdditionalBeanInfo();
+ }
+
+ public BeanDescriptor getBeanDescriptor() {
+ return delegate.getBeanDescriptor();
+ }
+
+ public int getDefaultEventIndex() {
+ return delegate.getDefaultEventIndex();
+ }
+
+ public int getDefaultPropertyIndex() {
+ return delegate.getDefaultPropertyIndex();
+ }
+
+ public EventSetDescriptor[] getEventSetDescriptors() {
+ return delegate.getEventSetDescriptors();
+ }
+
+ public Image getIcon(int arg0) {
+ return delegate.getIcon(arg0);
+ }
+
+ 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()]);
+ }
+
+
+ /**
+ * Sorts PropertyDescriptor instances alphanumerically 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;
+ }
+ }
+ return left.length() - right.length();
+ }
+ }
+}
\ No newline at end of file
diff --git a/org.springframework.beans/src/test/java/org/springframework/beans/ExtendedBeanInfoTests.java b/org.springframework.beans/src/test/java/org/springframework/beans/ExtendedBeanInfoTests.java
new file mode 100644
index 00000000000..b0fc2aa6a3e
--- /dev/null
+++ b/org.springframework.beans/src/test/java/org/springframework/beans/ExtendedBeanInfoTests.java
@@ -0,0 +1,587 @@
+/*
+ * Copyright 2002-2011 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 static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.lessThan;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+import java.beans.BeanInfo;
+import java.beans.IndexedPropertyDescriptor;
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+
+import org.junit.Ignore;
+import org.junit.Test;
+import org.springframework.beans.ExtendedBeanInfo.PropertyDescriptorComparator;
+
+import test.beans.TestBean;
+
+/**
+ * Unit tests for {@link ExtendedBeanInfo}.
+ *
+ * @author Chris Beams
+ * @since 3.1
+ */
+public class ExtendedBeanInfoTests {
+
+ @Test
+ public void standardReadMethodOnly() throws IntrospectionException {
+ @SuppressWarnings("unused") class C {
+ public String getFoo() { return null; }
+ }
+
+ BeanInfo bi = Introspector.getBeanInfo(C.class);
+ ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi);
+
+ assertThat(hasReadMethodForProperty(bi, "foo"), is(true));
+ assertThat(hasWriteMethodForProperty(bi, "foo"), is(false));
+
+ assertThat(hasReadMethodForProperty(ebi, "foo"), is(true));
+ assertThat(hasWriteMethodForProperty(ebi, "foo"), is(false));
+ }
+
+ @Test
+ public void standardWriteMethodOnly() throws IntrospectionException {
+ @SuppressWarnings("unused") class C {
+ public void setFoo(String f) { }
+ }
+
+ BeanInfo bi = Introspector.getBeanInfo(C.class);
+ ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi);
+
+ assertThat(hasReadMethodForProperty(bi, "foo"), is(false));
+ assertThat(hasWriteMethodForProperty(bi, "foo"), is(true));
+
+ assertThat(hasReadMethodForProperty(ebi, "foo"), is(false));
+ assertThat(hasWriteMethodForProperty(ebi, "foo"), is(true));
+ }
+
+ @Test
+ public void standardReadAndWriteMethods() throws IntrospectionException {
+ @SuppressWarnings("unused") class C {
+ public void setFoo(String f) { }
+ public String getFoo() { return null; }
+ }
+
+ BeanInfo bi = Introspector.getBeanInfo(C.class);
+ ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi);
+
+ assertThat(hasReadMethodForProperty(bi, "foo"), is(true));
+ assertThat(hasWriteMethodForProperty(bi, "foo"), is(true));
+
+ assertThat(hasReadMethodForProperty(ebi, "foo"), is(true));
+ assertThat(hasWriteMethodForProperty(ebi, "foo"), is(true));
+ }
+
+ @Test
+ public void nonStandardWriteMethodOnly() throws IntrospectionException {
+ @SuppressWarnings("unused") class C {
+ public C setFoo(String foo) { return this; }
+ }
+
+ BeanInfo bi = Introspector.getBeanInfo(C.class);
+ ExtendedBeanInfo 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));
+ }
+
+ @Test
+ public void standardReadAndNonStandardWriteMethods() throws IntrospectionException {
+ @SuppressWarnings("unused") class C {
+ public String getFoo() { return null; }
+ public C setFoo(String foo) { return this; }
+ }
+
+ BeanInfo bi = Introspector.getBeanInfo(C.class);
+ ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi);
+
+ assertThat(hasReadMethodForProperty(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 standardReadMethodsAndOverloadedNonStandardWriteMethods() throws Exception {
+ @SuppressWarnings("unused") class C {
+ public String getFoo() { return null; }
+ public C setFoo(String foo) { return this; }
+ public C setFoo(Number foo) { return this; }
+ }
+
+ BeanInfo bi = Introspector.getBeanInfo(C.class);
+ ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi);
+
+ assertThat(hasReadMethodForProperty(bi, "foo"), is(true));
+ assertThat(hasWriteMethodForProperty(bi, "foo"), is(false));
+
+ assertThat(hasReadMethodForProperty(ebi, "foo"), is(true));
+ 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 standardReadMethodInSuperclassAndNonStandardWriteMethodInSubclass() throws Exception {
+ @SuppressWarnings("unused") class B {
+ public String getFoo() { return null; }
+ }
+ @SuppressWarnings("unused") class C extends B {
+ public C setFoo(String foo) { return this; }
+ }
+
+ BeanInfo bi = Introspector.getBeanInfo(C.class);
+ ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi);
+
+ assertThat(hasReadMethodForProperty(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 standardReadMethodInSuperAndSubclassesAndGenericBuilderStyleNonStandardWriteMethodInSuperAndSubclasses() throws Exception {
+ abstract class B> {
+ @SuppressWarnings("unchecked")
+ protected final This instance = (This) this;
+ private String foo;
+ public String getFoo() { return foo; }
+ public This setFoo(String foo) {
+ this.foo = foo;
+ return this.instance;
+ }
+ }
+
+ class C extends B {
+ private int bar = -1;
+ public int getBar() { return bar; }
+ public C setBar(int bar) {
+ this.bar = bar;
+ return this.instance;
+ }
+ }
+
+ C c = new C()
+ .setFoo("blue")
+ .setBar(42);
+
+ assertThat(c.getFoo(), is("blue"));
+ assertThat(c.getBar(), is(42));
+
+ BeanInfo bi = Introspector.getBeanInfo(C.class);
+ ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi);
+
+ assertThat(hasReadMethodForProperty(bi, "foo"), is(true));
+ assertThat(hasWriteMethodForProperty(bi, "foo"), is(false));
+
+ assertThat(hasReadMethodForProperty(bi, "bar"), is(true));
+ assertThat(hasWriteMethodForProperty(bi, "bar"), is(false));
+
+ assertThat(hasReadMethodForProperty(ebi, "foo"), is(true));
+ assertThat(hasWriteMethodForProperty(ebi, "foo"), is(true));
+
+ assertThat(hasReadMethodForProperty(ebi, "bar"), is(true));
+ assertThat(hasWriteMethodForProperty(ebi, "bar"), is(true));
+ }
+
+ @Test
+ public void nonPublicStandardReadAndWriteMethods() throws Exception {
+ @SuppressWarnings("unused") class C {
+ String getFoo() { return null; }
+ C setFoo(String foo) { return this; }
+ }
+
+ BeanInfo bi = Introspector.getBeanInfo(C.class);
+ ExtendedBeanInfo 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(false));
+ }
+
+ /**
+ * {@link ExtendedBeanInfo} should behave exactly like {@link BeanInfo}
+ * in strange edge cases.
+ */
+ @Test
+ public void readMethodReturnsSupertypeOfWriteMethodParameter() throws IntrospectionException {
+ @SuppressWarnings("unused") class C {
+ public Number getFoo() { return null; }
+ public void setFoo(Integer foo) { }
+ }
+
+ BeanInfo bi = Introspector.getBeanInfo(C.class);
+ ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi);
+
+ assertThat(hasReadMethodForProperty(bi, "foo"), is(true));
+ assertThat(hasWriteMethodForProperty(bi, "foo"), is(false));
+
+ assertThat(hasReadMethodForProperty(ebi, "foo"), is(true));
+ assertThat(hasWriteMethodForProperty(ebi, "foo"), is(false));
+ }
+
+ @Test
+ public void indexedReadMethodReturnsSupertypeOfIndexedWriteMethodParameter() throws IntrospectionException {
+ @SuppressWarnings("unused") class C {
+ public Number getFoos(int index) { return null; }
+ public void setFoos(int index, Integer foo) { }
+ }
+
+ BeanInfo bi = Introspector.getBeanInfo(C.class);
+ ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi);
+
+ assertThat(hasIndexedReadMethodForProperty(bi, "foos"), is(true));
+ assertThat(hasIndexedWriteMethodForProperty(bi, "foos"), is(false));
+
+ assertThat(hasIndexedReadMethodForProperty(ebi, "foos"), is(true));
+ assertThat(hasIndexedWriteMethodForProperty(ebi, "foos"), is(false));
+ }
+
+ /**
+ * {@link ExtendedBeanInfo} should behave exactly like {@link BeanInfo}
+ * in strange edge cases.
+ */
+ @Test
+ public void readMethodReturnsSubtypeOfWriteMethodParameter() throws IntrospectionException {
+ @SuppressWarnings("unused") class C {
+ public Integer getFoo() { return null; }
+ public void setFoo(Number foo) { }
+ }
+
+ BeanInfo bi = Introspector.getBeanInfo(C.class);
+ ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi);
+
+ assertThat(hasReadMethodForProperty(bi, "foo"), is(true));
+ assertThat(hasWriteMethodForProperty(bi, "foo"), is(false));
+
+ assertThat(hasReadMethodForProperty(ebi, "foo"), is(true));
+ assertThat(hasWriteMethodForProperty(ebi, "foo"), is(false));
+ }
+
+ @Test
+ public void indexedReadMethodReturnsSubtypeOfIndexedWriteMethodParameter() throws IntrospectionException {
+ @SuppressWarnings("unused") class C {
+ public Integer getFoos(int index) { return null; }
+ public void setFoo(int index, Number foo) { }
+ }
+
+ BeanInfo bi = Introspector.getBeanInfo(C.class);
+ ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi);
+
+ assertThat(hasIndexedReadMethodForProperty(bi, "foos"), is(true));
+ assertThat(hasIndexedWriteMethodForProperty(bi, "foos"), is(false));
+
+ assertThat(hasIndexedReadMethodForProperty(ebi, "foos"), is(true));
+ assertThat(hasIndexedWriteMethodForProperty(ebi, "foos"), is(false));
+ }
+
+ @Test
+ public void indexedReadMethodOnly() throws IntrospectionException {
+ @SuppressWarnings("unused")
+ class C {
+ // indexed read method
+ public String getFoos(int i) { return null; }
+ }
+
+ BeanInfo bi = Introspector.getBeanInfo(C.class);
+ BeanInfo ebi = new ExtendedBeanInfo(Introspector.getBeanInfo(C.class));
+
+ assertThat(hasReadMethodForProperty(bi, "foos"), is(false));
+ assertThat(hasIndexedReadMethodForProperty(bi, "foos"), is(true));
+
+ assertThat(hasReadMethodForProperty(ebi, "foos"), is(false));
+ assertThat(hasIndexedReadMethodForProperty(ebi, "foos"), is(true));
+ }
+
+ @Test
+ public void indexedWriteMethodOnly() throws IntrospectionException {
+ @SuppressWarnings("unused")
+ class C {
+ // indexed write method
+ public void setFoos(int i, String foo) { }
+ }
+
+ BeanInfo bi = Introspector.getBeanInfo(C.class);
+ BeanInfo ebi = new ExtendedBeanInfo(Introspector.getBeanInfo(C.class));
+
+ assertThat(hasWriteMethodForProperty(bi, "foos"), is(false));
+ assertThat(hasIndexedWriteMethodForProperty(bi, "foos"), is(true));
+
+ assertThat(hasWriteMethodForProperty(ebi, "foos"), is(false));
+ assertThat(hasIndexedWriteMethodForProperty(ebi, "foos"), is(true));
+ }
+
+ @Test
+ public void indexedReadAndIndexedWriteMethods() throws IntrospectionException {
+ @SuppressWarnings("unused")
+ class C {
+ // indexed read method
+ public String getFoos(int i) { return null; }
+ // indexed write method
+ public void setFoos(int i, String foo) { }
+ }
+
+ BeanInfo bi = Introspector.getBeanInfo(C.class);
+ BeanInfo ebi = new ExtendedBeanInfo(Introspector.getBeanInfo(C.class));
+
+ assertThat(hasReadMethodForProperty(bi, "foos"), is(false));
+ assertThat(hasIndexedReadMethodForProperty(bi, "foos"), is(true));
+ assertThat(hasWriteMethodForProperty(bi, "foos"), is(false));
+ assertThat(hasIndexedWriteMethodForProperty(bi, "foos"), is(true));
+
+ assertThat(hasReadMethodForProperty(ebi, "foos"), is(false));
+ assertThat(hasIndexedReadMethodForProperty(ebi, "foos"), is(true));
+ assertThat(hasWriteMethodForProperty(ebi, "foos"), is(false));
+ assertThat(hasIndexedWriteMethodForProperty(ebi, "foos"), is(true));
+ }
+
+ @Test
+ public void readAndWriteAndIndexedReadAndIndexedWriteMethods() throws IntrospectionException {
+ @SuppressWarnings("unused")
+ class C {
+ // read method
+ public String[] getFoos() { return null; }
+ // indexed read method
+ public String getFoos(int i) { return null; }
+ // write method
+ public void setFoos(String[] foos) { }
+ // indexed write method
+ public void setFoos(int i, String foo) { }
+ }
+
+ BeanInfo bi = Introspector.getBeanInfo(C.class);
+ BeanInfo ebi = new ExtendedBeanInfo(Introspector.getBeanInfo(C.class));
+
+ assertThat(hasReadMethodForProperty(bi, "foos"), is(true));
+ assertThat(hasWriteMethodForProperty(bi, "foos"), is(true));
+ assertThat(hasIndexedReadMethodForProperty(bi, "foos"), is(true));
+ assertThat(hasIndexedWriteMethodForProperty(bi, "foos"), is(true));
+
+ assertThat(hasReadMethodForProperty(ebi, "foos"), is(true));
+ assertThat(hasWriteMethodForProperty(ebi, "foos"), is(true));
+ assertThat(hasIndexedReadMethodForProperty(ebi, "foos"), is(true));
+ assertThat(hasIndexedWriteMethodForProperty(ebi, "foos"), is(true));
+ }
+
+ @Test
+ public void indexedReadAndNonStandardIndexedWrite() throws IntrospectionException {
+ @SuppressWarnings("unused")
+ class C {
+ // indexed read method
+ public String getFoos(int i) { return null; }
+ // non-standard indexed write method
+ public C setFoos(int i, String foo) { return this; }
+ }
+
+ BeanInfo bi = Introspector.getBeanInfo(C.class);
+ BeanInfo ebi = new ExtendedBeanInfo(Introspector.getBeanInfo(C.class));
+
+ assertThat(hasIndexedReadMethodForProperty(bi, "foos"), is(true));
+ // interesting! standard Inspector picks up non-void return types on indexed write methods by default
+ assertThat(hasIndexedWriteMethodForProperty(bi, "foos"), is(true));
+
+ assertThat(hasIndexedReadMethodForProperty(ebi, "foos"), is(true));
+ assertThat(hasIndexedWriteMethodForProperty(ebi, "foos"), is(true));
+ }
+
+ @Test
+ public void indexedReadAndNonStandardWriteAndNonStandardIndexedWrite() throws IntrospectionException {
+ @SuppressWarnings("unused")
+ class C {
+ // non-standard write method
+ public C setFoos(String[] foos) { return this; }
+ // indexed read method
+ public String getFoos(int i) { return null; }
+ // non-standard indexed write method
+ public C setFoos(int i, String foo) { return this; }
+ }
+
+ BeanInfo bi = Introspector.getBeanInfo(C.class);
+ BeanInfo ebi = new ExtendedBeanInfo(Introspector.getBeanInfo(C.class));
+
+ assertThat(hasIndexedReadMethodForProperty(bi, "foos"), is(true));
+ assertThat(hasWriteMethodForProperty(bi, "foos"), is(false));
+ // again as above, standard Inspector picks up non-void return types on indexed write methods by default
+ assertThat(hasIndexedWriteMethodForProperty(bi, "foos"), is(true));
+
+ assertThat(hasIndexedReadMethodForProperty(ebi, "foos"), is(true));
+ assertThat(hasWriteMethodForProperty(ebi, "foos"), is(true));
+ assertThat(hasIndexedWriteMethodForProperty(ebi, "foos"), is(true));
+ }
+
+ @Test
+ public void subclassWriteMethodWithCovariantReturnType() throws IntrospectionException {
+ @SuppressWarnings("unused") class B {
+ public String getFoo() { return null; }
+ public Number setFoo(String foo) { return null; }
+ }
+ class C extends B {
+ public String getFoo() { return null; }
+ public Integer setFoo(String foo) { return null; }
+ }
+
+ BeanInfo bi = Introspector.getBeanInfo(C.class);
+ ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi);
+
+ assertThat(hasReadMethodForProperty(bi, "foo"), is(true));
+ assertThat(hasWriteMethodForProperty(bi, "foo"), is(false));
+
+ assertThat(hasReadMethodForProperty(ebi, "foo"), is(true));
+ assertThat(hasWriteMethodForProperty(ebi, "foo"), is(true));
+
+ assertThat(ebi.getPropertyDescriptors().length, equalTo(bi.getPropertyDescriptors().length));
+ }
+
+ @Test
+ public void nonStandardReadMethodAndStandardWriteMethod() throws IntrospectionException {
+ @SuppressWarnings("unused") class C {
+ public void getFoo() { }
+ public void setFoo(String foo) { }
+ }
+
+ BeanInfo bi = Introspector.getBeanInfo(C.class);
+ ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi);
+
+ assertThat(hasReadMethodForProperty(bi, "foo"), is(false));
+ assertThat(hasWriteMethodForProperty(bi, "foo"), is(true));
+
+ assertThat(hasReadMethodForProperty(ebi, "foo"), is(false));
+ assertThat(hasWriteMethodForProperty(ebi, "foo"), is(true));
+ }
+
+ @Test
+ public void propertyCountsMatch() throws IntrospectionException {
+ BeanInfo bi = Introspector.getBeanInfo(TestBean.class);
+ ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi);
+
+ assertThat(ebi.getPropertyDescriptors().length, equalTo(bi.getPropertyDescriptors().length));
+ }
+
+ @Test
+ public void propertyCountsWithNonStandardWriteMethod() throws IntrospectionException {
+ class ExtendedTestBean extends TestBean {
+ @SuppressWarnings("unused")
+ public ExtendedTestBean setFoo(String s) { return this; }
+ }
+ BeanInfo bi = Introspector.getBeanInfo(ExtendedTestBean.class);
+ ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi);
+
+ boolean found = false;
+ for (PropertyDescriptor pd : ebi.getPropertyDescriptors()) {
+ if (pd.getName().equals("foo")) {
+ found = true;
+ }
+ }
+ assertThat(found, is(true));
+ assertThat(ebi.getPropertyDescriptors().length, equalTo(bi.getPropertyDescriptors().length+1));
+ }
+
+ /**
+ * {@link BeanInfo#getPropertyDescriptors()} returns alphanumerically sorted.
+ * Test that {@link ExtendedBeanInfo#getPropertyDescriptors()} does the same.
+ */
+ @Test
+ public void propertyDescriptorOrderIsEqual() throws IntrospectionException {
+ BeanInfo bi = Introspector.getBeanInfo(TestBean.class);
+ ExtendedBeanInfo 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",
+ ebi.getPropertyDescriptors()[i].getName(), equalTo(bi.getPropertyDescriptors()[i].getName()));
+ }
+ }
+
+ @Test
+ public void propertyDescriptorComparator() throws IntrospectionException {
+ PropertyDescriptorComparator c = new PropertyDescriptorComparator();
+ assertThat(c.compare(new PropertyDescriptor("a", null, null), new PropertyDescriptor("a", null, null)), equalTo(0));
+ assertThat(c.compare(new PropertyDescriptor("abc", null, null), new PropertyDescriptor("abc", null, null)), equalTo(0));
+ assertThat(c.compare(new PropertyDescriptor("a", null, null), new PropertyDescriptor("b", null, null)), lessThan(0));
+ assertThat(c.compare(new PropertyDescriptor("b", null, null), new PropertyDescriptor("a", null, null)), greaterThan(0));
+ assertThat(c.compare(new PropertyDescriptor("abc", null, null), new PropertyDescriptor("abd", null, null)), lessThan(0));
+ assertThat(c.compare(new PropertyDescriptor("xyz", null, null), new PropertyDescriptor("123", null, null)), greaterThan(0));
+ assertThat(c.compare(new PropertyDescriptor("a", null, null), new PropertyDescriptor("abc", null, null)), lessThan(0));
+ assertThat(c.compare(new PropertyDescriptor("abc", null, null), new PropertyDescriptor("a", null, null)), greaterThan(0));
+ assertThat(c.compare(new PropertyDescriptor("abc", null, null), new PropertyDescriptor("b", null, null)), lessThan(0));
+
+ assertThat(c.compare(new PropertyDescriptor(" ", null, null), new PropertyDescriptor("a", null, null)), lessThan(0));
+ assertThat(c.compare(new PropertyDescriptor("1", null, null), new PropertyDescriptor("a", null, null)), lessThan(0));
+ assertThat(c.compare(new PropertyDescriptor("a", null, null), new PropertyDescriptor("A", null, null)), greaterThan(0));
+ }
+
+
+ private boolean hasWriteMethodForProperty(BeanInfo beanInfo, String propertyName) {
+ for (PropertyDescriptor pd : beanInfo.getPropertyDescriptors()) {
+ if (pd.getName().equals(propertyName)) {
+ return pd.getWriteMethod() != null;
+ }
+ }
+ return false;
+ }
+
+ private boolean hasReadMethodForProperty(BeanInfo beanInfo, String propertyName) {
+ for (PropertyDescriptor pd : beanInfo.getPropertyDescriptors()) {
+ if (pd.getName().equals(propertyName)) {
+ return pd.getReadMethod() != null;
+ }
+ }
+ return false;
+ }
+
+ 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));
+ return ((IndexedPropertyDescriptor)pd).getIndexedWriteMethod() != null;
+ }
+ }
+ return false;
+ }
+
+ 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));
+ return ((IndexedPropertyDescriptor)pd).getIndexedReadMethod() != null;
+ }
+ }
+ return false;
+ }
+
+}