We finish this chapter up as we have finished our others: with a look
at issues that affect using the tools in this chapter in a real-world
situation. In this chapter, these include threading issues with
writing XML data, alternatives to using JDOM for writing XML data,
and handling lost references to the
XmlRpcConfiguration
utility class.
Although we sped
through our look at the saveConfiguration( )
method and how it handled writing out XML, you may have noticed
something key in the method declaration:
/**
* <p>
* This will save the current state out to the specified
* <code>OutputStream</code>.
* </p>
*
* @throws <code>IOException</code> - when errors occur in saving.
*/
public synchronized
void saveConfiguration(OutputStream out)
throws IOException {
// Method implementation
}
We use the
synchronized
keyword here to ensure that the lock
for our XmlRpcConfiguration
object is obtained
before the configuration data is written. This is particularly
important when using XML, as APIs like JDOM can be built on
implementations that periodically reload the underlying data; in
other words, changes by other programs to the XML data could cause
serious errors or corrupted data if this method writes to the data as
well.
Additionally, you should be very careful when multiple applications write to the same XML data source. Entire database systems have been written to handle pessimistic and optimistic locking, sharing data, and the other complex issues that surround concurrent access to data. While XML addressed portable data, it certainly does not address issues such as this. A good rule of thumb is to provide one single point of entry for writing to any single XML data source. Multiple entry points for mutability can cause tricky bugs and serious headaches.
In Chapter 11, we discussed the alternatives to using JDOM for reading XML data. We now examine alternatives for writing and modifying XML. This provides you a complete look at the Java APIs, and should allow you to use any API you choose for your various projects.
For comparison, here is the saveConfiguration( )
method from the last chapter, which uses JDOM:
/** * <p> * This will save the current state out to the specified * <code>OutputStream</code>. * </p> * * @throws <code>IOException</code> - when errors occur in saving. */ public synchronized void saveConfiguration(OutputStream out) throws IOException { // Get the JavaXML namespace Namespace ns = Namespace.getNamespace("JavaXML", "http://www.oreilly.com/catalog/javaxml/"); // Create the root element Element root = new Element("xmlrpc-config", ns); Document doc = new Document(root); doc.setDocType(new DocType("JavaXML:xmlrpc-config", "DTD/XmlRpc.dtd")); root.addChild(new Element("hostname", ns) .setContent(hostname)) .addChild(new Element("port", ns) .addAttribute("type", "unprotected") .setContent(portNumber + "")) .addChild(new Element("parserClass", ns) .setContent(driverClass)); Element handlersElement = new Element("handlers", ns); Enumeration e = handlers.keys( ); while (e.hasMoreElements( )) { String handlerID = (String)e.nextElement( ); String handlerClass = (String)handlers.get(handlerID); handlersElement.addChild(new Element("handler", ns) .addChild(new Element("identifier", ns) .setContent(handlerID)) .addChild(new Element("class", ns) .setContent(handlerClass)) ); } root.addChild(new Element("xmlrpc-server", ns) .addChild(handlersElement)); // Output the document, use standard formatter XMLOutputter fmt = new XMLOutputter( ); fmt.output(doc, out); }
Now we look at the SAX and DOM alternatives to this approach.
The discussion of SAX is short: simply put, you cannot modify XML with SAX. Because SAX is an event-based approach to XML data, it is only useful in parsing an XML document. The callbacks it defines are specifically designed for that purpose, and as SAX has no concept of an overall picture of an XML document, it also has no concept of changing that overall picture. It is this very fact that has caused much of the popularity of DOM; until recently, DOM has been the only means of creating XML from Java without having to write out XML directly using streams.
The Document
Object Model does provide a means of creating and modifying XML from
Java. However, it takes a more rigid view of the tree structure of an
XML document. First, it considers everything in the DOM tree a
Node
(remember our discussions in Chapter 7)? Because
these Node
s are all part of a tree, it is
necessary to create a Node
with an association to
a particular DOM tree, represented by a DOM
Document
object. To ensure this model is adhered
to, there is no facility for instantiating a DOM Node
directly;
instead, it is the Document
interface within DOM
that defines the createElement( )
,
createAttribute( )
, and other
Node
creation operations. Another point to
remember is that this rigid tree model considers everything in the
tree a Node
, including textual data. This means
that there is no concept of an element’s content in DOM;
instead, a DOM Element
has child
Node
s, some of which may be
Text
Node
s. This means that
setting the value of an Element
requires using the
createTextNode( )
method to create a Text
Node
, and then adding that new
Node
to the desired parent
Element
. Of course, the createTextNode( )
method itself is invoked on the overall DOM Document
object, to ensure the correct association with the DOM tree.
This may feel a bit confusing to you: the creation of an XML element with text involves a DOM Element
Node
, a DOM Text
Node
, and a DOM Document
object all working in concert. This has caused some newer XML developers to become frustrated, as they attempt to change an Element
’s textual content with setNodeValue( )
, and get very different results than expected. Be careful when using DOM to keep thinking in a very strict tree model, and you will be able to avoid these types of problems.
With this in mind, let’s look at building up an XML document
from the data supplied by a user in the
XmlRpcConfigurationServlet
application.
You might be expecting us to repeat the process of finding each XML
element with data and then changing the value of the textual nodes,
as in our first saveConfiguration( )
method for
JDOM. Although this is certainly possible (as DOM provides a
setValue( )
method for textual
Node
s), this is neither the easiest nor the
quickest way to handle this task. Instead, we use the various
createXXX( )
methods that are defined in the DOM
Document
interface. A method for each type of DOM
Node
is provided, such as createElement( )
, createTextNode( )
, and createAttribute( )
. As each of these is created, it is assigned
to the Document
object used to create it. This
maintains an ownership between each created Node
and the DOM tree they belong to.
In this way, all the needed elements and data can be created within
our Java code. However, this is only half of the task; each element
and its data then need to be inserted into the DOM tree and assembled
into the correct hierarchy. The simplest way to insert a
Node
into the tree is to use the
appendChild( )
method on the parent node.
Make sure you understand the difference between createXXX( )
and appendNode( )
. While createXXX( )
results in a Node
that is associated with a DOM Document
object, it does not insert that Node
into the Document
; the appendNode( )
method must still be invoked upon the desired parent of the new Node
. Failure to distinguish between these two methods can result in a huge number of Node
s associated with a DOM Document
object, but an empty DOM tree.
In this way, we can build up the complete document starting with the root element. Finally, we can replace the old root element with the new one, completing the update of our tree.
In addition to our not needing to perform complicated searching and retrieval of elements and data, building a tree this way is often much quicker. Searches through a DOM tree, particularly when the DOM tree becomes large, can take quite a lot of processing time; creating XML from the root element up is a much faster alternative to this extensive searching. The code to perform this task is included here:
/** * <p>* This will save the current state out to the specified
* <code>OutputStream</code>, using DOM
* </p> * * @throws <code>IOException</code> - when errors occur in saving. */ public synchronized void saveConfiguration(OutputStream out) throws IOException { String NAMESPACE_URI = "http://www.oreilly.com/catalog/javaxml/"; // We assume the DOM Document object was loaded in // parseConfiguation( ) and is saved in a member variable called // <code>doc</code>. Element oldRoot = doc.getDocumentElement( ); Element newRoot = doc.createElementNS(NAMESPACE_URI, "JavaXML:xmlrpc-config"); // Handle hostname Element hostnameNode = doc.createElementNS(NAMESPACE_URI, "JavaXML:hostname"); hostnameNode.appendChild( doc.createTextNode(hostname)); newRoot.appendChild(hostnameNode); // Handle port number Element portNumberNode = doc.createElementNS(NAMESPACE_URI, "JavaXML:port"); portNumberNode.appendChild( doc.createTextNode(portNumber + "")); portNumberNode.setValue("type", "unprotected"); newRoot.appendChild(portNumberNode); // Handle SAX Driver class Element saxDriverNode = doc.createElementNS(NAMESPACE_URI, "JavaXML:parserClass"); saxDriverNode.appendChild( doc.createTextNode(driverClass)); newRoot.appendChild(saxDriverNode); Element serverNode = doc.createElementNS(NAMESPACE_URI, "JavaXML:xmlrpc-server"); Element handlersNode = doc.createElementNS(NAMESPACE_URI, "JavaXML:handlers"); // Handle handlers Enumeration handlerIDs = handlers.keys( ); while (handlerIDs.hasMoreElements( )) { String handlerID = (String)handlerIDs.nextElement( ); String handlerClass = (String)handlers.get(handlerID); Element handlerIDNode = doc.createElementNS(NAMESPACE_URI, "JavaXML:identifier"); handlerIDNode.appendChild( doc.createTextNode(handlerID)); Element handlerClassNode = doc.createElementNS(NAMESPACE_URI, "JavaXML:class"); handlerClassNode.appendChild( doc.createTextNode(handlerClass)); Element handlerNode = doc.createElementNS(NAMESPACE_URI, "JavaXML:handler"); handlerNode.appendChild(handlerIDNode); handlerNode.appendChild(handlerClassNode); handlersNode.appendChild(handlerNode); } serverNode.appendChild(handlersNode); newRoot.appendChild(serverNode); doc.replaceChild(newRoot, oldRoot); // Serialize the DOM tree }
We use the DOM Level 2 methods to create our XML document with
namespace awareness. The createElementNS( )
method takes in the namespace URI and then
the full name of the new element. The full name
does include the namespace prefix; this allows the method to ensure
that a namespace prefix and namespace URI are either both included,
or both excluded, helping to ensure properly formed XML.
Serialization of a DOM
tree is another task, like obtaining the DOM
Document
object, that is not outlined in the DOM
specification. To achieve serialization, you need to see if your
vendor provides a helper class that will handle that functionality
for you. Once you have located your parser’s serializer, you
typically need only to import that class and pass it an
OutputStream
or PrintWriter
instance. Using Apache Xerces, the class needed is the
org.apache.xml.serialize.XMLSerializer
. We can use
that class to write to the OutputStream
that our
saveConfiguration( )
method has supplied to it:
/** * <p> * This will save the current state out to the specified * <code>OutputStream</code>, using DOM * </p> * * @throws <code>IOException</code> - when errors occur in saving. */ public synchronized void saveConfiguration(OutputStream out) throws IOException { // Modify Document object doc.replaceChild(newRoot, oldRoot);// Serialize the DOM tree
org.apache.xml.serialize.XMLSerializer out =
new org.apache.xml.serialize.XMLSerializer(out, null);
out.serialize(doc);
}
The Apache
XMLSerializer
class has several different constructors;
the one used here takes in an OutputStream
and the
format to use for output. We specify null
to allow
the default format to be used. The serialize( )
method takes as input the Document
,
Element
, or DocumentFragment
to
serialize. We pass in the modified DOM tree
Document
, and the serialization occurs. At this
point, we have emulated the functionality we created with JDOM to
modify and output an XML document with user-supplied input.
When using a
servlet or other web-based construct for providing a user interface,
several issues must be handled that are not problems with thick or
static clients. One of these is garbage collection and user lag.
User lag refers to a user loading a servlet or
piece of Java code on the Internet, and then taking a coffee break,
attending three meetings, and eating a candy bar. What can happen is
that object references in the servlet that was accessed may be
garbage collected between the time of the original request and the
user’s (much later) action. When one servlet submits to
another, this is not a problem; however, in our example, the
XmlRpcConfigurationServlet
submitted data to
itself. The possible bug is that the config
member
variable, an instance of
com.oreilly.xml.XmlRpcConfiguration
, is only
created in the doGet( )
method, but is then reused
in the doPost( )
method:
public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.setContentType("text/html"); PrintWriter out = res.getWriter( );// Load the configuration information with our utility class
config = new XmlRpcConfiguration(CONFIG_FILENAME);
// Rest of method } public void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { // Save the hostname String hostname = req.getParameterValues("hostname")[0]; if ((hostname != null) && (!hostname.equals(""))) {config.setHostname(hostname);
} // Rest of method }
If sufficient time has passed between the initial GET request and the
subsequent POST request, a NullPointerException
can result when config
is accessed in the
doPost( )
method. This can also occur if the
servlet code is changed and reloaded in the middle of a request,
something the Jakarta Tomcat and other popular servlet engines
support.
To avoid this problem, it makes sense to ensure that the
config
member variable is valid in both the
doGet( )
and doPost( )
methods; however, there is a lot of overhead with instantiating the
XmlRpcConfiguration
class, as it must parse the
supplied filename again. Instead of recreating the variable each time,
we can take advantage of knowing the variables that are
garbage-collected have their values set back to
null
.[17] Thus, we make a comparison to
null
, and only reinstantiate
config
when needed:
public void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {// Update the configuration information
if (config == null) {
config = new XmlRpcConfiguration(CONFIG_FILENAME);
}
// Save the hostname String hostname = req.getParameterValues("hostname")[0]; if ((hostname != null) && (!hostname.equals(""))) {config.setHostname(hostname);
} // Rest of method }
This will ensure that if user lag occurs, it does not affect your program’s operation.
[17] This is a bit of an overstatement. However, in the case of a non-local variable, it will hold true, particularly when dealing with servlets. For more information on garbage collection and values of non-initialized or garbage-collected variables, you should consult Java in a Nutshell, by David Flanagan (O’Reilly & Associates).
18.217.107.229