/////////////////////////////////////////////////////////////////////////////
//
// Project ProjectForge Community Edition
// www.projectforge.org
//
// Copyright (C) 2001-2014 Kai Reinhard (k.reinhard@micromata.de)
//
// ProjectForge is dual-licensed.
//
// This community edition is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as published
// by the Free Software Foundation; version 3 of the License.
//
// This community edition is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
// Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, see http://www.gnu.org/licenses/.
//
/////////////////////////////////////////////////////////////////////////////
package org.projectforge.web.tree;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
/**
* A TreeTable is a really nice tree view which entries will be shown as HTML table rows. The functionality of this tree is like a file
* manager with tree view. It's much more than the source code looks like ;-) <br>
* Attention: This implementation of a tree isn't thread safe, because modification will be done only by one thread.
* @see TreeTableNode
*/
public abstract class TreeTable<T extends TreeTableNode> implements Serializable
{
private static final long serialVersionUID = -8178813483140004622L;
/** The root node. */
protected T root;
/**
* All opened nodes will be registered with their hashIds in an HashSet, so after reloading data the open node history should be restored.
*/
private Set<Serializable> openedNodes = new HashSet<Serializable>();
/** For faster finding containing nodes by hashId. */
protected Map<Serializable, T> allNodes = new HashMap<Serializable, T>();
/**
* @return True, if at least the root node is given.
*/
public boolean isInitialized()
{
return this.root != null;
}
/**
* Gets nodes as list. For the first time, only the top level childs will be shown. After the user is able to open and close some entries
* like in an file manager tree view.
*/
@SuppressWarnings("unchecked")
public List<T> getNodeList(TreeTableFilter<TreeTableNode> filter)
{
List<T> nodes = new ArrayList<T>();
if (root == null) {
return null;
}
SortedSet<T> childs = (SortedSet<T>) root.getChilds();
if (childs != null) {
for (T node : childs) {
node.buildNodeList((List<TreeTableNode>) nodes, 0, filter);
}
}
return nodes;
}
/**
* Gets all available nodes as list.
*/
public List<T> getAllNodes()
{
return getNodeList(null);
}
/**
* Called after user has clicked on folder / folder open icon.
* @param hashId The hashID of the node for which the event was released.
* @param eventKey If "open" then the node will be opened if already closed, otherwise the node will be opened if already closed.
* @return Found TreeNode by hashId, otherwise null.
*/
public TreeTableNode setOpenedStatusOfNode(String eventKey, Serializable hashId)
{
if ("explore".equals(eventKey) == true) {
return setOpenedStatusOfNode(TreeTableEvent.EXPLORE, hashId);
} else if ("implore".equals(eventKey) == true) {
return setOpenedStatusOfNode(TreeTableEvent.IMPLORE, hashId);
} else if ("open".equals(eventKey) == true) {
return setOpenedStatusOfNode(TreeTableEvent.OPEN, hashId);
} else {
return setOpenedStatusOfNode(TreeTableEvent.CLOSE, hashId);
}
}
/**
* Called after user has clicked on folder / folder open icon.
* @param hashId The hashID of the node for which the event was released.
* @param eventKey If "open" then the node will be opened if already closed, otherwise the node will be opened if already closed.
* @return Found TreeNode by hashId, otherwise null.
*/
public TreeTableNode setOpenedStatusOfNode(TreeTableEvent event, Serializable hashId)
{
T node = allNodes.get(hashId);
if (node == null) {
return null;
}
if (event == TreeTableEvent.EXPLORE) {
if (node.isOpened() == true) {
closeFoldersRecursive(node);
} else {
openFoldersRecursive(node);
}
} else if (event == TreeTableEvent.IMPLORE) {
if (node.hasChilds() == true)
closeFoldersRecursive(node);
} else if (event == TreeTableEvent.OPEN) {
node.setOpened(true);
openedNodes.add(node.hashId);
} else {
openedNodes.remove(node.hashId);
node.setOpened(false);
}
return node;
}
/**
* Opens the whole path to the given node (set open status of all ancestors to true).
* @param hashId
* @return
*/
public void openNode(Serializable hashId)
{
T node = allNodes.get(hashId);
if (node == null) {
return;
}
TreeTableNode parent = node.getParent();
while (parent != null) {
parent.setOpened(true);
parent = parent.getParent();
}
}
/**
* Get open nodes e. g. for persistent storing in the user's preferences.
* @return
*/
public Set<Serializable> getOpenNodes()
{
return this.openedNodes;
}
/**
* Set open nodes e. g. after restoring from the user's preferences.
* @param openNodes
*/
public void setOpenNodes(Set<Serializable> openNodes)
{
if (openNodes == null) {
this.openedNodes = new HashSet<Serializable>();
} else {
this.openedNodes = openNodes;
}
updateOpenStatus();
}
/**
* Update opened status of all tasks, given by openedNodes set.
*/
protected void updateOpenStatus()
{
for (TreeTableNode node : allNodes.values()) {
if (this.openedNodes.contains(node.getHashId()) == true) {
node.setOpened(true);
} else {
node.setOpened(false);
}
}
}
void openFoldersRecursive(TreeTableNode node)
{
node.setOpened(true);
openedNodes.add(node.hashId);
if (node.hasChilds() == true) {
node.getChilds();
for (TreeTableNode n : node.getChilds()) {
openFoldersRecursive(n);
}
}
}
void closeFoldersRecursive(TreeTableNode node)
{
node.setOpened(false);
openedNodes.remove(node.hashId);
if (node.hasChilds() == true) {
for (TreeTableNode n : node.getChilds()) {
closeFoldersRecursive(n);
}
}
}
/**
* Adds a new node by putting it into to the HashMap holding all nodes for faster finding by hashMap.
*/
protected void addTreeTableNode(T node)
{
TreeTableNode parent = node.getParent();
if (parent != null) {
parent.addChild(node);
} else {
root.addChild(node);
}
allNodes.put(node.hashId, node);
}
protected T getNode(Serializable hashId)
{
return allNodes.get(hashId);
}
/**
* Does the node specified by given hashID is an opened node (folder)? (Tests the existing of the given node in the HashMap of all opened
* nodes.)
* @param hashId The hashID of the node to examine.
* @return True, if the specified node (folder) is open, otherwise false.
*/
boolean containsOpenedNode(Serializable hashId)
{
return openedNodes.contains(hashId);
}
/**
* Helper method for getting the element after the given node (referenced by id) in the given node list which is not a child node!. This
* can be the next sibling, the parent's successor or null if the node is the last node in the tree table. <br/>
* This method is used by the Ajax implementations of the trees for manipulating the DOM tree.
* @param hashId
*/
public T getElementAfter(final List<T> nodeList, final Serializable hashId)
{
final Iterator<T> it = nodeList.iterator();
TreeTableNode node = null;
// Find node:
while (it.hasNext() == true) {
node = it.next();
if (node.getHashId().equals(hashId) == true) {
break;
}
}
if (node == null) {
return null;
}
// Find successive node which is not child of current node:
while (it.hasNext() == true) {
final T suc = it.next();
if (node.isParentOf(suc) == false) {
return suc;
}
}
return null;
}
/**
* Helper method.
* @param nodeList
* @param treeTableNode
*/
public List<T> getDescendants(final List<T> nodeList, final T treeTableNode)
{
final List<T> result = new ArrayList<T>();
boolean before = true;
for (final T entry : nodeList) {
if (before == true) {
if (entry.getHashId().equals(treeTableNode.getHashId()) == true) {
before = false;
}
} else {
if (treeTableNode.isParentOf(entry) == true) {
result.add(entry);
} else {
break; // End of descendants.
}
}
}
return result;
}
}