/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.click.extras.tree;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.StringTokenizer;
import org.apache.click.ActionListener;
import org.apache.click.Context;
import org.apache.click.Control;
import org.apache.click.ActionEventDispatcher;
import org.apache.click.control.AbstractControl;
import org.apache.click.control.ActionLink;
import org.apache.click.control.Decorator;
import org.apache.click.extras.control.SubmitLink;
import org.apache.click.util.ClickUtils;
import org.apache.click.util.HtmlStringBuffer;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
/**
* Provides a tree control for displaying hierarchical data. The tree operates
* on a hierarchy of {@link TreeNode}'s. Each TreeNode must provide a
* uniquely identified node in the hierarchy.
* <p/>
* Below is screenshot of how the tree will render in a browser.
*
* <table cellspacing='10'>
* <tr>
* <td>
* <img align='middle' hspace='2' src='tree.png' title='Tree'/>
* </td>
* </tr>
* </table>
*
* <h3>Tree Example</h3>
*
* An example tree usage is provided below (this code was used to produce the screenshot):
*
* <pre class="prettyprint">
* public class PlainTreePage extends BorderPage {
*
* public PlainTreePage() {
* Tree tree = buildTree();
* addControl(tree);
* }
*
* // This method creates a representation of a Windows OS directory.
* public Tree buildTree() {
* Tree tree = new Tree("tree");
*
* // Create a node representing the root directory with the specified
* // parameter as the value. Because an id is not specified, a random
* // one will be generated by the node. By default the root node is
* // not rendered by the tree. This can be changed by calling
* // tree.setRootNodeDisplayed(true).
* TreeNode root = new TreeNode("c:");
*
* // Create a new directory, setting the root directory as its parent. Here
* // we do specify a id as the 2nd argument, so no id is generated.
* TreeNode dev = new TreeNode("dev","1", root);
*
* // The following two nodes represent files in the directory.
* // The false argument to the constructor below means that these nodes
* // does not support child nodes. Makes sense since files cannot contain
* // directories or other files
* new TreeNode("java.pdf", "2", dev, false);
* new TreeNode("ruby.pdf", "3", dev, false);
*
* TreeNode programFiles = new TreeNode("program files", "4", root);
* TreeNode adobe = new TreeNode("Adobe", "5", programFiles);
*
* TreeNode download = new TreeNode("downloads","6", root);
* TreeNode web = new TreeNode("web", "7", download);
* new TreeNode("html.pdf", "8", web);
* new TreeNode("css.html", "9", web);
*
* TreeNode databases = new TreeNode("databases", "10", download);
* new TreeNode("mysql.html","11",databases);
* new TreeNode("oracle.pdf","12",databases);
* new TreeNode("postgres","13",databases);
*
* tree.setRootNode(root);
* return tree;
* }
* } </pre>
*
* <a name="resources"></a>
* <h3>CSS and JavaScript resources</h3>
*
* The Tree control makes use of the following resources
* (which Click automatically deploys to the application directory, <tt>/click/tree</tt>):
*
* <ul>
* <li><tt>click/tree/tree.css</tt></li>
* <li><tt>click/tree/tree.js</tt></li>
* <li><tt>click/tree/cookie-helper.js</tt></li>
* </ul>
*
* To import these Tree files simply reference the variables
* <span class="blue">$headElements</span> and
* <span class="blue">$jsElements</span> in the page template. For example:
*
* <pre class="codeHtml">
* <html>
* <head>
* <span class="blue">$headElements</span>
* </head>
* <body>
*
* <span class="red">$tree</span>
*
* <span class="blue">$jsElements</span>
* </body>
* </html> </pre>
*
* <a name="customization"></a>
* <h3>Tree customization</h3>
*
* The following list of stylesheet classes are used to render the tree
* icons. One can easily change the <tt>tree.css</tt> to use a different set of
* icons. Note: all CSS classes are set inline in <span> elements.
* <ul>
* <li><span class=<span class="blue">"leafIcon"</span>> - renders the leaf node of the tree</li>
* <li><span class=<span class="blue">"expandedIcon"</span>> - renders the expanded state of a node</li>
* <li><span class=<span class="blue">"collapsedIcon"</span>> - renders the collapsed state of a node</li>
* </ul>
*
* <strong>Credit</strong> goes to <a href="http://wicket.apache.org">Wicket</a>
* for these images:
* <ul>
* <li>images/folder-closed.png</li>
* <li>images/folder-open.png</li>
* <li>images/item.png</li>
* </ul>
*/
public class Tree extends AbstractControl {
// -------------------------------------------------------------- Constants
/** The tree's expand/collapse parameter name: <tt>"expandTreeNode"</tt>. */
public static final String EXPAND_TREE_NODE_PARAM = "expandTreeNode";
/** The tree's select/deselect parameter name: <tt>"selectTreeNode"</tt>. */
public static final String SELECT_TREE_NODE_PARAM = "selectTreeNode";
/** The Tree imports statement. */
public static final String TREE_IMPORTS =
"<link type=\"text/css\" rel=\"stylesheet\" href=\"{0}/click/tree/tree{1}.css\"/>\n";
/** Client side javascript imports statement. */
public static final String JAVASCRIPT_IMPORTS =
"<script type=\"text/javascript\" src=\"{0}/click/tree/tree{1}.js\"></script>\n";
/** Client side javascript cookie imports statement. */
public static final String JAVASCRIPT_COOKIE_IMPORTS =
"<script type=\"text/javascript\" src=\"{0}/click/tree/cookie-helper{1}.js\"></script>\n";
/** Indicator for using cookies to implement client side behavior. */
public final static int JAVASCRIPT_COOKIE_POLICY = 1;
/** Indicator for using the session to implement client side behavior. */
public final static int JAVASCRIPT_SESSION_POLICY = 2;
/** The tree's expand icon name: <tt>"expandedIcon"</tt>. */
protected static final String EXPAND_ICON = "expandedIcon";
/** The tree's collapsed icon name: <tt>"collapsedIcon"</tt>. */
protected static final String COLLAPSE_ICON = "collapsedIcon";
/** The tree's leaf icon name: <tt>"leafIcon"</tt>. */
protected static final String LEAF_ICON = "leafIcon";
/** default serial version id. */
private static final long serialVersionUID = 1L;
// ----------------------------------------------------- Instance Variables
/** The tree's hierarchical data model. */
protected TreeNode rootNode;
/** Array of ids that must be selected or deselected. */
protected String[] selectOrDeselectNodeIds = null;
/** Array of ids that must be expanded or collapsed. */
protected String[] expandOrCollapseNodeIds = null;
/** The Tree node select / deselect link. */
protected ActionLink selectLink;
/** The tree node expand / collapse link. */
protected ActionLink expandLink;
/** Callback provider for users to decorate tree nodes. */
private transient Decorator decorator;
/**
* Specifies if the root node should be displayed, or only its children.
* By default this value is false.
*/
private boolean rootNodeDisplayed = false;
/** Specifies if client side javascript functionality are enabled. By default this value is false.*/
private boolean javascriptEnabled = false;
/** List of subscribed listeners to tree events.*/
private List listeners = new ArrayList();
/** Current javascript policy in effect. */
private int javascriptPolicy = 0;
/** Flag indicates if listeners should be notified of any state changes. */
private boolean notifyListeners = true;
// ---------------------------------------------------- Public Constructors
/**
* Create an Tree control for the given name.
* <p/>
* The constructor also sets the id attribute to
* <tt>"tree"</tt> and the css class to <tt>"treestyle"</tt>
* to qualify the tree control when styled by tree.css. If the
* css class value is changed, ensure to also change the
* tree.css selectors that still reference <tt>"treestyle"</tt>.
*
* @param name the tree name
* @throws IllegalArgumentException if the name is null
*/
public Tree(String name) {
setName(name);
setAttribute("id", "tree");
setAttribute("class", "treestyle");
}
/**
* Create a Tree with no name defined.
* <p/>
* The constructor also sets the id attribute to
* <tt>"tree"</tt> and the css class to <tt>"treestyle"</tt>
* to qualify the tree control when styled by tree.css. If the
* css class value is changed, ensure to also change the
* tree.css selectors that still reference <tt>"treestyle"</tt>.
* <p/>
* <b>Please note</b> the control's name must be defined before it is valid.
*/
public Tree() {
super();
setAttribute("id", "tree");
setAttribute("class", "treestyle");
}
// --------------------------------------------- Public Getters and Setters
/**
* @see Control#setName(String)
*
* @param name of the control
* @throws IllegalArgumentException if the name is null
*/
public void setName(String name) {
super.setName(name);
getExpandLink().setName(name + "-expandLink");
getExpandLink().setLabel("");
getExpandLink().setParent(this);
getSelectLink().setName(name + "-selectLink");
getSelectLink().setLabel("");
getSelectLink().setParent(this);
}
/**
* Return the tree's root TreeNode. This method will recalculate
* the tree's root node in case a new root node was set.
*
* @return the tree's root TreeNode.
*/
public TreeNode getRootNode() {
//Calculate the root node dynamically by finding the node where parent == null.
//Thus if a new root node was created this method will still return
//the correct node
if (rootNode == null) {
return null;
}
while ((rootNode.getParent()) != null) {
rootNode = rootNode.getParent();
}
return rootNode;
}
/**
* Return if tree has a root node.
*
* @return boolean indicating if the tree's root has been set.
*/
public boolean hasRootNode() {
return getRootNode() != null;
}
/**
* Return if the tree's root node should be displayed or not.
*
* @return if root node should be displayed
*/
public boolean isRootNodeDisplayed() {
return rootNodeDisplayed;
}
/**
* Sets whether the tree's root node should be displayed or not.
*
* @param rootNodeDisplayed true if the root node should be displayed,
* false otherwise
*/
public void setRootNodeDisplayed(boolean rootNodeDisplayed) {
this.rootNodeDisplayed = rootNodeDisplayed;
}
/**
* Set the tree's root TreeNode.
*
* @param rootNode node will be set as the root
*/
public void setRootNode(TreeNode rootNode) {
if (rootNode == null) {
return;
}
this.rootNode = rootNode;
}
/**
* Get the tree's decorator.
*
* @return the tree's decorator.
*/
public Decorator getDecorator() {
return decorator;
}
/**
* Set the tree's decorator which enables a interception point for users to render
* the tree nodes.
*
* @param decorator the tree's decorator
*/
public void setDecorator(Decorator decorator) {
this.decorator = decorator;
}
/**
* Returns if javascript functionality are enabled or not.
*
* @return true if javascript functions are enabled, false otherwise
* @see #setJavascriptEnabled(boolean)
*/
public boolean isJavascriptEnabled() {
return javascriptEnabled;
}
/**
* Enables javascript functionality.
* <p/>
* If true the tree will be navigatable in the browser using javascript,
* instead of doing round trips to the server on each operation.
* <p/>
* With javascript enabled you need to store the values passed from the
* browser between requests. The tree currently supports the
* following options:
* <ul>
* <li>{@link #JAVASCRIPT_COOKIE_POLICY}
* <li>{@link #JAVASCRIPT_SESSION_POLICY}
* </ul>
* This method will try and determine which policy should be applied
* to the current request by checking the value
* {@link javax.servlet.http.HttpServletRequest#isRequestedSessionIdFromCookie()}.
* If {@link javax.servlet.http.HttpServletRequest#isRequestedSessionIdFromCookie()}
* returns true, {@link #JAVASCRIPT_COOKIE_POLICY} will be used, otherwise
* {@link #JAVASCRIPT_SESSION_POLICY}.
* <p/>
* <strong>Note:</strong> if javascript is enabled, then the entire
* tree is rendered even if some nodes are in a collapsed state. This
* enables the tree to still be fully navigatable in the browser. However
* nodes that are in a collapsed state are still displayed as collapsed
* using the style <tt>"display:none"</tt>.
*
* @see #setJavascriptEnabled(boolean, int)
*
* @param newValue the value to set the javascriptEnabled property to
* @throws IllegalArgumentException if the context is null
*/
public void setJavascriptEnabled(boolean newValue) {
if (getContext().getRequest().isRequestedSessionIdFromCookie()) {
setJavascriptEnabled(newValue, JAVASCRIPT_COOKIE_POLICY);
} else {
setJavascriptEnabled(newValue, JAVASCRIPT_SESSION_POLICY);
}
}
/**
* Overloads {@link #setJavascriptEnabled(boolean)}. Enables one
* to select the javascript policy to apply.
*
* @see #setJavascriptEnabled(boolean)
*
* @param newValue the value to set the javascriptEnabled property to
* @param javascriptPolicy the current javascript policy
* @throws IllegalArgumentException if the context is null
*/
public void setJavascriptEnabled(boolean newValue, int javascriptPolicy) {
this.javascriptEnabled = newValue;
if (javascriptEnabled) {
javascriptHandler = createJavascriptHandler(javascriptPolicy);
addListener(javascriptHandler);
this.javascriptPolicy = javascriptPolicy;
} else {
removeListener(javascriptHandler);
this.javascriptPolicy = 0;
}
}
/**
* Return the CSS "width" style attribute of the tree, or null if not
* defined.
*
* @return the CSS "width" style attribute of the tree, or null if not
* defined
*/
public String getWidth() {
return getStyle("width");
}
/**
* Set the the CSS "width" style attribute of the tree. For example:
*
* <pre class="prettyprint">
* Tree tree = new Tree("mytree");
* tree.setWidth("200px"); </pre>
*
* @param value the CSS "width" style attribute
*/
public void setWidth(String value) {
setStyle("width", value);
}
/**
* Return the CSS "height" style of the tree, or null if not defined.
*
* @return the CSS "height" style attribute of the tree, or null if not
* defined
*/
public String getHeight() {
return getStyle("height");
}
/**
* Set the the CSS "height" style attribute of the tree. For example:
*
* <pre class="prettyprint">
* Tree tree = new Tree("mytree");
* tree.setHeight("200px"); </pre>
*
* @param value the CSS "height" style attribute
*/
public void setHeight(String value) {
setStyle("height", value);
}
/**
* Return the Tree HTML head imports statements for the following
* resources:
* <p/>
* <ul>
* <li><tt>click/tree/tree.css</tt></li>
* <li><tt>click/tree/tree.js</tt></li>
* <li><tt>click/tree/cookie-helper.js</tt></li>
* </ul>
*
* @see org.apache.click.Control#getHtmlImports()
*
* @return the HTML head import statements for the control
*/
public String getHtmlImports() {
Context context = getContext();
HtmlStringBuffer buffer = new HtmlStringBuffer(256);
if (isJavascriptEnabled()) {
buffer.append(ClickUtils.createHtmlImport(JAVASCRIPT_IMPORTS,
context));
if (javascriptPolicy == JAVASCRIPT_COOKIE_POLICY) {
buffer.append(ClickUtils.createHtmlImport(JAVASCRIPT_COOKIE_IMPORTS,
context));
}
}
buffer.append(ClickUtils.createHtmlImport(TREE_IMPORTS, context));
return buffer.toString();
}
/**
* @see org.apache.click.Control#getHeadElements()
*
* @return the list of HEAD elements to be included in the page
*/
public List getHeadElements() {
if (headElements == null) {
headElements = super.getHeadElements();
headElements.addAll(getExpandLink().getHeadElements());
headElements.addAll(getSelectLink().getHeadElements());
}
return headElements;
}
/**
* Return the tree node expand / collapse link.
* <p/>
* This method returns a {@link org.apache.click.extras.control.SubmitLink}
* so that the Tree can function properly when added to a
* {@link org.apache.click.control.Form}.
*
* @return the tree node expand / collapse link
*/
public ActionLink getExpandLink() {
if (expandLink == null) {
expandLink = new SubmitLink();
}
return expandLink;
}
/**
* Return the tree node select / deselect link.
* <p/>
* This method returns a {@link org.apache.click.extras.control.SubmitLink}
* so that the Tree can function properly when added to a
* {@link org.apache.click.control.Form}.
*
* @return the tree node select / deselect link.
*/
public ActionLink getSelectLink() {
if (selectLink == null) {
selectLink = new SubmitLink();
}
return selectLink;
}
// --------------------------------------------------------- Public Methods
/**
* This method binds the users request of expanded and collapsed nodes to
* the tree's nodes.
*/
public void bindExpandOrCollapseValues() {
expandOrCollapseNodeIds = getExpandLink().getParameterValues(EXPAND_TREE_NODE_PARAM);
}
/**
* This method binds the users request of selected nodes to the tree's nodes.
*/
public void bindSelectOrDeselectValues() {
selectOrDeselectNodeIds = getSelectLink().getParameterValues(SELECT_TREE_NODE_PARAM);
}
/**
* Query if the tree will notify its tree listeners of any change
* to the tree's model.
*
* @return true if listeners should be notified of any changes.
*/
public boolean isNotifyListeners() {
return notifyListeners;
}
/**
* Enable or disable if the tree will notify its tree listeners of any change
* to the tree's model.
*
* @param notifyListeners true if the tree will notify its listeners ,
* false otherwise
*/
public void setNotifyListeners(boolean notifyListeners) {
this.notifyListeners = notifyListeners;
}
/**
* Expand the node with matching id and inform any listeners of the change.
* If {@link #isNotifyListeners()} returns false, this method will not
* notify its listeners of any change.
*
* @param id identifier of the node to be expanded.
*/
public void expand(String id) {
if (id == null) {
return;
}
setExpandState(id, true);
}
/**
* Expand the node and inform any listeners of the change.
* If {@link #isNotifyListeners()} returns false, this method will not
* notify listeners of any change.
*
* @param node the node to be expanded.
*/
public void expand(TreeNode node) {
if (node == null) {
return;
}
setExpandState(node, true);
}
/**
* Collapse the node with matching id and inform any listeners of the change.
* If {@link #isNotifyListeners()} returns false, this method will not
* notify listeners of any change.
*
* @param id identifier of node to be collapsed.
*/
public void collapse(String id) {
if (id == null) {
return;
}
setExpandState(id, false);
}
/**
* Collapse the node and inform any listeners of the change.
* If {@link #isNotifyListeners()} returns false, this method will not
* notify listeners of any change.
*
* @param node the node to be collapsed.
*/
public void collapse(TreeNode node) {
if (node == null) {
return;
}
setExpandState(node, false);
}
/**
* Expand all the nodes of the tree and inform any listeners of the change.
* If {@link #isNotifyListeners()} returns false, this method will not
* notify listeners of any change.
*/
public void expandAll() {
for (Iterator it = iterator(); it.hasNext();) {
TreeNode node = (TreeNode) it.next();
boolean oldValue = node.isExpanded();
node.setExpanded(true);
if (isNotifyListeners()) {
fireNodeExpanded(node, oldValue);
}
}
}
/**
* Collapse all the nodes of the tree and inform any listeners of the change.
* If {@link #isNotifyListeners()} returns false, this method will not
* notify listeners of any change.
*/
public void collapseAll() {
for (Iterator it = iterator(); it.hasNext();) {
TreeNode node = (TreeNode) it.next();
boolean oldValue = node.isExpanded();
node.setExpanded(false);
if (isNotifyListeners()) {
fireNodeCollapsed(node, oldValue);
}
}
}
/**
* Select the node with matching id and inform any listeners of the change.
* If {@link #isNotifyListeners()} returns false, this method will not
* notify listeners of any change.
*
* @param id identifier of node to be selected.
*/
public void select(String id) {
if (id == null) {
return;
}
setSelectState(id, true);
}
/**
* Select the node and inform any listeners of the change.
* If {@link #isNotifyListeners()} returns false, this method will not
* notify listeners of any change.
*
* @param node the node to be selected.
*/
public void select(TreeNode node) {
if (node == null) {
return;
}
setSelectState(node, true);
}
/**
* Deselect the node with matching id and inform any listeners of the change.
* If {@link #isNotifyListeners()} returns false, this method will not
* notify listeners of any change.
*
* @param id id of node to be deselected.
*/
public void deselect(String id) {
if (id == null) {
return;
}
setSelectState(id, false);
}
/**
* Deselect the node and inform any listeners of the change.
* If {@link #isNotifyListeners()} returns false, this method will not
* notify listeners of any change.
*
* @param node the node to be deselected.
*/
public void deselect(TreeNode node) {
if (node == null) {
return;
}
setSelectState(node, false);
}
/**
* Select all the nodes of the tree and inform any listeners of the change.
* If {@link #isNotifyListeners()} returns false, this method will not
* notify listeners of any change.
*/
public void selectAll() {
for (Iterator it = iterator(); it.hasNext();) {
TreeNode node = (TreeNode) it.next();
boolean oldValue = node.isSelected();
node.setSelected(true);
if (isNotifyListeners()) {
fireNodeSelected(node, oldValue);
}
}
}
/**
* Deselect all the nodes of the tree and inform any listeners of the change.
* If {@link #isNotifyListeners()} returns false, this method will not
* notify listeners of any change.
*/
public void deselectAll() {
for (Iterator it = iterator(); it.hasNext();) {
TreeNode node = (TreeNode) it.next();
boolean oldValue = node.isSelected();
node.setSelected(false);
if (isNotifyListeners()) {
fireNodeDeselected(node, oldValue);
}
}
}
/**
* Returns all the nodes that were expanded.
*
* @param includeInvisibleNodes indicator if only invisible nodes should be included.
* @return list of currently expanded nodes.
*/
public List getExpandedNodes(boolean includeInvisibleNodes) {
List currentlyExpanded = new ArrayList();
for (Iterator it = iterator(); it.hasNext();) {
TreeNode node = (TreeNode) it.next();
if (node.isExpanded()) {
if (includeInvisibleNodes || isVisible(node)) {
currentlyExpanded.add(node);
}
}
}
return currentlyExpanded;
}
/**
* Returns all the nodes that were selected.
*
* @param includeInvisibleNodes indicates if invisible nodes should be included.
* @return list of currently selected nodes.
*/
public List getSelectedNodes(boolean includeInvisibleNodes) {
List currentlySelected = new ArrayList();
for (Iterator it = iterator(); it.hasNext();) {
TreeNode node = (TreeNode) it.next();
if (node.isSelected()) {
if (includeInvisibleNodes || isVisible(node)) {
currentlySelected.add(node);
}
}
}
return currentlySelected;
}
/**
* Provides a TreeNode callback interface.
*/
protected interface Callback {
/**
* Callback on the provided tree node.
*
* @param node the TreeNode to callback
*/
public void callback(final TreeNode node);
}
/**
* Returns an iterator over all the nodes.
*
* @return iterator over all elements in the tree
*/
public Iterator iterator() {
return iterator(getRootNode());
}
/**
* Returns an iterator over all nodes starting from the specified node.
* If null is specified, root node is used instead.
*
* @param node starting point of nodes to iterator over
* @return iterator over all nodes starting form the specified node
*/
public Iterator iterator(TreeNode node) {
if (node == null) {
node = getRootNode();
}
return new BreadthTreeIterator(node);
}
/**
* Finds and returns the first node that matches the id.
*
* @param id identifier of the node to find
* @return TreeNode the first node matching the id.
* @throws IllegalArgumentException if argument is null.
*/
public TreeNode find(String id) {
if (id == null) {
throw new IllegalArgumentException("Argument cannot be null.");
}
return find(getRootNode(), id);
}
/**
* This method binds any expand/collapse and select/deselect changes from
* the request parameters.
* <p/>
* In other words the node id's of expanded, collapsed, selected and
* deselected nodes are retrieved from the request.
*
* @see #bindExpandOrCollapseValues()
* @see #bindSelectOrDeselectValues()
*/
public void bindRequestValue() {
bindExpandOrCollapseValues();
bindSelectOrDeselectValues();
}
/**
* Processes user request to change state of the tree.
* This implementation processes any expand/collapse and select/deselect
* changes as requested.
* <p/>
* Thus expanded nodes will be collapsed and collapsed nodes will be
* expanded. Similarly selected nodes will be deselected and deselected
* nodes will be selected.
*
* @see org.apache.click.Control#onProcess()
* @see #expandOrCollapse(java.lang.String[])
* @see #selectOrDeselect(java.lang.String[])
*
* @return true to continue Page event processing or false otherwise
*/
public boolean onProcess() {
getExpandLink().onProcess();
getSelectLink().onProcess();
bindRequestValue();
ActionEventDispatcher.dispatchActionEvent(this, new ActionListener() {
public boolean onAction(Control source) {
return postProcess();
}
});
return true;
}
/**
* This method cleans up the {@link #expandLink} and {@link #selectLink}.
* @see org.apache.click.Control#onDestroy()
*/
public void onDestroy() {
super.onDestroy();
getExpandLink().onDestroy();
getSelectLink().onDestroy();
}
/**
* Set the controls event listener.
* <p/>
* To receive notifications when TreeNodes are selected or expanded please
* use {@link #addListener(TreeListener)}.
*
* @param listener the listener object with the named method to invoke
* @param method the name of the method to invoke
*/
public void setListener(Object listener, String method) {
super.setListener(listener, method);
}
/**
* Set the control's action listener.
* <p/>
* To receive notifications when TreeNodes are selected or expanded please
* use {@link #addListener(TreeListener)}.
*
* @param listener the control's action listener
*/
public void setActionListener(ActionListener listener) {
super.setActionListener(listener);
}
/**
* Adds the listener to start receiving tree events.
*
* @param listener to add to start receiving tree events.
*/
public void addListener(TreeListener listener) {
listeners.add(listener);
}
/**
* Removes the listener to stop receiving tree events.
*
* @param listener to be removed to stop receiving tree events.
*/
public void removeListener(TreeListener listener) {
listeners.remove(listener);
}
// ------------------------------------------------------ Default Rendering
/**
* @see AbstractControl#getControlSizeEst()
*
* @return the estimated rendered control size in characters
*/
public int getControlSizeEst() {
return 256;
}
/**
* Utility method that force the Tree to remove any entries it made in the
* HttpSession.
* <p/>
* <b>Note</b> Tree only stores a value in the Session when JavaScript
* is enabled and set to {@link #JAVASCRIPT_SESSION_POLICY}.
*/
public void cleanupSession() {
Context context = getContext();
if (context.hasSession()) {
context.getSession().removeAttribute(SessionHandler.JS_HANDLER_SESSION_KEY);
}
}
/**
* Render the HTML representation of the tree.
*
* @see #toString()
*
* @param buffer the specified buffer to render the control's output to
*/
public void render(HtmlStringBuffer buffer) {
buffer.elementStart("div");
buffer.appendAttribute("id", getId());
appendAttributes(buffer);
buffer.append(">\n");
if (isRootNodeDisplayed()) {
TreeNode temp = new TreeNode();
//Do not use the method temp.add(), because that will
//set temp as the parent of the current root node. Temp
//will then become the new root node of the tree.
temp.addChildOnly(getRootNode());
renderTree(buffer, temp, 0);
} else {
renderTree(buffer, getRootNode(), 0);
}
buffer.elementEnd("div");
buffer.append("\n");
if (isJavascriptEnabled()) {
//Complete the lifecycle of the javascript handler.
javascriptHandler.destroy();
}
}
/**
* Return a HTML rendered Tree string of all the tree's nodes.
*
* <p/>Note: by default the tree's root node will not be rendered.
* However this behavior can be changed by calling
* {@link #setRootNodeDisplayed(boolean)} with true.
*
* @see java.lang.Object#toString()
* @return a HTML rendered Tree string
*/
public String toString() {
HtmlStringBuffer buffer = new HtmlStringBuffer(getControlSizeEst());
render(buffer);
return buffer.toString();
}
/**
* Render the children of the specified tree node as html markup and append
* the output to the specified buffer.
* <p/>
* <strong>Note:</strong> only the children of the specified tree node will
* be rendered not the treeNode itself. This method is recursive, so the
* node's children and their children will be rendered and so on.
*
* @param buffer string buffer containing the markup
* @param treeNode specified node who's children will be rendered
* @param indentation current level of the treeNode. The indentation increases each
* time the depth of the tree increments.
*
* @see #setRootNodeDisplayed(boolean)
*/
protected void renderTree(HtmlStringBuffer buffer, TreeNode treeNode, int indentation) {
indentation++;
buffer.elementStart("ul");
buffer.append(" class=\"");
if (isRootNodeDisplayed() && indentation == 1) {
buffer.append("rootLevel level");
} else {
buffer.append("level");
}
buffer.append(Integer.toString(indentation));
//If javascript is enabled and this is not the first level of <ul> elements,
//the css class 'hide' is appended to the <ul> element to ensure the tree
//is in a collapsed state on the browser. However, we must query the
//javascript handler if it does not perhaps veto the collapsed state.
if (indentation > 1 && shouldHideNode(treeNode)) {
buffer.append(" hide");
}
buffer.append("\">\n");
Iterator it = treeNode.getChildren().iterator();
while (it.hasNext()) {
TreeNode child = (TreeNode) it.next();
if (isJavascriptEnabled()) {
javascriptHandler.getJavascriptRenderer().init(child);
}
renderTreeNodeStart(buffer, child, indentation);
renderTreeNode(buffer, child, indentation);
//check if the child node should be renderered
if (shouldRenderChildren(child)) {
renderTree(buffer, child, indentation);
}
renderTreeNodeEnd(buffer, child, indentation);
}
buffer.append("</ul>\n");
}
/**
* Check the state of the specified node if its children
* should be rendered or not.
*
* @param treeNode specified node to check
* @return true if the child nodes should be rendered,
* false otherwise
*/
protected boolean shouldRenderChildren(TreeNode treeNode) {
if (treeNode.isLeaf()) {
return false;
}
if (treeNode.isExpanded()) {
return true;
} else {
//If javascript is enabled, the entire tree has to be rendered
//and sent to the browser. So even if the node is not
//expanded, we still render the node's children.
if (isJavascriptEnabled()) {
return true;
}
}
return false;
}
/**
* Interception point to render html before the tree node is rendered.
*
* @param buffer string buffer containing the markup
* @param treeNode specified node to render
* @param indentation current level of the treeNode
*/
protected void renderTreeNodeStart(HtmlStringBuffer buffer, TreeNode treeNode,
int indentation) {
buffer.append("<li><span class=\"");
if (treeNode.isRoot()) {
buffer.append("rootNode ");
}
buffer.append(getExpandClass(treeNode));
buffer.append("\"");
if (isJavascriptEnabled()) {
//hook to insert javascript specific code
javascriptHandler.getJavascriptRenderer().renderTreeNodeStart(buffer);
}
buffer.appendAttribute("style", "display:block;");
buffer.closeTag();
//Render the node's expand/collapse functionality.
//This includes adding a css class for the current expand/collapse state.
//In the tree.css file, the css classes are mapped to icons by default.
if (treeNode.hasChildren()) {
renderExpandAndCollapseAction(buffer, treeNode);
} else {
buffer.append("<span class=\"spacer\"></span>");
}
}
/**
* Interception point to render html after the tree node was rendered.
*
* @param buffer string buffer containing the markup
* @param treeNode specified node to render
* @param indentation current level of the treeNode
*/
protected void renderTreeNodeEnd(HtmlStringBuffer buffer, TreeNode treeNode,
int indentation) {
buffer.append("</span></li>\n");
}
/**
* Render the expand and collapse action of the tree.
* <p/>
* Default implementation creates a hyperlink that users can click on
* to expand or collapse the nodes.
*
* @param buffer string buffer containing the markup
* @param treeNode treeNode to render
*/
protected void renderExpandAndCollapseAction(HtmlStringBuffer buffer, TreeNode treeNode) {
getExpandLink().setParameter(EXPAND_TREE_NODE_PARAM, treeNode.getId());
if (treeNode.isRoot()) {
getExpandLink().setAttribute("class", "root spacer");
} else {
getExpandLink().setAttribute("class", "spacer");
}
if (isJavascriptEnabled()) {
//hook to insert javascript specific code
javascriptHandler.getJavascriptRenderer().renderExpandAndCollapseAction(buffer);
}
getExpandLink().render(buffer);
}
/**
* Render the specified treeNode.
* <p/>
* If a decorator was specified using {@link #setDecorator(Decorator) },
* this method will render using the decorator instead.
*
* @param buffer string buffer containing the markup
* @param treeNode treeNode to render
* @param indentation current level of the treeNode
*/
protected void renderTreeNode(HtmlStringBuffer buffer, TreeNode treeNode, int indentation) {
if (getDecorator() != null) {
Object value = getDecorator().render(treeNode, getContext());
if (value != null) {
buffer.append(value);
}
return;
}
renderIcon(buffer, treeNode);
buffer.elementStart("span");
if (treeNode.isSelected()) {
buffer.appendAttribute("class", "selected");
} else {
buffer.appendAttribute("class", "unselected");
}
buffer.closeTag();
//renders the node value
renderValue(buffer, treeNode);
buffer.elementEnd("span");
}
/**
* Render the node's icon depending on the current state of the node.
*
* @param buffer string buffer containing the markup
* @param treeNode treeNode to render
*/
protected void renderIcon(HtmlStringBuffer buffer, TreeNode treeNode) {
if (treeNode.getIcon() == null) {
//render the icon to display
buffer.elementStart("span");
buffer.appendAttribute("class", getIconClass(treeNode));
if (isJavascriptEnabled()) {
//An id is needed on the element to do quick lookup using javascript
//document.getElementById(id)
javascriptHandler.getJavascriptRenderer().renderIcon(buffer);
}
buffer.append(">");
buffer.append("</span>");
} else {
buffer.elementStart("img");
buffer.appendAttribute("class", "customIcon");
buffer.appendAttribute("src", treeNode.getIcon());
buffer.closeTag();
}
}
/**
* Render the node's value.
* <p/>
* Subclasses should override this method to change the rendering of the
* node's value. By default the value will be rendered as a hyperlink,
* passing its <em>id</em> to the server.
*
* @param buffer string buffer containing the markup
* @param treeNode treeNode to render
*/
protected void renderValue(HtmlStringBuffer buffer, TreeNode treeNode) {
getSelectLink().setParameter(SELECT_TREE_NODE_PARAM, treeNode.getId());
if (treeNode.getValue() != null) {
getSelectLink().setLabel(treeNode.getValue().toString());
}
getSelectLink().render(buffer);
}
/**
* Query the specified treeNode and check which css class to apply.
* <p/>
* Possible classes are expanded, collapsed, leaf, expandedLastNode,
* collapsedLastNode and leafLastNode.
*
* @param treeNode the tree node to check for css class
* @return string specific css class to apply
*/
protected String getExpandClass(TreeNode treeNode) {
StringBuffer buffer = new StringBuffer();
if (isExpandedParent(treeNode)) {
buffer.append("expanded");
} else if (treeNode.getChildren().size() > 0) {
buffer.append("collapsed");
} else {
buffer.append("leaf");
}
if (treeNode.isLastChild()) {
buffer.append("LastNode");
}
return buffer.toString();
}
/**
* Query the specified treeNode and check which css class to apply for
* the icons.
* <p/>
* Possible classes are expandedIcon, collapsedIcon and leafIcon.
*
* @param treeNode the tree node to check for css class
* @return string specific css class to apply
*/
protected String getIconClass(TreeNode treeNode) {
if (isExpandedParent(treeNode)) {
return EXPAND_ICON;
} else if (!treeNode.isExpanded() && treeNode.hasChildren() || treeNode.isChildrenSupported()) {
return COLLAPSE_ICON;
} else {
return LEAF_ICON;
}
}
/**
* Helper method indicating if the specified node is both
* expanded and has at least 1 child node.
*
* @param treeNode specified node to check
* @return true if the specified node is both expanded and
* contains at least 1 child node
*/
protected boolean isExpandedParent(TreeNode treeNode) {
return (treeNode.isExpanded() && treeNode.hasChildren());
}
// -------------------------------------------- Protected observer behavior
/**
* Notifies all listeners currently registered with the tree, about any
* expand events.
*
* @param node specify the TreeNode that was expanded
* @param previousState contains the previous expanded state
*/
protected void fireNodeExpanded(TreeNode node, boolean previousState) {
for (Iterator it = listeners.iterator(); it.hasNext();) {
TreeListener l = (TreeListener) it.next();
l.nodeExpanded(this, node, getContext(), previousState);
}
}
/**
* Notifies all listeners currently registered with the tree, about any
* collapse events.
*
* @param node specific the TreeNode that was collapsed
* @param previousState contains the previous expanded state
*/
protected void fireNodeCollapsed(TreeNode node, boolean previousState) {
for (Iterator it = listeners.iterator(); it.hasNext();) {
TreeListener l = (TreeListener) it.next();
l.nodeCollapsed(this, node, getContext(), previousState);
}
}
/**
* Notifies all listeners currently registered with the tree, about any
* selection events.
*
* @param node specific the TreeNode that was selected
* @param previousState contains the previous selected state
*/
protected void fireNodeSelected(TreeNode node, boolean previousState) {
for (Iterator it = listeners.iterator(); it.hasNext();) {
TreeListener l = (TreeListener) it.next();
l.nodeSelected(this, node, getContext(), previousState);
}
}
/**
* Notifies all listeners currently registered with the tree, about any
* deselection events.
*
* @param node specific the TreeNode that was deselected
* @param previousState contains the previous selected state
*/
protected void fireNodeDeselected(TreeNode node, boolean previousState) {
for (Iterator it = listeners.iterator(); it.hasNext();) {
TreeListener l = (TreeListener) it.next();
l.nodeDeselected(this, node, getContext(), previousState);
}
}
// ----------------------------------------------------- Protected behavior
/**
* Sets the TreeNode expand state to the new value.
*
* @param node specifies the TreeNode which expand state will be set
* @param newValue specifies the new expand state
*/
protected void setExpandState(TreeNode node, boolean newValue) {
boolean oldValue = node.isExpanded();
node.setExpanded(newValue);
if (isNotifyListeners()) {
if (newValue) {
fireNodeExpanded(node, oldValue);
} else {
fireNodeCollapsed(node, oldValue);
}
}
}
/**
* Swaps the expand state of all TreeNodes with specified id's.
* Thus if a node's expand state is currently 'true', calling
* expandOrCollapse will set the expand state to 'false' and vice versa.
*
* @param ids array of node id's
*/
protected void expandOrCollapse(String[] ids) {
processNodes(ids, new Callback() {
public void callback(TreeNode node) {
setExpandState(node, !node.isExpanded());
}
});
}
/**
* Sets the expand state of the TreeNode with specified id to the new value.
*
* @param id specifies the id of a TreeNode which expand state will be set
* @param newValue specifies the new expand state
*/
protected void setExpandState(final String id, final boolean newValue) {
TreeNode node = find(id);
if (node == null) {
return;
}
setExpandState(node, newValue);
}
/**
* Sets the TreeNode expand state of each node in the specified collection
* to the new value.
*
* @param nodes specifies the collection of a TreeNodes which expand states will be set
* @param newValue specifies the new expand state
*/
protected void setExpandState(final Collection nodes, final boolean newValue) {
processNodes(nodes, new Callback() {
public void callback(TreeNode node) {
setExpandState(node, newValue);
}
});
}
/**
* Sets the TreeNode select state to the new value.
*
* @param node specifies the TreeNode which select state will be set
* @param newValue specifies the new select state
*/
protected void setSelectState(TreeNode node, boolean newValue) {
boolean oldValue = node.isSelected();
node.setSelected(newValue);
if (isNotifyListeners()) {
if (newValue) {
fireNodeSelected(node, oldValue);
} else {
fireNodeDeselected(node, oldValue);
}
}
}
/**
* Swaps the select state of all TreeNodes with specified id's to the new value.
* Thus if a node's select state is currently 'true', calling selectOrDeselect
* will set the select state to 'false' and vice versa.
*
* @param ids array of node id's
*/
protected void selectOrDeselect(String[] ids) {
processNodes(ids, new Callback() {
public void callback(TreeNode node) {
setSelectState(node, !node.isSelected());
}
});
}
/**
* Sets the select state of the TreeNode with specified id to the new value.
*
* @param id specifies the id of a TreeNode which select state will be set
* @param newValue specifies the new select state
*/
protected void setSelectState(final String id, final boolean newValue) {
TreeNode node = find(id);
if (node == null) {
return;
}
setSelectState(node, newValue);
}
/**
* Sets the TreeNode select state of each node in the specified collection
* to the new value.
*
* @param nodes specifies the collection of a TreeNodes which select states will be set
* @param newValue specifies the new select state
*/
protected void setSelectState(final Collection nodes, final boolean newValue) {
processNodes(nodes, new Callback() {
public void callback(TreeNode node) {
setSelectState(node, newValue);
}
});
}
/**
* Provides callback functionality for all the specified nodes.
*
* @param ids the array of nodes to process
* @param callback object on which callbacks are made
*/
protected void processNodes(String[] ids, Callback callback) {
if (ids == null) {
return;
}
for (int i = 0, n = ids.length; i < n; i++) {
String id = ids[i];
if (id == null || id.length() == 0) {
continue;
}
TreeNode node = find(id);
if (node == null) {
continue;
}
callback.callback(node);
}
}
/**
* Provides callback functionality for all the specified nodes.
*
* @param nodes the collection of nodes to process
* @param callback object on which callbacks are made
*/
protected void processNodes(Collection nodes, Callback callback) {
if (nodes == null) {
return;
}
for (Iterator it = nodes.iterator(); it.hasNext();) {
TreeNode node = (TreeNode) it.next();
callback.callback(node);
}
}
/**
* Finds and returns the first node that matches the id, starting the search
* from the specified node.
*
* @param node specifies at which node the search must start from
* @param id specifies the id of the TreeNode to find
* @return TreeNode the first node matching the id or null if no match was found.
*/
protected TreeNode find(TreeNode node, String id) {
for (Iterator it = iterator(node); it.hasNext();) {
TreeNode result = (TreeNode) it.next();
if (result.getId().equals(id)) {
return result;
}
}
return null;
}
/**
* Returns the value of the specified named parameter or a empty string
* <span class="st">""</span> if not found.
*
* @param name specifies the parameter to return
* @return the specified parameter or a empty string <span class="st">""</span> if not found
*/
protected String getRequestValue(String name) {
String result = getContext().getRequestParameter(name);
if (result != null) {
return result.trim();
} else {
return "";
}
}
/**
* Returns an array of all values of the specified named parameter or null
* if the parameter does not exist.
*
* @param name specifies the parameter to return
* @return all matching parameters or null if no parameter was found
*/
protected String[] getRequestValues(String name) {
String[] resultArray = getContext().getRequest().getParameterValues(name);
return resultArray;
}
/**
* Return an anchor <a> tag href attribute for the given parameters.
* This method will encode the URL with the session ID
* if required using <tt>HttpServletResponse.encodeURL()</tt>.
*
* @param parameters the href parameters
* @return the HTML href attribute
*/
protected String getHref(Map parameters) {
Context context = getContext();
String uri = ClickUtils.getRequestURI(context.getRequest());
HtmlStringBuffer buffer =
new HtmlStringBuffer(uri.length() + (parameters.size() * 20));
buffer.append(uri);
if (parameters != null && !parameters.isEmpty()) {
buffer.append("?");
Iterator i = parameters.entrySet().iterator();
while (i.hasNext()) {
Map.Entry entry = (Map.Entry) i.next();
String name = entry.getKey().toString();
String value = entry.getValue().toString();
buffer.append(name);
buffer.append("=");
buffer.append(ClickUtils.encodeUrl(value, context));
if (i.hasNext()) {
buffer.append("&");
}
}
}
return context.getResponse().encodeURL(buffer.toString());
}
// ------------------------------------------------ Package Private Methods
/**
* Expand / collapse and select / deselect the tree nodes.
*
* @return true to continue Page event processing or false otherwise
*/
boolean postProcess() {
if (isJavascriptEnabled()) {
// Populate the javascript handler with its state. This call will
// notify any tree listeners about new values.
javascriptHandler.init(getContext());
}
if (!ArrayUtils.isEmpty(expandOrCollapseNodeIds)) {
expandOrCollapse(expandOrCollapseNodeIds);
}
if (!ArrayUtils.isEmpty(selectOrDeselectNodeIds)) {
selectOrDeselect(selectOrDeselectNodeIds);
}
return true;
}
//----------------------------------------------------------- Inner classes
/**
* Iterate over all the nodes in the tree in a breadth first manner.
*
* <p/>Thus in a tree with the following nodes (top to bottom):
* <pre class="codeHtml">
* <span class="red">root</span>
* <span class="blue">node1</span> <span class="blue">node2</span>
*node1.1 node1.2 node2.1 node2.2
* </pre>
*
* <p/>the iterator will return the nodes in the following order:
* <pre class="codeHtml">
* <span class="red">root</span>
* <span class="blue">node1</span>
* <span class="blue">node2</span>
* node1.1
* node1.2
* node2.1
* node2.2
* </pre>
*/
static class BreadthTreeIterator implements Iterator {
/**queue for storing node's. */
private List queue = new ArrayList();
/** indicator to iterate collapsed node's. */
private boolean iterateCollapsedNodes = true;
/**
* Creates a iterator and adds the specified node to the queue.
* The specified node will be set as the root of the traversal.
*
* @param node node will be set as the root of the traversal.
*/
public BreadthTreeIterator(TreeNode node) {
if (node == null) {
throw new IllegalArgumentException("Node cannot be null");
}
queue.add(node);
}
/**
* Creates a iterator and adds the specified node to the queue.
* The specified node will be set as the root of the traversal.
*
* @param node node will be set as the root of the traversal.
* @param iterateCollapsedNodes indicator to iterate collapsed node's
*/
public BreadthTreeIterator(TreeNode node, boolean iterateCollapsedNodes) {
if (node == null) {
throw new IllegalArgumentException("Node cannot be null");
}
queue.add(node);
this.iterateCollapsedNodes = iterateCollapsedNodes;
}
/**
* Returns true if there are more nodes, false otherwise.
*
* @return boolean true if there are more nodes, false otherwise.
*/
public boolean hasNext() {
return !queue.isEmpty();
}
/**
* Returns the next node in the iteration.
*
* @return the next node in the iteration.
* @exception NoSuchElementException iteration has no more node.
*/
public Object next() {
try {
//remove from the end of queue
TreeNode node = (TreeNode) queue.remove(queue.size() - 1);
if (node.hasChildren()) {
if (iterateCollapsedNodes || node.isExpanded()) {
push(node.getChildren());
}
}
return node;
} catch (IndexOutOfBoundsException e) {
throw new NoSuchElementException("There is no more node's to iterate");
}
}
/**
* Remove operation is not supported.
*
* @exception UnsupportedOperationException <tt>remove</tt> operation is
* not supported by this Iterator.
*/
public void remove() {
throw new UnsupportedOperationException("remove operation is not supported.");
}
/**
* Pushes the specified list of node's to push on the beginning of the queue.
*
* @param children list of node's to push on the beginning of the queue
*/
private void push(List children) {
for (Iterator it = children.iterator(); it.hasNext();) {
queue.add(0, it.next()); //add to the beginning of queue
}
}
}
// ------------------------------------------------------- Private behavior
/**
* Returns whether the specified node is visible. The semantics of visible
* in this context indicates whether the node is currently displayed on the
* screen. This means all parent nodes must be expanded for the node to be
* visible.
*
* @param node TreeNode's visibility to check
* @return boolean true if the node's parent is visible, false otherwise
*/
private boolean isVisible(TreeNode node) {
while (!node.isRoot()) {
if (!node.getParent().isExpanded()) {
return false;
}
node = node.getParent();
}
return true;
}
/**
* Returns an array of all the nodes in the hierarchy, starting from the specified
* node up to and including the root node.
* <p/>
* The specified node will be at the start of the array and the root node will be
* at the end of the array. Thus array[0] will return the specified node, while
* array[n - 1] where n is the size of the array, will return the root node.
*
* @return list of all nodes from the specified node to the root node
*/
private TreeNode[] getPathToRoot(TreeNode treeNode) {
TreeNode[] nodes = new TreeNode[] {treeNode};
while (treeNode.getParent() != null) {
int length = nodes.length;
System.arraycopy(nodes, 0, nodes = new TreeNode[length + 1], 0, length);
nodes[length] = treeNode = treeNode.getParent();
}
return nodes;
}
// ------------------------------------------- Javascript specific behavior
/**
* Creates a new JavascriptHandler based on the specified policy.
*
* @param javascriptPolicy the current javascript policy
* @return newly created JavascriptHandler
*/
protected JavascriptHandler createJavascriptHandler(int javascriptPolicy) {
if (javascriptPolicy == JAVASCRIPT_SESSION_POLICY) {
return new SessionHandler(getContext());
} else {
return new CookieHandler(getContext());
}
}
/**
* Keep track of node id's, as they are selected, deselected,
* expanded and collapsed.
*
* @see JavascriptHandler
*/
protected transient JavascriptHandler javascriptHandler;
/**
* <b>Please note</b> this class is <b>not</b> meant for public use.
* <p/>
* Provides the contract for pluggable javascript renderers for
* the Tree.
*/
protected interface JavascriptRenderer {
/**
* Called to initialize the renderer.
*
* @param node the current node rendered
*/
void init(TreeNode node);
/**
* Called before a tree node is rendered. Enables the renderer
* to add attributes needed by javascript functionality for example
* something like:
* <pre class="codeJava">
* buffer.appendAttribute(<span class="st">"id"</span>,expandId);
* </pre>
* The code above adds a id attribute to the element, to enable
* the javascript code to lookup the html element by its id.
* <p/>
* The above attribute is appended to whichever element the
* tree is currently rendering at the time renderTreeNodeStart
* is called.
*
* @param buffer string buffer containing the markup
*/
void renderTreeNodeStart(HtmlStringBuffer buffer);
/**
* Called when the expand and collapse action is rendered. Enables
* the renderer to add attributes needed by javascript functionality
* for example something like:
* <pre class="codeJava">
* buffer.append(<span class="st">"onclick=\"handleNodeExpansion(this,event)\""</span>);
* </pre>
* The code above adds a javascript function call to the element.
* <p/>
* The code above is appended to whichever element the
* tree is currently rendering at the time renderTreeNodeStart
* is called.
*
* @param buffer string buffer containing the markup
*/
void renderExpandAndCollapseAction(HtmlStringBuffer buffer);
/**
* Called when the tree icon is rendered. Enables the renderer
* to add attributes needed by javascript functionality for example
* something like:
* <pre class="codeJava">
* buffer.appendAttribute(<span class="st">"id"</span>,iconId);
* </pre>
* The code above adds a id attribute to the element, to enable
* the javascript code to lookup the html element by its id.
* <p/>
* The above attribute is appended to whichever element the
* tree is currently rendering at the time renderTreeNodeStart
* is called.
*
* @param buffer string buffer containing the markup
*/
void renderIcon(HtmlStringBuffer buffer);
}
/**
* <b>Please note</b> this class is <b>not</b> meant for public use.
* <p/>
* Provides a abstract implementation of JavascriptRenderer that
* subclasses can extend from.
*/
protected abstract class AbstractJavascriptRenderer implements JavascriptRenderer {
/** holds the id of the expand html element. */
protected String expandId;
/** holds the id of the icon html element. */
protected String iconId;
/** holds the javascript call to expand or collapse the node. */
protected String nodeExpansionString;
/**
* @see #init(TreeNode)
*
* @param treeNode the current node rendered
* @see #init(TreeNode)
*/
public void init(TreeNode treeNode) {
expandId = buildString("e_", treeNode.getId(), "");
iconId = buildString("i_", treeNode.getId(), "");
}
/**
* @see #renderTreeNodeStart(HtmlStringBuffer)
*
* @param buffer string buffer containing the markup
*/
public void renderTreeNodeStart(HtmlStringBuffer buffer) {
//An id is needed on the element to do quick lookup using javascript
//document.getElementById(id)
buffer.appendAttribute("id", expandId);
}
/**
* @see #renderExpandAndCollapseAction(HtmlStringBuffer)
*
* @param buffer string buffer containing the markup
*/
public void renderExpandAndCollapseAction(HtmlStringBuffer buffer) {
getExpandLink().setAttribute("onclick", nodeExpansionString);
}
/**
* @see #renderIcon(HtmlStringBuffer)
*
* @param buffer string buffer containing the markup
*/
public void renderIcon(HtmlStringBuffer buffer) {
//An id is needed on the element to do quick lookup using javascript
//document.getElementById(id)
buffer.appendAttribute("id", iconId);
}
/**
* Builds a new string consisting of a prefix, infix and postfix.
*
* @param prefix the string to append at the start of new string
* @param infix the string to append in the middle of the new string
* @param postfix the string to append at the end of the new string
* @return the newly create string
*/
protected String buildString(String prefix, String infix, String postfix) {
StringBuffer buffer = new StringBuffer();
buffer.append(prefix).append(infix).append(postfix);
return buffer.toString();
}
}
/**
* <strong>Please note</strong> this class is only meant for
* developers of this control, not users.
* <p/>
* Provides the rendering needed when a {@link #JAVASCRIPT_COOKIE_POLICY}
* is in effect.
*/
protected class CookieRenderer extends AbstractJavascriptRenderer {
/** Name of the cookie holding the expanded nodes id's. */
private String expandedCookieName;
/** Name of the cookie holding the collapsed nodes id's. */
private String collapsedCookieName;
/**
* Default constructor.
*
* @param expandedCookieName name of the cookie holding expanded id's
* @param collapsedCookieName name of the cookie holding collapsed id's
*/
public CookieRenderer(String expandedCookieName, String collapsedCookieName) {
this.collapsedCookieName = collapsedCookieName;
this.expandedCookieName = expandedCookieName;
}
/**
*@see #init(TreeNode)
*
* @param treeNode the current node rendered
* @see #init(TreeNode)
*/
public void init(TreeNode treeNode) {
super.init(treeNode);
StringBuffer tmp = new StringBuffer();
tmp.append("handleNodeExpansion(this,event,'").append(expandId).append("','");
tmp.append(iconId).append("'); handleCookie(this,event,'").append(expandId).append("','");
tmp.append(treeNode.getId()).append("','");
tmp.append(expandedCookieName).append("','");
tmp.append(collapsedCookieName).append("'); return false;");
nodeExpansionString = tmp.toString();
}
}
/**
* <strong>Please note</strong> this class is only meant for
* developers of this control, not users.
* <p/>
* Provides the rendering needed when a {@link #JAVASCRIPT_SESSION_POLICY}
* is in effect.
*/
protected class SessionRenderer extends AbstractJavascriptRenderer {
/**
* @see #init(TreeNode)
*
* @param treeNode the current node rendered
* @see #init(TreeNode)
*/
public void init(TreeNode treeNode) {
super.init(treeNode);
String tmp = buildString("handleNodeExpansion(this,event,'", expandId, "','");
nodeExpansionString = buildString(tmp, iconId, "'); return false;");
}
}
/**
* <b>Please note</b> this class is <b>not</b> meant for public use.
* <p/>
* Provides the contract for pluggable javascript handlers.
* <p/>
* One of the main tasks the handler must perform is keeping track
* of which nodes changed state after the user interacted with the
* tree in the browser. This is also the reason why the handler
* extends {@link TreeListener} to be informed of any changes
* to node state via other means.
*/
protected interface JavascriptHandler extends TreeListener {
/**
* Initialize the handler state.
*
* @param context provides information for initializing
* the handler.
*/
void init(Context context);
/**
* Queries the handler if the specified node should be rendered
* as a expanded node.
* <p/>
* The reason for this is that the handler might be keeping track
* of state that the node is not aware of. For example certain state
* could be stored in the session or cookies.
*
* @param treeNode the specified node to query for
* @return true if the node should be rendered as if it was
* expanded, false otherwise
*/
boolean renderAsExpanded(TreeNode treeNode);
/**
* Called to indicate the user request cycle is complete.
* Any last minute tasks can be performed here.
*/
void destroy();
/**
* Returns the javascript renderer associated with
* this handler.
*
* @return renderer associated with this handler
*/
JavascriptRenderer getJavascriptRenderer();
}
/**
* <strong>Please note</strong> this class is only meant for
* developers of this control, not users.
* <p/>
* This class implements a cookie based javascript handler.
* Cookies in the browser tracks the expand and collapse state
* of the nodes. When a request is made to the server the cookies
* is processed and the state of the nodes are modified accordingly.
* <p/>
* There are two cookies used to track the state:
* <ul>
* <li>a cookie tracking the expanded node id's
* <li>a cookie tracking the collapsed node id's
* </ul>
* The cookies are removed between requests. New requests
* issue new cookies and update the state of the nodes
* accordingly.
* <p/>
* Note: This class is used in conjuction with cookie-helper.js
* which manipulates the cookie values in the browser as the
* user navigates the tree.
*/
protected class CookieHandler implements JavascriptHandler {
/** Cookie value delimiter. */
private final static String DELIM = ",";
/** Name of cookie responsible for tracking the expanded node id's. */
protected final String expandedCookieName = "expanded_" + getName();
/** Name of cookie responsible for tracking the expanded node id's. */
protected final String collapsedCookieName = "collapsed_" + getName();
/** Variable holding a javascript renderer. */
protected JavascriptRenderer javascriptRenderer;
/** Tracker for the expanded node id's. */
private Set expandTracker;
/** Tracker for the collapsed node id's. */
private Set collapsedTracker;
/** Value of the cookie responsible for tracking the expanded node id's. */
private String expandedNodeCookieValue = null;
/** Value of the cookie responsible for tracking the collapsed node id's. */
private String collapsedNodeCookieValue = null;
/**
* Creates and initializes a new CookieHandler.
*
* @param context provides access to the http request, and session
*/
protected CookieHandler(Context context) {
expandedNodeCookieValue = context.getCookieValue(expandedCookieName);
collapsedNodeCookieValue = context.getCookieValue(collapsedCookieName);
expandedNodeCookieValue = prepareCookieValue(expandedNodeCookieValue);
collapsedNodeCookieValue = prepareCookieValue(collapsedNodeCookieValue);
}
/**
* Initialize the handler state from the current cookies.
*
* @param context provides access to the http request, and session
*/
public void init(Context context) {
//If already initialized
if (expandTracker != null || collapsedTracker != null) {
return;
}
expandTracker = new HashSet();
collapsedTracker = new HashSet();
if (context == null) {
throw new IllegalArgumentException("context cannot be null");
}
//No cookie values to digest
if (expandedNodeCookieValue == null
&& collapsedNodeCookieValue == null) {
return;
}
//build hashes of id's for fast lookup
Set expandHash = asSet(expandedNodeCookieValue, DELIM);
Set collapsedHash = asSet(collapsedNodeCookieValue, DELIM);
for (Iterator it = iterator(getRootNode()); it.hasNext();) {
TreeNode currentNode = (TreeNode) it.next();
//If currentNode was expanded by user in browser
if (expandHash.contains(currentNode.getId())) {
//If currentNode's state is collapsed
if (!currentNode.isExpanded()) {
//Calling expand(currentNode) will update the expandTracker
//because the CookieHandler is a TreeListener as well.
expand(currentNode);
} else {
//If the currentNode is already expanded we should not update the
//expandTracker via a call to expand(currentNode), because
//other listeners of the tree will receive the event as well.
//Instead we update the expandTracker directly.
expandTracker.add(currentNode.getId());
}
} else if (collapsedHash.contains(currentNode.getId())) {
//If currentNode was collapsed by user in browser
if (currentNode.isExpanded()) {
//Calling collapse(currentNode) will update the expandTracker
//because the CookieHandler is a TreeListener as well.
collapse(currentNode);
}
}
}
}
/**
* Currently this implementation just calls
* {@link #isExpandedParent(TreeNode)}.
* <p/>
* CookieHandler uses cookies to sync any state change on
* the browser with the server, so the handler will not
* contain any state outside of the treeNode.
*
* @param treeNode the specified treeNode to check if it is part of the
* users selected paths
* @return true if the specified treeNode is part of the users selected
* path, false otherwise
* @see #renderAsExpanded(TreeNode)
*/
public boolean renderAsExpanded(TreeNode treeNode) {
return isExpandedParent(treeNode);
}
/**
* @see #destroy()
*/
public void destroy() {
//Remove expanded cookies. If the tree is changed
//new cookies will be generated.
setCookie(null, expandedCookieName);
//Remove collapsed cookie.
setCookie(null, collapsedCookieName);
}
/**
* @see #getJavascriptRenderer()
*
* @return currently installed javascript renderer
*/
public JavascriptRenderer getJavascriptRenderer() {
if (javascriptRenderer == null) {
javascriptRenderer = new CookieRenderer(expandedCookieName, collapsedCookieName);
}
return javascriptRenderer;
}
/**
* Adds the specified node to the cookie handler tracker.
*
* @see TreeListener#nodeExpanded(Tree, TreeNode, Context, boolean)
*
* @param tree tree the operation was made on
* @param node node that was expanded
* @param context provides access to {@link org.apache.click.Context}
* @param oldValue contains the previous value of expanded state
*/
public void nodeExpanded(Tree tree, TreeNode node, Context context, boolean oldValue) {
expandTracker.add(node.getId());
}
/**
* Removes the specified node from the cookie handler tracker.
*
* @see TreeListener#nodeCollapsed(Tree, TreeNode, Context, boolean)
*
* @param tree tree the operation was made on
* @param node node that was collapsed
* @param context provides access to {@link org.apache.click.Context}
* @param oldValue contains the previous value of selected state
*/
public void nodeCollapsed(Tree tree, TreeNode node, Context context, boolean oldValue) {
expandTracker.remove(node.getId());
}
/**
* @see TreeListener#nodeSelected(Tree, TreeNode, Context, boolean)
*
* @param tree tree the operation was made on
* @param node node that was selected
* @param context provides access to {@link org.apache.click.Context}
* @param oldValue contains the previous value of selected state
*/
public void nodeSelected(Tree tree, TreeNode node, Context context, boolean oldValue) {
/* noop */
}
/**
* @see TreeListener#nodeDeselected(Tree, TreeNode, Context, boolean)
*
* @param tree tree the operation was made on
* @param node node that was selected
* @param context provides access to {@link org.apache.click.Context}
* @param oldValue contains the previous value of selected state
*/
public void nodeDeselected(Tree tree, TreeNode node, Context context, boolean oldValue) {
/* noop */
}
/**
* Sets a cookie with the specified name and value to the
* http response.
*
* @param value the cookie's value
* @param name the cookie's name
*/
protected void setCookie(String value, String name) {
Context context = getContext();
if (value == null) {
ClickUtils.setCookie(context.getRequest(), context.getResponse(),
name, value, 0, "/");
} else {
ClickUtils.setCookie(context.getRequest(), context.getResponse(),
name, value, -1, "/");
}
}
/**
* Does some preparation on the cookie value like
* decoding and stripping of unneeded characters.
*
* @param value the cookie's value to prepare
* @return the prepared value
*/
protected String prepareCookieValue(String value) {
try {
if (StringUtils.isNotBlank(value)) {
value = URLDecoder.decode(value, "UTF-8");
value = trimStr(value, "\"");
}
} catch (UnsupportedEncodingException ignore) {
//ignore
}
return value;
}
/**
* Returns the specified string value as a set, tokenizing the
* string based on the specified delimiter.
*
* @param value value to return as set
* @param delim delimiter used to tokenize the value
* @return set of tokens
*/
protected Set asSet(String value, String delim) {
Set set = new HashSet();
if (value == null) {
return set;
}
StringTokenizer tokenizer = new StringTokenizer(value, delim);
while (tokenizer.hasMoreTokens()) {
String id = tokenizer.nextToken();
set.add(id);
}
return set;
}
}
/**
* <strong>Please note</strong> this class is only meant for developers of
* this control, not users.
* <p/>
* This class implements a session based javascript handler. It manages the
* client side javascript behavior by tracking the client's selected tree paths.
* <p/>
* <strong>The problem</strong>: when javascript is enabled, the entire
* tree must be sent to the browser to be navigatable without round trips
* to the server. However the tree should not be displayed in an expanded
* state so css is used to apply the 'display: none' idiom to 'collapse' the
* nodes even though they are really expanded.
* <p/>
* On the browser as the user expands and collapses nodes he/she will
* make selections and deselections of certain nodes. Since the node's value
* is rendered as a hyperlink, selecting or deselecting the node will
* create a request to the server.
* After the round trip to the server the tree will again be rendered in a
* collapsed state because the server will apply the css 'display :none' idiom
* before returning to the browser. It would be nice if instead of collapsing
* the entire tree again, to keep those tree paths that lead to selected nodes
* in a expanded state.
* <p/>
* <strong>The solution</strong>: SessionHandler keeps track of the
* <em>selected paths</em> and is queried at rendering time which nodes
* should be hidden and which nodes should be displayed. The
* <em>selected path</em> are all the node's from the selected node up to the
* root node.
* <p/>
* SessionHandler also keeps track of the <em>overlaid paths</em>.
* <em>Overlaid paths</em> comes from two or more selected paths that share
* certain common nodes. Overlaid paths are used in determining when a
* selected path can be removed from the tracker.
* To understand this better here is an example tree (top to bottom):
* <p/>
* <pre class="codeHtml">
* <span class="red">root</span>
* <span class="blue">node1</span> <span class="blue">node2</span>
* node1.1 node1.2 node2.1 node2.2
* </pre>
* IF node1 is selected, the <em>selected path</em> would include the nodes
* "root and node1". If node1.1 is then selected, the <em>selected path</em> would
* also include the nodes "root, node1 and node1.1". The same ff node1.2 is selected.
* The <em>overlaid path</em> would include the shared nodes of the three selected
* paths. Thus the overlaid path would consist of "root" because that node is shared by
* all three paths. Overlaid path will also contain "node1" because it is shared by node1.1
* and node1.2.
* <p/>
* <strong>The implementation</strong>: To keep memory storage to a minimum,
* only the hashcode of a node is stored, and it is stored only once for any node on
* any selected path. Thus if n nodes in a tree are selected there will only be n
* hashcodes stored.
* <p/>
* The overlaid paths are stored in a object consisting of a counter which increments
* and decrements each time a path is selected or deselected. The overlaid path also
* stores a boolean indicating if a a node is the last node on the path or not. The
* last node in the selected path should not be expanded because we do not want
* the children of the last node to be displayed. Lets look at our previous example
* again. The overlaid paths would contain the following information:
* <ul>
* <li>root: int = 3, lastNodeInPath = false</li>
* <li>node1: int = 3, lastNodeInPath = false</li>
* <li>node1.1: int = 1, lastNodeInPath = true</li>
* <li>node1.2: int = 1, lastNodeInPath = true</li>
* </ul>
* The overlaid paths indicates that the root node was found on three selected paths,
* and it is not the last node in the path. It cannot be the last node in the path
* because its child, node1 is also found on the selected path. node1 is also found
* on three selected paths (remember that node1 was itself selected) and is also
* not the last node in the path because both its children are found on selected paths
* as well. node1.1 is only found once on a selected path and because it does not
* have any children, it is the last node in the path. node1.2 is the same as node1.1.
* <p/>
* When a user deselects a node, each node on the selected path are decremented
* from the overlaid path counter. If a overlaid counter is reduced to 0, the
* selected path is also removed from storage. Also as nodes are removed, each node's
* lastNodeInPath indicator is updated to reflect if that node is the new last node
* on the path. For example if a user deselects node1.1 the overlaid path will look as
* follows:
* <ul>
* <li>root: int = 1, lastNodeInPath = false</li>
* <li>node1: int = 1, lastNodeInPath = false</li>
* <li>node1.1: int = 1, lastNodeInPath = true</li>
* </ul>
* Only 1 instance of root and node1 are left on the paths, but both are still not the
* last node in the path. Because node1.2 counter was reduced to 0, it was removed
* from the <em>selected path</em> storage as well.
* <p/>
* If the user deselects node1.1 overlaid path will look like this:
* <ul>
* <li>root: int = 1, lastNodeInPath = false</li>
* <li>node1: int = 1, lastNodeInPath = true</li>
* </ul>
* node1 is now the lastNodeInPath. node1.1 is also removed from the
* <em>selected path</em> storage.
* <p/>
* <strong>Note:</strong> this class stores information between requests
* in the javax.servlet.http.HttpSession as a attribute. The attributes prefix
* is <tt>js_path_handler_</tt> followed by the name of the tree
* {@link Tree#name}. If two tree's in the same session have the same name they
* will <strong>overwrite</strong> each others session attribute!
*/
protected class SessionHandler implements JavascriptHandler {
/**
* The reserved session key prefix for the selected paths
* <tt>js_path_handler_</tt>.
*/
private static final String JS_HANDLER_SESSION_KEY = "js_path_handler_";
/** Renders the needed javascript for this handler. */
protected JavascriptRenderer javascriptRenderer;
/**
* Map of id's of all nodes that are on route to a selected node in the tree.
* The value of the map is a Integer indicating the number of times the id
* have been added to the map. This helps keep track of the number of
* parallel paths.
*/
private Map selectTracker = null;
/**
* This class is dependant on {@link org.apache.click.Context}, so this
* constructor enforces a valid context before the handler can be
* used.
*
* @param context provides access to the http request, and session
*/
protected SessionHandler(Context context) {
if (context == null) {
throw new IllegalArgumentException("Context cannot be null");
}
init(context);
}
/**
* Retrieves the tracker from the http session if it exists. Otherwise
* it creates a new tracker and stores it in the http session.
*
* @param context provides access to the http request, and session
*/
public void init(Context context) {
//If already initialized
if (selectTracker != null) {
return;
}
if (context == null) {
throw new IllegalArgumentException("context cannot be null");
}
StringBuffer buffer = new StringBuffer(JS_HANDLER_SESSION_KEY).
append(getName());
String key = buffer.toString();
selectTracker = (Map) context.getSessionAttribute(key);
if (selectTracker == null) {
selectTracker = new HashMap();
context.setSessionAttribute(key, selectTracker);
}
}
/**
* Queries the handler if the specified node should be rendered
* as expanded or not.
*
* @param treeNode the specified node to check if it is expanded
* or not
* @return true if the specified node should be expanded, false
* otherwise
* @see #renderAsExpanded(TreeNode)
*/
public boolean renderAsExpanded(TreeNode treeNode) {
Entry entry = (Entry) selectTracker.get(treeNode.getId());
if (entry == null || entry.lastNodeInPath) {
return false;
}
return true;
}
/**
* @see #destroy()
*/
public void destroy() { /*noop*/ }
/**
* @see #getJavascriptRenderer()
*
* @return currently installed javascript renderer
*/
public JavascriptRenderer getJavascriptRenderer() {
if (javascriptRenderer == null) {
javascriptRenderer = new SessionRenderer();
}
return javascriptRenderer;
}
/**
* Adds all node's that are part of the <tt>selected path</tt> to
* the tracker.
*
* @see TreeListener#nodeSelected(Tree, TreeNode, Context, boolean)
*
* @param tree tree the operation was made on
* @param node node that was selected
* @param context provides access to {@link org.apache.click.Context}
* @param oldValue contains the previous value of selected state
*/
public void nodeSelected(Tree tree, TreeNode node, Context context,
boolean oldValue) {
//Check for duplicate path's. A duplicate path means that the user
//selected a node that is already stored in the handler's tracker map.
//This can only really happen when the tree is representated with
//checkboxes. Each time the form is submitted all the "checked"
//checkboxes are submitted but these node's might already have been
//submitted in a previous request. So here is a check against the
//previous value of the node to ensure it is newly selected.
if (oldValue) {
return;
}
TreeNode[] nodes = getPathToRoot(node);
//Loop all nodes and check if they should be added or incremented
for (int i = 0; i < nodes.length; i++) {
TreeNode currentNode = nodes[i];
if (i > 0 && !currentNode.isExpanded()) {
// If the node to expand is root and isRootNodeDisplayed is
// false, don't notify listeners
if (currentNode.isRoot() && !isRootNodeDisplayed()
&& isNotifyListeners()) {
setNotifyListeners(false);
expand(currentNode);
setNotifyListeners(true);
} else {
expand(currentNode);
}
}
String id = currentNode.getId();
Entry entry = (Entry) selectTracker.get(id);
//If node is not yet tracked, add it to the selected path tracker,
//otherwise increment the overlaid path count
if (entry == null) {
Entry newEntry = new Entry();
newEntry.lastNodeInPath = i == 0;
selectTracker.put(id, newEntry);
} else {
entry.lastNodeInPath = false;
entry.count++;
}
}
}
/**
* Removes all node's that are part of the <tt>selected path</tt> from
* the tracker.
*
* @see TreeListener#nodeDeselected(Tree, TreeNode, Context, boolean)
*
* @param tree tree the operation was made on
* @param node node that was deselected
* @param context provides access to {@link org.apache.click.Context}
* @param oldValue contains the previous value of selected state
*/
public void nodeDeselected(Tree tree, TreeNode node, Context context,
boolean oldValue) {
if (!oldValue) {
return;
}
TreeNode[] nodes = getPathToRoot(node);
//Loop all nodes and check if they should be removed or decremented
for (int i = 0; i < nodes.length; i++) {
TreeNode currentNode = nodes[i];
String id = currentNode.getId();
Entry entry = (Entry) selectTracker.get(id);
//This node is not tracked, so we continue looping.
if (entry == null) {
continue;
}
//If count > 1 currentNode has children
if (entry.count > 1) {
entry.count--;
//After decrement the currentNode might be the last node in the path.
//However if currentNode.isSelected == false, there is another sibling
//of the currentNode that is the last node.
if (entry.count == 1 && currentNode.isSelected()) {
entry.lastNodeInPath = true;
}
} else {
if (currentNode.isExpanded()) {
// If the node to collapse is root and isRootNodeDisplayed
// is false, don't notify listeners
if (currentNode.isRoot() && !isRootNodeDisplayed()
&& isNotifyListeners()) {
setNotifyListeners(false);
collapse(currentNode);
setNotifyListeners(true);
} else {
collapse(currentNode);
}
}
selectTracker.remove(id);
}
}
}
/**
* @see TreeListener#nodeExpanded(Tree, TreeNode, Context, boolean)
*
* @param tree tree the operation was made on
* @param node node that was expanded
* @param context provides access to {@link org.apache.click.Context}
* @param oldValue contains the previous value of expanded state
*/
public void nodeExpanded(Tree tree, TreeNode node, Context context,
boolean oldValue) { /*noop*/ }
/**
* @see TreeListener#nodeCollapsed(Tree, TreeNode, Context, boolean)
*
* @param tree tree the operation was made on
* @param node node that was collapsed
* @param context provides access to {@link org.apache.click.Context}
* @param oldValue contains the previous value of selected state
*/
public void nodeCollapsed(Tree tree, TreeNode node, Context context,
boolean oldValue) { /*noop*/ }
/**
* Provides debug information about the map storing the tracked paths.
*/
// private void dumpPathTracker() {
// System.out.println("--------------------------------------Printing Path Tracker map\n");
// if (selectTracker == null) {
// System.out.println("Path tracker is null");
// return;
// }
// for (Iterator it = selectTracker.keySet().iterator(); it.hasNext();) {
// String key = (String) it.next();
// System.out.println("ids -> [" + key + "] value count -> ["
// + ((Entry) selectTracker.get(key)).count + "] : last node -> ["
// + ((Entry) selectTracker.get(key)).lastNodeInPath + "]");
// }
// System.out.println("--------------------------------------Done");
// }
}
/**
* Holds information about a selected node's path entry. Each entry corresponds
* to a specific tree node. The node's id and corresponding entry is stored in a
* java.util.Map, where the node's id is the key and the entry is the value.
*/
private static final class Entry implements Serializable {
/** default serial version id. */
private static final long serialVersionUID = 1L;
/** Number of times this entry was found on the path. */
private int count = 1;
/**
* Indicates if this entry is the last node in the path. At rendering time this
* variable is checked, and if it is true, the css class "hide" is
* appended, so that the nodes below this entry is not shown.
*/
private boolean lastNodeInPath = false;
/**
* Returns a string representation of the Entry instance.
*
* @return a string representation
*/
public String toString() {
StringBuffer buffer = new StringBuffer("Entry value -> (").append(count).
append(")");
buffer.append(" lastNodeInPath -> (").append(lastNodeInPath).append(")");
return buffer.toString();
}
}
/**
* Javascript helper method that checks if the specified tree
* node should be hidden or not.
*
* @param specified tree node to check
* @return true if the node should be hidden, false otherwise
*/
private boolean shouldHideNode(TreeNode treeNode) {
if (isJavascriptEnabled() && !javascriptHandler.renderAsExpanded(treeNode)) {
return true;
}
return false;
}
/**
* Remove the specified toTrim string from the front and back
* of the specified str argument.
*
* @param str to trim
* @param toTrim the specified string to remove
* @return trimmed string
*/
private String trimStr(String str, String toTrim) {
if (StringUtils.isBlank(toTrim)) {
return str;
}
if (str.startsWith(toTrim)) {
str = str.substring(toTrim.length());
}
if (str.endsWith(toTrim)) {
str = str.substring(0, str.indexOf(toTrim));
}
return str;
}
}