The Hidden Gem of Web Frameworks

Tutorial - Stripes: a lean, mean Java web framework - Part 2

 

Binding: the superfeature

Many a Stripes user, including myself, feel that the most powerful feature in Stripes is its binding of request parameters to action bean properties. We've looked at that feature briefly; now let's dig deeper. Suppose you have a model object that has properties which are, in turn, other model objects, such as a Person with an Address

 

  /* Person.java */ 
  package com.jaxenter.stripes.model;
  
  public class Person {
    private String firstName;
    private Address address;
  
    public String getFirstName() {
      return firstName;
    }
    public void setFirstName(String firstName) {
      this.firstName = firstName;
    }
    public Address getAddress() {
      return address;
    }
    public void setAddress(Address address) {
      this.address = address;
    }
  }
  /* Address.java */
  package com.jaxenter.stripes.model;
  
  public class Address {
    private String streetName;
  
    public String getStreetName() {
      return streetName;
    }
    public void setStreetName(String streetName) {
      this.streetName = streetName;
    }
  }

 

You'd like to enable the user to create a person with an address using an HTML form. With Stripes, it would look something like this - first, you'd have an action bean with a Person property:  

 

  /* PersonActionBean.java */
  package com.jaxenter.stripes.action;
  
  import net.sourceforge.stripes.action.DefaultHandler;
  import net.sourceforge.stripes.action.ForwardResolution;
  import net.sourceforge.stripes.action.Resolution;
  import com.jaxenter.stripes.model.Person;
  
  public class PersonActionBean extends BaseActionBean {
    private Person person;
  
    @DefaultHandler
    public Resolution view() {
      return new ForwardResolution("/WEB-INF/jsp/personForm.jsp");
    }
  
    public Person getPerson() {
      return person;
    }
    public void setPerson(Person person) {
      this.person = person;
    }
  }

Notice that we also have a default event handler that forwards to personForm.jsp; this is where we create the HTML form: 

 <%-- personForm.jsp --%>
  <stripes:form beanclass="com.jaxenter.stripes.action.PersonActionBean">
    <div>
      First name:
      <stripes:text name="person.firstName"/>
    </div>
    <div>
      Street name:
      <stripes:text name="person.address.streetName"/>
    </div>
    <div>
      <stripes:submit name="save" value="Save"/>
    </div>
  </stripes:form>

See how simple and straightforward that is? We use the <stripes:form> tag and indicate the target action bean class name. Then we create text inputs with the name attribute that corresponds to the target property: person.firstName and person.address.streetName. When the user submits the form, Stripes will automatically call getPerson().setFirstName(<value>) and getPerson().getAddress().setStreetName(<value>) on the action bean to populate the person object and its nested properties.

It gets better: you might think that you'll get a NullPointerException because we never created a new Person() object. Never fear! Stripes does it for you, even on nested properties. When it gets null from a getter when binding request parameters to properties, Stripes creates a new object and sets it. So you can just add properties and let Stripes take care of the housekeeping. Finally, we just need an event handler for the submit button, which is named save:

 

 public Resolution save() {
    // save the person object... 
  }

 

By the time the save() method is called by Stripes, the parameters have been bound to the action bean properties, so we can count on the person object to be created, populated, and ready to be saved. Well, that is not entirely true: for that to work, the user has to fill in the form, rather than submit a blank form. We can easily address this with validation.

Validation: easy and awesome

Stripes offers a simple way to perform validation on user input: annotations on action bean properties, and custom validation methods. Continuing our Person form example; say we want to make sure that the user enters values in both the person.firstName and person.address.streetName fields. We use annotations on the person property in the action bean:  

 

 /* PersonValidatedActionBean.java */
  package com.jaxenter.stripes.action;
  
  import net.sourceforge.stripes.action.DefaultHandler;
  import net.sourceforge.stripes.action.DontValidate;
  import net.sourceforge.stripes.action.ForwardResolution;
  import net.sourceforge.stripes.action.Resolution;
  import net.sourceforge.stripes.validation.Validate;
  import net.sourceforge.stripes.validation.ValidateNestedProperties;
  import com.jaxenter.stripes.model.Person;
  
  public class PersonValidatedActionBean extends BaseActionBean {
    @ValidateNestedProperties({
      @Validate(field="firstName", required=true),
      @Validate(field="address.streetName", required=true)
    })
    private Person person;
  
    @DefaultHandler
    @DontValidate
    public Resolution view() {
      return new ForwardResolution("/WEB-INF/jsp/personValidatedForm.jsp");
    }
  
    public Resolution save() {
      // save the person object... 
      return new ForwardResolution("/WEB-INF/jsp/personSaved.jsp");
    }
  
    public Person getPerson() {
      return person;
    }
    public void setPerson(Person person) {
      this.person = person;
    }
  }

 

The @Validate(required=true) annotation marks a property as a required field. We can place this annotation directly on an action bean property if we are submitting a value directly to that property. In our example, we're not doing that; we're submitting values to nested properties of the person property. In that case, we need to place our @Validate annotations inside a @ValidateNestedProperties annotation, and use the field attribute to indicate which nested property we are validating. Notice that we also added @DontValidate to the view() method so that the validations are not enforced when the user first arrives to the action bean–they haven't had a chance to fill in the form at that point.

Now, with these validations, a blank form will not cause our save event handler to be called. Instead, Stripes will catch the validation errors and redisplay the form instead, with error messages at the location where we add the <stripes:errors/> tag:

 

  <%-- personValidatedForm.jsp --%>
  <stripes:form beanclass="com.jaxenter.stripes.action.PersonValidatedActionBean">
    <stripes:errors/>
    <div>
      First name:
      <stripes:text name="person.firstName"/>
    </div>
    <div>
      Street name:
      <stripes:text name="person.address.streetName"/>
    </div>
    <div>
      <stripes:submit name="save" value="Save"/>
    </div>
  </stripes:form>

 

Submitting a blank form will result in a page as shown in the figure below (Fig. 1).

 

What's really nice is that if the user fills in one of the fields, but not the other, the page will be redisplayed with the error message concerning the required field that was left blank, but the field that was filled in will automatically be repopulated. Notice that we don't have to do anything in the <stripes:text> tag for that to happen. This keeps your JSP code clean and tight. Stripes comes with the following built-in validations:  

 

  • required field
  • minimum and maximum length
  • minimum and maximum value, for numerical input
  • matching a regular expression mask
  • evaluation of a JSP expression

Writing a custom validation method is a breeze, too. Just write a method annotated with @ValidationMethod. For example:

 

@ValidationMethod
  public void validateSomething(ValidationErrors errors) {
    if (!password.equals(confirmPassword)) {
      errors.add(new SimpleError("The passwords do not match."));
    }
  }

 

By adding an error to the errors list, we cause Stripes to return to the form instead of calling the event handler, just like when a built-in validation fails.

Finally, note that Stripes only calls the validation method if the built-in validations have passed. Again, this keeps your code clean and tight, because for example you don't have to do null checks on the password and confirmPassword if you made them required fields. If the required validation fails, the custom validation method will not be called. The error messages concerning the fields being required will be shown instead.

 

Pages

Fred Daoud
Fred Daoud

What do you think?

JAX Magazine - 2014 - 05 Exclucively for iPad users JAX Magazine on Android

Comments

Latest opinions