Package org.apache.click.extras.tree

Source Code of org.apache.click.extras.tree.Tree$Entry

/*
* 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.click.extras.tree;

import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.StringTokenizer;

import org.apache.click.ActionListener;
import org.apache.click.Context;
import org.apache.click.Control;
import org.apache.click.ActionEventDispatcher;
import org.apache.click.control.AbstractControl;
import org.apache.click.control.ActionLink;
import org.apache.click.control.Decorator;
import org.apache.click.extras.control.SubmitLink;
import org.apache.click.util.ClickUtils;
import org.apache.click.util.HtmlStringBuffer;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;

/**
* Provides a tree control  for displaying hierarchical data. The tree operates
* on a hierarchy of {@link TreeNode}'s. Each TreeNode must provide a
* uniquely identified node in the hierarchy.
* <p/>
* Below is screenshot of how the tree will render in a browser.
*
* <table cellspacing='10'>
* <tr>
* <td>
* <img align='middle' hspace='2' src='tree.png' title='Tree'/>
* </td>
* </tr>
* </table>
*
* <h3>Tree Example</h3>
*
* An example tree usage is provided below (this code was used to produce the screenshot):
*
* <pre class="prettyprint">
* public class PlainTreePage extends BorderPage {
*
*     public PlainTreePage() {
*         Tree tree = buildTree();
*         addControl(tree);
*     }
*
*     // This method creates a representation of a Windows OS directory.
*     public Tree buildTree() {
*         Tree tree = new Tree("tree");
*
*         // Create a node representing the root directory with the specified
*         // parameter as the value. Because an id is not specified, a random
*         // one will be generated by the node. By default the root node is
*         // not rendered by the tree. This can be changed by calling
*         // tree.setRootNodeDisplayed(true).
*         TreeNode root = new TreeNode("c:");
*
*         // Create a new directory, setting the root directory as its parent. Here
*         // we do specify a id as the 2nd argument, so no id is generated.
*         TreeNode dev = new TreeNode("dev","1", root);
*
*         // The following two nodes represent files in the directory.
*         // The false argument to the constructor below means that these nodes
*         // does not support child nodes. Makes sense since files cannot contain
*         // directories or other files
*         new TreeNode("java.pdf", "2", dev, false);
*         new TreeNode("ruby.pdf", "3", dev, false);
*
*         TreeNode programFiles = new TreeNode("program files", "4", root);
*         TreeNode adobe = new TreeNode("Adobe", "5", programFiles);
*
*         TreeNode download = new TreeNode("downloads","6", root);
*         TreeNode web = new TreeNode("web", "7", download);
*         new TreeNode("html.pdf", "8", web);
*         new TreeNode("css.html", "9", web);
*
*         TreeNode databases = new TreeNode("databases", "10", download);
*         new TreeNode("mysql.html","11",databases);
*         new TreeNode("oracle.pdf","12",databases);
*         new TreeNode("postgres","13",databases);
*
*         tree.setRootNode(root);
*         return tree;
*     }
* } </pre>
*
* <a name="resources"></a>
* <h3>CSS and JavaScript resources</h3>
*
* The Tree control makes use of the following resources
* (which Click automatically deploys to the application directory, <tt>/click/tree</tt>):
*
* <ul>
* <li><tt>click/tree/tree.css</tt></li>
* <li><tt>click/tree/tree.js</tt></li>
* <li><tt>click/tree/cookie-helper.js</tt></li>
* </ul>
*
* To import these Tree files simply reference the variables
* <span class="blue">$headElements</span> and
* <span class="blue">$jsElements</span> in the page template. For example:
*
* <pre class="codeHtml">
* &lt;html&gt;
* &lt;head&gt;
* <span class="blue">$headElements</span>
* &lt;/head&gt;
* &lt;body&gt;
*
* <span class="red">$tree</span>
*
* <span class="blue">$jsElements</span>
* &lt;/body&gt;
* &lt;/html&gt; </pre>
*
* <a name="customization"></a>
* <h3>Tree customization</h3>
*
* The following list of stylesheet classes are used to render the tree
* icons. One can easily change the <tt>tree.css</tt> to use a different set of
* icons. Note: all CSS classes are set inline in &lt;span&gt; elements.
* <ul>
* <li>&lt;span class=<span class="blue">"leafIcon"</span>&gt; - renders the leaf node of the tree</li>
* <li>&lt;span class=<span class="blue">"expandedIcon"</span>&gt; - renders the expanded state of a node</li>
* <li>&lt;span class=<span class="blue">"collapsedIcon"</span>&gt; - renders the collapsed state of a node</li>
* </ul>
*
* <strong>Credit</strong> goes to <a href="http://wicket.apache.org">Wicket</a>
* for these images:
* <ul>
*      <li>images/folder-closed.png</li>
*      <li>images/folder-open.png</li>
*      <li>images/item.png</li>
* </ul>
*/
public class Tree extends AbstractControl {

    // -------------------------------------------------------------- Constants

    /** The tree's expand/collapse parameter name: <tt>"expandTreeNode"</tt>. */
    public static final String EXPAND_TREE_NODE_PARAM = "expandTreeNode";

    /** The tree's select/deselect parameter name: <tt>"selectTreeNode"</tt>. */
    public static final String SELECT_TREE_NODE_PARAM = "selectTreeNode";

    /** The Tree imports statement. */
    public static final String TREE_IMPORTS =
            "<link type=\"text/css\" rel=\"stylesheet\" href=\"{0}/click/tree/tree{1}.css\"/>\n";

    /** Client side javascript imports statement. */
    public static final String JAVASCRIPT_IMPORTS =
            "<script type=\"text/javascript\" src=\"{0}/click/tree/tree{1}.js\"></script>\n";

    /** Client side javascript cookie imports statement. */
    public static final String JAVASCRIPT_COOKIE_IMPORTS =
            "<script type=\"text/javascript\" src=\"{0}/click/tree/cookie-helper{1}.js\"></script>\n";

    /** Indicator for using cookies to implement client side behavior. */
    public final static int JAVASCRIPT_COOKIE_POLICY = 1;

    /** Indicator for using the session to implement client side behavior. */
    public final static int JAVASCRIPT_SESSION_POLICY = 2;

    /** The tree's expand icon name: <tt>"expandedIcon"</tt>. */
    protected static final String EXPAND_ICON = "expandedIcon";

    /** The tree's collapsed icon name: <tt>"collapsedIcon"</tt>. */
    protected static final String COLLAPSE_ICON = "collapsedIcon";

    /** The tree's leaf icon name: <tt>"leafIcon"</tt>. */
    protected static final String LEAF_ICON = "leafIcon";

    /** default serial version id. */
    private static final long serialVersionUID = 1L;

    // ----------------------------------------------------- Instance Variables

    /** The tree's hierarchical data model. */
    protected TreeNode rootNode;

    /** Array of ids that must be selected or deselected. */
    protected String[] selectOrDeselectNodeIds = null;

    /** Array of ids that must be expanded or collapsed. */
    protected String[] expandOrCollapseNodeIds = null;

    /** The Tree node select / deselect link. */
    protected ActionLink selectLink;

    /** The tree node expand / collapse link. */
    protected ActionLink expandLink;

    /** Callback provider for users to decorate tree nodes. */
    private transient Decorator decorator;

    /**
     * Specifies if the root node should be displayed, or only its children.
     * By default this value is false.
     */
    private boolean rootNodeDisplayed = false;

    /** Specifies if client side javascript functionality are enabled. By default this value is false.*/
    private boolean javascriptEnabled = false;

    /** List of subscribed listeners to tree events.*/
    private List listeners = new ArrayList();

    /** Current javascript policy in effect. */
    private int javascriptPolicy = 0;

    /** Flag indicates if listeners should be notified of any state changes. */
    private boolean notifyListeners = true;

    // ---------------------------------------------------- Public Constructors

    /**
     * Create an Tree control for the given name.
     * <p/>
     * The constructor also sets the id attribute to
     * <tt>"tree"</tt> and the css class to <tt>"treestyle"</tt>
     * to qualify the tree control when styled by tree.css. If the
     * css class value is changed, ensure to also change the
     * tree.css selectors that still reference <tt>"treestyle"</tt>.
     *
     * @param name the tree name
     * @throws IllegalArgumentException if the name is null
     */
    public Tree(String name) {
        setName(name);
        setAttribute("id", "tree");
        setAttribute("class", "treestyle");
    }

    /**
     * Create a Tree with no name defined.
     * <p/>
     * The constructor also sets the id attribute to
     * <tt>"tree"</tt> and the css class to <tt>"treestyle"</tt>
     * to qualify the tree control when styled by tree.css. If the
     * css class value is changed, ensure to also change the
     * tree.css selectors that still reference <tt>"treestyle"</tt>.
     * <p/>
     * <b>Please note</b> the control's name must be defined before it is valid.
     */
    public Tree() {
        super();
        setAttribute("id", "tree");
        setAttribute("class", "treestyle");
    }

    // --------------------------------------------- Public Getters and Setters

    /**
     * @see Control#setName(String)
     *
     * @param name of the control
     * @throws IllegalArgumentException if the name is null
     */
    public void setName(String name) {
        super.setName(name);
        getExpandLink().setName(name + "-expandLink");
        getExpandLink().setLabel("");
        getExpandLink().setParent(this);

        getSelectLink().setName(name + "-selectLink");
        getSelectLink().setLabel("");
        getSelectLink().setParent(this);
    }

    /**
     * Return the tree's root TreeNode. This method will recalculate
     * the tree's root node in case a new root node was set.
     *
     * @return the tree's root TreeNode.
     */
    public TreeNode getRootNode() {
        //Calculate the root node dynamically by finding the node where parent == null.
        //Thus if a new root node was created this method will still return
        //the correct node
        if (rootNode == null) {
            return null;
        }
        while ((rootNode.getParent()) != null) {
            rootNode = rootNode.getParent();
        }
        return rootNode;
    }

    /**
     * Return if tree has a root node.
     *
     * @return boolean indicating if the tree's root has been set.
     */
    public boolean hasRootNode() {
        return getRootNode() != null;
    }

    /**
     * Return if the tree's root node should be displayed or not.
     *
     * @return if root node should be displayed
     */
    public boolean isRootNodeDisplayed() {
        return rootNodeDisplayed;
    }

    /**
     * Sets whether the tree's root node should be displayed or not.
     *
     * @param rootNodeDisplayed true if the root node should be displayed,
     * false otherwise
     */
    public void setRootNodeDisplayed(boolean rootNodeDisplayed) {
        this.rootNodeDisplayed = rootNodeDisplayed;
    }

