2007/04/04

MyFaces Tomahawk: Tree2 Component Improvement

Summary

MyFaces Tomahawk's Tree2 is a great JSF component to display hierarchic data, such as directory structure or sitemap. When used with dynamic data, however, its default implementation has some issues with tree state as described in TOMAHAWK-244 and TOMAHAWK-296. In this article, I will solve the issues by providing custom implementations of TreeModel and TreeWalker interfaces.

Scope

This article focuses on usage of tree2 component with the following attributes:
  • clientSideToggle="true"
  • preserveToggle="true"
Tested with MyFaces Core 1.1.3 and MyFaces Tomahawk 1.1.3. For server side toggle, no test has been done.

Problem Description

Tree2 component stores tree state (if each node is expanded or collapsed) in index-based node naming schema such that:
  • root node is named "0"
  • first child is named "0:0"
  • second child is named "0:1"
  • third child of second child is named "0:1:2"
This simple schema does not work as expected when tree data is dynamic. For example, in the diagram below, a root node has two nodes A and B, and node B is expanded. This tree state is described as "node 0:1 is expanded."
  • ROOT
  • + A
  • - B
  • + B1
  • + B2
Let's look at the situation when node C is inserted as a first child of ROOT. Then what you would expect is:
  • ROOT
  • + C
  • + A
  • - B
  • + B1
  • + B2
However, this is what really happens. Because tree state is stored as "node 0:1 is expaned," in this case what is expanded is node A:
  • ROOT
  • + C
  • - A
  • + A1
  • + A2
  • + B
Because tree state is index-based, when data is inserted/deleted/moved/sorted, tree state becomes unfavorable.

Solution Description

Good news is, Tree2 component has a mechanism to solve this problem by supplying custom naming schema implementation. Bad news is, there is no working example or good documentation on the Internet (as far as I know.) So, I have done some investigation and created one!
Idea is to use naming schema based on node identifier, as suggested in TOMAHAWK-244. In the above example, "node 0:1 is expanded" is instead described as "node ROOT:B is expanded" or "node 0:200 is expanded" if ROOT's identifier is 0 and B's identifier is 200 (I think this is typical case where record ids are integers generated by database sequence.)

Implementation

The following two files are the implementation of the solution.
  • CustomTreeModel.java
  • CustomTreeWalker.java
Naming schema can be customized by implementating TreeWalker interface, but besides that, a custom implementation of TreeModel is needed.
The code uses Java5 generics syntax, but you can remove them if you are using Java1.4.
CustomTreeModel.java
package example;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.StringTokenizer;

import org.apache.myfaces.custom.tree2.TreeModel;
import org.apache.myfaces.custom.tree2.TreeNode;
import org.apache.myfaces.custom.tree2.TreeState;
import org.apache.myfaces.custom.tree2.TreeStateBase;
import org.apache.myfaces.custom.tree2.TreeWalker;

public class CustomTreeModel implements TreeModel {

    private TreeNode rootTreeNode;
    private TreeState treeState;

    public CustomTreeModel(TreeNode rootTreeNode) {
        this.rootTreeNode = rootTreeNode;
        this.treeState = new TreeStateBase();
    }

    //

    public String[] getPathInformation(String nodeId) {
        if (nodeId == null) {
            throw new IllegalArgumentException("Cannot determine path for a null node.");
        }

        ArrayList pathList = new ArrayList();
        pathList.add(nodeId);
        
        while (nodeId.lastIndexOf(SEPARATOR) != -1) {
            nodeId = nodeId.substring(0, nodeId.lastIndexOf(SEPARATOR));
            pathList.add(nodeId);
        }

        Collections.reverse(pathList);

        return pathList.toArray(new String[0]);
    }

    public boolean isLastChild(String nodeId) {
        if (nodeId.lastIndexOf(SEPARATOR) == -1) { // return true for root node
            return true;
        }

        String parentNodeId = nodeId.substring(0, nodeId.lastIndexOf(SEPARATOR));
        String childIdentifier = nodeId.substring(nodeId.lastIndexOf(SEPARATOR) + 1);

        TreeNode parentNode = getNodeById(parentNodeId);
        TreeNode lastChildNode = (TreeNode)parentNode.getChildren().get(parentNode.getChildCount() - 1);

        return lastChildNode.getIdentifier().equals(childIdentifier);
    }

    public TreeNode getNodeById(String nodeId) {
        if (nodeId == null) return null;

        StringTokenizer st = new StringTokenizer(nodeId, SEPARATOR);

        // check first node identifier equals root node identifier
        if (!rootTreeNode.getIdentifier().equals(st.nextElement())) {
            throw new IllegalArgumentException("Cannot find a root node.");
        }
        
        TreeNode node = rootTreeNode;
        while (st.hasMoreTokens()) {
            String nodeIdentifier = st.nextToken();
            boolean found = false;
            for (TreeNode childNode : (List)node.getChildren()) {
                if (childNode.getIdentifier().equals(nodeIdentifier)) {
                    node = childNode;
                    found = true;
                    break;
                }
            }
            if (!found) {
                throw new IllegalArgumentException("Cannot find a node. (" + nodeId + ")");
            }
        }

        return node;
    }

    public void setTreeState(TreeState treeState) {
        this.treeState = treeState;
    }

    public TreeState getTreeState() {
        return treeState;
    }

    public TreeWalker getTreeWalker() {
        return new CustomTreeWalker();
    }

    //
    
