Programming XML in Java, Part 3

DOMination: Take control of structured documents with the Document Object Model

1 2 3 Page 2
Page 2 of 3

Once parsed into a DOM tree, the entire document can be accessed randomly in memory. This is useful in cases where random access to the DOM nodes is necessary. As a sample program, this article will use a multiple-language flash card program, which loads an entire vocabulary from an XML file into a DOM tree using a DOM parser. The program then quizzes the user on the vocabulary encoded in the DOM tree, selecting words from the language the user has chosen. Let's have a look at the sample code.

A DOM example

The main class in the example program is called FlashCard. To display the words to the user, a simple JavaBean, called FlashCardCanvas, is embedded in FlashCard's main panel. The FlashCardCanvas JavaBean is responsible for showing the value of its single property, string, in its drawable area in the font size that fits the canvas width. It also draws red and blue lines that make the FlashCardCanvas area look like an index card.

The input vocabulary is controlled by the input file, vocab.xml. The XML in vocab.xml is described by vocabulary.dtd.

Both of these files are included in the source files in the Resources section at the end of this article.

The running program looks like the image in Figure 5:

Figure 5. The FlashCard program in action

Almost all of the program logic and XML processing code occurs in the FlashCard class, so I'll focus on that. (The source code for the entire example can be downloaded from Resources. You can find the source code for the FlashCardCanvas class in the archive.)

FlashCard's method vParseArgs(), called by FlashCard's main method, sets the vocabulary for the program and the language to quiz. These two parameters are based on the two command-line arguments, which are the vocabulary filename (or URL) and one of the languages available (Italian, German, English, French, or Spanish). The program displays English words and prompts for the translation in the given language. The setLanguage() method simply sets the internal class field _sLanguage, but the real work occurs in the setVocabulary method, where the XML file is parsed and the DOM tree is built. Listing 2 below is the setVocabulary method for the FlashCard class.

Listing 2. The setVocabulary method parses the XML file

925 /**
926  * Set vocabulary by parsing XML file.
927  * @param sFilename java.lang.String
928  */
929 public void setVocabulary(String sFilename) {
930     // Create an instance of the DocumentBuilderFactory
931     DocumentBuilderFactory docBuilderFactory =DocumentBuilderFactory.newInstance();
932     docBuilderFactory.setValidating(true);
933     try {
934             //Get the DocumentBuilder from the factory that we just got above.
935             DocumentBuilder docBuilder =docBuilderFactory.newDocumentBuilder();
936             docBuilder.setErrorHandler(new org.xml.sax.ErrorHandler(){ // ignore fatal errors (an exception is guaranteed)
937                     public void fatalError(SAXParseExceptionexception) throws SAXException {
938                     }
939 
940                     // treat validation errors as fatal
941                     public void error(SAXParseException e) throwsSAXParseException {
942                             throw e;
943                     }
944 
945                     // dump warnings too
946                     public void warning(SAXParseException err) throws SAXParseException {
947                             System.out.println("** Warning" + ", line " + err.getLineNumber() + ", uri " +err.getSystemId());
948                             System.out.println("   " + err.getMessage());
949                     }
950             });
951 
952             // turn it into an in-memory object
953             setVocabulary(docBuilder.parse(new File(sFilename)));
954 
955     } catch (Exception ex) {
956             System.err.println("Exception: " + ex);
957             System.exit(2);
958     }
959 
960     // Missed assignment of _vocabulary AND size if parse fails.
961     if (_iVocSize < 0) {
962             System.out.println("Parse successful");
963     } else {
964             System.out.println("Parse of file " + sFilename+ " failed");
965     }
966 }
967 /**
968  * Set the vocabulary as a DOM Document
969  * @param vocabulary org.w3c.dom.Document
970  */
971 public void setVocabulary(Document vocabulary) {
972     _vocabulary = vocabulary;
973     _iVocSize = -1;
974     showFirstCueWord();
975 }

As you can see, the setVocabulary method parses the XML file into a DOM tree. The sample program uses the Sun XML parser to create the DOM structure. If you want to use a different parser, you may need to take other steps to create it. The DocumentBuilderFactory is a singleton that you can use to create parsers. Line 935 in Listing 2 shows a DocumentBuilder created by the DocumentBuilderFactory.

Lines 936 through 950 construct an anonymous error handler with the parser. Line 953 then calls the setVocabulary method, which sets the FlashCard class' vocabulary property to the DOM Document returned by the XML parser. Lines 961 through 966 check whether any words are loaded from the input XML file by checking the word count field _iVocSize, which is set by the setVocabulary method. Notice that the parse of the XML file occurs in the expression, which is the argument to setVocabulary. Parsing the input file in building the tree is handled entirely by the call to docBuilder.parse().

The setVocabulary method in lines 971 to 975 sets an internal field _vocabulary to the Document loaded by the parser. The other methods in this class are implemented in terms of this document tree. For an example, see the method setVocabularySize in Listing 3.

Listing 3. The method setVocabularySize() counts the number of WORD nodes in the vocabulary

744 /**
745  * Return the number of words in this vocabulary.
746  * @return int
747  */
748 public int getVocabularySize() {
749     if (_iVocSize >= 0)
750         return _iVocSize;
751
752     Element voc = getVocabulary();
753     if (voc == null)
754         return 0;
755     NodeList nl = voc.getElementsByTagName("WORD");
756     _iVocSize = nl.getLength();
757     return _iVocSize;
758 }

The operation of getVocabularySize() is really quite simple. The method calculates the number of words in the vocabulary by counting the number of WORD nodes that appear under the vocabulary tag. Line 755 calls the Document method getElementsByTagName(), which returns a NodeList of all of the nodes matching the name given in the argument. Since WORD occurs once for every word in the vocabulary, the length of the NodeList (acquired in line 756) accounts for the number of words in the vocabulary. The NodeList returned by getElementsByTagName() is itself a DOM interface. A NodeList, reasonably enough, represents a list of Nodes. It has two methods: getLength(), which returns the number of nodes in the list, and item(), which returns a list element by index, starting with 0.

Listing 4. The getVocabularyWord() method returns a specific word

759 /**
760  * Return the ith vocabulary word, where 0 <= i < getVocabularySize()
761  * @return int
762  */
763 public VocabularyWord getVocabularyWord(int i) {
764     Element voc = getVocabulary();
765     if (voc == null)
766         return null;
767     NodeList nl = voc.getElementsByTagName("WORD");
768 
769     if (i < 0 || i >= nl.getLength())
770         return null;
771 
772     Element eWord = (Element)(nl.item(i));
773     VocabularyWord vwResult = new VocabularyWord(eWord);
774     return vwResult;
775 }

getVocabularyWord() in Listing 4 uses the item() method of NodeList. getVocabularyWord() is very similar to the method in Listing 3; however, instead of returning the list length, it returns the item number i. You'll notice that the application methods are being written in terms of the DOM tree.

VocabularyWord is a convenience class I created for this application. It represents a single word in a vocabulary. It is constructed with a single Element, which must have the word tag. Listing 5 below shows the method VocabularyWord.getLanguages(), an excellent example of DOM programming.

Listing 5. Using DOM to implement VocabularyWord.getLanguages()

021 /**
022  * Return the list of strings for translation languages of current
word. Return NULL
023  * if none.
024  * @return java.lang.String[]
025  */
026 public String[] getLanguages() {
027     String[] asResult = null;
028     if (_eWord != null) {
029         NodeList nl = _eWord.getChildNodes();
030         int iLanguages = 0;
031         for (int i = 0; i < nl.getLength(); i++) {
032             if (nl.item(i) instanceof Element) {
033                 iLanguages++;
034             }
035         }
036         asResult = new String[iLanguages];
037         int j = 0;
038         for (int i = 0; i < nl.getLength(); i++) {
039             if (nl.item(i) instanceof Element) {
040                 asResult[j++] = ((Element)(nl.item(i))).getTagName();
041             }
042         }
043     }
044     return asResult;
045

The getLanguages() method in Listing 5 returns an array of strings, which is the list of available translations for a particular VocabularyWord. Line 29 gets the child nodes for the Element upon which the VocabularyWord is based. Lines 31 through 34 count the child nodes of the word Element, which are themselves Elements. This is necessary because some of the child nodes of the word Element may be Comment nodes, Text nodes containing white space, and so on. (You could, of course, check to be sure that each of these child Elements has a tag whose value is a valid language. But this is unnecessary because the DTD enforces the rule that a word's subelements are limited to valid language names like French, German, and so on. This is a great example of how using a DTD can free you from a lot of unnecessary coding -- here, the DTD does the checking for you.)

Line 36 allocates space for the result string list, and lines 38 through 42 collect the tag names of all Elements under the word Element and places them in the result list. Notice that in line 40 the result of the expression nl.item(i) has been type coerced to Element. Line 39 has just verified that item i is an Element. Why the type coercion? The item() method of NodeList returns a Node, not the Element, and so the type coercion is necessary in order to call getTagName(), which appears only in Element. I know the type coercion will succeed because of line 39.

Most of the code in this example is generated by my IDE and associates user events with method calls. The methods I've shown here demonstrate how to use the DOM interface calls to manipulate data from an XML document represented as a DOM tree in memory. Additional examples of how to manipulate data in a DOM tree appear in the static methods of the DomUtils convenience class (available in the source archive, which is downloadable in Resources). Reading the source code for DomUtils, try to figure out how the methods work by referring to the DOM documentation (also linked in Resources).

While the DOM is a general way to access XML structures, it also has some limitations. Let's take a quick look at some of them.

A word about JDOM

If you read JavaWorld regularly, you've probably read "Easy Java/XML integration with JDOM, Part 1," by Jason Hunter and Brett McLaughlin (see Resources). Hunter and McLaughlin found DOM difficult to use, so they created an open source Java API called JDOM that is designed to simplify XML manipulation in Java. DOM is a rather clunky API because it was designed to be usable from several different programming languages. JDOM, on the other hand, is specifically designed to take advantage of Java language features. You should understand DOM, since many systems use it, but you may find that JDOM is a much easier tool to use for manipulating XML in Java. Definitely check it out, and watch JavaWorld for upcoming articles on JDOM.

Limits to the Document Object Model

One of the most common complaints about DOM concerns the fact that it requires an entire document to be in memory. Though this is widely believed, it is not strictly true, because the DOM is only a package of interfaces, not implementations. Keeping all of the data in an XML file is not the only way to implement the DOM tree. For example, a parser could scan the input document and internally save just the tag names, attributes, and other elements that define the document structure, as well as seek positions of the document's text blocks. This would allow random access to all of the text blocks in the original XML document, albeit with a performance penalty caused by the need to seek and lazily read requested text blocks. Still, the most common implementations of DOM interfaces do indeed slurp the whole file into memory. This approach can cause performance and/or memory problems for large input documents.

A more interesting objection to DOM use is that parsers return objects that implement the DOM interface and do little else. If you look at my sample code, for example, my VocabularyWord class wraps a DOM Element object instance, essentially extending the Element's functionality. But why should my VocabularyWord object have to contain some other object whose only purpose in life is to be an Element? In the worst case, you will end up with two trees: a tree of DOM-implementing objects, and a parallel tree of application objects. This is indeed a rather tedious way to code.

Fortunately, it is possible to write application objects (such as VocabularyWord) that implement DOM interfaces directly and then configure the parser to return trees of application objects instead of trees of DOM-implementing objects. You can also build DOM trees directly in memory, in code, without an XML source document. I'll show how to do both of those things in the next article in this series on advanced DOM programming. Until then, enjoy experimenting with programming DOM in Java.

Related:
1 2 3 Page 2
Page 2 of 3