Thursday, October 6, 2011

A Note on JSF Lifecycle and Navigation Rule

The JSF text books or tutorials usually shows a clear picture of the jsf life cycle, which contains 6 phases. In particular, the last phase is Render Response phase, which comes after the Invoke Application phase. But this does not necessarily always happen depending on how you interpret the phase.

In the following analysis, the source code of jsf-api-1.2 and jsf-impl-1.2 are used.

The core of service(...) method in FacesServlet.java is the following:

try{
      lifecycle.execute(context);
      lifecycle.render(context);
   }catch(...)
   finally{
      context.release();
   }

It is worth noting that the render phase is called separately.

In the LifecycleImpl.java, the main contents of the two methods called by the servlet are the following:

private Phase phases[] = {
   null, // ANY_PHASE placeholder, not a real phase
   new RestoreViewPhase(),
   new ApplyRequestValuesPhase(),
   new ProcessValidationPhase(),
   new UpdateModelValuesPhase(),
   new InvokeApplicationPhase()
};

public void execute(FacesContext context) throws FacesException
{
   ...
   populateFacesELResolverForJsp(context);
   
   for(int i=1; i<phases.length; i++){
   
     if ( context.getRenderResponse() || context.getResponseComplete() ){
        break;
     }
     
     phase( (PhaseId)PhaseId.VALUES.get(i), phase[i], context);
     
     if ( reload( (PhaseId) PhaseId.VALUES.get(i), context)) {
         context.renderResponse();
     }
   }
}

public void render(FacesContext context) throws FacesException
{
    ...
    if ( !context.getResponseComplete()){
        phase(PhaseId.RENDER_RESPONSE, response, context);
    }
}


Note that in the execute(...) method, only the first 5 phases are iterated. Everytime the method phase(...) is called, it will log the message indicating that it is entering this phase and then exiting this phase. This log message can actually bring confusion. When do you think the life cycle has entered Render_Response phase? I think the lifecycel enters this phase when the FacesServlet calls the lifecycle.render(context) method. In this method, if the condition context.getResponseComplete() is true, then the phase(...) call to RENDER_RESPONSE will be skipped. You will never see the log message saying that it is entering or exiting the RENDER_RESPONSE phase. And this case actually happens quite normally. The following is one typical example.

In the example application called "javaquiz" described in the book <<Core JavaServer Faces>>, the first page displays a question. The JSF life cycle only goes through two phases: Restore_View and Render_Response. Now the user types in the correct answer on the screen. The browser will show the next page. In the log file, it shows the lifecycle goes through the phases 1-5. But it does not show phase 6 (RENDER_RESPONSE). Instead, after the phases 1-5, it shows the phase 1(RESTORE_VIEW) and 6(RENDER_RESPONSE). So what happened here? The truth is that after the user submits the answer, the lifecycle goes through phases 1-5. In phase 5 ( Invoke_Application), the NavigationHandlerImpl redirects to the viewId /success.jsp. When the FacesServlet calls the render(...) method. the condition context.getResponseComplete() returns true. So it does not call phase(PhaseId.RENDER_RESPONSE, response, context). Instead, the FacesServlet invokes another lifecycle for the page success.jsp.

In the access log file of Tomcat, you can actually see the following new messages after submitting the answer on the screen. The first log message is for index.faces because this is the page you submitted. The second log message is for success.faces. You actually did not submit this request. This is the result of the redirecting from phase 5.
127.0.0.1 - - [07/Oct/2011:10:26:32 -0400] "POST /javaquiz/index.faces HTTP/1.1" 302 -
127.0.0.1 - - [07/Oct/2011:10:26:36 -0400] "GET /javaquiz/success.faces HTTP/1.1" 200 1032
The actual call by the NavigationHandlerImpl for redirecting is in the following method:
public void handleNavigation(FacesContext context, String fromAction,
                                 String outcome) {
      ...
      ExternalContext extContext = context.getExternalContext();
      ...
      extContext.redirect(newPath), 
      ...
}

The following is the javadoc of the redirect(...) method called by the NavigationHandlerImpl:

/**
* Redirect a request to the specified URL, and cause the
* responseComplete() method to be called on the
* {@link FacesContext} instance for the current request.
* Servlet: This must be accomplished by calling the
* javax.servlet.http.HttpServletResponse method
* sendRedirect().
* Portlet: This must be accomplished by calling the
* javax.portlet.ActionResponse method
* sendRedirect().
* @param url Absolute URL to which the client should be redirected
* @throws IllegalArgumentException if the specified url is relative
* @throws IllegalStateException if, in a portlet environment,
*  the current response object is a RenderResponse
*  instead of an ActionResponse
* @throws IllegalStateException if, in a servlet environment,
*  the current response has already been committed
* @throws IOException if an input/output error occurs
*/
public abstract void redirect(String url)
throws IOException;

No comments:

Post a Comment