Redirect After Post 2
This article demonstrates the benefits of Redirect-after-Post pattern using a simple web application. See my previous article " Redirect-after-Post pattern in web applications" for the discussion of the concept.
Implementing Redirect-after-Post pattern using Struts
This article demonstrates the benefits of Redirect-after-Post pattern using a simple web application. See my previous article " Redirect-after-Post pattern in web applications" for the discussion of the concept.
The use case
The application stores, displays and updates abstract data items. The following functions are available:
- display all existing items in a list;
- view an item;
- edit an item;
- create a new item;
- delete an item.
Each data item has the following properties:
- item ID (randomly generated primary key);
- item value (short integer);
- item status ("New" or "Stored").
Application has simple business logic, provides input data validation and error handling. Errors caused by incorrect data input are displayed on the input page along with erroneous data. Generic errors are displayed on a separate page.
Business/persistence layers have the following rules:
- item value must be a short integer;
- item status is set to "New" when the item is created, then changed to "Stored" after item is persisted;
- item with "New" status can not be stored if an item with the same ID exists in the storage (prevents double insert of a new item from a cached web page);
- item with "Stored" status can be stored at any time (allows updating of existing items without constraints);
- storage can hold at most 10 items (to save memory, because items are stored in the HttpSession object).
I chose Struts as a web framework. Despite that choice the principles remain the same for other frameworks or programming languages.
Based on the application tasks, I created the following actions:
- viewList - displays item list; it is the application homepage;
- viewItem - displays one item;
- createItem - creates new empty item;
- editItem - allows to set a value for a new or for an existing item;
- storeItem - persists modified item;
- deleteItem - deletes an item from persistent storage;
- showError - displays generic error page.
Web flow defined
Follows is the application web flow in the form of struts-config.xml file.
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE struts-config PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 1.1//EN" "http://jakarta.apache.org/struts/dtds/struts-config_1_1.dtd"> <struts-config> <form-beans> <form-bean name="itemListForm" type="com.xorsystem.items.ItemListForm"/> <form-bean name="itemFormOutput" type="com.xorsystem.items.ItemFormOutput"/> <form-bean name="itemFormInput" type="com.xorsystem.items.ItemFormInput"/> <form-bean name="itemFormError" type="com.xorsystem.items.ItemFormError"/> </form-beans> <action-mappings> <!-- Item List: displays item list (the home page) --> <action path = "/viewList" type = "org.apache.struts.actions.ForwardAction" name = "itemListForm" parameter = "/WEB-INF/items/viewList.jsp"/> <!-- View Item: displays one item in read-only mode --> <action path = "/viewItem" type = "org.apache.struts.actions.ForwardAction" name = "itemFormOutput" input = "itemError" parameter = "/WEB-INF/items/viewItem.jsp"> <forward name="itemError" path="/WEB-INF/items/error.jsp"/> </action> <!-- Create Item: creates new item, then redirects to editing --> <action path = "/createItem" type = "com.xorsystem.items.CreateItemAction"> <forward name="itemCreated" path="/editItem.do" redirect="true"/> </action> <!-- Edit Item: edits new or existing item --> <action path = "/editItem" type = "org.apache.struts.actions.ForwardAction" name = "itemFormOutput" input = "itemError" parameter = "/WEB-INF/items/editItem.jsp"> <forward name="itemError" path="/WEB-INF/items/error.jsp"/> </action> <!-- Store Item: persists item in the storage --> <action path = "/storeItem" type = "com.xorsystem.items.StoreItemAction" name = "itemFormInput"> <forward name="itemStored" path="/viewList.do" redirect="true"/> <forward name="storeError" path="/editItem.do" redirect="true"/> </action> <!-- Delete Item: deletes item from the storage --> <action path = "/deleteItem" type = "com.xorsystem.items.DeleteItemAction" name = "itemFormInput" input = "itemError" validate = "false"> <forward name="itemDeleted" path="/viewList.do" redirect="true"/> <forward name="itemError" path="/showError.do" redirect="true"/> </action> <!-- Show Error: displays generic error page --> <action path = "/showError" type = "org.apache.struts.actions.ForwardAction" name = "itemFormError" parameter = "/WEB-INF/items/error.jsp"/> </action-mappings> <!-- Turn off debug messages, indicate that Action.input is not a URI but a name of a forward element, ensure that responses are not cached --> <controller debug="0" inputForward="true" nocache="true"/> <!-- Resources in WEB-INFclassesApplicationResources.properties --> <message-resources parameter="ApplicationResources"/> </struts-config>
Application defines four form beans, all having request scope:
- ItemListForm displays item list;
- ItemFormInput collects and validates an item value;
- ItemFormOutput provides data to be displayed on result page;
- ItemFormError provides messages for generic error page.
Input data is collected by input form bean, result page is rendered using output form bean. Output actions are always called using redirection thus utilizing Redirect-after-Post pattern. JSP pages are regarded as data-aware HTML and are used for output only. Each JSP page is displayed only by its "owner" action. JSP pages are stored in protected WEB-INF directory and are not accessible from a browser. These measures provide better separation of concerns and compliance with MVC pattern.
Input form bean uses validate method to verify user input. Output form bean has nothing to validate, so its validate method is used to load business data needed for result page. This little trick allows to get rid of custom action classes for output actions.
Input form bean defines only mutators for its fields, while output form bean defines only accessors. This ensures that Struts would not modify fields of output form bean; it also makes the code cleaner.
viewItem, createItem, editItem and deleteItem actions are invoked from a link on viewList.jsp page. storeItem is called from editItem.jsp page when "Store changes" button is clicked.
The glue between presentation and persistence
The application defines four classes which provide interface between Struts presentation layer and Business/Persistence layers.
- BusinessObjStorage is a database wrapper, it provides services to load, store and delete business items from the storage.
- BusinessObj is a business item this application works with, the actual data. Business item has ID, status and value.
- UIControl is a bridge between presentation and business layers. It is the central element of this application, I call it just a control. It aggregates both business and presentation data.
- UIControlFactory is the factory for controls. It creates, locates and removes the controls, and keeps track of them.
Controls are used to display and edit business items. They have a session scope and are stored in the HttpSession object. Each control aggregates a business item and related presentation information such as error messages and page title. Business item aggregated in a control is called a current item because a client works with it. A control is identified by its type and the business item ID.
This application has three types of controls: Edit Control for editing a business item, View Control for viewing a business item and Error Control for displaying generic error messages. The benefit of having different control types is in having different presentation styles. A separate copy of business item as well as a separate list of error messages is used for each presentation style. For example, a user can edit an item in one browser window while viewing a stable persisted copy of this item in another window.
- When an existing item is loaded for viewing, a copy if it is aggregated in the View Control.
- When an existing item is loaded for editing, a copy of it is aggregated in the Edit Control. If the changes to Edit Control are discarded, persistent storage is not updated.
- When a new item is created, it is aggregated in the Edit Control and becomes available for editing. If the item is discarded, nothing is inserted in persistent storage.
It is possible to have multiple instances of the same control type. This feature can be used, for example, to edit several items in different browser windows simultaneously. Apparently, Javascript code handling window closing events can help to decide when a control goes out of scope and can be safely removed. I am not a big Javascipt programmer, so I decided to keep things simple. This application allows to use only one instance of each control at a time. All controls are invalidated when the item list is displayed.
Interaction with business layer
Output form bean gets the name of the action mapping name by analyzing action mapping path. I used this trick to avoid creating two identical form beans for viewItem and editItem actions. Business item is loaded using item ID received with request.
Data item is first looked up among the current items. If the action mapping name is viewItem, then View Control is checked, if the action mapping name is editItem then Edit Control is checked.
- If needed the business item was found in the control, it is reused. The control can contain error messages saved from previous user interaction with the business item. These messages are copied to the request; validate method of output form bean returns null. Struts proceeds as if no errors were encountered, the errors are displayed using standard Struts <html:errors/> tag.
- If needed item cannot be found among current items, it is loaded from persistent storage. Its copy is aggregated in the appropriate control and is presented to the user. Because this application allows only one instance of each control type, unsaved value of current item is lost.
- If business object cannot be found in the storage, a control is created anyway. It is filled with an error message telling that the item could not be found. This error is returned by validate method of the output form bean, and Struts displays the error page. Thus the uniform approach of having business item and errors bundled together in one UI control is preserved.
Action: View List
viewList action reads all items directly from persistent storage and shows them in the table. Standard ForwardAction is used to display viewList.jsp page.
struts-config.xml: <action path = "/viewList" type = "org.apache.struts.actions.ForwardAction" name = "itemListForm" parameter = "/WEB-INF/items/viewList.jsp"/>
Normally the code which loads the item list would be placed in the Action.execute method. Because viewList action does not define a custom action class, the persistence layer is accessed from ItemListForm.validate method.
ItemListForm.java: private ArrayList itemList; public ArrayList getItemList() {return itemList;} public ActionErrors validate(ActionMapping mapping, HttpServletRequest request) { HttpSession session = request.getSession(); itemList = UIControlFactory.getItemList(session); return null; }
Each item is displayed in a separate row along with the links to three basic operations: View, Edit and Delete. Each link contains item ID which is passed as request parameter to viewItem, editItem and deleteItem action respectively.
Action: View Item
viewItem action uses built-in ForwardAction class to display viewItem.jsp page. This action takes item ID as a request parameter. If the item cannot be found, generic error page is shown.
struts-config.xml: ... <action path = "/viewItem" type = "org.apache.struts.actions.ForwardAction" name = "itemFormOutput" input = "itemError" parameter = "/WEB-INF/items/viewItem.jsp"> <forward name="itemError" path="/WEB-INF/items/error.jsp"/> </action>
First the View Control is checked if it already contains needed business item. If not, the item it is loaded from the persistent storage and its copy is aggregated in the View Control. Then business item is displayed on viewItem.jsp page.
ItemFormOutput.java: ... public ActionErrors validate(ActionMapping mapping, HttpServletRequest request) { /* * Search for Business Item in persistent storage * using Business Item ID, then build UI control. * UI control is never null. */ HttpSession session = request.getSession(); UIControl item = UIControlFactory.getItem(session, itemId); if (item.getBusinessItem() != null) { value = item.getBusinessItem().getValue(); status = item.getBusinessItem().getStatus(); title = item.getTitle(); /* * Copy all errors associated with business item * to the request and return null, * so Struts would not jump to error page */ ActionTools.saveErrors(request, item.getErrors()); return null; } else { /* * If Business Item is not found, let Struts * show an error page. Error is kept in the * current item. */ item.getItemErrors().add("ERROR", new ActionError("item.itemnotfound", itemId)); return item.getErrors(); } }
If the business object cannot be found in the storage, the View Control is created anyway, it contains page title and an error message corresponding to the missing business item.
Action: Create Item
createItem does not take any parameters and does not use any form beans.
struts-config.xml: ... <action path = "/createItem" type = "com.xorsystem.items.CreateItemAction"> <forward name="itemCreated" path="/editItem.do" redirect="true"/> </action>
This action creates a new business object with a random ID, aggregates this object with the Edit Control and redirects to editItem action, which displays item content and allows to enter item value. Nothing is saved in persistent storage on this step.
CreateItemAction.java: public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { HttpSession session = request.getSession(); UIControl itemUI = UIControlFactory.createItem(session); String itemId = "id=" + itemUI.getBusinessItem().getId(); return ActionTools.goForward( mapping, "itemCreated", new String[] {itemId}); }
The process of creating and editing of a new item is split between two actions. These actions communicate through redirection, employing Redirect-after-Post pattern. To make new item accessible to editAction, it is aggregated in the Edit Control, which has session scope. Item ID is appended to the URI of editItem action using a helper ActionTools.goForward method. This helper method verifies that target action should be called using redirection. If yes, it creates a new ActionForward object, copies the URI of target action to it and appends needed query parameters. This way editItem action receives ID of an item which has to be edited.
Note: createItem was originally invoked using pushbutton and POST method. But Struts throws an exception if an action corresponding to HTML FORM does not define a form bean. Because createItem action does not use form beans, the HTML FORM had to be changed to a link. Thus, createItem is now called using GET method.
Action: Edit Item
editItem displays a new or existing item and allows to update its value. This action receives an item ID as request parameter.
struts-config.xml: ... <action path = "/editItem" type = "org.apache.struts.actions.ForwardAction" name = "itemFormOutput" input = "itemError" parameter = "/WEB-INF/items/editItem.jsp"> <forward name="itemError" path="/WEB-INF/items/error.jsp"/> </action>
To edit an existing item this action is called from the link on viewList.jsp page, the item ID is provided in the link. To enter a value for a new item this action is called from createItem action using redirection.
The item is looked up in the Edit Control first. If not found, the item is loaded from the persistent storage. Then the item is aggregated with the Edit Control and displayed on editItem.jsp page. This page contains HTML FORM, which shows ID and status of an item, and allows to enter new item value.
editItem.jsp: ... <html:form action="/storeItem.do"> <table> <tr> <td>ID</td> <td><bean:write name="itemFormOutput" property="id"/></td> </tr> <tr> <td>Status<td> <td><bean:write name="itemFormOutput" property="status"/></td> </tr> <tr> <td>Value</td> <td><html:text name="itemFormOutput" property="value"/> </td> </tr> </table> <html:submit value="Save changes"/> <html:hidden name="itemFormOutput" property="id"/> <html:hidden name="itemFormOutput" property="status"/> </html:form>
The form contains two hidden fields. One field is an item ID, which allows to keep track of the current item, it is used by storeItem action as item primary key. Another field is item status, it allows to prevent double insert of the same item from a stale page. When a new item is created, it gets "New" status. After the item is submitted to the server, its status is changed to "Stored". If a user of a caching browser clicks Back button after submitting a new item, he would see the page that was submitted. If the user submits the page again, the server would locate this item in the storage, compare the status and throw an error. On the other hand, submitting the same item with "Stored" status is allowed. This means that an existing item can be updated as often as needed.
The item value is submitted to storeItem action, which persists the item.
Action: Store Item
storeItem saves item information in the persistent storage. Item information is submitted from editItem.jsp page. Item value is validated and stored, then client is redirected to viewList action which displays item list.
struts-config.xml: <action path = "/storeItem" type = "com.xorsystem.items.StoreItemAction" name = "itemFormInput"> <forward name="itemStored" path="/viewList.do" redirect="true"/> <forward name="storeError" path="/editItem.do" redirect="true"/> </action>
storeItem action obtains item ID from Edit Item HTML FORM, locates the Edit Control corresponding to the item, and tries to persist the aggregated item.
StoreItemAction.java: public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { HttpSession session = request.getSession(); String itemId = (ItemFormInput) form.getId(); UIControl item = UIControlFactory.getItem(session, itemId); /* * Store item in the database */ UIControlFactory.storeItem(session, itemId); /* * Could not find or store item, return to editing. */ if (item == null || item.getErrors().size() > 0) { return ActionTools.goForward( mapping, "itemError", new String[] {"id="+itemId}); /* * No errors generated when item was stored, display item list. * viewList does not accept parameters, so it is safe * to use standard findForward. */ } else { return mapping.findForward("itemStored"); } }
BusinessObj and BusinessObjStorage classes validate item before persisting it. If business item does not comply with one or more business rules, error messages are generated and stored placed in the control. If no errors were generated action class redirects to item list page. In case of errors, storeItem action redirects back to editItem action, which redisplays the item along with error messages. Item ID is appended to the request URI in the same way as createItem action does.
Action: Delete Item
deleteItem action removes an item from persistent storage. It receives item ID from the link on viewList.jsp page. Updating model in response to GET request is against semantics of GET method, but I decided to keep it that way.
struts-config.xml: <action path = "/deleteItem" type = "com.xorsystem.items.DeleteItemAction" name = "itemFormInput" input = "itemError" validate = "false"> <forward name="itemDeleted" path="/viewList.do" redirect="true"/> <forward name="itemError" path="/showError.do" redirect="true"/> </action>
ItemFormInput is used to obtain item ID from a browser. Other fields are unimportant, so the validation is turned off. After removing the item this action redirects back to viewList which redisplays item list using viewList.jsp page.
DeleteItemAction.java: HttpSession session = request.getSession(); String itemId = ((ItemFormInput) itemForm).getId(); /* * Try to delete item. If succeeded, transfer to Item List page. * If item could not be found, transfer to generic error page. */ if (UIControlFactory.deleteItem(session, itemId)) { return mapping.findForward("itemDeleted"); } else { ActionErrors errors = new ActionErrors(); errors.add("ERROR", new ActionError("item.itemnotfound")); UIControlFactory.createErrors(session, errors); return mapping.findForward("itemError"); }
If the item cannot be found, generic error page is displayed. This page is shown from its own action, so deleteItem action saves errors in the session before redirecting to error action.
Error Redirection
Generic error page is displayed using a separate action. Usually Struts applications display errors using Action.input property, which forwards to URI of the error page. This approach does not support redirection, which may be a problem if an error is displayed from input action.
The issue is solved in Struts 1.1 with Controller.inputForward property. This property indicates that Action.input is not a URI, but a name of an Action.forward element.
struts-config.xml: <!-- Regular action --> <action ... input="itemError"> ... <forward name="itemError" path="/showError.do" redirect="true"/> </action> ... <!-- Error action --> <action path = "/showError" type = "org.apache.struts.actions.ForwardAction" name = "itemFormError" parameter = "/WEB-INF/items/error.jsp"/> ... <controller inputForward="true"/>
When a form bean returns an error, Struts reads the name of a forward element from Action.input property. The Action.Forward element redirects to the error action, which displays an error page in the same way as other output actions. Because error page is redirected to, it can be refreshed without side effects.
Item error handling
A control may contain error messages. These errors belong either to a business item itself or to a preceding user act. Errors, related to a business item are not cleared when a page with errors is refreshed. Errors, related to a user act are shown only once and cleared on page refresh.
For example, if a user tries to insert a new item into the storage which is full, application would respond with "Storage exhausted" error message on top of the Edit Item page. If this page is refreshed, the error would be cleared.
Errors, which occurred in output action, are shown directly from that action. The error page can be safely refreshed, because output action does not change application status. Thus, viewItem and editItem actions show error page directly.
Errors, which occurred in an input action, are displayed on the generic error page using redirection. deleteAction stores ActionErrors object in the HttpSession and then redirects to showError action, which loads errors from the session and displays them.
The source code
You can download the source code or try the application online. I used Tomcat 4.0.6 and 4.1.29 as servlet container. Struts 1.1 is needed to compile and run the program.
Verify the settings of your browser or proxy and turn off caching to experience the proper application behavior. I tried different browsers, Microsoft Internet Explorer works well simply because it closely adheres to HTTP standard.
FireFox caches all pages even if explicitly instructed not to do so. It works good for standard forwarding applications, but breaks the well-designed ones. With FireFox you can check how the application handles double insert of the same item.
Source code: http://www.superinterface.com/files/prgpattern.zip
Live application: http://www.superinterface.com/rdapp/viewList.do
Afterword
Do you think that this approach is anything but special? Well, it is. But it is far from being a standard.
Recently I decided to use my eBay account again, but I forgot the password. I went to the password recovery page and filled out a small form. After I submitted the form, eBay informed me that a link to the password reset page was sent to my email address. Still having the confirmation page displayed, I clicked Refresh buttons several times. Guess, how many emails I received? What if instead of password recovery confirmation it were a payment confirmation?