Previous Section  < Day Day Up >  Next Section

14.2 Developing a New Component from Scratch

In Chapter 13 and the previous section of this chapter, you've seen how custom renderers can be used to customize the presentation of a standard component, and if that's not enough, how to extend an existing component to get a component with a slightly different behavior. These options cover most possibilities, and you very rarely need to develop a brand new component class. In fact, I had a hard time coming up with an example for this section. I wanted to develop a fairly simple component to illustrate the main points, but it turns out that all the simple components are provided already as standard JSF components, and almost any customization is better done with a custom renderer.

The only component I could think of that must be implemented from scratch (except for the basic behavior inherited from UIComponentBase) is a tree control, where nodes in a tree structure can be expanded or collapsed. It's a far more complex example than what I first had in mind, but I believe it's still a good one. Be aware, though, that this is not for the faint of heart so hold on tight as we dig deep into the intricacies of the JSF component API and its interaction with other JSF classes and the request processing lifecycle.

Figure 14-2 shows the tree control in action, exploring the component state information captured by the CaptureStatePhaseListener discussed in Chapter 12.

Figure 14-2. Tree control with captured component state nodes
figs/Jsf_1402.gif

In Figure 14-2, I use folder icons to represent all nodes in the tree that have children. Clicking an open folder collapses the node, while clicking on a closed folder expands the node. The leaf nodes are displayed as plain text, with the node name in bold. All of this is, of course, configurable. You can easily use input components to represent the nodes in order to let their values be edited instead of just displayed.

Example 14-2 shows the JSP page that creates the view shown in Figure 14-2.

Example 14-2. Using the tree control to show view state (custom/showViewState.jsp)
<%@ page contentType="text/html" %>

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>

<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>

<%@ taglib uri="http://mycompany.com/jsftaglib" prefix="my" %>



<f:view>

  <html>

    <head>

      <title><h:outputText value="State for #{param.viewId}" /></title>

    </head>

    <body>

      <h:form>

        <my:tree value="#{sessionScope['com.mycompany.debug'][param.viewId]}"

          var="node" varNodeToggler="t">

          <f:facet name="openNode">

            <h:panelGroup>

              <h:commandLink action="#{t.toggleExpanded}">

                <h:graphicImage value="/images/folder-open.gif" 

                  style="border: none" />

                <f:param name="viewId" value="#{param.viewId}" />

              </h:commandLink>

              <h:outputText value=" #{node.name}" />

            </h:panelGroup>

          </f:facet>

          <f:facet name="closedNode">

            <h:panelGroup>

              <h:commandLink action="#{t.toggleExpanded}">

                <h:graphicImage value="/images/folder-closed.gif" 

                  style="border: none" />

                <f:param name="viewId" value="#{param.viewId}" />

              </h:commandLink>

              <h:outputText value=" #{node.name}" />

            </h:panelGroup>

          </f:facet>

          <f:facet name="leafNode">

            <h:panelGrid columns="2">

              <h:outputText value="#{node.name}: " style="font-weight: bold" />

              <h:outputText value="#{node.value}" />

            </h:panelGrid>

          </f:facet>

        </my:tree>

      </h:form>

    </body>

  </html>

</f:view>

The tree control consists of three pieces: a custom component, a custom renderer, and a custom model; additionally, for use in a JSP page, there's a custom action.

The <my:tree> custom action creates the tree component and associates it with the tree renderer. The value attribute in Example 14-2 contains a value binding expression that evaluates to an instance of the TreeNode class we looked at in Chapter 12. The tree component accepts an instance of this type as its model but creates an instance of its real model class (TreeModel) around it, as you'll soon see. In this particular example, the TreeNode value is the root node for the data saved by the CaptureStatePhaseListener for the view specified by the viewId request parameter.

Three facets named openNode, closedNode, and leafNode contain components for handling the different types of nodes. For the first two, I use <h:commandLink> action elements with a nested <h:graphicImage> action element to render the folder images the user can click on, plus an <h:outputText> action element to render the node name. For the leaf node, I use a couple of <h:outputText> actions to render the node name and value. The components used as facets have access to the current TreeNode instance through the variable defined by the var attribute of the <my:tree> action element. They also have access to a convenient action method implemented by a bean exposed through the varNodeToggler attribute. The bean provides an action method that expands the node if it's collapsed or collapses it if it's expanded. We'll take a closer look at this bean when we go through the custom component code.

14.2.1 The TreeModel Class

