/*
 * Copyright 2007 Malcolm A. Edgar
 *
 * Licensed 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 net.sf.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 javax.servlet.ServletContext;
import net.sf.click.Context;
import net.sf.click.control.Decorator;
import net.sf.click.control.AbstractControl;
import net.sf.click.util.ClickUtils;
import net.sf.click.util.HtmlStringBuffer;
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="codeJava">
 * <span class="kw">public class</span> PlainTreePage <span class="kw">extends</span> BorderPage {
 *
 *     <span class="kw">public</span> PlainTreePage() {
 *         Tree tree = buildTree();
 *         addControl(tree);
 *     }
 *
 *     <span class="cm">//This method creates a representation of a Windows OS directory.</span>
 *     <span class="kw">public Tree</span> buildTree() {
 *         Tree tree = <span class="kw">new</span> Tree(<span class="st">"tree"</span>);
 *
 *         <span class="cm">//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).</span>
 *         TreeNode root = new TreeNode(<span class="st">"c:"</span>);
 *
 *         <span class="cm">//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.</span>
 *         TreeNode dev = <span class="kw">new</span> TreeNode(<span class="st">"dev"</span>,<span class="st">"1"</span>, root);
 *
 *         <span class="cm">//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</span>
 *         <span class="kw">new</span> TreeNode(<span class="st">"java.pdf"</span>, <span class="st">"2"</span>, dev, <span class="kw">false</span>);
 *         <span class="kw">new</span> TreeNode(<span class="st">"ruby.pdf"</span>, <span class="st">"3"</span>, dev, <span class="kw">false</span>);
 *
 *         TreeNode programFiles = <span class="kw">new</span> TreeNode(<span class="st">"program files"</span>, <span class="st">"4"</span>, root);
 *         TreeNode adobe = <span class="kw">new</span> TreeNode(<span class="st">"Adobe"</span>, <span class="st">"5"</span>, programFiles);
 *
 *         TreeNode download = <span class="kw">new</span> TreeNode(<span class="st">"downloads"</span>,<span class="st">"6"</span>, root);
 *         TreeNode web = <span class="kw">new</span> TreeNode(<span class="st">"web"</span>, <span class="st">"7"</span>, download);
 *         <span class="kw">new</span> TreeNode(<span class="st">"html.pdf"</span>, <span class="st">"8"</span>, web);
 *         <span class="kw">new</span> TreeNode(<span class="st">"css.html"</span>, <span class="st">"9"</span>, web);
 *
 *         TreeNode databases = <span class="kw">new</span> TreeNode(<span class="st">"databases"</span>, <span class="st">"10"</span>, download);
 *         <span class="kw">new</span> TreeNode(<span class="st">"mysql.html"</span>,<span class="st">"11"</span>,databases);
 *         <span class="kw">new</span> TreeNode(<span class="st">"oracle.pdf"</span>,<span class="st">"12"</span>,databases);
 *         <span class="kw">new</span> TreeNode(<span class="st">"postgres"</span>,<span class="st">"13"</span>,databases);
 *
 *         tree.setRootNode(root);
 *         <span class="kw">return</span> tree;
 *     }
 * } </pre>
 *
 * <h4>Tree Style</h4>
 *
 * The Tree control automatically deploys the tree CSS style sheet
 * (<tt>tree.css</tt>) and javascript (<tt>tree.js</tt>) to the application
 * directory <tt>/click/tree</tt>. To import the style sheet simply reference
 * the {@link net.sf.click.util.PageImports} object. For example:
 *
 * <pre class="codeHtml">
 * &lt;html&gt;
 *    &lt;head&gt;
 *       <span class="blue">$imports</span>
 *    &lt;/head&gt;
 *    &lt;body&gt;
 *       <span class="red">$tree</span>
 *    &lt;/body&gt;
 * &lt;/html&gt; </pre>
 * <p/>
 * <h4>Tree customization</h4>
 * The following list of stylesheet classes are used to render the tree
 * icons. One can easily change the tree.css to use a different set of
 * icons. Note: all classes are set in a &lt;span&gt; element.
 * <dl>
 *      <dt>&lt;span class=<span class="blue">"leafIcon"</span>&gt;</dt>
 *      <dd>renders the leaf node of the tree</dd>
 *      <dt>&lt;span class=<span class="blue">"expandedIcon"</span>&gt;</dt>
 *      <dd>renders the expanded state of a node</dd>
 *      <dt>&lt;span class=<span class="blue">"collapsedIcon"</span>&gt;</dt>
 *      <dd>renders the collapsed state of a node</dd>
 * </dl>
 *
 * <strong>Credit</strong> goes to <a href="http://wicket.sourceforge.net">Wicket</a>
 * for the following:
 * <ul>
 *      <li>images/folder-closed.png</li>
 *      <li>images/folder-open.png</li>
 *      <li>images/item.png</li>
 * </ul>
 *
 * @author Bob Schellink
 */
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's bookmark for scrolling to the last expanded/collapsed node. */
    public static final String TREE_BOOKMARK = "tbm";

    /** The tree.css style sheet import link. */
    public static final String TREE_IMPORTS =
            "<link type=\"text/css\" rel=\"stylesheet\" href=\"$/click/tree/tree.css\"></link>\n";

    /** Client side javascript import link. */
    public static final String JAVASCRIPT_IMPORTS =
            "<script type=\"text/javascript\" src=\"$/click/tree/tree.js\"></script>\n";

    /** Client side javascript cookie import link. */
    public static final String JAVASCRIPT_COOKIE_IMPORTS =
            "<script type=\"text/javascript\" src=\"$/click/tree/cookie-helper.js\"></script>\n";

    /** The Tree resource file names. */
    public static final String[] TREE_RESOURCES = {
        "/net/sf/click/extras/tree/tree.css",
        "/net/sf/click/extras/tree/tree.js",
        "/net/sf/click/extras/tree/cookie-helper.js"
    };

    /** 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 image file names. */
    protected static final String[] TREE_IMAGES = {
        "/net/sf/click/extras/tree/images/folder-open.png",
        "/net/sf/click/extras/tree/images/folder-closed.png",
        "/net/sf/click/extras/tree/images/item.png",
        "/net/sf/click/extras/tree/images/corner.png",
        "/net/sf/click/extras/tree/images/minus.png",
        "/net/sf/click/extras/tree/images/minus-corner.png",
        "/net/sf/click/extras/tree/images/plus.png",
        "/net/sf/click/extras/tree/images/plus-corner.png",
        "/net/sf/click/extras/tree/images/tjunction.png",
        "/net/sf/click/extras/tree/images/vertical-line.png"
    };

    /** 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 list of tree controls. */
    protected List controls;

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

    /** 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

    /**
     * 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;
    }

    /**
     * Sets whether javascript functionality is enabled or not. 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 from the
     * browser between requests. The tree currently supports the
     * following policies:
     * <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 using
     * {@link javax.servlet.http.HttpServletRequest#isRequestedSessionIdFromCookie()}. If
     * {@link javax.servlet.http.HttpServletRequest#isRequestedSessionIdFromCookie()}
     * returns true the policy selected will be
     * {@link #JAVASCRIPT_COOKIE_POLICY} 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 <tt>"display:none"</tt> idiom.
     *
     * @param newValue the value to set the javascriptEnabled property to
     * @throws IllegalArgumentException if the context is null
     * @see #setJavascriptEnabled(boolean, int)
     */
    public void setJavascriptEnabled(boolean newValue) {
        if (getContext() == null) {
            throw new IllegalStateException("Context is mandatory when enabling javascript support");
        }
        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.
     *
     * @param newValue the value to set the javascriptEnabled property to
     * @param javascriptPolicy the current javascript policy
     * @throws IllegalArgumentException if the context is null
     * @see #setJavascriptEnabled(boolean)
     */
    public void setJavascriptEnabled(boolean newValue, int javascriptPolicy) {
        if (getContext() == null) {
            throw new IllegalStateException("Context is mandatory when enabling javascript support");
        }

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

            //Populate the javascript handler with its state. This call will notify
            //any tree listeners about new values.
            javascriptHandler.init(getContext());
        } else {
            removeListener(javascriptHandler);
            this.javascriptPolicy = 0;
        }
    }

    /**
     * Return the HTML head import statements for the CSS stylesheet file:
     * <tt>click/tree/tree.css</tt>.
     * <p/>
     * If javascript is enabled, this method also includes the head import
     * Javascript file: <tt>click/tree/tree.js</tt>.
     *
     * @return the HTML head import statements for the control stylesheet
     */
    public String getHtmlImports() {
        String path = getContext().getRequest().getContextPath();
        StringBuffer buffer = new StringBuffer(100);
        if (isJavascriptEnabled()) {
            buffer.append(StringUtils.replace(JAVASCRIPT_IMPORTS, "$", path));
            if (javascriptPolicy == JAVASCRIPT_COOKIE_POLICY) {
                buffer.append(StringUtils.replace(JAVASCRIPT_COOKIE_IMPORTS, "$", path));
            }
        }
        buffer.append(StringUtils.replace(TREE_IMPORTS, "$", path));
        return buffer.toString();
    }

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

    /**
     * This method binds the users request of expanded and collapsed nodes to
     * the tree's nodes. The behavior of this method is to swap the value of the
     * expanded state. Thus expanded nodes will be collapsed and collapsed
     * nodes will be expanded.
     */
    public void bindExpandOrCollapseValues() {
        String[] nodeIds = getRequestValues(EXPAND_TREE_NODE_PARAM);
        if (nodeIds == null || nodeIds.length <= 0) {
            return;
        }

        expandOrCollapse(nodeIds);
    }

    /**
     * This method binds the users request of selected nodes to the tree's nodes.
     *
     * <p/>The behavior of this method is to swap the value of the selected state.
     * Thus selected nodes will be deselected and deselected nodes will be selected.
     */
    public void bindSelectOrDeselectValues() {
        String[] nodeIds = getRequestValues(SELECT_TREE_NODE_PARAM);
        if (nodeIds == null || nodeIds.length <= 0) {
            return;
        }

        selectOrDeselect(nodeIds);
    }

    /**
     * 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 its 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 its 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 its 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 its 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 its 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 its 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 its 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 its 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 its 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 its 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 its 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;
    }

    /**
     * Deploy all files defined in the constants <tt>{@link #TREE_RESOURCES}</tt>
     * and <tt>{@link #TREE_IMAGES}</tt> to the <tt>click/tree</tt> web
     * directory when the application is initialized.
     *
     * @param servletContext the servlet context
     * @see net.sf.click.Control#onDeploy(ServletContext)
     */
    public void onDeploy(ServletContext servletContext) {
        ClickUtils.deployFiles(servletContext, TREE_RESOURCES, "click/tree");
        ClickUtils.deployFiles(servletContext, TREE_IMAGES, "click/tree/images");
    }

    /**
     * TreeNode callback interface.
     */
    interface 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);
    }

    /**
     * Processes user request to change state of the tree.
     * This implementation processes any expand/collapse and select/deselect
     * changes as requested.
     *
     * @see net.sf.click.Control#onProcess()
     * @see #bindExpandOrCollapseValues()
     * @see #bindSelectOrDeselectValues()
     *
     * @return true to continue Page event processing or false otherwise
     */
    public boolean onProcess() {
        bindExpandOrCollapseValues();
        bindSelectOrDeselectValues();

        if (isJavascriptEnabled()) {
            //Populate the javascript handler with its state. This call will notify
            //any tree listeners about new values.
            //javascriptHandler.init();
        }
        return true;
    }

    /**
     * This method does nothing.
     * <p/>
     * Please use the {@link #addListener(TreeListener)} method instead.
     *
     * @see net.sf.click.Control#setListener(Object, String)
     *
     * @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) {
        // Does nothing
    }

    /**
     * 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

    /**
     * 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(256);
        buffer.elementStart("div");
        buffer.appendAttribute("id", getId());


        if (hasAttributes()) {
            buffer.appendAttributes(getAttributes());
        }
        if (hasStyles()) {
            buffer.appendStyleAttributes(getStyles());
        }
        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 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=\"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");
        StringBuffer sb = new StringBuffer();
        sb.append(getExpandClass(treeNode));
        buffer.appendAttribute("class", sb.toString());
        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>");
        }
        buffer.append("\n");
    }

    /**
     * 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) {
        buffer.elementStart("a");

        buffer.append(" href=\"");
        buffer.append(getContext().getRequest().getRequestURI());
        buffer.append("?");
        buffer.append(EXPAND_TREE_NODE_PARAM);
        buffer.append("=");
        buffer.append(treeNode.getId());
        buffer.append("\"");

        buffer.appendAttribute("class", "spacer");
        if (isJavascriptEnabled()) {
            //hook to insert javascript specific code
            javascriptHandler.getJavascriptRenderer().renderExpandAndCollapseAction(buffer);
        }

        buffer.closeTag();
        buffer.elementEnd("a");
        buffer.append("\n");
    }

    /**
     * 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);

        //TODO IE HACK. With a empty span <span></span> IE does not render the
        //icons. Putting a '&nbsp;' in the span seemed to work. Perhaps there is a
        //better workaround.
        buffer.append("&nbsp;");
        buffer.append("</span>");

        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) {
        //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(">");
    }

    /**
     * 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) {
        buffer.elementStart("a");
        buffer.append(" href=\"");
        buffer.append(getContext().getRequest().getRequestURI());
        buffer.append("?");
        buffer.append(SELECT_TREE_NODE_PARAM);
        buffer.append("=");
        buffer.append(treeNode.getId());
        buffer.append("\"");

        buffer.closeTag();
        if (treeNode.getValue() != null) {
            buffer.append(treeNode.getValue());
        }
        buffer.elementEnd("a");
        buffer.append("\n");
    }

    /**
     * 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 oldValue previous expanded value of the specified node
     */
    protected void fireNodeExpanded(TreeNode node, boolean oldValue) {
        for (Iterator it = listeners.iterator(); it.hasNext();) {
            TreeListener listener = (TreeListener) it.next();
            listener.nodeExpanded(this, node, getContext(), oldValue);
        }
    }

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

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

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

    // ----------------------------------------------------- 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;
    }

    //----------------------------------------------------------- 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;

    /**
     * <strong>Please note</strong> this interface is only meant for
     * developers of this control, not users.
     * <p/>
     * Provides the contract for pluggable javascript renderers for
     * the Tree.
     */
    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);
    }

    /**
     * <strong>Please note</strong> this class is only meant for
     * developers of this control, not users.
     * <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 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) {
            buffer.append(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(" onclick=\"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(" onclick=\"handleNodeExpansion(this,event,'", expandId, "','");
            nodeExpansionString = buildString(tmp, iconId, "'); return false;\"");
        }
    }

    /**
     * <strong>Please note</strong> this class is only meant for
     * developers of this control, not users.
     * <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 net.sf.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 net.sf.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 net.sf.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 net.sf.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) {
            if (value == null) {
                ClickUtils.setCookie(getContext().getRequest(), getContext().getResponse(),
                        name, value, 0, "/");
            } else {
                ClickUtils.setCookie(getContext().getRequest(), getContext().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 net.sf.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 net.sf.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()) {
                    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 net.sf.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()) {
                        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 net.sf.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 net.sf.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;
    }
}