    /**
     * Set the tree's root TreeNode.
     *
     * @param rootNode node will be set as the root
     */
    public void setRootNode(TreeNode rootNode) {
        if (rootNode == null) {
            return;
        }
        this.rootNode = rootNode;
    }

    /**
     * Get the tree's decorator.
     *
     * @return the tree's decorator.
     */
    public Decorator getDecorator() {
        return decorator;
    }

    /**
     * Set the tree's decorator which enables a interception point for users to render
     * the tree nodes.
     *
     * @param decorator the tree's decorator
     */
    public void setDecorator(Decorator decorator) {
        this.decorator = decorator;
    }

    /**
     * Returns if javascript functionality are enabled or not.
     *
     * @return true if javascript functions are enabled, false otherwise
     * @see #setJavascriptEnabled(boolean)
     */
    public boolean isJavascriptEnabled() {
        return javascriptEnabled;
    }

    /**
     * Enables javascript functionality.
     * <p/>
     * If true the tree will be navigatable in the browser using javascript,
     * instead of doing round trips to the server on each operation.
     * <p/>
     * With javascript enabled you need to store the values passed from the
     * browser between requests. The tree currently supports the
     * following options:
     * <ul>
     *     <li>{@link #JAVASCRIPT_COOKIE_POLICY}
     *     <li>{@link #JAVASCRIPT_SESSION_POLICY}
     * </ul>
     * This method will try and determine which policy should be applied
     * to the current request by checking the value
     * {@link javax.servlet.http.HttpServletRequest#isRequestedSessionIdFromCookie()}.
     * If {@link javax.servlet.http.HttpServletRequest#isRequestedSessionIdFromCookie()}
     * returns true, {@link #JAVASCRIPT_COOKIE_POLICY} will be used, otherwise
     * {@link #JAVASCRIPT_SESSION_POLICY}.
     * <p/>
     * <strong>Note:</strong> if javascript is enabled, then the entire
     * tree is rendered even if some nodes are in a collapsed state. This
     * enables the tree to still be fully navigatable in the browser. However
     * nodes that are in a collapsed state are still displayed as collapsed
     * using the style <tt>"display:none"</tt>.
     *
     * @see #setJavascriptEnabled(boolean, int)
     *
     * @param newValue the value to set the javascriptEnabled property to
     * @throws IllegalArgumentException if the context is null
     */
    public void setJavascriptEnabled(boolean newValue) {
        if (getContext().getRequest().isRequestedSessionIdFromCookie()) {
            setJavascriptEnabled(newValue, JAVASCRIPT_COOKIE_POLICY);
        } else {
            setJavascriptEnabled(newValue, JAVASCRIPT_SESSION_POLICY);
        }
    }

    /**
     * Overloads {@link #setJavascriptEnabled(boolean)}. Enables one
     * to select the javascript policy to apply.
     *
     * @see #setJavascriptEnabled(boolean)
     *
     * @param newValue the value to set the javascriptEnabled property to
     * @param javascriptPolicy the current javascript policy
     * @throws IllegalArgumentException if the context is null
     */
    public void setJavascriptEnabled(boolean newValue, int javascriptPolicy) {

        this.javascriptEnabled = newValue;
        if (javascriptEnabled) {
            javascriptHandler = createJavascriptHandler(javascriptPolicy);
            addListener(javascriptHandler);
            this.javascriptPolicy = javascriptPolicy;

        } else {
            removeListener(javascriptHandler);
            this.javascriptPolicy = 0;
        }
    }

    /**
     * Return the CSS "width" style attribute of the tree, or null if not
     * defined.
     *
     * @return the CSS "width" style attribute of the tree, or null if not
     * defined
     */
    public String getWidth() {
        return getStyle("width");
    }

    /**
     * Set the the CSS "width" style attribute of the tree. For example:
     *
     * <pre class="prettyprint">
     * Tree tree = new Tree("mytree");
     * tree.setWidth("200px"); </pre>
     *
     * @param value the CSS "width" style attribute
     */
    public void setWidth(String value) {
        setStyle("width", value);
    }

    /**
     * Return the CSS "height" style of the tree, or null if not defined.
     *
     * @return the CSS "height" style attribute of the tree, or null if not
     * defined
     */
    public String getHeight() {
        return getStyle("height");
    }

    /**
     * Set the the CSS "height" style attribute of the tree. For example:
     *
     * <pre class="prettyprint">
     * Tree tree = new Tree("mytree");
     * tree.setHeight("200px"); </pre>
     *
     * @param value the CSS "height" style attribute
     */
    public void setHeight(String value) {
        setStyle("height", value);
    }

    /**
     * Return the Tree HTML head imports statements for the following
     * resources:
     * <p/>
     * <ul>
     * <li><tt>click/tree/tree.css</tt></li>
     * <li><tt>click/tree/tree.js</tt></li>
     * <li><tt>click/tree/cookie-helper.js</tt></li>
     * </ul>
     *
     * @see org.apache.click.Control#getHtmlImports()
     *
     * @return the HTML head import statements for the control
     */
    public String getHtmlImports() {
        Context context = getContext();
        HtmlStringBuffer buffer = new HtmlStringBuffer(256);
        if (isJavascriptEnabled()) {
            buffer.append(ClickUtils.createHtmlImport(JAVASCRIPT_IMPORTS,
                context));
            if (javascriptPolicy == JAVASCRIPT_COOKIE_POLICY) {
                buffer.append(ClickUtils.createHtmlImport(JAVASCRIPT_COOKIE_IMPORTS,
                    context));
            }
        }
        buffer.append(ClickUtils.createHtmlImport(TREE_IMPORTS, context));
        return buffer.toString();
    }

   /**
     * @see org.apache.click.Control#getHeadElements()
     *
     * @return the list of HEAD elements to be included in the page
     */
    public List getHeadElements() {
        if (headElements == null) {
            headElements = super.getHeadElements();
            headElements.addAll(getExpandLink().getHeadElements());
            headElements.addAll(getSelectLink().getHeadElements());
        }
        return headElements;
    }

    /**
     * Return the tree node expand / collapse link.
     * <p/>
     * This method returns a {@link org.apache.click.extras.control.SubmitLink}
     * so that the Tree can function properly when added to a
     * {@link org.apache.click.control.Form}.
     *
     * @return the tree node expand / collapse link
     */
    public ActionLink getExpandLink() {
        if (expandLink == null) {
            expandLink = new SubmitLink();
        }
        return expandLink;
    }

    /**
     * Return the tree node select / deselect link.
     * <p/>
     * This method returns a {@link org.apache.click.extras.control.SubmitLink}
     * so that the Tree can function properly when added to a
     * {@link org.apache.click.control.Form}.
     *
     * @return the tree node select / deselect link.
     */
    public ActionLink getSelectLink() {
        if (selectLink == null) {
            selectLink = new SubmitLink();
        }
        return selectLink;
    }

    // --------------------------------------------------------- Public Methods

    /**
     * This method binds the users request of expanded and collapsed nodes to
     * the tree's nodes.
     */
    public void bindExpandOrCollapseValues() {
        expandOrCollapseNodeIds = getExpandLink().getParameterValues(EXPAND_TREE_NODE_PARAM);
    }

    /**
     * This method binds the users request of selected nodes to the tree's nodes.
     */
    public void bindSelectOrDeselectValues() {
        selectOrDeselectNodeIds = getSelectLink().getParameterValues(SELECT_TREE_NODE_PARAM);
    }

    /**
     * Query if the tree will notify its tree listeners of any change
     * to the tree's model.
     *
     * @return true if listeners should be notified of any changes.
     */
    public boolean isNotifyListeners() {
        return notifyListeners;
    }

    /**
     * Enable or disable if the tree will notify its tree listeners of any change
     * to the tree's model.
     *
     * @param notifyListeners true if the tree will notify its listeners ,
     * false otherwise
     */
    public void setNotifyListeners(boolean notifyListeners) {
        this.notifyListeners = notifyListeners;
    }

    /**
     * Expand the node with matching id and inform any listeners of the change.
     * If {@link #isNotifyListeners()} returns false, this method will not
     * notify its listeners of any change.
     *
     * @param id identifier of the node to be expanded.
     */
    public void expand(String id) {
        if (id == null) {
            return;
        }
        setExpandState(id, true);
    }

    /**
     * Expand the node and inform any listeners of the change.
     * If {@link #isNotifyListeners()} returns false, this method will not
     * notify listeners of any change.
     *
     * @param node the node to be expanded.
     */
    public void expand(TreeNode node) {
        if (node == null) {
            return;
        }
        setExpandState(node, true);
    }

    /**
     * Collapse the node with matching id and inform any listeners of the change.
     * If {@link #isNotifyListeners()} returns false, this method will not
     * notify listeners of any change.
     *
     * @param id identifier of node to be collapsed.
     */
    public void collapse(String id) {
        if (id == null) {
            return;
        }
        setExpandState(id, false);
    }

    /**
     * Collapse the node and inform any listeners of the change.
     * If {@link #isNotifyListeners()} returns false, this method will not
     * notify listeners of any change.
     *
     * @param node the node to be collapsed.
     */
    public void collapse(TreeNode node) {
        if (node == null) {
            return;
        }
        setExpandState(node, false);
    }

    /**
     * Expand all the nodes of the tree and inform any listeners of the change.
     * If {@link #isNotifyListeners()} returns false, this method will not
     * notify listeners of any change.
     */
    public void expandAll() {
        for (Iterator it = iterator(); it.hasNext();) {
            TreeNode node = (TreeNode) it.next();
            boolean oldValue = node.isExpanded();
            node.setExpanded(true);
            if (isNotifyListeners()) {
                fireNodeExpanded(node, oldValue);
            }
        }
    }

    /**
     * Collapse all the nodes of the tree and inform any listeners of the change.
     * If {@link #isNotifyListeners()} returns false, this method will not
     * notify listeners of any change.
     */
    public void collapseAll() {
        for (Iterator it = iterator(); it.hasNext();) {
            TreeNode node = (TreeNode) it.next();
            boolean oldValue = node.isExpanded();
            node.setExpanded(false);
            if (isNotifyListeners()) {
                fireNodeCollapsed(node, oldValue);
            }
        }
    }

    /**
     * Select the node with matching id and inform any listeners of the change.
     * If {@link #isNotifyListeners()} returns false, this method will not
     * notify listeners of any change.
     *
     * @param id identifier of node to be selected.
     */
    public void select(String id) {
        if (id == null) {
            return;
        }
        setSelectState(id, true);
    }

