/*
* Copyright 2004-2005 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.springmodules.validation.bean.conf.loader.xml;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.validation.Validator;
import org.springmodules.validation.bean.conf.*;
import org.springmodules.validation.bean.conf.loader.xml.handler.ClassValidationElementHandler;
import org.springmodules.validation.bean.conf.loader.xml.handler.PropertyValidationElementHandler;
import org.springmodules.validation.bean.rule.PropertyValidationRule;
import org.springmodules.validation.bean.rule.ValidationMethodValidationRule;
import org.springmodules.validation.bean.rule.ValidationRule;
import org.springmodules.validation.bean.rule.resolver.ErrorArgumentsResolver;
import org.springmodules.validation.bean.rule.resolver.FunctionErrorArgumentsResolver;
import org.springmodules.validation.util.cel.ConditionExpressionBased;
import org.springmodules.validation.util.cel.ConditionExpressionParser;
import org.springmodules.validation.util.cel.valang.ValangConditionExpressionParser;
import org.springmodules.validation.util.condition.Condition;
import org.springmodules.validation.util.condition.common.AlwaysTrueCondition;
import org.springmodules.validation.util.fel.FunctionExpressionBased;
import org.springmodules.validation.util.fel.FunctionExpressionParser;
import org.springmodules.validation.util.fel.parser.ValangFunctionExpressionParser;
import org.springmodules.validation.util.lang.ReflectionUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* The default xml bean validation configuration loader. This loader expects the following xml format:
* <p/>
* <pre>
* <validation [package="org.springmodules.validation.sample"]>
* <class name="Person">
* <global>
* <any/>...
* </global>
* <property name="firstName" [valid="true|false"]>
* <any/>...
* </property>
* </class>
* </validation>
* </pre>
* <p/>
* Please note the following:
* <p/>
* <ul>
* <li>Each <validation> element can contain multiple <class> elements.</li>
* <li>
* A <class> element can have only on <global> elements and multiple <property> elements. This
* elements hold validation rules to be bound globaly to the class instance or to specific properties.
* </li>
* <li>Both <global> and <property> elements can accept any element where each element represents
* a validation rule. These validation rule elements will eventually be evaluated in the order they are defined.
* When one of these rules fail, the evaluation stops
* </li>
* <li>
* A <property> may have a 'valid' attribute to indicate that the property value needs to be validated as
* well (cascade validation).
* </li>
* <li>
* The <validation> element may have a 'package' attribute. This will serve as a default package for all
* <class> elements (meaing there is not need to specify the fully qualified name in the 'name' attribute
* of this element.
* </li>
* <li>
* This XML format has a unique namespace which is defined by {@link #DEFAULT_NAMESPACE_URL}.
* </li>
* </ul>
* <p/>
* The validation rule element (sub-elements of <global> and <property>) are resolved using
* validation rule element handlers. This class holds a registry for such handlers, where new handlers can
* be registered as well. The default registry is {@link DefaultValidationRuleElementHandlerRegistry}.
*
* @author Uri Boness
*/
public class DefaultXmlBeanValidationConfigurationLoader extends AbstractXmlBeanValidationConfigurationLoader
implements ConditionExpressionBased, FunctionExpressionBased, ApplicationContextAware {
public static final String DEFAULT_NAMESPACE_URL = "http://www.springmodules.org/validation/bean";
private final static Log logger = LogFactory.getLog(DefaultXmlBeanValidationConfigurationLoader.class);
private static final String CLASS_TAG = "class";
private static final String GLOBAL_TAG = "global";
private static final String PROPERTY_TAG = "property";
private static final String METHOD_TAG = "method";
private static final String VALIDATOR_BEAN_TAG = "validator-ref";
private static final String VALIDATOR_TAG = "validator";
private static final String PACKAGE_ATTR = "package";
private static final String NAME_ATTR = "name";
private static final String CASCADE_ATTR = "cascade";
private static final String CASCADE_CONDITION_ATTR = "cascade-condition";
private static final String CLASS_ATTR = "class";
private static final String CODE_ATTR = "code";
private static final String MESSAGE_ATTR = "message";
private static final String ARGS_ATTR = "args";
private static final String APPLY_IF_ATTR = "apply-if";
private static final String CONTEXTS_ATTR = "contexts";
private static final String FOR_PROPERTY_ATTR = "for-property";
private ValidationRuleElementHandlerRegistry handlerRegistry;
private boolean conditionParserExplicitlySet = false;
private ConditionExpressionParser conditionExpressionParser;
private boolean functionParserExplicitlySet = false;
private FunctionExpressionParser functionExpressionParser;
private ApplicationContext applicationContext;
/**
* Constructs a new DefaultXmlBeanValidationConfigurationLoader with the default validation rule
* element handler registry.
*/
public DefaultXmlBeanValidationConfigurationLoader() {
this(new DefaultValidationRuleElementHandlerRegistry());
}
/**
* Constructs a new DefaultXmlBeanValidationConfigurationLoader with the given validation rule
* element handler registry.
*
* @param handlerRegistry The validation rule element handler registry that will be used by this loader.
*/
public DefaultXmlBeanValidationConfigurationLoader(ValidationRuleElementHandlerRegistry handlerRegistry) {
this(handlerRegistry, new ValangConditionExpressionParser(), new ValangFunctionExpressionParser());
}
/**
* Constructs a new DefaultXmlBeanValidationConfigurationLoader with the given validation rule
* element handler registry.
*
* @param handlerRegistry The validation rule element handler registry that will be used by this loader.
* @param conditionExpressionParser The condition parser this loader should use to parse the cascade validation conditions.
* @param functionExpressionParser The function parser this loader should use to parse the error arguments.
*/
public DefaultXmlBeanValidationConfigurationLoader(
ValidationRuleElementHandlerRegistry handlerRegistry,
ConditionExpressionParser conditionExpressionParser,
FunctionExpressionParser functionExpressionParser) {
this.handlerRegistry = handlerRegistry;
this.conditionExpressionParser = conditionExpressionParser;
this.functionExpressionParser = functionExpressionParser;
}
/**
* Loads the validation configuration from the given document that was created from the given resource.
*
* @see AbstractXmlBeanValidationConfigurationLoader#loadConfigurations(org.w3c.dom.Document, String)
*/
protected Map loadConfigurations(Document document, String resourceName) {
Map configurations = new HashMap();
Element validationDefinition = document.getDocumentElement();
String packageName = validationDefinition.getAttribute(PACKAGE_ATTR);
NodeList nodes = validationDefinition.getElementsByTagNameNS(DEFAULT_NAMESPACE_URL, CLASS_TAG);
for (int i = 0; i < nodes.getLength(); i++) {
Element classDefinition = (Element) nodes.item(i);
String className = classDefinition.getAttribute(NAME_ATTR);
className = (StringUtils.hasLength(packageName)) ? packageName + "." + className : className;
Class clazz;
try {
clazz = ClassUtils.forName(className);
} catch (ClassNotFoundException cnfe) {
logger.error("Could not load class '" + className + "' as defined in '" + resourceName + "'", cnfe);
continue;
}
configurations.put(clazz, handleClassDefinition(clazz, classDefinition));
}
return configurations;
}
/**
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
public void afterPropertiesSet() throws Exception {
initContext(handlerRegistry);
super.afterPropertiesSet();
findConditionExpressionParserInApplicationContext();
findFunctionExpressionParserInApplicationContext();
Assert.notNull(conditionExpressionParser);
Assert.notNull(functionExpressionParser);
}
//=============================================== Setter/Getter ====================================================
/**
* Sets the element handler registry this loader will use to fetch the handlers while loading
* validation configuration.
*
* @param registry The element handler registry to be used by this loader.
*/
public void setElementHandlerRegistry(ValidationRuleElementHandlerRegistry registry) {
this.handlerRegistry = registry;
}
/**
* Returns the element handler registry used by this loader.
*
* @return The element handler registry used by this loader.
*/
public ValidationRuleElementHandlerRegistry getElementHandlerRegistry() {
return handlerRegistry;
}
/**
* @see ConditionExpressionBased#setConditionExpressionParser(org.springmodules.validation.util.cel.ConditionExpressionParser)
*/
public void setConditionExpressionParser(ConditionExpressionParser conditionExpressionParser) {
this.conditionParserExplicitlySet = true;
this.conditionExpressionParser = conditionExpressionParser;
}
/**
* @see FunctionExpressionBased#setFunctionExpressionParser(org.springmodules.validation.util.fel.FunctionExpressionParser)
*/
public void setFunctionExpressionParser(FunctionExpressionParser functionExpressionParser) {
this.functionParserExplicitlySet = true;
this.functionExpressionParser = functionExpressionParser;
}
/**
* @see ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext)
*/
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
//=============================================== Helper Methods ===================================================
protected void initContext(Object object) throws Exception {
if (object instanceof ApplicationContextAware) {
((ApplicationContextAware) object).setApplicationContext(applicationContext);
}
if (object instanceof InitializingBean) {
((InitializingBean) object).afterPropertiesSet();
}
}
/**
* Creates and builds a bean validation configuration based for the given class, based on the given <class>
* element.
*
* @param element The <class> element.
* @param clazz The class for which the validation configuration is being loaded.
* @return The created bean validation configuration.
*/
public BeanValidationConfiguration handleClassDefinition(Class clazz, Element element) {
DefaultBeanValidationConfiguration configuration = new DefaultBeanValidationConfiguration();
NodeList nodes = element.getElementsByTagNameNS(DEFAULT_NAMESPACE_URL, VALIDATOR_TAG);
for (int i = 0; i < nodes.getLength(); i++) {
Element validatorDefinition = (Element) nodes.item(i);
handleValidatorDefinition(validatorDefinition, clazz, configuration);
}
nodes = element.getElementsByTagNameNS(DEFAULT_NAMESPACE_URL, VALIDATOR_BEAN_TAG);
for (int i = 0; i < nodes.getLength(); i++) {
Element validatorBeanDefinition = (Element) nodes.item(i);
handleValidatorBeanDefinition(validatorBeanDefinition, clazz, configuration);
}
nodes = element.getElementsByTagNameNS(DEFAULT_NAMESPACE_URL, GLOBAL_TAG);
for (int i = 0; i < nodes.getLength(); i++) {
Element globalDefinition = (Element) nodes.item(i);
handleGlobalDefinition(globalDefinition, clazz, configuration);
}
nodes = element.getElementsByTagNameNS(DEFAULT_NAMESPACE_URL, METHOD_TAG);
for (int i = 0; i < nodes.getLength(); i++) {
Element methodDefinition = (Element) nodes.item(i);
handleMethodDefinition(methodDefinition, clazz, configuration);
}
nodes = element.getElementsByTagNameNS(DEFAULT_NAMESPACE_URL, PROPERTY_TAG);
for (int i = 0; i < nodes.getLength(); i++) {
Element propertyDefinition = (Element) nodes.item(i);
handlePropertyDefinition(propertyDefinition, clazz, configuration);
}
return configuration;
}
protected void handleValidatorDefinition(Element validatorDefinition, Class clazz, MutableBeanValidationConfiguration configuration) {
String className = validatorDefinition.getAttribute(CLASS_ATTR);
configuration.addCustomValidator(constructValidator(className));
}
protected void handleValidatorBeanDefinition(Element definition, Class clazz, MutableBeanValidationConfiguration configuration) {
if (applicationContext == null) {
throw new UnsupportedOperationException(VALIDATOR_BEAN_TAG + " configuration cannot be applied for " +
"this configuration loader was not deployed in an application context");
}
String beanName = definition.getAttribute(NAME_ATTR);
Validator validator = (Validator)applicationContext.getBean(beanName, Validator.class);
configuration.addCustomValidator(validator);
}
/**
* Handles the <global> element and updates the given configuration with the global validation rules.
*
* @param globalDefinition The <global> element.
* @param clazz The validated class.
* @param configuration The bean validation configuration to update.
*/
protected void handleGlobalDefinition(Element globalDefinition, Class clazz, MutableBeanValidationConfiguration configuration) {
NodeList nodes = globalDefinition.getChildNodes();
for (int i = 0; i < nodes.getLength(); i++) {
Node node = nodes.item(i);
if (node.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
Element ruleDefinition = (Element) node;
ClassValidationElementHandler handler = handlerRegistry.findClassHandler(ruleDefinition, clazz);
if (handler == null) {
logger.error("Could not handle element '" + ruleDefinition.getTagName() +
"'. Please make sure the proper validation rule definition handler is registered");
throw new ValidationConfigurationException("Could not handler element '" + ruleDefinition.getTagName() + "'");
}
handler.handle(ruleDefinition, configuration);
}
}
protected void handleMethodDefinition(Element methodDefinition, Class clazz, MutableBeanValidationConfiguration configuration) {
String methodName = methodDefinition.getAttribute(NAME_ATTR);
if (!StringUtils.hasText(methodName)) {
logger.error("Could not parse method element. Missing or empty 'name' attribute");
throw new ValidationConfigurationException("Could not parse method element. Missing 'name' attribute");
}
String errorCode = methodDefinition.getAttribute(CODE_ATTR);
String message = methodDefinition.getAttribute(MESSAGE_ATTR);
String argsString = methodDefinition.getAttribute(ARGS_ATTR);
String conditionString = methodDefinition.getAttribute(APPLY_IF_ATTR);
String propertyName = methodDefinition.getAttribute(FOR_PROPERTY_ATTR);
String contextsString = methodDefinition.getAttribute(CONTEXTS_ATTR);
ValidationMethodValidationRule rule = createMethodValidationRule(
clazz,
methodName,
errorCode,
message,
argsString,
contextsString,
conditionString
);
if (StringUtils.hasText(propertyName)) {
validatePropertyExists(clazz, propertyName);
configuration.addPropertyRule(propertyName, rule);
} else {
configuration.addGlobalRule(rule);
}
}
protected ValidationMethodValidationRule createMethodValidationRule(
Class clazz,
String methodName,
String errorCode,
String message,
String argsString,
String contextsString,
String applyIfString) {
Method method = ReflectionUtils.findMethod(clazz, methodName);
if (method == null) {
throw new ValidationConfigurationException("Method named '" + methodName +
"' was not found in class hierarchy of '" + clazz.getName() + "'.");
}
if (!StringUtils.hasText(errorCode)) {
errorCode = methodName + "()";
}
if (!StringUtils.hasText(message)) {
message = errorCode;
}
if (!StringUtils.hasText(argsString)) {
argsString = "";
}
ErrorArgumentsResolver argsResolver = buildErrorArgumentsResolver(argsString);
Condition applyIfCondition = new AlwaysTrueCondition();
if (StringUtils.hasText(applyIfString)) {
applyIfCondition = conditionExpressionParser.parse(applyIfString);
}
String[] contexts = null;
if (StringUtils.hasText(contextsString)) {
contexts = StringUtils.commaDelimitedListToStringArray(contextsString);
}
ValidationMethodValidationRule rule = new ValidationMethodValidationRule(method);
rule.setErrorCode(errorCode);
rule.setDefaultErrorMessage(message);
rule.setErrorArgumentsResolver(argsResolver);
rule.setApplicabilityCondition(applyIfCondition);
rule.setContextTokens(contexts);
return rule;
}
protected ErrorArgumentsResolver buildErrorArgumentsResolver(String argsString) {
String[] args = StringUtils.tokenizeToStringArray(argsString, ", ");
return new FunctionErrorArgumentsResolver(args, functionExpressionParser);
}
/**
* Handles the given <property> element and updates the given bean validation configuration with the property
* validation rules.
*
* @param propertyDefinition The <property> element.
* @param clazz The validated class.
* @param configuration The bean validation configuration to update.
*/
protected void handlePropertyDefinition(Element propertyDefinition, Class clazz, MutableBeanValidationConfiguration configuration) {
String propertyName = propertyDefinition.getAttribute(NAME_ATTR);
if (!StringUtils.hasText(propertyName)) {
logger.error("Could not parse property element. Missing or empty 'name' attribute");
throw new ValidationConfigurationException("Could not parse property element. Missing 'name' attribute");
}
PropertyDescriptor propertyDescriptor = BeanUtils.getPropertyDescriptor(clazz, propertyName);
if (propertyDescriptor == null) {
logger.error("Property '" + propertyName + "' does not exist in class '" + clazz.getName() + "'");
}
if (propertyDefinition.hasAttribute(CASCADE_ATTR) && "true".equals(propertyDefinition.getAttribute(CASCADE_ATTR)))
{
CascadeValidation cascadeValidation = new CascadeValidation(propertyName);
if (propertyDefinition.hasAttribute(CASCADE_CONDITION_ATTR)) {
String conditionExpression = propertyDefinition.getAttribute(CASCADE_CONDITION_ATTR);
cascadeValidation.setApplicabilityCondition(conditionExpressionParser.parse(conditionExpression));
}
configuration.addCascadeValidation(cascadeValidation);
}
NodeList nodes = propertyDefinition.getChildNodes();
for (int i = 0; i < nodes.getLength(); i++) {
Node node = nodes.item(i);
if (node.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
Element ruleDefinition = (Element) node;
PropertyValidationElementHandler handler = handlerRegistry.findPropertyHandler(ruleDefinition, clazz, propertyDescriptor);
if (handler == null) {
logger.error("Could not handle element '" + ruleDefinition.getTagName() +
"'. Please make sure the proper validation rule definition handler is registered");
throw new ValidationConfigurationException("Could not handle element '" + ruleDefinition.getTagName() + "'");
}
handler.handle(ruleDefinition, propertyName, configuration);
}
}
protected PropertyValidationRule createPropertyRule(String propertyName, ValidationRule rule) {
return new PropertyValidationRule(propertyName, rule);
}
protected Validator constructValidator(String className) {
try {
Class clazz = ClassUtils.forName(className);
if (!Validator.class.isAssignableFrom(clazz)) {
throw new ValidationConfigurationException("class '" + className + "' is not a Validator implementation");
}
return (Validator) clazz.newInstance();
} catch (ClassNotFoundException e) {
throw new ValidationConfigurationException("Could not load validator class '" + className + "'");
} catch (IllegalAccessException e) {
throw new ValidationConfigurationException("Could not instantiate validator '" + className +
"'. Make sure it has a default constructor.");
} catch (InstantiationException e) {
throw new ValidationConfigurationException("Could not instantiate validator '" + className +
"'. Make sure it has a default constructor.");
}
}
protected void findConditionExpressionParserInApplicationContext() {
if (applicationContext == null || conditionParserExplicitlySet) {
return;
}
String[] names = applicationContext.getBeanNamesForType(ConditionExpressionParser.class);
if (names.length == 0) {
return;
}
if (names.length > 1) {
logger.warn("Multiple condition expression parsers are defined in the application context. " +
"Only the first encountered one will be used");
}
conditionExpressionParser = (ConditionExpressionParser) applicationContext.getBean(names[0]);
}
protected void findFunctionExpressionParserInApplicationContext() {
if (applicationContext == null || functionParserExplicitlySet) {
return;
}
String[] names = applicationContext.getBeanNamesForType(FunctionExpressionParser.class);
if (names.length == 0) {
return;
}
if (names.length > 1) {
logger.warn("Multiple function expression parsers are defined in the application context. " +
"Only the first encountered one will be used");
}
functionExpressionParser = (FunctionExpressionParser) applicationContext.getBean(names[0]);
}
protected void validatePropertyExists(Class clazz, String property) {
BeanUtils.getPropertyDescriptor(clazz, property);
}
}