The JavaTM Web Services Tutorial
Home
TOC
Index
PREV TOP NEXT
Divider

Displaying a DOM Hierarchy

To create a Document Object Hierarchy (DOM) or manipulate one, 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.

Echoing Tree Nodes

What you need at this point is a way to expose the nodes in a DOM so you can see what it contains. To do that, you'll convert a DOM into a JTreeModel and display the full DOM in a JTree. It's going to take 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.

Convert DomEcho to a GUI App

Since the DOM is a tree, and the Swing JTree component is all about displaying trees, it makes sense to stuff the DOM into a JTree, so you can look at it. The first step in that process is to hack up the DomEcho program so it becomes a GUI application.


Note: The code discussed in this section is in DomEcho02.java.

Add Import Statements

Start by importing the GUI components you're going to need to set up the application and display a JTree:

// GUI components and layouts
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTree; 
 

Later on in the DOM tutorial, we'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 be displaying subelements in an adjacent editor pane. So, while we'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):

import javax.swing.JSplitPane;
import javax.swing.JEditorPane; 
 

Add a few support classes you're going to need to get this thing off the ground:

// GUI support classes
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Toolkit;
import java.awt.event.WindowEvent;
import java.awt.event.WindowAdapter; 
 

Finally, import some classes to make a fancy border:

// For creating borders
import 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, the static main method will create an instance of the main 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 DomEcho02 extends 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 need to 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:

   ...
} // main
 
public 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 make use of the border classes you imported earlier to make a regal border (optional):

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));

} // Constructor
 

Next, create an empty tree and put it a JScrollPane so users can see its contents as it gets large:

public DomEcho02(
{
  ...

  // Set up the tree
  JTree tree = new JTree();

  // Build left-side view
  JScrollPane treeView = new JScrollPane(tree);
  treeView.setPreferredSize( 
    new Dimension( leftWidth, windowHeight ));

} // Constructor
 

Now create a non-editable JEditPane that will eventually hold the contents pointed to by selected JTree nodes:

public DomEcho02(
{
  ....

  // Build right-side view
  JEditorPane htmlPane = new JEditorPane("text/html","");
  htmlPane.setEditable(false);
  JScrollPane htmlView = new JScrollPane(htmlPane);
  htmlView.setPreferredSize( 
    new Dimension( rightWidth, windowHeight ));

}  // Constructor
 

With the left-side JTree and the right-side JEditorPane constructed, create a JSplitPane to hold them:

public DomEcho02()
{
  ....

  // 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 ));

}  // Constructor
 

With this code, you set up the JSplitPane with a vertical divider. That produces a "horizontal split" between the tree and the editor pane. (More of a horizontal layout, really.) You also set the location of the divider so that the tree got 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 components
  this.setLayout(new BorderLayout());
  this.add("Center", splitPane );

} // Constructor 
 

Congratulations! The program is now a GUI application. You can run it now to see what the general layout will look like on 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 );

} // Constructor 
 

Create Adapters to Display the DOM in a JTree

Now that you have a GUI framework to display a JTree in, the next step is get the JTree to display the DOM. But a JTree wants to display a TreeModel. A DOM is a tree, but it's not a TreeModel. So you'll need to create an adapter class that makes the DOM look like a TreeModel to a JTree.

Now, when the TreeModel passes nodes to the JTree, JTree uses the toString function of those nodes to get the text to display in the tree. The standard toString function isn't going to be very pretty, so you'll need to wrap the DOM nodes in an AdapterNode that returns the text we want. What the TreeModel gives to the JTree, then, will in fact be AdapterNode 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 class as external classes.

Define the AdapterNode Class

Start by importing the tree, event, and utility classes you're going to need to make this work:

// For creating a TreeModel
import 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 nodes types can be found in the Document Object Model (DOM) Level 2 Core Specification at http://www.w3.org/TR/2000/REC-DOM/Level-2-Core-20001113, under the specification for Node. That table is reproduced below, with the headings modified for clarity, and with the nodeType() column added:

Table 1 Node Types 
Node
nodeName()
nodeValue()
attributes
nodeType()
Attr
name of attribute
value of attribute
null
2
CDATASection
#cdata-section
content of the CDATAsection
null
4
Comment
#comment
content of the comment
null
8
Document
#document
null
null
9
DocumentFragment
#document-fragment
null
null
11
DocumentType
document type name
null
null
10
Element
tag name
null
NamedNodeMap
1
Entity
entity name
null
null
6
EntityReference
name of entity referenced
null
null
5
Notation
notation name
null
null
12
ProcessingInstruction
target
entire contentexcluding thetarget
null
7
Text
#text
content of the text node
null
3

