Introduce ExtendedBeanInfo

Decorator for instances returned from
Introspector#getBeanInfo(Class<?>) that supports detection and inclusion
of non-void returning setter methods. Fully supports indexed properties
and otherwise faithfully mimics the default
BeanInfo#getPropertyDescriptors() behavior, e.g., PropertyDescriptor
ordering, etc.

This decorator has been integrated with CachedIntrospectionResults
meaning that, in simple terms, the Spring container now supports
injection of setter methods having any return type.

Issue: SPR-8079
This commit is contained in:
Chris Beams
2011-03-31 12:06:36 +00:00
parent ec1b230ae5
commit 2f5085aef1
3 changed files with 914 additions and 1 deletions

View File

@@ -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<This extends B<This>> {
@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<C> {
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;
}
}