/*
* The Spring Framework is published under the terms
* of the Apache Software License.
*/
package org.springframework.beans;
import java.beans.Introspector;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyVetoException;
import java.beans.VetoableChangeListener;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Convenience superclass for JavaBeans VetoableChangeListeners.
* This class implements the VetoableChangeListener interface to delegate
* the method call to one of any number of validation methods defined in
* concrete subclasses. This is a typical use of reflection to avoid the
* need for a chain of if/else statements, discussed in
* <a href="http://www.amazon.com/exec/obidos/tg/detail/-/0764543857/">Expert One-on-One J2EE Design and Development</a>.
*
* <p>The signature for validation methods must be of this form
* (the following example validates an int property named age):
* <p><code>
* public void validateAge(int age, PropertyChangeEvent e) throws PropertyVetoException
* </code>
* <p>Note that the field can be expected to have been converted to the required type,
* simplifying validation logic.
*
* <p>Validation methods must be public or protected. The return value is not required,
* but will be ignored.
*
* <p>Subclasses should be threadsafe: nothing in this superclass will cause
* a problem. Because validation methods are cached by this class's constructor,
* the overhead of reflection is not great.
*
* <p><b>NB:</b>Validation methods will receive a reversion event after they have
* vetoed a change. So, if an email property is initially null and an invalid email address
* is supplied and vetoed by the first call to validateEmail for the given validator,
* a second event will be sent when the email field is reverted to null. This means that
* validation methods must be able to cope with initial values. They can, however,
* throw another PropertyVetoException, which will be ignored by the caller.
*
* @author Rod Johnson
*/
public abstract class AbstractVetoableChangeListener implements VetoableChangeListener {
private final Log logger = LogFactory.getLog(getClass());
/**
* Prefix for validation methods: a typical name might be validateAge()
*/
protected static final String VALIDATE_METHOD_PREFIX = "validate";
/** Validation methods, keyed by propertyName */
private HashMap validationMethodHash = new HashMap();
/**
* Creates new AbstractVetoableChangeListener.
* Caches validation methods for efficiency.
*/
public AbstractVetoableChangeListener() throws SecurityException {
// Look at all methods in the subclass, trying to find
// methods that are validators according to our criteria
Method [] methods = getClass().getMethods();
for (int i = 0; i < methods.length; i++) {
// We're looking for methods with names starting with the given prefix,
// and two parameters: the value (which may be of any type, primitive or object)
// and a PropertyChangeEvent.
if (methods[i].getName().startsWith(VALIDATE_METHOD_PREFIX) &&
methods[i].getParameterTypes().length == 2 &&
PropertyChangeEvent.class.isAssignableFrom(methods[i].getParameterTypes()[1])) {
// We've found a potential validator: it has the right number of parameters
// and its name begins with validate...
logger.debug("Found potential validator method [" + methods[i] + "]");
Class[] exceptions = methods[i].getExceptionTypes();
// We don't care about the return type, but we must ensure that
// the method throws only one checked exception, PropertyVetoException
if (exceptions.length == 1 && PropertyVetoException.class.isAssignableFrom(exceptions[0])) {
// We have a valid validator method
// Ensure it's accessible (for example, it might be a method on an inner class)
methods[i].setAccessible(true);
String propertyName = Introspector.decapitalize(methods[i].getName().substring(VALIDATE_METHOD_PREFIX.length()));
validationMethodHash.put(propertyName, methods[i]);
logger.debug(methods[i] + " is validator for property " + propertyName);
}
else {
logger.debug("Invalid validator");
}
}
else {
logger.debug("Method [" + methods[i] + "] is not a validator");
}
}
}
/**
* Implementation of VetoableChangeListener.
* Will attempt to locate the appropriate validator method and invoke it.
* Will do nothing if there is no validation method for this property.
*/
public final void vetoableChange(PropertyChangeEvent e) throws PropertyVetoException {
if (logger.isDebugEnabled())
logger.debug("VetoableChangeEvent: old value=[" + e.getOldValue() + "] new value=[" + e.getNewValue() + "]");
Method method = (Method) validationMethodHash.get(e.getPropertyName());
if (method != null) {
try {
logger.debug("Using validator method: " + method);
Object val = e.getNewValue();
method.invoke(this, new Object[] { val, e });
}
catch (IllegalAccessException ex) {
logger.warn("Can't validate: Method isn't accessible");
}
catch (InvocationTargetException ex) {
// This is what we're looking for: the subclass's
// validator method vetoed the property change event
// We know that the exception must be of the correct type (unless
// it's a runtime exception) as we checked the declared exceptions of the
// validator method in this class's constructor.
// If it IS a runtime exception, we just rethrow it, to encourage the
// author of the subclass to write robust code...
if (ex.getTargetException() instanceof RuntimeException)
throw (RuntimeException) ex.getTargetException();
PropertyVetoException pex = (PropertyVetoException) ex.getTargetException();
throw pex;
}
} // if there was a validator method for this property
else {
logger.debug("No validation method for property: " + e.getPropertyName());
}
}
}