1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 * <validation [package="org.springmodules.validation.sample"]>
65 * <class name="Person">
66 * <global>
67 * <any/>...
68 * </global>
69 * <property name="firstName" [valid="true|false"]>
70 * <any/>...
71 * </property>
72 * </class>
73 * </validation>
74 * </pre>
75 * <p/>
76 * Please note the following:
77 * <p/>
78 * <ul>
79 * <li>Each <validation> element can contain multiple <class> elements.</li>
80 * <li>
81 * A <class> element can have only on <global> elements and multiple <property> elements. This
82 * elements hold validation rules to be bound globaly to the class instance or to specific properties.
83 * </li>
84 * <li>Both <global> and <property> 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 <property> 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 <validation> element may have a 'package' attribute. This will serve as a default package for all
94 * <class> 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 <global> and <property>) 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
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
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 <class>
292 * element.
293 *
294 * @param element The <class> 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 <global> element and updates the given configuration with the global validation rules.
352 *
353 * @param globalDefinition The <global> 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 <property> element and updates the given bean validation configuration with the property
460 * validation rules.
461 *
462 * @param propertyDefinition The <property> 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 }