    public TreeNode getRootTreeNode() {
        return rootTreeNode;
    }

}
To use a custom TreeWalker implementation, getTreeWalker() method of TreeModel must be implemented to return it. I first tried just to override getTreeWalker() method of TreeModelBase (the default implementation of TreeModel,) but I found it not possible because part of the code depends of index-based naming schema. So I created a new CustomTreeModel class. getRootTreeNode() is added so that CustomTreeWalker can get access to TreeNode's.
CustomTreeWalker.java
package example;

import java.util.List;
import java.util.Stack;

import org.apache.commons.lang.StringUtils;
import org.apache.myfaces.custom.tree2.TreeModel;
import org.apache.myfaces.custom.tree2.TreeNode;
import org.apache.myfaces.custom.tree2.TreeWalker;
import org.apache.myfaces.custom.tree2.UITreeData;

public class CustomTreeWalker implements TreeWalker {

    private static final String SEPARATOR = TreeModel.SEPARATOR;

    private UITreeData treeData;
    private Stack idStack = new Stack();
    private Stack nodeStack = new Stack();
    private boolean checkState = true;
    private boolean startedWalking = false;

    //

    public boolean isCheckState() {
        return checkState;
    }

    public void setCheckState(boolean checkState) {
        this.checkState = checkState;
    }

    public boolean next() {
        if (!startedWalking) { // first step of walking : start from the root
            CustomTreeModel treeModel = (CustomTreeModel)treeData.getDataModel();
            TreeNode rootTreeNode = treeModel.getRootTreeNode();
            treeData.setNodeId(rootTreeNode.getIdentifier());
            idStack.push(rootTreeNode.getIdentifier());
            nodeStack.push(rootTreeNode);

            startedWalking = true;
            return true;
        }

        if (nodeStack.isEmpty()) { // if stack is empty, end the walking
            return false;
        }

        String prevNodeId = idStack.peek();
        TreeNode prevNode = nodeStack.peek();

        if (prevNode.isLeaf()) { // if previous stacked node is a leaf, go up the hierarchy
            idStack.pop();
            nodeStack.pop();
            return next();

        } else {

            String nextNodeId = null;
            TreeNode nextNode = null;

            if (prevNodeId.equals(treeData.getNodeId())) { // going down the hierarchy

                if (checkState) { // if checkState is true, check if the node is expanded and skip it if not.
                    if (!treeData.getDataModel().getTreeState().isNodeExpanded(prevNodeId)) {
                        idStack.pop();
                        nodeStack.pop();
                        return next();
                    }
                }

                nextNode = (TreeNode)prevNode.getChildren().get(0);
                nextNodeId = prevNodeId + SEPARATOR + nextNode.getIdentifier();

            } else { // walk on the same level

                String currentNodeId = treeData.getNodeId();
                String currentNodeIdentifier = treeData.getNode().getIdentifier();
                String parentNodeId = StringUtils.substringBeforeLast(currentNodeId, SEPARATOR);
                TreeNode parentNode = treeData.getDataModel().getNodeById(parentNodeId);

                if (treeData.getDataModel().isLastChild(currentNodeId)) { // go up the hierarchy if last child
                    treeData.setNodeId(parentNodeId);
                    idStack.pop();
                    nodeStack.pop();
                    return next();
                }

                boolean nextIsNext = false;
                for (TreeNode childNode : (List)parentNode.getChildren()) { // find next child
                    if (nextIsNext) {
                        nextNode = childNode;
                        nextNodeId = parentNodeId + SEPARATOR + nextNode.getIdentifier();
                        break;
                    } else if (childNode.getIdentifier().equals(currentNodeIdentifier)) {
                        nextIsNext = true;
                    }
                }

            }

            treeData.setNodeId(nextNodeId);
            idStack.push(nextNodeId);
            nodeStack.push(nextNode);

            return true;
        }
    }

    public String getRootNodeId() {
        return ((CustomTreeModel)treeData.getDataModel()).getRootTreeNode().getIdentifier();
    }

    public void setTree(UITreeData treeData) {
        this.treeData = treeData;
    }

    public void reset() {
        idStack.empty();
        nodeStack.empty();
        startedWalking = false;
    }

}
CustomTreeWalker implementation uses naming schema based on node identifier, separated by colon(:) so it assumes that node identifier does not include any colon. next() method is the main method which walks through the tree nodes the same way as the default TreeWalkerBase does.

Download

No download available, so just copy the code above.

Copyright?

Please use them for free!
posted by apptaro at 11:58 | Comment(3) | TrackBack(0) | JSF
Comments
Doesn´t this:
nextNode = (TreeNode)prevNode.getChildren().get(0);

in the TreeWalker result in only showing the first child of the root?

because of the hard coded 0?
Posted by Momo at 2007/07/10 18:52
Sorry I was too fast, this is only for initialising the first node if current node is the root. So forget the last comment ;-)
Posted by Momo at 2007/07/10 20:20
thanks for your solution
its perfect but after i insert many node as childs of any node its done then i change to another node i can not insert.
i debug it and i find that the action method to insert is not called .
can you help me to solve my problem?
Posted by moj at 2007/12/20 17:58
Write Comments
Name: [Required]

Email:

HP:

Comments: [Required]

Code: [Required]


*Input characters in the image
Trackback URL
http://blog.seesaa.jp/tb/37718310
Trackback without referring link will not be accepted.

Trackbacks
×

この広告は1年以上新しい記事の投稿がないブログに表示されております。