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...
- What is the purpose of this package?
- What is a FormMap?
- What is a FormElement?
- What FormTypes are supported?
- What is a FormValidator?
- What types of generic validators are available?
- What about custom validators?
- What about exceptions?
- Ok, so what does some sample code look like?
- Where do we go from here?
Q: What is the purpose of this package?
A: 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:
- org.enhydra.barracuda.core.forms - contains all of the basic forms
classes needed to define, map, and validate a form.
- org.enhydra.barracuda.core.forms.validators - contains reusable
validators which can be applied to forms or form elements to perform standard validations.
We should also note that the only dependencies are on the org.enhydra.barracuda.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:
- FormMap
- FormElement
- FormValidators
Q: What is a FormMap?
A: 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:
- it must allow the developer to define FormElements
- it must be able to map FormElements (based on the definitions)
- 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?
A: 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:
- key - the key name (typically corresponds to the HTML element name, and
is used to retrieve the raw form String from the ServletRequest or StateMap source)
- type - the FormType (ie. String, Boolean, Integer, etc)
- defaultVal - the default value (ie. the value to be used if the key is
not present in the form source)
- validator - FormValidators responsible for validating this particular
element
- allowMultiples - is it possible for this key to have multiple values in
the form source
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?
A: As we mentioned above, there are a number of different supported
FormTypes. Currently, we support all the basic Java data types:
- java.lang.String
- java.lang.Boolean
- java.lang.Integer
- java.lang.Long
- java.lang.Short
- java.lang.Double
- java.lang.Float
- java.util.Date
In the future, we may consider adding additional "complex" types, like
PhoneNumber, CreditCardNumber, etc.
Q: What is a FormValidator?
A: 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:
- 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:
- if we are validating a specific element, invoke methods 3 and 4 below
- if we are not validating a specific element, then invoke method 2 below
- finally, invoke validate on any child FormValidators contained in this validator
- validate(FormMap formMap) - this is a convenience method for performing
"form" level validation (ie validation that applies to the entire form).
- 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.
- 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:
- NotNullValidator - throws a ValidationException if the incoming value is null
We do have plans, however, to create a number of additional validators:
- Min/Max length validators - ensure the value lenght is < or > x characters in
length
- Min/Max value validators - ensure the value is < or > than x value
- Value equality/inequality validators - ensure a values equals or does not equal a
specified value
- Valid characters validator - only allow certain characters
- Invalid characters validator - disallow certain characters
- Valid date - ensure the value is a valid date
- Valid currency - ensure it's a valid currency number
- Valid credit card - ensure it's a valid credit card number
- etc.
This is a great place for people to contribute.
Q: What about custom validators?
A: 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?
A: 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?
A: 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:
- the FormMap will have 5 fields
- each field is of type FormType.STRING
- none of the fields have a default value
- each field has a basic NotNullValidator
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:
- the PASSWORD value matches the REPASSWORD value
- the USER is not already on file
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?
A: Good question. At this point, there are several areas that need
additional attention.
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...
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...
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... |