Skip to main content

XML and Java technologies: Java document model usage

A look at how different XML document models work in Java

Dennis Sosnoski, President, Sosnoski Software Solutions, Inc.
Photo of Dennis Sosnoski
Dennis Sosnoski (dms@sosnoski.com) is the founder and lead consultant of Seattle-area Java consulting company Sosnoski Software Solutions, Inc. His professional software development experience spans over 30 years, with the last several years focused on server-side Java technologies including servlets, Enterprise JavaBeans, and XML. He's given a number of presentations on both Java performance issues and general server-side Java technologies, and chairs the Seattle Java-XML SIG.

Summary:  In this article, XML tool watcher Dennis Sosnoski compares the usability of several Java document models. It's not always clear what the tradeoffs are when you choose a model, and it can require extensive recoding to switch if you later change your mind. Combining sample code with analysis of the model APIs, the author gives recommendations for which models may really make your job easier. Includes code samples that show the methods for the five different document models.

Date:  01 Feb 2002
Level:  Introductory
Activity:  8426 views

(For another perspective on the DOM versus language-specific spin-offs, see Joe Kesselman's thoughts in the sidebar, A few words about document models.)

In the first article of this series I looked at the performance of some of the leading XML document models in Java. Performance is only part of the story when it comes to choosing a technology of this type, though. Ease of use is at least as important, and that's been one of the main arguments presented in favor of using Java-specific models rather than the language-independent DOM.

To fill in the picture of which models really deliver, you need to see how they rank on the usability scale. In this article I'll try to do just that, starting with sample code to demonstrate how common types of operations can be coded in each of the models. I end with a summary of the results, and I suggest some other factors that make one representation easier to use than another.

See the earlier article (see Resources or the handy link under this article's table of contents) for background on the models used in this comparison, including the actual version numbers. Also see the Resources section for the source code download, links to the home pages for the models, and other related information.

Code comparisons

In these comparisons of usage techniques for the different document representations I'll show how to accomplish three types of basic operations in each model:

  • Build the document from an input stream
  • Walk through the elements and content, making some changes:
    • Strip leading and trailing whitespace from text content
    • If the resulting text content is empty, delete it
    • Otherwise, wrap it in a new element with the name "text" in the namespace of the parent element
  • Write the modified document to an output stream

The code for these examples is based on the benchmark program I used in the preceding article, with some simplifications. The focus in the benchmarks is to show each model at its best performance; for this article, I've tried to show the easiest way of accomplishing the operations in each model.

I've structured the example for each model as two separate code fragments. The first fragment gives code for reading the document, calling the modify code, and writing the modified document. The second fragment is a recursive method to actually walk through the document representation and perform the modifications. To avoid distractions I've ignored exception handling in the code.

You can get the complete code for all samples on the download page, linked from the Resources section at the bottom of this page. The download versions of the samples include a test driver and also have some added code for checking the operation of the different models by counting elements, deletes, and adds.

It's worth glancing over the description of DOM usage, below, even if you have no intention of using a DOM implementation. Since the DOM example is first I use it to go into more detail about some of the issues and structure of the example than in the models that follow. Scanning through this can fill you in on some details you'll miss if you go directly to one of the other models.


DOM

The DOM specification covers all types of manipulations of a document representation, but it does not cover issues such as parsing a document and generating text output. The two DOM implementations included in the performance tests, Xerces and Crimson, use different techniques for these operations. Listing 1 shows one form of the top-level code for Xerces.


Listing 1. Xerces DOM top-level code

 1  // parse the document from input stream ("in")
 2  DOMParser parser = new DOMParser();
 3  parser.setFeature("http://xml.org/sax/features/namespaces", true);
 4  parser.parse(new InputSource(in));
 5  Document doc = parser.getDocument();

 6  // recursively walk and modify document
 7  modifyElement(doc.getDocumentElement());

 8  // write the document to output stream ("out")
 9  OutputFormat format = new OutputFormat(doc);
10  XMLSerializer serializer = new XMLSerializer(out, format);
11  serializer.serialize(doc.getDocumentElement());

As I indicate in the comments, the first block of code (lines 1-5) in Listing 1 handles parsing an input stream to build a document representation. The DOMParser class is defined by Xerces to build a document from the output of the Xerces parser. The InputSource class is part of the SAX specification, and it can adapt any of several forms of input for use by a SAX parser. The actual parsing and document construction takes place with a single call, and if this completes successfully, the constructed Document can then be retrieved and used by the application.

The second code block (lines 6-7) just passes the root element of the document off to the recursive modification method I'll get to in a moment. This code is essentially the same for all the document models in this article, so I'll skip over it in the remaining examples without any discussion.

The third block of code (lines 8-11) handles writing the document to an output stream as text. Here the OutputFormat class wraps the document, giving a wide variety of options for the formatting of the generated text. The XMLSerializer class handles the actual generation of the output text.

The modify method for Xerces uses only standard DOM interfaces, so it's also compatible with any other DOM implementation. Listing 2 shows the code.


Listing 2. DOM Modify method

 1  protected void modifyElement(Element element) {

 2    // loop through child nodes
 3    Node child;
 4    Node next = (Node)element.getFirstChild();
 5    while ((child = next) != null) {

 6      // set next before we change anything
 7      next = child.getNextSibling();

 8      // handle child by node type
 9      if (child.getNodeType() == Node.TEXT_NODE) {

10        // trim whitespace from content text
11        String trimmed = child.getNodeValue().trim();
12        if (trimmed.length() == 0) {

13          // delete child if nothing but whitespace
14          element.removeChild(child);

15        } else {

16          // create a "text" element matching parent namespace
17          Document doc = element.getOwnerDocument();
18          String prefix = element.getPrefix();
19          String name = (prefix == null) ? "text" : (prefix + ":text");
20          Element text = 
21            doc.createElementNS(element.getNamespaceURI(), name);

22          // wrap the trimmed content with new element
23          text.appendChild(doc.createTextNode(trimmed));
24          element.replaceChild(text, child);

25        }
26      } else if (child.getNodeType() == Node.ELEMENT_NODE) {

27        // handle child elements with recursive call
28        modifyElement((Element)child);

29      }
30    }
31  }

The basic approach used by the method shown in Listing 2 is the same for all the document representations. It's called with an element, and it goes through the children of that element in order. If a text content child is found, the text is either deleted (if it consists only of whitespace) or wrapped by a new element with the name "text" in the same namespace as the containing element (if there are non-whitespace characters). If an element child is found, the method recursively calls itself with the child element.

For the DOM implementation I use a pair of references, child and next, to track my position in the ordered list of children. The reference for the next child node is loaded (line 7) before any other processing is done for the current child. This allows me to delete or replace the current child without losing track of where I am in the list.

The DOM interface starts to get a little messy when I create a new element to wrap non-whitespace text content (lines 16-24). The method used to create an element is associated with the document as a whole, so I need to retrieve the owner document for the element I'm currently processing (line 17). I want the new element to be in the same namespace as the existing parent element, and in DOM this means I need to construct the qualified name of the element. This is done differently depending on whether there's a namespace prefix or not (lines 18-19). With the qualified name for the new element, and the namespace URI from the existing element, I can then create the new element (lines 20-21).

Once the new element is created I can just create and add a text node to wrap the content String, and then replace the original text node with the newly created element (lines 22-24).


Listing 3. Crimson DOM top-level code

 1  // parse the document from input stream
 2  System.setProperty("javax.xml.parsers.DocumentBuilderFactory",
 3      "org.apache.crimson.jaxp.DocumentBuilderFactoryImpl");
 4  DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
 5  dbf.setNamespaceAware(true);
 6  DocumentBuilder builder = dbf.newDocumentBuilder();
 7  Document doc = builder.parse(in);

 8  // recursively walk and modify document
 9  modifyElement(doc.getDocumentElement());

10  // write the document to output stream
11  ((XmlDocument)doc).write(out);

The Crimson DOM example code in Listing 3 uses the JAXP interface for parsing. JAXP provides a standardized interface for parsing and transforming XML documents. The parsing code from this example could also be used for Xerces (with the appropriate change to the property setting for the document builder class name) in place of the Xerces-specific example code given earlier.

In this example, I first set a system property in lines 2 to 3 to select the builder factory class for the DOM representation to be constructed (JAXP directly supports building only DOM representations, not any of the other ones discussed in this article). This step is needed only if you want to select a specific DOM to be used by JAXP; otherwise, it uses a default implementation. I included setting this property in the code for completeness, but it would more commonly be set as a JVM command-line parameter.

In lines 4 to 6 I then create an instance of the builder factory, enable namespace support for builders constructed using that factory instance, and create a document builder from the builder factory. Finally (line 7), I use the document builder to parse the input stream and construct the document representation.

For writing out the document I use the basic method defined internally within Crimson. This approach is not guaranteed to be supported in future versions of Crimson, but the alternative of using the JAXP transform code to output the document as text requires an XSL processor such as Xalan. That's beyond the scope of this article, but for details you can refer to the JAXP tutorial at Sun.


JDOM

Using JDOM, the top-level code is a little simpler than with the DOM implementations. For building the document representation (lines 1-3), I use a SAXBuilder with validation disabled by the parameter value. Writing the modified document to an output stream is equally simple (lines 6-8), using the supplied XMLOutputter class.


Listing 4. JDOM top-level code

 1  // parse the document from input stream
 2  SAXBuilder builder = new SAXBuilder(false);
 3  Document doc = builder.build(in);

 4  // recursively walk and modify document
 5  modifyElement(doc.getRootElement());

 6  // write the document to output stream
 7  XMLOutputter outer = new XMLOutputter();
 8  outer.output(doc, out);

The modify method for JDOM in Listing 5 is also simpler than the same method for DOM. I get a list of all the content of the element and scan through the list, checking for text (as String objects) and elements. The list is "live," so I can make changes to the list directly rather than needing to call a method on the parent element.


Listing 5. JDOM modify method

 1  protected void modifyElement(Element element) {

 2    // loop through child nodes
 3    List children = element.getContent();
 4    for (int i = 0; i < children.size(); i++) {

 5      // handle child by node type
 6      Object child = children.get(i);
 7      if (child instanceof String) {

 8        // trim whitespace from content text
 9        String trimmed = child.toString().trim();
10        if (trimmed.length() == 0) {

11          // delete child if only whitespace (adjusting index)
12          children.remove(i--);

13        } else {

14          // wrap the trimmed content with new element
15          Element text = new Element("text", element.getNamespace());
16          text.setText(trimmed);
17          children.set(i, text);

18        }
19      } else if (child instanceof Element) {

20        // handle child elements with recursive call
21        modifyElement((Element)child);

22      }
23    }
24  }

The technique for creating a new element (lines 14-17) is very simple, and unlike the DOM version, it does not require access to the parent document.


dom4j

The top-level code for dom4j is a little more involved than that for JDOM, but it works along very similar lines. The main differences here are that I save the DocumentFactory used to build the dom4j document representation (line 5) and flush the writer after outputting the modified document text (line 10).


Listing 6. dom4j top-level code

 1  // parse the document from input stream
 2  SAXReader reader = new SAXReader(false);
 3  Document doc = reader.read(in);

 4  // recursively walk and modify document
 5  m_factory = reader.getDocumentFactory();
 6  modifyElement(doc.getRootElement());

 7  // write the document to output stream
 8  XMLWriter writer = new XMLWriter(out);
 9  writer.write(doc);
10  writer.flush();

As you can see in Listing 6, dom4j uses a factory approach for constructing objects included in the document representation built from a parse. Each component object is defined in terms of an interface, and so any type of object that implements one of these interfaces can be included in the representation (as opposed to JDOM, which uses concrete classes: The classes can be subclassed and extended in some cases, but any class used in the document representation needs to be based on the original JDOM class). By using different factories for the dom4j document build you can get documents constructed from different families of components.

In the sample code (line 5) I retrieve the (default) document factory used to build the document and store it in an instance variable (m_factory) for use by the modify method. This step is not strictly necessary -- components from different factories can be used together in a document, or you can bypass the factory and create instances of the components directly -- but in this case I just want to create the same types of components as used in the rest of the document, and using the same factory assures that this is done.


Listing 7. dom4j modify method

 1  protected void modifyElement(Element element) {

 2    // loop through child nodes
 3    List children = element.content();
 4    for (int i = 0; i < children.size(); i++) {

 5      // handle child by node type
 6      Node child = (Node)children.get(i);
 7      if (child.getNodeType() == Node.TEXT_NODE) {

 8        // trim whitespace from content text
 9        String trimmed = child.getText().trim();
10        if (trimmed.length() == 0) {

11          // delete child if only whitespace (adjusting index)
12          children.remove(i--);

13        } else {

14          // wrap the trimmed content with new element
15          Element text = m_factory.createElement
16            (QName.get("text", element.getNamespace()));
17          text.addText(trimmed);
18          children.set(i, text);

19        }
20      } else if (child.getNodeType() == Node.ELEMENT_NODE) {

21        // handle child elements with recursive call
22        modifyElement((Element)child);

23      }
24    }
25  }

The dom4j modify method in Listing 7 is very similar to that used with JDOM. Instead of checking the type of content item by using the instanceof operator, I can get the type code through the Node interface method getNodeType (instanceof would also work, but the type code approach seems cleaner). The new element creation technique (lines 15-16) differs both by the use of a QName object to represent the element name and by calling a method of the saved factory to build the element.


Electric XML

The top-level code for Electric XML (EXML) in Listing 8 is the simplest of any of these examples, with a single method call each for reading and writing the document.


Listing 8. EXML top-level code

 1  // parse the document from input stream
 2  Document doc = new Document(in);

 3  // recursively walk and modify document
 4  modifyElement(doc.getRoot());

 5  // write the document to output stream
 6  doc.write(out);

The EXML modify method in Listing 9 is most similar to the DOM approach, though it requires the use of instanceof checks, as with JDOM. In EXML there's no way to create an element with a namespace-qualified name, so I instead create the new element and then set its name to get the same effect.


Listing 9. EXML modify method

 1  protected void modifyElement(Element element) {

 2    // loop through child nodes
 3    Child child;
 4    Child next = element.getChildren().first();
 5    while ((child = next) != null) {

 6      // set next before we change anything
 7      next = child.getNextSibling();

 8      // handle child by node type
 9      if (child instanceof Text) {

10        // trim whitespace from content text
11        String trimmed = ((Text)child).getString().trim();
12        if (trimmed.length() == 0) {

13          // delete child if only whitespace
14          child.remove();

15        } else {

16          // wrap the trimmed content with new element
17          Element text = new Element();
18          text.addText(trimmed);
19          child.replaceWith(text);
20          text.setName(element.getPrefix(), "text");

21        }
22      } else if (child instanceof Element) {

23        // handle child elements with recursive call
24        modifyElement((Element)child);

25      }
26    }
27  }


XPP

The top-level code for XPP (in Listing 10) is the longest of the examples, with considerably more setup required than the other models.


Listing 10. XPP top-level code

 1  // parse the document from input stream
 2  m_parserFactory = XmlPullParserFactory.newInstance();
 3  m_parserFactory.setNamespaceAware(true);
 4  XmlPullParser parser = m_parserFactory.newPullParser();
 5  parser.setInput(new BufferedReader(new InputStreamReader(in)));
 6  parser.next();
 7  XmlNode doc = m_parserFactory.newNode();
 8  parser.readNode(doc);

 9  // recursively walk and modify document
10  modifyElement(doc);

11  // write the document to output stream
12  XmlRecorder recorder = m_parserFactory.newRecorder();
13  Writer writer = new OutputStreamWriter(out);
14  recorder.setOutput(writer);
15  recorder.writeNode(doc);
16  writer.close();

As with the JAXP interface, I first have to create an instance of the parser factory and enable namespace handling before creating a parser instance (lines 2-4). Once I've got a parser instance I can set the input to the parser and actually build the document representation (lines 5-8), but this involves more steps than for the other models.

The output handling (lines 11-16) also involves more steps than for the other models, mainly because XPP requires a Writer rather than accepting a Stream directly as an output target.

The XPP modify method in Listing 11 is most similar to the JDOM approach, though it requires more code to create a new element (lines 13-21). The namespace handling is a little on the cumbersome side here. I have to first create the qualified name for the element (lines 15-16), then create the element, and finally set the name and namespace URI later (lines 18-21).


Listing 11. XPP modify method

 1  protected void modifyElement(XmlNode element) throws Exception {

 2    // loop through child nodes
 3    for (int i = 0; i < element.getChildrenCount(); i++) {

 4      // handle child by node type
 5      Object child = element.getChildAt(i);
 6      if (child instanceof String) {

 7        // trim whitespace from content text
 8        String trimmed = child.toString().trim();
 9        if (trimmed.length() == 0) {

10          // delete child if only whitespace (adjusting index)
11          element.removeChildAt(i--);

12        } else {

13          // construct qualified name for wrapper element
15          String prefix = element.getPrefix();
16          String name = (prefix == null) ? "text" : (prefix + ":text");

17          // wrap the trimmed content with new element
18          XmlNode text = m_parserFactory.newNode();
19          text.appendChild(trimmed);
20          element.replaceChildAt(i, text);
21          text.modifyTag(element.getNamespaceUri(), "text", name);

22        }
23      } else if (child instanceof XmlNode) {

24        // handle child elements with recursive call
25        modifyElement((XmlNode)child);

26      }
27    }
28  }


Conclusions

JDOM, dom4j, and Electric XML all come across in these code samples as about equally easy to use, with EXML perhaps the easiest and dom4j the more difficult by small margins. DOM offers the very real benefits of language independence, but if you're only working with Java code it looks a little cumbersome by comparison with the Java-specific models. I think this shows that the Java-specific models have generally succeeded in their goal of simplifying XML document handling in Java code.

Beyond the basics: Real-world usability

The code samples show that JDOM and EXML provide simple and clean interfaces for basic document manipulation (working with elements, attributes, and text). In my experience their approach does much less well for programming tasks that work with the entire document representation. For these types of tasks, the component approach used by DOM and dom4j -- where all the document components from attributes through namespaces implement some common interface -- works much better.

A case in point is the XML Streaming (XMLS) encoding I recently implemented for both JDOM and dom4j. This code goes through the entire document and encodes each component. The JDOM implementation is considerably more complex than the dom4j implementation, mainly because JDOM uses unique classes with no common interfaces for representing each component.

Because of JDOM's lack of a common interface, code for working with a Document object needs to be different from code for working with an Element object, even though they both can have some of the same types of components as children. Special methods are also needed to retrieve Namespace components as opposed to other types of children. Even when handling the types of child components that are considered content, you need to use multiple if statements with instanceof checks instead of a cleaner and faster switch statement on the component type.

It's perhaps ironic that one of JDOM's original goals was to make use of the Java Collections classes, which are themselves based heavily upon interfaces. The use of interfaces in libraries adds a lot of flexibility at the cost of a little added complexity, and that's usually a good tradeoff for code that's designed for reuse. It's also probably contributed significantly to dom4j arriving at a mature and stable state much more quickly than JDOM.

DOM is still a great choice for developers who work in multiple languages, though. DOM implementations are available for a wide variety of programming languages. It's also the basis for a number of other XML-related standards, so even if you work with a Java-specific model there's a good chance you'll need to develop at least a casual acquaintance with DOM. Because it's an official W3C recommendation (as opposed to the non-standards-based Java models) it may also be required in certain types of projects.

Among the three leading contenders in the ease of use category, dom4j differs from the others in using an interface-based approach with multiple layers of inheritance. This can make the API JavaDocs a little harder to follow. For example, a method you're looking for (such as content(), used on line 3 of our dom4j modify method example) may be part of the Branch interface that Element extends rather than part of the Element interface itself. This interface-based design adds a lot of flexibility, though (see sidebar Beyond the basics: Real-world usability). Given dom4j's performance, stability, and feature-set advantages, you should consider it a strong candidate for most projects.

JDOM has probably the largest user base of any of the Java-specific document models, and it is certainly one of the easiest to use. As a choice for project development it suffers from having an API that's still fluid and subject to change from one version to the next, though, and it also came out poorly in the performance comparisons. Based on the present implementations I'd recommend dom4j rather than JDOM for anyone starting a new project.

EXML has a much smaller footprint than any of the other models except XPP, and given EXML's ease of use advantage you should definitely consider it for applications where jar file size is important. Still, the limitations of EXML's XML support and the restricted license, along with the relatively poor performance for larger files, have to count against it in many applications.

XPP suffers from requiring more steps for parsing and writing text documents, and for working with namespaces. If XPP were to add some convenience methods to handle some of these common cases it'd probably come out much better in the comparison. As it is now, the performance leader from the last article is the usability loser in this one. It's still worth considering as an alternative to EXML for applications requiring a small jar file size, though, given the performance advantage.


Next up...

In the pair of articles so far, I've covered performance and usability of XML document models in Java. For the next pair of articles in this series I'll look into XML data binding approaches in Java technology. These have many similarities to the document model approaches, but they go a step further by mapping XML documents into actual application data structures. We'll see how well this pays off in ease of use and performance.

Check back on developerWorks for the scoop on XML data binding for Java code. In the meantime, you can give me your comments and questions on this article in the discussion forum, linked below.


Resources

About the author

Photo of Dennis Sosnoski

Dennis Sosnoski (dms@sosnoski.com) is the founder and lead consultant of Seattle-area Java consulting company Sosnoski Software Solutions, Inc. His professional software development experience spans over 30 years, with the last several years focused on server-side Java technologies including servlets, Enterprise JavaBeans, and XML. He's given a number of presentations on both Java performance issues and general server-side Java technologies, and chairs the Seattle Java-XML SIG.

Comments (Undergoing maintenance)



Trademarks  |  My developerWorks terms and conditions

Help: Update or add to My dW interests

What's this?

This little timesaver lets you update your My developerWorks profile with just one click! The general subject of this content (AIX and UNIX, Information Management, Lotus, Rational, Tivoli, WebSphere, Java, Linux, Open source, SOA and Web services, Web development, or XML) will be added to the interests section of your profile, if it's not there already. You only need to be logged in to My developerWorks.

And what's the point of adding your interests to your profile? That's how you find other users with the same interests as yours, and see what they're reading and contributing to the community. Your interests also help us recommend relevant developerWorks content to you.

View your My developerWorks profile

Return from help

Help: Remove from My dW interests

What's this?

Removing this interest does not alter your profile, but rather removes this piece of content from a list of all content for which you've indicated interest. In a future enhancement to My developerWorks, you'll be able to see a record of that content.

View your My developerWorks profile

Return from help

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=XML, Java technology
ArticleID=10633
ArticleTitle=XML and Java technologies: Java document model usage
publish-date=02012002
author1-email=dms@sosnoski.com
author1-email-cc=

My developerWorks community

Tags

Help
Use the search field to find all types of content in My developerWorks with that tag.

Use the slider bar to see more or fewer tags.

Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere).

My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Use the search field to find all types of content in My developerWorks with that tag. Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere). My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Rate a product. Write a review.

Special offers