Saturday, February 4, 2012

POST-Redirect-GET pattern

Doing the PRG in JSF

http://balusc.blogspot.com/2007/03/post-redirect-get-pattern.html

The POST-Redirect-GET pattern is commonly used in web applications to prevent double submit when refreshing a POST request and navigation problems/annoyances when using browser back/forward button to page between POST requests. Basically it works as follows: after processing the POST request (unnecessarily submitted/displayed form data and/or irritating "Are you sure to resend data?" popups), but right before sending any response to the client, redirect the response to a new GET request. This way refreshing the request won't (re)invoke the initial POST request anymore, but only the GET request.

JSF also supports it, you just need to add line to the navigation case so that it automatically invokes a redirect to the given view after POST. This is highly recommended when you're using commandlinks instead of outputlinks to navigate between pages (which I wouldn't call a good practice; POST should not be used for plain navigation, GET should be used for it). But in case of request based forms you will lost all submitted input values and the eventual FacesMessages. Not very handy if you want to redisplay submitted values after a succesful form submit and/or use FacesMessages to display error/succes message of a form submit.

Fortunately the problem of lost input values and FacesMessages is fixable with a PhaseListener. The below PhaseListener example will implement the PRG pattern that way so that for all POST requests all submitted input values and FacesMessages are saved in session for a once and are restored in the redirected view. All you need to do is just copypaste it and define it once in the faces-config of your JSF webapplication. No need to do any other configurations or make any changes in your JSF webapp.

