Showing posts with label POST-Redirect-GET. Show all posts
Showing posts with label POST-Redirect-GET. Show all posts

Monday, March 19, 2007

Facade POST by GET

Introduction

By default the JSF-generated forms only provides POST functionality. There is no default option to transform the POST forms (<form method="post">) into GET forms (<form method="get">). Not a big problem as far, as it is recommended to use POST only to handle form data. But it can sometimes be very useful to handle forms by GET, like for Search Results. So that you can bookmark or copypaste the URL in the address bar and reinvoke exact the same form action in another browser session.

You can't change POST by GET in JSF, but you can facade POST requests like it are GET requests using a PhaseListener which captures POST requests before the render response and redirects them to a GET request with the request parameters in the URL.

Back to top

Passing GET parameters to backing beans

This technique is also described in Communication. Here is how the JSF could look like:

<h:form>
    <h:inputText id="paramname1" value="#{myBean.paramname1}" />
    <h:inputText id="paramname2" value="#{myBean.paramname2}" />
    <h:commandButton value="submit" action="#{myBean.action}" />
    <h:outputText value="#{myBean.result}" />
</h:form>

Please note: it is important that the ID value of the UIInput component exactly the same is as the name of the bean property. The h:outputText component value is just an example to show the results. It can be anything, for example a h:dataTable is also possible.

Define the request/input parameters as managed properties in the faces-config.xml:

<managed-bean>
    <managed-bean-name>myBean</managed-bean-name>
    <managed-bean-class>mypackage.MyBean</managed-bean-class>
    <managed-bean-scope>request</managed-bean-scope>
    <managed-property>
        <property-name>paramname1</property-name>
        <value>#{param.paramname1}</value>
    </managed-property>
    <managed-property>
        <property-name>paramname2</property-name>
        <value>#{param.paramname2}</value>
    </managed-property>
</managed-bean>

As the request parameters are transferred by GET here, there is absolutely no need to put the managed bean in the session scope. If you really want to store some data in the session, then rather use the SessionMap from the ExternalContext or just define another managed bean which you put in the session scope and use this for session data only.

Here is how the backing bean MyBean.java look like. You can use the @PostConstruct annotation to process the GET parameters. The method with this annotation will only be invoked when the managed properties are all already set.

package mypackage;

import javax.annotation.PostConstruct;

public class MyBean {

    // Init --------------------------------------------------------------------------------------

    private String paramname1;
    private String paramname2;
    private String result;

    // Actions -----------------------------------------------------------------------------------

    @PostConstruct
    public void init() {
        // You can process the GET parameters here.
        result = paramname1 + ", " + paramname2;
    }

    public void action() {
        // You can do your form submit thing here.
    }

    // Getters -----------------------------------------------------------------------------------

    public String getParamname1() {
        return paramname1;
    }

    public String getParamname2() {
        return paramname2;
    }

    public String getResult() {
        return result;
    }

    // Setters -----------------------------------------------------------------------------------

    public void setParamname1(String paramname1) {
        this.paramname1 = paramname1;
    }

    public void setParamname2(String paramname2) {
        this.paramname2 = paramname2;
    }

}

The #{param} is a predefinied variable referring to the request parameter map. Invoking a GET request using the following URL will set the parameter values automatically in the managed bean instance and therefore also in the input fields, thanks to the managed-property configuration in the faces-config.xml:
http://example.com/mypage.jsf?paramname1=paramvalue1&paramname2=paramvalue2

Back to top

Facade POST requests like it are GET requests

This PhaseListener will facade POST requests like it are GET requests.

package mypackage;

import java.io.IOException;
import java.util.ArrayList;
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.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import javax.servlet.http.HttpServletRequest;

/**
 * Facade POST requests like it are GET requests.
 * <p>
 * This phaselistener is designed to be used for JSF 1.2 with request scoped beans. The beans are 
 * expected to have the request parameters definied as managed properties in the faces-config.xml.
 * 
 * @author BalusC
 * @link http://balusc.blogspot.com/2007/03/facade-post-by-get.html
 */
