You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@myfaces.apache.org by lu...@apache.org on 2010/01/29 00:13:18 UTC

svn commit: r904296 [3/3] - in /myfaces/tomahawk/trunk/core20/src/main/java/org/apache/myfaces: component/html/ component/html/ext/ custom/aliasbean/ custom/datascroller/ custom/document/ custom/inputHtml/ custom/schedule/ custom/tabbedpane/ custom/tre...

Added: myfaces/tomahawk/trunk/core20/src/main/java/org/apache/myfaces/custom/tree/HtmlTree.java
URL: http://svn.apache.org/viewvc/myfaces/tomahawk/trunk/core20/src/main/java/org/apache/myfaces/custom/tree/HtmlTree.java?rev=904296&view=auto
==============================================================================
--- myfaces/tomahawk/trunk/core20/src/main/java/org/apache/myfaces/custom/tree/HtmlTree.java (added)
+++ myfaces/tomahawk/trunk/core20/src/main/java/org/apache/myfaces/custom/tree/HtmlTree.java Thu Jan 28 23:13:17 2010
@@ -0,0 +1,1071 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.myfaces.custom.tree;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.faces.component.UIViewRoot;
+import javax.faces.component.html.HtmlPanelGroup;
+import javax.faces.context.FacesContext;
+import javax.faces.el.ValueBinding;
+
+import org.apache.myfaces.custom.tree.event.TreeSelectionEvent;
+import org.apache.myfaces.custom.tree.event.TreeSelectionListener;
+import org.apache.myfaces.custom.tree.model.TreeModel;
+import org.apache.myfaces.custom.tree.model.TreeModelEvent;
+import org.apache.myfaces.custom.tree.model.TreeModelListener;
+import org.apache.myfaces.custom.tree.model.TreePath;
+
+
+/**
+ * A tree data component. 
+ * Unless otherwise specified, all attributes accept static values or EL expressions.
+ * <p>
+ * Tree implementation based on javax.swing.JTree.
+ * </p>
+ * <p>
+ * The tree model is assigned by using a value binding named <code>model</code>
+ * and is not stored in view state.
+ * </p>
+ * <p>
+ * A hierarchy of {@link HtmlTreeNode}objects is used to represent the current
+ * expanded state of the tree. The root node is held as a faces named * 
+ * <code>rootNode</code>.
+ * </p>
+ *
+ * @JSFComponent
+ *   name = "t:tree"
+ *   tagClass = "org.apache.myfaces.custom.tree.taglib.TreeTag"
+ *   tagSuperclass = "org.apache.myfaces.custom.tree.taglib.AbstractTreeTag"
+ *   type = "org.apache.myfaces.HtmlTree"
+ *   tagHandler = "org.apache.myfaces.custom.tree.taglib.TreeTagHandler"
+ *
+ * @JSFJspProperty name = "headerClass" returnType = "java.lang.String"
+ * @JSFJspProperty name = "footerClass" returnType = "java.lang.String"
+ * @JSFJspProperty name = "expandRoot" returnType = "boolean" literalOnly="true" inheritedTag="true"
+ * @JSFJspProperty name = "style" tagExcluded = "true"
+ * @author <a href="mailto:oliver@rossmueller.com">Oliver Rossmueller </a>
+ * @version $Revision: 672986 $ $Date: 2008-06-30 23:13:55 -0500 (lun, 30 jun 2008) $
+ */
+public class HtmlTree extends HtmlPanelGroup implements TreeModelListener
+{
+    public static final String COMPONENT_TYPE = "org.apache.myfaces.HtmlTree";
+    public static final String COMPONENT_FAMILY = "org.apache.myfaces.HtmlTree";
+    private static final String DEFAULT_RENDERER_TYPE = "org.apache.myfaces.HtmlTree";
+    
+    public static final long DEFAULT_EXPIRE_LISTENERS = 8 * 60 * 60 * 1000; // 8 hours
+    private static final String FACET_ROOTNODE = "rootNode";
+    private static final String PREVIOUS_VIEW_ROOT = HtmlTree.class.getName() + ".PREVIOUS_VIEW_ROOT";
+    private static final int EVENT_CHANGED = 0;
+    private static final int EVENT_INSERTED = 1;
+    private static final int EVENT_REMOVED = 2;
+    private static final int EVENT_STRUCTURE_CHANGED = 3;
+    private static int counter = 0;
+
+    private IconProvider iconProvider;
+    private boolean itemStatesRestored = false;
+    private String var;
+    private String nodeClass;
+    private String rowClasses;
+    private String columnClasses;
+    private String selectedNodeClass;
+    private String iconClass;
+    private String iconLine;
+    private String iconNoline;
+    private String iconChildFirst;
+    private String iconChildMiddle;
+    private String iconChildLast;
+    private String iconNodeOpen;
+    private String iconNodeOpenFirst;
+    private String iconNodeOpenMiddle;
+    private String iconNodeOpenLast;
+    private String iconNodeClose;
+    private String iconNodeCloseFirst;
+    private String iconNodeCloseMiddle;
+    private String iconNodeCloseLast;
+    private int uniqueIdCounter = 0;
+    private int[] selectedPath;
+    private int internalId;
+    private Long expireListeners;
+
+
+    /**
+     * <p/>
+     * Default constructor.
+     * </p>
+     */
+    public HtmlTree()
+    {
+        internalId = counter++;
+    }
+
+
+    /**
+     * @JSFProperty
+     *   jspName = "value"
+     *   required = "true"
+     *   inheritedTag = "true"
+     */
+    public TreeModel getModel(FacesContext context)
+    {
+        ValueBinding binding = getValueBinding("model");
+
+        if (binding != null)
+        {
+            TreeModel model = (TreeModel) binding.getValue(context);
+            if (model != null)
+            {
+                return model;
+            }
+        }
+        return null;
+    }
+
+
+    public String createUniqueId(FacesContext context)
+    {
+        return getClientId(context).replaceAll(":", "_") + "_node_" + uniqueIdCounter++;
+    }
+
+
+    public void addTreeSelectionListener(TreeSelectionListener listener)
+    {
+        addFacesListener(listener);
+    }
+
+
+    public IconProvider getIconProvider()
+    {
+        return iconProvider;
+    }
+
+
+    public void setIconProvider(IconProvider iconProvider)
+    {
+        this.iconProvider = iconProvider;
+    }
+
+
+    /**
+     * @JSFProperty
+     * @return Returns the var.
+     */
+    public String getVar()
+    {
+        return getStringValue(var, "var");
+    }
+
+
+    /**
+     * @param var The var to set.
+     */
+    public void setVar(String var)
+    {
+        this.var = var;
+    }
+
+    protected String getStringValue(String value, String vbName)
+    {
+        if(value != null)
+        {
+            return value;
+        }
+        ValueBinding vb = getValueBinding(vbName);
+        if(vb != null)
+        {
+            Object obj = vb.getValue(getFacesContext());
+            if(obj != null)
+            {
+                return obj.toString();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * @JSFProperty
+     */
+    public String getIconLine()
+    {
+        return getStringValue(iconLine, "iconLine");
+    }
+
+    public void setIconLine(String iconLine)
+    {
+        this.iconLine = iconLine;
+    }
+
+    /**
+     * @JSFProperty
+     */
+    public String getIconNoline()
+    {
+        return getStringValue(iconNoline, "iconNoline");
+    }
+
+
+    public void setIconNoline(String iconNoline)
+    {
+        this.iconNoline = iconNoline;
+    }
+
+    /**
+     * @JSFProperty
+     */
+    public String getIconChildFirst()
+    {
+        return getStringValue(iconChildFirst, "iconChildFirst");
+    }
+
+
+    public void setIconChildFirst(String iconChildFirst)
+    {
+        this.iconChildFirst = iconChildFirst;
+    }
+
+
+    /**
+     * @JSFProperty
+     */
+    public String getIconChildMiddle()
+    {
+        return getStringValue(iconChildMiddle, "iconChildMiddle");
+    }
+
+
+    public void setIconChildMiddle(String iconChildMiddle)
+    {
+        this.iconChildMiddle = iconChildMiddle;
+    }
+
+
+    /**
+     * @JSFProperty
+     */
+    public String getIconChildLast()
+    {
+        return getStringValue(iconChildLast, "iconChildLast");
+    }
+
+
+    public void setIconChildLast(String iconChildLast)
+    {
+        this.iconChildLast = iconChildLast;
+    }
+
+
+    /**
+     * @JSFProperty
+     */
+    public String getIconNodeOpen()
+    {
+        return getStringValue(iconNodeOpen, "iconNodeOpen");
+    }
+
+
+    public void setIconNodeOpen(String iconNodeOpen)
+    {
+        this.iconNodeOpen = iconNodeOpen;
+    }
+
+
+    /**
+     * @JSFProperty
+     */
+    public String getIconNodeOpenFirst()
+    {
+        return getStringValue(iconNodeOpenFirst, "iconNodeOpenFirst");
+    }
+
+
+    public void setIconNodeOpenFirst(String iconNodeOpenFirst)
+    {
+        this.iconNodeOpenFirst = iconNodeOpenFirst;
+    }
+
+
+    /**
+     * @JSFProperty
+     */
+    public String getIconNodeOpenMiddle()
+    {
+        return getStringValue(iconNodeOpenMiddle, "iconNodeOpenMiddle");
+    }
+
+
+    public void setIconNodeOpenMiddle(String iconNodeOpenMiddle)
+    {
+        this.iconNodeOpenMiddle = iconNodeOpenMiddle;
+    }
+
+
+    /**
+     * @JSFProperty
+     */
+    public String getIconNodeOpenLast()
+    {
+        return getStringValue(iconNodeOpenLast, "iconNodeOpenLast");
+    }
+
+
+    public void setIconNodeOpenLast(String iconNodeOpenLast)
+    {
+        this.iconNodeOpenLast = iconNodeOpenLast;
+    }
+
+
+    /**
+     * @JSFProperty
+     */
+    public String getIconNodeClose()
+    {
+        return getStringValue(iconNodeClose, "iconNodeClose");
+    }
+
+
+    public void setIconNodeClose(String iconNodeClose)
+    {
+        this.iconNodeClose = iconNodeClose;
+    }
+
+
+    /**
+     * @JSFProperty
+     */
+    public String getIconNodeCloseFirst()
+    {
+        return getStringValue(iconNodeCloseFirst, "iconNodeCloseFirst");
+    }
+
+
+    public void setIconNodeCloseFirst(String iconNodeCloseFirst)
+    {
+        this.iconNodeCloseFirst = iconNodeCloseFirst;
+    }
+
+
+    /**
+     * @JSFProperty
+     */
+    public String getIconNodeCloseMiddle()
+    {
+        return getStringValue(iconNodeCloseMiddle, "iconNodeCloseMiddle");
+    }
+
+
+    public void setIconNodeCloseMiddle(String iconNodeCloseMiddle)
+    {
+        this.iconNodeCloseMiddle = iconNodeCloseMiddle;
+    }
+
+
+    /**
+     * @JSFProperty
+     */
+    public String getIconNodeCloseLast()
+    {
+        return getStringValue(iconNodeCloseLast, "iconNodeCloseLast");
+    }
+
+
+    public void setIconNodeCloseLast(String iconNodeCloseLast)
+    {
+        this.iconNodeCloseLast = iconNodeCloseLast;
+    }
+
+
+    /**
+     * @JSFProperty
+     */
+    public String getNodeClass()
+    {
+        return getStringValue(nodeClass, "nodeClass");
+    }
+
+
+    public void setNodeClass(String nodeClass)
+    {
+        this.nodeClass = nodeClass;
+    }
+
+
+    /**
+     * @JSFProperty
+     * @return Returns the rowClasses.
+     */
+    public String getRowClasses()
+    {
+        return getStringValue(rowClasses, "rowClasses");
+    }
+
+
+    /**
+     * @param rowClasses The rowClasses to set.
+     */
+    public void setRowClasses(String rowClasses)
+    {
+        this.rowClasses = rowClasses;
+    }
+
+
+    /**
+     * @JSFProperty
+     * @return Returns the columnClasses.
+     */
+    public String getColumnClasses()
+    {
+        return getStringValue(columnClasses, "columnClasses");
+    }
+
+
+    /**
+     * @param columnClasses The columnClasses to set.
+     */
+    public void setColumnClasses(String columnClasses)
+    {
+        this.columnClasses = columnClasses;
+    }
+
+
+    /**
+     * @JSFProperty
+     * @return Returns the selectedNodeClass.
+     */
+    public String getSelectedNodeClass()
+    {
+        return getStringValue(selectedNodeClass, "selectedNodeClass");
+    }
+
+
+    /**
+     * @param selectedNodeClass The selectedNodeClass to set.
+     */
+    public void setSelectedNodeClass(String selectedNodeClass)
+    {
+        this.selectedNodeClass = selectedNodeClass;
+    }
+
+
+    /**
+     * @JSFProperty
+     */
+    public String getIconClass()
+    {
+        return getStringValue(iconClass, "iconClass");
+    }
+
+
+    public void setIconClass(String iconClass)
+    {
+        this.iconClass = iconClass;
+    }
+
+
+    /**
+     * Time interval the tree will remain registered as a TreeModelListener 
+     * without being accessed
+     * 
+     * @JSFProperty
+     */
+    public long getExpireListeners()
+    {
+        if(expireListeners != null)
+        {
+            return expireListeners.longValue();
+        }
+        ValueBinding vb = getValueBinding("expireListeners");
+        if(vb != null)
+        {
+            Object obj = vb.getValue(getFacesContext());
+            if(obj instanceof java.lang.Number)
+            {
+                return ((java.lang.Number)obj).longValue();
+            }
+        }
+        return DEFAULT_EXPIRE_LISTENERS;
+    }
+
+
+    public void setExpireListeners(long expireListeners)
+    {
+        this.expireListeners = new Long(expireListeners);
+    }
+
+
+    public String getFamily()
+    {
+        return "org.apache.myfaces.HtmlTree";
+    }
+
+
+    /**
+     * Ensures that the node identified by the specified path is expanded and
+     * viewable. If the last item in the path is a leaf, this will have no
+     * effect.
+     *
+     * @param path the <code>TreePath</code> identifying a node
+     */
+    public void expandPath(TreePath path, FacesContext context)
+    {
+        // Only expand if not leaf!
+        TreeModel model = getModel(context);
+
+        if (path != null && model != null && !model.isLeaf(path.getLastPathComponent()))
+        {
+            int[] translatedPath = HtmlTreeNode.translatePath(path, getModel(context));
+            HtmlTreeNode rootNode = getRootNode();
+            if (rootNode == null)
+            {
+                createRootNode(context);
+                rootNode = getRootNode();
+            }
+            if (!rootNode.isExpanded())
+            {
+                rootNode.setExpanded(true);
+            }
+            rootNode.expandPath(translatedPath, 0);
+
+        }
+    }
+
+
+    /**
+     * Ensures that the node identified by the specified path is collapsed and
+     * viewable.
+     *
+     * @param path the <code>TreePath</code> identifying a node
+     */
+    public void collapsePath(TreePath path, FacesContext context)
+    {
+        HtmlTreeNode node = findNode(path, context);
+
+        if (node != null)
+        {
+            node.setExpanded(false);
+        }
+    }
+
+
+    public boolean isExpanded(TreePath path, FacesContext context)
+    {
+        if (path == null)
+        {
+            return false;
+        }
+
+        return findNode(path, context) != null;
+    }
+
+
+    private HtmlTreeNode findNode(TreePath path, FacesContext context)
+    {
+        HtmlTreeNode node = getRootNode();
+        int[] translatedPath = HtmlTreeNode.translatePath(path, getModel(context));
+
+        for (int i = 0; i < translatedPath.length; i++)
+        {
+            if (!node.isExpanded())
+            {
+                return null;
+            }
+            int index = translatedPath[i];
+            node = (HtmlTreeNode) node.getChildren().get(index);
+        }
+        return node;
+    }
+
+
+    public TreePath getSelectionPath()
+    {
+        if (selectedPath == null)
+        {
+            return null;
+        }
+        return HtmlTreeNode.translatePath(selectedPath, getModel(FacesContext.getCurrentInstance()));
+    }
+
+
+    public void selectionChanged(HtmlTreeNode node)
+    {
+        TreePath oldPath = null;
+
+        if (selectedPath != null)
+        {
+            oldPath = HtmlTreeNode.translatePath(selectedPath, getModel(FacesContext.getCurrentInstance()));
+        }
+        selectedPath = node.getTranslatedPath();
+        if (node.isSelected())
+        {
+            queueEvent(new TreeSelectionEvent(this, oldPath, node.getPath()));
+        } else
+        {
+            queueEvent(new TreeSelectionEvent(this, oldPath, null));
+        }
+    }
+
+
+    private void createRootNode(FacesContext context)
+    {
+        HtmlTreeNode node;
+        TreeModel model = getModel(context);
+        Object root = model.getRoot();
+        node = (HtmlTreeNode) context.getApplication().createComponent(HtmlTreeNode.COMPONENT_TYPE);
+        String id = createUniqueId(context);
+        node.setId(id);
+
+        node.setPath(new TreePath(new Object[]{root}));
+        node.setUserObject(root);
+        node.setLayout(new int[]{HtmlTreeNode.CLOSED_SINGLE});
+        getFacets().put(FACET_ROOTNODE, node);
+    }
+
+
+    public HtmlTreeNode getRootNode()
+    {
+        return (HtmlTreeNode) getFacet(FACET_ROOTNODE);
+    }
+
+
+    public Object saveState(FacesContext context)
+    {
+        Object values[] = new Object[24];
+        values[0] = super.saveState(context);
+        values[1] = iconChildFirst;
+        values[2] = iconChildMiddle;
+        values[3] = iconChildLast;
+        values[4] = iconLine;
+        values[5] = iconNodeClose;
+        values[6] = iconNodeCloseFirst;
+        values[7] = iconNodeCloseLast;
+        values[8] = iconNodeCloseMiddle;
+        values[9] = iconNodeOpen;
+        values[10] = iconNodeOpenFirst;
+        values[11] = iconNodeOpenLast;
+        values[12] = iconNodeOpenMiddle;
+        values[13] = iconNoline;
+        values[14] = var;
+        values[15] = nodeClass;
+        values[16] = selectedNodeClass;
+        values[17] = new Integer(uniqueIdCounter);
+        values[18] = selectedPath;
+        values[19] = iconClass;
+        values[20] = new Integer(internalId);
+        values[21] = expireListeners;
+        values[22] = rowClasses;
+        values[23] = columnClasses;
+        return ((Object) (values));
+    }
+
+
+    public void restoreState(FacesContext context, Object state)
+    {
+        Object values[] = (Object[]) state;
+        super.restoreState(context, values[0]);
+        iconChildFirst = (String) values[1];
+        iconChildMiddle = (String) values[2];
+        iconChildLast = (String) values[3];
+        iconLine = (String) values[4];
+        iconNodeClose = (String) values[5];
+        iconNodeCloseFirst = (String) values[6];
+        iconNodeCloseLast = (String) values[7];
+        iconNodeCloseMiddle = (String) values[8];
+        iconNodeOpen = (String) values[9];
+        iconNodeOpenFirst = (String) values[10];
+        iconNodeOpenLast = (String) values[11];
+        iconNodeOpenMiddle = (String) values[12];
+        iconNoline = (String) values[13];
+        var = (String) values[14];
+        nodeClass = (String) values[15];
+        selectedNodeClass = (String) values[16];
+        uniqueIdCounter = ((Integer) values[17]).intValue();
+        selectedPath = (int[]) values[18];
+        iconClass = (String) values[19];
+        internalId = ((Integer) values[20]).intValue();
+        expireListeners = (Long) values[21];
+        rowClasses = (String) values[22];
+        columnClasses = (String) values[23];
+        addToModelListeners();
+    }
+
+
+    public void decode(FacesContext context)
+    {
+        super.decode(context);
+
+        //Save the current view root for later reference...
+        context.getExternalContext().getRequestMap().put(PREVIOUS_VIEW_ROOT, context.getViewRoot());
+        //...and remember that this instance needs NO special treatment on
+        // rendering:
+        itemStatesRestored = true;
+    }
+
+
+    public void processDecodes(FacesContext context)
+    {
+        addToModelListeners();
+        super.processDecodes(context);
+    }
+
+
+    public void processValidators(FacesContext context)
+    {
+        addToModelListeners();
+        super.processValidators(context);
+    }
+
+
+    public void processUpdates(FacesContext context)
+    {
+        addToModelListeners();
+        super.processUpdates(context);
+    }
+
+
+    public void encodeBegin(FacesContext context) throws IOException
+    {
+        addToModelListeners();
+        processModelEvents();
+
+        HtmlTreeNode node = getRootNode();
+
+        if (node == null)
+        {
+            createRootNode(context);
+        }
+
+        if (!itemStatesRestored)
+        {
+            UIViewRoot previousRoot = (UIViewRoot) context.getExternalContext().getRequestMap().get(PREVIOUS_VIEW_ROOT);
+            if (previousRoot != null)
+            {
+                restoreItemStates(context, previousRoot);
+            } else
+            {
+                //no previous root, means no decode was done
+                //--> a new request
+            }
+        }
+
+        super.encodeBegin(context);
+    }
+
+
+    public void encodeEnd(FacesContext context) throws IOException
+    {
+        super.encodeEnd(context);
+    }
+
+
+    public void restoreItemStates(FacesContext facesContext, UIViewRoot previousRoot)
+    {
+        HtmlTree previousTree = (HtmlTree) previousRoot.findComponent(getClientId(facesContext));
+
+        if (previousTree != null)
+        {
+            HtmlTreeNode node = previousTree.getRootNode();
+
+            if (node != null)
+            {
+                getRootNode().restoreItemState(node);
+            }
+
+            selectedPath = previousTree.selectedPath;
+        }
+    }
+
+
+    public void treeNodesChanged(TreeModelEvent e)
+    {
+        TreePath path = e.getTreePath();
+        FacesContext context = FacesContext.getCurrentInstance();
+        HtmlTreeNode node = findNode(path, context);
+
+        if (node != null)
+        {
+            node.childrenChanged(e.getChildIndices(), context);
+        }
+    }
+
+
+    public void treeNodesInserted(TreeModelEvent e)
+    {
+        TreePath path = e.getTreePath();
+        FacesContext context = FacesContext.getCurrentInstance();
+        HtmlTreeNode node = findNode(path, context);
+
+        if (node != null)
+        {
+            node.childrenAdded(e.getChildIndices(), context);
+        }
+    }
+
+
+    public void treeNodesRemoved(TreeModelEvent e)
+    {
+        TreePath path = e.getTreePath();
+        FacesContext context = FacesContext.getCurrentInstance();
+        HtmlTreeNode node = findNode(path, context);
+
+        if (node != null)
+        {
+            node.childrenRemoved(e.getChildIndices());
+        }
+    }
+
+
+    public void treeStructureChanged(TreeModelEvent e)
+    {
+        TreePath path = e.getTreePath();
+        FacesContext context = FacesContext.getCurrentInstance();
+
+        if (isExpanded(path, context))
+        {
+            collapsePath(path, context);
+            expandPath(path, context);
+        }
+    }
+
+
+    public boolean equals(Object obj)
+    {
+        if (!(obj instanceof HtmlTree))
+        {
+            return false;
+        }
+        HtmlTree other = (HtmlTree) obj;
+
+        return other.getId().equals(getId());
+    }
+
+
+    public int hashCode()
+    {
+        return getClientId(FacesContext.getCurrentInstance()).hashCode();
+    }
+
+
+    public void addToModelListeners()
+    {
+        Collection listeners = getModel(FacesContext.getCurrentInstance()).getTreeModelListeners();
+        long currentTime = System.currentTimeMillis();
+        boolean found = false;
+
+        for (Iterator iterator = listeners.iterator(); iterator.hasNext();)
+        {
+            ModelListener listener = (ModelListener) iterator.next();
+
+            if (listener.getId() == internalId)
+            {
+                found = true;
+            } else if (currentTime - listener.getLastAccessTime() > getExpireListeners())
+            {
+                iterator.remove();
+            }
+        }
+        if (!found)
+        {
+            listeners.add(new ModelListener(internalId));
+        }
+    }
+
+
+    private void processModelEvents()
+    {
+        Collection listeners = getModel(FacesContext.getCurrentInstance()).getTreeModelListeners();
+
+        for (Iterator iterator = listeners.iterator(); iterator.hasNext();)
+        {
+            ModelListener listener = (ModelListener) iterator.next();
+
+            if (listener.getId() == internalId)
+            {
+                for (Iterator events = listener.getEvents().iterator(); events.hasNext();)
+                {
+                    Event event = (Event) events.next();
+                    event.process(this);
+                    events.remove();
+                }
+                break;
+            }
+        }
+    }
+
+
+    public void collapseAll()
+    {
+        HtmlTreeNode root = getRootNode();
+        FacesContext context = FacesContext.getCurrentInstance();
+        collapsePath(root.getPath(), context);
+        for (int i = 0; i < root.getChildren().size(); i++)
+        {
+            HtmlTreeNode child = (HtmlTreeNode) (root.getChildren().get(i));
+            collapsePath(child.getPath(), context);
+            if (!child.isLeaf(context))
+            {
+                collapseChildren(context, child);
+            }
+        }
+    }
+
+
+    private void collapseChildren(FacesContext context, HtmlTreeNode parent)
+    {
+        for (int i = 0; i < parent.getChildren().size(); i++)
+        {
+            HtmlTreeNode child = (HtmlTreeNode) (parent.getChildren().get(i));
+            collapsePath(child.getPath(), context);
+            if (!child.isLeaf(context))
+            {
+                collapseChildren(context, child);
+            }
+        }
+
+    }
+
+
+    public void expandAll()
+    {
+        HtmlTreeNode root = getRootNode();
+        FacesContext context = FacesContext.getCurrentInstance();
+        expandPath(root.getPath(), context);
+        for (int i = 0; i < root.getChildren().size(); i++)
+        {
+            HtmlTreeNode child = (HtmlTreeNode) (root.getChildren().get(i));
+            expandPath(child.getPath(), context);
+            if (!child.isLeaf(context))
+            {
+                expandChildren(context, child);
+            }
+        }
+    }
+
+
+    private void expandChildren(FacesContext context, HtmlTreeNode parent)
+    {
+        for (int i = 0; i < parent.getChildren().size(); i++)
+        {
+            HtmlTreeNode child = (HtmlTreeNode) (parent.getChildren().get(i));
+            expandPath(child.getPath(), context);
+            if (!child.isLeaf(context))
+            {
+                expandChildren(context, child);
+            }
+        }
+    }
+
+    private static class ModelListener implements TreeModelListener, Serializable
+    {
+
+        private long lastAccessTime = System.currentTimeMillis();
+
+        private LinkedList events = new LinkedList();
+
+        int id;
+
+
+        public ModelListener(int id)
+        {
+            this.id = id;
+        }
+
+
+        public List getEvents()
+        {
+            lastAccessTime = System.currentTimeMillis();
+            return events;
+        }
+
+
+        public int getId()
+        {
+            return id;
+        }
+
+
+        public long getLastAccessTime()
+        {
+            return lastAccessTime;
+        }
+
+
+        public void treeNodesChanged(TreeModelEvent e)
+        {
+            events.addLast(new Event(EVENT_CHANGED, e));
+        }
+
+
+        public void treeNodesInserted(TreeModelEvent e)
+        {
+            events.addLast(new Event(EVENT_INSERTED, e));
+        }
+
+
+        public void treeNodesRemoved(TreeModelEvent e)
+        {
+            events.addLast(new Event(EVENT_REMOVED, e));
+        }
+
+
+        public void treeStructureChanged(TreeModelEvent e)
+        {
+            events.addLast(new Event(EVENT_STRUCTURE_CHANGED, e));
+        }
+    }
+
+
+    private static class Event
+    {
+
+        private int kind;
+
+        private TreeModelEvent event;
+
+
+        public Event(int kind, TreeModelEvent event)
+        {
+            this.kind = kind;
+            this.event = event;
+        }
+
+
+        public void process(HtmlTree tree)
+        {
+            switch (kind)
+            {
+                case EVENT_CHANGED:
+                    tree.treeNodesChanged(event);
+                    break;
+                case EVENT_INSERTED:
+                    tree.treeNodesInserted(event);
+                    break;
+                case EVENT_REMOVED:
+                    tree.treeNodesRemoved(event);
+                    break;
+                case EVENT_STRUCTURE_CHANGED:
+                    tree.treeStructureChanged(event);
+                    break;
+            }
+        }
+    }
+}

Added: myfaces/tomahawk/trunk/core20/src/main/java/org/apache/myfaces/custom/tree/HtmlTreeNode.java
URL: http://svn.apache.org/viewvc/myfaces/tomahawk/trunk/core20/src/main/java/org/apache/myfaces/custom/tree/HtmlTreeNode.java?rev=904296&view=auto
==============================================================================
--- myfaces/tomahawk/trunk/core20/src/main/java/org/apache/myfaces/custom/tree/HtmlTreeNode.java (added)
+++ myfaces/tomahawk/trunk/core20/src/main/java/org/apache/myfaces/custom/tree/HtmlTreeNode.java Thu Jan 28 23:13:17 2010
@@ -0,0 +1,591 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.myfaces.custom.tree;
+
+import org.apache.myfaces.custom.tree.model.TreeModel;
+import org.apache.myfaces.custom.tree.model.TreePath;
+
+import javax.faces.component.html.HtmlCommandLink;
+import javax.faces.context.FacesContext;
+import java.util.Iterator;
+import java.util.List;
+
+
+/**
+ * Represents a single node of a three. A custom html link component representing the expand/collapse icon
+ * is held as a facet named <code>expandCollapse</code>.
+ *
+ * @JSFComponent
+ * @author <a href="mailto:oliver@rossmueller.com">Oliver Rossmueller</a>
+ * @version $Revision: 659874 $ $Date: 2008-05-24 15:59:15 -0500 (sáb, 24 may 2008) $
+ */
+public class HtmlTreeNode
+    extends HtmlCommandLink
+{
+
+    private static final String DEFAULT_RENDERER_TYPE = "org.apache.myfaces.HtmlTreeNode";
+
+    public static final String COMPONENT_TYPE = "org.apache.myfaces.HtmlTreeNode";
+    public static final String EXPAND_COLLAPSE_FACET = "expandCollapse";
+
+    public static final int OPEN = 0;
+    public static final int OPEN_FIRST = 1;
+    public static final int OPEN_LAST = 2;
+    public static final int OPEN_SINGLE = 3;
+    public static final int CLOSED = 10;
+    public static final int CLOSED_FIRST = 11;
+    public static final int CLOSED_LAST = 12;
+    public static final int CLOSED_SINGLE = 13;
+    public static final int CHILD = 20;
+    public static final int CHILD_FIRST = 21;
+    public static final int CHILD_LAST = 22;
+    public static final int LINE = 30;
+    public static final int EMPTY = 40;
+    private static final int OFFSET = 10;
+    private static final int MOD_FIRST = 1;
+    private static final int MOD_LAST = 2;
+
+    private transient TreePath path;
+    private int[] translatedPath;
+    private transient Object userObject;
+    private boolean expanded = false;
+    private boolean selected = false;
+    private int[] layout;
+
+
+    public HtmlTreeNode()
+    {
+        setRendererType(DEFAULT_RENDERER_TYPE);
+    }
+
+
+    public int getLevel()
+    {
+        return layout.length - 1;
+    }
+
+
+    public int getMaxChildLevel()
+    {
+        if (getChildCount() == 0)
+        {
+            return getLevel();
+        }
+
+        int max = getLevel();
+        for (Iterator iterator = getChildren().iterator(); iterator.hasNext();)
+        {
+            HtmlTreeNode child = (HtmlTreeNode) iterator.next();
+            max = Math.max(max, child.getMaxChildLevel());
+        }
+        return max;
+    }
+
+
+    public TreePath getPath()
+    {
+        if (path == null)
+        {
+            if (translatedPath == null)
+            {
+                throw new IllegalStateException("No path and no translated path available");
+            }
+            path = translatePath(translatedPath, getTreeModel(FacesContext.getCurrentInstance()));
+        }
+        return path;
+    }
+
+
+    public void setPath(TreePath path)
+    {
+        this.path = path;
+    }
+
+
+    int[] getTranslatedPath()
+    {
+        if (translatedPath != null)
+        {
+            return translatedPath;
+        }
+        
+        return translatePath(getPath(), getTreeModel(FacesContext.getCurrentInstance()));
+    }
+
+
+    public Object getUserObject()
+    {
+        if (userObject == null)
+        {
+            userObject = getPath().getLastPathComponent();
+        }
+        return userObject;
+    }
+
+
+    public void setUserObject(Object userObject)
+    {
+        this.userObject = userObject;
+        setValue(userObject.toString());
+    }
+
+
+    public boolean isExpanded()
+    {
+        return expanded;
+    }
+
+
+    void childrenAdded(int[] children, FacesContext context)
+    {
+        if (getChildCount() == 0 && children.length > 0)
+        {
+            // change to CLOSED_*
+            layout[layout.length - 1] -= OFFSET;
+        }
+
+        if (!expanded)
+        {
+            // nothing to do
+            return;
+        }
+
+        TreeModel model = getTreeModel(context);
+        int childCount = model.getChildCount(getUserObject());
+        int pathUpdateIndex = getTranslatedPath().length;
+
+        for (int i = 0; i < children.length; i++)
+        {
+            int index = children[i];
+            translateChildrenPath(pathUpdateIndex, index, 1);
+            Object userObject = model.getChild(getUserObject(), index);
+            addNode(model, userObject, index, childCount, context);
+        }
+    }
+
+
+    void childrenChanged(int[] children, FacesContext context)
+    {
+        if (!expanded)
+        {
+            // nothing to do
+            return;
+        }
+
+        TreeModel model = getTreeModel(context);
+
+        for (int i = 0; i < children.length; i++)
+        {
+            int index = children[i];
+            Object userObject = model.getChild(getUserObject(), index);
+            HtmlTreeNode node = (HtmlTreeNode) getChildren().get(index);
+            node.setUserObject(userObject);
+            // todo: modify path????
+        }
+    }
+
+
+    private void addNode(TreeModel model, Object userObject, int index, int childCount, FacesContext context)
+    {
+        HtmlTreeNode node = createNode(model, userObject, childCount, index, context);
+        List children = getChildren();
+
+        if (!children.isEmpty())
+        {
+            if (index == 0)
+            {
+                HtmlTreeNode first = (HtmlTreeNode) getChildren().get(0);
+                int[] layout = first.getLayout();
+                if (layout[layout.length - 1] % OFFSET == MOD_FIRST)
+                {
+                    // change from *_FIRST to *
+                    layout[layout.length - 1]--;
+                }
+            } else if (index == childCount - 1)
+            {
+                HtmlTreeNode last = (HtmlTreeNode) getChildren().get(childCount - 2);
+                int[] layout = last.getLayout();
+                if (layout[layout.length - 1] % OFFSET == MOD_LAST)
+                {
+                    // change from *_LAST to *
+                    layout[layout.length - 1] -= 2;
+                }
+            }
+        }
+
+        children.add(index, node);
+    }
+
+
+    void childrenRemoved(int[] children)
+    {
+        if (!expanded)
+        {
+            // nothing to do
+            return;
+        }
+        List childNodes = getChildren();
+        int pathUpdateIndex = getTranslatedPath().length;
+
+        for (int i = children.length - 1; i >= 0; i--)
+        {
+            translateChildrenPath(pathUpdateIndex, children[i], -1);
+            HtmlTreeNode child = (HtmlTreeNode) childNodes.get(children[i]);
+
+            if (child.isSelected()) {
+                child.setSelected(false);
+                if (children[i] < childNodes.size() - 1) {
+                    ((HtmlTreeNode) childNodes.get(children[i] + 1)).setSelected(true);
+                } else {
+                    if (children[i] > 0) {
+                        ((HtmlTreeNode) childNodes.get(children[i] - 1)).setSelected(true);
+                    } else {
+                        setSelected(true);
+                    }
+                }
+            }
+            childNodes.remove(children[i]);
+        }
+    }
+
+
+    /**
+     * Update the translatedPath of all child nodes so the path points to the same object in the model
+     * after adding/removing a node
+     *
+     * @param pathUpdateIndex
+     * @param startIndex
+     * @param offset
+     */
+    private void translateChildrenPath(int pathUpdateIndex, int startIndex, int offset) {
+        List children = getChildren();
+        int childrenSize = children.size();
+
+        for (int i = startIndex; i < childrenSize; i++) {
+            HtmlTreeNode node = (HtmlTreeNode) children.get(i);
+            node.updateTranslatedPath(pathUpdateIndex, offset);
+        }
+    }
+
+
+    private void updateTranslatedPath(int pathUpdateIndex, int offset)
+    {
+        translatedPath[pathUpdateIndex] += offset;
+        // reset path and userObject so the values are acquired from the model
+        path = null;
+        userObject = null;
+
+        if (isExpanded()) {
+            // continue with the children of this node
+            translateChildrenPath(pathUpdateIndex, 0, offset);
+        }
+    }
+
+
+    public void setExpanded(boolean expanded)
+    {
+        if (this.expanded == expanded)
+        {
+            // no change
+            return;
+        }
+        this.expanded = expanded;
+
+        if (expanded)
+        {
+            FacesContext context = FacesContext.getCurrentInstance();
+            TreeModel model = getTreeModel(context);
+            int childCount = model.getChildCount(getUserObject());
+
+            for (int i = 0; i < childCount; i++)
+            {
+                Object child = model.getChild(getUserObject(), i);
+                HtmlTreeNode node = createNode(model, child, childCount, i, context);
+                getChildren().add(node);
+            }
+            layout[layout.length - 1] -= OFFSET;
+        } else
+        {
+            if (clearSelection())
+            {
+                setSelected(true);
+            }
+            getChildren().clear();
+            layout[layout.length - 1] += OFFSET;
+        }
+
+    }
+
+
+    private HtmlTreeNode createNode(TreeModel model, Object child, int childCount, int index, FacesContext context)
+    {
+        HtmlTreeNode node = (HtmlTreeNode) context.getApplication().createComponent(HtmlTreeNode.COMPONENT_TYPE);
+        String id = getTree().createUniqueId(context);
+        node.setId(id);
+
+        node.setPath(getPath().pathByAddingChild(child));
+        node.setUserObject(child);
+        int state = CHILD;
+
+        if (model.isLeaf(child))
+        {
+
+            if (childCount > 1)
+            {
+                if (index == 0)
+                {
+                    state = CHILD;
+                } else if (index == childCount - 1)
+                {
+                    state = CHILD_LAST;
+                }
+            } else
+            {
+                state = CHILD_LAST;
+            }
+        } else
+        {
+            if (childCount > 1)
+            {
+                if (index == 0)
+                {
+                    state = CLOSED;
+                } else if (index == childCount - 1)
+                {
+                    state = CLOSED_LAST;
+                } else
+                {
+                    state = CLOSED;
+                }
+            } else
+            {
+                state = CLOSED_LAST;
+            }
+
+        }
+        node.setLayout(layout, state);
+
+        return node;
+    }
+
+
+    public void toggleExpanded()
+    {
+        setExpanded(!expanded);
+    }
+
+
+    public boolean isSelected()
+    {
+        return selected;
+    }
+
+
+    public void setSelected(boolean selected)
+    {
+        if (selected)
+        {
+            getTree().getRootNode().clearSelection();
+        }
+        this.selected = selected;
+        getTree().selectionChanged(this);
+    }
+
+
+    public void toggleSelected()
+    {
+        setSelected(!selected);
+    }
+
+
+    private boolean clearSelection()
+    {
+        if (isSelected())
+        {
+            selected = false;
+            return true;
+        }
+        for (Iterator iterator = getChildren().iterator(); iterator.hasNext();)
+        {
+            HtmlTreeNode node = (HtmlTreeNode) iterator.next();
+            if (node.clearSelection())
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+
+    public int[] getLayout()
+    {
+        return layout;
+    }
+
+
+    public void setLayout(int[] layout)
+    {
+        this.layout = layout;
+    }
+
+
+    public void setLayout(int[] parentLayout, int layout)
+    {
+        this.layout = new int[parentLayout.length + 1];
+        this.layout[parentLayout.length] = layout;
+
+        for (int i = 0; i < parentLayout.length; i++)
+        {
+            int state = parentLayout[i];
+            if (state == OPEN || state == OPEN_FIRST || state == CLOSED || state == CLOSED_FIRST || state == CHILD || state == CHILD_FIRST || state == LINE)
+            {
+                this.layout[i] = LINE;
+            } else
+            {
+                this.layout[i] = EMPTY;
+            }
+        }
+    }
+
+
+    public HtmlTreeImageCommandLink getExpandCollapseCommand(FacesContext context)
+    {
+        HtmlTreeImageCommandLink command = (HtmlTreeImageCommandLink) getFacet(EXPAND_COLLAPSE_FACET);
+
+        if (command == null)
+        {
+            command = (HtmlTreeImageCommandLink) context.getApplication().createComponent(HtmlTreeImageCommandLink.COMPONENT_TYPE);
+            String id = getTree().createUniqueId(context);
+            command.setId(id);
+            getFacets().put(EXPAND_COLLAPSE_FACET, command);
+        }
+        return command;
+    }
+
+
+    public Object saveState(FacesContext context)
+    {
+        Object values[] = new Object[5];
+        values[0] = super.saveState(context);
+        values[1] = Boolean.valueOf(expanded);
+        values[2] = layout;
+        values[3] = translatePath(getPath(), getTreeModel(context));
+        values[4] = Boolean.valueOf(selected);
+        return values;
+    }
+
+
+    public void restoreState(FacesContext context, Object state)
+    {
+        Object values[] = (Object[]) state;
+        super.restoreState(context, values[0]);
+        expanded = ((Boolean) values[1]).booleanValue();
+        layout = (int[]) values[2];
+        translatedPath = (int[]) values[3];
+        selected = ((Boolean) values[4]).booleanValue();
+    }
+
+
+    protected static int[] translatePath(TreePath treePath, TreeModel model)
+    {
+        Object[] path = treePath.getPath();
+        int[] translated = new int[path.length - 1];
+
+        Object parent = path[0];
+
+        for (int i = 1; i < path.length; i++)
+        {
+            Object element = path[i];
+            translated[i - 1] = model.getIndexOfChild(parent, element);
+            parent = element;
+        }
+        return translated;
+    }
+
+
+    protected static TreePath translatePath(int[] path, TreeModel model)
+    {
+        Object[] translated = new Object[path.length + 1];
+        Object parent = model.getRoot();
+
+        translated[0] = parent;
+
+        for (int i = 0; i < path.length; i++)
+        {
+            int index = path[i];
+            translated[i + 1] = model.getChild(parent, index);
+            parent = translated[i + 1];
+        }
+        return new TreePath(translated);
+    }
+
+
+    protected TreeModel getTreeModel(FacesContext context)
+    {
+        return getTree().getModel(context);
+    }
+
+
+    protected HtmlTree getTree()
+    {
+        if (getParent() instanceof HtmlTree)
+        {
+            return (HtmlTree) getParent();
+        }
+        return ((HtmlTreeNode) getParent()).getTree();
+    }
+
+
+    public boolean isLeaf(FacesContext context)
+    {
+        return getTreeModel(context).isLeaf(getUserObject());
+    }
+
+
+    public void expandPath(int[] translatedPath, int current)
+    {
+        if (current >= translatedPath.length)
+        {
+            return;
+        }
+
+        HtmlTreeNode child = (HtmlTreeNode) getChildren().get(translatedPath[current]);
+
+        if (!child.isExpanded())
+        {
+            child.setExpanded(true);
+        }
+
+        child.expandPath(translatedPath, current + 1);
+    }
+
+
+    public void restoreItemState(HtmlTreeNode node)
+    {
+        setExpanded(node.isExpanded());
+        selected = node.isSelected();
+        List children = getChildren();
+        List otherChildren = node.getChildren();
+        for (int i = 0; i < children.size(); i++)
+        {
+            HtmlTreeNode child = (HtmlTreeNode) children.get(i);
+            child.restoreItemState((HtmlTreeNode) otherChildren.get(i));
+        }
+    }
+}

Added: myfaces/tomahawk/trunk/core20/src/main/java/org/apache/myfaces/custom/tree2/AbstractHtmlTree.java
URL: http://svn.apache.org/viewvc/myfaces/tomahawk/trunk/core20/src/main/java/org/apache/myfaces/custom/tree2/AbstractHtmlTree.java?rev=904296&view=auto
==============================================================================
--- myfaces/tomahawk/trunk/core20/src/main/java/org/apache/myfaces/custom/tree2/AbstractHtmlTree.java (added)
+++ myfaces/tomahawk/trunk/core20/src/main/java/org/apache/myfaces/custom/tree2/AbstractHtmlTree.java Thu Jan 28 23:13:17 2010
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.myfaces.custom.tree2;
+
+import java.util.Map;
+
+import javax.faces.component.UICommand;
+import javax.faces.component.html.HtmlCommandLink;
+import javax.faces.context.FacesContext;
+import javax.faces.el.MethodBinding;
+
+import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFComponent;
+import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFProperty;
+import org.apache.myfaces.component.LocationAware;
+
+/**
+ * Represents "tree data" in an HTML format.  Also provides a mechanism for maintaining expand/collapse
+ * state of the nodes in the tree.
+ *
+ * A component that provides an HTML-based tree from data supplied by a 
+ * backing bean. The tree is highly customizable and allows for 
+ * fine-grained control over the appearance of each of the nodes 
+ * depending on their type. 
+ * 
+ * Almost any type of JSF component (text, image, checkbox, etc.) can 
+ * be rendered inside the nodes and there is an option for client-side 
+ * or server-side toggling of the expand/collapse state. 
+ * 
+ * Unless otherwise specified, all attributes accept static values or EL expressions.
+ * 
+ * @since 1.1.7
+ * @author Sean Schofield
+ */
+@JSFComponent(
+    name = "t:tree2",
+    clazz = "org.apache.myfaces.custom.tree2.HtmlTree",
+    tagClass = "org.apache.myfaces.custom.tree2.TreeTag")
+public abstract class AbstractHtmlTree extends UITreeData
+    implements LocationAware
+{
+    public static final String COMPONENT_TYPE = "org.apache.myfaces.HtmlTree2";
+    private static final String DEFAULT_RENDERER_TYPE = "org.apache.myfaces.HtmlTree2";
+
+    private UICommand _expandControl = null;
+
+    /**
+     * Perform client-side toggling of expand/collapse state via javascript (default is true.)
+     * 
+     * @return  the new clientSideToggle value
+     */
+    @JSFProperty(defaultValue = "true")
+    public boolean isClientSideToggle()
+    {
+        return (Boolean) getStateHelper().eval(PropertyKeys.clientSideToggle, true);
+    }
+    
+    /**
+     * Sets 
+     * 
+     * @param clientSideToggle  the new clientSideToggle value
+     */
+    public void setClientSideToggle(boolean clientSideToggle)
+    {
+        getStateHelper().put(PropertyKeys.clientSideToggle, clientSideToggle ); 
+    }  
+        
+    /**
+     * @see org.apache.myfaces.custom.tree2.UITreeData#processNodes(javax.faces.context.FacesContext, int, org.apache.myfaces.custom.tree2.TreeWalker)
+     */
+    protected void processNodes(FacesContext context, int processAction, TreeWalker walker)
+    {
+        if (isClientSideToggle()) {
+            walker.setCheckState(false);
+        }
+        super.processNodes(context, processAction, walker);
+    }
+    
+    //public abstract String getLocalVarNodeToggler();
+
+    // see superclass for documentation
+    public void setNodeId(String nodeId)
+    {
+        super.setNodeId(nodeId);
+
+        String varNodeToggler = (String) getStateHelper().get(PropertyKeys.varNodeToggler);
+        if (varNodeToggler != null)
+        {
+            Map requestMap = getFacesContext().getExternalContext().getRequestMap();
+            requestMap.put(varNodeToggler, this);
+        }
+    }
+
+    /**
+     * Gets the expand/collapse control that can be used to handle expand/collapse nodes.  This is only used in server-side
+     * mode.  It allows the nagivation controls (if any) to be clickable as well as any commandLinks the user has set up in
+     * their JSP.
+     * 
+     * @return UICommand
+     */
+    public UICommand getExpandControl()
+    {
+        if (_expandControl == null){
+            _expandControl = new HtmlCommandLink();
+            _expandControl.setParent(this);
+        }
+        return _expandControl;
+    }
+    
+    /**
+     * Gets 
+     *
+     * @return  the new varNodeToggler value
+     */
+    @JSFProperty(literalOnly=true)
+    public String getVarNodeToggler()
+    {
+        return (String) getStateHelper().get(PropertyKeys.varNodeToggler);
+    }
+    
+    public void setVarNodeToggler(String varNodeToggler)
+    {
+        getStateHelper().put(PropertyKeys.varNodeToggler, varNodeToggler );
+
+        // create a method binding for the expand control
+        String bindingString = "#{" + varNodeToggler + ".toggleExpanded}";
+        MethodBinding actionBinding = FacesContext.getCurrentInstance().getApplication().createMethodBinding(bindingString, null);
+        getExpandControl().setAction(actionBinding);
+    }
+        
+    /**
+     * Show the "plus" and "minus" navigation icons (default is true.) 
+     * Value is ignored if clientSideToggle is true.
+     * 
+     */
+    @JSFProperty(defaultValue="true")
+    public abstract boolean isShowNav();
+
+    /**
+     * Show the connecting lines (default is true.)
+     * 
+     */
+    @JSFProperty(defaultValue="true")
+    public abstract boolean isShowLines();
+
+    /**
+     * Include the root node when rendering the tree (default is true.)
+     * 
+     */
+    @JSFProperty(defaultValue="true")
+    public abstract boolean isShowRootNode();
+
+    /**
+     * Preserve changes in client-side toggle information between 
+     * requests (default is true.)
+     * 
+     */
+    @JSFProperty(defaultValue="true")
+    public abstract boolean isPreserveToggle();
+
+    protected enum PropertyKeys
+    {
+         clientSideToggle
+        , varNodeToggler
+    }
+}

Added: myfaces/tomahawk/trunk/core20/src/main/java/org/apache/myfaces/custom/tree2/UITreeData.java
URL: http://svn.apache.org/viewvc/myfaces/tomahawk/trunk/core20/src/main/java/org/apache/myfaces/custom/tree2/UITreeData.java?rev=904296&view=auto
==============================================================================
--- myfaces/tomahawk/trunk/core20/src/main/java/org/apache/myfaces/custom/tree2/UITreeData.java (added)
+++ myfaces/tomahawk/trunk/core20/src/main/java/org/apache/myfaces/custom/tree2/UITreeData.java Thu Jan 28 23:13:17 2010
@@ -0,0 +1,852 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.myfaces.custom.tree2;
+
+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.component.EditableValueHolder;
+import javax.faces.component.NamingContainer;
+import javax.faces.component.UIComponent;
+import javax.faces.component.UIComponentBase;
+import javax.faces.context.FacesContext;
+import javax.faces.el.ValueBinding;
+import javax.faces.event.AbortProcessingException;
+import javax.faces.event.ActionEvent;
+import javax.faces.event.FacesEvent;
+import javax.faces.event.FacesListener;
+import javax.faces.event.PhaseId;
+
+import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFComponent;
+import org.apache.myfaces.shared_tomahawk.util.MessageUtils;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * TreeData is a {@link UIComponent} that supports binding data stored in a tree represented
+ * by a {@link TreeNode} instance.  During iterative processing over the tree nodes in the
+ * data model, the object for the current node is exposed as a request attribute under the key
+ * specified by the <code>var</code> property.  {@link javax.faces.render.Renderer}s of this
+ * component should use the appropriate facet to assist in rendering.
+ *
+ * @author Sean Schofield
+ * @author Hans Bergsten (Some code taken from an example in his O'Reilly JavaServer Faces book. Copied with permission)
+ * @version $Revision: 703742 $ $Date: 2008-10-11 17:10:36 -0500 (sáb, 11 oct 2008) $
+ */
+@JSFComponent
+public class UITreeData extends UIComponentBase implements NamingContainer, Tree {
+    private Log log = LogFactory.getLog(UITreeData.class);
+
+    public static final String COMPONENT_TYPE = "org.apache.myfaces.UITree2";
+    public static final String COMPONENT_FAMILY = "org.apache.myfaces.HtmlTree2";
+    //private static final String DEFAULT_RENDERER_TYPE = "org.apache.myfaces.Tree2";
+    private static final String MISSING_NODE = "org.apache.myfaces.tree2.MISSING_NODE";
+    private static final int PROCESS_DECODES = 1;
+    private static final int PROCESS_VALIDATORS = 2;
+    private static final int PROCESS_UPDATES = 3;
+
+    private TreeModel _cachedModel;
+    private String _nodeId;
+    private TreeNode _node;
+
+    private Object _value;
+    private String _var;
+    private Map _saved = new HashMap();
+
+    private TreeState _restoredState = null;
+
+    /**
+     * Constructor
+     */
+    public UITreeData()
+    {
+        //setRendererType(DEFAULT_RENDERER_TYPE);
+    }
+
+
+    // see superclass for documentation
+    public String getFamily()
+    {
+        return COMPONENT_FAMILY;
+    }
+
+    //  see superclass for documentation
+    public Object saveState(FacesContext context)
+    {
+        Object values[] = new Object[3];
+        values[0] = super.saveState(context);
+        values[1] = _var;
+        values[2] = _restoredState;
+        return ((Object) (values));
+    }
+
+
+    // see superclass for documentation
+    public void restoreState(FacesContext context, Object state)
+    {
+        Object values[] = (Object[]) state;
+        super.restoreState(context, values[0]);
+
+        _var = (String)values[1];
+        _restoredState = (TreeState) values[2];
+    }
+
+    public void encodeEnd(FacesContext context) throws IOException {
+        super.encodeEnd(context);
+
+        // prepare to save the tree state -- fix for MYFACES-618
+        // should be done in saveState() but Sun RI does not call saveState() and restoreState()
+        // with javax.faces.STATE_SAVING_METHOD = server
+        TreeState state = getDataModel().getTreeState();
+        if ( state == null)
+        {
+            // the model supplier has forgotten to return a valid state manager, but we need one
+            state = new TreeStateBase();
+        }
+        // save the state with the component, unless it should explicitly not saved eg. session-scoped model and state
+        _restoredState = (state.isTransient()) ? null : state;
+
+    }
+
+    public void queueEvent(FacesEvent event)
+    {
+        super.queueEvent(new FacesEventWrapper(event, getNodeId(), this));
+    }
+
+
+    public void broadcast(FacesEvent event) throws AbortProcessingException
+    {
+        if (event instanceof FacesEventWrapper)
+        {
+            FacesEventWrapper childEvent = (FacesEventWrapper) event;
+            String currNodeId = getNodeId();
+            setNodeId(childEvent.getNodeId());
+            FacesEvent nodeEvent = childEvent.getFacesEvent();
+            nodeEvent.getComponent().broadcast(nodeEvent);
+            setNodeId(currNodeId);
+            return;
+        }
+        else if(event instanceof ToggleExpandedEvent)
+        {
+            ToggleExpandedEvent toggleEvent = (ToggleExpandedEvent) event;
+            String currentNodeId = getNodeId();
+            setNodeId(toggleEvent.getNodeId());
+            toggleExpanded();
+            setNodeId(currentNodeId);
+        }
+        else
+        {
+            super.broadcast(event);
+            return;
+        }
+    }
+
+
+    // see superclass for documentation
+    public void processDecodes(FacesContext context)
+    {
+        if (context == null) throw new NullPointerException("context");
+        if (!isRendered()) return;
+
+        _cachedModel = null;
+        _saved = new HashMap();
+
+        setNodeId(null);
+        decode(context);
+
+        processNodes(context, PROCESS_DECODES, getDataModel().getTreeWalker());
+        // After processNodes is executed, the node active is the last one
+        // we have to set it to null again to avoid inconsistency on outsider
+        // code (just like UIData components does)
+        setNodeId(null);
+
+    }
+
+    // see superclass for documentation
+    public void processValidators(FacesContext context)
+    {
+        if (context == null) throw new NullPointerException("context");
+        if (!isRendered()) return;
+
+        processNodes(context, PROCESS_VALIDATORS, getDataModel().getTreeWalker());
+
+        setNodeId(null);
+    }
+
+
+    // see superclass for documentation
+    public void processUpdates(FacesContext context)
+    {
+        if (context == null) throw new NullPointerException("context");
+        if (!isRendered()) return;
+
+        processNodes(context, PROCESS_UPDATES, getDataModel().getTreeWalker());
+
+        setNodeId(null);
+    }
+
+    // see superclass for documentation
+    public String getClientId(FacesContext context)
+    {
+        String ownClientId = super.getClientId(context);
+        if (_nodeId != null)
+        {
+            return ownClientId + NamingContainer.SEPARATOR_CHAR + _nodeId;
+        } else
+        {
+            return ownClientId;
+        }
+    }
+
+    // see superclass for documentation
+    public void setValueBinding(String name, ValueBinding binding)
+    {
+        if ("value".equals(name))
+        {
+            _cachedModel = null;
+        } else if ("nodeVar".equals(name) || "nodeId".equals(name) || "treeVar".equals(name))
+        {
+            throw new IllegalArgumentException("name " + name);
+        }
+        super.setValueBinding(name, binding);
+    }
+
+    // see superclass for documentation
+    public void encodeBegin(FacesContext context) throws IOException
+    {
+        /**
+         * The renderer will handle most of the encoding, but if there are any
+         * error messages queued for the components (validation errors), we
+         * do want to keep the saved state so that we can render the node with
+         * the invalid value.
+         */
+
+        if (!keepSaved(context))
+        {
+            _saved = new HashMap();
+        }
+
+        // FIX for MYFACES-404
+        // do not use the cached model the render phase
+        _cachedModel = null;
+
+        super.encodeBegin(context);
+    }
+
+    /**
+     * Sets the value of the TreeData.
+     *
+     * @param value The new value
+     *
+     * @deprecated
+     */
+    public void setValue(Object value)
+    {
+        _cachedModel = null;
+        _value = value;
+    }
+
+
+    /**
+     * Gets the model of the TreeData -
+     *  due to backwards-compatibility, this can also be retrieved by getValue.
+     *
+     * @return The value
+     */
+    public Object getModel()
+    {
+        return getValue();
+    }
+
+    /**
+     * Sets the model of the TreeData -
+     *  due to backwards-compatibility, this can also be set by calling setValue.
+     *
+     * @param model The new model
+     */
+    public void setModel(Object model)
+    {
+        setValue(model);
+    }
+
+
+    /**
+     * Gets the value of the TreeData.
+     *
+     * @JSFProperty
+     *   required="true"
+     * @return The value
+     *
+     * @deprecated
+     */
+    public Object getValue()
+    {
+        if (_value != null) return _value;
+        ValueBinding vb = getValueBinding("value");
+        return vb != null ? vb.getValue(getFacesContext()) : null;
+    }
+
+    /**
+     * Set the request-scope attribute under which the data object for the current node wil be exposed
+     * when iterating.
+     *
+     * @param var The new request-scope attribute name
+     */
+    public void setVar(String var)
+    {
+        _var = var;
+    }
+
+
+    /**
+     * Return the request-scope attribute under which the data object for the current node will be exposed
+     * when iterating. This property is not enabled for value binding expressions.
+     * 
+     * @JSFProperty
+     * @return The iterator attribute
+     */
+    public String getVar()
+    {
+        return _var;
+    }
+
+    /**
+     * Calls through to the {@link TreeModel} and returns the current {@link TreeNode} or <code>null</code>.
+     *
+     * @return The current node
+     */
+    public TreeNode getNode()
+    {
+        return _node;
+    }
+
+
+    public String getNodeId()
+    {
+        return _nodeId;
+    }
+
+
+    public void setNodeId(String nodeId)
+    {
+        saveDescendantState();
+
+        _nodeId = nodeId;
+
+        TreeModel model = getDataModel();
+        if (model == null)
+        {
+            return;
+        }
+
+        try
+        {
+            _node = model.getNodeById(nodeId);
+        }
+        //TODO: change to an own exception
+        catch (IndexOutOfBoundsException aob)
+        {
+            /**
+             * This might happen if we are trying to process a commandLink for a node that node that no longer
+             * exists.  Instead of allowing a RuntimeException to crash the application, we will add a warning
+             * message so the user can optionally display the warning.  Also, we will allow the user to provide
+             * their own value binding method to be called so they can handle it how they see fit.
+             */
+            FacesMessage message = MessageUtils.getMessage(MISSING_NODE, new String[] {nodeId});
+            message.setSeverity(FacesMessage.SEVERITY_WARN);
+            FacesContext.getCurrentInstance().addMessage(getId(), message);
+
+            /** @todo call hook */
+            /** @todo figure out whether or not to abort this method gracefully */
+        }
+
+        restoreDescendantState();
+
+        if (_var != null)
+        {
+            Map requestMap = getFacesContext().getExternalContext().getRequestMap();
+
+            if (nodeId == null)
+            {
+                requestMap.remove(_var);
+            } else
+            {
+                requestMap.put(_var, getNode());
+            }
+        }
+    }
+
+    /**
+     * Gets an array of String containing the ID's of all of the {@link TreeNode}s in the path to
+     * the specified node.  The path information will be an array of <code>String</code> objects
+     * representing node ID's. The array will starting with the ID of the root node and end with
+     * the ID of the specified node.
+     *
+     * @param nodeId The id of the node for whom the path information is needed.
+     * @return String[]
+     */
+    public String[] getPathInformation(String nodeId)
+    {
+        return getDataModel().getPathInformation(nodeId);
+    }
+
+    /**
+     * Indicates whether or not the specified {@link TreeNode} is the last child in the <code>List</code>
+     * of children.  If the node id provided corresponds to the root node, this returns <code>true</code>.
+     *
+     * @param nodeId The ID of the node to check
+     * @return boolean
+     */
+    public boolean isLastChild(String nodeId)
+    {
+        return getDataModel().isLastChild(nodeId);
+    }
+
+    /**
+     * Returns a previously cached {@link TreeModel}, if any, or sets the cache variable to either the
+     * current value (if its a {@link TreeModel}) or to a new instance of {@link TreeModel} (if it's a
+     * {@link TreeNode}) with the provided value object as the root node.
+     *
+     * @return TreeModel
+     */
+    public TreeModel getDataModel()
+    {
+        if (_cachedModel != null)
+        {
+            return _cachedModel;
+        }
+
+        Object value = getValue();
+        if (value != null)
+        {
+            if (value instanceof TreeModel)
+            {
+                _cachedModel = (TreeModel) value;
+            }
+            else if (value instanceof TreeNode)
+            {
+                _cachedModel = new TreeModelBase((TreeNode) value);
+            } else
+            {
+                throw new IllegalArgumentException("Value must be a TreeModel or TreeNode");
+            }
+        }
+
+        if (_restoredState != null)
+            _cachedModel.setTreeState(_restoredState); // set the restored state (if there is one) on the model
+
+        return _cachedModel;
+    }
+
+    /**
+     * Epands all nodes by default.
+     */
+    public void expandAll()
+    {
+        toggleAll(true);
+    }
+
+    /**
+     * Collapse all nodes by default.
+     */
+    public void collapseAll()
+    {
+        toggleAll(false);
+    }
+
+    /**
+     * Toggles all of the nodes to either expanded or collapsed depending on the
+     * parameter supplied.
+     *
+     * @param expanded Expand all of the nodes (a value of false indicates collapse
+     * all nodes)
+     */
+    private void toggleAll(boolean expanded)
+    {
+        TreeWalker walker = getDataModel().getTreeWalker();
+        walker.reset();
+
+        TreeState state =  getDataModel().getTreeState();
+        walker.setCheckState(false);
+        walker.setTree(this);
+
+        while(walker.next())
+        {
+            String id = getNodeId();
+            if ((expanded && !state.isNodeExpanded(id)) || (!expanded && state.isNodeExpanded(id)))
+            {
+                state.toggleExpanded(id);
+            }
+        }
+    }
+
+    /**
+     * Expands all of the nodes in the specfied path.
+     * @param nodePath The path to expand.
+     */
+    public void expandPath(String[] nodePath)
+    {
+        getDataModel().getTreeState().expandPath(nodePath);
+    }
+
+    /**
+     * Expands all of the nodes in the specfied path.
+     * @param nodePath The path to expand.
+     */
+    public void collapsePath(String[] nodePath)
+    {
+        getDataModel().getTreeState().collapsePath(nodePath);
+    }
+
+
+    protected void processNodes(FacesContext context, int processAction, TreeWalker walker)
+    {
+        UIComponent facet = null;
+        walker.reset();
+        walker.setTree(this);
+
+        while(walker.next())
+        {
+            TreeNode node = getNode();
+            facet = getFacet(node.getType());
+
+            if (facet == null)
+            {
+                log.warn("Unable to locate facet with the name: " + node.getType());
+                continue;
+                //throw new IllegalArgumentException("Unable to locate facet with the name: " + node.getType());
+            }
+
+            switch (processAction)
+            {
+                case PROCESS_DECODES:
+
+                    facet.processDecodes(context);
+                    break;
+
+                case PROCESS_VALIDATORS:
+
+                    facet.processValidators(context);
+                    break;
+
+                case PROCESS_UPDATES:
+
+                    facet.processUpdates(context);
+                    break;
+            }
+        }
+
+    }
+
+    /**
+     * To support using input components for the nodes (e.g., input fields, checkboxes, and selection
+     * lists) while still only using one set of components for all nodes, the state held by the components
+     * for the current node must be saved for a new node is selected.
+     */
+    private void saveDescendantState()
+    {
+        FacesContext context = getFacesContext();
+        Iterator i = getFacets().values().iterator();
+        while (i.hasNext())
+        {
+            UIComponent facet = (UIComponent) i.next();
+            saveDescendantState(facet, context);
+        }
+    }
+
+    /**
+     * Overloaded helper method for the no argument version of this method.
+     *
+     * @param component The component whose state needs to be saved
+     * @param context   FacesContext
+     */
+    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());
+        }
+
+        List kids = component.getChildren();
+        for (int i = 0; i < kids.size(); i++)
+        {
+            saveDescendantState((UIComponent) kids.get(i), context);
+        }
+    }
+
+
+    /**
+     * Used to configure a new node with the state stored previously.
+     */
+    private void restoreDescendantState()
+    {
+        FacesContext context = getFacesContext();
+        Iterator i = getFacets().values().iterator();
+        while (i.hasNext())
+        {
+            UIComponent facet = (UIComponent) i.next();
+            restoreDescendantState(facet, context);
+        }
+    }
+
+    /**
+     * Overloaded helper method for the no argument version of this method.
+     *
+     * @param component The component whose state needs to be restored
+     * @param context   FacesContext
+     */
+    private void restoreDescendantState(UIComponent component, FacesContext context)
+    {
+        String id = component.getId();
+        component.setId(id); // forces the cilent 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());
+        }
+
+        List kids = component.getChildren();
+        for (int i = 0; i < kids.size(); i++)
+        {
+            restoreDescendantState((UIComponent)kids.get(i), context);
+        }
+        Map facets = component.getFacets();
+        for(Iterator i = facets.values().iterator(); i.hasNext();)
+        {
+            restoreDescendantState((UIComponent)i.next(), context);
+        }
+    }
+
+    /**
+     * A regular bean with accessor methods for all state variables.
+     *
+     * @author Sean Schofield
+     * @author Hans Bergsten (Some code taken from an example in his O'Reilly JavaServer Faces book. Copied with permission)
+     * @version $Revision: 703742 $ $Date: 2008-10-11 17:10:36 -0500 (sáb, 11 oct 2008) $
+     */
+    private static class SavedState implements Serializable
+    {
+        private static final long serialVersionUID = 273343276957070557L;
+        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;
+        }
+
+        void setValue(Object value)
+        {
+            this.value = value;
+        }
+
+        boolean isLocalValueSet()
+        {
+            return localValueSet;
+        }
+
+        void setLocalValueSet(boolean localValueSet)
+        {
+            this.localValueSet = localValueSet;
+        }
+    }
+
+    /**
+     * Inner class used to wrap the original events produced by child components in the tree.
+     * This will allow the tree to find the appropriate component later when its time to
+     * broadcast the events to registered listeners.  Code is based on a similar private
+     * class for UIData.
+     */
+    private static class FacesEventWrapper extends FacesEvent
+    {
+        private static final long serialVersionUID = -3056153249469828447L;
+        private FacesEvent _wrappedFacesEvent;
+        private String _nodeId;
+
+
+        public FacesEventWrapper(FacesEvent facesEvent, String nodeId, UIComponent component)
+        {
+            super(component);
+            _wrappedFacesEvent = facesEvent;
+            _nodeId = nodeId;
+        }
+
+
+        public PhaseId getPhaseId()
+        {
+            return _wrappedFacesEvent.getPhaseId();
+        }
+
+
+        public void setPhaseId(PhaseId phaseId)
+        {
+            _wrappedFacesEvent.setPhaseId(phaseId);
+        }
+
+
+        public void queue()
+        {
+            _wrappedFacesEvent.queue();
+        }
+
+
+        public String toString()
+        {
+            return _wrappedFacesEvent.toString();
+        }
+
+
+        public boolean isAppropriateListener(FacesListener faceslistener)
+        {
+            // this event type is only intended for wrapping a real event
+            return false;
+        }
+
+
+        public void processListener(FacesListener faceslistener)
+        {
+            throw new UnsupportedOperationException("This event type is only intended for wrapping a real event");
+        }
+
+
+        public FacesEvent getFacesEvent()
+        {
+            return _wrappedFacesEvent;
+        }
+
+
+        public String getNodeId()
+        {
+            return _nodeId;
+        }
+    }
+
+    /**
+     * Returns true if there is an error message queued for at least one of the nodes.
+     *
+     * @param context FacesContext
+     * @return whether an error message is present
+     */
+    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;
+    }
+
+    /**
+     * Toggle the expanded state of the current node.
+     */
+    public void toggleExpanded()
+    {
+        getDataModel().getTreeState().toggleExpanded(getNodeId());
+    }
+
+    /**
+     * Indicates whether or not the current {@link TreeNode} is expanded.
+     * @return boolean
+     */
+    public boolean isNodeExpanded()
+    {
+        return getDataModel().getTreeState().isNodeExpanded(getNodeId());
+    }
+
+    /**
+     * Implements the {@link javax.faces.event.ActionListener} interface.  Basically, this
+     * method is used to listen for node selection events (when a user has clicked on a
+     * leaf node.)
+     *
+     * @param event ActionEvent
+     */
+    public void setNodeSelected(ActionEvent event)
+    {
+        getDataModel().getTreeState().setSelected(getNodeId());
+    }
+
+    /**
+     * Indicates whether or not the current {@link TreeNode} is selected.
+     * @return boolean
+     */
+    public boolean isNodeSelected()
+    {
+        return (getNodeId() != null) ? getDataModel().getTreeState().isSelected(getNodeId()) : false;
+    }
+}