Displaying a DOM Hierarchy
To create or manipulate a DOM, it helps to have a clear idea of how the nodes in a DOM are structured. In this section of the tutorial, you'll expose the internal structure of a DOM.
At this point you need a way to expose the nodes in a DOM so that you can see what it contains. To do that, you'll convert a DOM into a
JTreeModel
and display the full DOM in aJTree
. It takes a bit of work, but the end result will be a diagnostic tool you can use in the future, as well as something you can use to learn about DOM structure now.
Note: In this section, we build a Swing GUI that can display a DOM. The code is in
DomEcho02.java
. If you have no interest in the Swing details, you can skip ahead to Examining the Structure of a DOM and copyDomEcho02.java
to proceed from there. (But be sure to look at Table 6-1, Node Types.)
Convert DomEcho to a GUI Application
Because the DOM is a tree and because the Swing
JTree
component is all about displaying trees, it makes sense to stuff the DOM into aJTree
so that you can look at it. The first step is to hack up the DomEcho program so that it becomes a GUI application.Add Import Statements
Start by importing the GUI components you'll need to set up the application and display a
JTree
:// GUI components and layoutsimport javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTree;
Later, you'll tailor the DOM display to generate a user-friendly version of the
JTree
display. When the user selects an element in that tree, you'll display subelements in an adjacent editor pane. So while you're doing the setup work here, import the components you need to set up a divided view (JSplitPane
) and to display the text of the subelements (JEditorPane
):Next, add a few support classes you'll need to get this thing off the ground:
// GUI support classesimport java.awt.BorderLayout; import java.awt.Dimension; import java.awt.Toolkit; import java.awt.event.WindowEvent; import java.awt.event.WindowAdapter;
And, import some classes to make a fancy border:
// For creating bordersimport javax.swing.border.EmptyBorder; import javax.swing.border.BevelBorder; import javax.swing.border.CompoundBorder;
(These are optional. You can skip them and the code that depends on them if you want to simplify things.)
Create the GUI Framework
The next step is to convert the application into a GUI application. To do that, you make the static
main
method create an instance of the class, which will have become a GUI pane.Start by converting the class into a GUI pane by extending the Swing
JPanel
class:public class DomEcho02extends JPanel
{ // Global value so it can be ref'd by the tree adapter static Document document; ...While you're there, define a few constants you'll use to control window sizes:
public class DomEcho02 extends JPanel { // Global value so it can be ref'd by the tree adapter static Document document;static final int windowHeight = 460; static final int leftWidth = 300; static final int rightWidth = 340; static final int windowWidth = leftWidth + rightWidth;
Now, in the main method, invoke a method that will create the outer frame that the GUI pane will sit in:
public static void main(String argv[]) { ... DocumentBuilderFactory factory ... try { DocumentBuilder builder = factory.newDocumentBuilder(); document = builder.parse( new File(argv[0]) );makeFrame();
} catch (SAXParseException spe) { ...Next, you'll define the
makeFrame
method itself. It contains the standard code to create a frame, handle the exit condition gracefully, give it an instance of the main panel, size it, locate it on the screen, and make it visible:... } // mainpublic static void makeFrame() { // Set up a GUI framework JFrame frame = new JFrame("DOM Echo"); frame.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) {System.exit(0);} }); // Set up the tree, the views, and display it all final DomEcho02 echoPanel = new DomEcho02(); frame.getContentPane().add("Center", echoPanel ); frame.pack(); Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); int w = windowWidth + 10; int h = windowHeight + 10; frame.setLocation(screenSize.width/3 - w/2, screenSize.height/2 - h/2); frame.setSize(w, h); frame.setVisible(true); } // makeFrame
Add the Display Components
The only thing left in the effort to convert the program to a GUI application is to create the class constructor and make it create the panel's contents. Here is the constructor:
public class DomEcho02 extends JPanel { ... static final int windowWidth = leftWidth + rightWidth;public DomEcho02() { } // Constructor
Here, you use the border classes you imported earlier to make a regal border (optional):
public DomEcho02() { // Make a nice borderEmptyBorder eb = new EmptyBorder(5,5,5,5); BevelBorder bb = new BevelBorder(BevelBorder.LOWERED); CompoundBorder cb = new CompoundBorder(eb,bb); this.setBorder(new CompoundBorder(cb,eb));
} // ConstructorNext, create an empty tree and put it into a
JScrollPane
so that users can see its contents as it gets large:public DomEcho02( { ...// Set up the tree JTree tree = new JTree();
// Build left-side viewJScrollPane treeView = new JScrollPane(tree); treeView.setPreferredSize( new Dimension( leftWidth, windowHeight ));
} // ConstructorNow create a noneditable
JEditPane
that will eventually hold the contents pointed to by selectedJTree
nodes:public DomEcho02( { .... // Build right-side viewJEditorPane htmlPane = new JEditorPane("text/html",""); htmlPane.setEditable(false); JScrollPane htmlView = new JScrollPane(htmlPane); htmlView.setPreferredSize( new Dimension( rightWidth, windowHeight ));
} // ConstructorWith the left-side
JTree
and the right-sideJEditorPane
constructed, create aJSplitPane
to hold them:public DomEcho02() { .... // Build split-pane viewJSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, treeView, htmlView ); splitPane.setContinuousLayout( true ); splitPane.setDividerLocation( leftWidth ); splitPane.setPreferredSize( new Dimension( windowWidth + 10, windowHeight+10 ));
} // ConstructorWith this code, you set up the
JSplitPane
with a vertical divider. That produces a horizontal split between the tree and the editor pane. (It's really more of a horizontal layout.) You also set the location of the divider so that the tree gets the width it prefers, with the remainder of the window width allocated to the editor pane.Finally, specify the layout for the panel and add the split pane:
public DomEcho02() { ... // Add GUI componentsthis.setLayout(new BorderLayout()); this.add("Center", splitPane );
} // ConstructorCongratulations! The program is now a GUI application. You can run it now to see what the general layout will look like on the screen. For reference, here is the completed constructor:
public DomEcho02() { // Make a nice border EmptyBorder eb = new EmptyBorder(5,5,5,5); BevelBorder bb = new BevelBorder(BevelBorder.LOWERED); CompoundBorder CB = new CompoundBorder(eb,bb); this.setBorder(new CompoundBorder(CB,eb)); // Set up the tree JTree tree = new JTree(); // Build left-side view JScrollPane treeView = new JScrollPane(tree); treeView.setPreferredSize( new Dimension( leftWidth, windowHeight )); // Build right-side view JEditorPane htmlPane = new JEditorPane("text/html",""); htmlPane.setEditable(false); JScrollPane htmlView = new JScrollPane(htmlPane); htmlView.setPreferredSize( new Dimension( rightWidth, windowHeight )); // Build split-pane view JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, treeView, htmlView ) splitPane.setContinuousLayout( true ); splitPane.setDividerLocation( leftWidth ); splitPane.setPreferredSize( new Dimension( windowWidth + 10, windowHeight+10 )); // Add GUI components this.setLayout(new BorderLayout()); this.add("Center", splitPane ); } // ConstructorCreate Adapters to Display the DOM in a JTree
Now that you have a GUI framework to display a
JTree
in, the next step is to get theJTree
to display the DOM. But aJTree
wants to display aTreeModel
. A DOM is a tree, but it's not aTreeModel
. So you'll create an adapter class that makes the DOM look like aTreeModel
to aJTree
.Now, when the
TreeModel
passes nodes to theJTree
,JTree
uses thetoString
function of those nodes to get the text to display in the tree. The value returned by the standardtoString
function isn't very pretty, so you'll wrap the DOM nodes in anAdapterNode
that returns the text we want. What theTreeModel
gives to theJTree
, then, will in fact beAdapterNode
objects that wrap DOM nodes.
Note: The classes that follow are defined as inner classes. If you are coding for the 1.1 platform, you will need to define these classes as external classes.
Define the AdapterNode Class
Start by importing the tree, event, and utility classes you'll need to make this work:
// For creating a TreeModelimport javax.swing.tree.*; import javax.swing.event.*; import java.util.*;
public class DomEcho extends JPanel {Moving back down to the end of the program, define a set of strings for the node element types:
... } // makeFrame // An array of names for DOM node types // (Array indexes = nodeType() values.)static final String[] typeName = { "none", "Element", "Attr", "Text", "CDATA", "EntityRef", "Entity", "ProcInstr", "Comment", "Document", "DocType", "DocFragment", "Notation", };
} // DomEcho
These are the strings that will be displayed in the
JTree
. The specification of these node types can be found in the DOM Level 2 Core Specification athttp://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113
, under the specification for Node. Table 6-1 is adapted from that specification.
Note: Print this table and keep it handy! You need it when working with the DOM, because all these types are intermixed in a DOM tree. So your code is forever asking, "Is this the kind of node I'm interested in?"
Next, define the
AdapterNode
wrapper for DOM nodes as an inner class:static final String[] typeName = { ... };public class AdapterNode { org.w3c.dom.Node domNode; // Construct an Adapter node from a DOM node public AdapterNode(org.w3c.dom.Node node) { domNode = node; } // Return a string that identifies this node // in the tree public String toString() { String s = typeName[domNode.getNodeType()]; String nodeName = domNode.getNodeName(); if (! nodeName.startsWith("#")) { s += ": " + nodeName; } if (domNode.getNodeValue() != null) { if (s.startsWith("ProcInstr")) s += ", "; else s += ": "; // Trim the value to get rid of NL's // at the front String t = domNode.getNodeValue().trim(); int x = t.indexOf("\n
"); if (x >= 0) t = t.substring(0, x); s += t; } return s; } }
// AdapterNode } // DomEchoThis class declares a variable to hold the DOM node and requires it to be specified as a constructor argument. It then defines the
toString
operation, which returns the node type from theString
array, and then adds more information from the node to further identify it.As you can see Table 6-1, every node has a type, a name, and a value, which may or may not be empty. Where the node name starts with
#
, that field duplicates the node type, so there is no point in including it. That explains the lines that readThe remainder of the
toString
method deserves a couple of notes. For example these lines merely provide a little syntactic sugar:The
type
field for processing instructions ends with a colon (:) anyway, so those lines keep the code from doubling the colon.The other interesting lines are
String t = domNode.getNodeValue().trim(); int x = t.indexOf("\n"); if (x >= 0) t = t.substring(0, x); s += t;These lines trim the value field down to the first newline (linefeed) character in the field. If you omit these lines, you will see some funny characters (square boxes, typically) in the
JTree
.
Note: Recall that XML stipulates that all line endings are normalized to newlines, regardless of the system the data comes from. That makes programming quite a bit simpler.
Wrapping a
DomNode
and returning the desired string are theAdapterNode
's major functions. But because theTreeModel
adapter must answer questions such as "How many children does this node have?" and must satisfy commands such as "Give me this node's Nth child," it will be helpful to define a few additional utility methods. (The adapter can always access the DOM node and get that information for itself, but this way things are more encapsulated.)Next, add the following highlighted code to return the index of a specified child, the child that corresponds to a given index, and the count of child nodes:
public class AdapterNode { ... public String toString() { ... }public int index(AdapterNode child) { //System.err.println("Looking for index of " + child); int count = childCount(); for (int i=0; i<count; i++) { AdapterNode n = this.child(i); if (child == n) return i; } return -1; // Should never get here. } public AdapterNode child(int searchIndex) { //Note: JTree index is zero-based. org.w3c.dom.Node node = domNode.getChildNodes().item(searchIndex); return new AdapterNode(node); } public int childCount() { return domNode.getChildNodes().getLength(); }
} // AdapterNode } // DomEcho
Note: During development, it was only after I started writing the
TreeModel
adapter that I realized these were needed and went back to add them. In a moment, you'll see why.
Define the TreeModel Adapter
Now, at last, you are ready to write the
TreeModel
adapter. One of the really nice things about theJTree
model is the ease with which you can convert an existing tree for display. One reason for that is the clear separation between the displayable view, whichJTree
uses, and the modifiable view, which the application uses. For more on that separation, see "Understanding the TreeModel" athttp://java.sun.com/products/jfc/tsc/articles/jtree/index.html
. For now, the important point is that to satisfy theTreeModel
interface we need only (a) provide methods to access and report on children and (b) register the appropriateJTree
listener so that it knows to update its view when the underlying model changes.Add the following highlighted code to create the
TreeModel
adapter and specify the child-processing methods:... } // AdapterNode // This adapter converts the current Document (a DOM) into // a JTree model.public class DomToTreeModelAdapter implements javax.swing.tree.TreeModel { // Basic TreeModel operations public Object getRoot() { //System.err.println("Returning root: " +document); return new AdapterNode(document); } public boolean isLeaf(Object aNode) { // Determines whether the icon shows up to the left. // Return true for any node with no children AdapterNode node = (AdapterNode) aNode; if (node.childCount() > 0) return false; return true; } public int getChildCount(Object parent) AdapterNode node = (AdapterNode) parent; return node.childCount(); } public Object getChild(Object parent, int index) { AdapterNode node = (AdapterNode) parent; return node.child(index); } public int getIndexOfChild(Object parent, Object child) { AdapterNode node = (AdapterNode) parent; return node.index((AdapterNode) child); } public void valueForPathChanged( TreePath path, Object newValue) { // Null. We won't be making changes in the GUI // If we did, we would ensure the new value was // really new and then fire a TreeNodesChanged event. } }
// DomToTreeModelAdapter } // DomEchoIn this code, the
getRoot
method returns the root node of the DOM, wrapped as anAdapterNode
object. From this point on, all nodes returned by the adapter will beAdapterNode
s that wrap DOM nodes. By the same token, whenever theJTree
asks for the child of a given parent, the number of children that parent has, and so on, theJTree
will pass us anAdapterNode
. We know that, because we control every node theJTree
sees, starting with the root node.
JTree
uses theisLeaf
method to determine whether or not to display a clickable expand/contract icon to the left of the node, so that method returns true only if the node has children. In this method, we see the cast from the generic objectJTree
sends us to theAdapterNode
object we know it must be. We know it is sending us an adapter object, but the interface, to be general, defines objects, so we must do the casts.The next three methods return the number of children for a given node, the child that lives at a given index, and the index of a given child, respectively. That's all straightforward.
The last method is invoked when the user changes a value stored in the
JTree
. In this application, we won't support that. But if we did, the application would have to make the change to the underlying model and then inform any listeners that a change has occurred. (TheJTree
might not be the only listener. In many applications, it isn't.)To inform listeners that a change has occurred, you'll need the ability to register them. That brings us to the last two methods required to implement the
TreeModel
interface. Add the following highlighted code to define them:public class DomToTreeModelAdapter ... { ... public void valueForPathChanged( TreePath path, Object newValue) { ... }private Vector listenerList = new Vector(); public void addTreeModelListener( TreeModelListener listener ) { if ( listener != null && ! listenerList.contains(listener) ) { listenerList.addElement( listener ); } } public void removeTreeModelListener( TreeModelListener listener ) { if ( listener != null ) { listenerList.removeElement( listener ); } }
} // DomToTreeModelAdapterBecause this application won't be making changes to the tree, these methods will go unused for now. However, they'll be there in the future when you need them.
Note: This example uses
Vector
so that it will work with 1.1 applications. If coding for 1.2 or later, though, I'd use the excellent collections framework instead:private LinkedList listenerList = new LinkedList();
The operations on the
List
are thenadd
andremove
. To iterate over the list, as in the following operations, you would useIterator it = listenerList.iterator(); while ( it.hasNext() ) { TreeModelListener listener = (TreeModelListener) it.next(); ... }Here, too, are some optional methods you won't use in this application. At this point, though, you have constructed a reasonable template for a
TreeModel
adapter. In the interest of completeness, you might want to add the following highlighted code. You can then invoke them whenever you need to notifyJTree
listeners of a change:public void removeTreeModelListener( TreeModelListener listener) { ... }public void fireTreeNodesChanged( TreeModelEvent e ) { Enumeration listeners = listenerList.elements(); while ( listeners.hasMoreElements() ) { TreeModelListener listener = (TreeModelListener) listeners.nextElement(); listener.treeNodesChanged( e ); } } public void fireTreeNodesInserted( TreeModelEvent e ) { Enumeration listeners = listenerList.elements(); while ( listeners.hasMoreElements() ) { TreeModelListener listener = (TreeModelListener) listeners.nextElement(); listener.treeNodesInserted( e ); } } public void fireTreeNodesRemoved( TreeModelEvent e ) { Enumeration listeners = listenerList.elements(); while ( listeners.hasMoreElements() ) { TreeModelListener listener = (TreeModelListener) listeners.nextElement(); listener.treeNodesRemoved( e ); } } public void fireTreeStructureChanged( TreeModelEvent e ) { Enumeration listeners = listenerList.elements(); while ( listeners.hasMoreElements() ) { TreeModelListener listener = (TreeModelListener) listeners.nextElement(); listener.treeStructureChanged( e ); } }
} // DomToTreeModelAdapter
Note: These methods are taken from the
TreeModelSupport
class described in "Understanding the TreeModel." That architecture was produced by Tom Santos and Steve Wilson and is a lot more elegant than the quick hack going on here. It seemed worthwhile to put them here, though, so that they would be immediately at hand when and if they're needed.
Finishing Up
At this point, you are basically finished constructing the GUI. All you need to do is to jump back to the constructor and add the code to construct an adapter and deliver it to the
JTree
as theTreeModel
:You can now compile and run the code on an XML file. In the next section, you will do that, as well as explore the DOM structures that result.