The Validator: Automating Field Checking


Using DynaBean s has enabled you to drastically reduce the size of the form bean, but wouldn't it be nice to get rid of it all together?

After moving all the properties out of the bean and into the DynaForm definition in struts-config.xml , the only thing left in the bean is the validate function. By using the Struts Validation framework, which ties into the Commons Validator package, you can remove this last piece of code and get rid of the form bean.

WHAT IS COMMONS?

Commons (or more formally , the Jakarta Commons project) is an effort to gather a lot of reusable Java code in one place.

The idea is that there are any number of commonly coded tasks that can be generalized and placed in one place, so that no one ever needs to reinvent the wheel again.

Commons is divided into two pieces: the Commons proper and the sandbox. The Commons proper is where well- tested robust packages live; they have release cycles and beta testing. The sandbox is where the "not ready for prime time" packages live while they undergo their initial development, and is a much less formal environment.

You'll find that many of the Jakarta projects, from Torque to Struts and beyond, depend on various Commons packages to work. You can learn more about the Commons project by visiting the Jakarta Web site at http://Jakarta.apache.org/commons/

Adding validation to Struts forms is done in a few steps. To begin, you need to add the Validator plug-in to the struts-config.xml file. This is just a piece of boilerplate XML, shown in Listing 17.5.

Listing 17.5 Adding the Validator Plug-In to Struts
 <plug-in className="org.apache.struts.validator.ValidatorPlugIn">   <set-property   property="pathnames"   value="/WEB-INF/validator-rules.xml,/WEB-INF/validation.xml"/> </plug-in> 

This plug-in should be the very last thing in the file before the </struts-config> tag.

The plug-in entry does two things. First, it notifies Struts that the validator should be made available. Secondly, it defines the locations of the validator rules files, which are used to define the validations that will be run over the forms. The validator-rules.xml file comes standard with the validator. It defines the most commonly used rules (such as credit cards, dates, strings, numbers , and so on). The validation.xml file is one that you will define, and contains specialized validations used by your application, as well as the actual validation-to-form field mappings.

First, take a look at a typical rule from validator-rules.xml as delivered by the Validator package (see Listing 17.6).

Listing 17.6 A Rule from validator-rules.xml
 <validator name="creditCard"            classname="org.apache.struts.util.StrutsValidator"            method="validateCreditCard"            methodParams="java.lang.Object,                          org.apache.commons.validator.ValidatorAction,                          org.apache.commons.validator.Field,                          org.apache.struts.action.ActionErrors,                          javax.servlet.http.HttpServletRequest"            depends=""            msg="errors.creditcard">    <javascript><![CDATA[       function validateCreditCard(form) {           var bValid = true;           var focusField = null;           var i = 0;           var fields = new Array();           oCreditCard = new creditCard();           for (x in oCreditCard) {               if ((form[oCreditCard[x][0]].type == 'text'                     form[oCreditCard[x][0]].type == 'textarea') &&                    form[oCreditCard[x][0]].value.length > 0) {                  if (!luhnCheck(form[oCreditCard[x][0]].value)) {                     if (i == 0)                        focusField = form[oCreditCard[x][0]];                     fields[i++] = oCreditCard[x][1];                     bValid = false;                  }               }           }           if (fields.length > 0) {              focusField.focus();               alert(fields.join('\n'));         }         return bValid;     }     /**      * Reference: http://www.ling.nwu.edu/~sburke/pub/luhn_lib.pl     */     function luhnCheck(cardNumber) {         if (isLuhnNum(cardNumber)) {             var no_digit = cardNumber.length;             var oddoeven = no_digit & 1;             var sum = 0;             for (var count = 0; count < no_digit; count++) {                 var digit = parseInt(cardNumber.charAt(count));                 if (!((count & 1) ^ oddoeven)) {                     digit *= 2;                     if (digit > 9) digit -= 9;                 };                 sum += digit;             };             if (sum == 0) return false;             if (sum % 10 == 0) return true;         };         return false;     }     function isLuhnNum(argvalue) {         argvalue = argvalue.toString();         if (argvalue.length == 0)            return false;         for (var n = 0; n < argvalue.length; n++)            if (argvalue.substring(n, n+1) < "0"  argvalue.substring(n,n+1) > "9")               return false;          return true;       }]]>    </javascript> </validator> 

Unless you're a JavaScript wonk, only the first 10 lines of the definition are of any real importance to how the validator works with Struts. The first line of XML declares the official name of this validator ”that's the name that other rules or field definitions will use to refer to it.

The next line of XML tells the validator which class defines the validation method for this rule. Unlike many Java-related (and especially JSP- related ) tools, the validator doesn't call a standard method on each class. Instead, you can declare any number of methods in a single class and use different ones for different validations.

The method value tells the validator which method of the class that was just specified handles the validation for this rule. In this case, validation is handled by the aptly named validateCreditCard() method.

Adding to the ways in which the validator is different from everything else in Java, the validation method isn't defined as an interface or even a standard API. It can take a number of different arguments in a number of different orders. The validator code figures out which arguments go where by looking at the types of the arguments as specified in the methodParams arguments. They are

  • java.lang.Object ” The ActionForm that the value comes from, unless the property is a String array or Collection , (in which case it is the string itself).

  • org.apache.commons.validator.ValidatorAction ” The object created when the validation rules are parsed, one per rule.

  • org.apache.commons.validator.Field ” An object that holds information about the field being validated , such as its name.

  • org.apache.struts.action.ActionErrors ” The same old ActionError s that you know and love; you add to it if there's a validation error.

  • javax.servlet.http.HttpServletRequest ” The request being processed .

  • org.apache.commons.validator.Validator ” Gives you a handle to the Validator itself, useful for accessing other fields values.

Given all that information, the method should be able to figure out whether there was a validation error or not, eh?

Next, the depends tag specifies any other rules that should be run before this rule runs. In this case, the file used to say that the required rule should be run first. But, in my opinion, that was wrong. It would mean that you couldn't have a form with both a credit card and bank account field and let the user fill out one or the other, because credit card would be a required field. Luckily, I was able to convince the Struts folks to remove these dependencies, so now you need to explicitly use the " required " tag if you need a field to be required .

Finally, the message tag enables you to define the error message, which will be drawn from the default Resource , to use for this validation error.

The remainder of the file defines the JavaScript that's used to provide client-side validation of the field, if requested . It's optional, and those who are interested are welcome to refer to the documentation for the validator to learn more.

Adding the Validator to NewUserAddress

By using the validator, you can completely eliminate the need to create an ActionForm . This can be demonstrated with the same NewUserAddress action you converted to DynaBean s in the first half of this chapter.

To start, you need to create a validation.xml file, which goes into the same directory ( WEB-INF ) as the validator-rules.xml file. Listing 17.7 shows the file with only the rules for this form placed in it.

Listing 17.7 validation.xml
[View full width]
 <form-validation>  <global>   <constant>    <constant-name>phone</constant-name>    <constant-value>^\(?(\d{3})\)?[- ]?(\d{3})[- ]?(\d{4})$</constant-value>   </constant>   <constant>     <constant-name>states</constant-name>     <constant-value>^( graphics/ccc.gif graphics/ccc.gif ALAKASAZARCACOCTDEDCFMFLGAGUHIIDILINIAKSKYLAMEMHMDMAMIMNMSMOMTNENVNHNJNMNYNCNDMPOHOKORPWPAPRRISCSDTNTXUTVTVIVAWAWVWIWY)$</constant-value>   </constant>   <constant>     <constant-name>zip</constant-name>     <constant-value>^\d{5}(-\d{4})?$</constant-value>   </constant>  </global>  <formset>   <form name="newUserAddressForm">    <field      property="streetAddress1"      depends="required">      <arg0 key="newUserAddressForm.streetAddress1.label"/>     </field>    <field>      property="city"      depends="required">      <arg0 key="newUserAddressForm.city.label"/>    </field>    <field     property="state"     depends="required,mask">     <arg0 key="newUserAddressForm.state.label"/>     <var>      <var-name>mask</var-name>      <var-value>${states}</var-value>     </var>    </field>    <field     property="postalCode"     depends="required,mask">     <arg0 key="newUserAddressForm.postalCode.label"/>      <var>       <var-name>mask</var-name>       <var-value>${zip}</var-value>      </var>    </field>  <field     property="workPhone"     depends="required,mask">     <arg0 key="newUserAddressForm.workPhone.label"/>      <var>        <var-name>mask</var-name>        <var-value>${phone}</var-value>      </var>    </field>   <field     property="homePhone"     depends="required,mask">     <arg0 key="newUserAddressForm.homePhone.label"/>       <var>         <var-name>mask</var-name>         <var-value>${phone}</var-value>       </var>     </field>   </form>   </formset> </form-validation> 

This file is broken into two main sections. The top section (inside the globals tag) is a place to define any global values that will be used throughout the file. Normally, these are strings that will be used for the mask validation, but they could also be used in other places, such as for key values.