Suggestion:

Print this table and keep it handy. You need it when working with the DOM, because all of 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(");
      if (x >= 0) t = t.substring(0, x);
      s += t;
    }
    return s;
  }

} // AdapterNode

} // DomEcho 
 

This 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 the String array, and then adds to that additional information from the node, to further identify it.

As you can see in the table of node types in org.w3c.dom.Node, every node has a type, and name, and a value, which may or may not be empty. In those cases where the node name starts with "#", that field duplicates the node type, so there is in point in including it. That explains the lines that read:

if (! nodeName.startsWith("#")) {
  s += ": " + nodeName;
} 
 

The remainder of the toString method deserves a couple of notes, as well. For instance, these lines:

if (s.startsWith("ProcInstr")) 
  s += ", "; 
else 
  s += ": ";
 

Merely provide a little "syntactic sugar". The type field for a Processing Instructions end with a colon (:) anyway, so those codes keep from doubling the colon.

The other interesting lines are:

String t = domNode.getNodeValue().trim();
int x = t.indexOf(");
if (x >= 0) t = t.substring(0, x);
s += t;
 

Those lines trim the value field down to the first newline (linefeed) character in the field. If you leave those lines out, 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 the AdapterNode's major functions. But since the TreeModel adapter will need to answer questions like "How many children does this node have?" and satisfy commands like "Give me this node's Nth child", it will be helpful to define a few additional utility methods. (The adapter could always access the DOM node and get that information for itself, but this way things are more encapsulated.)

Next, add the code highlighted below 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 just 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 the JTree model is the relative ease with which you convert an existing tree for display. One of the reasons for that is the clear separation between the displayable view, which JTree uses, and the modifiable view, which the application uses. For more on that separation, see Understanding the TreeModel at http://java.sun.com/products/jfc/tsc/articles/jtree/index.html. For now, the important point is that to satisfy the TreeModel interface we only need to (a) provide methods to access and report on children and (b) register the appropriate JTree listener, so it knows to update its view when the underlying model changes.

Add the code highlighted below 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

} // DomEcho  
 

In this code, the getRoot method returns the root node of the DOM, wrapped as an AdapterNode object. From here on, all nodes returned by the adapter will be AdapterNodes that wrap DOM nodes. By the same token, whenever the JTree asks for the child of a given parent, the number of children that parent has, etc., the JTree will be passing us an AdapterNode. We know that, because we control every node the JTree sees, starting with the root node.

JTree uses the isLeaf 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 object JTree sends us to the AdapterNode object we know it has to be. We know it is sending us an adapter object, but the interface, to be general, defines objects, so we have to 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 pretty straightforward.

The last method is invoked when the user changes a value stored in the JTree. In this app, 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 had occurred. (The JTree might not be the only listener. In many an application it isn't, in fact.)

To inform listeners that a change 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 code highlighted below 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 );
    }
  }

} // DomToTreeModelAdapter
 

Since 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 it will work with 1.1 apps. 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 then add and remove. To iterate over the list, as in the operations below, you would use:

Iterator it = listenerList.iterator();
while ( it.hasNext() ) {
  TreeModelListener listener = (TreeModelListener) it.next();
    ... 
}
 

Here, too, are some optional methods you won't be using in this application. At this point, though, you have constructed a reasonable template for a TreeModel adapter. In the interests of completeness, you might want to add the code highlighted below. You can then invoke them whenever you need to notify JTree 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 they would be immediately at hand when and if they're needed.

Finishing Up

At this point, you are basically done. All you need to do is jump back to the constructor and add the code to construct an adapter and deliver it to the JTree as the TreeModel:

// Set up the tree
JTree tree = new JTree(new DomToTreeModelAdapter()); 
 

You can now compile and run the code on an XML file. In the next section, you will do that, and explore the DOM structures that result.

Divider
Home
TOC
Index
PREV TOP NEXT
Divider

This tutorial contains information on the 1.0 version of the Java Web Services Developer Pack.

All of the material in The Java Web Services Tutorial is copyright-protected and may not be published in other works without express written permission from Sun Microsystems.