View Javadoc

1   /*
2    * Copyright 2004-2009 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.springmodules.validation.bean.conf.loader.xml;
18  
19  import java.beans.PropertyDescriptor;
20  import java.lang.reflect.Method;
21  import java.util.HashMap;
22  import java.util.Map;
23  
24  import org.slf4j.Logger;
25  import org.slf4j.LoggerFactory;
26  import org.springframework.beans.BeanUtils;
27  import org.springframework.beans.factory.InitializingBean;
28  import org.springframework.context.ApplicationContext;
29  import org.springframework.context.ApplicationContextAware;
30  import org.springframework.util.Assert;
31  import org.springframework.util.ClassUtils;
32  import org.springframework.util.ReflectionUtils;
33  import org.springframework.util.StringUtils;
34  import org.springframework.validation.Validator;
35  import org.springmodules.validation.bean.conf.BeanValidationConfiguration;
36  import org.springmodules.validation.bean.conf.CascadeValidation;
37  import org.springmodules.validation.bean.conf.DefaultBeanValidationConfiguration;
38  import org.springmodules.validation.bean.conf.MutableBeanValidationConfiguration;
39  import org.springmodules.validation.bean.conf.ValidationConfigurationException;
40  import org.springmodules.validation.bean.conf.loader.xml.handler.ClassValidationElementHandler;
41  import org.springmodules.validation.bean.conf.loader.xml.handler.PropertyValidationElementHandler;
42  import org.springmodules.validation.bean.rule.PropertyValidationRule;
43  import org.springmodules.validation.bean.rule.ValidationMethodValidationRule;
44  import org.springmodules.validation.bean.rule.ValidationRule;
45  import org.springmodules.validation.bean.rule.resolver.ErrorArgumentsResolver;
46  import org.springmodules.validation.bean.rule.resolver.FunctionErrorArgumentsResolver;
47  import org.springmodules.validation.util.cel.ConditionExpressionBased;
48  import org.springmodules.validation.util.cel.ConditionExpressionParser;
49  import org.springmodules.validation.util.cel.valang.ValangConditionExpressionParser;
50  import org.springmodules.validation.util.condition.Condition;
51  import org.springmodules.validation.util.condition.common.AlwaysTrueCondition;
52  import org.springmodules.validation.util.fel.FunctionExpressionBased;
53  import org.springmodules.validation.util.fel.FunctionExpressionParser;
54  import org.springmodules.validation.util.fel.parser.ValangFunctionExpressionParser;
55  import org.w3c.dom.Document;
56  import org.w3c.dom.Element;
57  import org.w3c.dom.Node;
58  import org.w3c.dom.NodeList;
59  
60  /**
61   * The default xml bean validation configuration loader. This loader expects the following xml format:
62   * <p/>
63   * <pre>
64   * &lt;validation [package="org.springmodules.validation.sample"]>
65   *      &lt;class name="Person"&gt;
66   *          &lt;global>
67   *              &lt;any/&gt;...
68   *          &lt;/global>
69   *          &lt;property name="firstName" [valid="true|false"]&gt;
70   *              &lt;any/&gt;...
71   *          &lt;/property>
72   *      &lt;/class>
73   * &lt;/validation>
74   * </pre>
75   * <p/>
76   * Please note the following:
77   * <p/>
78   * <ul>
79   * <li>Each &lt;validation&gt; element can contain multiple &lt;class&gt; elements.</li>
80   * <li>
81   * A &lt;class&gt; element can have only on &lt;global&gt; elements and multiple &lt;property&gt; elements. This
82   * elements hold validation rules to be bound globaly to the class instance or to specific properties.
83   * </li>
84   * <li>Both &lt;global&gt; and &lt;property&gt; elements can accept any element where each element represents
85   * a validation rule. These validation rule elements will eventually be evaluated in the order they are defined.
86   * When one of these rules fail, the evaluation stops
87   * </li>
88   * <li>
89   * A &lt;property&gt; may have a 'valid' attribute to indicate that the property value needs to be validated as
90   * well (cascade validation).
91   * </li>
92   * <li>
93   * The &lt;validation&gt; element may have a 'package' attribute. This will serve as a default package for all
94   * &lt;class&gt; elements (meaing there is not need to specify the fully qualified name in the 'name' attribute
95   * of this element.
96   * </li>
97   * <li>
98   * This XML format has a unique namespace which is defined by {@link #DEFAULT_NAMESPACE_URL}.
99   * </li>
100  * </ul>
101  * <p/>
102  * The validation rule element (sub-elements of &lt;global&gt; and &lt;property&gt;) are resolved using
103  * validation rule element handlers. This class holds a registry for such handlers, where new handlers can
104  * be registered as well. The default registry is {@link DefaultValidationRuleElementHandlerRegistry}.
105  *
106  * @author Uri Boness
107  */
108 public class DefaultXmlBeanValidationConfigurationLoader extends AbstractXmlBeanValidationConfigurationLoader
109     implements ConditionExpressionBased, FunctionExpressionBased, ApplicationContextAware {
110 
111     public static final String DEFAULT_NAMESPACE_URL = "http://www.springmodules.org/validation/bean";
112 
113     private final Logger logger = LoggerFactory.getLogger(DefaultXmlBeanValidationConfigurationLoader.class);
114 
115     private static final String CLASS_TAG = "class";
116 
117     private static final String GLOBAL_TAG = "global";
118 
119     private static final String PROPERTY_TAG = "property";
120 
121     private static final String METHOD_TAG = "method";
122 
123     private static final String VALIDATOR_BEAN_TAG = "validator-ref";
124 
125     private static final String VALIDATOR_TAG = "validator";
126 
127     private static final String PACKAGE_ATTR = "package";
128 
129     private static final String NAME_ATTR = "name";
130 
131     private static final String CASCADE_ATTR = "cascade";
132 
133     private static final String CASCADE_CONDITION_ATTR = "cascade-condition";
134 
135     private static final String CLASS_ATTR = "class";
136 
137     private static final String CODE_ATTR = "code";
138 
139     private static final String MESSAGE_ATTR = "message";
140 
141     private static final String ARGS_ATTR = "args";
142 
143     private static final String APPLY_IF_ATTR = "apply-if";
144 
145     private static final String CONTEXTS_ATTR = "contexts";
146 
147     private static final String FOR_PROPERTY_ATTR = "for-property";
148 
149     private ValidationRuleElementHandlerRegistry handlerRegistry;
150 
151     private boolean conditionParserExplicitlySet = false;
152 
153     private ConditionExpressionParser conditionExpressionParser;
154 
155     private boolean functionParserExplicitlySet = false;
156 
157     private FunctionExpressionParser functionExpressionParser;
158 
159     private ApplicationContext applicationContext;
160 
161     /**
162      * Constructs a new DefaultXmlBeanValidationConfigurationLoader with the default validation rule
163      * element handler registry.
164      */
165     public DefaultXmlBeanValidationConfigurationLoader() {
166         this(new DefaultValidationRuleElementHandlerRegistry());
167     }
168 
169     /**
170      * Constructs a new DefaultXmlBeanValidationConfigurationLoader with the given validation rule
171      * element handler registry.
172      *
173      * @param handlerRegistry The validation rule element handler registry that will be used by this loader.
174      */
175     public DefaultXmlBeanValidationConfigurationLoader(ValidationRuleElementHandlerRegistry handlerRegistry) {
176         this(handlerRegistry, new ValangConditionExpressionParser(), new ValangFunctionExpressionParser());
177     }
178 
179     /**
180      * Constructs a new DefaultXmlBeanValidationConfigurationLoader with the given validation rule
181      * element handler registry.
182      *
183      * @param handlerRegistry The validation rule element handler registry that will be used by this loader.
184      * @param conditionExpressionParser The condition parser this loader should use to parse the cascade validation conditions.
185      * @param functionExpressionParser The function parser this loader should use to parse the error arguments.
186      */
187     public DefaultXmlBeanValidationConfigurationLoader(
188         ValidationRuleElementHandlerRegistry handlerRegistry,
189         ConditionExpressionParser conditionExpressionParser,
190         FunctionExpressionParser functionExpressionParser) {
191 
192         this.handlerRegistry = handlerRegistry;
193         this.conditionExpressionParser = conditionExpressionParser;
194         this.functionExpressionParser = functionExpressionParser;
195     }
196 
197     /**
198      * Loads the validation configuration from the given document that was created from the given resource.
199      *
200      * @see AbstractXmlBeanValidationConfigurationLoader#loadConfigurations(org.w3c.dom.Document, String)
201      */
202     protected Map loadConfigurations(Document document, String resourceName) {
203         Map configurations = new HashMap();
204         Element validationDefinition = document.getDocumentElement();
205         String packageName = validationDefinition.getAttribute(PACKAGE_ATTR);
206         NodeList nodes = validationDefinition.getElementsByTagNameNS(DEFAULT_NAMESPACE_URL, CLASS_TAG);
207         for (int i = 0; i < nodes.getLength(); i++) {
208             Element classDefinition = (Element) nodes.item(i);
209             String className = classDefinition.getAttribute(NAME_ATTR);
210             className = (StringUtils.hasLength(packageName)) ? packageName + "." + className : className;
211             Class clazz;
212             try {
213                 clazz = ClassUtils.forName(className);
214             } catch (ClassNotFoundException cnfe) {
215                 logger.error("Could not load class '" + className + "' as defined in '" + resourceName + "'", cnfe);
216                 continue;
217             }
218             configurations.put(clazz, handleClassDefinition(clazz, classDefinition));
219         }
220         return configurations;
221     }
222 
223     /**
224      * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
225      */
226     public void afterPropertiesSet() throws Exception {
227         initContext(handlerRegistry);
228         super.afterPropertiesSet();
229         findConditionExpressionParserInApplicationContext();
230         findFunctionExpressionParserInApplicationContext();
231         Assert.notNull(conditionExpressionParser);
232         Assert.notNull(functionExpressionParser);
233     }
234 
235     //=============================================== Setter/Getter ====================================================
236 
237     /**
238      * Sets the element handler registry this loader will use to fetch the handlers while loading
239      * validation configuration.
240      *
241      * @param registry The element handler registry to be used by this loader.
242      */
243     public void setElementHandlerRegistry(ValidationRuleElementHandlerRegistry registry) {
244         this.handlerRegistry = registry;
245     }
246 
247     /**
248      * Returns the element handler registry used by this loader.
249      *
250      * @return The element handler registry used by this loader.
251      */
252     public ValidationRuleElementHandlerRegistry getElementHandlerRegistry() {
253         return handlerRegistry;
254     }
255 
256     /**
257      * @see ConditionExpressionBased#setConditionExpressionParser(org.springmodules.validation.util.cel.ConditionExpressionParser)
258      */
259     public void setConditionExpressionParser(ConditionExpressionParser conditionExpressionParser) {
260         this.conditionParserExplicitlySet = true;
261         this.conditionExpressionParser = conditionExpressionParser;
262     }
263 
264     /**
265      * @see FunctionExpressionBased#setFunctionExpressionParser(org.springmodules.validation.util.fel.FunctionExpressionParser)
266      */
267     public void setFunctionExpressionParser(FunctionExpressionParser functionExpressionParser) {
268         this.functionParserExplicitlySet = true;
269         this.functionExpressionParser = functionExpressionParser;
270     }
271 
272     /**
273      * @see ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext)
274      */
275     public void setApplicationContext(ApplicationContext applicationContext) {
276         this.applicationContext = applicationContext;
277     }
278 
279     //=============================================== Helper Methods ===================================================
280 
281     protected void initContext(Object object) throws Exception {
282         if (object instanceof ApplicationContextAware) {
283             ((ApplicationContextAware) object).setApplicationContext(applicationContext);
284         }
285         if (object instanceof InitializingBean) {
286             ((InitializingBean) object).afterPropertiesSet();
287         }
288     }
289 
290     /**
291      * Creates and builds a bean validation configuration based for the given class, based on the given &lt;class&gt;
292      * element.
293      *
294      * @param element The &lt;class&gt; element.
295      * @param clazz The class for which the validation configuration is being loaded.
296      * @return The created bean validation configuration.
297      */
298     public BeanValidationConfiguration handleClassDefinition(Class clazz, Element element) {
299 
300         DefaultBeanValidationConfiguration configuration = new DefaultBeanValidationConfiguration();
301 
302         NodeList nodes = element.getElementsByTagNameNS(DEFAULT_NAMESPACE_URL, VALIDATOR_TAG);
303         for (int i = 0; i < nodes.getLength(); i++) {
304             Element validatorDefinition = (Element) nodes.item(i);
305             handleValidatorDefinition(validatorDefinition, clazz, configuration);
306         }
307 
308         nodes = element.getElementsByTagNameNS(DEFAULT_NAMESPACE_URL, VALIDATOR_BEAN_TAG);
309         for (int i = 0; i < nodes.getLength(); i++) {
310             Element validatorBeanDefinition = (Element) nodes.item(i);
311             handleValidatorBeanDefinition(validatorBeanDefinition, clazz, configuration);
312         }
313 
314         nodes = element.getElementsByTagNameNS(DEFAULT_NAMESPACE_URL, GLOBAL_TAG);
315         for (int i = 0; i < nodes.getLength(); i++) {
316             Element globalDefinition = (Element) nodes.item(i);
317             handleGlobalDefinition(globalDefinition, clazz, configuration);
318         }
319 
320         nodes = element.getElementsByTagNameNS(DEFAULT_NAMESPACE_URL, METHOD_TAG);
321         for (int i = 0; i < nodes.getLength(); i++) {
322             Element methodDefinition = (Element) nodes.item(i);
323             handleMethodDefinition(methodDefinition, clazz, configuration);
324         }
325 
326         nodes = element.getElementsByTagNameNS(DEFAULT_NAMESPACE_URL, PROPERTY_TAG);
327         for (int i = 0; i < nodes.getLength(); i++) {
328             Element propertyDefinition = (Element) nodes.item(i);
329             handlePropertyDefinition(propertyDefinition, clazz, configuration);
330         }
331 
332         return configuration;
333     }
334 
335     protected void handleValidatorDefinition(Element validatorDefinition, Class clazz, MutableBeanValidationConfiguration configuration) {
336         String className = validatorDefinition.getAttribute(CLASS_ATTR);
337         configuration.addCustomValidator(constructValidator(className));
338     }
339 
340     protected void handleValidatorBeanDefinition(Element definition, Class clazz, MutableBeanValidationConfiguration configuration) {
341         if (applicationContext == null) {
342             throw new UnsupportedOperationException(VALIDATOR_BEAN_TAG + " configuration cannot be applied for " +
343                 "this configuration loader was not deployed in an application context");
344         }
345         String beanName = definition.getAttribute(NAME_ATTR);
346         Validator validator = (Validator)applicationContext.getBean(beanName, Validator.class);
347         configuration.addCustomValidator(validator);
348     }
349 
350     /**
351      * Handles the &lt;global&gt; element and updates the given configuration with the global validation rules.
352      *
353      * @param globalDefinition The &lt;global&gt; element.
354      * @param clazz The validated class.
355      * @param configuration The bean validation configuration to update.
356      */
357     protected void handleGlobalDefinition(Element globalDefinition, Class clazz, MutableBeanValidationConfiguration configuration) {
358         NodeList nodes = globalDefinition.getChildNodes();
359         for (int i = 0; i < nodes.getLength(); i++) {
360             Node node = nodes.item(i);
361             if (node.getNodeType() != Node.ELEMENT_NODE) {
362                 continue;
363             }
364             Element ruleDefinition = (Element) node;
365             ClassValidationElementHandler handler = handlerRegistry.findClassHandler(ruleDefinition, clazz);
366             if (handler == null) {
367                 logger.error("Could not handle element '" + ruleDefinition.getTagName() +
368                     "'. Please make sure the proper validation rule definition handler is registered");
369                 throw new ValidationConfigurationException("Could not handler element '" + ruleDefinition.getTagName() + "'");
370             }
371             handler.handle(ruleDefinition, configuration);
372         }
373     }
374 
375     protected void handleMethodDefinition(Element methodDefinition, Class clazz, MutableBeanValidationConfiguration configuration) {
376         String methodName = methodDefinition.getAttribute(NAME_ATTR);
377         if (!StringUtils.hasText(methodName)) {
378             logger.error("Could not parse method element. Missing or empty 'name' attribute");
379             throw new ValidationConfigurationException("Could not parse method element. Missing 'name' attribute");
380         }
381 
382         String errorCode = methodDefinition.getAttribute(CODE_ATTR);
383         String message = methodDefinition.getAttribute(MESSAGE_ATTR);
384         String argsString = methodDefinition.getAttribute(ARGS_ATTR);
385         String conditionString = methodDefinition.getAttribute(APPLY_IF_ATTR);
386         String propertyName = methodDefinition.getAttribute(FOR_PROPERTY_ATTR);
387         String contextsString = methodDefinition.getAttribute(CONTEXTS_ATTR);
388 
389         ValidationMethodValidationRule rule = createMethodValidationRule(
390             clazz,
391             methodName,
392             errorCode,
393             message,
394             argsString,
395             contextsString,
396             conditionString
397         );
398 
399         if (StringUtils.hasText(propertyName)) {
400             validatePropertyExists(clazz, propertyName);
401             configuration.addPropertyRule(propertyName, rule);
402         } else {
403             configuration.addGlobalRule(rule);
404         }
405     }
406 
407     protected ValidationMethodValidationRule createMethodValidationRule(
408         Class clazz,
409         String methodName,
410         String errorCode,
411         String message,
412         String argsString,
413         String contextsString,
414         String applyIfString) {
415 
416         Method method = ReflectionUtils.findMethod(clazz, methodName);
417         if (method == null) {
418             throw new ValidationConfigurationException("Method named '" + methodName +
419                 "' was not found in class hierarchy of '" + clazz.getName() + "'.");
420         }
421 
422         if (!StringUtils.hasText(errorCode)) {
423             errorCode = methodName + "()";
424         }
425         if (!StringUtils.hasText(message)) {
426             message = errorCode;
427         }
428         if (!StringUtils.hasText(argsString)) {
429             argsString = "";
430         }
431         ErrorArgumentsResolver argsResolver = buildErrorArgumentsResolver(argsString);
432         Condition applyIfCondition = new AlwaysTrueCondition();
433         if (StringUtils.hasText(applyIfString)) {
434             applyIfCondition = conditionExpressionParser.parse(applyIfString);
435         }
436 
437         String[] contexts = null;
438         if (StringUtils.hasText(contextsString)) {
439             contexts = StringUtils.commaDelimitedListToStringArray(contextsString);
440         }
441 
442         ValidationMethodValidationRule rule = new ValidationMethodValidationRule(method);
443         rule.setErrorCode(errorCode);
444         rule.setDefaultErrorMessage(message);
445         rule.setErrorArgumentsResolver(argsResolver);
446         rule.setApplicabilityCondition(applyIfCondition);
447         rule.setContextTokens(contexts);
448 
449         return rule;
450     }
451 
452     protected ErrorArgumentsResolver buildErrorArgumentsResolver(String argsString) {
453         String[] args = StringUtils.tokenizeToStringArray(argsString, ", ");
454         return new FunctionErrorArgumentsResolver(args, functionExpressionParser);
455     }
456 
457 
458     /**
459      * Handles the given &lt;property&gt; element and updates the given bean validation configuration with the property
460      * validation rules.
461      *
462      * @param propertyDefinition The &lt;property&gt; element.
463      * @param clazz The validated class.
464      * @param configuration The bean validation configuration to update.
465      */
466     protected void handlePropertyDefinition(Element propertyDefinition, Class clazz, MutableBeanValidationConfiguration configuration) {
467         String propertyName = propertyDefinition.getAttribute(NAME_ATTR);
468         if (!StringUtils.hasText(propertyName)) {
469             logger.error("Could not parse property element. Missing or empty 'name' attribute");
470             throw new ValidationConfigurationException("Could not parse property element. Missing 'name' attribute");
471         }
472 
473         PropertyDescriptor propertyDescriptor = BeanUtils.getPropertyDescriptor(clazz, propertyName);
474         if (propertyDescriptor == null) {
475             logger.error("Property '" + propertyName + "' does not exist in class '" + clazz.getName() + "'");
476         }
477 
478         if (propertyDefinition.hasAttribute(CASCADE_ATTR) && "true".equals(propertyDefinition.getAttribute(CASCADE_ATTR)))
479         {
480             CascadeValidation cascadeValidation = new CascadeValidation(propertyName);
481             if (propertyDefinition.hasAttribute(CASCADE_CONDITION_ATTR)) {
482                 String conditionExpression = propertyDefinition.getAttribute(CASCADE_CONDITION_ATTR);
483                 cascadeValidation.setApplicabilityCondition(conditionExpressionParser.parse(conditionExpression));
484             }
485             configuration.addCascadeValidation(cascadeValidation);
486         }
487 
488         NodeList nodes = propertyDefinition.getChildNodes();
489         for (int i = 0; i < nodes.getLength(); i++) {
490             Node node = nodes.item(i);
491             if (node.getNodeType() != Node.ELEMENT_NODE) {
492                 continue;
493             }
494             Element ruleDefinition = (Element) node;
495             PropertyValidationElementHandler handler = handlerRegistry.findPropertyHandler(ruleDefinition, clazz, propertyDescriptor);
496             if (handler == null) {
497                 logger.error("Could not handle element '" + ruleDefinition.getTagName() +
498                     "'. Please make sure the proper validation rule definition handler is registered");
499                 throw new ValidationConfigurationException("Could not handle element '" + ruleDefinition.getTagName() + "'");
500             }
501             handler.handle(ruleDefinition, propertyName, configuration);
502         }
503     }
504 
505     protected PropertyValidationRule createPropertyRule(String propertyName, ValidationRule rule) {
506         return new PropertyValidationRule(propertyName, rule);
507     }
508 
509     protected Validator constructValidator(String className) {
510         try {
511             Class clazz = ClassUtils.forName(className);
512             if (!Validator.class.isAssignableFrom(clazz)) {
513                 throw new ValidationConfigurationException("class '" + className + "' is not a Validator implementation");
514             }
515             return (Validator) clazz.newInstance();
516         } catch (ClassNotFoundException e) {
517             throw new ValidationConfigurationException("Could not load validator class '" + className + "'");
518         } catch (IllegalAccessException e) {
519             throw new ValidationConfigurationException("Could not instantiate validator '" + className +
520                 "'. Make sure it has a default constructor.");
521         } catch (InstantiationException e) {
522             throw new ValidationConfigurationException("Could not instantiate validator '" + className +
523                 "'. Make sure it has a default constructor.");
524         }
525     }
526 
527     protected void findConditionExpressionParserInApplicationContext() {
528         if (applicationContext == null || conditionParserExplicitlySet) {
529             return;
530         }
531         String[] names = applicationContext.getBeanNamesForType(ConditionExpressionParser.class);
532         if (names.length == 0) {
533             return;
534         }
535         if (names.length > 1) {
536             logger.warn("Multiple condition expression parsers are defined in the application context. " +
537                 "Only the first encountered one will be used");
538         }
539         conditionExpressionParser = (ConditionExpressionParser) applicationContext.getBean(names[0]);
540     }
541 
542     protected void findFunctionExpressionParserInApplicationContext() {
543         if (applicationContext == null || functionParserExplicitlySet) {
544             return;
545         }
546         String[] names = applicationContext.getBeanNamesForType(FunctionExpressionParser.class);
547         if (names.length == 0) {
548             return;
549         }
550         if (names.length > 1) {
551             logger.warn("Multiple function expression parsers are defined in the application context. " +
552                 "Only the first encountered one will be used");
553         }
554         functionExpressionParser = (FunctionExpressionParser) applicationContext.getBean(names[0]);
555     }
556 
557     protected void validatePropertyExists(Class clazz, String property) {
558         BeanUtils.getPropertyDescriptor(clazz, property);
559     }
560 }