The com.mycompany.jsf.model.TreeModel class is the model class for the tree component. It provides random access to nodes in a tree made up of instances of the TreeNode class we looked at in Chapter 12:

package com.mycompany.jsf.model;



import java.util.StringTokenizer;

import javax.faces.component.NamingContainer;



public class TreeModel {

    private final static String SEPARATOR = 

        String.valueOf(NamingContainer.SEPARATOR_CHAR);



    private TreeNode root;

    private TreeNode currentNode;



    public TreeModel(TreeNode root) {

        this.root = root;

    }



    public TreeNode getNode( ) {

        return currentNode;

    }



    public void setNodeId(String nodeId) {

        if (nodeId == null) {

            currentNode = null;

            return;

        }

        

        TreeNode node = root;

        StringBuffer sb = new StringBuffer( );

        StringTokenizer st = 

            new StringTokenizer(nodeId, SEPARATOR);



        sb.append(st.nextToken( )).append(SEPARATOR);

        while (st.hasMoreTokens( )) {

            int nodeIndex = Integer.parseInt(st.nextToken( ));

            sb.append(nodeIndex);

            try {

                node = (TreeNode) node.getChildren( ).get(nodeIndex);

            }

            catch (IndexOutOfBoundsException e) {

                String msg = "Node node with ID " + sb.toString( ) +

                    ". Failed to parse " + nodeId;

                throw new IllegalArgumentException(msg);

            }

            sb.append(SEPARATOR);

        }

        currentNode = node;

    }

}

The TreeModel constructor takes the root TreeNode reference as its single argument and saves it in an instance variable. The setNodeId() method sets the current node to the specified node ID, which is a colon-separated list of node indexes. For instance, "0:0:1" means "the second child node of the first child node under the root node." The getNode() method returns the current node or null if no node ID is selected.

14.2.2 The UITree Component Class

The com.mycompany.jsf.component.UITree class is the implementation of the tree component. It's quite similar to the standard UIData component. It uses a set of component instances defined as facets to render all objects in the model. It repeatedly calls the setNodeId() method as it traverses the tree in all request processing lifecycle phases and exposes the current node through a request scope variable, and then asks the appropriate facet for the current node to process it. The main difference is that the UITree component works with a tree structure model, while UIData works with a model of rows and columns.

The first part of the class should look familiar from the previous section:

package com.mycompany.jsf.component;



import java.io.IOException;

import java.io.Serializable;

import java.util.HashMap;

import java.util.Iterator;

import java.util.List;

import java.util.Map;



import javax.faces.application.FacesMessage;

import javax.faces.context.FacesContext;

import javax.faces.component.EditableValueHolder;

import javax.faces.component.NamingContainer;

import javax.faces.component.UIComponent;

import javax.faces.component.UIComponentBase;



import javax.faces.el.ValueBinding;

import javax.faces.event.AbortProcessingException;

import javax.faces.event.FacesEvent;

import javax.faces.event.FacesListener;

import javax.faces.event.PhaseId;



import com.mycompany.jsf.model.TreeNode;

import com.mycompany.jsf.model.TreeModel;



public class UITree extends UIComponentBase implements NamingContainer {





    public static final String COMPONENT_TYPE = "com.mycompany.Tree";

    public static final String COMPONENT_FAMILY = "com.mycompany.Tree";



    public UITree( ) {

        super( );

        setRendererType("com.mycompany.Tree");

    }



