cool stuff
/src_docs/architecture/forms/overview.html, v125

High Level Overview

This document attempts to provide a very high level overview of the Barracuda Form Mapping & Validation framework by answering a lot of basic questions. We start with the obvious... [NOTE: this doc is a bit stale]

  1. What is the purpose of this package?
  2. What is a FormMap?
  3. What is a FormElement?
  4. What FormTypes are supported?
  5. What is a FormValidator?
  6. What types of generic validators are available?
  7. What about custom validators?
  8. What about exceptions?
  9. Ok, so what does some sample code look like?
  10. Where do we go from here?

Q: What is the purpose of this package?

Typically, when you want to work with HTML forms, you must know the actual name (in the HTML) of the form element. Given that key value, you can retrieve that element(s) from an HttpServletRequest. The value is returned as a String, and then it's up to you to manually parse, convert, validate, etc.  This tends to be cumbersome.

The form mapping and validation package is designed to make it easier to work with HTML forms. It accomplishes this goal by allowing you to specify which elements a form should contain and how they should be mapped and validated. This allows the form mapping and validation framework to do most of the dirty work for you. When you receive an HttpServletRequest, you simply say "given the form specifications I have defined, map/validate this request". Once the form has been mapped, you can easily access elements in the form as first class Java object (ie. String, Boolean, etc).  If an element in the form is not valid, you'll be notified of the error.

The package structure is actually very simple:

We should also note that the only dependencies are on the org.barracudamvc.core.util packages...this means that the whole forms mapping system is completely decoupled from the event model, which in turn provides added flexibility.

Now, the model for the forms package is also quite straightforward; if you'd like to view the UML Class Diagram, you can view it here (larger). The important thing to note is that there are only three key entities:

  1. FormMap
  2. FormElement
  3. FormValidators

Q: What is a FormMap?

A FormMap corresponds logically with an HTML Form, although the actual implementation is bound to form source which is either a ServletRequest or a StateMap. This decoupling makes it easy to populate forms form other sources (including data models).

Basically, a form map is responsible for 3 distinct tasks:

  1. it must allow the developer to define FormElements
  2. it must be able to map FormElements (based on the definitions)
  3. it must be able to validate itself and all FormElements it contains

We'll look at the specifics for each of these below.

As an added benefit, the form map provides convenience methods to easily retrieve values from the FormElements it contains.

Q: What is a FormElement?

In order to define an element in a FormMap, we need to first create a FormElement object. The constructor for a FormElement takes a number of different parameters:

This effectively provides all the information the FormMap will need to map a String value from the form source to an actual form element object.

Once a form element has been mapped, you can access it's value by invoking the getVal() method (which returns an Object), or the getStringVal(), getBooleanVal(), etc. method depending on the object type. When using these convenience methods, the form element will do the casting for you.  The form element also keeps track of the original pre-converted form source value.

Q: What FormTypes are supported?

As we mentioned above, there are a number of different supported FormTypes. Currently, we support all the basic Java data types:

In the future, we may consider adding additional "complex" types, like PhoneNumber, CreditCardNumber, etc.

Q: What is a FormValidator?

As it's name implies, a FormValidator is primarily responsible for validation: in this case, for validating either a Form or a specific Form Element in the form. There is however, a secondary role: FormValidators must also be capable of containing other FormValidators. This capacity for a validator to contain other validators is extremely powerful: it makes it possible for any number of FormValidators to be assembled into one "master" validator. Here's how the validation process works.

The FormValidator interface defines several different methods to do validation:

  1. validate(FormElement element, FormMap formMap) - this is the method that gets invoked by the FormMap to validate the entire form and all elements in it. When this method if invoked, the default implementation in DefaultFormValidator performs a basic validation sequence:
    1. if we are validating a specific element, invoke methods 3 and 4 below
    2. if we are not validating a specific element, then invoke method 2 below
    3. finally, invoke validate on any child FormValidators contained in this validator
  2. validate(FormMap formMap) - this is a convenience method for performing "form" level validation (ie validation that applies to the entire form).
  3. validate(Object val, FormElement element) - this is a convenience method for performing  "element" level validation. Val is the actual value of the element; element is the form element that backs the value.
  4. validate(Object val, FormElement element, FormMap formMap) - this is another convenience method for performing  "element" level validation. It's identical to method 3 except that you also get a reference to the parent form map, which may be necessary if you need to validate an element by comparing it against other elements in the form.

