FileDocCategorySizeDatePackage
UITree.javaAPI DocExample20650Tue Jun 08 11:26:42 BST 2004com.mycompany.jsf.component

UITree.java

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;

/**
 * This class is a JSF component that represents a tree control.
 * Facets named "openNode", "closedNode" and "leafNode" represent
 * the components resposible for processing nodes of a TreeModel
 * in all request processing lifecycle phases, and may be either
 * input or output components. The class implements the NamingContainer
 * interface to adjust the client IDs for the facets, so that a
 * unique client ID is used for each node even though all nodes
 * are in fact processed by one set of components.
 * <p>
 * The value of this component must be either a TreeModel or a
 * TreeNode representing the root node of a tree.
 * <p>
 * The component is rendered by the "com.mycompany.Tree" renderer
 * type by default.
 *
 * @author Hans Bergsten, Gefion Software <hans@gefionsoftware.com>
 * @version 1.0
 */
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";

    /**
     * The component's value, either a TreeModel or a TreeNode instance.
     */
    private Object value = null;

    /**
     * The TreeModel used by this component, either set explicitly as
     * the value of created as a wrapper around a TreeNode value.
     */
    private TreeModel model = null;

    /**
     * The current node ID.
     */
    private String nodeId;

    /**
     * This map contains SavedState instances for each node in the
     * tree, keyed by the client identifier of the component representing
     * node, which contains the nodeId value for uniqueness.
     */
    private Map saved = new HashMap();

    /**
     * The name of the request scope variable through which the data
     * object for the current node is exposed.
     */
    private String var = null;

    /**
     * The name of the request scope variable through which the node
     * toggler object is exposed.
     */
    private String varNodeToggler = null;

    /**
     * The NodeToggler instance.
     */
    private NodeToggler nodeToggler;

    /**
     * Creates an instance and sets the renderer type to
     * "com.mycompany.Tree".
     */
    public UITree() {
        super();
        setRendererType("com.mycompany.Tree");
    }

    /**
     * Returns the COMPONENT_TYPE value.
     */
    public String getFamily() {
        return (COMPONENT_FAMILY);
    }

    /**
     * Returns the name of the request scope through which the data object
     * for the current node is exposed.
     */
    public String getVar() {
        return var;
    }

    /**
     * Sets the name of the request-scope variable through which the
     * data object for the current node is exposed.
     */
    public void setVar(String var) {
        this.var = var;
    }

    /**
     * Returns the name of the request scope through which the NodeToggler
     * is exposed.
     */
    public String getVarNodeToggler() {
        return varNodeToggler;
    }

    /**
     * Sets the name of the request scope through which the NodeToggler
     * is exposed.
     */
    public void setVarNodeToggler(String varNodeToggler) {
        this.varNodeToggler = varNodeToggler;
    }

    /**
     * Returns the single instance of the NodeToggler, creating it
     * if needed.
     */
    private NodeToggler getNodeToggler() {
	if (nodeToggler == null) {
	    nodeToggler = new NodeToggler(this);
	}
	return nodeToggler;
    }

    /**
     * Returns the "openNode" facet.
     */
    public UIComponent getOpenNode() {
        return getFacet("openNode");
    }

    /**
     * Sets the "openNode" facet.
     */
    public void setOpenNode(UIComponent openNode) {
        getFacets().put("openNode", openNode);
    }

    /**
     * Returns the "closedNode" facet.
     */
    public UIComponent getClosedNode() {
        return getFacet("closedNode");
    }

    /**
     * Sets the "openNode" facet.
     */
    public void setClosedNode(UIComponent closedNode) {
        getFacets().put("closedNode", closedNode);
    }

    /**
     * Returns the "leafNode" facet.
     */
    public UIComponent getLeafNode() {
        return getFacet("leafNode");
    }

    /**
     * Sets the "leafNode" facet.
     */
    public void setLeafNode(UIComponent closedNode) {
        getFacets().put("leafNode", closedNode);
    }

    /**
     * Returns the current node from the TreeModel, or "null" if
     * no node is currently processed.
     */
    public TreeNode getNode() {
	if (getDataModel() == null) {
	    return null;
	}
        return (getDataModel().getNode());
    }

    /**
     * Returns the current node ID, or "null" if no node is currently
     * processed.
     */
    public String getNodeId() {
        return nodeId;
    }

    /**
     * Sets the node ID, saving the state of all facet components
     * for the previous node ID and restoring it for the new
     * if it was saved for the new node ID previously, and exposes the
     * node for the new node ID and the NodeToggler through their
     * request scope variables.
     */
    public void setNodeId(String nodeId) {
        // Save current state for the previous node
        saveDescendantState();

        this.nodeId = nodeId;
	TreeModel model = getDataModel();
	if (model == null) {
	    return;
	}
	model.setNodeId(nodeId);

        // Reset current state information for the new row index
        restoreDescendantState();

        // Clear or expose the current row data as a request scope attribute
	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());
            }
        }
    }

    /**
     * Returns the component value, set explicitly or through a
     * ValueBinding, or null if the value isn't set.
     */
    public Object getValue() {
	if (value != null) {
	    return value;
	}
	ValueBinding vb = getValueBinding("value");
	if (vb != null) {
	    return (vb.getValue(getFacesContext()));
	} else {
	    return null;
	}
    }

    /**
     * Sets the component value, either a TreeModel or a TreeNode,
     * and resets the previously cached model, if any.
     */
    public void setValue(Object value) {
        this.model = null;
        this.value = value;
    }

    /**
     * Throws an IllegalArgumentException if the name is "var",
     * "varToggler" or "nodeId" (these properties must be set
     * to explicit values); otherwise, delegates to the superclass.
     */
    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);
    }

    /**
     * Returns a client ID composed from the regular client ID
     * (returned by the superclass getClientId() method) and the
     * current node ID (if any) separated by a colon.
     */
    public String getClientId(FacesContext context) {
	String ownClientId = super.getClientId(context);
        if (nodeId != null) {
            return ownClientId + NamingContainer.SEPARATOR_CHAR + nodeId;
        } else {
            return ownClientId;
        }
    }

    /**
     * Wraps the event in a ChildEvent and calls the superclass method
     * add it to the queue with this component as the event source.
     */
    public void queueEvent(FacesEvent event) {
        super.queueEvent(new ChildEvent(this, event, getNodeId()));
    }

    /**
     * If the event is a ChildEvent, unwraps the real event, calls
     * setNodeId() with the node ID in the ChildEvent and delegates
     * the processing to the real event source.
     */
    public void broadcast(FacesEvent event) throws AbortProcessingException {

	if (!(event instanceof ChildEvent)) {
	    super.broadcast(event);
	    return;
	}

	// Set up the correct context and fire our wrapped event
	ChildEvent childEvent = (ChildEvent) event;
        String currNodeId = getNodeId();
	setNodeId(childEvent.getNodeId());
	FacesEvent nodeEvent = childEvent.getFacesEvent();
	nodeEvent.getComponent().broadcast(nodeEvent);
        setNodeId(currNodeId);
	return;
    }

    /**
     * Before delegating to the superclass, resets the saved
     * per-node state for facet input components unless it is 
     * needed to rerender the current page with errors, as
     * determined by the keepSaved() method.
     */
    public void encodeBegin(FacesContext context) throws IOException {
        model = null; // Re-evaluate even with server-side state saving
        if (!keepSaved(context)) {
            saved = new HashMap();
        }
        super.encodeBegin(context);
    }

    /**
     * If "rendered" is true, resets the cached model and saved
     * per-node state, calls processNodes() with a PhaseId for
     * this phase, resets the node ID, and calls the decode() method.
     */
    public void processDecodes(FacesContext context) {
        if (!isRendered()) {
            return;
        }

        model = null; // Re-evaluate even with server-side state saving
        saved = new HashMap(); // We don't need saved state here

	processNodes(context, PhaseId.APPLY_REQUEST_VALUES, null, 0);
	setNodeId(null);
	decode(context);
    }

    /**
     * If "rendered" is true, calls processNodes() with a PhaseId for
     * this phase, resets the node ID, and calls the decode() method.
     */
    public void processValidators(FacesContext context) {
        if (!isRendered()) {
            return;
        }

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

    /**
     * If "rendered" is true, calls processNodes() with a PhaseId for
     * this phase, resets the node ID, and calls the decode() method.
     */
    public void processUpdates(FacesContext context) {
        if (!isRendered()) {
            return;
        }

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

    /**
     * Returns the component state to be saved as part of the view
     * state.
     */
    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);
    }

    /**
     * Restores the component to the provided state, previously 
     * returned by the saveState() method.
     */
    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];
    }

    /**
     * Returns the cached model, if any, or the components value,
     * as-is if it's a TreeModel or wrapped in a new TreeModel if
     * it's a TreeNode, saving a reference in the "model" variable.
     */
    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;
    }

    /**
     * Recursively process all nodes at the root of the tree and
     * all nodes under an open node for the provided phase, i.e.,
     * by calling processDecodes(), processValidators() or
     * processUpdates() on the facet representing the node type.
     */
    private void processNodes(FacesContext context, PhaseId phaseId, 
			 String parentId, int childLevel) {

	// Iterate over all expanded nodes in the model and process the
	// appropriate facet for each node.
	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++);
	    }
	}
    }

    /**
     * Returns "true" if there's at least one error message queued
     * for a client ID matching one of the nodes.
     */
    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;
    }


    /**
     * Restores state information for all facets by calling the
     * restoreDescendantState(UIComponent, FacesContext) method
     * on each facet.
     */
    private void restoreDescendantState() {
        FacesContext context = getFacesContext();
        Iterator i = getFacets().values().iterator();
        while (i.hasNext()) {
            UIComponent facet = (UIComponent) i.next();
	    restoreDescendantState(facet, context);
        }
    }

    /**
     * Restore state information for the specified component and its
     * children from the previously saved state, if any.
     */
    private void restoreDescendantState(UIComponent component,
                                        FacesContext context) {

        // Reset the client identifier for this component
        String id = component.getId();
        component.setId(id); // Forces 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());
            // This *must* be set after the call to setValue(), since
            // calling setValue() always resets "localValueSet" to true.
            input.setLocalValueSet(state.isLocalValueSet());
        }

        Iterator kids = component.getChildren().iterator();
        while (kids.hasNext()) {
            restoreDescendantState((UIComponent) kids.next(), context);
        }
    }

    /**
     * Saves state information for all facets by calling the
     * saveDescendantState(UIComponent, FacesContext) method
     * on each facet.
     */
    private void saveDescendantState() {
        FacesContext context = getFacesContext();
        Iterator i = getFacets().values().iterator();
        while (i.hasNext()) {
            UIComponent facet = (UIComponent) i.next();
	    saveDescendantState(facet, context);
        }
    }

    /**
     * Saves state information for the specified component, if it
     * implements the EditableValueHolder interface, and its
     * children.
     */
    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);
        }
    }

    /**
     * Private class to represent saved state information.
     */
    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;
	}
    }

    /**
     * Private class to wrap an event with a node ID.
     */
    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();
	}
    }

    /**
     * A class with an action method that toggles the "expanded"
     * property value of the current node. It's intended to be
     * bound to command components used as the "closedNode" and
     * "openNode" facets.
     */
    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";
	}
    }
}