    public String getFamily( ) {

        return (COMPONENT_FAMILY);

    }

The UITree class extends the UIComponentBase class (the class with default implementations of all UIComponent methods). It also implements the NamingContainer interface, giving it a say in the generation of client IDs for its children. I'll show you how this comes into play later. Just like the UITabLabel component we looked at earlier, UITree declares COMPONENT_TYPE and COMPONENT_FAMILY constants, sets its default renderer type in the constructor, and returns the family name from the getFamily() method.

The component-specific properties are implemented as standard bean accessor methods:

private Object value = null;

private String var = null;

private String varNodeToggler = null;

private TreeModel model = null;



public Object getValue( ) {

    if (value != null) {

        return value;

    }

    ValueBinding vb = getValueBinding("value");

    if (vb != null) {

        return (vb.getValue(getFacesContext( )));

    } else {

        return null;

    }

}



public void setValue(Object value) {

    model = null;

    this.value = value;

}



public void setValueBinding(String name, ValueBinding binding) {

    if ("value".equals(name)) {

        model = null;

    } else if ("var".equals(name) || "nodeId".equals(name) ||

               "varNodeToggler".equals(name)) {

        throw new IllegalArgumentException( );

    }

    super.setValueBinding(name, binding);

}



public String getVar( ) {

    return var;

}



public void setVar(String var) {

    this.var = var;

}



public String getVarNodeToggler( ) {

    return varNodeToggler;

}



public void setVarNodeToggler(String varNodeToggler) {

    this.varNodeToggler = varNodeToggler;

}



private TreeModel getDataModel( ) {

    if (model != null) {

        return model;

    }



    Object value = getValue( );

    if (value != null) {

        if (value instanceof TreeModel) {

            model = (TreeModel) value;

        } else if (value instanceof TreeNode) {

            model = new TreeModel((TreeNode) value);

        }

    }

    return model;

}

The value (i.e., the model) can be set by calling setValue( ) with an instance of either TreeModel or TreeNode or by calling setValueBinding() with a ValueBinding instance that evaluates to an object of one of these two types. The getValue() method returns the value set by the setValue() method or the evaluation result of the value binding if an explicit value hasn't been set. This is standard behavior for all properties that can be set either explicitly or through a value binding; setting an explicit value in effect shadows a value binding.

The setValueBinding() method specializes the default behavior by throwing an exception if it's called with value bindings for the properties var, varNodeToggler, or nodeId, because these properties must only be set to explicit values: var and varNodeToggler hold variables names and must be explicit to allow better type-checking in future versions of the JSF and JSP specifications, and nodeId is used to traverse through the tree in a controlled manner, so a value binding wouldn't be useful for this property. The accessor methods for var and varNodeToggler follow the standard pattern of setting and returning the corresponding instance variable value.

The getDataModel() method returns a previously cached model, if any, or sets the cache variable to either the current value (if it's a TreeModel) or to a new instance of TreeModel (if it's a TreeNode) with the provided value object as the root node. Deciding when to reset the cache is an issue that contributes to the complexity for this type of component. A cache is needed in the first place because the model is used frequently and performance would suffer if a value binding had to be evaluated for every access—causing a new TreeModel instance to be created when a TreeNode is used as the value. On the other hand, not resetting the cache at certain points leads to old data being processed. The UITree component (as well as the UIData component) resets the cache when setValue() or setValueBinding( ) is called to set a new value, before decoding (in case the model has changed since the response was sent), and before rendering (in case the model has been changed by event processing).

Another set of methods provide type-safe accessors for the facets:

public UIComponent getOpenNode( ) {

    return getFacet("openNode");

}



public void setOpenNode(UIComponent openNode) {

    getFacets( ).put("openNode", openNode);

}



public UIComponent getClosedNode( ) {

    return getFacet("closedNode");

}



public void setClosedNode(UIComponent closedNode) {

    getFacets( ).put("closedNode", closedNode);

}



public UIComponent getLeafNode( ) {

    return getFacet("leafNode");

}



public void setLeafNode(UIComponent closedNode) {

    getFacets( ).put("leafNode", closedNode);

}

All these methods call through to the getFacet() method implemented by UIComponentBase to save or get the corresponding facet.

The next set of methods provide access to the nodes in the model:

...

private String nodeId;

...

public TreeNode getNode( ) {

    if (getDataModel( ) == null) {

        return null;

    }

    return (getDataModel( ).getNode( ));

}



public String getNodeId( ) {

    return nodeId;

}



public void setNodeId(String nodeId) {

    saveDescendantState( );



    this.nodeId = nodeId;

    TreeModel model = getDataModel( );

    if (model == null) {

        return;

    }

    model.setNodeId(nodeId);



    restoreDescendantState( );



    Map requestMap =

        getFacesContext( ).getExternalContext( ).getRequestMap( );

    if (var != null) {

        if (nodeId == null) {

            requestMap.remove(var);

        } else {

            requestMap.put(var, getNode( ));

        }

    }

    if (varNodeToggler != null) {

        if (nodeId == null) {

            requestMap.remove(varNodeToggler);

        } else {

            requestMap.put(varNodeToggler, getNodeToggler( ));

        }

    }

}

The getNode() method just calls through to the model and returns the current node or null. The getNodeId() simply returns the corresponding instance variable value.

The setNodeId() method is a hardworking method. To support using input components for the nodes (e.g., input fields, checkboxes, and selections lists) while still using only one set of components for all nodes, the state held by the components for the current node must be saved before a new node is selected. This is handled by the saveDescendantState() method. After saving the state, it's safe to tell the model to select the new node and then call the restoreDescendantState() method to configure the components with the state saved previously for the new node.

The model's current node is exposed to the facets through the variable defined by the var property. The setNodeId() method obtains a Map with all request scope variables and saves the current node under the var variable name, or removes the variable if the current node ID is set to null. It then does the same for the varNodeToggler variable, setting or removing an instance of the bean containing the node-toggler action method.

The saveDescendantState() method and its helper method look like this:

...

private Map saved = new HashMap( );

...

private void saveDescendantState( ) {

    FacesContext context = getFacesContext( );

    Iterator i = getFacets( ).values( ).iterator( );

    while (i.hasNext( )) {

        UIComponent facet = (UIComponent) i.next( );

        saveDescendantState(facet, context);

    }

}



private void saveDescendantState(UIComponent component, 

    FacesContext context) {



    if (component instanceof EditableValueHolder) {

        EditableValueHolder input = (EditableValueHolder) component;

        String clientId = component.getClientId(context);

        SavedState state = (SavedState) saved.get(clientId);

        if (state == null) {

            state = new SavedState( );

            saved.put(clientId, state);

        }

        state.setValue(input.getLocalValue( ));

        state.setValid(input.isValid( ));

        state.setSubmittedValue(input.getSubmittedValue( ));

        state.setLocalValueSet(input.isLocalValueSet( ));

    }



    Iterator kids = component.getChildren( ).iterator( );

    while (kids.hasNext( )) {

        saveDescendantState((UIComponent) kids.next( ), context);

    }

}

The main saveDescendantState() method iterates through all facets and calls the helper method with the same name for each facet. The helper method checks if the facet implements the EditableValueHolder interface, designating it as an input component. If the facet does implement the interface, the helper method saves the component's local value, submitted value, and the flag indicating if the submitted value is valid or not in a Map with the client ID as the key. The method then iterates through the facet's children and calls itself recursively for each one.

The saved state for each input component is held by an instance of an inner class named SavedState:

private static class SavedState implements Serializable {



