From 2f5085aef1e9ac3655a1b1250b6ceca9d0ca3398 Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Thu, 31 Mar 2011 12:06:36 +0000 Subject: [PATCH] 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 --- .../beans/CachedIntrospectionResults.java | 2 +- .../beans/ExtendedBeanInfo.java | 326 ++++++++++ .../beans/ExtendedBeanInfoTests.java | 587 ++++++++++++++++++ 3 files changed, 914 insertions(+), 1 deletion(-) create mode 100644 org.springframework.beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.java create mode 100644 org.springframework.beans/src/test/java/org/springframework/beans/ExtendedBeanInfoTests.java 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 569c317b30..fc6438434e 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 0000000000..32709cb427 --- /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 0000000000..b0fc2aa6a3 --- /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; + } + +} -- GitLab