Previous Section  < Day Day Up >  Next Section

12.6 Debugging and Error Handling Ideas

JSF is such a new technology that very few best practices for debugging and error handling have emerged yet, but let's look at some possibilities.

12.6.1 Using Standard Java Debugging Techniques

Because JSF lends itself to a design where application logic is implemented as plain Java classes, all the usual techniques—such as Java debuggers and JUnit[3] test cases—can be used for a large part of the application.

[3] JUnit is available at http://www.junit.org/.

12.6.2 Using Standard Web Application Error Handling

A JSF application is also a servlet-based application, so truly exceptional runtime exceptions can be trapped and handled by error handlers declared in the web.xml file:

<web-xml>

  ...

  <error-page>

    <exception-type>java.lang.Throwable</exception-type>

    <location>/errorpage.jsp</location>

  </error-page>

  ...

</web-xml>

The <error-page> element contains an <exception-type> element that names an exception type and a <location> element with a context-relative path for the resource to invoke if the exception is thrown by any request. The resource can be a JSP page or servlet that displays a friendly message instead of the stack trace most web containers show by default. It can also log information about the error, available as request scope attributes as well as through the implicit pageContext variable in a JSP page. If you need to deal with different types of exceptions in different ways, you can declare more than one error handler. The web container picks the one with an exception type that most closely matches the thrown exception. For more on all of this, I recommend my book JavaServer Pages (O'Reilly).

12.6.3 Using Client-Side State Saving During Development

A JSF-specific suggestion is to use client-side state saving during development; in other words, include this declaration in the web.xml file:

<faces-config>

  ...

  <context-param>

    <param-name>javax.faces.STATE_SAVING_METHOD</param-name>

    <param-value>client</param-value>

  </context-param>

  ...

</faces-config>

The first time JSF processes a JSP page with JSF component actions, it creates the components and configures them based on the action element attributes' values. It then saves the whole component tree, either on the server or in the response sent back to the client. When the next request for the same view arrives, JSF restores the saved component tree and does not reconfigure it based on action element attributes in the JSP page. This means that if you change an attribute value in the JSP page, or add or remove an attribute, the changes are ignored as long as the saved state is available.

If you use server-side state saving, one way to get rid of the view state is to kill the session, but this method is not guaranteed to work in all JSF implementations because the specification doesn't say where on the server the state must be saved. While most implementations probably save the state in the session, there are other implementation options.

When you use client-side state saving, the state is always part of the response (typically encoded in hidden form fields in the response) so it can be sent back with the next POST request. But a GET request doesn't include the state, so JSF must recreate the view from the JSP page. You can trigger a GET request in many ways; for example, by selecting the URL in the browser's history list, clicking a link to the page, or typing the URL in the browser's address field. A request without the saved state is guaranteed to create a view from scratch, so all changes you've made in the JSP page are considered in this case.

12.6.4 Capturing State with a PhaseListener

An interesting use of the PhaseListener class we discussed in the previous section is to capture data about the request and all components in each phase and save the data where it can be picked up and displayed later. The captured data can be used to better understand what happens in the different phases and to figure out why things don't work as expected.

I developed a PhaseListener that captures most data of interest, including the component tree structure, all component attributes and properties, and all variables in the request, session and application scopes, before and after each phase. Figure 12-8 shows an example of a response rendered by a servlet where you can analyze the captured data.

Figure 12-8. Analyzing captured data
figs/Jsf_1208.gif

The data is displayed as a tree. The first level of the tree has one node for each point in the lifecycle. For each such node, there's a node that holds the component tree and another that holds the scoped variables. You can compare a component's property values in different phases and see when they change. For instance, if you look at an input component, you see how the submitted value migrates from the submittedValue property to the localValue and value properties.

I've commented out the registration elements for this listener in the example application's faces-config.file because there's a fair amount of processing overhead when it's used. To try it out, remove the comment, restart the web container (or just the application, if your container allows that), and run a few examples to capture data:

<faces-config>

  ...

<!--

  <lifecycle>

    <phase-listener>

      com.mycompany.jsf.event.CaptureStatePhaseListener

    </phase-listener>

  </lifecycle>

-->

  ...

</faces-config>

The source code for the listener and the servlet is included in the examples download, so you can look at the details at your leisure and modify the listener to capture additional data that I left out. All captured data is represented as instances of a simple TreeNode class arranged as a tree:

package com.mycompany.jsf.model;



import java.util.ArrayList;

import java.util.List;



public class TreeNode {

    private TreeNode parent;

    private String name;

    private Object value;

    private boolean isExpanded;

    private boolean isLeafNode;

    private List children;



    public String getName( ) {

        return name;

    }



    public void setName(String name) {

        this.name = name;

    }



    public Object getValue( ) {

        return value;

    }



    public void setValue(Object value) {

        this.value = value;

    }



    public boolean isExpanded( ) {

        return isExpanded;

    }



    public void setExpanded(boolean isExpanded) {

        this.isExpanded = isExpanded;

    }



    public boolean isLeafNode( ) {

        return isLeafNode;

    }



    public void setLeafNode(boolean isLeafNode) {

        this.isLeafNode = isLeafNode;

    }



    public List getChildren( ) {

        if (children == null) {

            children = new ArrayList( );

        }

        return children;

    }



    public void addChild(TreeNode child) {

        if (children == null) {

            children = new ArrayList( );

        }

        child.setParent(this);

        children.add(child);

    }



    public String getPath( ) {

        List chain = new ArrayList( );

        chain.add(getName( ));

        TreeNode parent = getParent( );

        while (parent != null) {

            chain.add(parent.getName( ));

            parent = parent.getParent( );            

        }

        StringBuffer sb = new StringBuffer( );

        for (int i = chain.size( ) - 1; i >= 0; i--) {

            sb.append("/").append(chain.get(i));

        }

        return sb.toString( );

    }



    private TreeNode getParent( ) {

        return parent;

    }



    private void setParent(TreeNode parent) {

        this.parent = parent;

    }

}

The TreeNode class is a bean with five public properties: name, value, isExpanded, isLeafNode, and children. The children property is populated through the addChild( ) method, which also establishes a parent-child relationship used by the getPath() method to return a slash-separated absolute path for a node.

The listener creates a tree per view, with subtrees for the before and after data for each phase. For instance, this is how it captures the application scope variables values:

package com.mycompany.jsf.event;



import java.util.Map;

import java.util.Map.Entry;

import javax.faces.context.FacesContext;

import javax.faces.event.PhaseEvent;

import javax.faces.event.PhaseId;

import javax.faces.event.PhaseListener;

import com.mycompany.jsf.model.TreeNode;

...



public class CaptureStatePhaseListener implements PhaseListener {

    ...

    public void beforePhase(PhaseEvent event) {

        String phaseName = event.getPhaseId( ).toString( );

        if (event.getPhaseId( ) != PhaseId.RESTORE_VIEW) {

            capturePhaseData("Before " + phaseName, event.getFacesContext( ));

        }

    }



    public void afterPhase(PhaseEvent event) {

        String phaseName = event.getPhaseId( ).toString( );

        capturePhaseData("After " + phaseName, event.getFacesContext( ));



        if (event.getPhaseId( ) == PhaseId.RENDER_RESPONSE) {

            captureRequestData(event.getFacesContext( ));

        }

    }

    ...

    private void capturePhaseData(String phaseName, FacesContext context) {

        TreeNode root = getRoot(context);



        TreeNode phaseNode = new TreeNode( );

        phaseNode.setName(phaseName);

        root.addChild(phaseNode);

        ...

        TreeNode varNode = new TreeNode( );

        varNode.setName("Scoped Variables");

        phaseNode.addChild(varNode);



        TreeNode appNode = new TreeNode( );

        appNode.setName("applicationMap");

        Map appMap = context.getExternalContext( ).getApplicationMap( );

        addLeafNodes(appNode, appMap);

        varNode.addChild(appNode);

        ...

    }



    private void addLeafNodes(TreeNode parent, Map map) {

        Iterator i = map.entrySet( ).iterator( );

        while (i.hasNext( )) {

            Map.Entry me = (Map.Entry) i.next( );

            TreeNode leaf = new TreeNode( );

            leaf.setLeafNode(true);

            leaf.setName(me.getKey( ).toString( ));

            leaf.setValue(toString(me.getValue( )));

            parent.addChild(leaf);

        }

    }

    ...

}

The listener calls the capturePhaseData() from the beforePhase() and afterPhase( ) methods. The getRoot() method creates a new root TreeNode instance for the view or picks up an existing one for the view from a Map stored as a session variable. It then creates one TreeNode to hold all data for this call and a child TreeNode to hold all scoped variables and chains the nodes together with the root node.

Next, the capturePhaseData() method gets a Map with all application scope variables from the ExternalContext and calls the addLeafNodes() method. The addLeafNode( ) method loops through the Map and creates one leaf node for each variable, with the variable name and value as the node name and value. It then adds the leaf node to the application scope node. The rest of the CaptureStatePhaseListener class captures the other data in a very similar way.

The servlet, called com.mycompany.jsf.servlets.ShowViewStateServlet and mapped to the URL pattern /showViewState/*, lists all available view state trees if it's called without any parameters or the tree for the view identified by the viewId parameter. We won't look at the code here because we're going to replace it with a custom JSF component in Chapter 14.

    Previous Section  < Day Day Up >  Next Section