    private Object submittedValue;

    private boolean valid = true;

    private Object value;

    private boolean localValueSet;



    Object getSubmittedValue( ) {

        return submittedValue;

    }



    void setSubmittedValue(Object submittedValue) {

        this.submittedValue = submittedValue;

    }



    boolean isValid( ) {

        return valid;

    }



    void setValid(boolean valid) {

        this.valid = valid;

    }



    Object getValue( ) {

        return value;

    }



    public void setValue(Object value) {

        this.value = value;

    }



    boolean isLocalValueSet( ) {

        return localValueSet;

    }



    public void setLocalValueSet(boolean localValueSet) {

        this.localValueSet = localValueSet;

    }

}

The SavedState class is a regular bean with accessor methods for all state variables.

The restoreDescendantState() method and its helper method look like this:

private void restoreDescendantState( ) {

    FacesContext context = getFacesContext( );

    Iterator i = getFacets( ).values( ).iterator( );

    while (i.hasNext( )) {

        UIComponent facet = (UIComponent) i.next( );

        restoreDescendantState(facet, context);

    }

}



private void restoreDescendantState(UIComponent component,

    FacesContext context) {



    String id = component.getId( );

    component.setId(id); // Forces the client ID to be reset



    if (component instanceof EditableValueHolder) {

        EditableValueHolder input = (EditableValueHolder) component;

        String clientId = component.getClientId(context);

        SavedState state = (SavedState) saved.get(clientId);

        if (state == null) {

            state = new SavedState( );

        }

        input.setValue(state.getValue( ));

        input.setValid(state.isValid( ));

        input.setSubmittedValue(state.getSubmittedValue( ));

        input.setLocalValueSet(state.isLocalValueSet( ));

    }



    Iterator kids = component.getChildren( ).iterator( );

    while (kids.hasNext( )) {

        restoreDescendantState((UIComponent) kids.next( ), context);

    }

}

These methods are the reverse of the saveDescendantState( ) methods, setting the state of the facets represented by input components to the state saved previously.

The saved state is kept in a Map with the client ID as the key, as I mentioned earlier, so let's look at how the UITree manipulates the client IDs to ensure that a unique ID is used for each node. The UITree class implements the NamingContainer interface. The getClientId() method in the UIComponentBase checks if the component has a parent or grandparent that implements this interface. If so, it calls the naming container's getClientId( ) method and concatenates the component's component ID to the value returned by the naming container's getClientId() method, separated by a colon. The UITree class—which is a naming container—implements getClientId() like this:

public String getClientId(FacesContext context) {

    String ownClientId = super.getClientId(context);

    if (nodeId != null) {

        return ownClientId + NamingContainer.SEPARATOR_CHAR + nodeId;

    } else {

        return ownClientId;

    }

}

It returns the client ID for the UITree concatenated with the current node ID value separated by the value of the NamingContainer.SEPARATOR_CHAR constant, which is a colon. Every time a facet's getClientId() method is called, the UITree getClientId() method is also called, resulting in a unique client ID for each node in the model. The getClientId() implementation in UIComponentBase caches the client ID, so for this to work, the cache must also be reset by calling setId( ) on the facet component in the restoreDescendantState() method.

A JSF request goes through a number of phases defined by a request processing lifecycle, as you may recall from Chapter 8 (Appendix C describes them in more detail). The UITree component specializes the behavior of the request processing lifecycle methods for the Apply Request Values, Process Validations, and Update Model Values phases:

public void processDecodes(FacesContext context) {

    if (!isRendered( )) {

        return;

    }



    model = null;

    saved = new HashMap( );



    processNodes(context, PhaseId.APPLY_REQUEST_VALUES, null, 0);

    setNodeId(null);

    decode(context);

}



public void processValidators(FacesContext context) {

    if (!isRendered( )) {

        return;

    }



    processNodes(context, PhaseId.PROCESS_VALIDATIONS, null, 0);

    setNodeId(null);

}



public void processUpdates(FacesContext context) {

    if (!isRendered( )) {

        return;

    }



    processNodes(context, PhaseId.UPDATE_MODEL_VALUES, null, 0);

    setNodeId(null);

}

The JSF Lifecycle class calls these methods on the root component in the view's component tree, and the default behavior implemented by the UIComponentBase class is to iterate through all facets and children and call the same method on each. Because the UITree component uses one set of facets to process all nodes in its tree model, it must modify this behavior and instead invoke each facet once for each node.

The processDecodes() method resets the model cache, as I mentioned earlier, and also removes the saved state to start fresh for a potentially new model. With exception for resetting the cache, all the phase processing methods follow the same pattern. They call a method called processNodes() with a PhaseId instance that identifies the phase each method is responsible for. During the processing of the nodes, the current node ID is manipulated, so all methods reset it to null when processNodes() returns.

The processNodes() method looks like this:

private void processNodes(FacesContext context, PhaseId phaseId, 

    String parentId, int childLevel) {



    UIComponent facet = null;

    setNodeId(parentId != null ? 

        parentId + NamingContainer.SEPARATOR_CHAR + childLevel :

        "0");

    TreeNode node = getNode( );

    if (node.isLeafNode( )) {

        facet = getLeafNode( );

    } 

    else if (node.isExpanded( )) {

        facet = getOpenNode( );

    }

    else {

        facet = getClosedNode( );

    }



    if (phaseId == PhaseId.APPLY_REQUEST_VALUES) {

        facet.processDecodes(context);

    } else if (phaseId == PhaseId.PROCESS_VALIDATIONS) {

        facet.processValidators(context);

    } else {

        facet.processUpdates(context);

    }

        

    if (node.isExpanded( )) {

        int kidId = 0;

        String currId = getNodeId( );

        Iterator i = node.getChildren( ).iterator( );

        while (i.hasNext( )) {

            TreeNode kid = (TreeNode) i.next( );

            processNodes(context, phaseId, currId, kidId++);

        }

    }

}

In addition to the PhaseId and the FacesContext, the processNodes( ) method has one parameter containing the node ID for the parent node or null for the root node, and one parameter containing the index for the child node to process. The two parameters are used to construct and set the node ID for the node to process. Then the processNodes() method retrieves the facet matching the current node's type and invokes the appropriate phase processing method on the facet. This means that a submitted value (if any) is saved in the facet, events may be queued, and the current node in the model is updated with the submitted value if it passes validation. Finally, if the node is expanded, the processNodes() method calls itself recursively for each child node, adjusting the parent node ID and the child node index for each call.

The facets may queue events during the Apply Request Values and the Process Validations phases, and the UITree component must modify the default event processing behavior to ensure that the model's current node matches the node for which the event was fired before the event listeners are invoked. The first piece of this puzzle is the queueEvent() method:

public void queueEvent(FacesEvent event) {

    super.queueEvent(new ChildEvent(this, event, getNodeId( )));

}

The facets call this method to queue an event, and the UIComponentBase implementation they inherit calls the same method on the component's parent. Usually this means that the implementation in UIViewRoot is called, adding the event to the queue, but for UITree children, the UITree implementation intercepts the call. It wraps the event in an instance of an inner class named ChildEvent and saves the current node ID along with the original event, and then calls the queueEvent() method on its parent to add the wrapped event to the event queue with itself as the source.

Eventually, JSF calls the broadcast() method on the event source component so it can notify its event listeners of the event. The UITree class provides a customized version of this method as well:

public void broadcast(FacesEvent event) throws AbortProcessingException {



    if (!(event instanceof ChildEvent)) {

        super.broadcast(event);

        return;

    }



    ChildEvent childEvent = (ChildEvent) event;

    String currNodeId = getNodeId( );

    setNodeId(childEvent.getNodeId( ));

    FacesEvent nodeEvent = childEvent.getFacesEvent( );

    nodeEvent.getComponent( ).broadcast(nodeEvent);

    setNodeId(currNodeId);

    return;

}

If the event is an instance of the ChildEvent class, the broadcast() method sets the current node for the model to the node ID saved in the event, unwraps the original event, and asks the real source component to broadcast the original event to its listeners.

The ChildEvent class looks like this:

private static class ChildEvent extends FacesEvent {

    private FacesEvent event;

    private String nodeId;



    public ChildEvent(UIComponent component, FacesEvent event, 

        String nodeId) {

        super(component);

        this.event = event;

        this.nodeId = nodeId;

    }



    public FacesEvent getFacesEvent( ) {

        return event;

    }



    public String getNodeId( ) {

        return nodeId;

    }



    public PhaseId getPhaseId( ) {

        return event.getPhaseId( );

    }



    public void setPhaseId(PhaseId phaseId) {

        event.setPhaseId(phaseId);

    }



    public boolean isAppropriateListener(FacesListener listener) {

        return false;

    }



    public void processListener(FacesListener listener) {

        throw new IllegalStateException( );

    }

}

It extends the FacesEvent class, like all JSF event classes must, but implements the isAppropriateListener() and processListener() methods to return false and throw an exception because this event type is intended only for wrapping a real event. It also provides accessor methods for the wrapped event and the node ID, and accessor methods for the phaseId property that delegate to the wrapped event.

The tree renderer that we'll look at next performs the actual rendering, but the UITree class prepares for the rendering with a customized encodeBegin( ) method:

public void encodeBegin(FacesContext context) throws IOException {

    model = null;

    if (!keepSaved(context)) {

        saved = new HashMap( );

    }

    super.encodeBegin(context);

}



private boolean keepSaved(FacesContext context) {

    Iterator clientIds = saved.keySet( ).iterator( );

    while (clientIds.hasNext( )) {

        String clientId = (String) clientIds.next( );

        Iterator messages = context.getMessages(clientId);

        while (messages.hasNext( )) {

            FacesMessage message = (FacesMessage) messages.next( );

            if (message.getSeverity( ).compareTo(FacesMessage.SEVERITY_ERROR)

                >= 0) {

                return true;

            }

        }

    }

    return false;

}

It resets the model cache, as I mentioned earlier, so that a possible value binding is reevaluated in case a new model has been created while processing the events. Depending on the result of validation and event processing, it may also reset the saved component values. The keepSaved() method returns true if there's an error message queued for at least one of the nodes. If so, the saved state is kept in order to render the node with the invalid value; otherwise, the saved state is dropped so that the new values are rendered.

Only two things remain: the mysterious NodeToggler, and how to save and restore the state for the UITree itself. Let's start with the NodeToggler. It's implemented as an inner class:

public static class NodeToggler {