In this case, you're defining three constants, which are Perl-style regular expressions that will be used to validate phone numbers, ZIP codes, and states.

After the globals section comes the formset section. Each form begins with a form tag containing a single attribute, which is the name of the form (and must match the name as defined in struts-config.xml ).

Inside the form tag are a number of field tags. This tag has several attributes. The first is the property of the form that this entry validates , which must match a form-property . Next is the depends attribute, which specifies one or more validations that must be confirmed for this field to pass. For example, because the postalCode field has required,mask for a depends , both the required and mask validation must pass for this field to validate.

Inside the form tag, two types of values are commonly defined. The arg x (that is, arg0 , arg1 , and so on) tags are used to provide values to the error message if one is generated. Usually, all that's done here is to provide the key that corresponds to the name of the field in the Resource file.

The second tag, var , is used to pass arguments to the validators themselves . In this example, the mask validation is handed several different masks, depending on which field is being validated. By using the ${var} syntax, values defined in the globals section can be used here.

For the error messages to work, a number of strings must be put in the ApplicationResources.properties file (see Listing 17.8).

Listing 17.8 Additions to ApplicationResources.properties
 # Validator errors errors.required={0} is required. errors.minlength={0} can not be less than {1} characters. errors.maxlength={0} can not be greater than {1} characters. errors.invalid={0} is invalid. errors.byte={0} must be an byte. errors.short={0} must be an short. errors.integer={0} must be an integer. errors.long={0} must be an long. errors.float={0} must be an float. errors.double={0} must be an double. errors.date={0} is not a date. errors.range={0} is not in the range {1} through {2}. errors.creditcard={0} is not a valid credit card number. errors.email={0} is an invalid e-mail address #New user address form newUserAddressForm.streetAddress1.label=Street Address newUserAddressForm.city.label=City newUserAddressForm.state.label=State newUserAddressForm.postalCode.label=Postal Code newUserAddressForm.homePhone.label=Home Telephone newUserAddressForm.workPhone.label=Work Telephone 

The top section of strings is the generic validator error messages. You're free to alter them as you want; these are just the ones suggested in the code. The {0} will be replaced with the arg0 value defined in the field tag, as will the others by other arguments (for example, the {1} will be replaced by the minimum required length in the minlength validation).

The lower section simply defines the labels (names) that were used as keys to the field names in the validation.xml file. You can also used them with the bean:message tag in your JSP to internationalize your forms.

A few more steps are needed to complete the transition. The type attribute of newUserAddressForm in struts-config.xml must be changed to org.apache.struts.validator.DynaValidatorForm . Similarly, the line in NewUserAddressAction where the Form is cast to from a generic form must be changed to read:

 DynaValidatorForm uf = (DynaValidatorForm) form; 

With those changes, you can start the application and test it. You can also delete NewUserAddressForm.java because you've totally eliminated it.

Last Minute Validator News

One of the frustrations I (James speaking here) had with Struts was that the validator was languishing for lack of development. Specifically, there was no way to say "validate the checking account number only if the Use Checking Account button is pushed ," short of doing those validations in the action.

Also, the Commons Validator package, which is what the Struts Validator Framework depends on, had stalled short of a 1.0 release. I (and a lot of other Struts developers) felt nervous about depending on a package that hadn't even gotten out of the Commons Sandbox.

As this book was being completed, I was accepted as a committer for the Commons Validator package, and volunteered to spearhead a 1.0 release, which is currently scheduled for November of 2002. In addition, I refactored some of the underlying Validator code and added the needed hooks so that cross-form dependencies can be implemented. With that in mind, I submitted a patch against the Struts Validator that added a "requiredif" dependency rule, and which is now part of the core Struts Validator Framework.

Defining a New Validation

Before we leave the topic, it's a useful exercise to see how a new validation type could be written. A good example is the validation done on bank account routing numbers, known in the industry as the ACH (automated clearing house) routing number.

The basic algorithm for this validation is similar to the one done on credit card numbers, in that it's a simple checksum. The number itself is nine digits long. The digits are handled in groups of three starting from the left. For each group of three, the first digit is multiplied by thee, the second by seven, and the third left as is. All the results are added together, and if the resulting number is evenly divisible by 10, the number passes .

To implement this check, you must first define a new class, which in turn defines a method that does the check. Listing 17.9 shows this file.

