Programming Thoughts
Struts 2 - Post/Redirect/Get
Use ModelDriven

Expanding on forms preserved during Post/Redirect/Get

Previous articles mention storing forms and detecting whether one was injected. This article explains how this is best achieved and why design choices were made, including the unusual use of ModelDriven.

Resurrecting a Struts 1 concept

In order to preserve and copy form data across a redirect, we must define where it's kept. Normally, they're in the fields of the Action processing the form but such Actions can have all manner of ancillary state injected into them, which may not be serializable and not allowed as session data. Using Actions as form data is asking for future interceptors to break code. For example, servlet request objects injected by ServletRequestAware aren't serializable.

If not the Action, perhaps the request parameter names and values, which are strings and serializable. This copies all parameters, whether from the intended HTML form or hacked into it by a technical user, into the viewer Action. That risks a security violation. Worse, the viewer Action must check every form field to detect if a form was submitted.

This leaves form fields in a POJO, much like the Struts 1 forms, and making form processing Actions ModelDriven. As the form can only contain data it explicitly defines, there is no danger of hacked fields or non-serializable data.

No automatic type conversion

One problem with ModelDriven is type conversion is not automatic and requires a lot more work. Type conversion make useful error messages tedious anyway and may as well be dropped.

More importantly, the StrutsConversionErrorInterceptor interceptor, which handles type conversion errors, places the rejected form fields on the ValueStack for redisplay but a ValueStack isn't copied across a redirect.

Validation workflow

Despite what the documentation says, validation is not performed on the model object. Rather, the validate function is called on the Action, which must validate the form. In the interests of encapsulation, forms must know how to validate their own fields. Although a new form instance is created for each request, eliminating the original need for it in Struts 1, the resetFields function is also resurrected to ensure all fields have non-null defaults. All forms should derive from the following class.

/** * Template for Struts 2 forms that receives form data. */ public abstract class AbstractForm implements Serializable { public AbstractForm() { resetFields(); } /** * Sets all displayed fields to default values for data entry of a new record, which is usually an empty string. */ protected abstract void resetFields(); /** * Validates and parses form data itself, such as required fields or date validation, writing any error or field * messages to the ValidationAware message receiver. Any error message indicates a failure. * * @param validationAware Error message receiver, usually the calling action. * @param textProvider Message name translator that generates localised text, if this is set up. */ public abstract void doValidate(ValidationAware validationAware, TextProvider textProvider); }

Actions expected to use forms should thus be derived from the following class.

/** * Base class of Struts 2 action that for finding or updating based on user entered data. */ public abstract class AbstractFormDrivenActionSupport<F extends AbstractForm> extends ActionSupport { private F form; /** * Returns a new instance of the form. */ protected abstract F makeForm(); protected F getForm() { return form; } protected void setForm(F form) { this.form = form; } @Override public F getModel() { if (form == null) { form = makeForm(); } return form; } @Override public void validate() { form.doValidate(this, this); } }

Form injection into viewer Actions

Keeping form data in a POJO means they can be injected by the form preservation interceptor into viewer Actions as a single member variable. Detecting whether a form was injected is simply checking if it's non-null, much like the following.

private ProductForm form; ... if (form == null) { product = getProductRecord(); form = new ProductForm(product); }

Next part

Continued in More Form Injection.