package mypackage;  import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry;  import javax.faces.FacesException; import javax.faces.application.FacesMessage; import javax.faces.component.UIComponent; import javax.faces.component.UIInput; import javax.faces.component.UIViewRoot; import javax.faces.context.ExternalContext; import javax.faces.context.FacesContext; import javax.faces.event.PhaseEvent; import javax.faces.event.PhaseId; import javax.faces.event.PhaseListener; import javax.servlet.http.HttpServletRequest;  /**  * Implement the POST-Redirect-GET pattern for JSF.  * 

* This phaselistener is designed to be used for JSF 1.2 with request scoped beans of which its * facesmessages and input values should be retained in the new GET request. If you're using session * scoped beans only, then you can safely remove the saveUIInputValues() and * restoreUIInputValues() methods to save (little) performance. If you're using JSF 1.1, * then you can also remove the saveViewRoot() and restoreViewRoot methods, * because it is not needed with its view state saving system. * * @author BalusC * @link http://balusc.blogspot.com/2007/03/post-redirect-get-pattern.html */ public class PostRedirectGetListener implements PhaseListener { // Init --------------------------------------------------------------------------------------- private static final String PRG_DONE_ID = "PostRedirectGetListener.postRedirectGetDone"; private static final String SAVED_VIEW_ROOT_ID = "PostRedirectGetListener.savedViewRoot"; private static final String ALL_FACES_MESSAGES_ID = "PostRedirectGetListener.allFacesMessages"; private static final String ALL_UIINPUT_VALUES_ID = "PostRedirectGetListener.allUIInputValues"; // Actions ------------------------------------------------------------------------------------ /** * @see javax.faces.event.PhaseListener#getPhaseId() */ public PhaseId getPhaseId() { // Only listen during the render response phase. return PhaseId.RENDER_RESPONSE; } /** * @see javax.faces.event.PhaseListener#beforePhase(javax.faces.event.PhaseEvent) */ public void beforePhase(PhaseEvent event) { // Prepare. FacesContext facesContext = event.getFacesContext(); ExternalContext externalContext = facesContext.getExternalContext(); HttpServletRequest request = (HttpServletRequest) externalContext.getRequest(); Map sessionMap = externalContext.getSessionMap(); if ("POST".equals(request.getMethod())) { // Save viewroot, facesmessages and UIInput values from POST request in session so that // they'll be available on the subsequent GET request. saveViewRoot(facesContext); saveFacesMessages(facesContext); saveUIInputValues(facesContext); // Redirect POST request to GET request. redirect(facesContext); // Set the PRG toggle. sessionMap.put(PRG_DONE_ID, true); } else if (sessionMap.containsKey(PRG_DONE_ID)) { // Restore any viewroot, facesmessages and UIInput values in the GET request. restoreViewRoot(facesContext); restoreFacesMessages(facesContext); restoreUIInputValues(facesContext); // Remove the PRG toggle. sessionMap.remove(PRG_DONE_ID); } } /** * @see javax.faces.event.PhaseListener#afterPhase(javax.faces.event.PhaseEvent) */ public void afterPhase(PhaseEvent event) { // Do nothing. } // Helpers ------------------------------------------------------------------------------------ /** * Save the current viewroot of the given facescontext in session. This is important in JSF 1.2, * because the viewroot would be lost in the new GET request and will only be created during * the afterPhase of RENDER_RESPONSE. But as we need to restore the input values in the * beforePhase of RENDER_RESPONSE, we have to save and restore the viewroot first ourselves. * @param facesContext The involved facescontext. */ private static void saveViewRoot(FacesContext facesContext) { UIViewRoot savedViewRoot = facesContext.getViewRoot(); facesContext.getExternalContext().getSessionMap() .put(SAVED_VIEW_ROOT_ID, savedViewRoot); } /** * Save all facesmessages of the given facescontext in session. This is done so because the * facesmessages are purely request scoped and would be lost in the new GET request otherwise. * @param facesContext The involved facescontext. */ private static void saveFacesMessages(FacesContext facesContext) { // Prepare the facesmessages holder in the sessionmap. The LinkedHashMap has precedence over // HashMap, because in a LinkedHashMap the FacesMessages will be kept in order, which can be // very useful for certain error and focus handlings. Anyway, it's just your design choice. Map> allFacesMessages = new LinkedHashMap>(); facesContext.getExternalContext().getSessionMap() .put(ALL_FACES_MESSAGES_ID, allFacesMessages); // Get client ID's of all components with facesmessages. Iterator clientIdsWithMessages = facesContext.getClientIdsWithMessages(); while (clientIdsWithMessages.hasNext()) { String clientIdWithMessage = clientIdsWithMessages.next(); // Prepare client-specific facesmessages holder in the main facesmessages holder. List clientFacesMessages = new ArrayList(); allFacesMessages.put(clientIdWithMessage, clientFacesMessages); // Get all messages from client and add them to the client-specific facesmessage list. Iterator facesMessages = facesContext.getMessages(clientIdWithMessage); while (facesMessages.hasNext()) { clientFacesMessages.add(facesMessages.next()); } } } /** * Save all input values of the given facescontext in session. This is done specific for request * scoped beans, because its properties would be lost in the new GET request otherwise. * @param facesContext The involved facescontext. */ private static void saveUIInputValues(FacesContext facesContext) { // Prepare the input values holder in sessionmap. Map allUIInputValues = new HashMap(); facesContext.getExternalContext().getSessionMap() .put(ALL_UIINPUT_VALUES_ID, allUIInputValues); // Pass viewroot children to the recursive method which saves all input values. saveUIInputValues(facesContext, facesContext.getViewRoot().getChildren(), allUIInputValues); } /** * A recursive method to save all input values of the given facescontext in session. * @param facesContext The involved facescontext. */ private static void saveUIInputValues( FacesContext facesContext, List components, Map allUIInputValues) { // Walk through the components and if it is an instance of UIInput, then save the value. for (UIComponent component : components) { if (component instanceof UIInput) { UIInput input = (UIInput) component; allUIInputValues.put(input.getClientId(facesContext), input.getValue()); } // Pass the children of the current component back to this recursive method. saveUIInputValues(facesContext, component.getChildren(), allUIInputValues); } } /** * Invoke a redirect to the same URL as the current action URL. * @param facesContext The involved facescontext. */ private static void redirect(FacesContext facesContext) { // Obtain the action URL of the current view. String url = facesContext.getApplication().getViewHandler().getActionURL( facesContext, facesContext.getViewRoot().getViewId()); try { // Invoke a redirect to the action URL. facesContext.getExternalContext().redirect(url); } catch (IOException e) { // Uhh, something went seriously wrong. throw new FacesException("Cannot redirect to " + url + " due to IO exception.", e); } } /** * Restore any viewroot from session in the given facescontext. * @param facesContext The involved FacesContext. */ private static void restoreViewRoot(FacesContext facesContext) { // Remove the saved viewroot from session. UIViewRoot savedViewRoot = (UIViewRoot) facesContext.getExternalContext().getSessionMap().remove(SAVED_VIEW_ROOT_ID); // Restore it in the given facescontext. facesContext.setViewRoot(savedViewRoot); } /** * Restore any facesmessages from session in the given FacesContext. * @param facesContext The involved FacesContext. */ @SuppressWarnings("unchecked") private static void restoreFacesMessages(FacesContext facesContext) { // Remove all facesmessages from session. Map> allFacesMessages = (Map>) facesContext.getExternalContext().getSessionMap().remove(ALL_FACES_MESSAGES_ID); // Restore them in the given facescontext. for (Entry> entry : allFacesMessages.entrySet()) { for (FacesMessage clientFacesMessage : entry.getValue()) { facesContext.addMessage(entry.getKey(), clientFacesMessage); } } } /** * Restore any input values from session in the given FacesContext. * @param facesContext The involved FacesContext. */ @SuppressWarnings("unchecked") private static void restoreUIInputValues(FacesContext facesContext) { // Remove all input values from session. Map allUIInputValues = (Map) facesContext.getExternalContext().getSessionMap().remove(ALL_UIINPUT_VALUES_ID); // Restore them in the given facescontext. for (Entry entry : allUIInputValues.entrySet()) { UIInput input = (UIInput) facesContext.getViewRoot().findComponent(entry.getKey()); input.setValue(entry.getValue()); } } }

Activate this phaselistener by adding the following lines to the faces-config.xml:

     mypackage.PostRedirectGetListener 

Now when you submit a form by POST using commandLink or commandButton, then it will automatically be redirected to a GET request, hereby keeping the submitted input values and FacesMessages in the new GET request.

Back to top

1 comment: