Monday, October 10, 2011

How Does the JSF Restore_View Phase Restore the View?

The Restore_View phase is one of the key points in the JSF lifecycle. How does JSF restore the view? From where?

When does the Restore_View actually restore the view?

When a user navigates through various pages of an application on the brower, he or she may visit the same jsp file multiple times. A jsp file is a view in JSF. Will this view be saved by the JSF framework and then be restored in every subsequent visit? In general the answer is no. We will use jsf-1.2 in this analysis.

The java file in JSF to do the RESTORE_VIEW phase is com.sun.faces.lifecycle.RestoreViewPhase.java. Two of its methods are the following
/**
     * PRECONDITION: the necessary factories have been installed in the
     * ServletContext attr set. 
     * POSTCONDITION: The facesContext has been initialized with a tree.
     */

    public void execute(FacesContext facesContext) throws FacesException {
        if (logger.isLoggable(Level.FINE)) {
            logger.fine("Entering RestoreViewPhase");
        }
        
        ......
        
        Util.getViewHandler(facesContext).initView(facesContext);        

        // If an app had explicitely set the tree in the context, use that;
        //
        UIViewRoot viewRoot = facesContext.getViewRoot();
        Locale locale = null;
        if (viewRoot != null) {
            if (logger.isLoggable(Level.FINE)) {
                logger.fine("Found a pre created view in FacesContext");
            }
            locale = facesContext.getExternalContext().getRequestLocale();
            facesContext.getViewRoot().setLocale(locale);
            doPerComponentActions(facesContext, viewRoot);
            return;
        }

        ......
        
 if (isPostback(facesContext)) {
     // try to restore the view
            ViewHandler viewHandler = Util.getViewHandler(facesContext);
     if (null == (viewRoot = viewHandler.restoreView(facesContext, viewId))) {
                JSFVersionTracker tracker = 
                        ApplicationAssociate.getInstance(facesContext.getExternalContext()).getJSFVersionTracker();

  // The tracker will be null if the user turned off the 
  // version tracking feature.  
                if (null != tracker) {
      // Get the versions of the current ViewHandler and
      // StateManager.  If they are older than the current
      // version of the implementation, fall back to the
      // JSF 1.1 behavior.
                    Version toTest = tracker.
                            getVersionForTrackedClassName(viewHandler.getClass().getName());
                    Version currentVersion = tracker.getCurrentVersion();
      boolean viewHandlerIsOld = false,
   stateManagerIsOld = false;
      
      viewHandlerIsOld = (toTest.compareTo(currentVersion) < 0);
      toTest = tracker.
   getVersionForTrackedClassName(facesContext.getApplication().getStateManager().getClass().getName());
      stateManagerIsOld = (toTest.compareTo(currentVersion) < 0);

                    if (viewHandlerIsOld || stateManagerIsOld) {
                        viewRoot = viewHandler.createView(facesContext, viewId);
                        if (null != viewRoot) {
                            facesContext.renderResponse();
                        }
                    }
                }
                
                if (null == viewRoot) {
                    Object[] params = {viewId};
                    throw new ViewExpiredException(MessageUtils.getExceptionMessageString(
                            MessageUtils.RESTORE_VIEW_ERROR_MESSAGE_ID, params), viewId);
                }
     }
            if (logger.isLoggable(Level.FINE)) {
                logger.fine("Postback: Restored view for " + viewId);
            }
 }
 else {
            if (logger.isLoggable(Level.FINE)) {
                logger.fine("New request: creating a view for " + viewId);
            }
            // if that fails, create one
            viewRoot = (Util.getViewHandler(facesContext)).
                createView(facesContext, viewId);
            facesContext.renderResponse();
        } 
        assert (null != viewRoot);

        facesContext.setViewRoot(viewRoot);
        doPerComponentActions(facesContext, viewRoot);

        if (logger.isLoggable(Level.FINE)) {
            logger.fine("Exiting RestoreViewPhase");
        }
    }

    /**
     *
     * @return true if the request method is POST or PUT, or the method
     * is GET but there are query parameters, or the request is not an
     * instance of HttpServletRequest.
     */

    private boolean isPostback(FacesContext context) {
        // Get the renderKitId by calling viewHandler.calculateRenderKitId().
        String renderkitId = 
                context.getApplication().getViewHandler().
                calculateRenderKitId(context);
        ResponseStateManager rsm = RenderKitUtils.getResponseStateManager(context,
                renderkitId);
        return rsm.isPostback(context);
    }
A typical case is the following:

Step 1. The user visits a page the first time. And that page has a form.

Step 2. The user enters the data into the form and then submits the page.

In Step 1, JSF will execute two phases Restore_View and Render_Response. It creates the view in the Restoer_View phase. But here it is just an empty ViewRoot and it is put into the facescontext. In the Render_Response phase, JSF will creates the actual UI components and build the view. It will also save the view into the session using the class com.sun.faces.application.StateManagerImpl in this phase. In Step 2, by default, the page is submitted to the same page. JSF will go through all the phases in the lifecycle. Now looking at the method execute(...) of RestoreViewPhase, we can see an if-else clause. The condition is whether or not this mehtod

