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

@ -221,7 +221,7 @@ public class CachedIntrospectionResults {
if (logger.isTraceEnabled()) { if (logger.isTraceEnabled()) {
logger.trace("Getting BeanInfo for class [" + beanClass.getName() + "]"); 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 // Immediately remove class from Introspector cache, to allow for proper
// garbage collection on class loader shutdown - we cache it here anyway, // garbage collection on class loader shutdown - we cache it here anyway,

View File

@ -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
* <a href="http://download.oracle.com/javase/tutorial/javabeans/properties/indexed.html">
* indexed properties</a> are fully supported.
*
* <p>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<PropertyDescriptor> propertyDescriptors =
new TreeSet<PropertyDescriptor>(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.
*
* <p>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<PropertyDescriptor> {
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();
}
}
}

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;
}
}