    /**
     * Select the node and inform any listeners of the change.
     * If {@link #isNotifyListeners()} returns false, this method will not
     * notify listeners of any change.
     *
     * @param node the node to be selected.
     */
    public void select(TreeNode node) {
        if (node == null) {
            return;
        }
        setSelectState(node, true);
    }

    /**
     * Deselect the node with matching id and inform any listeners of the change.
     * If {@link #isNotifyListeners()} returns false, this method will not
     * notify listeners of any change.
     *
     * @param id id of node to be deselected.
     */
    public void deselect(String id) {
        if (id == null) {
            return;
        }
        setSelectState(id, false);
    }

    /**
     * Deselect the node and inform any listeners of the change.
     * If {@link #isNotifyListeners()} returns false, this method will not
     * notify listeners of any change.
     *
     * @param node the node to be deselected.
     */
    public void deselect(TreeNode node) {
        if (node == null) {
            return;
        }
        setSelectState(node, false);
    }

    /**
     * Select all the nodes of the tree and inform any listeners of the change.
     * If {@link #isNotifyListeners()} returns false, this method will not
     * notify listeners of any change.
     */
    public void selectAll() {
        for (Iterator it = iterator(); it.hasNext();) {
            TreeNode node = (TreeNode) it.next();
            boolean oldValue = node.isSelected();
            node.setSelected(true);
            if (isNotifyListeners()) {
                fireNodeSelected(node, oldValue);
            }
        }
    }

    /**
     * Deselect all the nodes of the tree and inform any listeners of the change.
     * If {@link #isNotifyListeners()} returns false, this method will not
     * notify listeners of any change.
     */
    public void deselectAll() {
        for (Iterator it = iterator(); it.hasNext();) {
            TreeNode node = (TreeNode) it.next();
            boolean oldValue = node.isSelected();
            node.setSelected(false);
            if (isNotifyListeners()) {
                fireNodeDeselected(node, oldValue);
            }
        }
    }

    /**
     * Returns all the nodes that were expanded.
     *
     * @param includeInvisibleNodes indicator if only invisible nodes should be included.
     * @return list of currently expanded nodes.
     */
    public List getExpandedNodes(boolean includeInvisibleNodes) {
        List currentlyExpanded = new ArrayList();
        for (Iterator it = iterator(); it.hasNext();) {
            TreeNode node = (TreeNode) it.next();
            if (node.isExpanded())  {
                if (includeInvisibleNodes || isVisible(node)) {
                    currentlyExpanded.add(node);
                }
            }
        }
        return currentlyExpanded;
    }

    /**
     * Returns all the nodes that were selected.
     *
     * @param includeInvisibleNodes indicates if invisible nodes should be included.
     * @return list of currently selected nodes.
     */
    public List getSelectedNodes(boolean includeInvisibleNodes) {
        List currentlySelected = new ArrayList();
        for (Iterator it = iterator(); it.hasNext();) {
            TreeNode node = (TreeNode) it.next();
            if (node.isSelected()) {
                if (includeInvisibleNodes || isVisible(node)) {
                    currentlySelected.add(node);
                }
            }
        }
        return currentlySelected;
    }

    /**
     * Provides a TreeNode callback interface.
     */
    protected interface Callback {

        /**
         * Callback on the provided tree node.
         *
         * @param node the TreeNode to callback
         */
        public void callback(final TreeNode node);
    }

    /**
     * Returns an iterator over all the nodes.
     *
     * @return iterator over all elements in the tree
     */
    public Iterator iterator() {
        return iterator(getRootNode());
    }

    /**
     * Returns an iterator over all nodes starting from the specified node.
     * If null is specified, root node is used instead.
     *
     * @param node starting point of nodes to iterator over
     * @return iterator over all nodes starting form the specified node
     */
    public Iterator iterator(TreeNode node) {
        if (node == null) {
            node = getRootNode();
        }
        return new BreadthTreeIterator(node);
    }

    /**
     * Finds and returns the first node that matches the id.
     *
     * @param id identifier of the node to find
     * @return TreeNode the first node matching the id.
     * @throws IllegalArgumentException if argument is null.
     */
    public TreeNode find(String id) {
        if (id == null) {
            throw new IllegalArgumentException("Argument cannot be null.");
        }
        return find(getRootNode(), id);
    }

    /**
     * This method binds any expand/collapse and select/deselect changes from
     * the request parameters.
     * <p/>
     * In other words the node id's of expanded, collapsed, selected and
     * deselected nodes are retrieved from the request.
     *
     * @see #bindExpandOrCollapseValues()
     * @see #bindSelectOrDeselectValues()
     */
    public void bindRequestValue() {
        bindExpandOrCollapseValues();
        bindSelectOrDeselectValues();
    }

    /**
     * Processes user request to change state of the tree.
     * This implementation processes any expand/collapse and select/deselect
     * changes as requested.
     * <p/>
     * Thus expanded nodes will be collapsed and collapsed nodes will be
     * expanded. Similarly selected nodes will be deselected and deselected
     * nodes will be selected.
     *
     * @see org.apache.click.Control#onProcess()
     * @see #expandOrCollapse(java.lang.String[])
     * @see #selectOrDeselect(java.lang.String[])
     *
     * @return true to continue Page event processing or false otherwise
     */
    public boolean onProcess() {
        getExpandLink().onProcess();
        getSelectLink().onProcess();
        bindRequestValue();

        ActionEventDispatcher.dispatchActionEvent(this, new ActionListener() {
            public boolean onAction(Control source) {
                return postProcess();
            }
        });
        return true;
    }

    /**
     * This method cleans up the {@link #expandLink} and {@link #selectLink}.
     * @see org.apache.click.Control#onDestroy()
     */
    public void onDestroy() {
        super.onDestroy();
        getExpandLink().onDestroy();
        getSelectLink().onDestroy();
    }

    /**
     * Set the controls event listener.
     * <p/>
     * To receive notifications when TreeNodes are selected or expanded please
     * use {@link #addListener(TreeListener)}.
     *
     * @param listener the listener object with the named method to invoke
     * @param method the name of the method to invoke
     */
    public void setListener(Object listener, String method) {
        super.setListener(listener, method);
    }

    /**
     * Set the control's action listener.
     * <p/>
     * To receive notifications when TreeNodes are selected or expanded please
     * use {@link #addListener(TreeListener)}.
     *
     * @param listener the control's action listener
     */
    public void setActionListener(ActionListener listener) {
        super.setActionListener(listener);
    }

    /**
     * Adds the listener to start receiving tree events.
     *
     * @param listener to add to start receiving tree events.
     */
    public void addListener(TreeListener listener) {
        listeners.add(listener);
    }

    /**
     * Removes the listener to stop receiving tree events.
     *
     * @param listener to be removed to stop receiving tree events.
     */
    public void removeListener(TreeListener listener) {
        listeners.remove(listener);
    }

    // ------------------------------------------------------ Default Rendering

    /**
     * @see AbstractControl#getControlSizeEst()
     *
     * @return the estimated rendered control size in characters
     */
    public int getControlSizeEst() {
        return 256;
    }

    /**
     * Utility method that force the Tree to remove any entries it made in the
     * HttpSession.
     * <p/>
     * <b>Note</b> Tree only stores a value in the Session when JavaScript
     * is enabled and set to {@link #JAVASCRIPT_SESSION_POLICY}.
     */
    public void cleanupSession() {
        Context context = getContext();
        if (context.hasSession()) {
            context.getSession().removeAttribute(SessionHandler.JS_HANDLER_SESSION_KEY);
        }
    }

    /**
     * Render the HTML representation of the tree.
     *
     * @see #toString()
     *
     * @param buffer the specified buffer to render the control's output to
     */
    public void render(HtmlStringBuffer buffer) {
        buffer.elementStart("div");
        buffer.appendAttribute("id", getId());

        appendAttributes(buffer);

        buffer.append(">\n");

        if (isRootNodeDisplayed()) {
            TreeNode temp = new TreeNode();

            //Do not use the method temp.add(), because that will
            //set temp as the parent of the current root node. Temp
            //will then become the new root node of the tree.
            temp.addChildOnly(getRootNode());
            renderTree(buffer, temp, 0);
        } else {
            renderTree(buffer, getRootNode(), 0);
        }

        buffer.elementEnd("div");
        buffer.append("\n");

        if (isJavascriptEnabled()) {
            //Complete the lifecycle of the javascript handler.
            javascriptHandler.destroy();
        }
    }

    /**
     * Return a HTML rendered Tree string of all the tree's nodes.
     *
     * <p/>Note: by default the tree's root node will not be rendered.
     * However this behavior can be changed by calling
     * {@link #setRootNodeDisplayed(boolean)} with true.
     *
     * @see java.lang.Object#toString()
     * @return a HTML rendered Tree string
     */
    public String toString() {
        HtmlStringBuffer buffer = new HtmlStringBuffer(getControlSizeEst());
        render(buffer);
        return buffer.toString();
    }

    /**
     * Render the children of the specified tree node as html markup and append
     * the output to the specified buffer.
     * <p/>
     * <strong>Note:</strong> only the children of the specified  tree node will
     * be rendered not the treeNode itself. This method is recursive, so the
     * node's children and their children will be rendered and so on.
     *
     * @param buffer string buffer containing the markup
     * @param treeNode specified node who's children will be rendered
     * @param indentation current level of the treeNode. The indentation increases each
     * time the depth of the tree increments.
     *
     * @see #setRootNodeDisplayed(boolean)
     */
    protected void renderTree(HtmlStringBuffer buffer, TreeNode treeNode, int indentation) {
        indentation++;

        buffer.elementStart("ul");

        buffer.append(" class=\"");
        if (isRootNodeDisplayed() && indentation == 1) {
            buffer.append("rootLevel level");
        } else {
            buffer.append("level");
        }
        buffer.append(Integer.toString(indentation));

        //If javascript is enabled and this is not the first level of <ul> elements,
        //the css class 'hide' is appended to the <ul> element to ensure the tree
        //is in a collapsed state on the browser. However, we must query the
        //javascript handler if it does not perhaps veto the collapsed state.
        if (indentation > 1 && shouldHideNode(treeNode)) {
            buffer.append(" hide");
        }
        buffer.append("\">\n");

        Iterator it = treeNode.getChildren().iterator();
        while (it.hasNext()) {
            TreeNode child = (TreeNode) it.next();

            if (isJavascriptEnabled()) {
                javascriptHandler.getJavascriptRenderer().init(child);
            }
            renderTreeNodeStart(buffer, child, indentation);
            renderTreeNode(buffer, child, indentation);

            //check if the child node should be renderered
            if (shouldRenderChildren(child)) {
                renderTree(buffer, child, indentation);
            }
            renderTreeNodeEnd(buffer, child, indentation);
        }
        buffer.append("</ul>\n");
    }

