Java XOM: XML Made Simpler

This article originally appeared in the March 2003 issue of Linux Magazine.

Elliotte Rusty Harold, a highly-esteemed programmer and author, came to a realization as he was writing the book Processing XML with Java, A Guide to DOM, JAXP, JDOM, SAX, and TRAX. After writing 1,000 pages documenting how each of those APIs can be used to produce, consume, and transform XML, Harold -- in a fit of exasperation that ought to be familiar to anyone who writes computer documentation -- decided that there had to be a better way for programmers to process XML. So, he wrote one.

XOM, the XML Object Model for Java, is a new, tree-based API for processing XML that strives to be simple to learn, simple to use, and uncompromising in its adherence to well-formed XML.

"While documenting the various APIs, I found lots of things to like and lots of things to dislike," Harold says. "XOM is my effort to synthesize the best features of the existing APIs, while eliminating the worst."

The XOM project was originally envisioned as a fork of JDOM, an existing, popular, tree-based model for representing an XML document. Harold contributed code to that open source project and participated avidly in its development. But instead of forking the JDOM code, Harold decided to start almost entirely from scratch and adopt some of JDOM's core design principles in XOM. "If there hadn't been a JDOM first, then there wouldn't have been a XOM," he says.

JDOM has significant developer interest and has been accepted as a Java Specification Request (JSR), the first step towards its possible adoption as a standard part of Sun's Java 2 class library. XOM is likely to be evaluated most often in comparison to JDOM, especially by developers with some experience using the latter. So, let's start with some comparisons.

There are several important similarities between XOM and JDOM:

  • Both XOM and JDOM model an XML document as a tree, with Java classes representing nodes such as elements, comments, processing instructions, and document type declarations. A programmer can add and remove nodes to manipulate the document in memory, a simple approach that can be implemented gracefully in Java.
  • Both represent an XML document and its contents as classes with constructor methods rather than interfaces that must be implemented, a marked departure from the Simple API for XML Processing (SAX).
  • XOM and JDOM rely on other parsers to do low-level work. XOM uses a SAX2 parser to read an XML document and fill a tree with its data. Right now, the preferred parser is Xerces 2.1.0, though version 2.2.x may also work. Other parsers do not. "Piccolo 1.0.2, Crimson, GNU JAXP 1.0b1, and Xerces 2.0.x all have bugs that prevent them from doing what XOM needs them to do," says Harold.

There are also some striking differences between XOM and JDOM:

  • XOM models XML documents with a class hierarchy that has a generic Node superclass above a ParentNode class for tree nodes that can have children (documents and elements) and a LeafNode for those that cannot (comments, document type declarations, processing instructions, and text). In JDOM, there are no node classes and the different contents of an XML document inherit directly from java.lang.Object.
  • XOM always produces XML documents that are well-formed and have a well-formed namespace. All Node objects in XOM are either well-formed XML documents or portions of well-formed documents.
  • XOM provides minimal public methods. Fewer public methods makes the API easier to learn and master.
  • XOM does not support object serialization through the Serializable interface. Instead, programmers are encouraged to use XML as the format for serialized data, enabling it to be readily exchanged with any software that reads XML.

XOM is available for download from www.cafeconleche.org/XOM and requires Java 2 version 1.2 or greater because it makes internal use of the Java Collections API. Harold, the lead (and only) developer (up to this point), has released XOM under the GNU Lesser General Public License (LGPL) along with JavaDoc documentation and other supporting materials. The version documented in this article, 1.0d8, reflects several months of active user feedback and ongoing development on the XOM-interest mailing list.

The remainder of this article introduces XOM programming with four Java applications that read, write, and modify XML documents. These programs were created with XOM 1.0d8, the Xerces 2.2 parser, and Java 2 version 1.4.

Creating an XML Document

The first application, Domains, shown in Listing One, creates an XML document that contains configuration information for a domain name.

import nu.xom.*;

