org.springframework.beans.ExtendedBeanInfo Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2002-2015 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.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.ObjectUtils;
/**
* Decorator for a standard {@link BeanInfo} object, e.g. as created by
* {@link Introspector#getBeanInfo(Class)}, designed to discover and register static
* and/or non-void returning setter methods. For example:
*
* public class Bean {
* private Foo foo;
*
* public Foo getFoo() {
* return this.foo;
* }
*
* public Bean setFoo(Foo foo) {
* this.foo = foo;
* return this;
* }
* }
* The standard JavaBeans {@code Introspector} will discover the {@code getFoo} read
* method, but will bypass the {@code #setFoo(Foo)} write method, because its non-void
* returning signature does not comply with the JavaBeans specification.
* {@code ExtendedBeanInfo}, on the other hand, will recognize and include it. This is
* designed to allow APIs with "builder" or method-chaining style setter signatures to be
* used within Spring {@code } XML. {@link #getPropertyDescriptors()} returns all
* existing property descriptors from the wrapped {@code BeanInfo} as well any added for
* non-void returning setters. Both standard ("non-indexed") and
*
* indexed properties are fully supported.
*
* @author Chris Beams
* @since 3.1
* @see #ExtendedBeanInfo(BeanInfo)
* @see ExtendedBeanInfoFactory
* @see CachedIntrospectionResults
*/
class ExtendedBeanInfo implements BeanInfo {
private static final Log logger = LogFactory.getLog(ExtendedBeanInfo.class);
private final BeanInfo delegate;
private final Set propertyDescriptors =
new TreeSet(new PropertyDescriptorComparator());
/**
* Wrap the given {@link BeanInfo} instance; copy all its existing property descriptors
* locally, wrapping each in a custom {@link SimpleIndexedPropertyDescriptor indexed}
* or {@link SimplePropertyDescriptor non-indexed} {@code PropertyDescriptor}
* variant that bypasses default JDK weak/soft reference management; then search
* through its method descriptors to find any non-void returning write methods and
* update or create the corresponding {@link PropertyDescriptor} for each one found.
* @param delegate the wrapped {@code BeanInfo}, which is never modified
* @throws IntrospectionException if any problems occur creating and adding new
* property descriptors
* @see #getPropertyDescriptors()
*/
public ExtendedBeanInfo(BeanInfo delegate) throws IntrospectionException {
this.delegate = delegate;
for (PropertyDescriptor pd : delegate.getPropertyDescriptors()) {
try {
this.propertyDescriptors.add(pd instanceof IndexedPropertyDescriptor ?
new SimpleIndexedPropertyDescriptor((IndexedPropertyDescriptor) pd) :
new SimplePropertyDescriptor(pd));
}
catch (IntrospectionException ex) {
// Probably simply a method that wasn't meant to follow the JavaBeans pattern...
if (logger.isDebugEnabled()) {
logger.debug("Ignoring invalid bean property '" + pd.getName() + "': " + ex.getMessage());
}
}
}
MethodDescriptor[] methodDescriptors = delegate.getMethodDescriptors();
if (methodDescriptors != null) {
for (Method method : findCandidateWriteMethods(methodDescriptors)) {
try {
handleCandidateWriteMethod(method);
}
catch (IntrospectionException ex) {
// We're only trying to find candidates, can easily ignore extra ones here...
if (logger.isDebugEnabled()) {
logger.debug("Ignoring candidate write method [" + method + "]: " + ex.getMessage());
}
}
}
}
}
private List findCandidateWriteMethods(MethodDescriptor[] methodDescriptors) {
List matches = new ArrayList();
for (MethodDescriptor methodDescriptor : methodDescriptors) {
Method method = methodDescriptor.getMethod();
if (isCandidateWriteMethod(method)) {
matches.add(method);
}
}
// Sort non-void returning write methods to guard against the ill effects of
// non-deterministic sorting of methods returned from Class#getDeclaredMethods
// under JDK 7. See http://bugs.sun.com/view_bug.do?bug_id=7023180
Collections.sort(matches, new Comparator() {
@Override
public int compare(Method m1, Method m2) {
return m2.toString().compareTo(m1.toString());
}
});
return matches;
}
public static boolean isCandidateWriteMethod(Method method) {
String methodName = method.getName();
Class>[] parameterTypes = method.getParameterTypes();
int nParams = parameterTypes.length;
return (methodName.length() > 3 && methodName.startsWith("set") && Modifier.isPublic(method.getModifiers()) &&
(!void.class.isAssignableFrom(method.getReturnType()) || Modifier.isStatic(method.getModifiers())) &&
(nParams == 1 || (nParams == 2 && int.class == parameterTypes[0])));
}
private void handleCandidateWriteMethod(Method method) throws IntrospectionException {
int nParams = method.getParameterTypes().length;
String propertyName = propertyNameFor(method);
Class> propertyType = method.getParameterTypes()[nParams - 1];
PropertyDescriptor existingPd = findExistingPropertyDescriptor(propertyName, propertyType);
if (nParams == 1) {
if (existingPd == null) {
this.propertyDescriptors.add(new SimplePropertyDescriptor(propertyName, null, method));
}
else {
existingPd.setWriteMethod(method);
}
}
else if (nParams == 2) {
if (existingPd == null) {
this.propertyDescriptors.add(
new SimpleIndexedPropertyDescriptor(propertyName, null, null, null, method));
}
else if (existingPd instanceof IndexedPropertyDescriptor) {
((IndexedPropertyDescriptor) existingPd).setIndexedWriteMethod(method);
}
else {
this.propertyDescriptors.remove(existingPd);
this.propertyDescriptors.add(new SimpleIndexedPropertyDescriptor(
propertyName, existingPd.getReadMethod(), existingPd.getWriteMethod(), null, method));
}
}
else {
throw new IllegalArgumentException("Write method must have exactly 1 or 2 parameters: " + method);
}
}
private PropertyDescriptor findExistingPropertyDescriptor(String propertyName, Class> propertyType) {
for (PropertyDescriptor pd : this.propertyDescriptors) {
final Class> candidateType;
final String candidateName = pd.getName();
if (pd instanceof IndexedPropertyDescriptor) {
IndexedPropertyDescriptor ipd = (IndexedPropertyDescriptor) pd;
candidateType = ipd.getIndexedPropertyType();
if (candidateName.equals(propertyName) &&
(candidateType.equals(propertyType) || candidateType.equals(propertyType.getComponentType()))) {
return pd;
}
}
else {
candidateType = pd.getPropertyType();
if (candidateName.equals(propertyName) &&
(candidateType.equals(propertyType) || propertyType.equals(candidateType.getComponentType()))) {
return pd;
}
}
}
return null;
}
private String propertyNameFor(Method method) {
return Introspector.decapitalize(method.getName().substring(3, method.getName().length()));
}
/**
* 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)
*/
@Override
public PropertyDescriptor[] getPropertyDescriptors() {
return this.propertyDescriptors.toArray(new PropertyDescriptor[this.propertyDescriptors.size()]);
}
@Override
public BeanInfo[] getAdditionalBeanInfo() {
return this.delegate.getAdditionalBeanInfo();
}
@Override
public BeanDescriptor getBeanDescriptor() {
return this.delegate.getBeanDescriptor();
}
@Override
public int getDefaultEventIndex() {
return this.delegate.getDefaultEventIndex();
}
@Override
public int getDefaultPropertyIndex() {
return this.delegate.getDefaultPropertyIndex();
}
@Override
public EventSetDescriptor[] getEventSetDescriptors() {
return this.delegate.getEventSetDescriptors();
}
@Override
public Image getIcon(int iconKind) {
return this.delegate.getIcon(iconKind);
}
@Override
public MethodDescriptor[] getMethodDescriptors() {
return this.delegate.getMethodDescriptors();
}
static class SimplePropertyDescriptor extends PropertyDescriptor {
private Method readMethod;
private Method writeMethod;
private Class> propertyType;
private Class> propertyEditorClass;
public SimplePropertyDescriptor(PropertyDescriptor original) throws IntrospectionException {
this(original.getName(), original.getReadMethod(), original.getWriteMethod());
PropertyDescriptorUtils.copyNonMethodProperties(original, this);
}
public SimplePropertyDescriptor(String propertyName, Method readMethod, Method writeMethod) throws IntrospectionException {
super(propertyName, null, null);
this.readMethod = readMethod;
this.writeMethod = writeMethod;
this.propertyType = PropertyDescriptorUtils.findPropertyType(readMethod, writeMethod);
}
@Override
public Method getReadMethod() {
return this.readMethod;
}
@Override
public void setReadMethod(Method readMethod) {
this.readMethod = readMethod;
}
@Override
public Method getWriteMethod() {
return this.writeMethod;
}
@Override
public void setWriteMethod(Method writeMethod) {
this.writeMethod = writeMethod;
}
@Override
public Class> getPropertyType() {
if (this.propertyType == null) {
try {
this.propertyType = PropertyDescriptorUtils.findPropertyType(this.readMethod, this.writeMethod);
}
catch (IntrospectionException ex) {
// Ignore, as does PropertyDescriptor#getPropertyType
}
}
return this.propertyType;
}
@Override
public Class> getPropertyEditorClass() {
return this.propertyEditorClass;
}
@Override
public void setPropertyEditorClass(Class> propertyEditorClass) {
this.propertyEditorClass = propertyEditorClass;
}
@Override
public boolean equals(Object other) {
return (this == other || (other instanceof PropertyDescriptor &&
PropertyDescriptorUtils.equals(this, (PropertyDescriptor) other)));
}
@Override
public int hashCode() {
return (ObjectUtils.nullSafeHashCode(getReadMethod()) * 29 + ObjectUtils.nullSafeHashCode(getWriteMethod()));
}
@Override
public String toString() {
return String.format("%s[name=%s, propertyType=%s, readMethod=%s, writeMethod=%s]",
getClass().getSimpleName(), getName(), getPropertyType(), this.readMethod, this.writeMethod);
}
}
static class SimpleIndexedPropertyDescriptor extends IndexedPropertyDescriptor {
private Method readMethod;
private Method writeMethod;
private Class> propertyType;
private Method indexedReadMethod;
private Method indexedWriteMethod;
private Class> indexedPropertyType;
private Class> propertyEditorClass;
public SimpleIndexedPropertyDescriptor(IndexedPropertyDescriptor original) throws IntrospectionException {
this(original.getName(), original.getReadMethod(), original.getWriteMethod(),
original.getIndexedReadMethod(), original.getIndexedWriteMethod());
PropertyDescriptorUtils.copyNonMethodProperties(original, this);
}
public SimpleIndexedPropertyDescriptor(String propertyName, Method readMethod, Method writeMethod,
Method indexedReadMethod, Method indexedWriteMethod) throws IntrospectionException {
super(propertyName, null, null, null, null);
this.readMethod = readMethod;
this.writeMethod = writeMethod;
this.propertyType = PropertyDescriptorUtils.findPropertyType(readMethod, writeMethod);
this.indexedReadMethod = indexedReadMethod;
this.indexedWriteMethod = indexedWriteMethod;
this.indexedPropertyType = PropertyDescriptorUtils.findIndexedPropertyType(
propertyName, this.propertyType, indexedReadMethod, indexedWriteMethod);
}
@Override
public Method getReadMethod() {
return this.readMethod;
}
@Override
public void setReadMethod(Method readMethod) {
this.readMethod = readMethod;
}
@Override
public Method getWriteMethod() {
return this.writeMethod;
}
@Override
public void setWriteMethod(Method writeMethod) {
this.writeMethod = writeMethod;
}
@Override
public Class> getPropertyType() {
if (this.propertyType == null) {
try {
this.propertyType = PropertyDescriptorUtils.findPropertyType(this.readMethod, this.writeMethod);
}
catch (IntrospectionException ex) {
// Ignore, as does IndexedPropertyDescriptor#getPropertyType
}
}
return this.propertyType;
}
@Override
public Method getIndexedReadMethod() {
return this.indexedReadMethod;
}
@Override
public void setIndexedReadMethod(Method indexedReadMethod) throws IntrospectionException {
this.indexedReadMethod = indexedReadMethod;
}
@Override
public Method getIndexedWriteMethod() {
return this.indexedWriteMethod;
}
@Override
public void setIndexedWriteMethod(Method indexedWriteMethod) throws IntrospectionException {
this.indexedWriteMethod = indexedWriteMethod;
}
@Override
public Class> getIndexedPropertyType() {
if (this.indexedPropertyType == null) {
try {
this.indexedPropertyType = PropertyDescriptorUtils.findIndexedPropertyType(
getName(), getPropertyType(), this.indexedReadMethod, this.indexedWriteMethod);
}
catch (IntrospectionException ex) {
// Ignore, as does IndexedPropertyDescriptor#getIndexedPropertyType
}
}
return this.indexedPropertyType;
}
@Override
public Class> getPropertyEditorClass() {
return this.propertyEditorClass;
}
@Override
public void setPropertyEditorClass(Class> propertyEditorClass) {
this.propertyEditorClass = propertyEditorClass;
}
/*
* See java.beans.IndexedPropertyDescriptor#equals(java.lang.Object)
*/
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof IndexedPropertyDescriptor)) {
return false;
}
IndexedPropertyDescriptor otherPd = (IndexedPropertyDescriptor) other;
return (ObjectUtils.nullSafeEquals(getIndexedReadMethod(), otherPd.getIndexedReadMethod()) &&
ObjectUtils.nullSafeEquals(getIndexedWriteMethod(), otherPd.getIndexedWriteMethod()) &&
ObjectUtils.nullSafeEquals(getIndexedPropertyType(), otherPd.getIndexedPropertyType()) &&
PropertyDescriptorUtils.equals(this, otherPd));
}
@Override
public int hashCode() {
int hashCode = ObjectUtils.nullSafeHashCode(getReadMethod());
hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getWriteMethod());
hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getIndexedReadMethod());
hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getIndexedWriteMethod());
return hashCode;
}
@Override
public String toString() {
return String.format("%s[name=%s, propertyType=%s, indexedPropertyType=%s, " +
"readMethod=%s, writeMethod=%s, indexedReadMethod=%s, indexedWriteMethod=%s]",
getClass().getSimpleName(), getName(), getPropertyType(), getIndexedPropertyType(),
this.readMethod, this.writeMethod, this.indexedReadMethod, this.indexedWriteMethod);
}
}
/**
* Sorts PropertyDescriptor instances alpha-numerically to emulate the behavior of
* {@link java.beans.BeanInfo#getPropertyDescriptors()}.
* @see ExtendedBeanInfo#propertyDescriptors
*/
static class PropertyDescriptorComparator implements Comparator {
@Override
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();
}
}
}