isPostback(facesContext)
will return true or false. The javadoc of this method states clearly that it will return true if the request method is POST or PUT, or the method is GET but there are query parameters, or the request is not an instance of HttpServletRequest. In our case, it is a form and method is POST, so the condition is true. Hence the view will be restored using the one saved in the session in Step 1. In a different case, suppose it is still the page that the user visited before. But this time it is a redirect as a result of the processing on the server side. Under this situation, the method isPostback(...) will return false. So even though the page has been visited before, a brand new view will be created just like in Step 1 described above. A typical case is that in the Invoke_Application phase of the lifecycle,JSF determines that everything is successful and invokes a redirect call to some page. Now no matter whether or not that page has been visited before, it will be considered to be a new request. And immmediately following the Invoke_Application phase, the actual call to the Render_Response phase will be skipped. Instead, a new JSF lifecycle will start. And since it is regarded a new request, only two phases Restore_View and Render_Response will be executed for the page that is redirected to.

So under the redirect situation, a view will be re-created just as when the page is visited the first time. But this is not the whole story. To make this more precise, there is some difference here from the first time the page is visited. In the appache tomcat implementation, when the page is visited the first time, the Render_Reponse phase also does the following:

  1. Calls the jsp compiler ( org.apache.jasper.compiler.Compiler ). This compiler will removeGeneratedFiles, generateJava, and generateClass for the jsp page. Note that the three phrases in bold are from the actual log file.
  2. Creates the managed beans used in the JSP page. If the scope of the bean is session, the bean will be stored in the session.
In the subsequest visists to the same page, these compiler actions won't happen. And the session-scoped beans do not need to be created again if it is in the same session.

A closer look at how the view is stored and retrieved

Simply put, a view is stored in the session. When a page is visited the first time, two phases Restore_View and Render_Response will be called. In the Render_Response phase, the method is the following:
public void execute(FacesContext facesContext) throws FacesException
{
   ......
   facesContext.getApplication().getViewHandler().renderView(facesContext, facesContext.getViewRoot());
   ......
}
The ViewHandler will eventually call the following:
stateManager.saveView(context);
where stateManager is an instance of com.sun.faces.applicaiton.StateManagerImpl. This method will in turn invokes the following:
/**
     * Return an opaque Object containing sufficient
     * information for this same instance to restore the state of the
     * current {@link UIViewRoot} on a subsequent request.  The returned
     * object must implement java.io.Serializable. If there
     * is no state information to be saved, return null
     * instead.
     *
     * Components may opt out of being included in the serialized view
     * by setting their transient property to true.
     * This must cause the component itself, as well as all of that component's
     * children and facets, to be omitted from the saved  tree structure
     * and component state information.
     *
     * 

This method must also enforce the rule that, for components with * non-null ids, all components that are descendants of the * same nearest {@link NamingContainer} must have unique identifiers. * * For backwards compatability with existing * StateManager implementations, the default * implementation of this method calls {@link #saveSerializedView} * and creates and returns a two element Object array * with element zero containing the structure property * and element one containing the state property of the * SerializedView. * * @since 1.2 * * @param context {@link FacesContext} for the current request * * @throws IllegalStateException if more than one component or * facet within the same {@link NamingContainer} in this view has * the same non-null component id */ public Object saveView(FacesContext context) { SerializedView view = saveSerializedView(context); Object stateArray[] = { view.getStructure(), view.getState() }; return stateArray; }

And the method saveSerializedView(...) does all the dirty work. Basically it will create two objects treeStructure and componentState from the facesContext in the following code:
public SerializedView saveSerializedView(FacesContext context)
          throws IllegalStateException {
  SerializedView result = null;
  Object treeStructure = null;
  Object componentState = null;
  ......
  result = new SerializedView(treeStructure =
              getTreeStructureToSave(context),
                                    componentState =
                                          getComponentStateToSave(context));

  ......
Object stateArray[] = {treeStructure, componentState};
......
actualMap.put(idInActualMap, stateArray);
......
}
In the above code, the Map object actualMap is in another Map logicalMap. And the logicalMap is created in sessionMap which is an object in the session.

Now in another lifecycle when the view needs to be restored, the Restore_View phase will call viewHander.restoreView(facesContext, viewId) in its execute(...) method. And the restoreView(...) method of the ViewHandlerImpl is the following:

public UIViewRoot restoreView(FacesContext context, String viewId,
                                  String renderKitId) {
   ......
   viewRoot = Util.getStateManager(context).restore(context,viewId,renderKitId);
   ......
}
So it basically uses the StateManage again to retrieve the view saved there before.

3 comments:

  1. You said viewHandler eventually calls
    stateManager.saveView(context);
    But I couldn't find this line anywhere in jar

    ReplyDelete
    Replies
    1. Maybe you looked at a jsf jar of different version. In my analysis, jsf-1.2 is used. The code is in jsf-impl-1.2.jar. The renderView(...) method of the class ViewHandlerImpl.java has a call to replaceMarkers(bodyContent, context), which calls stateManager.saveView(context).

      Delete
  2. Does JSF do all of this just to keep the values that the user typed in the form fields? Or is there something else that it needs to keep track of?

    ReplyDelete