Typically you would not override method 1.  Instead you would override method 2 (if you wanted to perform "form" level validation) or method 3 or 4 (if you wanted to perform "element" level validation).

Q: What types of generic validators are available?

A: At this point, we've only implemented a few generic validators:

We do have plans, however, to create a number of additional validators:

This is a great place for people to contribute.

Q: What about custom validators?

Inevitably, you'll need to write custom validators to perform more sophisticated, application specific validations. The validation framework is well suited for this requirement. All you have to do is write a class (either inner or regular) that implements the FormValidator interface. Then simply add the validator to the appropriate form and your validator will be invoked when the form is validated.

Q: What about exceptions?

This is a good time to talk about exceptions. Typically, when a validator encounters an error it throws a ValidationException, which indicates the nature of the error and causes all validation to stop. There are times, however, where you may wish the validation continue. An example of this might be if you want to collect all errors on a given form, so that you can allow the user to fix all of them at once (rather than repeatedly resubmitting only to find out there was yet another problem).

In the case where you would like to indicate an error but still allow validation to continue, you can throw a DeferredValidationException. The default validator implementation will catch this error and save it within a "master" validation exception. Once the validation process is complete, the master exception will be thrown and individual exceptions can be retrieved from it. This can be very useful behaviour.

Q: Ok, so what does some sample code look like?

Alright, you say, let's get on with it! What does some sample code look like? We can find a good example by looking at the Barracuda Disc Rack source. If we take a look in the RegistrationScreen, we see code that looks something like this:

First, let's look at the HTML:

<html>
    <head><title>Register for Barracuda Disc Rack</title></head>
    <body style="font-family: Georgia, Arial, Times New Roman" bgcolor="#FFFFFF">
    <form name="LoginForm" method="POST" action="Register.html?DoRegister">
        <input type="hidden" name="discID" value="invalidID">
        <div align="center">
            <center><h2>Barracuda Disc Rack Registration</h2></center>
        </div>
        <hr width="480" size="1">
        <div align="center">
            <center>
                <p><font color="#FF0000"><span ID="ErrorText">Optional Error Text Goes Here</span></font></p>
            </center>
        </div>
        <div align="center">
            <center>
                <table SUMMARY="Signup form" border="0" cellPadding="1" cellSpacing="1">
                    <tr>
                        <td align="right">First Name: </td>
                        <td><input name="firstname" id="firstname" size="20"></td>
                    </tr>
                    <tr>
                        <td align="right">Last Name: </td>
                        <td><input name="lastname" id="lastname" size="20"></td>
                    </tr>
                    <tr>
                        <td align="right">Login Name: </td>
                        <td><input name="login" id="login" size="20"></td>
                    </tr>
                    <tr>
                        <td align="right">Password: </td>
                        <td><input type="password" name="password" id="password" size="20"></td>
                    </tr>
                    <tr>
                        <td align="right">Confirm Password: </td>
                        <td><input type="password" name="repassword" id="repassword" size="20"></td>
                    </tr>
                    <tr>
                        <td align="right"></td>
                        <td><div align="right"><p><input type="submit" value="Sign Me Up!"></div></td>
                    </tr>
                </table>
            </center>
        </div>
    </form>
    </body>
</html>

If you look closely you can see that we have several fields (all String values) named firstname, lastname, login, password, and repassword.

Now, in the source, we can see that we use an inner class to define the FormMap:

    class RegistrationForm extends DefaultFormMap {

        //registration form constants (these values correspond to the HTML params)
        static final String FIRST_NAME = "firstname";       
        static final String LAST_NAME = "lastname";           
        static final String USER = "login";                    
        static final String PASSWORD = "password";           
        static final String REPASSWORD = "repassword";       

        public RegistrationForm() {
            //define the elements
            this.defineElement(new DefaultFormElement(FIRST_NAME, FormType.STRING, null,
                    new NotNullValidator("You must enter a First name.")));
            this.defineElement(new DefaultFormElement(LAST_NAME, FormType.STRING, null,
                    new NotNullValidator("You must enter a Last name.")));
            this.defineElement(new DefaultFormElement(USER, FormType.STRING, null,
                    new NotNullValidator("You must enter a Login.")));
            this.defineElement(new DefaultFormElement(PASSWORD, FormType.STRING, null,
                    new NotNullValidator("You must enter a Password.")));
            this.defineElement(new DefaultFormElement(REPASSWORD, FormType.STRING, null,
                    new NotNullValidator("You must Confirm your password.")));
           
            //define a form validator
            this.defineValidator(new RegistrationValidator());
        }
    }

