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.