Moving right along, recall the discussion from the last chapter on JDOM and factories. I mentioned that you would never see code like this (at least with the current versions) in JDOM applications:
// This code does not work!! JDOMFactory factory = new JDOMFactory( ); factory.setDocumentClass("javaxml2.BrettsDocumentClass"); factory.setElementClass("javaxml2.BrettsElementClass"); Element rootElement = JDOMFactory.createElement("root"); Document document = JDOMFactory.createDocument(rootElement);
Well, that remains true. However, I glossed over some pretty important aspects of that discussion, and want to pick it up again here. As I mentioned in Chapter 7, "JDOM", being able to have some form of factories allows greater flexibility in how your XML is modeled in Java. Take a look at the simple subclass of JDOM's Element class shown in Example 8-1.
package javaxml2; import org.jdom.Element; import org.jdom.Namespace; public class ORAElement extends Element { private static final Namespace ORA_NAMESPACE = Namespace.getNamespace("ora", "http://www.oreilly.com"); public ORAElement(String name) { super(name, ORA_NAMESPACE); } public ORAElement(String name, Namespace ns) { super(name, ORA_NAMESPACE); } public ORAElement(String name, String uri) { super(name, ORA_NAMESPACE); } public ORAElement(String name, String prefix, String uri) { super(name, ORA_NAMESPACE); } }
This is about as simple a subclass as you could come up with; it is somewhat similar to the NamespaceFilter class from Chapter 4, "Advanced SAX ". It disregards whatever namespace is actually supplied to the element (even if there isn't a namespace supplied!), and sets the element's namespace defined by the URI http://www.oreilly.com with the prefix ora.[12] This is a simple case, but it gives you an idea of what is possible, and serves as a good example for this section.
[12]It is slightly different from NamespaceFilter in that it changes all elements to a new namespace, rather than just those elements with a particular namespace.
Once you've got a custom subclass, the next step is actually using it. As I already mentioned, JDOM considers having to create all objects with factories a bit over-the-top. Simple element creation in JDOM works like this:
// Create a new Element Element element = new Element("guitar");
Things remain equally simple with a custom subclass:
// Create a new Element, typed as an ORAElement Element oraElement = new ORAElement("guitar");
The element is dropped into the O'Reilly namespace because of the custom subclass. Additionally, this method is more self-documenting than using a factory. It's clear at any point exactly what classes are being used to create objects. Compare that to this code fragment:
// Create an element: what type is created? Element someElement = doc.createElement("guitar");
It's not clear if the object created is an Element instance, an ORAElement instance, or something else entirely. For these reasons, the custom class approach serves JDOM well. For object creation, you can simply instantiate your custom subclass directly. However, the need for factories arises when you are building a document:
// Build from an input source SAXBuilder builder = new SAXBuilder( ); Document doc = builder.build(someInputStream);
Obviously, here you were not able to specify custom classes through the building process. I suppose you could be really bold and modify the SAXBuilder class (and the related org.jdom.input.SAXHandler class), but that's a little ridiculous. So, to facilitate this, the JDOMFactory interface, in the org.jdom.input package, was introduced. This interface defines methods for every type of object creation (see Appendix A, "API Reference" for the complete set of methods). For example, there are four methods for element creation, which match up to the four constructors for the Element class:
public Element element(String name); public Element element(String name, Namespace ns); public Element element(String name, String uri); public Element element(String name, String prefix, String uri);
You will find similar methods for Document, Attribute, CDATA, and all the rest. By default, JDOM uses the org.jdom.input.DefaultJDOMFactory, which simply returns all of the core JDOM classes within these methods. However, you can easily subclass this implementation and provide your own factory methods. Look at Example 8-2, which defines a custom factory.
package javaxml2; import org.jdom.Element; import org.jdom.Namespace; import org.jdom.input.DefaultJDOMFactory; class CustomJDOMFactory extends DefaultJDOMFactory { public Element element(String name) { return new ORAElement(name); } public Element element(String name, Namespace ns) { return new ORAElement(name, ns); } public Element element(String name, String uri) { return new ORAElement(name, uri); } public Element element(String name, String prefix, String uri) { return new ORAElement(name, prefix, uri); } }
This is a simple implementation; it doesn't need to be very complex. It overrides each of the element( ) methods and returns an instance of the custom subclass, ORAElement, instead of the default JDOM Element class. At this point, any builder that uses this factory will end up with ORAElement instances in the created JDOM Document object, rather than the default Element instances you would normally see. All that's left is to let the build process know about this custom factory.
Once you have a valid implementation of JDOMFactory, let your builders know to use it by invoking the setFactory( ) method and passing in a factory instance. This method is available on both of the current JDOM builders, SAXBuilder and DOMBuilder. To see it in action, check out Example 8-3. This simple class takes in an XML document and builds it using the ORAElement class and CustomJDOMFactory from Example 8-1 and Example 8-2. It then writes the document back out to a supplied output filename, so you can see the effect of the custom classes.
package javaxml2; import java.io.File; import java.io.FileWriter; import java.io.IOException; import org.jdom.Document; import org.jdom.JDOMException; import org.jdom.input.SAXBuilder; import org.jdom.input.JDOMFactory; import org.jdom.output.XMLOutputter; public class ElementChanger { public void change(String inputFilename, String outputFilename) throws IOException, JDOMException { // Create builder and set up factory SAXBuilder builder = new SAXBuilder( ); JDOMFactory factory = new CustomJDOMFactory( ); builder.setFactory(factory); // Build document Document doc = builder.build(inputFilename); // Output document XMLOutputter outputter = new XMLOutputter( ); outputter.output(doc, new FileWriter(new File(outputFilename))); } public static void main(String[] args) { if (args.length != 2) { System.out.println("Usage: javaxml2.ElementChanger " + "[XML Input Filename] [XML Output Filename]"); return; } try { ElementChanger changer = new ElementChanger( ); changer.change(args[0], args[1]); } catch (Exception e) { e.printStackTrace( ); } } }
I ran this on the contents.xml file used throughout the first several chapters:
bmclaugh@GANDALF $ java javaxml2.ElementChanger contents.xml newContents.xml
This hummed along for a second, and then gave me a new document (newContents.xml). A portion of that new document is shown in Example 8-4.
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE book SYSTEM "DTD/JavaXML.dtd"> <!-- Java and XML Contents --> <ora:book xmlns:ora="http://www.oreilly.com"> <ora:title ora:series="Java">Java and XML</ora:title> <!-- Chapter List --> <ora:contents> <ora:chapter title="Introduction" number="1"> <ora:topic name="XML Matters" /> <ora:topic name="What's Important" /> <ora:topic name="The Essentials" /> <ora:topic name="What's Next?" /> </ora:chapter> <ora:chapter title="Nuts and Bolts" number="2"> <ora:topic name="The Basics" /> <ora:topic name="Constraints" /> <ora:topic name="Transformations" /> <ora:topic name="And More..." /> <ora:topic name="What's Next?" /> </ora:chapter> <ora:chapter title="SAX" number="3"> <ora:topic name="Getting Prepared" /> <ora:topic name="SAX Readers" /> <ora:topic name="Content Handlers" /> <ora:topic name="Gotcha!" /> <ora:topic name="What's Next?" /> </ora:chapter> <ora:chapter title="Advanced SAX" number="4"> <ora:topic name="Properties and Features" /> <ora:topic name="More Handlers" /> <ora:topic name="Filters and Writers" /> <ora:topic name="Even More Handlers" /> <ora:topic name="Gotcha!" /> <ora:topic name="What's Next?" /> </ora:chapter> <!-- Other chapters --> </ora:book>
Each element is now in the O'Reilly namespace, prefixed and referencing the URI specified in the ORAElement class.
Obviously, you can take this subclassing to a much higher degree of complexity. Common examples include adding specific attributes or even child elements to every element that comes through. Many developers have existing business interfaces, and define custom JDOM classes that extend the core JDOM classes and also implement these business-specific interfaces. Other developers have built "lightweight" subclasses that discard namespace information and maintain only the bare essentials, keeping documents small (albeit not XML-compliant in some cases). The only limitations are your own ideas in subclassing. Just remember to set up your own factory before building documents, so your new functionality is included.
Copyright © 2002 O'Reilly & Associates. All rights reserved.