Here we indicate several important facts:

We also define a custom validator called RegistrationValidator which validates the entire form. The code for RegistrationValidator looks something like this:

    class RegistrationValidator extends DefaultFormValidator {
        public void validateForm(FormMap map) throws ValidationException {

            //make sure the passwords match
            String password = map.getStringVal(RegistrationForm.PASSWORD);
            String repassword = map.getStringVal(RegistrationForm.REPASSWORD);
            if (!password.equals(repassword)) throw new ValidationException(
                    map.getElement(RegistrationForm.PASSWORD), "Passwords do not match. Please re-enter.");
           
            //validate the login information
            String user = map.getStringVal(RegistrationForm.USER);
            try {
                if (PersonFactory.findPerson(user)!=null)
                            throw new ValidationException(map.getElement(RegistrationForm.USER),
                                    "This login already taken. Please select another.");
            } catch (DiscRackBusinessException e) {
                throw new ValidationException(map.getElement(RegistrationForm.USER),
                    "Error checking login against database. Please try again. If the problem persists, "+
                    "contact your system administrator.", e);
            }
        }
    }

This custom validator ensures that:

With us so far? Great. Now it gets easy! We can instantiate, populate, and validate the form in a few easy steps:

    RegistrationForm rf = new RegistrationForm();
    try {
        rf.map(req).validate();

        ...perform biz processing here...

    } catch (ValidationException e) {
       ... handle errors here...
    }

As you can see, we simply take a ServletRequest, map it, and validate the resulting form all in one fell swoop.

Assuming the form is valid and we were able to map the data, we can get the data out by using code that looks something like this:

    page.getElementFirstname().setValue(""+rf.getStringVal(RegistrationForm.FIRST_NAME));
    page.getElementLastname().setValue(""+rf.getStringVal(RegistrationForm.LAST_NAME));
    page.getElementLogin().setValue(""+rf.getStringVal(RegistrationForm.USER));
    page.getElementPassword().setValue(""+rf.getStringVal(RegistrationForm.PASSWORD));
    page.getElementRepassword().setValue(""+rf.getStringVal(RegistrationForm.REPASSWORD));

If there are any errors, we can process them like so:

    if (ve==null) {
        page.getElementErrorText().getParentNode().removeChild(page.getElementErrorText());
    } else {
        List errlist = ve.getExceptionList();
        if (showDebug>1) Debug.println (this, 0, errlist.size()+"error(s) found");            
        StringBuffer sb = new StringBuffer(errlist.size()>1 ? "There were several errors:" : "");
        Iterator it = errlist.iterator();
        while (it.hasNext()) {
            sb.append(" "+((ValidationException) it.next()).getMessage());
        }
        page.setTextErrorText(sb.toString());
    }

Pretty slick, huh?

Q: Where do we go from here?

Good question. At this point, there are several areas that need additional attention.

  1. More custom validators - first and foremost, we need to create additional validators. This would be a great place to get involved if you'd like to actually contribute to the Barracuda project. As we indicated above, there are a lot of basic validators which would be immediately useful...
  2. Additional data types - we need to also consider creating additional data types like PhoneNumber, CreditCardNumber, etc. Again, this would be a great place to get involved...
  3. Auto-repopulation - one of the things we haven't addressed yet is how to take a form and automatically map it back into an XMLC object. Right now, that process is still manual. We'll be looking at this closely however, as we move forward with Barracuda component model.

Suggestions or ideas on other things we should consider? Please let us know..

 


Last Modified: 2006-01-02 15:59:13 -0500 (Mon, 02 Jan 2006)