Book HomeJava and XML, 2nd EditionSearch this book

7.3. XMLProperties

Let's take things to the next logical step, and look at reading XML. Continuing with the example of converting a properties file to XML, you are now probably wondering how to access the information in your XML file. Luckily, there's a solution for that, too! In this section, for the sake of explaining how JDOM reads XML, I want to introduce a new utility class, XMLProperties. This class is essentially an XML-aware version of the Java Properties class; in fact, it extends that class. This class allows access to an XML document through the typical property-access methods like getProperty( ) and properties( ); in other words, it allows Java-style access (using the Properties class) to XML-style storage. In my opinion, this is the best combination you can get.

To accomplish this task, you can start by creating an XMLProperties class that extends the java.util.Properties class. With this approach, making things work becomes simply a matter of overriding the load( ), save( ), and store( ) methods. The first of these, load( ) , reads in an XML document and loads the properties within that document into the superclass object.

WARNING: Don't mistake this class for an all-purpose XML-to-properties converter; it only will read in XML that is in the format detailed earlier in this chapter. In other words, properties are elements with either textual or attribute values but not both; I'll cover both approaches, but you'll have to choose one or the other. Don't try to take all your XML documents, read them in, and expect things to work as planned!

The second method, save( ) , is actually deprecated in Java 2, as it doesn't expose any error information; still, it needs to be overridden for Java 1.1 users. To facilitate this, the implementation in XMLProperties simply calls store( ) . And store( ) handles the task of writing the properties information out to an XML document. Example 7-6 is a good start at this, and provides a skeleton within which to work.

Example 7-6. The skeleton of the XMLProperties class

package javaxml2;

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;

import org.jdom.Attribute;
import org.jdom.Comment;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.jdom.output.XMLOutputter;

public class XMLProperties extends Properties {
    
    public void load(Reader reader) 
        throws IOException {
         
        // Read XML document into a Properties object    
    }    

    public void load(InputStream inputStream) 
        throws IOException {
         
        load(new InputStreamReader(inputStream));    
    }
    
    public void load(File xmlDocument) 
        throws IOException {
        
        load(new FileReader(xmlDocument));    
    }  

    public void save(OutputStream out, String header) {
        try {            
            store(out, header);
        } catch (IOException ignored) {
            // Deprecated version doesn't pass errors
        }        
    }   
     
    public void store(Writer writer, String header)
        throws IOException {
            
        // Convert properties to XML and output
    }    
          
    public void store(OutputStream out, String header)
        throws IOException {
            
        store(new OutputStreamWriter(out), header);
    }
 
    public void store(File xmlDocument, String header)
        throws IOException {
            
        store(new FileWriter(xmlDocument), header);
    }    
}

Take note that I overloaded the load( ) and store( ) methods; while the Properties class only has versions that take an InputStream and OutputStream (respectively), I'm a firm believer in providing users options. The extra versions, which take Files and Readers/Writers, make it easier for users to interact, and add a marginal amount of code to the class. Additionally, these overloaded methods can all delegate to existing methods, which leaves the code ready for loading and storing implementation.

7.3.1. Storing XML

I'll deal with storing XML first, mainly because the code is already written. The logic to take a Properties object and output it as XML is the purpose of the PropsToXML class, and I'll simply reuse some of that code here to make things work nicely:

    public void store(Writer writer, String header)
        throws IOException {
            
        // Create a new JDOM Document with a root element "properties"
        Element root = new Element("properties");
        Document doc = new Document(root);
        
        // Get the property names
        Enumeration propertyNames = propertyNames( );
        while (propertyNames.hasMoreElements( )) {
            String propertyName = (String)propertyNames.nextElement( );
            String propertyValue = getProperty(propertyName);
            createXMLRepresentation(root, propertyName, propertyValue);
        }        
        
        // Output document to supplied filename
        XMLOutputter outputter = new XMLOutputter("  ", true);
        outputter.output(doc, writer);
    }

    private void createXMLRepresentation(Element root, 
                                         String propertyName,
                                         String propertyValue) {
        
        int split;
        String name = propertyName;
        Element current = root;
        Element test = null;
              
        while ((split = name.indexOf(".")) != -1) {
            String subName = name.substring(0, split);
            name = name.substring(split+1);
            
            // Check for existing element            
            if ((test = current.getChild(subName)) == null) {
                Element subElement = new Element(subName);
                current.addContent(subElement);
                current = subElement;
            } else {
                current = test;
            }
        }
        
        // When out of loop, what's left is the final element's name        
        Element last = new Element(name);                        
        last.setText(propertyValue);
        /** Uncomment this for Attribute usage */
        /*
        last.setAttribute("value", propertyValue);
        */
        current.addContent(last);
    }

