1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.springmodules.validation.bean;
18
19 import java.lang.reflect.Array;
20 import java.util.Collection;
21 import java.util.HashSet;
22 import java.util.Iterator;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Set;
26 import java.util.Map.Entry;
27
28 import org.slf4j.Logger;
29 import org.slf4j.LoggerFactory;
30 import org.springframework.beans.BeanWrapper;
31 import org.springframework.beans.BeanWrapperImpl;
32 import org.springframework.util.StringUtils;
33 import org.springframework.validation.Errors;
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.loader.BeanValidationConfigurationLoader;
38 import org.springmodules.validation.bean.conf.loader.xml.DefaultXmlBeanValidationConfigurationLoader;
39 import org.springmodules.validation.bean.converter.ErrorCodeConverter;
40 import org.springmodules.validation.bean.converter.ModelAwareErrorCodeConverter;
41 import org.springmodules.validation.bean.rule.ValidationRule;
42 import org.springmodules.validation.util.condition.Condition;
43
44 /**
45 * An {@link org.springmodules.validation.validator.AbstractTypeSpecificValidator} implementation that applies all validation rules
46 * on a bean of a specific type, based on an appropriate {@link org.springmodules.validation.bean.conf.BeanValidationConfiguration}. The validation
47 * configuration is loaded per bean type by the configured {@link BeanValidationConfigurationLoader}.
48 *
49 * @author Uri Boness
50 */
51 public class BeanValidator extends RuleBasedValidator {
52
53 private final static Logger logger = LoggerFactory.getLogger(BeanValidator.class);
54
55 private final static String PROPERTY_KEY_PREFIX = "[";
56
57 private final static String PROPERTY_KEY_SUFFIX = "]";
58
59 private BeanValidationConfigurationLoader configurationLoader;
60
61 private ErrorCodeConverter errorCodeConverter;
62
63 private boolean shortCircuitFieldValidation = true;
64
65 /**
66 * Constructs a new BeanValidator. By default the
67 * {@link org.springmodules.validation.bean.conf.loader.SimpleBeanValidationConfigurationLoader} is
68 * used as the bean validation configuration loader.
69 */
70 public BeanValidator() {
71 this(new DefaultXmlBeanValidationConfigurationLoader());
72 }
73
74 /**
75 * Constructs a new BeanValidator for the given bean class using the given validation configuration loader.
76 *
77 * @param configurationLoader The {@link org.springmodules.validation.bean.conf.loader.BeanValidationConfigurationLoader} that is used to load the bean validation
78 * configuration.
79 */
80 public BeanValidator(BeanValidationConfigurationLoader configurationLoader) {
81 this.configurationLoader = configurationLoader;
82 errorCodeConverter = new ModelAwareErrorCodeConverter();
83 }
84
85 /**
86 * This validator supports only those classes that are supported by the validation configuration loader it uses.
87 *
88 * @see org.springmodules.validation.bean.RuleBasedValidator#supports(Class)
89 * @see org.springmodules.validation.bean.conf.loader.BeanValidationConfigurationLoader#supports(Class)
90 */
91 public boolean supports(Class clazz) {
92 return configurationLoader.supports(clazz) || super.supports(clazz);
93 }
94
95 /**
96 * Applies all validation rules as defined in the {@link org.springmodules.validation.bean.conf.BeanValidationConfiguration} retrieved for the given
97 * bean from the configured {@link org.springmodules.validation.bean.conf.loader.BeanValidationConfigurationLoader}.
98 *
99 * @see Validator#validate(Object, org.springframework.validation.Errors)
100 */
101 public void validate(Object obj, Errors errors) {
102
103
104 validateObjectGraphConstraints(obj, obj, errors, new HashSet());
105
106
107 super.validate(obj, errors);
108 }
109
110
111
112 /**
113 * Sets the error code converter this validator will use to resolve the error codes to be registered with the
114 * {@link Errors} object.
115 *
116 * @param errorCodeConverter The error code converter this validator will use to resolve the error
117 * different error codes.
118 */
119 public void setErrorCodeConverter(ErrorCodeConverter errorCodeConverter) {
120 this.errorCodeConverter = errorCodeConverter;
121 }
122
123 /**
124 * Sets the bean validation configuration loader this validator will use to load the bean validation configurations.
125 *
126 * @param configurationLoader The loader this validator will use to load the bean validation configurations.
127 */
128 public void setConfigurationLoader(BeanValidationConfigurationLoader configurationLoader) {
129 this.configurationLoader = configurationLoader;
130 }
131
132 /**
133 * Determines whether field validation will be short-ciruite, that is, if multiple validation rules are defined
134 * on a field, the first rule to fail will stop the validation process for that field. By default the field
135 * validation <b>will</b> be short-circuited.
136 *
137 * @param shortCircuitFieldValidation
138 */
139 public void setShortCircuitFieldValidation(boolean shortCircuitFieldValidation) {
140 this.shortCircuitFieldValidation = shortCircuitFieldValidation;
141 }
142
143
144
145 /**
146 * The heart of this validator. This is a recursive method that validates the given object (object) under the
147 * context of the given object graph root (root). The validation rules to be applied are loaded using the
148 * configured {@link org.springmodules.validation.bean.conf.loader.BeanValidationConfigurationLoader}. All errors are registered with the given {@link Errors}
149 * object under the context of the object graph root.
150 *
151 * @param root The root of the object graph.
152 * @param obj The object to be validated
153 * @param errors The {@link Errors} instance where the validation errors will be registered.
154 * @param validatedObjects keeps track of all the validated objects (to prevent revalidating the same objects when
155 * a circular relationship exists).
156 */
157 protected void validateObjectGraphConstraints(Object root, Object obj, Errors errors, Set validatedObjects) {
158
159
160 if (obj == null) {
161 return;
162 }
163
164
165 if (validatedObjects.contains(obj)) {
166 if (logger.isDebugEnabled()) {
167 logger.debug("Skipping validation of object in path '" + errors.getObjectName() +
168 "' for it was already validated");
169 }
170 return;
171 }
172
173 if (logger.isDebugEnabled()) {
174 logger.debug("Validating object in path '" + errors.getNestedPath() + "'");
175 }
176
177
178 Class clazz = obj.getClass();
179 BeanValidationConfiguration configuration = configurationLoader.loadConfiguration(clazz);
180
181 if (configuration == null) {
182 return;
183 }
184
185
186 applyBeanValidation(configuration, obj, errors);
187 validatedObjects.add(obj);
188
189
190
191 CascadeValidation[] cascadeValidations = configuration.getCascadeValidations();
192 BeanWrapper wrapper = wrapBean(obj);
193 for (int i = 0; i < cascadeValidations.length; i++) {
194 CascadeValidation cascadeValidation = cascadeValidations[i];
195 Condition applicabilityCondition = cascadeValidation.getApplicabilityCondition();
196
197 if (!applicabilityCondition.check(obj)) {
198 continue;
199 }
200
201 String propertyName = cascadeValidation.getPropertyName();
202 Class propertyType = wrapper.getPropertyType(propertyName);
203 Object propertyValue = wrapper.getPropertyValue(propertyName);
204
205
206 if (propertyValue == null) {
207 continue;
208 }
209
210
211
212
213
214
215 if (propertyType.isArray()) {
216 validateArrayProperty(root, propertyValue, propertyName, errors, validatedObjects);
217 } else if (List.class.isAssignableFrom(propertyType) || Set.class.isAssignableFrom(propertyType)) {
218 validateListOrSetProperty(root, (Collection) propertyValue, propertyName, errors, validatedObjects);
219 } else if (Map.class.isAssignableFrom(propertyType)) {
220 validateMapProperty(root, ((Map) propertyValue), propertyName, errors, validatedObjects);
221 } else {
222
223
224 validatedSubBean(root, propertyValue, propertyName, errors, validatedObjects);
225 }
226 }
227 }
228
229 /**
230 * Wraps the given bean in a {@link BeanWrapper}.
231 *
232 * @param bean The bean to be wraped.
233 * @return The bean wrapper that wraps the given bean.
234 */
235 protected BeanWrapper wrapBean(Object bean) {
236 return new BeanWrapperImpl(bean);
237 }
238
239 /**
240 * Validates the elements of the given array property.
241 *
242 * @param root The root of the object graph that is being validated.
243 * @param array The given array.
244 * @param propertyName The name of the array property.
245 * @param errors The {@link Errors} instance where all validation errors will be registered.
246 * @param validatedObjects A registry of all objects that were already validated.
247 */
248 protected void validateArrayProperty(
249 Object root,
250 Object array,
251 String propertyName,
252 Errors errors,
253 Set validatedObjects) {
254
255 for (int i = 0; i < Array.getLength(array); i++) {
256 String nestedPath = propertyName + PROPERTY_KEY_PREFIX + i + PROPERTY_KEY_SUFFIX;
257 errors.pushNestedPath(nestedPath);
258 validateObjectGraphConstraints(root, Array.get(array, i), errors, validatedObjects);
259 errors.popNestedPath();
260 }
261 }
262
263 /**
264 * Validates the elements of the given list or set property.
265 *
266 * @param root The root of the object graph that is being validated.
267 * @param collection The given list or set.
268 * @param propertyName The name of the array property.
269 * @param errors The {@link Errors} instance where all validation errors will be registered.
270 * @param validatedObjects A registry of all objects that were already validated.
271 */
272 protected void validateListOrSetProperty(
273 Object root,
274 Collection collection,
275 String propertyName,
276 Errors errors,
277 Set validatedObjects) {
278
279 int i = 0;
280 for (Iterator iter = collection.iterator(); iter.hasNext();) {
281 Object element = iter.next();
282 String nestedPath = propertyName + PROPERTY_KEY_PREFIX + i + PROPERTY_KEY_SUFFIX;
283 errors.pushNestedPath(nestedPath);
284 validateObjectGraphConstraints(root, element, errors, validatedObjects);
285 errors.popNestedPath();
286 i++;
287 }
288 }
289
290 /**
291 * Validates the elements within the given map property.
292 *
293 * @param root The root of the object graph that is being validated.
294 * @param map The given map or set.
295 * @param propertyName The name of the array property.
296 * @param errors The {@link Errors} instance where all validation errors will be registered.
297 * @param validatedObjects A registry of all objects that were already validated.
298 */
299 protected void validateMapProperty(Object root, Map map, String propertyName, Errors errors, Set validatedObjects) {
300 for (Iterator entries = map.entrySet().iterator(); entries.hasNext();) {
301 Entry entry = (Entry) entries.next();
302 Object key = entry.getKey();
303 if (!(key instanceof String)) {
304
305
306 continue;
307 }
308 Object value = entry.getValue();
309 String nestedPath = propertyName + PROPERTY_KEY_PREFIX + String.valueOf(key) + PROPERTY_KEY_SUFFIX;
310 errors.pushNestedPath(nestedPath);
311 validateObjectGraphConstraints(root, value, errors, validatedObjects);
312 errors.popNestedPath();
313 }
314 }
315
316 /**
317 * Validates the given nested property bean (sub-bean).
318 *
319 * @param root The root of the object graph that is being validated.
320 * @param subBean The given nested property value (the sub-bean).
321 * @param propertyName The name of the array property.
322 * @param errors The {@link Errors} instance where all validation errors will be registered.
323 * @param validatedObjects A registry of all objects that were already validated.
324 */
325 protected void validatedSubBean(
326 Object root,
327 Object subBean,
328 String propertyName,
329 Errors errors,
330 Set validatedObjects) {
331
332 errors.pushNestedPath(propertyName);
333 validateObjectGraphConstraints(root, subBean, errors, validatedObjects);
334 errors.popNestedPath();
335 }
336
337 /**
338 * Applying the validation rules listed in the given validation configuration on the given object, and registering
339 * all validation errors with the given {@link Errors} object.
340 *
341 * @param configuration The bean validation configuration that define the validation rules to be applied.
342 * @param obj The validated object.
343 * @param errors The {@link Errors} instance where the validation error will be registered.
344 */
345 protected void applyBeanValidation(BeanValidationConfiguration configuration, Object obj, Errors errors) {
346 if (logger.isDebugEnabled()) {
347 logger.debug("Validating global rules...");
348 }
349 applyGlobalValidationRules(configuration, obj, errors);
350
351 if (logger.isDebugEnabled()) {
352 logger.debug("Validating properties rules...");
353 }
354 applyPropertiesValidationRules(configuration, obj, errors);
355
356 if (logger.isDebugEnabled()) {
357 logger.debug("Executing custom validator...");
358 }
359 applyCustomValidator(configuration, obj, errors);
360 }
361
362 /**
363 * Applies the global validation rules as listed in the given validation configuration on the given object, and
364 * registering all global validation errors with the given {@link Errors}.
365 *
366 * @param configuration The bean validation configuration that holds all the global validation rules.
367 * @param obj The validated object.
368 * @param errors The {@link Errors} instance where all global validation errors will be registered.
369 */
370 protected void applyGlobalValidationRules(BeanValidationConfiguration configuration, Object obj, Errors errors) {
371 ValidationRule[] globalRules = configuration.getGlobalRules();
372 for (int i = 0; i < globalRules.length; i++) {
373 ValidationRule rule = globalRules[i];
374 if (rule.isApplicable(obj) && !rule.getCondition().check(obj)) {
375 String errorCode = errorCodeConverter.convertGlobalErrorCode(rule.getErrorCode(), obj.getClass());
376
377
378
379
380
381
382 if (StringUtils.hasLength(errors.getNestedPath())) {
383 String nestedPath = errors.getNestedPath();
384 String propertyName = nestedPath.substring(0, nestedPath.length() - 1);
385 errors.popNestedPath();
386 errors.rejectValue(propertyName, errorCode, rule.getErrorArguments(obj), rule.getDefaultErrorMessage());
387 errors.pushNestedPath(propertyName);
388 } else {
389 errors.reject(errorCode, rule.getErrorArguments(obj), rule.getDefaultErrorMessage());
390 }
391 }
392 }
393 }
394
395 /**
396 * Applies the property validation rules as listed in the given validation configuration on the given object, and
397 * registering all property validation errors with the given {@link Errors}.
398 *
399 * @param configuration The bean validation configuration that holds all the property validation rules.
400 * @param obj The validated object.
401 * @param errors The {@link Errors} instance where all property validation errors will be registered
402 * (see {@link Errors#rejectValue(String, String)}).
403 */
404 protected void applyPropertiesValidationRules(BeanValidationConfiguration configuration, Object obj, Errors errors) {
405 String[] propertyNames = configuration.getValidatedProperties();
406 for (int i = 0; i < propertyNames.length; i++) {
407 String propertyName = propertyNames[i];
408 if (logger.isDebugEnabled()) {
409 logger.debug("Validating property '" + propertyName + "' rules...");
410 }
411 ValidationRule[] rules = configuration.getPropertyRules(propertyName);
412
413
414
415
416
417 validateAndShortCircuitRules(rules, propertyName, obj, errors);
418 }
419 }
420
421 /**
422 * Applying the given validation rules on the given property of the given object. The validation stops as soon as
423 * one of the validation rules produces validation errors. This errors are then registered with the given
424 * {@link Errors) instance.
425 *
426 * @param rules The validation rules that should be applied on the given property of the given object.
427 * @param propertyName The name of the property to be validated.
428 * @param obj The validated object.
429 * @param errors The {@link Errors} instance where the validation errors will be registered.
430 */
431 protected void validateAndShortCircuitRules(ValidationRule[] rules, String propertyName, Object obj, Errors errors) {
432 for (int i = 0; i < rules.length; i++) {
433 ValidationRule rule = rules[i];
434 if (rule.isApplicable(obj) && !rule.getCondition().check(obj)) {
435 String errorCode = errorCodeConverter.convertPropertyErrorCode(rule.getErrorCode(), obj.getClass(), propertyName);
436 errors.rejectValue(propertyName, errorCode, rule.getErrorArguments(obj), rule.getDefaultErrorMessage());
437 if (shortCircuitFieldValidation) {
438 return;
439 }
440 }
441 }
442 }
443
444 /**
445 * Applies the custom validator of the given configuration (if one exists) on the given object.
446 *
447 * @param configuration The configuration from which the custom validator will be taken from.
448 * @param obj The object to be validated.
449 * @param errors The {@link Errors} instance where all validation errors will be registered.
450 */
451 protected void applyCustomValidator(BeanValidationConfiguration configuration, Object obj, Errors errors) {
452 Validator validator = configuration.getCustomValidator();
453 if (validator != null) {
454 if (validator.supports(obj.getClass())) {
455 validator.validate(obj, errors);
456 }
457 }
458 }
459
460 }