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

3 comments:

mail2bansi said...

Another great article from you.
Wondering how to apply your solution to my problem

I want to bookmark JSF page from search results always get appended with JSessionId
like:
http://localhost:8080/namsNG/login.faces;jsessionid=6DD6C3BE51CE502B0036E528BDB253FF

And if i copy/paste this url into another browser instance it returns the Homepage instead of actual page

armen said...

Hello Balusc. Thanks for your articles.
One worst problem for me it is a view expired exception handling, then use jsf/apache myfaces, tomahawk, richfaces, facelets togheter. Is there any solution for this?

Fru said...

Hello Balus,
Very nice code. I will definitely be using it for my application.

I however have one other very big problem and was wondering if you could be of any help. To the problem.

I am developing an application which has a phaselistener to check if a user is already logged in. If the user is not loggedIn then I want to redirect him to the loggin page using for example:

facesContext.getExternalContext().redirect(facesContext.getExternalContext().getRequestContextPath() + "/login.em?currentUrl=" + currentUrl);

My idea is to carry the Servletpath along such that after successful login I can direct the user to the page he was originally on.

redirecting to the login page functions very well for me. My difficultiy is to read the currentUrl from the LoginForm bean. I always seem to be getting null for the currentUrl, even though it could be seen on the url of the login page as a parameter(after redirecting). I will grateful if you could give me some tipps on this issue.