Not much needs comment. There are a few lines of code highlighted to illustrate some changes, though. The first two changes ensure that the superclass is used to obtain the property names and values, rather than the Properties object that was passed into the version of this method in PropsToXML. The third change moves from using a string filename to the supplied Writer for output. With those few modifications, you're all set to compile the XMLProperties source file.

There is one item missing, though. Note that the store( ) method allows specification of a header variable; in a standard Java properties file, this is added as a comment to the head of the file. To keep things parallel, the XMLProperties class can be modified to do the same thing. You will need to use the Comment class to do this. The following code additions put this change into effect:

    public void store(Writer writer, String header)
        throws IOException {
            
        // Create a new JDOM Document with a root element "properties"
        Element root = new Element("properties");
        Document doc = new Document(root);
        
        // Add in header information
        Comment comment = new Comment(header);
        doc.addContent(comment);
        
        // Get the property names
        Enumeration propertyNames = propertyNames( );
        while (propertyNames.hasMoreElements( )) {
            String propertyName = (String)propertyNames.nextElement( );
            String propertyValue = getProperty(propertyName);
            createXMLRepresentation(root, propertyName, propertyValue);
        }        
        
        // Output document to supplied filename
        XMLOutputter outputter = new XMLOutputter("  ", true);
        outputter.output(doc, writer);
    }

The addContent( ) method of the Document object is overloaded to take both Comment and ProcessingInstruction objects, and appends the content to the file. It's used here to add in the header parameter as a comment to the XML document being written to.

7.3.2. Loading XML

There's not much left to do here; basically, the class writes out to XML, provides access to XML (through the methods already existing on the Properties class), and now simply needs to read in XML. This is a fairly simple task; it boils down to more recursion. I'll show you the code modifications needed, and then walk you through them. Enter the code shown here into your XMLProperties.java source file:

    public void load(Reader reader) 
        throws IOException {
        
        try { 
            // Load XML into JDOM Document
            SAXBuilder builder = new SAXBuilder( );
            Document doc = builder.build(reader);
            
            // Turn into properties objects
            loadFromElements(doc.getRootElement().getChildren( ), 
                new StringBuffer(""));
            
        } catch (JDOMException e) {
            throw new IOException(e.getMessage( ));
        }        
    }

    private void loadFromElements(List elements, StringBuffer baseName) {
        // Iterate through each element
        for (Iterator i = elements.iterator(); i.hasNext( ); ) {
            Element current = (Element)i.next( );
            String name = current.getName( );
            String text = current.getTextTrim( );
            
            // Don't add "." if no baseName
            if (baseName.length( ) > 0) {
                baseName.append(".");
            }            
            baseName.append(name);
            
            // See if we have an element value
            if ((text == null) || (text.equals(""))) {
                // If no text, recurse on children
                loadFromElements(current.getChildren( ),
                                 baseName);
            } else {                
                // If text, this is a property
                setProperty(baseName.toString( ), 
                            text);
            }            
            
            // On unwind from recursion, remove last name
            if (baseName.length() == name.length( )) {
                baseName.setLength(0);
            } else {                
                baseName.setLength(baseName.length( ) - 
                    (name.length( ) + 1));
            }            
        }        
    }

The implementation of the load( ) method (which all overloaded versions delegate to) uses SAXBuilder to read in the supplied XML document. I discussed this earlier in the chapter, and I'll look at it in even more detail in the next; for now, it's enough to realize that it simply reads XML into a JDOM Document object.

The name for a property consists of the names of all the elements leading to the property value, with a period separating each name. Here's a sample property in XML:

<properties>
  <org>
    <enhydra>
      <classpath>"."</classpath>
    </enhydra>
  </org>
</properties>

The property name can be obtained by taking the element names leading to the value (excluding the properties element, which was used as a root-level container): org, enhydra, and classpath. Throw a period between each, and you get org.enhydra.classpath, which is the property name in question. To accomplish this, I coded up the loadFromElements( ) method. This takes in a list of elements, iterates through them, and deals with each element individually. If the element has a textual value, that value is added to the superclass object's properties. If it has child elements instead, then the children are obtained, and recursion begins again on the new list of children. At each step of recursion, the name of the element being dealt with is appended to the baseName variable, which keeps track of the property names. Winding through recursion, baseName would be org, then org.enhydra, then org.enhydra.classpath. And, as recursion unwinds, the baseName variable is shortened to remove the last element name. Let's look at the JDOM method calls that make it possible.