Listing 17.9 ACHCheck.java
 package stocktrack.validator; import java.io.Serializable; import javax.servlet.http.HttpServletRequest; import org.apache.commons.validator.Field; import org.apache.commons.validator.ValidatorAction; import org.apache.struts.action.ActionErrors; import org.apache.commons.validator.GenericValidator; import org.apache.commons.validator.ValidatorUtil; import org.apache.struts.util.StrutsValidatorUtil; public class ACHCheck implements Serializable {     public static boolean validateACHRouting(Object bean,                         ValidatorAction va, Field field,                         ActionErrors errors,                         HttpServletRequest request) {       String value = null;       boolean results = false;       if (isString(bean)) {           value = (String) bean;       } else {           value = ValidatorUtil.getValueAsString(bean, field.getProperty());       }        if (GenericValidator.isBlankOrNull(value)) {         return true;       }       if (value.length() != 9) {         errors.add(field.getKey(),                    StrutsValidatorUtil.getActionError(request, va, field));         return false;       }       int n = 0;       for (int i = 0; i < value.length(); i += 3) {         n += CharToInt(value.charAt(i)) * 3                         +  CharToInt(value.charAt(i + 1)) * 7                         +  CharToInt(value.charAt(i + 2));       }      // If the resulting sum is an even multiple of ten (but not zero),      // the aba routing number is good.      if (n != 0 && n % 10 == 0)          return true;     else {        errors.add(field.getKey(),                   StrutsValidatorUtil.getActionError(request, va, field));        return false;          }   }   public static int CharToInt(char chr)    {      return Integer.parseInt(CharToString(chr));    }   public static String CharToString(char chr)     {       return String.valueOf(chr);     }   private static Class stringClass = new String().getClass();   private static boolean isString(Object o) {     if (o == null) return true;     return (stringClass.isInstance(o));   } } 

The first thing to notice is that the arguments to the validation function should look very familiar because they're the same types as the values of the methodParams attribute in the validator-rules.xml file. That's because you're now defining the method that will be called by the validator and will follow the template described in the validator tag.

The first thing to do is see whether the bean value passed in is a string. If it is not a string, the function must have been called during validation of a string array.

If it's not a string (or null), the function must look up the property name in the bean. ValidatorUtil has a nice helper function to do this.

After the function has the value, it checks whether it's of the right length, and if so, whether it passes the checksum. If it fails, a helper function from StrutsValidatorUtil is used to generate the appropriate ActionError for the property.

In addition to adding to the ActionError s variable, the method must also return true or false because the validator depends on this to determine whether further validations should be run on this field.

Next, you must add the validation rule. Technically, it could go in either validation.xml or validator-rules.xml because both files are the same format (and, in fact, you can load an arbitrary number of these files by changing the list in struts-config.xml ). However, it's a good idea to put local extensions into validation.xml because the other file comes with Struts, and might be overwritten. Here are the additional lines of XML, placed right after the global tag:

 <validator name="achRouting"      classname="stocktrack.validator.ACHCheck"      method="validateACHRouting"      methodParams="java.lang.Object,                    org.apache.commons.validator.ValidatorAction,                    org.apache.commons.validator.Field,                    org.apache.struts.action.ActionErrors,                    javax.servlet.http.HttpServletRequest"      depends=""      msg="errors.achRouting"/> 

Now you can add a new property to the new address form for testing:

 <form-property name="bankRouting" type="java.lang.String"/> 

Of course, there are new resource strings, too:

 errors.achRouting={0} is not a valid routing number. newUserAddressForm.bankRouting.label=Bank Routing Number 

And, finally, the form itself needs to handle the field:

 <tr>   <td>Bank Routing</td><td>       <html:text property="bankRouting" maxlength="9" size="9"/></td>   <td><html:errors property="bankRouting"/></td> </tr> 

With all that work in place, you can now validate bank routing numbers. If you want a good one, 123123123 will pass.

IS THE VALIDATOR WORTH IT?

I have to say that I'm honestly of two minds in regards to the validator. On one hand, I like the way it enables you to eliminate many FormAction classes altogether.

On the other hand, you end up writing a lot of boilerplate XML for every form, and for every field of every form. In fact, a rough estimate showed that a 20-line Java class file that implemented the validations in a FormAction might be replaced by 40 or more lines of XML and properties to do the same thing.

On the other other hand, the validator does reduce the amount of validation logic you have to write. So, I can't say that there's a good answer one way or the other.



Struts Kick Start
Struts Kick Start
ISBN: 0672324725
EAN: 2147483647
Year: 2002
Pages: 177

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net