Monday, March 19, 2007

POST-Redirect-GET pattern

POST-Redirect-GET

The POST-Redirect-GET pattern is commonly used in web applications to prevent double submit and navigation problems using browser back button. Basically it works as follows: after processing the POST request, 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 <redirect /> line to the navigation case so that it automatically invokes a redirect to the given view after POST. But this way 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.

This PhaseListener will implement the PRG pattern at the way that all submitted input values and FacesMessages will be restored in the redirected view. You just need to define it once in your JSF webapplication. No need to do any other configurations or 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 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.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.
 * <p>
 * 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 <tt>saveUIInputValues()</tt> and
 * <tt>restoreUIInputValues()</tt> methods to save (little) performance. If you're using JSF 1.1,
 * then you can also remove the <tt>saveViewRoot()</tt> and <tt>restoreViewRoot</tt> 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 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();
        HttpServletRequest request = (HttpServletRequest)
            facesContext.getExternalContext().getRequest();

        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);

            // Resolve action URL and redirect POST request to GET request.
            redirect(facesContext, resolveActionURL(facesContext));

        } else {

            // Restore any viewroot, facesmessages and UIInput values in the GET request.
            restoreViewRoot(facesContext);
            restoreFacesMessages(facesContext);
            restoreUIInputValues(facesContext);
        }
    }

    /**
     * @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<String, List<FacesMessage>> allFacesMessages =
            new LinkedHashMap<String, List<FacesMessage>>();
        facesContext.getExternalContext().getSessionMap()
            .put(ALL_FACES_MESSAGES_ID, allFacesMessages);

        // Get client ID's of all components with facesmessages.
        Iterator<String> clientIdsWithMessages = facesContext.getClientIdsWithMessages();
        while (clientIdsWithMessages.hasNext()) {
            String clientIdWithMessage = clientIdsWithMessages.next();

            // Prepare client-specific facesmessages holder in the main facesmessages holder.
            List<FacesMessage> clientFacesMessages = new ArrayList<FacesMessage>();
            allFacesMessages.put(clientIdWithMessage, clientFacesMessages);

            // Get all messages from client and add them to the client-specific facesmessage list.
            Iterator<FacesMessage> 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<String, Object> allUIInputValues = new HashMap<String, Object>();
        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<UIComponent> components, Map<String, Object> 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);
        }
    }

    /**
     * Resolve the action URL of the current view of the given facescontext.
     * @param facesContext The involved facescontext.
     */
    private static String resolveActionURL(FacesContext facesContext) {

        // Obtain the action URL of the current view.
        return facesContext.getApplication().getViewHandler().getActionURL(
            facesContext, facesContext.getViewRoot().getViewId());
    }

    /**
     * Invoke a redirect to the given URL.
     * @param facesContext The involved facescontext.
     */
    private static void redirect(FacesContext facesContext, String url) {
        try {
            // Invoke a redirect to the given 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);

        // If any, then restore it in the given facescontext.
        if (savedViewRoot != null) {
            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<String, List<FacesMessage>> allFacesMessages = (Map<String, List<FacesMessage>>)
            facesContext.getExternalContext().getSessionMap().remove(ALL_FACES_MESSAGES_ID);

        // If any, then restore them in the given facescontext.
        if (allFacesMessages != null) {
            for (String clientId : allFacesMessages.keySet()) {
                List<FacesMessage> allClientFacesMessages = allFacesMessages.get(clientId);
                for (FacesMessage clientFacesMessage : allClientFacesMessages) {
                    facesContext.addMessage(clientId, 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<String, Object> allUIInputValues = (Map<String, Object>)
            facesContext.getExternalContext().getSessionMap().remove(ALL_UIINPUT_VALUES_ID);

        // If any, then restore them in the given facescontext.
        if (allUIInputValues != null) {
            for (String clientId : allUIInputValues.keySet()) {
                UIInput input = (UIInput) facesContext.getViewRoot().findComponent(clientId);
                input.setValue(allUIInputValues.get(clientId));
            }
        }
    }

}

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

<lifecycle>
    <phase-listener>mypackage.PostRedirectGetListener</phase-listener>
</lifecycle>

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

Copyright - There is no copyright on the code. You can copy, change and distribute it freely. Just mentioning this site should be fair.

(C) March 2007, BalusC

33 comments:

mitch said...

This is really great code. I used it and it helped a lot. Thanks for posting it.

A few comments: this will get called for all jsf pages in your application so you need to make it detect which page it is working on.

For me all the searches in facesContext.getViewRoot().findComponent(paramKey);
were returning null. So I had to just do it for all the parameters rather than ones for specific components.

This was a huge help to me in my project. Thanks.

sandy said...

How to call the Phase listener in from mybean.action() method. On clicking the commandButton how can the user call the redirect method.

BalusC said...

There is no need to call it. Just define the phaselistener in the faces-config.xml. It is always invoked on every request.

sandy said...

I did not understand. My case is in the following way. I have to submit a form from h:form to an absolute url that can read the values or the input data based on the parameter names of the form. On clicking the h:commandButton all the form data should be submitted to this url. I understand that phase listener does some work. But what should be in the action attribute of h:commadButton which will trigger this phaseListener?

BalusC said...

Your problem is a completely different subject. The PRG pattern simply isn't applicable in this. You've posted the same problem in the Sun forum. I've suggested to use a plain vanilla HTML form, or to use Javascript to change the h:form action value so that it points to another URL.

Iceriver said...

That is great post. Thanks.
I see this as a good way to change post to get, so users can record url as bookmarks.
I think this idea (phase listener) can be extended to handle cases to handle mapping, like
to map http://www.abc.com/hello/action/23 to
hello.jsf?action=23
In phase listener, we execute some actions before rendering response page.

Is this a valid thought, balusc?

Jan said...

Hi,
I´ve have the same problem like mitch in its first post. All searches via facesContext.getViewRoot().findComponent(paramKey); return null. I think I know why - because If an navigation-case ist triggered (the action method of the form returns a defined outcome in INVOKE APPLICATION) and therefor the view changes the current view root in RENDER RESPONSE refers to "new" site which maybe does not contain the input components then the view from which the action was performed.

So the solution should be to just go through all request parameters and add them to the url (which is not to good sometimes) or maybe build the url earlier then the end of INVOKE APPLICATION to refer to the original view.

FutureMien said...

This code was really helpful, thanks.

However, I have a question: I am working on a project that uses JBoss Seam as well. I am in the process of implementing code to save the UIViewRoot instance after a post, before redirecting to a get request which functions in a similar manner to your example. However, the web application is raising an exception stating that the UIViewRoot instance is not serializable. I have tried the UIComponentBase#saveState(..) and related methods, but no success yet. Any ideas you may have will be greatly appreciated. Thanks.

NCD said...

BalusC,

Why not just add <redirect /> in your navigation rule in faces-config.xml.

The <redirect /> should accomplish the same thing as your PRG pattern and save a lot of extra work, right?

BalusC said...

FacesMessages and (submitted) input values get lost.

NCD said...

BalusC,

What do you mean by "submitted" input values?

NCD said...

BalusC,

Thanks for your reply to my original post. I would have had problems if I had used the faces-config redirect option blindly.

NCD

iroh said...

BalusC you are the man! This worked phenomenally for what I needed.

Imre said...

Saved me a day.

Thank you.

thomas said...

Hi. First I would like to say that this is a really great code.

But will it work with facelets and if it will, then for which phase should it be set?

Should it be after the facelets have done thier job or before?

Thanks

removeps-groups said...

Very interesting.

What is the meaning of

> But this way you will lost all
> submitted input values and the
> eventual FacesMessages.

I don't know about FacesMessage, but submitted values are not lost. When the page is submitted, it calls set functions on the managed beans. So you you had h:inputText value="#{mybean.firstValue" ... there will be a call to mybean.setFirstValue. If mybean is a session bean, no values will be lost.

Where are the input values and so on saved? It looks like they're saved to facesContext.getExternalContext().sessionMap. What is this thing? Is it the place where all managed JSF session beans are stored?

What does the string returned by resolveActionURL look like? I guess I should try the code, but it will take me a while to set it up.

If you have pages A.jsp, B.jsp, C.jsp, and the user requests "http://localhost/A.faces", what will be the sequence of events? (From the code, it seems that the phase listener code is hit, and since the request is a GET request, the faces context is restored though there is nothing to restore for this initial request).

Afterward pressing the next button on page A, faces-config.xml says to navigate to page B without a redirect. A POST request sent to the server. The phase listener code will save the input values are redirect you a new URL. What is the name of this new URL? When does the code call the set functions on the managed beans used in page A?

BalusC said...

"submitted values are not lost"
You used a session scoped bean for request scoped data instead of a request scoped bean. Learn about the scopes and read the article text and the javadoc/comments of the code.

sri... said...

This works well.
But Im facing an issue with Tomahak components. For example.

I am using h:inputText inside the tag t:dataList in this case. The text boxes inside dataList tag are lost the values, what user typed. Becoz the components are not identifed in the saveUIInputValues method.

Leonardo Pessoa said...

BalusC, that's a great code helped me a lot....
but i am having a problem with it, now i'm using facelets to render my views when i tried to use this listener it not worked, do i have to do something else?

thank you

Mireille's Blog said...

Hi,
In my project I use Facelets too, and the listener did not work :(
That's possible because of render response phase...

ivan.eggel said...

Very nice

mahesh said...

Hello BalusC,

I tried your code its working great for double submits but the users I have are not patient so they click 4 to 5 times and when I tested your code by clicking multiple times its breaking.

Will this code only work for double
submits and not for multiple submits.

BalusC said...

When talking about "double submits" in this article, I meant the double submit which happen when you navigate forward/backward with browser buttons or refresh the submitted page again.

Pressing impatiently on a button is best to be avoided by disabling the button shortly after press using JS, or to freeze the complete window using a transparent iframe.

jane mathew said...

Hi BalusC,

This works fine for me in all cases except where I am populatng data in a h:datatable. I have a file upload in the same jsp. When I submit the page, the values are not retained. Could you please help me.


- Jain mathew

itzli said...

Great!

This a great code, JSF have a lot of inconsistencies. This one is the most important. Thanks to solve it and publicate!

veena said...

Can you please explain again what you mean by the following?

JSF also supports it, you just need to add redirect line to the navigation case so that it automatically invokes a redirect to the given view after POST. But this way 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.



I just took the PostRedirectGetListener and implemented it, configured it in Faces-config.xml and it works like a charm. But not sure what you mean by "add a redirect "

BalusC said...

Instead of the PostRedirectGetListener you can also add the following line to the navigation case:

<redirect/>

But this has thus the described disadvantages.

veena said...

After implementing this in my application, I found a problem.
I have a navigation case to dialogs and these are getting lost. So the dialog does not pop up. Any suggestions on how to fix this problem?

BalusC said...

I do not understand what exactly you're talking about with 'dialogs'. This seems something 3rd-party-library-specific. Post your issue in a more generic forum, e.g. the JSF forum at SDN.

veena said...

By the way, we are using JSF 1.1.

Thanks,

Veena

veena said...

We are using Trinidad. A dialog page can be opened up as a pop up. Instead of page flow to a jspx, you can have a page flow to a dialog simply by returning a prefixed String dialog:String. In the faces-config you define the navigation case for dialog:String.

BalusC said...

Best what you can do is to let the phaselistener skip the PRG pattern based on some request parameter or attribute which indicates that such a dialog is to be fired.

guima said...

BalusC, Just adding PostRedirectGetListener to application and activating this
phaselistener in faces-config.xml didn't worked for me. So, i tried to do this to a simple application found in the book Core JavaServer Faces called login. Changing the scope of the backing bean to request makes the application seems just like using the tag redirect. What am i doing wrong? Thanks.