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;
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         // validation the object graph using the class validation manager.
104         validateObjectGraphConstraints(obj, obj, errors, new HashSet());
105 
106         // applying the registered validation rules.
107         super.validate(obj, errors);
108     }
109 
110     //============================================== Setter/Getter =====================================================
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     //=============================================== Helper Methods ===================================================
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         // cannot load any validation rules for null values
160         if (obj == null) {
161             return;
162         }
163 
164         // if this object was already validated, the skipping this valiation.
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         // loading the bean validation configuration based on the validated object class.
178         Class clazz = obj.getClass();
179         BeanValidationConfiguration configuration = configurationLoader.loadConfiguration(clazz);
180 
181         if (configuration == null) {
182             return; // no validation configuration for this object, then there's nothing to validate.
183         }
184 
185         // applying all the validation rules for the object and registering the object as "validated"
186         applyBeanValidation(configuration, obj, errors);
187         validatedObjects.add(obj);
188 
189         // after all the validation rules where applied, checking what properties of the object require their own
190         // validation and recursively calling this method on them.
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             // if the property value is not there nothing to validate.
206             if (propertyValue == null) {
207                 continue;
208             }
209 
210             // if the property is an array of a collection, then iterating on it and validating each element. Note that
211             // the error codes that are registered for arrays/collection elements follow the pattern supported by
212             // spring's PropertyAccessor. Also note that just before each recursive call, the context of the validation
213             // is appropriately adjusted using errors.pushNestedPath(...), and after each call it is being adjusted back
214             // using errors.popNestedPath().
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                 // if the object is just a normal object (not an array or a collection), then applying its
223                 // validation rules.
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                 // skipping validation of elements that are mapped to non-string keys for there is no proper
305                 // representation of such elements as property path.
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                 // if there is a nested path in errors, the global errors should be registered as field errors
378                 // for the nested path. Otherwise, they should be registered as global errors. Starting from Spring 2.0-rc2
379                 // this is actually not required - it's just enough to call rejectValue() with null as the field name,
380                 // but we keep this implementation for now to support earlier versions.
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             // only allow one error to be associated with a property at once. This is to prevent situations where
414             // dependent validation rules will fail. An example can be a "minLength()" validation rule that is dependent
415             // on "notNull()" rule (there is not length to a null value), in this case, if the "notNull()" rule
416             // produces an error, the "minLength()" rule should not be applied.
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 }