Programming Thoughts
Struts 2 - Post/Redirect/Get
More Form Injection
More examination of form injection
The code shown in Preserving forms is a simplification of existing code. This article will explain more sophistication.
Form reception
Usually, a rejected form should be displayed whilst a successful task means the updated database should be displayed. Occasionally, it's friendly to redisplay the form to the user whether it failed or not. Field annotations can change the conditions for injecting a form, as shown below.
ERROR | Only if form data was rejected. Default. |
ALWAYS | Always accepts form data. |
For the sake of completeness, other options are possible but are never used in practice.
NEVER | Never accepts form data. |
SUCCESS | Only if form data was accepted. |
Derived ModelDriven interface
Injecting forms depending on validation success means the interceptor knowing validation results. Normally, that's if there are any error or field error messages but frameworks should permit odd definitions, such as empty search results. Form based Actions should implement the following interface.
/** * Interface indicating Struts 2 action accepts form data into a form object and describes how the form should be * handled in workflow. */ public interface FormDriven<T extends AbstractForm> extends ModelDriven<T> { /** * Returns whether the user's request was rejected, such as form data itself is invalid, an update rejected by the * back-end or zero query results with zero results not allowed. This is usually defined as the presence of any * error messages. */ public boolean formValidationFailed(); }
The template class for form based Actions becomes the following.
public abstract class AbstractFormDrivenActionSupport<F extends AbstractForm> extends ActionSupport implements FormDriven<F> { ... @Override public boolean formValidationFailed() { return hasErrors(); } }
Form processor
Creating forms as their own class means they can be reused and a viewer Action can display more than one instance, each processed by a different Action. To handle such rare cases, a stored form should include the class of the processing Action, so the form retrieving interceptor can match the correct field.
Browser refreshing
Removing a stored form once read, as stated in Preserving forms, means it's lost if the user refreshes the browser and GET requests must be idempotent. This can be solved by the form retrieval interceptor letting the viewer Action own the form, reusing the form if the Action already owns it, and discarding the form if it's owned by a different Action.
Null form
As explained in later articles, there are two types of Struts 2 Action, one for processing data and the other for displaying it. Some data processing Actions don't receive a user submitted form, so there's a need for a 'no form' placeholder. This is the null form and is the following class.
/** * Form for Template Struts 2 Actions that use a form class as a type parameter, such as {@link AbstractFormDrivenActionSupport}, * but don't actually accept any form data. */ public class NullForm extends AbstractForm { @Override protected void resetFields() { // No fields } @Override public void doValidate(ValidationAware validationAware, TextProvider textProvider) { // No fields } }
Viewer Action field annotation
This leads to the annotation class for unusual fields receiving injected forms.
/** * <P>Defines rules for a member variable of a Struts view Action receiving a form processed by a form driven Struts * Action, whether successfully or not, injected by {@link FormRetrieveInterceptor}. The member variable is only set if * the form's type is compatible with its type. If a member variable lacks this annotation, form injection uses default * rules, described below.</P> * * <P>If any <CODE>processors</CODE> are set, the class of the Struts action that processed the form (successfully or * not) must match or be a subclass one of the values. This is useful if multiple displayed forms happen to be the same * type. Defaults to no processors, so the form can be from any Struts Action.</P> * * <P>List below describes how <CODE>reception</CODE> values affect form data reception:</P> * <DL> * <DT>NEVER</DT><DD>Nevers accepts form data.</DD> * <DT>ERROR</DT><DD>Only if form data was rejected. Default.</DD> * <DT>SUCCESS</DT><DD>Only if form data was accepted.</DD> * <DT>ALWAYS</DT><DD>Always accepts form data.</DD> * </DL> */ @Documented @Inherited @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Form { public enum Reception {NEVER, ERROR, SUCCESS, ALWAYS} public Class<?>[] processors() default {}; public Reception reception() default Reception.ERROR; }
Field annotation isn't normally needed but on the rare occasion it it, it can look like the following.
@Form(reception=Reception.ALWAYS, processors={SetInactivityTimeoutParameterAction.class}) private ParameterForm form;
Form store interceptor
The extra information means the form store interceptor should use a class encapsulating the form with such information. Its member fields are the following.
public static class StoredForm { private Object form; // The form being preserved private boolean invalid; // Whether form was rejected private String owningURL; // URL of viewer Action set by form retrieval interceptor private Class<?> processor; // Class of form processing Action ...
The interceptor becomes the following.
/** * <P>If the action implements {@link FormDriven}, stores the form in the session so it may be retrieved by a later * action that displays it. It is usually part of the aflUpdateStack interceptor stack and retrieved by * {@link FormRetrieveInterceptor} as part of the aflViewStack interceptor stack.</P> * * * <H3>Interceptor parameters</H3> * <P>none</P> * * <H3>Extending the interceptor</H3> * <P>This can't be usefully extended.</P> * * <H3>Example code</H3> * <PRE> * @InterceptorRefs({ * @InterceptorRef(value="formStore"), * @InterceptorRef(value="basicStack") *}) * </PRE> */ public class FormStoreInterceptor extends AbstractInterceptor { public static final String SESSION_STORED_FORM = FormStoreInterceptor.class + "_STORED_FORM"; public FormStoreInterceptor() { // Empty } @Override public String intercept(ActionInvocation invocation) throws Exception { FormDriven<?> formDriven; StoredForm storedForm; String result; result = invocation.invoke(); if (invocation.getAction() instanceof FormDriven) { formDriven = (FormDriven<?>)invocation.getAction(); if (formDriven.getModel() != null && formDriven.getModel().getClass() != NullForm.class) { storedForm = new StoredForm(); storedForm.setForm(formDriven.getModel()); storedForm.setInvalid(formDriven.formValidationFailed()); storedForm.setProcessor(invocation.getAction().getClass()); ActionContext.getContext().getSession().put(SESSION_STORED_FORM, storedForm); } else { ActionContext.getContext().getSession().put(SESSION_STORED_FORM, null); } } return result; } }
Form retrieval interceptor
Combining the thoughts this article and Preserving forms, interceptor becomes the following.
/** * <P>Retrieves a form stored in the session by {@link FormStoreInterceptor} and injects it into the action's member * variable configured by the {@link Form} annotation to accept it. It is usually part of the aflViewStack interceptor * stack so a view action can display rejected form data.</P> * * * <H3>Interceptor parameters</H3> * <DL> * <DT>disabled</DT><DD>If true, all processing is disabled. This is useful for standalone popup windows, especially * self-refreshing ones, that never display messages. Defaults to false.</DD> * </DL> * * <H3>Extending the interceptor</H3> * <P>The following method could be overriden :-</P> * <DL> * <DT>fieldReceives</DT><DD>Whether Struts action member variable can be set to the stored form.</DD> * </DL> * * <H3>Example code</H3> * <PRE> * @InterceptorRefs({ * @InterceptorRef(value="formRetrieve"), * @InterceptorRef(value="basicStack") *}) * </PRE> */ public class FormRetrieveInterceptor extends AbstractInterceptor { private boolean disabled; public FormRetrieveInterceptor() { // Empty } /** * Returns all member fields an instance of a class has. */ private static Collection<Field> getProperties(Class<?> type) { Collection<Field> fields; Class<?> c; fields = new ArrayList<Field>(); for (c = type; c != null; c = c.getSuperclass()) { fields.addAll(Arrays.asList(c.getDeclaredFields())); } return fields; } /** * Returns URL of current request, including query parameters. */ private String getFullURL() { if (ServletActionContext.getRequest().getQueryString() != null) { return ServletActionContext.getRequest().getRequestURL().append('?').append(ServletActionContext.getRequest().getQueryString()).toString(); } else { return ServletActionContext.getRequest().getRequestURL().toString(); } } /** * Returns whether Struts Action member variable can be set to the stored form.. */ protected boolean fieldReceives(Class<?> actionClass, Field field, StoredForm storedForm) { Form formAnnotation; Reception reception; Class<?>[] processors; boolean result; if (!field.getType().isAssignableFrom(storedForm.getForm().getClass())) { return false; } if (field.getAnnotation(Form.class) != null) { formAnnotation = field.getAnnotation(Form.class); reception = formAnnotation.reception(); processors = formAnnotation.processors(); } else { reception = Reception.ERROR; processors = new Class<?>[0]; } result = false; switch (reception) { case NEVER: result = false; break; case ERROR: result = storedForm.getInvalid(); break; case SUCCESS: result = !storedForm.getInvalid(); break; case ALWAYS: result = true; break; } if (result && processors.length > 0) { result = false; for (Class<?> processor: processors) { if (processor.isAssignableFrom(actionClass)) { result = true; break; } } } return result; } /** * Returns whether Action will not retrieve forms. */ public boolean getDisabled() { return disabled; } public void setDisabled(boolean value) { disabled = value; } @Override public String intercept(ActionInvocation invocation) throws Exception { StoredForm storedForm; Collection<Field> fields; Object rawObject; boolean receive; if (!disabled) { rawObject = ActionContext.getContext().getSession().get(FormStoreInterceptor.SESSION_STORED_FORM); if (rawObject != null) { if (rawObject instanceof StoredForm) { storedForm = (StoredForm)rawObject; if (storedForm.getOwningURL() == null || storedForm.getOwningURL().equals(getFullURL())) { fields = getProperties(invocation.getAction().getClass()); for (Field field: fields) { receive = fieldReceives(invocation.getAction().getClass(), field, storedForm); if (receive) { field.setAccessible(true); field.set(invocation.getAction(), storedForm.getForm()); } } storedForm.setOwningURL(getFullURL()); } else { // Remove as user is displaying another page ActionContext.getContext().getSession().remove(FormStoreInterceptor.SESSION_STORED_FORM); } } } } return invocation.invoke(); } }
Next part
Continued in Different Interceptor Stacks.