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.
|
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 9-6 is a good start at this, and provides a skeleton within which to work.
Example The skeleton of theXMLProperties class
package javaxml3; 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.Format; 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); } } |
Please note that I overloaded the load( ) and store( ) methods. While the Properties class only has versions that take an InputStream and OutputStream (respectively), I am a firm believer in providing users with 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.
Storing XML
Let's discuss 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 will simply reuse some of that code here:
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(Format.getPrettyFormat( )); 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" Document doc = new Document( ); Element root = new Element("properties"); doc.setRootElement(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 Format format = Format.getPrettyFormat( ); XMLOutputter outputter = new XMLOutputter(format); 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 is used here to add in the header parameter as a comment to the XML document being written to.
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, which 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.getTextNormalize( ); // 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. 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 is 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 getTextNormalize( ) method is used. This method returns the textual content of an element, and returns the element without surrounding whitespace.[*] 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:
[*] It also removes more than one space between words. The textual content "lots of spaces" would be returned through getTextNormalize( ) 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 getTextNormalize( ) 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.
Taking a Test Drive
Once everything is in place in the XMLProperties class, compile it. To test it out, you can enter in or download Example 9-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 Testing theXMLProperties class
package javaxml3; 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 outbut all in XML. You can run this program on the XML file generated by the PropsToXML class I showed you earlier in the chapter.
|
Supply the test program with the XML input file and the name of the output file:
C:\javaxml3\build>java javaxml3.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.
Backtracking
Before wrapping up on the code, there are a few items that should be addressed. 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 9-8 if you used the XML version of enhydra.properties detailed earlier in this chapter.
Example Output fromTestXMLProperties
<?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" Document doc = new Document( ); Element root = new Element("properties"); doc.setRootElement(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. We could add the comment before calling setRootElement( ), but to demonstrate another feature of JDOM, we'll use a new method, getContent( ). This method returns a List that 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 looks 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 Text objects 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)); } } }
After compiling the changes, you're set to deal with attribute values instead of element content. Leave the code in the state you prefer (as I mentioned earlier, I actually like the values as element content). 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 beginning to know your way around JDOM. And just like SAX, DOM, and StAX, I highly recommend tutorialmarking the JavaDoc (either locally or online) as a quick reference for those methods you just can't quite remember.