First, you'll notice several invocations of the getChildren( ) method on instances of the Element class. This method returns all child elements of the current element as a Java List. There are versions of this method that also take in the name of an element to search for, and return either all elements with that name (getChildren(String name)), or just the first child element with that name (getChild(String name)). There are also namespace-aware versions of the method, which will be covered in the next chapter. To start the recursion process, the root element is obtained from the JDOM Document object through the getRootElement( ) method, and then its children are used to seed recursion. Once in the loadFromElements( ) method, standard Java classes are used to move through the list of elements (such as java.util.Iterator). To check for textual content, the getTextTrim( ) method is used. This method returns the textual content of an element, and returns the element without surrounding whitespace.[11] Thus, the content " textual content " (note the surrounding whitespace) would be returned as "textual content". While this seems somewhat trivial, consider this more realistic example of XML:

[11]It also removes more than one space between words. The textual content "lots of spaces" would be returned through getTextTrim( ) as "lots of spaces".

<chapter>
  <title>
    Advanced SAX
  </title>
</chapter>

The actual textual content of the title element turns out to be several spaces, followed by a line feed, followed by more space, the characters "Advanced SAX", more space, another line feed, and even more space. In other words, probably not what you expected. The returned string data from a call to getTextTrim( ) would simply be "Advanced SAX", which is what you want in most cases anyway. However, if you do want the complete content (often used for reproducing the input document exactly as it came in), you can use the getText( ) method, which returns the element's content unchanged. If there is no content, the return value from this method is an empty string (""), which makes for an easy comparison, as shown in the example code. And that's about it: a few simple method calls and the code is reading XML with JDOM. Let's see this class in action.

7.3.3. Taking a Test Drive

Once you've got everything in place in the XMLProperties class, compile it. To test it out, you can enter in or download Example 7-7, which is a class that uses the XMLProperties class to load an XML document, print some information out about it, and then write the properties back out as XML.

Example 7-7. Testing the XMLProperties class

package javaxml2;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.Enumeration;

public class TestXMLProperties {

    public static void main(String[] args) {
        if (args.length != 2) {
            System.out.println("Usage: java javaxml2.TestXMLProperties " +
                "[XML input document] [XML output document]");
            System.exit(0);
        }
    
        try {
            // Create and load properties
            System.out.println("Reading XML properties from " + args[0]);
            XMLProperties props = new XMLProperties( );
            props.load(new FileInputStream(args[0]));
            
            // Print out properties and values
            System.out.println("\n\n---- Property Values ----");
            Enumeration names = props.propertyNames( );
            while (names.hasMoreElements( )) {
                String name = (String)names.nextElement( );
                String value = props.getProperty(name);
                System.out.println("Property Name: " + name + 
                                   " has value " + value);
            }            
            
            // Store properties
            System.out.println("\n\nWriting XML properies to " + args[1]);
            props.store(new FileOutputStream(args[1]),
                "Testing XMLProperties class");
        } catch (Exception e) {
            e.printStackTrace( );
        }
    }
}

This doesn't do much; it reads in properties, uses them to print out all the property names and values, and then writes those properties back out -- but all in XML. You can run this program on the XML file generated by the PropsToXML class I showed you earlier in the chapter.

WARNING: The version of XMLProperties used here deals with property values as textual content of elements (the first version of PropsToXML shown), not as attribute values (the second version of PropsToXML). You'll need to use that earlier version of PropsToXML, or back out your changes, if you are going to use it to generate XML for input to the TestXMLProperties class. Otherwise, you won't pick up any property values with this code.

Supply the test program with the XML input file and the name of the output file:

C:\javaxml2\build>java javaxml2.TestXMLProperties enhydraProps.xml output.xml
Reading XML properties from enhydraProps.xml


---- Property Values ----
Property Name: org.enhydra.classpath.separator has value ":"
Property Name: org.enhydra.initialargs has value "./bootstrap.conf"
Property Name: org.enhydra.initialclass has value 
  org.enhydra.multiServer.bootstrap.Bootstrap
Property Name: org.enhydra.classpath has value "."
Property Name: org.xml.sax.parser has value 
  "org.apache.xerces.parsers.SAXParser"


Writing XML properties to output.xml

