Announcement Announcement Module
Collapse
No announcement yet.
Ruby on Rails-ish validation for Spring MVC Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • Ruby on Rails-ish validation for Spring MVC

    Here is some code I have been using to do Ruby on Rails-ish style validation in Spring MVC validators

    First, clients look like this:
    Code:
        public void validate(Object command, Errors errors) {
            Job job = (Job) command;
            ValidationUtil.validatePresenceOf(job, "title", errors);
            ValidationUtil.validatePresenceOf(job, "jobType", errors);
            ValidationUtil.validatePresenceOf(job, "description", errors);
            ValidationUtil.validateLengthOf(job, "title", 3, 100, errors);
            ValidationUtil.validateLengthOf(job, "employer", 0, 100, errors);
            ValidationUtil.validateLengthOf(job, "education", 0, 100, errors);        
            ValidationUtil.validateLengthOf(job, "immigrationStatus", 0, 100, errors);        
            ValidationUtil.validateLengthOf(job, "salary", 0, 100, errors);        
            ValidationUtil.validateLengthOf(job, "salaryType", 0, 100, errors);        
            ValidationUtil.validateLengthOf(job, "jobType", 0, 100, errors);        
            ValidationUtil.validateLengthOf(job, "description", 50, 10000, errors);        
            ValidationUtil.validateNumericalityOf(job, "salary", errors);
            ValidationUtil.validateFormatOf(job, "email", ValidationUtil.REGEX_EMAIL, errors);
        }
    Code:
    import java.lang.reflect.Method;
    
    import org.apache.commons.lang.StringUtils;
    import org.apache.commons.lang.math.NumberUtils;
    import org.springframework.validation.Errors;
    
    /**
     * Ruby on Rails style validation for Spring MVC applications
     * 
     * @author John Wheeler
     */
    public class ValidationUtil {
    
        public static final String REGEX_EMAIL = "^([^@\\s]+)@((?:[-a-z0-9]+\\.)+[a-z]{2,})$";
        
        private static final String KEY_REQUIRED    = "error.required";    
        private static final String KEY_MIN_LENGTH  = "error.minlength";    
        private static final String KEY_MAX_LENGTH  = "error.maxlength";    
        private static final String KEY_NUMERIC     = "error.numeric";
        private static final String KEY_FORMAT      = "error.format";
        
        private static final String DEFAULT_MSG_REQUIRED    = "Value required.";
        private static final String DEFAULT_MSG_MIN_LENGTH  = "Value under minimum length.";
        private static final String DEFAULT_MSG_MAX_LENGTH  = "Value exceeds maximum length.";
        private static final String DEFAULT_MSG_NUMERIC     = "Value must be numeric.";
        private static final String DEFAULT_MSG_FORMAT      = "Value is formatted incorrectly.";
        
        /**
         * Ensures the value of the property specified is not blank or null. 
         */
        public static void validatePresenceOf(Object bean, String property, Errors errors) {
            String value = (String) invokeGetter(bean, property);
            if (StringUtils.isBlank(value)) {
                errors.rejectValue(property, 
                        KEY_REQUIRED, 
                        new String[] { humanize(property) }, 
                        DEFAULT_MSG_REQUIRED);
            }
        }
    
        /**
         * Ensures the value of the property specified is greater than <code>min</code> and less than
         * <code>max</code>. 
         */    
        public static void validateLengthOf(Object bean, String property, int min, int max, Errors errors) {
            String value = (String) invokeGetter(bean, property);
            int length = value.length();
            if (length < min) {
                errors.rejectValue(property, 
                        KEY_MIN_LENGTH, 
                        new String[] { humanize(property), ""+min, ""+length }, 
                        DEFAULT_MSG_MIN_LENGTH);            
            } else if (length > max) {
                errors.rejectValue(property, 
                        KEY_MAX_LENGTH, 
                        new String[] { humanize(property), ""+max, ""+length }, 
                        DEFAULT_MSG_MAX_LENGTH);            
            }
        }
        
        /**
         * Ensures the value of the property specified is numeric.
         */        
        public static void validateNumericalityOf(Object bean, String property, Errors errors) {
            String value = (String) invokeGetter(bean, property);        
            if ("".equals(value)) return;        
            if (!NumberUtils.isNumber(value)) {
                errors.rejectValue(property, 
                        KEY_NUMERIC, 
                        new String[] { humanize(property) }, 
                        DEFAULT_MSG_NUMERIC);            
            }
        }
    
        /**
         * Ensures the value of the property specified matches the given regex.
         * This class comes with handy constants that start with <code>REGEX_</code>
         */            
        public static void validateFormatOf(Object bean, String property, String regex, Errors errors) {
            String value = ((String) invokeGetter(bean, property)).toLowerCase();
            if ("".equals(value)) return;
            if (!value.matches(regex)) {
                errors.rejectValue(property,
                        KEY_FORMAT,
                        new String[] { humanize(property) },
                        DEFAULT_MSG_FORMAT);
            }
        }
        
        /**
         * Trys to make a JavaBean property identifier human-readable.
         * <p>
         * e.g. <code>immigrationStatus</code> becomes Immigration status
         * 
         * @param propertyName the property to try and make human-readable
         * @return the human-readable representation of a JavaBean property identifier
         */
        private static String humanize(String propertyName) {
            return StringUtils.capitalize(propertyName.replaceAll("([A-Z]{1})", " $1").toLowerCase());
        }
        
        /**
         * Invokes a "getter" on the supplied <code>bean</code>. Clients are responsible
         * for casting the return value.
         * <p>
         * e.g. <code>String value = (String) invokeGetter(bean, property);</code>
         * 
         * @param bean The bean instance to invoke a getter on
         * @param propertyName The getter's property (don't put 'get' in front of it)
         * @return the result of the getter
         */
        private static Object invokeGetter(Object bean, String propertyName) {
            try {            
                String methodName = "get" + StringUtils.capitalize(propertyName);
                Method method = bean.getClass().getMethod(methodName, new Class[]{});
                return method.invoke(bean, new Object[]{});
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }
    messages.properties
    Code:
    error.required={0} cannot be blank.
    error.minlength={0} must be at least {1} characters (currently {2} characters).
    error.maxlength={0} cannot be more than {1} characters (currently {2} characters).
    error.numeric={0} must be numeric.
    error.format={0} must be formatted correctly.
    is this pretty good, or is there a better utility?
    Last edited by john.wheeler; Jun 3rd, 2006, 03:16 AM.

  • #2
    Very clean design. Some comments:

    (1) I'm not certain of the need to add DEFAULT message strings to your class and using the versions of the errors() methods that provide an argument for defaultMessage[1]. If you accidently forget to add the values to your messages.properties file, I think you would want the application to blow up so you can be aware of the problem and fix it. AFAICT default messages are for when you *really* do not want to add the message string to your messages.properties file (not the case here), you just send NULL for the errorCode and default message for what you want the user to see.

    (2) Also note the Spring ValidationUtils class[2] does not require the form backing object ("job" in your example) as an argument, that it can obtain specific field values from the Errors object. Whether or not that is a good design I am not certain, but at any rate, you may wish to look at the ValidationUtils implementation (can't link to--the Spring ViewCVS appears not to be working right now) and do the same thing for your class. Note that if you do remove the "job" argument from your code, it looks like you can also get rid of the invokeGetter() method in your validation class, because you will again be getting that value automatically from the Errors object.

    (3) Validation of max lengths will not be as common as min lengths, because (1) the input field can normally limit maximum sizes, and (2) you don't want to have to define and maintain maximum lengths in two places--the JSP and in the validation class. So I would recommend also providing separate public validateMinimumLength() and validateMaximumLength() methods, and have validateLengthOf() just call those two methods.

    (4) I think your ValidationUtil.validatePresenceOf() can be replaced with Spring ValidationUtils.rejectIfEmptyOrWhitespace(). I like the regular expression validation and validateMinimum() methods in particular, and wonder if it can be added to Spring's ValidationUtils class (JIRA submission?).


    [1]
    http://static.springframework.org/sp...on/Errors.html
    [2] http://static.springframework.org/sp...tionUtils.html

    Comment


    • #3
      Re: changes

      I've incorporated your excellent suggestions. I've also finalized the class and added a private default constructor so clients can't instantiate it. I got rid of the toLowerCase in validatesFormatOf, and put a case-insensitive embedded flag in REGEX_EMAIL.

      If I take out the DEFAULT_MESSAGE_ fields, it seems like I lose the ability to perform substitutions in my message bundle. The overloaded rejectValue that doesn't take in the defaultMessage doesn't allow you to pass in an array of substitution values. Am I missing the point? Please advise.

      Now client code looks like this:

      Code:
              ValidationUtil.validatePresenceOf("title", errors);
              ValidationUtil.validatePresenceOf("jobType", errors);
              ValidationUtil.validatePresenceOf("description", errors);
              ValidationUtil.validateLengthOf("title", 3, 100, errors);
              ValidationUtil.validateLengthOf("description", 50, 10000, errors);        
              ValidationUtil.validateMaximumLengthOf("employer", 100, errors);
              ValidationUtil.validateMaximumLengthOf("education", 100, errors);        
              ValidationUtil.validateMaximumLengthOf("immigrationStatus", 100, errors);        
              ValidationUtil.validateMaximumLengthOf("salary", 100, errors);        
              ValidationUtil.validateMaximumLengthOf("salaryType", 100, errors);        
              ValidationUtil.validateMaximumLengthOf("jobType", 100, errors);        
              ValidationUtil.validateNumericalityOf("salary", errors);
      ValidationUtil.java
      Code:
      package com.xrecruit.ui.validator;
      
      import org.apache.commons.lang.StringUtils;
      import org.apache.commons.lang.math.NumberUtils;
      import org.springframework.validation.Errors;
      
      /**
       * Ruby on Rails style validation for Spring MVC applications
       * 
       * @author John Wheeler
       */
      public final class ValidationUtil {
      
          public static final String REGEX_EMAIL = "^(?i)([^@\\s]+)@((?:[-a-z0-9]+\\.)+[a-z]{2,})$";
          
          private static final String KEY_REQUIRED    = "error.required";    
          private static final String KEY_MIN_LENGTH  = "error.minlength";    
          private static final String KEY_MAX_LENGTH  = "error.maxlength";    
          private static final String KEY_NUMERIC     = "error.numeric";
          private static final String KEY_FORMAT      = "error.format";
          
          private static final String DEFAULT_MSG_REQUIRED    = "Value required.";
          private static final String DEFAULT_MSG_MIN_LENGTH  = "Value under minimum length.";
          private static final String DEFAULT_MSG_MAX_LENGTH  = "Value exceeds maximum length.";
          private static final String DEFAULT_MSG_NUMERIC     = "Value must be numeric.";
          private static final String DEFAULT_MSG_FORMAT      = "Value is formatted incorrectly.";
          
          private ValidationUtil() {
              throw new UnsupportedOperationException("do not use");
          }
          
          /**
           * Ensures the value of the field specified is not blank or null. 
           */
          public static void validatePresenceOf(String field, Errors errors) {
              String value = (String) errors.getFieldValue(field);
              if (StringUtils.isBlank(value)) {
                  errors.rejectValue(field, 
                          KEY_REQUIRED, 
                          new String[] { humanize(field) }, 
                          DEFAULT_MSG_REQUIRED);
              }
          }
      
          /**
           * Ensures the value of the field specified is greater than <code>min</code> and less than
           * <code>max</code>. 
           */    
          public static void validateLengthOf(String field, int min, int max, Errors errors) {
              validateMinimumLengthOf(field, min, errors);
              validateMaximumLengthOf(field, max, errors);
          }
      
          /**
           * Ensures the value of the field specified is greater than <code>min</code>. 
           */        
          public static void validateMinimumLengthOf(String field, int min, Errors errors) {
              String value = (String) errors.getFieldValue(field);
              int length = value.length();
              if (length < min) {
                  errors.rejectValue(field, 
                          KEY_MIN_LENGTH, 
                          new String[] { humanize(field), ""+min, ""+length }, 
                          DEFAULT_MSG_MIN_LENGTH);
              }
          }
      
          /**
           * Ensures the value of the field specified is less than <code>max</code>. 
           */            
          public static void validateMaximumLengthOf(String field, int max, Errors errors) {
              String value = (String) errors.getFieldValue(field);
              int length = value.length();
              if (length > max) {
                  errors.rejectValue(field, 
                          KEY_MAX_LENGTH, 
                          new String[] { humanize(field), ""+max, ""+length }, 
                          DEFAULT_MSG_MAX_LENGTH);
              }
          }
              
          /**
           * Ensures the value of the field specified is numeric.
           */        
          public static void validateNumericalityOf(String field, Errors errors) {
              String value = (String) errors.getFieldValue(field);        
              if ("".equals(value)) return;        
              if (!NumberUtils.isNumber(value)) {
                  errors.rejectValue(field, 
                          KEY_NUMERIC, 
                          new String[] { humanize(field) }, 
                          DEFAULT_MSG_NUMERIC);            
              }
          }
      
          /**
           * Ensures the value of the field specified matches the given regex.
           * This class comes with handy constants that start with <code>REGEX_</code>
           */            
          public static void validateFormatOf(String field, String regex, Errors errors) {
              String value = (String) errors.getFieldValue(field);
              if ("".equals(value)) return;
              if (!value.matches(regex)) {
                  errors.rejectValue(field,
                          KEY_FORMAT,
                          new String[] { humanize(field) },
                          DEFAULT_MSG_FORMAT);
              }
          }
          
          /**
           * Trys to make a JavaBean field identifier human-readable.
           * <p>
           * e.g. <code>immigrationStatus</code> becomes Immigration status
           * 
           * @param fieldName the field to try and make human-readable
           * @return the human-readable representation of a JavaBean field identifier
           */
          private static String humanize(String fieldName) {
              return StringUtils.capitalize(fieldName.replaceAll("([A-Z]{1})", " $1").toLowerCase());
          }
      }
      Thank you for your help.
      Last edited by john.wheeler; Jun 3rd, 2006, 12:29 PM.

      Comment


      • #4
        Originally posted by john.wheeler
        If I take out the DEFAULT_MESSAGE_ fields, it seems like I lose the ability to perform substitutions in my message bundle. The overloaded rejectValue that doesn't take in the defaultMessage doesn't allow you to pass in an array of substitution values. Am I missing the point? Please advise.
        No you weren't -- I didn't realize that it is required to provide default messages if you want to perform substitutions. I checked the API again and now see that. So never mind.

        Glen

        Comment

        Working...
        X