UC San Diego SearchMenu

Form Basics

We will demonstrate how to collect student contact information for a student using a form. It uses the same configuration as the template web application.

There are three stages in a form life cycle. First a form page is displayed for user to enter data. Then the form is submitted and the data processed. Finally the result page is displayed. If you discovered that the user has entered invalid data while you were processing the data, redirect the user back to the form page with error messages. We'll start first by building a form backing object Contact POJO with the following attributes:

public class ContactCmd {
    private String lastName;
    private String firstName;
    private Long studentId;
    private String email;
    private Date birthDate;
    private College college;
    private String comment;
    ...

The POJO should have getter and setter methods for all the of above attributes.

Now we build a controller ContactController.java that handles all the requests related to Contacts.  For now, it will have a method setupForm() that displays the form page using a new ContactCmd form backing object.  Any URL that matches the RequestMapping will be directed to this method.

@Controller
public class ContactController {
    @RequestMapping(value = "/contact-form.htm")
    public @ModelAttribute("contactCmd")
    ContactCmd setupForm() {
        return new ContactCmd();
    }
    ...

Since this method does not specify a view name in return, Spring MVC will automatically look for /contact-form.jsp as the default view.

Now we build a form page contact-form.jsp.  Please refer to our style guide for all the extra div classes.

<form:form modelAttribute="contactCmd" action="contact.htm" method="post">

    <form:errors path="*">
        <div class="msg error">
        <h4>ATTENTION!</h4>
        <p>Please make the following correction(s) before proceeding.</p>
        </div>
    </form:errors>

    <fieldset><legend>Contact Information</legend>
    <div class="help icon astrisk">required</div>
    <div class="field">
    <div class="label required"><form:label path="lastName"
        cssErrorClass="invalid">Last Name</form:label></div>
    <div class="input"><form:input path="lastName"
        cssErrorClass="invalid " /><form:label path="lastName"
        cssErrorClass="icon invalid" /><form:errors path="lastName"
        cssClass="invalid" /></div>
    </div>

    <div class="field">
    <div class="label required"><form:label path="firstName"
        cssErrorClass="invalid">First Name</form:label></div>
    <div class="input"><form:input path="firstName"
        cssErrorClass="invalid " /><form:label path="firstName"
        cssErrorClass="icon invalid" /><form:errors path="firstName"
        cssClass="invalid" /></div>
    </div>

    <div class="field">
    <div class="label required"><form:label path="studentId"
        cssErrorClass="invalid">Student Id</form:label></div>
    <div class="input"><form:input path="studentId"
        cssErrorClass="invalid " /><form:label path="studentId"
        cssErrorClass="icon invalid" /><form:errors path="studentId"
        cssClass="invalid" /></div>
    </div>

    <div class="field">
    <div class="label required"><form:label path="email"
        cssErrorClass="invalid">Email</form:label></div>
    <div class="input"><form:input path="email"
        cssErrorClass="invalid " /><form:label path="email"
        cssErrorClass="icon invalid" /><form:errors path="email"
        cssClass="invalid" /></div>
    </div>

    <div class="field">
    <div class="label required"><form:label path="birthDate"
        cssErrorClass="invalid">Birth Date</form:label></div>
    <div class="input"><form:input path="birthDate"
        cssErrorClass="invalid " /><form:label path="birthDate"
        cssErrorClass="icon invalid" /><div class="help">(mm/dd/yyyy)</div><form:errors path="birthDate"
        cssClass="invalid" /></div>
    </div>

    <div class="field">
    <div class="label"><form:label path="college">College</form:label></div>
    <div class="input"><form:select path="college"
        items="${collegeList}" itemLabel="name" itemValue="id" /></div>
    </div>

    <div class="field">
    <div class="label"><form:label path="comment">Comment</form:label></div>
    <div class="input"><form:textarea path="comment" /><span
        class="help">Please supply any special instructions when
    contacting you.</span></div>
    </div>

    <div class="field">
    <div class="input"><input type="submit" class="button primary"
        value="Save" /></div>
    </div>

    </fieldset>

</form:form>
There is a drop down list in the form that consists of the six colleges here at UCSD.  How does this information gets passed into the form?  This is done by providing a method in the ContactController.  College is a enum class with id and name.
@ModelAttribute("collegeList")
public List<College> getCollegeList() {
    return Arrays.asList(College.values());
}

The ModelAttribute annotation tells the page that any reference to collegeList via EL ${collegeList} will get its value by calling this method getCollegeList().

Noticed the beginning of the form:form tag we specified that the model attribute for this form is contactCmd, which is what we created in the setupForm() method.  It also specifies that the submit action will do a HTTP POST to the URL contact.htm.

<form:form modelAttribute="contactCmd" action="contact.htm" method="post">

The resulting form page looks like this:

Contact Information
required
(mm/dd/yyyy)

Now we are ready to process the form submit.  Add a new method in the ContactController:

@RequestMapping(value = "/contact.htm", method = RequestMethod.POST)
public String save(HttpServletRequest req,
    @ModelAttribute("contactCmd") ContactCmd contactCmd,
    BindingResult bindResult, ModelMap model) {

        // validate
        new ContactValidator().validate(contactCmd, bindResult);
        if (bindResult.hasErrors())
            return "contact-form";

        // save the new contact and get the id of the new contact
        ...

        model.addAttribute("id", id);
        return "redirect:contact.htm";

This method specifies that it will handle all HTTP POST requests with URL /contact.htm.  It then proceeds to do some validation, making sure all data entered by the user are correct.  It saves the contact information and then redirects the user to contact.htm to view the newly created contact information.  If there are any errors while binding the data to a form backing object, the user is shown the contact-form page again with error messages.

ContactValidator is a validator class with validation logic to verify all data in ContactCmd is correct.  The following validator class

public class ContactValidator implements Validator {
    @Override
    public void validate(Object obj, Errors e) {
        ContactCmd cmd = (ContactCmd) obj;

        ValidationUtils.rejectIfEmpty(e, "firstName", "firstName.empty");
        ValidationUtils.rejectIfEmpty(e, "lastName", "lastName.empty");
        ValidationUtils.rejectIfEmpty(e, "studentId", "studentId.empty");
        ValidationUtils.rejectIfEmpty(e, "email", "email.empty");
        ValidationUtils.rejectIfEmpty(e, "birthDate", "birthDate.empty");

        String email = cmd.getEmail();
        if (StringUtils.isNotBlank(email)) {
            EmailValidator emailValidator = EmailValidator.getInstance();
            if (!emailValidator.isValid(cmd.getEmail()))
                e.rejectValue("email", "email.invalid", new Object[] { email },
                        "Invalid email format.");
        }

        Date birthDate = cmd.getBirthDate();
        if (birthDate != null && birthDate.after(new Date())) {
            e.rejectValue("birthDate", "birthDate.future");
        }
    }

We also need to fill the web/WEB-INF/config/messages.properties with the actual error messages that are specified in the Validator class.

lastName.empty=Last name cannot be empty.
firstName.empty=First name cannot be empty.
studentId.empty=Student id cannot be empty.
email.empty=Email cannot be empty.
birthDate.empty=Birth date cannot be empty.
college.empty=Please select a college.

email.invalid={0} is an invalid email address.
birthDate.future=Cannot have a birth date in the future.

typeMismatch.java.lang.Long=Invalid integer.
typeMismatch.java.util.Date=Invalid date format.

The last step in validation is to setup property editors for our attributes in ContactCmd that's not supported by Spring MVC automatic binding.  These attributes are java.util.Date and our College enum class.  Simply provide a initBinder method in the ContactController.

@InitBinder
public void initBinder(WebDataBinder binder) {
    // custom date binding
    SimpleDateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy");
    binder.registerCustomEditor(Date.class, new CustomDateEditor(
            dateFormat, true));

    // college binding
    binder.registerCustomEditor(College.class, new PropertyEditorSupport() {
        @Override
        public void setAsText(String text) {
            int id = Integer.parseInt(text);
            College college = College.get(id);
            setValue(college);
        }
    });
}

The date binding uses CustomDateEditor that was provided by Spring MVC.  The college binding uses a custom property editor.  It's build here as an anonymous inner class.  If you have other forms that need to bind to College enum type, you probably want to make it a regular class and inject it into the controller.

A sample of validator at work.

ATTENTION!

Please make the following correction(s) before proceeding.

Contact Information
required
bad email address is an invalid email address.
(mm/dd/yyyy)Invalid date format.

Finally, build a result page that displays the newly added contact information.  We first add a new request mapping in the ContactController class.

@RequestMapping(value = "/contact.htm", method = RequestMethod.GET)
public @ModelAttribute("contactCmd") ContactCmd show(@RequestParam("id") Long id) {
    ContactCmd contactCmd = contactMgr.getContact(id);
    return contactCmd;
}

The contactMgr is just a class that helps us retrieve a Contact based on the provided id.  Now we build the result page contact.htm.

<fieldset><legend>Contact Information</legend>
<div class="field">
<div class="label">Last Name</div>
<div class="output"><c:out value="${contactCmd.lastName}" /></div>
</div>

<div class="field">
<div class="label">First Name</div>
<div class="output"><c:out value="${contactCmd.firstName}" /></div>
</div>

<div class="field">
<div class="label">Student Id</div>
<div class="output"><c:out value="${contactCmd.studentId}" /></div>
</div>

<div class="field">
<div class="label">Email</div>
<div class="output"><c:out value="${contactCmd.email}" /></div>
</div>

<div class="field">
<div class="label">Birth Date</div>
<div class="output"><fmt:formatDate value="${contactCmd.birthDate}" pattern="MM/dd/yyyy"/></div>
</div>

<div class="field">
<div class="label">College</div>
<div class="output"><c:out value="${contactCmd.college.name}" /></div>
</div>

<div class="field">
<div class="label">Comment</div>
<div class="output"><c:out value="${contactCmd.comment}" /></div>
</div>

</fieldset>

Form Backing Object with Collections

In cases when a form backing object contains a list or a map, you can build the JSP in such a way that Spring can bind the collection automatically for you.  The collection can be static or dynamic.  See how to bind collections for detailed information.


Double Submit

You will notice in the previous section that when we process a submit request, the result page is redirected.  This technique of post and redirect prevents the form from double submits.  All attributes added in the model will be translated into URL request parameter.  For example, id was automatically added to the redirect url contact.htm?id=4.  Complex data type cannot be translated so be careful what you put into the model when you do a redirect.  See the Redirect After Post article by Michael Jouravlev that explains this pattern in detail.  Note the down side of this approach is that the client will make an extra request.

Building a Form