Faking a Postback with JSF + Facelets

By , 27 February 2007

Faking a Postback with JSF + Facelets

Have you ever needed to post a form to a different action URL with JSF? How about posting a form from an email or when a session has expired? The JSF spec doesn't allow for these situations because it requires a view to be present in the user's session before the complete lifecycle will be invoked. Here's a handy trick using an extended Facelets ViewHandler that allows you to make a fake postback to a view that doesn't yet exist in the user's session.

Faking a Postback with JSF + Facelets

The motivation for this exercise was to implement a "quick subscribe" form for a mailing list in the sidebar of a site. I had already implemented a thorough form, validations and actions which I wanted to reuse as much as I could. In fact, all I wanted was a small version of the form that would operate in exactly the same way as the main form, except the results of posting the small form would land the user on the page showing the main form.

JSF doesn't allow you to specify the action attribute of a form, but you can always create your own <form> tag by hand and set the action attribute manually. By reproducing the request parameters as they appear in the real form, you can trick JSF into thinking the original form was submitted. Here is an example of the quick subscribe form I've implemented:

<!-- 
    A miniature version of the subscription form. Variable names
    must match the actual subscription form for the fack postback
    to work properly. The $list variable should be a number which
    is the index of the list to subscribe to from the real form.
  -->
  <form id="fb-quickSubscribe" action="modules/lists/user/subscriptions.jsf">
    <input type="hidden" name="javax.faces.ViewState" value="0:0"/>
    <input type="hidden" name="subscriptions" value="subscriptions"/>
    <input type="hidden" name="submit" value="Submit"/>
    <input type="hidden" name="lists:${list}:subscribe" value="true"/>
    <input type="text" id="name" name="name" value="name" max="255"/><br/>
    <input type="text" id="email" name="email" value="email" max="319"/>
    <input type="image" id="submit" src="modules/core/icons/send.png" width="16" height="16"/>
  </form>

At first, once you've magically crafted your parameters to imitate the form you might think you've finished and the problem is solved. Unfortunately though, it may only be working because you've visited the real form's page and this has created a view for the form in your session. If there is no such view already created, this method will fail because the restoreView() method will return null and the complete lifecycle will not be invoked.

Enter Facelets. Facelets (unlike the JSP ViewHandler) was designed sensibly to separate page rendering into two phases - build and render. We can, in fact, build the view at any time in the lifecycle. This means we can create an extended ViewHandler which overrides restoreView() to build a view for us if one doesn't already exist:

public class SeamlessViewHandler extends FaceletViewHandler {
  
    /** constructor */
    public SeamlessViewHandler(ViewHandler parent) {
        super(parent);
    }
    
    /**
     * To allow the user to postback to a view which was not rendered in the
     * first place, or has expired, we modify the default restoreView 
     * method to build a new view if there was not one to restore. This
     * allows us to invoke the full JSF lifecycle on an initial page view,
     * which is useful for email forms and doing 'fake' postbacks.
     */
    public UIViewRoot restoreView(FacesContext context, String viewId) {
        UIViewRoot viewRoot = super.restoreView(context, viewId);
        if (viewRoot == null) {
            viewRoot = createView(context, viewId);
            context.setViewRoot(viewRoot);
            try {
                this.buildView(context, viewRoot);
                context.getExternalContext().getRequestMap().put(
                        "seamless.viewBuilt", getRenderedViewId(context, viewId));
            } catch (IOException ioe) {
                log.log(Level.SEVERE, "Error Building View", ioe);
            }
        }
        return viewRoot;
    }
    
    /* only build the view if it wasn't built by restoreView */
    protected void buildView(FacesContext context, UIViewRoot viewToRender)
            throws IOException, FacesException {
      
        String viewId = getRenderedViewId(context, viewToRender.getViewId());
        String viewBuilt = (String) context.getExternalContext()
                .getRequestMap().get("seamless.viewBuilt");
        
        if (viewBuilt == null || !viewId.equals(viewBuilt)) {
            super.buildView(context, viewToRender);
        }
    }
}

Voila! The key ingredient to making our fake postbacks work. Have a go yourself by submitting the subscription form using this link.

This view handler is a part of the Furnace Webapp Framework. Please feel free to use it in your own applications!

About Roger Keays

Faking a Postback with JSF + Facelets

Roger Keays is an artist, an engineer, and a student of life. He has no fixed address and has left footprints on 40-something different countries around the world. Roger is addicted to surfing. His other interests are music, psychology, languages, the proper use of semicolons, and finding good food.

Leave a Comment

Please visit https://rogerkeays.com/blog/faking-a-postback-with-jsf-facelets to add your comments.

Comment posted by: Desperated man , 13 years ago

I will check. I use the OpenSessionInView pattern that requires a Servlet filter, maybe this filter is messing up the new recreate view. Belive me, When I solve this problem, I will put the solution here. Thanks.

Comment posted by: , 13 years ago
  1. check your DOCTYPE tag in the html output
  2. check the server response content-type
  3. try <f:view contentType="text/html">
Comment posted by: Desperate man, 13 years ago

My xhtml não render, it opens in the browser (transformed from JSF to HTML).

:(

Comment posted by: Khaled, 15 years ago

ViewExpiredException

I found the only way to avoid this exception is to do add following to web.xml:

<context-param>
    <param-name>javax.faces.STATE_SAVING_METHOD</param-name>
     <param-value>client</param-value>
 </context-param>

 

Comment posted by: Dmitry, 15 years ago

To recover from session expired error (MyFaces 1.2.2 with Facelets 1.1.13)

In web.xml

<context-param>
        <description> Use this to suppress Facelets error page</description>
        <param-name>org.apache.myfaces.ERROR_HANDLING</param-name>
        <param-value>false</param-value>
    </context-param>
    <context-param>
        <description>see bug https://issues.apache.org/jira/browse/MYFACES-1786
        ...if the web container is restarted, a new secret is generated..causing javax.crypto.BadPaddingException
        when JSF tries to restore state
        </description>
        <param-name>org.apache.myfaces.USE_ENCRYPTION</param-name>
        <param-value>false</param-value>
    </context-param>

    <error-page>
        <!-- This works if org.apache.myfaces.ERROR_HANDLING is turned off-->
        <exception-type>javax.faces.application.ViewExpiredException</exception-type>
        <location>/home.jsf?sessionexpired=true</location>
    </error-page>

Comment posted by: lmk, 15 years ago

Hi

how can this solution manage session expired, with JSF 1.2, the viewExpiredException is thrown, and handled somewhere. so, the container cannot manage the session expiration, and  error occurs:

javax.faces.application.ViewExpiredException: viewId:/dataTable.jsf - View /dataTable.jsf could not be restored.

thanks a lot !

Comment posted by: jpcuvelliez, 16 years ago

great! thanks a lot!