    private UITree tree;



    public NodeToggler(UITree tree) {

        this.tree = tree;

}



    public String toggleExpanded( ) {

        TreeNode node = tree.getDataModel( ).getNode( );

        node.setExpanded(!node.isExpanded( ));

        return "toggledExpanded";

    }

}

The NodeToggler class provides a method named toggleExpand() that gets the current node from the UITree and reverses the value of the node's expanded property. This is the behavior most developers using a tree control want when the user clicks on a node, so making it available as part of the component itself saves them from implementing it over and over. The setNodeId() method we looked at earlier makes an instance of the NodeToggler available through a request scope variable named by the varNodeToggler property. The setNodeId() method gets hold of the bean instance by calling getNodeToggler():

private NodeToggler getNodeToggler( ) {

    if (nodeToggler == null) {

        nodeToggler = new NodeToggler(this);

    }

    return nodeToggler;

}

The method creates an instance the first time it's called and returns the same instance from then on.

That takes care of the components fundamental behavior. The only thing I haven't showed you yet is saving and restoring the component's state, so let's do that now. The saveState( ) and restoreState() methods look like this:

public Object saveState(FacesContext context) {

    Object values[] = new Object[4];

    values[0] = super.saveState(context);

    values[1] = value;

    values[2] = var;

    values[3] = varNodeToggler;

    return (values);

}



public void restoreState(FacesContext context, Object state) {

    Object values[] = (Object[]) state;

    super.restoreState(context, values[0]);

    value = values[1];

    var = (String) values[2];

    varNodeToggler = (String) values[3];

}

Both these methods are defined by an interface called StateHolder that all JSF components implement indirectly because the UIComponent class implements it. JSF may call the saveState() method sometime during the Render Response phase. Exactly when depends on which presentation layer technology and JSF implementation you use. The only requirement is that the method must return an object containing the values that need to be saved in order to restore a new instance of the component class to the same state as the current instance. All standard components in the JSF reference implementation return an Object array, but a Map or a List would work fine as well. The first element in the array is the object returned by the superclass implementation and it holds all generic state, such as value bindings, listeners, converters, validators, and the id property value. The next three elements hold the only properties that are unique to the UITree class: value, var, and varNodeToggler.

The restoreState() method is called during the Restore View State phase with the object returned by saveState() during the previous request. It simply unpacks the data and assigns it to the appropriate instance variables.

There's no guarantee that any of these methods is called, so you should never count on it—for instance, to reset some per-request state variables. The only specification requirement is that if one of these methods is called, the other must also be called. The JSF reference implementation calls them only when the view state is saved on the client. When the state is saved on the server, the reference implementation keeps the whole tree in the session as is instead. Other implementations may use different strategies, e.g., call these methods even when the state is saved on the server to minimize the memory needs.

14.2.3 The TreeRenderer Class

The com.mycompany.jsf.renderer.TreeRenderer class is similar to the renderers we've looked at earlier:

package com.mycompany.jsf.renderer;



import java.io.IOException;

import java.util.Iterator;

import java.util.List;



import javax.faces.context.FacesContext;

import javax.faces.context.ResponseWriter;

import javax.faces.component.NamingContainer;

import javax.faces.component.UIComponent;

import javax.faces.component.UIViewRoot;

import javax.faces.render.Renderer;



import com.mycompany.jsf.component.UITree;

import com.mycompany.jsf.model.TreeNode;



public class TreeRenderer extends Renderer {