    /**
     * Check the state of the specified node if its children
     * should be rendered or not.
     *
     * @param treeNode specified node to check
     * @return true if the child nodes should be rendered,
     * false otherwise
     */
    protected boolean shouldRenderChildren(TreeNode treeNode) {
        if (treeNode.isLeaf()) {
            return false;
        }
        if (treeNode.isExpanded()) {
            return true;
        } else {

            //If javascript is enabled, the entire tree has to be rendered
            //and sent to the browser. So even if the node is not
            //expanded, we still render the node's children.
            if (isJavascriptEnabled()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Interception point to render html before the tree node is rendered.
     *
     * @param buffer string buffer containing the markup
     * @param treeNode specified node to render
     * @param indentation current level of the treeNode
     */
    protected void renderTreeNodeStart(HtmlStringBuffer buffer, TreeNode treeNode,
            int indentation) {
        buffer.append("<li><span class=\"");
        if (treeNode.isRoot()) {
            buffer.append("rootNode ");
        }
        buffer.append(getExpandClass(treeNode));
        buffer.append("\"");

        if (isJavascriptEnabled()) {
            //hook to insert javascript specific code
            javascriptHandler.getJavascriptRenderer().renderTreeNodeStart(buffer);
        }
        buffer.appendAttribute("style", "display:block;");
        buffer.closeTag();

        //Render the node's expand/collapse functionality.
        //This includes adding a css class for the current expand/collapse state.
        //In the tree.css file, the css classes are mapped to icons by default.
        if (treeNode.hasChildren()) {
            renderExpandAndCollapseAction(buffer, treeNode);
        } else {
            buffer.append("<span class=\"spacer\"></span>");
        }
    }

    /**
     * Interception point to render html after the tree node was rendered.
     *
     * @param buffer string buffer containing the markup
     * @param treeNode specified node to render
     * @param indentation current level of the treeNode
     */
    protected void renderTreeNodeEnd(HtmlStringBuffer buffer, TreeNode treeNode,
            int indentation) {
        buffer.append("</span></li>\n");
    }

    /**
     * Render the expand and collapse action of the tree.
     * <p/>
     * Default implementation creates a hyperlink that users can click on
     * to expand or collapse the nodes.
     *
     * @param buffer string buffer containing the markup
     * @param treeNode treeNode to render
     */
    protected void renderExpandAndCollapseAction(HtmlStringBuffer buffer, TreeNode treeNode) {
        getExpandLink().setParameter(EXPAND_TREE_NODE_PARAM, treeNode.getId());
        if (treeNode.isRoot()) {
            getExpandLink().setAttribute("class", "root spacer");
        } else {
            getExpandLink().setAttribute("class", "spacer");
        }
        if (isJavascriptEnabled()) {
            //hook to insert javascript specific code
            javascriptHandler.getJavascriptRenderer().renderExpandAndCollapseAction(buffer);
        }

        getExpandLink().render(buffer);
    }

    /**
     * Render the specified treeNode.
     * <p/>
     * If a decorator was specified using {@link #setDecorator(Decorator) },
     * this method will render using the decorator instead.
     *
     * @param buffer string buffer containing the markup
     * @param treeNode treeNode to render
     * @param indentation current level of the treeNode
     */
    protected void renderTreeNode(HtmlStringBuffer buffer, TreeNode treeNode, int indentation) {
        if (getDecorator() != null) {
            Object value = getDecorator().render(treeNode, getContext());
            if (value != null) {
                buffer.append(value);
            }
            return;
        }

        renderIcon(buffer, treeNode);

        buffer.elementStart("span");
        if (treeNode.isSelected()) {
            buffer.appendAttribute("class", "selected");
        } else {
            buffer.appendAttribute("class", "unselected");
        }
        buffer.closeTag();

        //renders the node value
        renderValue(buffer, treeNode);

        buffer.elementEnd("span");
    }

    /**
     * Render the node's icon depending on the current state of the node.
     *
     * @param buffer string buffer containing the markup
     * @param treeNode treeNode to render
     */
    protected void renderIcon(HtmlStringBuffer buffer, TreeNode treeNode) {
        if (treeNode.getIcon() == null) {

            //render the icon to display
            buffer.elementStart("span");
            buffer.appendAttribute("class", getIconClass(treeNode));

            if (isJavascriptEnabled()) {
                //An id is needed on the element to do quick lookup using javascript
                //document.getElementById(id)
                javascriptHandler.getJavascriptRenderer().renderIcon(buffer);
            }
            buffer.append(">");

            buffer.append("</span>");

        } else {
            buffer.elementStart("img");
            buffer.appendAttribute("class", "customIcon");
            buffer.appendAttribute("src", treeNode.getIcon());
            buffer.closeTag();
        }
    }

    /**
     * Render the node's value.
     * <p/>
     * Subclasses should override this method to change the rendering of the
     * node's value. By default the value will be rendered as a hyperlink,
     * passing its <em>id</em> to the server.
     *
     * @param buffer string buffer containing the markup
     * @param treeNode treeNode to render
     */
    protected void renderValue(HtmlStringBuffer buffer, TreeNode treeNode) {
        getSelectLink().setParameter(SELECT_TREE_NODE_PARAM, treeNode.getId());
        if (treeNode.getValue() != null) {
            getSelectLink().setLabel(treeNode.getValue().toString());
        }
        getSelectLink().render(buffer);
    }

    /**
     * Query the specified treeNode and check which css class to apply.
     * <p/>
     * Possible classes are expanded, collapsed, leaf, expandedLastNode,
     * collapsedLastNode and leafLastNode.
     *
     * @param treeNode the tree node to check for css class
     * @return string specific css class to apply
     */
    protected String getExpandClass(TreeNode treeNode) {
        StringBuffer buffer = new StringBuffer();
        if (isExpandedParent(treeNode)) {
            buffer.append("expanded");
        } else if (treeNode.getChildren().size() > 0) {
            buffer.append("collapsed");
        } else {
            buffer.append("leaf");
        }

        if (treeNode.isLastChild()) {
            buffer.append("LastNode");
        }
        return buffer.toString();
    }

    /**
     * Query the specified treeNode and check which css class to apply for
     * the icons.
     * <p/>
     * Possible classes are expandedIcon, collapsedIcon and leafIcon.
     *
     * @param treeNode the tree node to check for css class
     * @return string specific css class to apply
     */
    protected String getIconClass(TreeNode treeNode) {
        if (isExpandedParent(treeNode)) {
            return EXPAND_ICON;
        } else if (!treeNode.isExpanded() && treeNode.hasChildren() || treeNode.isChildrenSupported()) {
            return COLLAPSE_ICON;
        } else {
            return LEAF_ICON;
        }
    }

    /**
     * Helper method indicating if the specified node is both
     * expanded and has at least 1 child node.
     *
     * @param treeNode specified node to check
     * @return true if the specified node is both expanded and
     * contains at least 1 child node
     */
    protected boolean isExpandedParent(TreeNode treeNode) {
        return (treeNode.isExpanded() && treeNode.hasChildren());
    }

    // -------------------------------------------- Protected observer behavior

    /**
     * Notifies all listeners currently registered with the tree, about any
     * expand events.
     *
     * @param node specify the TreeNode that was expanded
     * @param previousState contains the previous expanded state
     */
    protected void fireNodeExpanded(TreeNode node, boolean previousState) {
        for (Iterator it = listeners.iterator(); it.hasNext();) {
            TreeListener l = (TreeListener) it.next();
            l.nodeExpanded(this, node, getContext(), previousState);
        }
    }

    /**
     * Notifies all listeners currently registered with the tree, about any
     * collapse events.
     *
     * @param node specific the TreeNode that was collapsed
     * @param previousState contains the previous expanded state
     */
    protected void fireNodeCollapsed(TreeNode node, boolean previousState) {
        for (Iterator it = listeners.iterator(); it.hasNext();) {
            TreeListener l = (TreeListener) it.next();
            l.nodeCollapsed(this, node, getContext(), previousState);
        }
    }

    /**
     * Notifies all listeners currently registered with the tree, about any
     * selection events.
     *
     * @param node specific the TreeNode that was selected
     * @param previousState contains the previous selected state
     */
    protected void fireNodeSelected(TreeNode node, boolean previousState) {
        for (Iterator it = listeners.iterator(); it.hasNext();) {
            TreeListener l = (TreeListener) it.next();
            l.nodeSelected(this, node, getContext(), previousState);
        }
    }

    /**
     * Notifies all listeners currently registered with the tree, about any
     * deselection events.
     *
     * @param node specific the TreeNode that was deselected
     * @param previousState contains the previous selected state
     */
    protected void fireNodeDeselected(TreeNode node, boolean previousState) {
        for (Iterator it = listeners.iterator(); it.hasNext();) {
            TreeListener l = (TreeListener) it.next();
            l.nodeDeselected(this, node, getContext(), previousState);
        }
    }

    // ----------------------------------------------------- Protected behavior

    /**
     * Sets the TreeNode expand state to the new value.
     *
     * @param node specifies the TreeNode which expand state will be set
     * @param newValue specifies the new expand state
     */
    protected void setExpandState(TreeNode node, boolean newValue) {
        boolean oldValue = node.isExpanded();
        node.setExpanded(newValue);
        if (isNotifyListeners()) {
            if (newValue) {
                fireNodeExpanded(node, oldValue);
            } else {
                fireNodeCollapsed(node, oldValue);
            }
        }
    }

    /**
     * Swaps the expand state of all TreeNodes with specified id's.
     * Thus if a node's expand state is currently 'true', calling
     * expandOrCollapse will set the expand state to 'false' and vice versa.
     *
     * @param ids array of node id's
     */
    protected void expandOrCollapse(String[] ids) {
        processNodes(ids, new Callback() {
            public void callback(TreeNode node) {
                setExpandState(node, !node.isExpanded());
            }
        });
    }

    /**
     * Sets the expand state of the TreeNode with specified id to the new value.
     *
     * @param id specifies the id of a TreeNode which expand state will be set
     * @param newValue specifies the new expand state
     */
    protected void setExpandState(final String id, final boolean newValue) {
        TreeNode node = find(id);
        if (node == null) {
            return;
        }
        setExpandState(node, newValue);
    }

    /**
     * Sets the TreeNode expand state of each node in the specified collection
     * to the new value.
     *
     * @param nodes specifies the collection of a TreeNodes which expand states will be set
     * @param newValue specifies the new expand state
     */
    protected void setExpandState(final Collection nodes, final boolean newValue) {
        processNodes(nodes, new Callback() {
            public void callback(TreeNode node) {
                setExpandState(node, newValue);
            }
        });
    }

    /**
     * Sets the TreeNode select state to the new value.
     *
     * @param node specifies the TreeNode which select state will be set
     * @param newValue specifies the new select state
     */
    protected void setSelectState(TreeNode node, boolean newValue) {
        boolean oldValue = node.isSelected();
        node.setSelected(newValue);
        if (isNotifyListeners()) {
            if (newValue) {
                fireNodeSelected(node, oldValue);
            } else {
                fireNodeDeselected(node, oldValue);
            }
        }
    }

    /**
     * Swaps the select state of all TreeNodes with specified id's to the new value.
     * Thus if a node's select state is currently 'true', calling selectOrDeselect
     * will set the select state to 'false' and vice versa.
     *
     * @param ids array of node id's
     */
    protected void selectOrDeselect(String[] ids) {
        processNodes(ids, new Callback() {
            public void callback(TreeNode node) {
                setSelectState(node, !node.isSelected());
            }
        });
    }

    /**
     * Sets the select state of the TreeNode with specified id to the new value.
     *
     * @param id specifies the id of a TreeNode which select state will be set
     * @param newValue specifies the new select state
     */
    protected void setSelectState(final String id, final boolean newValue) {
        TreeNode node = find(id);
        if (node == null) {
            return;
        }
        setSelectState(node, newValue);
    }

    /**
     * Sets the TreeNode select state of each node in the specified collection
     * to the new value.
     *
     * @param nodes specifies the collection of a TreeNodes which select states will be set
     * @param newValue specifies the new select state
     */
    protected void setSelectState(final Collection nodes, final boolean newValue) {
        processNodes(nodes, new Callback() {
            public void callback(TreeNode node) {
                setSelectState(node, newValue);
            }
        });
    }

    /**
     * Provides callback functionality for all the specified nodes.
     *
     * @param ids the array of nodes to process
     * @param callback object on which callbacks are made
     */
    protected void processNodes(String[] ids, Callback callback) {
        if (ids == null) {
            return;
        }
        for (int i = 0, n = ids.length; i < n; i++) {
            String id = ids[i];
            if (id == null || id.length() == 0) {
                continue;
            }
            TreeNode node = find(id);
            if (node == null) {
                continue;
            }
            callback.callback(node);
        }
    }

    /**
     * Provides callback functionality for all the specified nodes.
     *
     * @param nodes the collection of nodes to process
     * @param callback object on which callbacks are made
     */
    protected void processNodes(Collection nodes, Callback callback) {
        if (nodes == null) {
            return;
        }
        for (Iterator it = nodes.iterator(); it.hasNext();) {
            TreeNode node = (TreeNode) it.next();
            callback.callback(node);
        }
    }

    /**
     * Finds and returns the first node that matches the id, starting the search
     * from the specified node.
     *
     * @param node specifies at which node the search must start from
     * @param id specifies the id of the TreeNode to find
     * @return TreeNode the first node matching the id or null if no match was found.
     */
    protected TreeNode find(TreeNode node, String id) {
        for (Iterator it = iterator(node); it.hasNext();) {
            TreeNode result = (TreeNode) it.next();
            if (result.getId().equals(id)) {
                return result;
            }
        }
        return null;
    }

    /**
     * Returns the value of the specified named parameter or a empty string
     * <span class="st">""</span> if not found.
     *
     * @param name specifies the parameter to return
     * @return the specified parameter or a empty string <span class="st">""</span> if not found
     */
    protected String getRequestValue(String name) {
        String result = getContext().getRequestParameter(name);

        if (result != null) {
            return result.trim();
        } else {
            return "";
        }
    }

    /**
     * Returns an array of all values of the specified named parameter or null
     * if the parameter does not exist.
     *
     * @param name specifies the parameter to return
     * @return all matching parameters or null if no parameter was found
     */
    protected String[] getRequestValues(String name) {
        String[] resultArray = getContext().getRequest().getParameterValues(name);
        return resultArray;
    }

    /**
     * Return an anchor &lt;a&gt; tag href attribute for the given parameters.
     * This method will encode the URL with the session ID
     * if required using <tt>HttpServletResponse.encodeURL()</tt>.
     *
     * @param parameters the href parameters
     * @return the HTML href attribute
     */
    protected String getHref(Map parameters) {
        Context context = getContext();
        String uri = ClickUtils.getRequestURI(context.getRequest());

        HtmlStringBuffer buffer =
                new HtmlStringBuffer(uri.length() + (parameters.size() * 20));

        buffer.append(uri);
        if (parameters != null && !parameters.isEmpty()) {
            buffer.append("?");
            Iterator i = parameters.entrySet().iterator();
            while (i.hasNext()) {
                Map.Entry entry = (Map.Entry) i.next();
                String name = entry.getKey().toString();
                String value = entry.getValue().toString();

                buffer.append(name);
                buffer.append("=");
                buffer.append(ClickUtils.encodeUrl(value, context));
                if (i.hasNext()) {
                    buffer.append("&amp;");
                }
            }
        }

        return context.getResponse().encodeURL(buffer.toString());
    }

    // ------------------------------------------------ Package Private Methods

    /**
     * Expand / collapse and select / deselect the tree nodes.
     *
     * @return true to continue Page event processing or false otherwise
     */
    boolean postProcess() {
        if (isJavascriptEnabled()) {
            // Populate the javascript handler with its state. This call will
            // notify any tree listeners about new values.
            javascriptHandler.init(getContext());
        }

        if (!ArrayUtils.isEmpty(expandOrCollapseNodeIds)) {
            expandOrCollapse(expandOrCollapseNodeIds);
        }

        if (!ArrayUtils.isEmpty(selectOrDeselectNodeIds)) {
            selectOrDeselect(selectOrDeselectNodeIds);
        }
        return true;
    }

    //----------------------------------------------------------- Inner classes

    /**
     * Iterate over all the nodes in the tree in a breadth first manner.
     *
     * <p/>Thus in a tree with the following nodes (top to bottom):
     * <pre class="codeHtml">
     *                           <span class="red">root</span>
     *       <span class="blue">node1</span>                            <span class="blue">node2</span>
     *node1.1  node1.2          node2.1  node2.2
     * </pre>
     *
     * <p/>the iterator will return the nodes in the following order:
     * <pre class="codeHtml">
     *      <span class="red">root</span>
     *      <span class="blue">node1</span>
     *      <span class="blue">node2</span>
     *      node1.1
     *      node1.2
     *      node2.1
     *      node2.2
     * </pre>
     */
    static class BreadthTreeIterator implements Iterator {

        /**queue for storing node's. */
        private List queue = new ArrayList();

        /** indicator to iterate collapsed node's. */
        private boolean iterateCollapsedNodes = true;

        /**
         * Creates a iterator and adds the specified node to the queue.
         * The specified node will be set as the root of the traversal.
         *
         * @param node node will be set as the root of the traversal.
         */
        public BreadthTreeIterator(TreeNode node) {
            if (node == null) {
                throw new IllegalArgumentException("Node cannot be null");
            }
            queue.add(node);
        }

        /**
         * Creates a iterator and adds the specified node to the queue.
         * The specified node will be set as the root of the traversal.
         *
         * @param node node will be set as the root of the traversal.
         * @param iterateCollapsedNodes indicator to iterate collapsed node's
         */
        public BreadthTreeIterator(TreeNode node, boolean iterateCollapsedNodes) {
            if (node == null) {
                throw new IllegalArgumentException("Node cannot be null");
            }
            queue.add(node);
            this.iterateCollapsedNodes = iterateCollapsedNodes;
        }

        /**
         * Returns true if there are more nodes, false otherwise.
         *
         * @return boolean true if there are more nodes, false otherwise.
         */
        public boolean hasNext() {
            return !queue.isEmpty();
        }

        /**
         * Returns the next node in the iteration.
         *
         * @return the next node in the iteration.
         * @exception NoSuchElementException iteration has no more node.
         */
        public Object next() {
            try {
                //remove from the end of queue
                TreeNode node = (TreeNode) queue.remove(queue.size() - 1);
                if (node.hasChildren()) {
                    if (iterateCollapsedNodes || node.isExpanded()) {
                        push(node.getChildren());
                    }
                }
                return node;
            } catch (IndexOutOfBoundsException e) {
                throw new NoSuchElementException("There is  no more node's to iterate");
            }
        }

        /**
         * Remove operation is not supported.
         *
         * @exception UnsupportedOperationException <tt>remove</tt> operation is
         * not supported by this Iterator.
         */
        public void remove() {
            throw new UnsupportedOperationException("remove operation is not supported.");
        }

        /**
         * Pushes the specified list of node's to push on the beginning of the queue.
         *
         * @param children list of node's to push on the beginning of the queue
         */
        private void push(List children) {
            for (Iterator it = children.iterator(); it.hasNext();) {
                queue.add(0, it.next()); //add to the beginning of queue
            }
        }
    }

    // ------------------------------------------------------- Private behavior

    /**
     * Returns whether the specified node is visible. The semantics of visible
     * in this context indicates whether the node is currently displayed on the
     * screen. This means all parent nodes must be expanded for the node to be
     * visible.
     *
     * @param node TreeNode's visibility to check
     * @return boolean true if the node's parent is visible, false otherwise
     */
    private boolean isVisible(TreeNode node) {
        while (!node.isRoot()) {
            if (!node.getParent().isExpanded()) {
                return false;
            }
            node = node.getParent();
        }
        return true;
    }

    /**
     * Returns an array of all the nodes in the hierarchy, starting from the specified
     * node up to and including the root node.
     * <p/>
     * The specified node will be at the start of the array and the root node will be
     * at the end of the array. Thus array[0] will return the specified node, while
     * array[n - 1] where n is the size of the array, will return the root node.
     *
     * @return list of all nodes from the specified node to the root node
     */
    private TreeNode[] getPathToRoot(TreeNode treeNode) {
        TreeNode[] nodes = new TreeNode[] {treeNode};
        while (treeNode.getParent() != null) {
            int length = nodes.length;
            System.arraycopy(nodes, 0, nodes = new TreeNode[length + 1], 0, length);
            nodes[length] = treeNode = treeNode.getParent();
        }
        return nodes;
    }

    // ------------------------------------------- Javascript specific behavior

    /**
     * Creates a new JavascriptHandler based on the specified policy.
     *
     * @param javascriptPolicy the current javascript policy
     * @return newly created JavascriptHandler
     */
    protected JavascriptHandler createJavascriptHandler(int javascriptPolicy) {
        if (javascriptPolicy == JAVASCRIPT_SESSION_POLICY) {
            return new SessionHandler(getContext());
        } else {
            return new CookieHandler(getContext());
        }
    }

    /**
     * Keep track of node id's, as they are selected, deselected,
     * expanded and collapsed.
     *
     * @see JavascriptHandler
     */
    protected transient JavascriptHandler javascriptHandler;

    /**
     * <b>Please note</b> this class is <b>not</b> meant for public use.
     * <p/>
     * Provides the contract for pluggable javascript renderers for
     * the Tree.
     */
    protected interface JavascriptRenderer {

        /**
         * Called to initialize the renderer.
         *
         * @param node the current node rendered
         */
        void init(TreeNode node);

        /**
         * Called before a tree node is rendered. Enables the renderer
         * to add attributes needed by javascript functionality for example
         * something like:
         * <pre class="codeJava">
         *      buffer.appendAttribute(<span class="st">"id"</span>,expandId);
         * </pre>
         * The code above adds a id attribute to the element, to enable
         * the javascript code to lookup the html element by its id.
         * <p/>
         * The above attribute is appended to whichever element the
         * tree is currently rendering at the time renderTreeNodeStart
         * is called.
         *
         * @param buffer string buffer containing the markup
         */
        void renderTreeNodeStart(HtmlStringBuffer buffer);

        /**
         * Called when the expand and collapse action is rendered. Enables
         * the renderer to add attributes needed by javascript functionality
         * for example something like:
         * <pre class="codeJava">
         *      buffer.append(<span class="st">"onclick=\"handleNodeExpansion(this,event)\""</span>);
         * </pre>
         * The code above adds a javascript function call to the element.
         * <p/>
         * The code above is appended to whichever element the
         * tree is currently rendering at the time renderTreeNodeStart
         * is called.
         *
         * @param buffer string buffer containing the markup
         */
        void renderExpandAndCollapseAction(HtmlStringBuffer buffer);

        /**
         * Called when the tree icon is rendered. Enables the renderer
         * to add attributes needed by javascript functionality for example
         * something like:
         * <pre class="codeJava">
         *      buffer.appendAttribute(<span class="st">"id"</span>,iconId);
         * </pre>
         * The code above adds a id attribute to the element, to enable
         * the javascript code to lookup the html element by its id.
         * <p/>
         * The above attribute is appended to whichever element the
         * tree is currently rendering at the time renderTreeNodeStart
         * is called.
         *
         * @param buffer string buffer containing the markup
         */
        void renderIcon(HtmlStringBuffer buffer);
    }

    /**
     * <b>Please note</b> this class is <b>not</b> meant for public use.
     * <p/>
     * Provides a abstract implementation of JavascriptRenderer that
     * subclasses can extend from.
     */
    protected abstract class AbstractJavascriptRenderer implements JavascriptRenderer {

        /** holds the id of the expand html element. */
        protected String expandId;

        /** holds the id of the icon html element. */
        protected String iconId;

        /** holds the javascript call to expand or collapse the node. */
        protected String nodeExpansionString;

        /**
         * @see #init(TreeNode)
         *
         * @param treeNode the current node rendered
         * @see #init(TreeNode)
         */
        public void init(TreeNode treeNode) {
            expandId = buildString("e_", treeNode.getId(), "");
            iconId = buildString("i_", treeNode.getId(), "");
        }

        /**
         * @see #renderTreeNodeStart(HtmlStringBuffer)
         *
         * @param buffer string buffer containing the markup
         */
        public void renderTreeNodeStart(HtmlStringBuffer buffer) {
            //An id is needed on the element to do quick lookup using javascript
            //document.getElementById(id)
            buffer.appendAttribute("id", expandId);
        }

        /**
         * @see #renderExpandAndCollapseAction(HtmlStringBuffer)
         *
         * @param buffer string buffer containing the markup
         */
        public void renderExpandAndCollapseAction(HtmlStringBuffer buffer) {
            getExpandLink().setAttribute("onclick", nodeExpansionString);
        }

        /**
         * @see #renderIcon(HtmlStringBuffer)
         *
         * @param buffer string buffer containing the markup
         */
        public void renderIcon(HtmlStringBuffer buffer) {
            //An id is needed on the element to do quick lookup using javascript
            //document.getElementById(id)
            buffer.appendAttribute("id", iconId);
        }

        /**
         * Builds a new string consisting of a prefix, infix and postfix.
         *
         * @param prefix the string to append at the start of new string
         * @param infix the string to append in the middle of the new string
         * @param postfix the string to append at the end of the new string
         * @return the newly create string
         */
        protected String buildString(String prefix, String infix, String postfix) {
            StringBuffer buffer = new StringBuffer();
            buffer.append(prefix).append(infix).append(postfix);
            return buffer.toString();
        }
    }

    /**
     * <strong>Please note</strong> this class is only meant for
     * developers of this control, not users.
     * <p/>
     * Provides the rendering needed when a {@link #JAVASCRIPT_COOKIE_POLICY}
     * is in effect.
     */
    protected class CookieRenderer extends AbstractJavascriptRenderer {

        /** Name of the cookie holding the expanded nodes id's. */
        private String expandedCookieName;

        /** Name of the cookie holding the collapsed nodes id's. */
        private String collapsedCookieName;

        /**
         * Default constructor.
         *
         * @param expandedCookieName name of the cookie holding expanded id's
         * @param collapsedCookieName name of the cookie holding collapsed id's
         */
        public CookieRenderer(String expandedCookieName, String collapsedCookieName) {
            this.collapsedCookieName = collapsedCookieName;
            this.expandedCookieName = expandedCookieName;
        }

        /**
         *@see #init(TreeNode)
         *
         * @param treeNode the current node rendered
         * @see #init(TreeNode)
         */
        public void init(TreeNode treeNode) {
            super.init(treeNode);
            StringBuffer tmp = new StringBuffer();
            tmp.append("handleNodeExpansion(this,event,'").append(expandId).append("','");
            tmp.append(iconId).append("'); handleCookie(this,event,'").append(expandId).append("','");
            tmp.append(treeNode.getId()).append("','");
            tmp.append(expandedCookieName).append("','");
            tmp.append(collapsedCookieName).append("'); return false;");
            nodeExpansionString = tmp.toString();
        }
    }

    /**
     * <strong>Please note</strong> this class is only meant for
     * developers of this control, not users.
     * <p/>
     * Provides the rendering needed when a {@link #JAVASCRIPT_SESSION_POLICY}
     * is in effect.
     */
    protected class SessionRenderer extends AbstractJavascriptRenderer {

        /**
         * @see #init(TreeNode)
         *
         * @param treeNode the current node rendered
         * @see #init(TreeNode)
         */
        public void init(TreeNode treeNode) {
            super.init(treeNode);
            String tmp = buildString("handleNodeExpansion(this,event,'", expandId, "','");
            nodeExpansionString = buildString(tmp, iconId, "'); return false;");
        }
    }

    /**
     * <b>Please note</b> this class is <b>not</b> meant for public use.
     * <p/>
     * Provides the contract for pluggable javascript handlers.
     * <p/>
     * One of the main tasks the handler must perform is keeping track
     * of which nodes changed state after the user interacted with the
     * tree in the browser. This is also the reason why the handler
     * extends {@link TreeListener} to be informed  of any changes
     * to node state via other means.
     */
    protected interface JavascriptHandler extends TreeListener {

        /**
         * Initialize the handler state.
         *
         * @param context provides information for initializing
         * the handler.
         */
        void init(Context context);

        /**
         * Queries the handler if the specified node should be rendered
         * as a expanded node.
         * <p/>
         * The reason for this is that the handler might be keeping track
         * of state that the node is not aware of. For example certain state
         * could be stored in the session or cookies.
         *
         * @param treeNode the specified node to query for
         * @return true if the node should be rendered as if it was
         * expanded, false otherwise
         */
        boolean renderAsExpanded(TreeNode treeNode);

        /**
         * Called to indicate the user request cycle is complete.
         * Any last minute tasks can be performed here.
         */
        void destroy();

        /**
         * Returns the javascript renderer associated with
         * this handler.
         *
         * @return renderer associated with this handler
         */
        JavascriptRenderer getJavascriptRenderer();
    }

    /**
     * <strong>Please note</strong> this class is only meant for
     * developers of this control, not users.
     * <p/>
     * This class implements a cookie based javascript handler.
     * Cookies in the browser tracks the expand and collapse state
     * of the nodes. When a request is made to the server the cookies
     * is processed and the state of the nodes are modified accordingly.
     * <p/>
     * There are two cookies used to track the state:
     * <ul>
     *     <li>a cookie tracking the expanded node id's
     *     <li>a cookie tracking the collapsed node id's
     * </ul>
     * The cookies are removed between requests. New requests
     * issue new cookies and update the state of the nodes
     * accordingly.
     * <p/>
     * Note: This class is used in conjuction with cookie-helper.js
     * which manipulates the cookie values in the browser as the
     * user navigates the tree.
     */
    protected class CookieHandler implements JavascriptHandler {

        /** Cookie value delimiter. */
        private final static String DELIM = ",";

        /** Name of cookie responsible for tracking the expanded node id's. */
        protected final String expandedCookieName = "expanded_" + getName();

        /** Name of cookie responsible for tracking the expanded node id's. */
        protected final String collapsedCookieName = "collapsed_" + getName();

        /** Variable holding a javascript renderer. */
        protected JavascriptRenderer javascriptRenderer;

        /** Tracker for the expanded node id's. */
        private Set expandTracker;

        /** Tracker for the collapsed node id's. */
        private Set collapsedTracker;

        /** Value of the cookie responsible for tracking the expanded node id's. */
        private String expandedNodeCookieValue = null;

        /** Value of the cookie responsible for tracking the collapsed node id's. */
        private String collapsedNodeCookieValue = null;

        /**
         * Creates and initializes a new CookieHandler.
         *
         * @param context provides access to the http request, and session
         */
        protected CookieHandler(Context context) {
            expandedNodeCookieValue = context.getCookieValue(expandedCookieName);
            collapsedNodeCookieValue = context.getCookieValue(collapsedCookieName);
            expandedNodeCookieValue = prepareCookieValue(expandedNodeCookieValue);
            collapsedNodeCookieValue = prepareCookieValue(collapsedNodeCookieValue);
        }

        /**
         * Initialize the handler state from the current cookies.
         *
         * @param context provides access to the http request, and session
         */
        public void init(Context context) {
            //If already initialized
            if (expandTracker != null || collapsedTracker != null) {
                return;
            }
            expandTracker = new HashSet();
            collapsedTracker = new HashSet();

            if (context == null) {
                throw new IllegalArgumentException("context cannot be null");
            }

            //No cookie values to digest
            if (expandedNodeCookieValue == null
                    && collapsedNodeCookieValue == null) {
                return;
            }

            //build hashes of id's for fast lookup
            Set expandHash = asSet(expandedNodeCookieValue, DELIM);
            Set collapsedHash = asSet(collapsedNodeCookieValue, DELIM);

            for (Iterator it = iterator(getRootNode()); it.hasNext();) {
                TreeNode currentNode = (TreeNode) it.next();

                //If currentNode was expanded by user in browser
                if (expandHash.contains(currentNode.getId())) {

                    //If currentNode's state is collapsed
                    if (!currentNode.isExpanded()) {
                        //Calling expand(currentNode) will update the expandTracker
                        //because the CookieHandler is a TreeListener as well.
                        expand(currentNode);
                    } else {
                        //If the currentNode is already expanded we should not update the
                        //expandTracker via a call to expand(currentNode), because
                        //other listeners of the tree will receive the event as well.
                        //Instead we update the expandTracker directly.
                        expandTracker.add(currentNode.getId());
                    }
                } else if (collapsedHash.contains(currentNode.getId())) {
                    //If currentNode was collapsed by user in browser

                    if (currentNode.isExpanded()) {
                        //Calling collapse(currentNode) will update the expandTracker
                        //because the CookieHandler is a TreeListener as well.
                        collapse(currentNode);
                    }
                }
            }
        }

        /**
         * Currently this implementation just calls
         * {@link #isExpandedParent(TreeNode)}.
         * <p/>
         * CookieHandler uses cookies to sync any state change on
         * the browser with the server, so the handler will not
         * contain any state outside of the treeNode.
         *
         * @param treeNode the specified treeNode to check if it is part of the
         * users selected paths
         * @return true if the specified treeNode is part of the users selected
         * path, false otherwise
         * @see #renderAsExpanded(TreeNode)
         */
        public boolean renderAsExpanded(TreeNode treeNode) {
            return isExpandedParent(treeNode);
        }

        /**
         * @see #destroy()
         */
        public void destroy() {
            //Remove expanded cookies. If the tree is changed
            //new cookies will be generated.
            setCookie(null, expandedCookieName);

            //Remove  collapsed cookie.
            setCookie(null, collapsedCookieName);
        }

        /**
         * @see #getJavascriptRenderer()
         *
         * @return currently installed javascript renderer
         */
        public JavascriptRenderer getJavascriptRenderer() {
            if (javascriptRenderer == null) {
                javascriptRenderer = new CookieRenderer(expandedCookieName, collapsedCookieName);
            }
            return javascriptRenderer;
        }

        /**
         * Adds the specified node to the cookie handler tracker.
         *
         * @see TreeListener#nodeExpanded(Tree, TreeNode, Context, boolean)
         *
         * @param tree tree the operation was made on
         * @param node node that was expanded
         * @param context provides access to {@link org.apache.click.Context}
         * @param oldValue contains the previous value of expanded state
         */
        public void nodeExpanded(Tree tree, TreeNode node, Context context, boolean oldValue) {
            expandTracker.add(node.getId());
        }

        /**
         * Removes the specified node from the cookie handler tracker.
         *
         * @see TreeListener#nodeCollapsed(Tree, TreeNode, Context, boolean)
         *
         * @param tree tree the operation was made on
         * @param node node that was collapsed
         * @param context provides access to {@link org.apache.click.Context}
         * @param oldValue contains the previous value of selected state
         */
        public void nodeCollapsed(Tree tree, TreeNode node, Context context, boolean oldValue) {
            expandTracker.remove(node.getId());
        }

        /**
         * @see TreeListener#nodeSelected(Tree, TreeNode, Context, boolean)
         *
         * @param tree tree the operation was made on
         * @param node node that was selected
         * @param context provides access to {@link org.apache.click.Context}
         * @param oldValue contains the previous value of selected state
         */
        public void nodeSelected(Tree tree, TreeNode node, Context context, boolean oldValue) {
            /* noop */
        }

        /**
         * @see TreeListener#nodeDeselected(Tree, TreeNode, Context, boolean)
         *
         * @param tree tree the operation was made on
         * @param node node that was selected
         * @param context provides access to {@link org.apache.click.Context}
         * @param oldValue contains the previous value of selected state
         */
        public void nodeDeselected(Tree tree, TreeNode node, Context context, boolean oldValue) {
            /* noop */
        }

        /**
         * Sets a cookie with the specified name and value to the
         * http response.
         *
         * @param value the cookie's value
         * @param name the cookie's name
         */
        protected void setCookie(String value, String name) {
            Context context = getContext();
            if (value == null) {
                ClickUtils.setCookie(context.getRequest(), context.getResponse(),
                        name, value, 0, "/");
            } else {
                ClickUtils.setCookie(context.getRequest(), context.getResponse(),
                        name, value, -1, "/");
            }
        }

        /**
         * Does some preparation on the cookie value like
         * decoding and stripping of unneeded characters.
         *
         * @param value the cookie's value to prepare
         * @return the prepared value
         */
        protected String prepareCookieValue(String value) {
            try {
                if (StringUtils.isNotBlank(value)) {
                    value = URLDecoder.decode(value, "UTF-8");
                    value = trimStr(value, "\"");
                }
            } catch (UnsupportedEncodingException ignore) {
                //ignore
            }
            return value;
        }

        /**
         * Returns the specified string value as a set, tokenizing the
         * string based on the specified delimiter.
         *
         * @param value value to return as set
         * @param delim delimiter used to tokenize the value
         * @return set of tokens
         */
        protected Set asSet(String value, String delim) {
            Set set = new HashSet();
            if (value  == null) {
                return set;
            }

            StringTokenizer tokenizer = new StringTokenizer(value, delim);
            while (tokenizer.hasMoreTokens()) {
                String id = tokenizer.nextToken();
                set.add(id);
            }
            return set;
        }
    }

    /**
     * <strong>Please note</strong> this class is only meant for developers of
     * this control, not users.
     * <p/>
     * This class implements a session based javascript handler. It manages the
     * client side javascript behavior by tracking the client's selected tree paths.
     * <p/>
     * <strong>The problem</strong>: when javascript is enabled, the entire
     * tree must be sent to the browser to be navigatable without round trips
     * to the server. However the tree should not be displayed in an expanded
     * state so css is used to apply the 'display: none' idiom to 'collapse' the
     * nodes even though they are really expanded.
     * <p/>
     * On the browser as the user expands and collapses nodes he/she will
     * make selections and deselections of certain nodes. Since the node's value
     * is rendered as a hyperlink, selecting or deselecting the node will
     * create a request to the server.
     * After the round trip to the server the tree will again be rendered in a
     * collapsed state because the server will apply the css 'display :none' idiom
     * before returning to the browser. It would be nice if instead of collapsing
     * the entire tree again, to keep those tree paths that lead to selected nodes
     * in a expanded state.
     * <p/>
     * <strong>The solution</strong>: SessionHandler keeps track of the
     * <em>selected paths</em> and is queried at rendering time which nodes
     * should be hidden and which nodes should be displayed. The
     * <em>selected path</em> are all the node's from the selected node up to the
     * root node.
     * <p/>
     * SessionHandler also keeps track of the <em>overlaid paths</em>.
     * <em>Overlaid paths</em> comes from two or more selected paths that share
     * certain common nodes. Overlaid paths are used in determining when a
     * selected path can be removed from the tracker.
     * To understand this better here is an example tree (top to bottom):
     * <p/>
     * <pre class="codeHtml">
     *                           <span class="red">root</span>
     *       <span class="blue">node1</span>                            <span class="blue">node2</span>
     * node1.1  node1.2          node2.1  node2.2
     * </pre>
     * IF node1 is selected, the <em>selected path</em> would include the nodes
     * "root and node1". If node1.1 is then selected, the <em>selected path</em> would
     * also include the nodes "root, node1 and node1.1". The same ff node1.2 is selected.
     * The <em>overlaid path</em> would include the shared nodes of the three selected
     * paths. Thus the overlaid path would consist of "root" because that node is shared by
     * all three paths. Overlaid path will also contain "node1" because it is shared by node1.1
     * and node1.2.
     * <p/>
     * <strong>The implementation</strong>: To keep memory storage to a minimum,
     * only the hashcode of a node is stored, and it is stored only once for any node on
     * any selected path. Thus if n nodes in a tree are selected there will only be n
     * hashcodes stored.
     * <p/>
     * The overlaid paths are stored in a object consisting of a counter which increments
     * and decrements each time a path is selected or deselected. The overlaid path also
     * stores a boolean indicating if a a node is the last node on the path or not. The
     * last node in the selected path should not be expanded because we do not want
     * the children of  the last node to be displayed. Lets look at our previous example
     * again. The overlaid paths would contain the following information:
     * <ul>
     *    <li>root: int = 3, lastNodeInPath = false</li>
     *    <li>node1: int = 3, lastNodeInPath = false</li>
     *    <li>node1.1: int = 1, lastNodeInPath = true</li>
     *    <li>node1.2: int = 1, lastNodeInPath = true</li>
     * </ul>
     * The overlaid paths indicates that the root node was found on three selected paths,
     * and it is not the last node in the path. It cannot be the last node in the path
     * because its child, node1 is also found on the selected path. node1 is also found
     * on three selected paths (remember that node1 was itself selected) and is also
     * not the last node in the path because both its children are found on selected paths
     * as well. node1.1 is only found once on a selected path and because it does not
     * have any children, it is the last node in the path. node1.2 is the same as node1.1.
     * <p/>
     * When a user deselects a node, each node on the selected path are decremented
     * from the overlaid path counter. If a overlaid counter is reduced to 0, the
     * selected path is also removed from storage. Also as nodes are removed, each node's
     * lastNodeInPath indicator is updated to reflect if that node is the new last node
     * on the path. For example if a user deselects node1.1 the overlaid path will look as
     * follows:
     * <ul>
     *    <li>root: int = 1, lastNodeInPath = false</li>
     *    <li>node1: int = 1, lastNodeInPath = false</li>
     *    <li>node1.1: int = 1, lastNodeInPath = true</li>
     * </ul>
     * Only 1 instance of root and node1 are left on the paths, but both are still not the
     * last node in the path. Because node1.2 counter was reduced to 0, it was removed
     * from the <em>selected path</em> storage as well.
     * <p/>
     * If the user deselects node1.1 overlaid path will look like this:
     * <ul>
     *    <li>root: int = 1, lastNodeInPath = false</li>
     *    <li>node1: int = 1, lastNodeInPath = true</li>
     * </ul>
     * node1 is now the lastNodeInPath. node1.1 is also removed from the
     * <em>selected path</em> storage.
     * <p/>
     * <strong>Note:</strong> this class stores information between requests
     * in the javax.servlet.http.HttpSession as a attribute. The attributes prefix
     * is <tt>js_path_handler_</tt> followed by the name of the tree
     * {@link Tree#name}. If two tree's in the same session have the same name they
     * will <strong>overwrite</strong> each others session attribute!
     */
    protected class SessionHandler implements JavascriptHandler {

        /**
         * The reserved session key prefix for the selected paths
         * <tt>js_path_handler_</tt>.
         */
        private static final String JS_HANDLER_SESSION_KEY = "js_path_handler_";

        /** Renders the needed javascript for this handler. */
        protected JavascriptRenderer javascriptRenderer;

        /**
         * Map of id's of all nodes that are on route to a selected node in the tree.
         * The value of the map is a Integer indicating the number of times the id
         * have been added to the map. This helps keep track of the number of
         * parallel paths.
         */
        private Map selectTracker = null;

        /**
         * This class is dependant on {@link org.apache.click.Context}, so this
         * constructor enforces a valid context before the handler can be
         * used.
         *
         * @param context provides access to the http request, and session
         */
        protected SessionHandler(Context context) {
            if (context == null) {
                throw new IllegalArgumentException("Context cannot be null");
            }
            init(context);
        }

        /**
         * Retrieves the tracker from the http session if it exists. Otherwise
         * it creates a new tracker and stores it in the http session.
         *
         * @param context provides access to the http request, and session
         */
        public void init(Context context) {
            //If already initialized
            if (selectTracker != null) {
                return;
            }
            if (context == null) {
                throw new IllegalArgumentException("context cannot be null");
            }
            StringBuffer buffer = new StringBuffer(JS_HANDLER_SESSION_KEY).
                    append(getName());
            String key = buffer.toString();
            selectTracker = (Map) context.getSessionAttribute(key);
            if (selectTracker == null) {
                selectTracker = new HashMap();
                context.setSessionAttribute(key, selectTracker);
            }
        }

        /**
         * Queries the handler if the specified node should be rendered
         * as expanded or not.
         *
         * @param treeNode the specified node to check if it is expanded
         * or not
         * @return true if the specified node should be expanded, false
         * otherwise
         * @see #renderAsExpanded(TreeNode)
         */
        public boolean renderAsExpanded(TreeNode treeNode) {
            Entry entry = (Entry) selectTracker.get(treeNode.getId());
            if (entry == null || entry.lastNodeInPath) {
                return false;
            }
            return true;
        }

        /**
         * @see #destroy()
         */
        public void destroy() { /*noop*/ }

        /**
         * @see #getJavascriptRenderer()
         *
         * @return currently installed javascript renderer
         */
        public JavascriptRenderer getJavascriptRenderer() {
            if (javascriptRenderer == null) {
                javascriptRenderer = new SessionRenderer();
            }
            return javascriptRenderer;
        }

        /**
         * Adds all node's that are part of the <tt>selected path</tt> to
         * the tracker.
         *
         * @see TreeListener#nodeSelected(Tree, TreeNode, Context, boolean)
         *
         * @param tree tree the operation was made on
         * @param node node that was selected
         * @param context provides access to {@link org.apache.click.Context}
         * @param oldValue contains the previous value of selected state
         */
        public void nodeSelected(Tree tree, TreeNode node, Context context,
                boolean oldValue) {
            //Check for duplicate path's. A duplicate path means that the user
            //selected a node that is already stored in the handler's tracker map.
            //This can only really happen when the tree is representated with
            //checkboxes. Each time the form is submitted all the "checked"
            //checkboxes are submitted but these node's might already have been
            //submitted in a previous request. So here is a check against the
            //previous value of the node to ensure it is newly selected.
            if (oldValue) {
                return;
            }

            TreeNode[] nodes = getPathToRoot(node);

            //Loop all nodes and check if they should be added or incremented
            for (int i = 0; i < nodes.length; i++) {
                TreeNode currentNode = nodes[i];
                if (i > 0 && !currentNode.isExpanded()) {

                    // If the node to expand is root and isRootNodeDisplayed is
                    // false, don't notify listeners
                    if (currentNode.isRoot() && !isRootNodeDisplayed()
                        && isNotifyListeners()) {
                        setNotifyListeners(false);
                        expand(currentNode);
                        setNotifyListeners(true);
                    } else {
                        expand(currentNode);
                    }
                }
                String id = currentNode.getId();
                Entry entry = (Entry) selectTracker.get(id);

                //If node is not yet tracked, add it to the selected path tracker,
                //otherwise increment the overlaid path count
                if (entry == null) {
                    Entry newEntry = new Entry();
                    newEntry.lastNodeInPath = i == 0;
                    selectTracker.put(id, newEntry);
                } else {
                    entry.lastNodeInPath = false;
                    entry.count++;
                }
            }
        }

        /**
         * Removes all node's that are part of the <tt>selected path</tt> from
         * the tracker.
         *
         * @see TreeListener#nodeDeselected(Tree, TreeNode, Context, boolean)
         *
         * @param tree tree the operation was made on
         * @param node node that was deselected
         * @param context provides access to {@link org.apache.click.Context}
         * @param oldValue contains the previous value of selected state
         */
        public void nodeDeselected(Tree tree, TreeNode node, Context context,
                boolean oldValue) {
            if (!oldValue) {
                return;
            }

            TreeNode[] nodes = getPathToRoot(node);

            //Loop all nodes and check if they should be removed or decremented
            for (int i = 0; i < nodes.length; i++) {
                TreeNode currentNode = nodes[i];
                String id = currentNode.getId();
                Entry entry = (Entry) selectTracker.get(id);

                //This node is not tracked, so we continue looping.
                if (entry == null) {
                    continue;
                }
                //If count > 1 currentNode has children
                if (entry.count > 1) {
                    entry.count--;

                    //After decrement the currentNode might be the last node in the path.
                    //However if currentNode.isSelected == false, there is another sibling
                    //of the currentNode that is the last node.
                    if (entry.count == 1 && currentNode.isSelected()) {
                        entry.lastNodeInPath = true;
                    }
                } else {
                    if (currentNode.isExpanded()) {
                        // If the node to collapse is root and isRootNodeDisplayed
                        // is false, don't notify listeners
                        if (currentNode.isRoot() && !isRootNodeDisplayed()
                            && isNotifyListeners()) {
                            setNotifyListeners(false);
                            collapse(currentNode);
                            setNotifyListeners(true);
                        } else {
                            collapse(currentNode);
                        }
                    }
                    selectTracker.remove(id);
                }
            }
        }

        /**
         * @see TreeListener#nodeExpanded(Tree, TreeNode, Context, boolean)
         *
         * @param tree tree the operation was made on
         * @param node node that was expanded
         * @param context provides access to {@link org.apache.click.Context}
         * @param oldValue contains the previous value of expanded state
         */
        public void nodeExpanded(Tree tree, TreeNode node, Context context,
                boolean oldValue) { /*noop*/ }

        /**
         * @see TreeListener#nodeCollapsed(Tree, TreeNode, Context, boolean)
         *
         * @param tree tree the operation was made on
         * @param node node that was collapsed
         * @param context provides access to {@link org.apache.click.Context}
         * @param oldValue contains the previous value of selected state
         */
        public void nodeCollapsed(Tree tree, TreeNode node, Context context,
                boolean oldValue) { /*noop*/ }

        /**
         * Provides debug information about the map storing the tracked paths.
         */
//        private void dumpPathTracker() {
//            System.out.println("--------------------------------------Printing Path Tracker map\n");
//            if (selectTracker == null) {
//                System.out.println("Path tracker is null");
//                return;
//            }
//            for (Iterator it = selectTracker.keySet().iterator(); it.hasNext();) {
//                String key = (String) it.next();
//                System.out.println("ids -> [" + key + "]  value count -> ["
//                        + ((Entry) selectTracker.get(key)).count + "] : last node -> ["
//                        + ((Entry) selectTracker.get(key)).lastNodeInPath + "]");
//            }
//            System.out.println("--------------------------------------Done");
//        }
    }

    /**
     * Holds information about a selected node's path entry. Each entry corresponds
     * to a specific tree node. The node's id and corresponding entry is stored in a
     * java.util.Map, where the node's id is the key and the entry is the value.
     */
    private static final class Entry implements Serializable {

        /** default serial version id. */
        private static final long serialVersionUID = 1L;

        /** Number of times this entry was found on the path. */
        private int count = 1;

        /**
         * Indicates if this entry is the last node in the path. At rendering time this
         * variable is checked, and if it is true, the css class "hide" is
         * appended, so that the nodes below this entry is not shown.
         */
        private boolean lastNodeInPath = false;

        /**
         * Returns a string representation of the Entry instance.
         *
         * @return a string representation
         */
        public String toString() {
            StringBuffer buffer = new StringBuffer("Entry value -> (").append(count).
                    append(")");
            buffer.append(" lastNodeInPath -> (").append(lastNodeInPath).append(")");
            return buffer.toString();
        }
    }

    /**
     * Javascript helper method that checks if the specified tree
     * node should be hidden or not.
     *
     * @param specified tree node to check
     * @return true if the node should be hidden, false otherwise
     */
    private boolean shouldHideNode(TreeNode treeNode) {
        if (isJavascriptEnabled() && !javascriptHandler.renderAsExpanded(treeNode)) {
            return true;
        }
        return false;
    }

    /**
     * Remove the specified toTrim string from the front and back
     * of the specified str argument.
     *
     * @param str to trim
     * @param toTrim the specified string to remove
     * @return trimmed string
     */
    private String trimStr(String str, String toTrim) {
        if (StringUtils.isBlank(toTrim)) {
            return str;
        }
        if (str.startsWith(toTrim)) {
            str = str.substring(toTrim.length());
        }
        if (str.endsWith(toTrim)) {
            str = str.substring(0, str.indexOf(toTrim));
        }
        return str;
    }
}
TOP

Related Classes of org.apache.click.extras.tree.Tree$Entry

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.