public class Domains {
  public static void main(String[] arguments) {
    // Create elements, text elements, and an element attribute
    Element domains = new Element("domains");
    Element domain = new Element("domain");
    Element name = new Element("name");
    Element dns = new Element("dns");
    Element ttl = new Element("ttl");
    Element ip = new Element("ip");
    Element webdir = new Element("webdir");
    Attribute status = new Attribute("status", "parked");
    Text nameText = new Text("prefect.com");
    Text ttlText = new Text("86400");
    // Create a document using the domains root element
    Document doc = new Document(domains);
    // Add XML data to the root element
    domains.appendChild(domain);
    domain.appendChild(name);
    name.appendChild(nameText);
    domain.appendChild(dns);
    dns.appendChild(ttl);
    ttl.appendChild(ttlText);
    dns.appendChild(ip);
    ip.appendChild("64.81.250.253");
    domain.appendChild(webdir);
    webdir.appendChild("/web/prefect");
    webdir.addAttribute(status);
    // Display the XML document
    System.out.println(doc.toXML());
  }
}

The XML it produces looks like this:

<?xml version="1.0"?>
<domains>
  <domain>
    <name>prefect.com</name>
    <dns>
      <ttl>86400</ttl>
      <ip>64.81.250.253</ip>
    </dns>
    <webdir status="parked">/web/prefect </webdir>
  </domain>
</domains>

XOM base nu.xom package contains classes for a complete XML document (Document) and the nodes a document can contain (Attribute, Comment, DocType, Element, ProcessingInstruction, and Text). The Domains application makes use of several of these classes.

First, an Element object is created by specifying the XML element's name as an argument. (If you need to specify namespaces, a second argument, the namespace URI of the element, is also necessary. Other XOM classes support namespaces in a similar manner.)

Element domains = new Element("domains");

This statement creates an object for the root element of the document, <domains>.

In the XML document shown above, the <webdir> element includes an attribute named status with the value parked. An attribute can be created by specifying its name and value in consecutive arguments like so:

Attribute status = new Attribute("status", "parked");

The text within an element is represented by the Text class, which is constructed by specifying the text as a String argument:

Text ttlText = new Text("86400");

To create a Document object, call the Document constructor with the root element as an argument. In the Domains application, this element is called domains. Any Element object can be the root of a document:

Document doc = new 
  Document(domains);

In XOM's tree structure, the classes representing an XML document and its constituent parts are organized into a hierarchy below the generic superclass nu.xom.Node. This class has three subclasses in the same package: Attribute, LeafNode, and ParentNode.

To add a child to a parent node, call the parent's appendChild() method with the node to add as the only argument. The following code creates three elements: a parent called <domain> and two of its children, <name> and <dns>:

Element domain = new Element("domain");
Element name = new Element("name");
Element dns = new Element("dns");
domain.appendChild(name);
domain.appendChild(dns);

The appendChild() method appends a new child below all other children of that parent. So the preceding statements would produce this XML fragment:

<domain>
  <name />
  <dns />
</domain>

The appendChild() method also can be called with a String argument instead of a node. A Text object representing the string will be created and added to the element. Here is one way to create the <ip> node:

Element ip = new Element("ip");
ip.appendChild("64.81.250.253");

Attributes are distinct from nodes and require a different method. To add an attribute to a node, call the node's addAttribute() method with the attribute as an argument:

Attribute status = new Attribute("status", "parked");
webdir.addAttribute(status);

After a tree's been created and filled with nodes, it can be displayed by calling the Document method toXML(), which returns the complete and well-formed XML document as a String.

The Domains application displays the XML document on standard output. The following command runs the application and redirects its output to a file called domains.xml:

% java Domains > domains.xml

XOM automatically precedes a document with an XML declaration, but does not indent the document in any way. Elements are stacked on the same line. XOM only preserves significant whitespace when representing XML data. A subsequent example will demonstrate how to control indentation.

Modifying an XML Document

The ChangeDomains application shown in Listing Two makes several changes to domains.xml, the XML document produced by the previous example. The text enclosed by the ttl element is changed from 86400 to 604800 and a new <email> element is added. The new file, changedomains.xml, looks like this:

import java.io.*;
import nu.xom.*;

public class ChangeDomains {
  public static void main(String[] arguments) throws IOException {
    try {
    // Create a tree from the XML document domains.xml
    Builder builder = new Builder();
    File xmlFile = new File("domains.xml");
    Document doc = builder.build(xmlFile);
    // Get the root element <domains>
    Element root = doc.getRootElement();
    // Loop through of its <domain> elements
    Elements rootChildren = root.getChildElements("domain");
    for (int i = 0; i < rootChildren.size(); i++) {
      // Get a <domain> element
      Element domain = rootChildren.get(i);
      // Get its <name> element and the text it encloses
      Element name = domain.getFirstChildElement("name");
      Text nameText = (Text) name.getChild(0);
      // Update any domain with the <name> text "prefect.com"
      if (nameText.getValue().equals("prefect.com"))
      updateDomain(domain);
    }
    // Display the XML document
    System.out.println(doc.toXML());
} catch (ParseException pe) {
    System.out.println("Error parsing document: " + pe.getMessage());
    pe.printStackTrace();
    System.exit(-1);
  }
}

private static void updateDomain(Element domain) {
    // Get the domain's <dns> element
    Element dns = domain.getFirstChildElement("dns");
    // Get its <ttl> element
    Element ttl = dns.getFirstChildElement("ttl");
    // Replace its Text child with "604800"
    ttl.removeChild(0);
    ttl.appendChild("604800");
    // Create new elements and attributes to add
    Element email = new Element("email");
    Element address = new Element("address");
    Attribute user = new Attribute("user", "postmaster@prefect.com");
    Attribute destination = new Attribute("destination", "rcade");
    // Add them to the <domain> element
    domain.appendChild(email);
    email.appendChild(address);
    address.addAttribute(user);
    address.addAttribute(destination);
  }
}
<?xml version="1.0"?>
<domains>
  <domain>
    <name>prefect.com</name>
    <dns>
      <ttl>604800</ttl>
      <ip>64.81.250.253</ip>
    </dns>
    <webdir status="parked">/web/prefect</webdir>
    <email>
      <address user="postmaster@prefect.com" destination="rcade" />
    </email>
  </domain>
</domains>

Using the nu.xom package, XML documents can be loaded into a tree from several sources: a File, an InputStream, a Reader, or a URL (which is specified as a String instead of a java.net .URL object).

The class Builder represents a SAX2 parser that can load an XML document into a Document object. Constructor methods can be used to specify a particular parser or you can let XOM use the first available parser from this list: Xerces 2.1.0, Piccolo, Aelfred, Oracle, Crimson, and the parser specified by the org.xml.sax.driver system property. Constructors also determine whether the parser is validating or non-validating.

The Builder() and Builder(true) constructors both use the default parser -- most likely a version of Xerces. If you provide the boolean argument true, Builder configures the parser to validate. Otherwise, the parser won't validate. A validating parser throws nu.xom.ValidityException if the XML document doesn't validate against its document type declaration.

The Builder object's build() method loads an XML document from a source and returns a Document object:

Builder builder = new Builder();
File xmlFile = new File("domains.xml");
Document doc = builder.build(xmlFile);

The preceding statements load an XML document from the file domains.xml, barring one of two problems: nu.xom.ParseException is thrown if the file does not contain well-formed XML, and java.io.IOException is thrown if the I/O operation fails.

Elements are retrieved from the tree by calling a method of their parent node. For example, a Document's getRootElement() method returns the root element of the document:

Element root = doc.getRootElement();

In the XML document domains.xml, the root element is <domains>.

Elements with names can be retrieved by calling their parent node's getFirstChildElement() method with the name as a String argument:

Element name = domain.getFirstChildElement("name");

The previous statement retrieves the <name> element contained in the <domain> element or returns null if that element could not be found. (Like other examples, this is simplified by the lack of a namespace in the document -- XOM also provides methods where an element name and namespace are arguments.)

When several elements within a parent have the same name, the parent node's getChildElements() method can be used instead:

Elements rootChildren = root.getChildElements("domain");

The getChildElements() method returns an Elements object containing each of the elements. This object is a read-only list and does not change automatically if the parent node's contents change after getChildElements() is called.

Elements has a size() method that returns an integer count of the elements it holds. The count can be used in a loop to cycle through each element in turn, beginning with the first element at position 0. There's a get() method to retrieve each element -- call it with the integer position of the element to be retrieved:

Elements rootChildren = root.getChildElements("domain");
for (int i = 0; i < rootChildren.size(); i++) {
  Element domain = rootChildren.get(i);
}

This for loop cycles through each <domain> element that's a child of the root element <domains> (the example domains .xml document contains only one).

Elements without names can be retrieved by calling their parent node's getChild() method with one argument: an integer indicating the element's position within the parent node:

Text nameText = (Text) name.getChild(0);

This statement creates the Text object for the text "prefect.com" found within the <name> element. Text elements always will be at position 0 within the enclosing parent.

To work with this text as a String, call the Text object's getValue() method:

if (nameText.getValue().equals ("prefect.com")) {
  updateDomain(domain);
}

The ChangeDomain application only modifies a <domain> element if its <name> element encloses the text "prefect.com." The application makes the following changes: the text of the <ttl> element is deleted, the new text 604800 is added in its place, and a new <email> element is added.

A parent node has two removeChild() methods to delete a child node from the document. Calling the method with an integer deletes the child at that position:

Element dns = domain.getFirstChildElement("dns");
Element ttl = dns.getFirstChildElement("ttl");
ttl.removeChild(0);

These statements delete the Text object contained within the <ttl> element.

Calling the removeChild() method with a node as an argument deletes that particular node. Extending the previous example, the <ttl> element could be deleted like so:

dns.removeChild(ttl);

The ChangeDomains application displays the modified XML document to standard output. If you want to capture the output, just redirect it with the command

% java ChangeDomains > changedomains.xml

Formatting an XML Document

The SerialDomains application (shown in Listing Three) adds a comment to the beginning of the XML document changedomains.xml and serializes it with indented lines.

import java.io.*;
import nu.xom.*;

public class SerialDomains {
  public static void main(String[] arguments) throws IOException {
    try {
      // Create a tree from an XML document
      // specified as a command-line argument
      Builder builder = new Builder();
      Document doc = builder.build(arguments[0]);
      // Create a comment with the current time and date
      Comment timestamp = new Comment ("File created " + new java.util.Date());

      // Add the comment above everything else in the
      // document
      doc.insertChild(0, timestamp);
      // Create a file output stream to a new file
      File inFile = new File(arguments[0]);
      FileOutputStream fos = new FileOutputStream ("new_" + inFile.getName());

      // Using a serializer with indention set to 2 spaces,
      // write the XML document to the file
      Serializer output = new Serializer(fos, "ISO-8859-1");
      output.setIndent(2);
      output.write(doc);
    } catch (ParseException pe) {
      System.out.println ("Error parsing document: " + pe.getMessage());
      pe.printStackTrace();
      System.exit(-1);
    }
  }
}

As described earlier, XOM does not retain insignificant whitespace when representing XML documents. This is in keeping with one of XOM's design goals -- to disregard anything that has no syntactic significance in XML. (Another example of this is how text is treated identically whether created using character entities, CDATA sections, or regular characters.)

The Serializer class in nu.xom controls how an XML document is formatted when it's displayed or stored serially. Indentation, character encoding, line breaks, and other formatting are set using this class. A Serializer object can be created by specifying an output stream and the character encoding as arguments to the constructor:

File inFile = new File(arguments[0]);
FileOutputStream fos = new FileOutputStream("new_" + 
    inFile.getName());
Serializer output = new Serializer (fos, "ISO-8859-1");

These statements serialize a file using ISO-8859-1 encoding. The file's name is taken from a command-line argument.

Serializer currently supports these encodings: ISO-10646-UCS-2, ISO-8859-1 through ISO-8859-10, ISO-8859-13 through ISO-8859-16, UTF-8, and UTF-16. There's also a Serializer() constructor that takes only an output stream as an argument -- this uses the UTF-8 encoding by default.

Indentation is set by calling the Serializer's setIndentation() method with an integer argument specifying the number of spaces:

output.setIndentation(2);

An entire document is written to the destination by calling the serializer's write() method with the document as an argument:

output.write(doc);

The SerialDomains application inserts a comment atop the XML document rather than appending it at the end of a parent node's children. This requires another method of the parent node, insertChild(), which is called with two arguments: the integer position of the insertion and the element to add:

Builder builder = new Builder();
Document doc = builder.build(arguments[0]);
Comment timestamp = new Comment("File created " + new java.util.Date());
doc.insertChild(0, timestamp);

Here, the comment is placed at position 0 atop the document, moving the <domains> tag down one line, but remaining below the XML declaration.

The SerialDomains application takes an XML filename as a command-line argument when run:

% java SerialDomains changedomains.xml

This command produces a file called new_changedomains.xml that contains an indented copy of the XML document with a timestamp inserted as a comment.

Evaluating XOM

The previous three example applications cover the core features of the main XOM package and are representative of its 116 classes straight forward approach to XML processing. There also are smaller nu.xom.canonical, nu.xom.xinclude, and nu.xom.xslt packages to support Canonical XML serialization, XInclude, and XSLT.

Listing Four shows an application that works with XML from an RSS feed. The RssFilter application searches the feed for specified text in headlines, producing a new XML document that contains only the matching items and shorter indentation. It also modifies the feed's title and adds an RSS 0.91 document type declaration if one is needed.

import nu.xom.*;

public class RssFilter {
  public static void main(String[] arguments) {
    if (arguments.length < 2) {
      System.out.println("Usage: java RssFilter rssFile searchTerm");
      System.exit(-1);
    }
    // Save the RSS location and search term
    String rssFile = arguments[0];
    String searchTerm = arguments[1];
    try {
      // Fill a tree with an RSS file's XML data
      // The file can be local or something on the
      // Web accessible via a URL.
      Builder bob = new Builder();
      Document doc = bob.build(rssFile);
      // Get the file's root element (<rss>)
      Element rss = doc.getRootElement();
      // Get the element's version attribute
      Attribute rssVersion = rss.getAttribute("version");
      String version = rssVersion.getValue();
      // Add the DTD for RSS 0.91 feeds, if needed
      if ( (version.equals("0.91")) & (doc.getDocType() == null) ) {
        DocType rssDtd = new DocType  ("rss", "http://my.netscape.com/publish/formats/rss-0.91.dtd");
        doc.insertChild(0, rssDtd);
      }
      // Get the first (and only) <channel> element
      Element channel = rss.getFirstChildElement("channel");
      // Get its <title> element
      Element title = channel.getFirstChildElement("title");
      Text titleText = (Text)title.getChild(0);
     // Change the title to reflect the search term
     titleText.setData (titleText.getValue() + ": Search for " + searchTerm + " articles");
      // Get all of the <item> elements and loop through them
      Elements items = channel.getChildElements("item");
      for (int i = 0; i < items.size(); i++) {
        // Get an <item> element
         Element item = items.get(i);
        // Look for a <title> element inside it
        Element itemTitle = item.getFirstChildElement("title");
        // If found, look for its contents
        if (itemTitle != null) {
          Text itemTitleText = (Text) itemTitle.getChild(0);
          // If the search text is not found in the item,
          // delete it from the tree
          if (itemTitleText.toString().indexOf(searchTerm) == -1)
            channel.removeChild(item);
        }
      }
      // Display the results with a serializer
      output.setIndent(2);
      output.write(doc);
    } catch (Exception exc) {
      System.out.println("Error: " + exc.getMessage());
      exc.printStackTrace();
    }
  }
}

One good feed to use with the application is Linux Magazine's RSS feed of recent articles at feeds.cadenhead.org/workbench. To search the feed for the term "Java", use the command line java RssFilter http://www.linux-mag.com/lm.rss Java.

Curb the Curve

XOM's design is strongly informed by one overriding principle: enforced simplicity. "XOM by its nature should help inexperienced developers do the right thing and keep them from doing the wrong thing," explains Harold. "The learning curve needs to be really shallow, and that includes not relying on best practices that are known in the community, but are not obvious at first glance."

Though it's an open question whether XOM lives up to that lofty goal, the new class library is already a compelling option for Java programmers who are feeding their code a steady diet of XML.

Add a Comment

All comments are moderated before publication. These HTML tags are permitted: <p>, <b>, <i>, <a>, and <blockquote>. This site is protected by reCAPTCHA (for which the Google Privacy Policy and Terms of Service apply).