JDOM and Factories






JDOM and Factories

As noted earlier in this chapter, the ability 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 Figure.

Subclassing the JDOM Element class

package javaxml3;

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 in that 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.[] This is a simple case, but it gives you an idea of what is possible, and serves as a good example for this section.

[] 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.

Creating a Factory

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 is 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 unable 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 package, was introduced. This interface defines methods for every type of object creation. 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.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 Figure, which defines a custom factory.

A customJDOMFactory implementation

package javaxml3;

import org.jdom.DefaultJDOMFactory;
import org.jdom.Element; 
import org.jdom.JDOMFactory;
import org.jdom.Namespace;

class CustomJDOMFactory extends DefaultJDOMFactory implements JDOMFactory {

    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.

Building with Custom Classes

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 Figure. This simple class takes in an XML document and builds it using the ORAElement class and CustomJDOMFactory from Examples 9-12 and 9-13. It then writes the document back out to a supplied output filename, so you can see the effect of the custom classes.

Building with custom classes using a custom factory

package javaxml3;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import org.jdom.Document;
import org.jdom.JDOMException; 
import org.jdom.JDOMFactory;
import org.jdom.input.SAXBuilder;
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:

[email protected]
$ java javaxml3.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 Figure.

Output fragment from contents.xml afterElementChanger

<?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" label="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" label="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" label="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" label="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.

UncheckedJDOMFactory

JDOM includes an implementation of the JDOMFactory interface that warrants some attention: org.jdom.UncheckedJDOMFactory. When you create JDOM objects through the default factory or the new keyword, the object constructors do some basic checks to ensure that the objects follow XML standards. For example, trying to create a new Element with an invalid name produces an org.jdom.IllegalNameException:

Element el = new Element("0name");

This is definitely a good thing and worth the extra processing needed to do it, but when parsing a document with a modern parser, it is unnecessary. If you had this element in a file:

<book>
    <0name>Java &amp; XML</0name>
</book>

And tried to parse it, a good SAX parser will throw org.xml.sax.SAXParseException. If you are confident your parser will do this, you can skip JDOM's checks by using UncheckedJDOMFactory, as in:

SAXBuilder builder = new SAXBuilder(  );
builder.setFactory(new UncheckedJDOMFactory(  ));
// parse away

And get your parsing done in about 20 percent less time. UncheckedJDOMFactory should definitely be used sparingly and never when creating documents from user input.



 Python   SQL   Java   php   Perl 
 game development   web development   internet   *nix   graphics   hardware 
 telecommunications   C++ 
 Flash   Active Directory   Windows