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]
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:
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:
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.
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.
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.
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:
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).
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.
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.
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.
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?
Good question. At this point, there are several areas that need additional attention.
Suggestions or ideas on other things we should consider? Please let us know..