Using JXTreeTable With Custom Models to Display Hierarchical Data

[ad_1]

Introduction

The JXTreeTable is a swingx component that builds on the JTreeTable component (see Resources(1). It adds features like highlighters, filters etc, and numerous enhancements to the JTreeTable component. The JXTreeTable component allows you to display hierarchical data in one of the columns of the table usually the first one. Hence it can be used to display hierarchical data of any type such XML, your filesystem structure, organization chart etc.

First steps:

How do you go about making use of the JXTreeTable component in your application that displays a view of the filesystem? The answer is simple. First you subclass JXTreeTable to create a JFilesystemTreeTable class and provide it with a constructor that takes a FileSystemTreeModel as shown in Listing 1:

public class JFileSystemTreeTable extends JXTreeTable {

this(model, false);

}

private JFileSystemTreeTable(XmlTreeModel treeModel,

boolean redun) {

this.renderer = new

FileSystemTreeTableCellRenderer(treeModel);

treeTableAdapter = new

SimpleTreeModelAdapter(treeModel, renderer,

this);

super.setModel(treeTableAdapter);

}

Listing 1

The above call to ‘this’ with a boolean argument is a hack that is needed to get around errors like ‘super should be the first line in a constructor’ and ‘call to this should be the first line in a constructor’.

First we call super() to initialize the JXTable and JXTreeTable instances and then call setModel() on super again once we have created the renderer and adapter objects. The call to super() is wasteful as it sets in motion an entire sequence of operation inside the JXTable and JTable classes. This could be avoided by calling other constructors in the same class and in the super class as follows:

private JXmlTreeTable(XmlTreeModel treeModel,

boolean redun) {

super(new XmlTreeTableCellRenderer(treeModel));

}

But this method of calling does not allow us to retain a reference to the renderer and adapter objects which we will be needing later.

Next you proceed to fill in the 2-argument constructor with initialization code as in Listing 2.

Swingx Refactoring

// renderer-related initialization — also called from setTreeTableModel()

init(renderer); // private method

initActions();

initActionsAndBindings();

// No grid.

setShowGrid(false); // superclass default is “true”

setRowHeight(50);

Listing 2:

All the 3 methods beginning with ‘init’ are defined in either JXTreeTable or JXTable and marked as private. These have to me changed into protected before getting our application to work. The remaining 2 lines are fairly straightforward and are taken straight from the base class.

The renderer class is written as shown in Listing 3. The problem is that our renderer extends the renderer written inside JXTreeTable that is not public or protected. So JXTreeTable has to be first modified before we can use it with our custom model. The same argument also applies for the model adapter. The SimpleTreeModelAdapter extends JXTreeTable.TreeTableModelAdapter. The modified JXTreeTable is placed in a package of its own that looks like com.bonanzasoft.org.jesktop.swingx.JXTreeTable in my environment but you can name it anyway you want. I however recommend that you don’t change the part after the org to keep it in sync with the swingx project.

protected class FileSystemTreeTableCellRenderer extends JXTreeTable.TreeTableCellRenderer {

public FileSystemTreeTableCellRenderer(FileSystemTreeModel

fileSystemModel) {

super(xmlModel);

}

}

public static class TreeTableModelAdapter extends AbstractTableModel {

public TreeTableModelAdapter(TreeTableModel model, JTree tree) {

Listing 3

The class is public because the class I have extended from it is in another package. If you plan to keep all your files together in the same package, the class can remain protected.

Programming to an Interface

The interface using which I will be calling the SimpleTreeModel methods is shown in Listing 4.

public interface FileSystemTreeModel extends TreeTableModel {

public ArrayList getParentList();

public Document getDocument();

public TreeNode[] getPathToRoot(TreeNode en);

public void setRowMapper(HashMap mapper);

public void setParentList(ArrayList plist);

public Object getRoot();

public boolean getRootVisible();

public HashMap getRowMapper();

public Object getValueAt(Object node, int row, int col);

public boolean isCellEditable(Object node, int row, int col);

}

Listing 4

Now let’s focus on the methods in the FileSystemTreeModel inteface and their uses. We have already seen the use of methods getRowMapper and getParentList and now we will see what the other methods (Listing 7) are for.

Public void setRowMapper

Public void setParentList

public Object getRoot();

public boolean getRootVisible();

public Object getValueAt(Object node, int row, int col);

public boolean isCellEditable(Object node, int row, int col);

Listing 7

The function of the methods setParentList and setRowMapper should be evident easily. They are the setter methods corresponding to the getters we saw earlier. The method getRoot is useful if you’d like to retrieve the root of your tree in your treetable. Similarly, the method getRootVisible is needed if you’d like to set whether the root node is visible. Of importance are the getValueAt() and isCellEditable() methods which s you can see have been modified to pass the row value as well to the SimpleTreeModel. This is what enables the JTable to fetch the value to be set in each table cell by calling getValueAt on the model. The implementation of getValueAt is very similar to that of getCellRenderer. Only instead of returning a renderer, the node value should be returned.

The solution

With this interface in place, we will see how the getValueAt method can be implemented trivially in the SimpleTreeModel and called by the swing framework automatically as shown in Listing 5:

public Object getValueAt(Object node, int column) {

if(column == 0) {

FileNode fileNode = (FileNode) node;

/* tableData is a global ArrayList that stores the data for the remainder of the columns when the first column is passed to this method.care should be taken to reinitialize this arraylist for each row */

tableData = new ArrayList(MAXCOLS)

// where MAXCOLS is the number of columns your

// treetable would contain.

tableData.set(1, fileNode.getSize());

tableData.set(2, fileNode.getLastModified());

tableData.set(3, fileNode.getType());

return fileNode.getName();

} else {

return tableData.get(column);

}

}

….

}

Listing 5

The other important methods in SimpleTreeModel are fairly straightforward and these include getColumnName, getColumnClass, isLeaf and so on.

You might ask what is the purpose of the FileSystemTreeModel interface since we have not used it anywhere. This interface becomes useful as soon as your requirement becomes slightly more complex. What if instead of a filesystem you want to display employee records in a treetable? For each employee, you want to display his demographic data like address, phone etc and also display his perks if any. Employee Joe who is a staff may not have any perks whereas Peter who is a vice-president may have perks like car, house etc. To distinguish the perks from the demographic information, you may want to use different renderer for a perk than say a renderer for a phone number. Since we don’t know beforehand how many perks an employee has, we cannot set the renderer at the column level and hope things will be fine.

Implementing Custom renderers

So how can we overcome this problem? It so happens that the facility is there already in JTable and we just override it with our own implementation. The facility Iam talking about is the method getCellRenderer(int row, int column) available in the JTable class. As you can see this method takes both a row and a column as parameter as opposed to getColumnClass(int column) which accepts only a column number a sparameter. We can now override getCellRenderer method as shown in Listing 6:

public TableCellRenderer getCellRenderer(int row, int column) {

HashMap rowMappper = fileSystemTreeModel.getRowMapper();

ArrayList parentList = fileSystemTreeModel.getParentList();

String rcp = rowMappper.get(row);

String [] rowcol = rcp.split(“,”);

int r = Integer.parseInt(rowcol[0]);

int c = Integer.parseInt(rowcol[1]);

int p = Integer.parseInt(rowcol[2]);

EmployeeNode empNode = ((EmployeeNode) parentList.get(ppx));

if(row == r ) {

if(column == c) {

if(empNode.getNodeType() == EMPCONSTANTS.PERK_NODE)

return perkRenderer;

else if(empNode.getNodeType() ==

EMPCONSTANTS.EMPLOYEE_NODE)

return TreeTableCellRenderer;

else if(empNode.getNodeType() ==

EMPCONSTANTS.DEMOGRAPHIC_NODE)

return demographicRenderer;

}

}

}

Listing 6

Now you can see that our FileSystemTreeModel proves useful. Without it, your treetable would have to know about the SimpleTreeModel that is not required now. Also, in the code above, perkRenderer, treeTableCellRenderer and demographicRenderer are custom renderers defined elsewhere and not included here. We have already seen how the FileSystemTreeTableCellRenderer class looks like in our filesystem example. You can design the PerkRenderer and DemographicRenderer classes any way you want. For example, you may want to display ‘bank note’ icon for a perk renderer and a ‘contact’ icon for demographic renderer.

Getting the model ready

There’s still one important problem that I have not discussed which is the creation of the rowMapper and parentList. As you can see, this is being used by both the getValueAt method of the SimpleTreeModel and the getCellRenderer of the JTable. The JTable calls the methods in order first calling getValueAt and then calling getCellRenderer but it is not good design to rely upon this fact. The proper way to do this is to create a routine and then call this routine before either of these methods gets called. Listing 8 shows the routine the discussion of which I will defer till later when I would have explained some other stuff that needs to be explained.

private void buildColumnMapping(EmployeeNode root, int currCol, String src) {

nodeMapTbl.put(root, currRow + “,” + currCol);

SelectiveBreadthFirstEnumeration benumer = new

SelectiveBreadthFirstEnumeration(root);

while (benumer.hasMoreElements())

{

currCol = 0;

EmployeeNode inode = (EmployeeNode) benumer.nextElement();

if(inode.getRole().equals(“Manager”)) {

nodeMapTbl.put(inode, currRow + “,” + currCol);

Node empNode = inode.getDomNode();

int h = 0;

NamedNodeMap perks = empNode.getAttributes();

for(int j=0;j Node perkNode = perks.get(j);

EmployeeNode anode = new EmployeeNode (perkNode);

nodeMapTbl.put(anode, currRow + “,” + (currCol+h+1));

}

if(inode.hasSubordinates()) {

NodeList subordinates = empInfoNode. getChildNodes();

int w=0;

for(int u=0;u EmpInfoNode tc = Subordinates.get(u);

EmployeeNode subNode = new EmployeeNode (tc);

nodeMapTbl.put(subNode, currRow + “,” + currCol +h+u+1));

w++;

}

}

currRow++;

}

}

}

Listing 8

The finished product

There a few important steps we need to perform if we are to get our treetable right. First, let’s go with our Employee example for this article. Then we will see how to model that for use with our treetable. To this end, I will assume that some employees are managers and that some managers have staff while others don’t. Similarly some employees have perks while others don’t. The final result once rendered in our JEmployeeTreeTable will look like that shown in Fig. 1:

Figure 1

Let’s assume that the employee records are residing in a database and we are using the database vendor’s native support for returning results as XML. We have thus obtained the employee details as XML, which I then parse and convert into a DOM.tree. In order to use the DOM tree as a JTree model, I then do as shown in Listing 9:

protected ExaltoXmlNode createTreeNode(Node root) {

EmployeeNode treeNode = new EmployeeNode (root);

NodeList list = root.getChildNodes();

for (int k=0; k Node nd = list.item(k);

if(nd.getNodeType() != 3) {

EmployeeNode child = createTreeNode(nd);

if (child != null)

treeNode.add(child);

}

}

return treeNode;

}

Listing 9

The createTreeNode is initially called with the root of your DOM tree after which it gets called recursively for each employee node who is a manager thus traversing the DOM tree and building a parallel tree of DefaultMutableTreeNode objects. For convenience sake, the DOM tree nodes are wrapped into instances of EmployeeNode, which provide some convenience methods over DefaultMutableTreeNode.

The next step is to call the routine shown earlier in Listing 8 and pass the root of the EmployeeNode tree we have constructed above as argument. This routine returns a HashMap where the keys are EmployeeNodes and the value is the row and column in which the node should appear. Finally, before we can use this HashMap, it needs to be split into a HashMap and an ArrayList that would be our rowMapper and parentList objects.

Conclusion

To conclude, the ability to set the renderer for each cell of a table has always been there as there has been the facility to display hierarchical data using treetables. What we have achieved here is the porting of JTreeTable to use JXTreeTable component of the swingx project and also embellish our display with the use of custom renderers. Finally, this article would not have been possible without the JTreeTable originally developed by Scott Violet and co. at SUN(tm), Richard Bair, the tech lead for the swingx project and Jeannette Winzberg, the creator of the JXTreeTable and JXTable components.

[ad_2]

Source by Omprakash Visvanathan

admin