Wednesday, 2 September 2009

Calling JSF actions by URL

In JSF 1, there is no provided functionality to invoke an action (by this I mean a java function, not a JSF action invoked by a component). JSF 2 adds functionality to support this, through f:viewParam / f:event and the PreRenderViewEvent.

However in JSF 1, you can bind a phase listener to a specific page using the f:phaseListener tag to call the code before the Render Phase. To make sure that the code is only called when the page is access via the url and not from a postback caused by a component you can use the function added in JSF 1.2: ResponseStateManager.isPostback(FacesContext context).

First off we configure the phase listener in the page (action.xhtml):
<html xmlns="http://www.w3.org/1999/xhtml"
 xmlns:ui="http://java.sun.com/jsf/facelets"
 xmlns:f="http://java.sun.com/jsf/core">
<f:phaselistener binding="#{urlActionBean.listener}" />

Next is the phase listener and action code:
public class UrlActionBean
{
   /** value of request paramer somevalue=value */
   @NamedEL("param.somevalue")
   private String someValue;
   
   /**
    * Listener activated on render phase calling when not postback.
    */
   public PhaseListener getListener()
   {
      return new PhaseListener() {
         public void beforePhase(PhaseEvent event)
         {
            //only ever call from a 'GET' request (ie not action pressed).
            if (!isPostBack())
               processAction();
         }

         public PhaseId getPhaseId()
         {
            return PhaseId.RENDER_RESPONSE;
         }
      
         public void beforePhase(PhaseEvent event)
         {
         }
      };
   }

   public static boolean isPostBack()
   {
      FacesContext context = FacesContext.getCurrentInstance();
      return context.getRenderKit().getResponseStateManager().isPostback(context);
   }
      
   /**
    * Called after values injected, automatically calls
    */
   public void processAction()
   {
      //only do something if value provided
      if (someValue == null || someValue.length() == 0)
      {
         //ignore
         return;
      }

      String forward = null;
      String viewId = WebUtil.viewId();
      // do action based on viewId. This allows the bean to be used by multiple pages.
      // store result of action
      if ("action".equals(viewId))
         forward = doAction();
         
      //forward to page
      if (forward != null)
      {
         redirect(forward);
         return;
      }
   }
   
   /**
    * Causes the navigation handler to switch pages / send redirect.
    * @param outcome page result mapped in faces-config.xml
    */
   public static void redirect(String outcome)
   {
      FacesContext context = FacesContext.getCurrentInstance();
      context.getApplication().getNavigationHandler().handleNavigation(context, null, outcome);
   }   
}

The listener is a render phase listener, that only calls the action handling when it isn't a postback. The action handling code then checks that the required parameter is supplied and then calls an action based on the viewId. This allows for multiple views to use the same bean, but you could have one bean per url and hence not need this check. The field someValue, gets injected the request parameter named 'somevalue' and since it is injected the bean needs to be request scoped as the injection only occurs when the bean is first created.

In this I use the Guice injection of EL expressions described here, to inject request parameters, however they could be looked up using the ELContext if you can't use this method.