Skip to content

Commit 2f5085a

Browse files
committed
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
1 parent ec1b230 commit 2f5085a

File tree

3 files changed

+914
-1
lines changed

3 files changed

+914
-1
lines changed

org.springframework.beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ private CachedIntrospectionResults(Class beanClass, boolean cacheFullMetadata) t
221221
if (logger.isTraceEnabled()) {
222222
logger.trace("Getting BeanInfo for class [" + beanClass.getName() + "]");
223223
}
224-
this.beanInfo = Introspector.getBeanInfo(beanClass);
224+
this.beanInfo = new ExtendedBeanInfo(Introspector.getBeanInfo(beanClass));
225225

226226
// Immediately remove class from Introspector cache, to allow for proper
227227
// garbage collection on class loader shutdown - we cache it here anyway,
Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
/*
2+
* Copyright 2002-2011 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.beans;
18+
19+
import java.awt.Image;
20+
import java.beans.BeanDescriptor;
21+
import java.beans.BeanInfo;
22+
import java.beans.EventSetDescriptor;
23+
import java.beans.IndexedPropertyDescriptor;
24+
import java.beans.IntrospectionException;
25+
import java.beans.Introspector;
26+
import java.beans.MethodDescriptor;
27+
import java.beans.PropertyDescriptor;
28+
import java.lang.reflect.Method;
29+
import java.util.Comparator;
30+
import java.util.SortedSet;
31+
import java.util.TreeSet;
32+
33+
import org.springframework.util.ReflectionUtils;
34+
import org.springframework.util.StringUtils;
35+
36+
/**
37+
* Decorates a standard {@link BeanInfo} object (likely created created by
38+
* {@link Introspector#getBeanInfo(Class)}) by including non-void returning setter
39+
* methods in the collection of {@link #getPropertyDescriptors() property descriptors}.
40+
* Both regular and
41+
* <a href="http://download.oracle.com/javase/tutorial/javabeans/properties/indexed.html">
42+
* indexed properties</a> are fully supported.
43+
*
44+
* <p>The wrapped {@code BeanInfo} object is not modified in any way.
45+
*
46+
* @author Chris Beams
47+
* @since 3.1
48+
* @see CachedIntrospectionResults
49+
*/
50+
public class ExtendedBeanInfo implements BeanInfo {
51+
private final BeanInfo delegate;
52+
private final SortedSet<PropertyDescriptor> propertyDescriptors =
53+
new TreeSet<PropertyDescriptor>(new PropertyDescriptorComparator());
54+
55+
/**
56+
* Wrap the given delegate {@link BeanInfo} instance and find any non-void returning
57+
* setter methods, creating and adding a {@link PropertyDescriptor} for each.
58+
*
59+
* <p>The wrapped {@code BeanInfo} is not modified in any way by this process.
60+
*
61+
* @see #getPropertyDescriptors()
62+
* @throws IntrospectionException if any problems occur creating and adding new {@code PropertyDescriptors}
63+
*/
64+
public ExtendedBeanInfo(BeanInfo delegate) throws IntrospectionException {
65+
this.delegate = delegate;
66+
67+
// PropertyDescriptor instances from the delegate object are never added directly, but always
68+
// copied to the local collection of #propertyDescriptors and returned by calls to
69+
// #getPropertyDescriptors(). this algorithm iterates through all methods (method descriptors)
70+
// in the wrapped BeanInfo object, copying any existing PropertyDescriptor or creating a new
71+
// one for any non-standard setter methods found.
72+
73+
ALL_METHODS:
74+
for (MethodDescriptor md : delegate.getMethodDescriptors()) {
75+
Method method = md.getMethod();
76+
77+
// bypass non-getter java.lang.Class methods for efficiency
78+
if (ReflectionUtils.isObjectMethod(method) && !method.getName().startsWith("get")) {
79+
continue ALL_METHODS;
80+
}
81+
82+
// is the method a NON-INDEXED setter? ignore return type in order to capture non-void signatures
83+
if (method.getName().startsWith("set") && method.getParameterTypes().length == 1) {
84+
String propertyName = propertyNameFor(method);
85+
for (PropertyDescriptor pd : delegate.getPropertyDescriptors()) {
86+
Method readMethod = pd.getReadMethod();
87+
Method writeMethod = pd.getWriteMethod();
88+
// has the setter already been found by the wrapped BeanInfo?
89+
if (writeMethod != null
90+
&& writeMethod.getName().equals(method.getName())) {
91+
// yes -> copy it, including corresponding getter method (if any -- may be null)
92+
this.addOrUpdatePropertyDescriptor(propertyName, readMethod, writeMethod);
93+
continue ALL_METHODS;
94+
}
95+
// has a getter corresponding to this setter already been found by the wrapped BeanInfo?
96+
if (readMethod != null
97+
&& readMethod.getName().equals(getterMethodNameFor(propertyName))
98+
&& readMethod.getReturnType().equals(method.getParameterTypes()[0])) {
99+
this.addOrUpdatePropertyDescriptor(propertyName, readMethod, method);
100+
continue ALL_METHODS;
101+
}
102+
}
103+
// the setter method was not found by the wrapped BeanInfo -> add a new PropertyDescriptor for it
104+
// no corresponding getter was detected, so the 'read method' parameter is null.
105+
this.addOrUpdatePropertyDescriptor(propertyName, null, method);
106+
continue ALL_METHODS;
107+
}
108+
109+
// is the method an INDEXED setter? ignore return type in order to capture non-void signatures
110+
if (method.getName().startsWith("set") && method.getParameterTypes().length == 2 && method.getParameterTypes()[0].equals(int.class)) {
111+
String propertyName = propertyNameFor(method);
112+
DELEGATE_PD:
113+
for (PropertyDescriptor pd : delegate.getPropertyDescriptors()) {
114+
if (!(pd instanceof IndexedPropertyDescriptor)) {
115+
continue DELEGATE_PD;
116+
}
117+
IndexedPropertyDescriptor ipd = (IndexedPropertyDescriptor) pd;
118+
Method readMethod = ipd.getReadMethod();
119+
Method writeMethod = ipd.getWriteMethod();
120+
Method indexedReadMethod = ipd.getIndexedReadMethod();
121+
Method indexedWriteMethod = ipd.getIndexedWriteMethod();
122+
// has the setter already been found by the wrapped BeanInfo?
123+
if (indexedWriteMethod != null
124+
&& indexedWriteMethod.getName().equals(method.getName())) {
125+
// yes -> copy it, including corresponding getter method (if any -- may be null)
126+
this.addOrUpdatePropertyDescriptor(propertyName, readMethod, writeMethod, indexedReadMethod, indexedWriteMethod);
127+
continue ALL_METHODS;
128+
}
129+
// has a getter corresponding to this setter already been found by the wrapped BeanInfo?
130+
if (indexedReadMethod != null
131+
&& indexedReadMethod.getName().equals(getterMethodNameFor(propertyName))
132+
&& indexedReadMethod.getReturnType().equals(method.getParameterTypes()[1])) {
133+
this.addOrUpdatePropertyDescriptor(propertyName, readMethod, writeMethod, indexedReadMethod, method);
134+
continue ALL_METHODS;
135+
}
136+
}
137+
// the INDEXED setter method was not found by the wrapped BeanInfo -> add a new PropertyDescriptor
138+
// for it. no corresponding INDEXED getter was detected, so the 'indexed read method' parameter is null.
139+
this.addOrUpdatePropertyDescriptor(propertyName, null, null, null, method);
140+
continue ALL_METHODS;
141+
}
142+
143+
// the method is not a setter, but is it a getter?
144+
for (PropertyDescriptor pd : delegate.getPropertyDescriptors()) {
145+
// have we already copied this read method to a property descriptor locally?
146+
for (PropertyDescriptor existingPD : this.propertyDescriptors) {
147+
if (method.equals(pd.getReadMethod())
148+
&& existingPD.getName().equals(pd.getName())) {
149+
if (existingPD.getReadMethod() == null) {
150+
// no -> add it now
151+
this.addOrUpdatePropertyDescriptor(pd.getName(), method, pd.getWriteMethod());
152+
}
153+
// yes -> do not add a duplicate
154+
continue ALL_METHODS;
155+
}
156+
}
157+
if (method == pd.getReadMethod()
158+
|| (pd instanceof IndexedPropertyDescriptor && method == ((IndexedPropertyDescriptor) pd).getIndexedReadMethod())) {
159+
// yes -> copy it, including corresponding setter method (if any -- may be null)
160+
if (pd instanceof IndexedPropertyDescriptor) {
161+
this.addOrUpdatePropertyDescriptor(pd.getName(), pd.getReadMethod(), pd.getWriteMethod(), ((IndexedPropertyDescriptor)pd).getIndexedReadMethod(), ((IndexedPropertyDescriptor)pd).getIndexedWriteMethod());
162+
} else {
163+
this.addOrUpdatePropertyDescriptor(pd.getName(), pd.getReadMethod(), pd.getWriteMethod());
164+
}
165+
continue ALL_METHODS;
166+
}
167+
}
168+
}
169+
}
170+
171+
private void addOrUpdatePropertyDescriptor(String propertyName, Method readMethod, Method writeMethod) throws IntrospectionException {
172+
addOrUpdatePropertyDescriptor(propertyName, readMethod, writeMethod, null, null);
173+
}
174+
175+
private void addOrUpdatePropertyDescriptor(String propertyName, Method readMethod, Method writeMethod, Method indexedReadMethod, Method indexedWriteMethod) throws IntrospectionException {
176+
for (PropertyDescriptor existingPD : this.propertyDescriptors) {
177+
if (existingPD.getName().equals(propertyName)) {
178+
// is there already a descriptor that captures this read method or its corresponding write method?
179+
if (existingPD.getReadMethod() != null) {
180+
if (readMethod != null && existingPD.getReadMethod().getReturnType() != readMethod.getReturnType()
181+
|| writeMethod != null && existingPD.getReadMethod().getReturnType() != writeMethod.getParameterTypes()[0]) {
182+
// no -> add a new descriptor for it below
183+
break;
184+
}
185+
}
186+
// update the existing descriptor's read method
187+
if (readMethod != null) {
188+
try {
189+
existingPD.setReadMethod(readMethod);
190+
} catch (IntrospectionException ex) {
191+
// there is a conflicting setter method present -> null it out and try again
192+
existingPD.setWriteMethod(null);
193+
existingPD.setReadMethod(readMethod);
194+
}
195+
}
196+
197+
// is there already a descriptor that captures this write method or its corresponding read method?
198+
if (existingPD.getWriteMethod() != null) {
199+
if (readMethod != null && existingPD.getWriteMethod().getParameterTypes()[0] != readMethod.getReturnType()
200+
|| writeMethod != null && existingPD.getWriteMethod().getParameterTypes()[0] != writeMethod.getParameterTypes()[0]) {
201+
// no -> add a new descriptor for it below
202+
break;
203+
}
204+
}
205+
// update the existing descriptor's write method
206+
if (writeMethod != null) {
207+
existingPD.setWriteMethod(writeMethod);
208+
}
209+
210+
// is this descriptor indexed?
211+
if (existingPD instanceof IndexedPropertyDescriptor) {
212+
IndexedPropertyDescriptor existingIPD = (IndexedPropertyDescriptor) existingPD;
213+
214+
// is there already a descriptor that captures this indexed read method or its corresponding indexed write method?
215+
if (existingIPD.getIndexedReadMethod() != null) {
216+
if (indexedReadMethod != null && existingIPD.getIndexedReadMethod().getReturnType() != indexedReadMethod.getReturnType()
217+
|| indexedWriteMethod != null && existingIPD.getIndexedReadMethod().getReturnType() != indexedWriteMethod.getParameterTypes()[1]) {
218+
// no -> add a new descriptor for it below
219+
break;
220+
}
221+
}
222+
// update the existing descriptor's indexed read method
223+
try {
224+
existingIPD.setIndexedReadMethod(indexedReadMethod);
225+
} catch (IntrospectionException ex) {
226+
// there is a conflicting indexed setter method present -> null it out and try again
227+
existingIPD.setIndexedWriteMethod(null);
228+
existingIPD.setIndexedReadMethod(indexedReadMethod);
229+
}
230+
231+
// is there already a descriptor that captures this indexed write method or its corresponding indexed read method?
232+
if (existingIPD.getIndexedWriteMethod() != null) {
233+
if (indexedReadMethod != null && existingIPD.getIndexedWriteMethod().getParameterTypes()[1] != indexedReadMethod.getReturnType()
234+
|| indexedWriteMethod != null && existingIPD.getIndexedWriteMethod().getParameterTypes()[1] != indexedWriteMethod.getParameterTypes()[1]) {
235+
// no -> add a new descriptor for it below
236+
break;
237+
}
238+
}
239+
// update the existing descriptor's indexed write method
240+
if (indexedWriteMethod != null) {
241+
existingIPD.setIndexedWriteMethod(indexedWriteMethod);
242+
}
243+
}
244+
245+
// the descriptor has been updated -> return immediately
246+
return;
247+
}
248+
}
249+
250+
// we haven't yet seen read or write methods for this property -> add a new descriptor
251+
if (indexedReadMethod == null && indexedWriteMethod == null) {
252+
this.propertyDescriptors.add(new PropertyDescriptor(propertyName, readMethod, writeMethod));
253+
} else {
254+
this.propertyDescriptors.add(new IndexedPropertyDescriptor(propertyName, readMethod, writeMethod, indexedReadMethod, indexedWriteMethod));
255+
}
256+
}
257+
258+
private String propertyNameFor(Method method) {
259+
return Introspector.decapitalize(method.getName().substring(3,method.getName().length()));
260+
}
261+
262+
private Object getterMethodNameFor(String name) {
263+
return "get" + StringUtils.capitalize(name);
264+
}
265+
266+
public BeanInfo[] getAdditionalBeanInfo() {
267+
return delegate.getAdditionalBeanInfo();
268+
}
269+
270+
public BeanDescriptor getBeanDescriptor() {
271+
return delegate.getBeanDescriptor();
272+
}
273+
274+
public int getDefaultEventIndex() {
275+
return delegate.getDefaultEventIndex();
276+
}
277+
278+
public int getDefaultPropertyIndex() {
279+
return delegate.getDefaultPropertyIndex();
280+
}
281+
282+
public EventSetDescriptor[] getEventSetDescriptors() {
283+
return delegate.getEventSetDescriptors();
284+
}
285+
286+
public Image getIcon(int arg0) {
287+
return delegate.getIcon(arg0);
288+
}
289+
290+
public MethodDescriptor[] getMethodDescriptors() {
291+
return delegate.getMethodDescriptors();
292+
}
293+
294+
/**
295+
* Return the set of {@link PropertyDescriptor}s from the wrapped {@link BeanInfo}
296+
* object as well as {@code PropertyDescriptor}s for each non-void returning setter
297+
* method found during construction.
298+
* @see #ExtendedBeanInfo(BeanInfo)
299+
*/
300+
public PropertyDescriptor[] getPropertyDescriptors() {
301+
return this.propertyDescriptors.toArray(new PropertyDescriptor[this.propertyDescriptors.size()]);
302+
}
303+
304+
305+
/**
306+
* Sorts PropertyDescriptor instances alphanumerically to emulate the behavior of {@link java.beans.BeanInfo#getPropertyDescriptors()}.
307+
*
308+
* @see ExtendedBeanInfo#propertyDescriptors
309+
*/
310+
static class PropertyDescriptorComparator implements Comparator<PropertyDescriptor> {
311+
public int compare(PropertyDescriptor desc1, PropertyDescriptor desc2) {
312+
String left = desc1.getName();
313+
String right = desc2.getName();
314+
for (int i = 0; i < left.length(); i++) {
315+
if (right.length() == i) {
316+
return 1;
317+
}
318+
int result = left.getBytes()[i] - right.getBytes()[i];
319+
if (result != 0) {
320+
return result;
321+
}
322+
}
323+
return left.length() - right.length();
324+
}
325+
}
326+
}

0 commit comments

Comments
 (0)