And there you have it: XML data formatting, properties behavior.

7.3.4. Backtracking

Before wrapping up on the code, there are a few items I want to address. First, take a look at the XML file generated by TestXMLProperties , the result of invoking store( ) on the properties. It should look similar to Example 7-8 if you used the XML version of enhydra.properties detailed earlier in this chapter.

Example 7-8. Output from TestXMLProperties

<?xml version="1.0" encoding="UTF-8"?>
<properties>
  <org>
    <enhydra>
      <classpath>
        <separator>":"</separator>
      </classpath>
      <initialargs>"./bootstrap.conf"</initialargs>
      <initialclass>org.enhydra.multiServer.bootstrap.Bootstrap</initialclass>
      <classpath>"."</classpath>
    </enhydra>
    <xml>
      <sax>
        <parser>"org.apache.xerces.parsers.SAXParser"</parser>
      </sax>
    </xml>
  </org>
</properties>
<!--Testing XMLProperties class-->

Notice anything wrong? The header comment is in the wrong place. Take another look at the code that added in that comment, from the store( ) method:

        // Create a new JDOM Document with a root element "properties"
        Element root = new Element("properties");
        Document doc = new Document(root);
        
        // Add in header information
        Comment comment = new Comment(header);
        doc.addContent(comment);

The root element appears before the comment because it is added to the Document object first. However, the Document object can't be created without supplying a root element -- a bit of a chicken-or-egg situation. To work with this, you need to use a new method, getContent( ) . This method returns a List, but that List contains all the content of the Document, including comments, the root element, and processing instructions. Then, you can prepend the comment to this list, as shown here, using methods of the List class:

        // Add in header information
        Comment comment = new Comment(header);
        doc.getContent( ).add(0, comment);

With this change in place, your output will look as it should:

<?xml version="1.0" encoding="UTF-8"?>
<!--Testing XMLProperties class-->
<properties>
  <org>
    <enhydra>
      <classpath>
        <separator>":"</separator>
      </classpath>
      <initialargs>"./bootstrap.conf"</initialargs>
      <initialclass>org.enhydra.multiServer.bootstrap.Bootstrap</initialclass>
      <classpath>"."</classpath>
    </enhydra>
    <xml>
      <sax>
        <parser>"org.apache.xerces.parsers.SAXParser"</parser>
      </sax>
    </xml>
  </org>
</properties>

The getContent( ) method is also available on the Element class, and returns all content of the element, regardless of type (elements, processing instructions, comments, entities, and Strings for textual content).

Also important are the modifications necessary for XMLProperties to use attributes for property values, instead of element content. You've already seen the code change needed in storage of properties (in fact, the change is commented out in the source code, so you don't need to write anything new). As for loading, the change involves checking for an attribute instead of an element's textual content. This can be done with the getAttributeValue(String name) method, which returns the value of the named attribute, or null if no value exists. The change is shown here:

    private void loadFromElements(List elements, StringBuffer baseName) {
        // Iterate through each element
        for (Iterator i = elements.iterator(); i.hasNext( ); ) {
            Element current = (Element)i.next( );
            String name = current.getName( );
            // String text = current.getTextTrim( );
            String text = current.getAttributeValue("value");            
            
            // Don't add "." if no baseName
            if (baseName.length( ) > 0) {
                baseName.append(".");
            }            
            baseName.append(name);
            
            // See if we have an attribute value
            if ((text == null) || (text.equals(""))) {
                // If no text, recurse on children
                loadFromElements(current.getChildren( ),
                                 baseName);
            } else {                
                // If text, this is a property
                setProperty(baseName.toString( ), 
                            text);
            }            
            
            // On unwind from recursion, remove last name
            if (baseName.length() == name.length( )) {
                baseName.setLength(0);
            } else {                
                baseName.setLength(baseName.length( ) - 
                    (name.length( ) + 1));
            }            
        }        
    }    

Compile in the changes, and you're set to deal with attribute values instead of element content. Leave the code in the state you prefer it (as I mentioned earlier, I actually like the values as element content), so if you want textual element content, be sure to back out these changes after seeing how they affect output. Whichever you prefer, hopefully you are starting to know your way around JDOM. And just like SAX and DOM, I highly recommend bookmarking the Javadoc (either locally or online) as a quick reference for those methods you just can't quite remember. In any case, before wrapping up, let's talk a little about a common issue with regard to JDOM: standardization.



Library Navigation Links

Copyright © 2002 O'Reilly & Associates. All rights reserved.