public class PostFacadeGetListener implements PhaseListener {

    // Init --------------------------------------------------------------------------------------

    private static final String ALL_FACES_MESSAGES_ID = "PostFacadeGetListener.allFacesMessages";

    // 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 facesmessages from POST request in session so that they'll be available on the
            // subsequent GET request.
            saveFacesMessages(facesContext);

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

        } else {

            // Restore any facesmessages in the GET request.
            restoreFacesMessages(facesContext);
        }
    }

    /**
     * @see javax.faces.event.PhaseListener#afterPhase(javax.faces.event.PhaseEvent)
     */
    public void afterPhase(PhaseEvent event) {
        // Nothing to do here.
    }

    // Helpers -----------------------------------------------------------------------------------

    /**
     * Save all facesmessages of the given facescontext in session.
     * @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());
            }
        }
    }

    /**
     * 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());
    }

    /**
     * Add POST parameters of the given facescontext as GET parameters to the given url.
     * @param facesContext The facescontext to obtain POST request parameters from.
     * @param url The URL to append the GET query parameters to.
     */
    private static String addQueryParameters(FacesContext facesContext, String url) {

        // Prepare.
        StringBuilder builder = new StringBuilder(url);
        int i = 0;

        // Gather the POST request parameters.
        Map<String, String> requestParameterMap = 
            facesContext.getExternalContext().getRequestParameterMap();

        // Walk through the POST request parameters and determine its source.
        for (String parameterKey : requestParameterMap.keySet()) {
            UIComponent component = facesContext.getViewRoot().findComponent(parameterKey);

            if (component instanceof UIInput) {
                // You may change this if-block if you want. This is done so, because the
                // requestParameterMap can contain more stuff than only UIInput values, for example
                // the UICommand element responsible for the action and the parent UIForm.

                // IMPORTANT: keep in mind that the values of HtmlInputSecret components will also
                // be passed to the GET here so that they would become visible in the address bar.
                // If you want to prevent this, then consider to set some specific request parameter
                // which should let this phaselistener skip the PRG completely for that request.

                // Append POST request parameters as GET query parameters to the URL.
                String parameterName = parameterKey.substring(parameterKey.lastIndexOf(':') + 1);
                String parameterValue = requestParameterMap.get(parameterKey);
                builder.append((i++ == 0 ? "?" : "&") + parameterName + "=" + parameterValue);
            }
        }
        
        return builder.toString();
    }

    /**
     * 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 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);
                }
            }
        }
    }

}

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

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

Now when you submit a form by POST using commandLink or commandButton, then it will automatically be redirected to a GET URL, with the POST parameters visible in the address bar, like if it was a GET request.

Back to top

Hide parameters

If you want to implement the PRG pattern, but you don't want to use visible UIInput components in the page, then just use h:inputHidden to hide the parameter and transfer it from request to request:

<h:form>
    <h:inputText id="paramname1" value="#{myBean.paramname1}" />
    <h:inputHidden id="paramname2" value="#{myBean.paramname2}" />
    <h:commandButton value="submit" action="#{myBean.action}" />
    <h:outputText value="#{myBean.result}" />
</h:form>

In this case, the value of the paramname2 is not visible on the page, but just hidden in a <input type="hidden"> element. And of course it is just visible in the GET request string of the URL.

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

POST-Redirect-GET pattern

WARNING - OUTDATED CONTENT!

This article is targeted on JSF 1.2. For JSF 2.0 with ajax, this can easier be achieved using the new Flash scope. See also this answer.

Doing the PRG in JSF

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 <redirect /> 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.
 * <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 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<String, Object> 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<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);
        }
    }

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

        // Restore them in the given facescontext.
        for (Entry<String, List<FacesMessage>> 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<String, Object> allUIInputValues = (Map<String, Object>)
            facesContext.getExternalContext().getSessionMap().remove(ALL_UIINPUT_VALUES_ID);

        // Restore them in the given facescontext.
        for (Entry<String, Object> 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:

<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