    public boolean getRendersChildren( ) {

        return true;

    }



    public void encodeChildren(FacesContext context, UIComponent component)

        throws IOException {



        if (!component.isRendered( )) {

            return;

        }



        if (((UITree) component).getValue( ) == null) {

            return;

        }

        

        ResponseWriter out = context.getResponseWriter( );

        String clientId = null;

        if (component.getId( ) != null && 

            !component.getId( ).startsWith(UIViewRoot.UNIQUE_ID_PREFIX)) {

            clientId = component.getClientId(context);

        }



        boolean isOuterSpanUsed = false;

        if (clientId != null) {

            isOuterSpanUsed = true;

            out.startElement("span", component);

            out.writeAttribute("id", clientId, "id");

        }

        encodeNodes(context, out, (UITree) component, null, 0);

        ((UITree) component).setNodeId(null);

        if (isOuterSpanUsed) {

            out.endElement("span");

        }

    }

It extends the Renderer class and returns true from the getRendersChildren( ) method because it controls the rendering of the UITree component's children.

The encodeChildren() method renders the whole tree. It generates a <span> element with an id attribute if the component has been given an explicit ID (just like the BarRenderer we developed in Chapter 13). The model nodes are rendered recursively by the encodeNodes() method:

private void encodeNodes(FacesContext context, ResponseWriter out,

    UITree tree, String parentId, int childLevel) throws IOException {



    UIComponent facet = null;

    tree.setNodeId(parentId != null ? 

        parentId + NamingContainer.SEPARATOR_CHAR + childLevel : "0");

    TreeNode node = tree.getNode( );

    if (node.isLeafNode( )) {

        facet = tree.getLeafNode( );

    } 

    else if (node.isExpanded( )) {

        facet = tree.getOpenNode( );

    }

    else {

        facet = tree.getClosedNode( );

    }



    encodeRecursive(context, facet);

    out.startElement("br", tree);

    out.endElement("br");

    if (node.isExpanded( )) {

        out.startElement("blockquote", tree);

        int kidId = 0;

        String currId = tree.getNodeId( );

        Iterator i = node.getChildren( ).iterator( );

        while (i.hasNext( )) {

            TreeNode kid = (TreeNode) i.next( );

            encodeNodes(context, out, tree, currId, kidId++);

        }

        out.endElement("blockquote");

    }

}

The encodeNodes() method is similar to the processNodes() method in the UITree class. It first calls the setNodeId() method on the UITree component to set the current node based on the parent node ID and the child node index. It then gets the current node and the facet that corresponds to the current node's type, and lets the facet render the node followed by a <br> element. If the node is expanded, encodeNodes() renders the current node's child nodes recursively within a <blockquote> element.

The facets and all their children are rendered recursively by the encodeRecursive() method:

    private void encodeRecursive(FacesContext context, UIComponent component)

        throws IOException {



        if (!component.isRendered( )) {

            return;

        }



        component.encodeBegin(context);

        if (component.getRendersChildren( )) {

            component.encodeChildren(context);

        } else {

            Iterator i = component.getChildren( ).iterator( );

            while (i.hasNext( )) {

                UIComponent child = (UIComponent) i.next( );

                encodeRecursive(context, child);

            }

        }

        component.encodeEnd(context);

    }

}

This method is identical to the one with the same name in the renderers we developed in Chapter 13, so if you develop many renderers, it's a good idea to put this method in a base class that all other custom renderers extend.

14.2.4 Registering the Component and the Renderer

The components and its renderer are registered like this in the faces-config.xml file:

<faces-config>

  ...

  <component>

    <component-type>

      com.mycompany.Tree

    </component-type>

    <component-class>

      com.mycompany.jsf.component.UITree

    </component-class>

  </component>

  ...

  <render-kit>

    <renderer>

      <component-family>com.mycompany.Tree</component-family>

      <renderer-type>com.mycompany.Tree</renderer-type>

      <renderer-class>

        com.mycompany.jsf.renderer.TreeRenderer

      </renderer-class>

    </renderer>

  </render-kit>

  ...

</faces-config>

This is the same kind of declarations as you've seen earlier, with a <component> element for mapping the component type identifier to the component implementation class and a <renderer> element for mapping the combination of a component family and a renderer type to the renderer implementation class.

14.2.5 The JSP Tag Handler Class

As usual, we also need a custom action tag handler class for the component/renderer combination:

package com.mycompany.jsf.taglib;



import javax.faces.webapp.UIComponentTag;

import javax.faces.component.UIComponent;

import javax.faces.context.FacesContext;

import javax.faces.el.ValueBinding;

import com.mycompany.jsf.component.UITree;



public class TreeTag extends UIComponentTag {

    private String value;

    private String var;

    private String varNodeToggler;



    public void setValue(String value) {

        this.value = value;

    }



    public void setVar(String var) {

        this.var = var;

    }



    public void setVarNodeToggler(String varNodeToggler) {

        this.varNodeToggler = varNodeToggler;

    }



    public String getComponentType( ) {

        return "com.mycompany.Tree";

    }



    public String getRendererType( ) {

        return "com.mycompany.Tree";

    }



    protected void setProperties(UIComponent component) {

        super.setProperties(component);



        FacesContext context = getFacesContext( );

        if (value != null) {

            ValueBinding vb = 

                context.getApplication( ).createValueBinding(value);

            component.setValueBinding("value", vb);

        }

                

        if (var != null) {

            ((UITree) component).setVar(var);

        }

                

        if (varNodeToggler != null) {

            ((UITree) component).setVarNodeToggler(varNodeToggler);

        }

    }

}

The com.mycompany.jsf.taglib.TreeTag follows the same pattern as all other component tag handlers, with setter methods for all action attributes, getComponentType() and getRendererType() methods returning the appropriate values, and a setProperties() method that sets the component properties and attributes to the custom action attribute values. It must be declared also in the TLD for the tag library, just as you've seen for the other tag handlers.

The tag handler class completes the set of classes for the tree control. This custom component implementation is a complex as it gets, so if you managed to follow along through all twists and turns, you should be able to tackle any type of component. Even if you never need to implement a custom component, I hope the example showed you how flexible the component API is and how you can accomplish even complex features—such as preprocessing of child events—by specializing just a few methods.

    Previous Section  < Day Day Up >  Next Section