Chapter 23

Creating and Modifying XML Documents

WHAT YOU WILL LEARN IN THIS CHAPTER:

  • What the Document Object Model is
  • How you create a DOM parser
  • How you access the contents of a document using DOM
  • How you can create and update a new XML document
  • What the Extensible Stylesheet Language (XSL) is
  • How the Extensible Stylesheet Language Transformation (XSLT) language relates to XSL
  • How you can use a Transformer object to read and write XML files
  • How to modify Sketcher to store and retrieve sketches as XML documents

In this chapter you explore what you can do with the Document Object Model (DOM) application program interface (API). As I outlined in the previous chapter, DOM uses a mechanism that is completely different from Simple API for XML (SAX). As well as providing an alternative mechanism for parsing XML documents, DOM also adds the capability for you to modify them and create new ones. You also make a short excursion into XSLT and apply it with DOM in Sketcher. By the end of this chapter you have a version of Sketcher that can store and retrieve sketches as XML files.

THE DOCUMENT OBJECT MODEL

As you saw in the previous chapter, a DOM parser presents you with a Document object that encapsulates an entire XML structure. You can call methods for this object to navigate through the document tree and process the elements and attributes in whatever way you want. This is quite different from SAX, but there is still quite a close relationship between DOM and SAX.

The mechanism for getting access to a DOM parser is very similar to what you used to obtain a SAX parser. You start with a factory object that you obtain like this:

DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
 

The newInstance() method is a static method in the javax.xml.parsers.DocumentBuilderFactory class for creating factory objects. As with SAX, this approach of dynamically creating a factory object that you then use to create a parser allows you to change the parser you are using without modifying or recompiling your code. The factory object creates a javax.xml.parsers.DocumentBuilder object that encapsulates a DOM parser:

DocumentBuilder builder = null;
try {
  builder = builderFactory.newDocumentBuilder();
} catch(ParserConfigurationException e) {
  e.printStackTrace();
}
 

When a DOM parser reads an XML document, it makes it available in its entirety as an org.w3c.dom.Document object. The name of the class that encapsulates a DOM parser has obviously been chosen to indicate that it can also build new Document objects. A DOM parser can throw exceptions of type SAXException, and parsing errors in DOM are handled in essentially the same way as in SAX2. The DocumentBuilderFactory, DocumentBuilder, and ParserConfigurationException classes are all defined in the javax.xml.parsers package. Let’s jump straight in and try this out for real.

TRY IT OUT: Creating an XML Document Builder

Here’s the code to create a document builder object:

image
import javax.xml.parsers.*;
import javax.xml.parsers.ParserConfigurationException;
 
public class TryDOM {
  public static void main(String args[]) {
     DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
     DocumentBuilder builder = null;
     try {
       builder = builderFactory.newDocumentBuilder();
     }
     catch(ParserConfigurationException e) {
       e.printStackTrace();
       System.exit(1);
     }
     System.out.println(
              "Builder Factory = " + builderFactory + "
Builder = " + builder);
  }
}
 

Directory "TryDOM 1"

I got the following output:

Builder Factory = com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl@3f4ebd
Builder = com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderImpl@4a5c78
 

How It Works

The static newInstance() method in the DocumentBuilderFactory class returns a reference to a factory object. You call the newDocumentBuilder() method for the factory object to obtain a reference to a DocumentBuilder object that encapsulates a DOM parser. This is the default parser. If you want the parser to validate the XML or provide other capabilities, you can set the parser features before you create the DocumentBuilder object by calling methods for the DocumentBuilderFactory object.

You can see that you get a version of the Xerces parser as a DOM parser. Many DOM parsers are built on top of SAX parsers, and this is the case with the Xerces parser.

SETTING DOM PARSER FEATURES

The idea of a feature for a DOM parser is the same as with SAX — a parser option that can be either on or off. The following methods are provided by the DocumentBuilderFactory object for setting DOM parser features:

  • void setNamespaceAware(boolean aware): Calling this method with a true argument sets the parser to be namespace-aware. The default setting is false.
  • void setValidating(boolean validating): Calling this method with a true argument sets the parser to validate the XML in a document as it is parsed. The default setting is false.
  • void setIgnoringElementContentWhitespace(boolean ignore): Calling this method with a true argument sets the parser to remove ignorable whitespace so the Document object produced by the parser does not contain ignorable whitespace. The default setting is false.
  • void setIgnoringComments(boolean ignore): Calling this method with a true argument sets the parser to remove comments as the document is parsed. The default setting is false.
  • void setExpandEntityReferences(boolean expand): Calling this method with a true argument sets the parser to expand entity references into the referenced text. The default setting is true.
  • void setCoalescing(boolean coalesce): Calling this method with a true argument sets the parser to convert CDATA sections to text and append it to any adjacent text. The default setting is false.

By default the parser that is produced is neither namespace-aware nor validating. You should at least set these two features before creating the parser. This is quite simple:

DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
builderFactory.setNamespaceAware(true);
builderFactory.setValidating(true);
 

If you add the bolded statements to the example, the newDocumentBuilder() method for the factory object should now return a validating and namespace-aware parser. With a validating parser, you should define an ErrorHandler object that deals with parsing errors. You identify the ErrorHandler object to the parser by calling the setErrorHandler() method for the DocumentBuilder object:

builder.setErrorHandler(handler);
 

Here handler refers to an object that implements the three methods declared in the org.xml.sax.ErrorHandler interface. I discussed these in the previous chapter in the context of SAX parser error handling, and the same applies here. If you do create a validating parser, you should always implement and register an ErrorHandler object. Otherwise, the parser may not work properly.

The factory object has methods corresponding to each of the getXXX() methods in the preceding table to check the status of parser features. The checking methods all have corresponding names of the form isXXX(), so to check whether a parser is namespace-aware, you call the isNamespaceAware() method. Each method returns true if the parser to be created has the feature set, and false otherwise.

You can identify a schema to be used by a DOM parser when validating documents. You pass a reference to a Schema object to the setSchema() method for the DocumentBuilderFactory object. The parser that you create then uses the specified schema when validating a document.

PARSING A DOCUMENT

After you have created a DocumentBuilder object, you just call its parse() method with a document source as an argument to parse a document. The parse() method returns a reference of type Document to an object that encapsulates the entire XML document. The Document interface is defined in the org.w3c.dom package.

There are five overloaded versions of the parse() method that provide various options for you to identify the source of the XML document to be parsed. They all return a reference to a Document object encapsulating the XML document:

  • parse(File file): Parses the document in the file identified by file.
  • parse(String uri): Parses the document at the URI uri.
  • parse(InputSource srce): Parses the document read from srce.
  • parse(InputStream in): Parses the document read from the stream in.
  • parse(InputStream in, String systemID): Parses the document read from the stream in. The systemID argument is used as the base to resolve relative URIs in the document.

All five versions of the parse() method can throw three types of exception. An IllegalArgumentException is thrown if you pass null to the method for the parameter that identifies the document source. The method throws an IOException if any I/O error occurs and a SAXException in the event of a parsing error. The last two exceptions must be caught. Note that it is a SAXException that can be thrown here. Exceptions of type DOMException arise only when you are navigating the element tree for a Document object.

The org.xml.sax.InputSource class defines objects that encapsulate a source of an XML document. The InputSource class defines constructors that enable you to create an object from a java.io.InputStream object, a java.io.Reader object, or a String object specifying a URI for the document source. If the URI is a URL, it must not be a relative URL.

You could parse() a document that is stored in a file using the DocumentBuilder object builder like this:

Path xmlFile = Paths.get(System.getProperty("user.home")).
                                  resolve("Beginning Java Stuff").resolve("circlewithDTD.bin");
Document xmlDoc = null;
try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(xmlFile))){
  xmlDoc = builder.parse(in);
} catch(SAXException | IOException e) {
  e.printStackTrace();
  System.exit(1);
}
 

This creates a Path object for the file and creates an input stream for the file in the try block. Calling parse() for the builder object with the input stream as the argument parses the XML file and returns it as a Document object. Note that the entire XML file contents are encapsulated by the Document object, so in practice this can require a lot of memory.

To compile this code you need import statements for the BufferedInputStream and IOException names in the java.io package, and Paths, Path, and Files names in the java.nio.file package, as well as the org.w3c.dom.Document class name. After this code executes, you can call methods for the xmlDoc object to navigate through the elements in the document tree structure. Let’s look at what the possibilities are.

NAVIGATING A DOCUMENT OBJECT TREE

The org.w3c.dom.Node interface is fundamental to all objects that encapsulate components of an XML document, and this includes the Document object itself. It represents a type that encapsulates a node in the document tree. Node is also a super-interface of a number of other interfaces that declare methods for accessing document components. The subinterfaces of Node that identify components of a document are the following:

  • Element: Represents an XML element.
  • Text: Represents text that is part of element content. This is a subinterface of CharacterData, which is a subinterface of Node. Text references, therefore, have methods from all three interfaces.
  • CDATASection : Represents a CDATA section — unparsed character data. This extends Text.
  • Comment: Represents a document comment. This interface extends the CharacterData interface.
  • DocumentType: Represents the type of a document.
  • Document: Represents the entire XML document.
  • DocumentFragment: Represents a lightweight document object that encapsulates a subtree of a document.
  • Entity : Represents an entity that may be parsed or unparsed.
  • EntityReference : Represents a reference to an entity.
  • Notation: Represents a notation declared in the DTD for a document. A notation is a definition of an unparsed entity type.
  • ProcessingInstruction: Represents a processing instruction for an application.

Each of these interfaces declares its own set of methods and inherits the fields and methods declared in the Node interface. Every XML document is modeled as a hierarchy of nodes that are accessible as one or another of the interface types in the list. At the top of the node hierarchy for a document is the Document node that is returned by the parse() method. Each type of node may or may not have child nodes in the hierarchy, and those that do can have only certain types of child nodes. The types of nodes in a document that can have children are shown in Table 23-1:

TABLE 23-1: Nodes that Can Have Children

NODE TYPE POSSIBLE CHILDREN
Document Element (only 1), DocumentType (only 1), Comment, ProcessingInstruction
Element Element, Text, Comment, CDATASection, EntityReference, ProcessingInstruction
Attr Text, EntityReference
Entity Element, Text, Comment, CDATASection, EntityReference, ProcessingInstruction
EntityReference Element, Text, Comment, CDATASection, EntityReference, ProcessingInstruction

Of course, what each node may have as children follows from the XML specification, not just the DOM specification. There is one other type of node that extends the Node interface — DocumentFragment. This is not formally part of a document in the sense that a node of this type is a programming convenience. It is used to house a fragment of a document — a subtree of elements — for use when moving fragments of a document around, for example, so it provides a similar function to a Document node but with less overhead. A DocumentFragment node can have the same range of child nodes as an Element node.

The starting point for exploring the entire document tree is the root element for the document. You can obtain a reference to an object that encapsulates the root element by calling the getDocumentElement() method for the Document object:

Element root = xmlDoc.getDocumentElement();
 

This method returns the root element for the document as type Element. You can also get the node corresponding to the DOCTYPE declaration as type DocumentType like this:

DocumentType doctype = xmlDoc.getDoctype();
 

If there is no DOCTYPE declaration, or the parser cannot find the DTD for the document, the getDocType() method returns null. If the value returned is not null, you can obtain the contents of the DTD as a string by calling the getInternalSubset() method for the DocumentType object:

System.out.println("Document type:
" + doctype.getInternalSubset());
 

This statement outputs the contents of the DTD for the document.

After you have an object encapsulating the root element for a document, the next step is to obtain its child nodes. You can use the getChildNodes() method that is defined in the Node interface for this. This method returns a org.w3c.dom.NodeList reference that encapsulates all the child elements for that element. You can call this method for any node that has children, including the Document node, if you wish. You can therefore obtain the child elements for the root element with the following statement:

NodeList children = root.getChildNodes();
 

A NodeList reference encapsulates an ordered collection of Node references, each of which will be one of the possible node types for the current node. So with an Element node, any of the Node references in the list that is returned can be of type Element, Text, Comment, CDATASection, EntityReference, or ProcessingInstruction. Note that if there are no child nodes, the getChildNodes() method returns a NodeList reference that is empty, not null. You call the getChildNodes() method to obtain a list of child nodes for any node type that can have them.

The NodeList interface declares just two methods: The getLength() method returns the number of nodes in the list as type int, and the item()method returns a Node reference to the object at index position in the list specified by the argument of type int.

You can use these methods to iterate through the child elements of the root element, perhaps like this:

Node[] nodes = new Node[children.getLength()];
for(int i = 0 ; i < nodes.getLength() ; ++i) {
  nodes[i] = children.item(i);
}
 

You allocate sufficient elements in the nodes array to accommodate the number of child nodes and then populate the array in the for loop.

Node Types

Of course, you will normally be interested in the specific types of nodes that are returned, so you will want to extract them as specific types, or at least determine what they are before processing them. This is not difficult. One possibility is to test the type of any node using the instanceof operator. Here’s one way you could extract just the child nodes that are of type Element and store them in a vector container:

Vector<Element> elements = new Vector<>();
Node node = null;
for(int i = 0 ; i < nodes.getLength() ; ++i) {
  node = children.item(i);
  if(node instanceof Element) {
    elements.add(node);
  }
}
 

Another possibility is provided by the getNodeType() method that is declared in the Node interface. This method returns a value of type short that is one of the following constants that are defined in the Node interface:

  • DOCUMENT_NODE
  • DOCUMENT_TYPE_NODE
  • DOCUMENT_FRAGMENT_NODE
  • DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC
  • DOCUMENT_POSITION_PRECEDING
  • DOCUMENT_POSITION_FOLLOWING
  • DOCUMENT_POSITION_CONTAINED_BY
  • DOCUMENT_POSITION_CONTAINS
  • DOCUMENT_POSITION_DISCONNECTED
  • CDATA_SECTION_NODE
  • ENTITY_REFERENCE_NODE
  • ENTITY_NODE
  • TEXT_NODE
  • COMMENT_NODE
  • NOTATION_NODE
  • ELEMENT_NODE
  • ATTRIBUTE_NODE
  • PROCESSING_INSTRUCTION_NODE

The advantage of using the getNodeType() method is that you can test for the node type using a switch statement with the constants as case values. This makes it easy to farm out processing for nodes of various types to separate methods.

A simple loop like the one in the previous code fragment is not a very practical approach to navigating a document. In general, you have no idea of the level to which elements are nested in the document, and this loop examines only one level. You need an approach that allows any level of nesting. This is a job for recursion. Let’s put together a working example to illustrate how you can do this.

TRY IT OUT: Listing a Document

You can extend the previous example to list the nodes in a document. You add a static method to the TryDOM class to list child elements recursively. You also add a helper method that identifies what each node is. The program outputs details of each node followed by its children. Here’s the code:

image
import javax.xml.parsers.*;
import org.xml.sax.*;
import org.w3c.dom.*;
import java.io.*;
import java.nio.file.*;
import static org.w3c.dom.Node.*;                     // For node type constants
 
public class TryDOM implements ErrorHandler {
  public static void main(String args[]) {
    if(args.length == 0) {
      System.out.println("No file to process." + "Usage is:
java TryDOM "filename"");
                           
      System.exit(1);
    }
    DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
    builderFactory.setNamespaceAware(true);           // Set namespace aware
    builderFactory.setValidating(true);               // & validating parser
 
    DocumentBuilder builder = null;
    try {
      builder = builderFactory.newDocumentBuilder();  // Create the parser
      builder.setErrorHandler(new TryDOM());          // Error handler is TryDOM instance 
    } catch(ParserConfigurationException e) {
      e.printStackTrace();
      System.exit(1);
    }
 
    Path xmlFile = Paths.get(args[0]);
    Document xmlDoc = null;
    try (BufferedInputStream in = new BufferedInputStream(
                                              Files.newInputStream(xmlFile))){
      xmlDoc = builder.parse(in);
    } catch(SAXException | IOException e) {
      e.printStackTrace();
      System.exit(1);
    }
    DocumentType doctype = xmlDoc.getDoctype();      // Get the DOCTYPE node
    if(doctype == null) {                            // If it's not null...
      System.out.println("DOCTYPE is null");
    } else {                                         // ...output it
      System.out.println("DOCTYPE node:
" + doctype.getInternalSubset());  
    }
 
    System.out.println("
Document body contents are:");
    listNodes(xmlDoc.getDocumentElement(), " ");     // Root element & children
  }
  
  // output a node and all its child nodes
  static void listNodes(Node node, String indent) {
    // List the current node
    String nodeName = node.getNodeName();
    System.out.println(indent + " Node: " + nodeName);
    System.out.println(indent + " Node Type: " + nodeType(node.getNodeType()));
 
    NodeList list = node.getChildNodes();            // Get the list of child nodes
    if(list.getLength() > 0) {                       // If there are some...
      //...list them & their children...
      // ...by calling listNodes() for each
      System.out.println(indent+" Child Nodes of " + nodeName + " are:");
      for(int i = 0 ; i < list.getLength() ; ++i) {  
        listNodes(list.item(i),indent + "  ");    
      }
    }
  }
 
  // Method to identify the node type
  static String nodeType(short type) {
    switch(type) {
      case ELEMENT_NODE:                return "Element";
      case DOCUMENT_TYPE_NODE:          return "Document type";
      case ENTITY_NODE:                 return "Entity";
      case ENTITY_REFERENCE_NODE:       return "Entity reference";
      case NOTATION_NODE:               return "Notation";
      case TEXT_NODE:                   return "Text";
      case COMMENT_NODE:                return "Comment";
      case CDATA_SECTION_NODE:          return "CDATA Section";
      case ATTRIBUTE_NODE:              return "Attribute";
      case PROCESSING_INSTRUCTION_NODE: return "Attribute";
    }
    return "Unidentified";
  }
 
  public void fatalError(SAXParseException spe) throws SAXException {
    System.out.println("Fatal error at line " + spe.getLineNumber());
    System.out.println(spe.getMessage());
    throw spe;
  }
 
  public void warning(SAXParseException spe) {
    System.out.println("Warning at line " + spe.getLineNumber());
    System.out.println(spe.getMessage());
  }
 
  public void error(SAXParseException spe) {
    System.out.println("Error at line " + spe.getLineNumber());
    System.out.println(spe.getMessage());
  }
}
 

Directory "TryDOM 2 with node details output"

I have removed the statement that outputs details of the parser from the previous version of the TryDOM class to reduce the output a little. Run this with a document file AddressWithDTD.xml that contains the following:

image
<?xml version="1.0"?>
<!DOCTYPE address 
[
   <!ELEMENT address (buildingnumber, street, city, state, zip)>
   <!ATTLIST address xmlns CDATA #IMPLIED>
   <!ELEMENT buildingnumber (#PCDATA)>
   <!ELEMENT street (#PCDATA)>
   <!ELEMENT city (#PCDATA)>
   <!ELEMENT state (#PCDATA)>
   <!ELEMENT zip (#PCDATA)>
]>
 
<address>
  <buildingnumber> 29 </buildingnumber>
  <street> South Lasalle Street</street>
  <city>Chicago</city>
  <state>Illinois</state>
  <zip>60603</zip>
</address>
 

Directory "TryDOM 2 with node details output"

This is the Address.xml document from the previous chapter with the DTD included in the document. If you store this file in the same directory as the source file, you can just put the file name as the command-line argument, like this:

java TryDOM AddressWithDTD.xml
 

The program produces quite a lot of output starting with:

DOCTYPE node:
<!ELEMENT address (buildingnumber,street,city,state,zip)>
<!ATTLIST address xmlns CDATA #IMPLIED>
<!ELEMENT buildingnumber (#PCDATA)>
<!ELEMENT street (#PCDATA)>
<!ELEMENT city (#PCDATA)>
<!ELEMENT state (#PCDATA)>
<!ELEMENT zip (#PCDATA)>
 
Document body contents are:
Node: address
 Node Type: Element
 Child Nodes of address are:
   Node: #text
   Node Type: Text
   Node: buildingnumber
   Node Type: Element
   Child Nodes of buildingnumber are:
     Node: #text
     Node Type: Text
 

and so on down to the last few lines:

   Node: zip
   Node Type: Element
   Child Nodes of zip are:
     Node: #text
     Node Type: Text
   Node: #text
   Node Type: Text
 

How It Works

Because you have set the parser configuration in the factory object to include validating the XML, you have to provide an org.xml.sax.ErrorHandler object for the parser. The TryDOM class implements the warning(), error(), and fatalError() methods declared by the ErrorHandler interface, so an instance of this class takes care of it.

You call the getDoctype() method for the Document object to obtain the node corresponding to the DOCTYPE declaration:

    DocumentType doctype = xmlDoc.getDoctype();       // Get the DOCTYPE node
    if(doctype == null) {                             // If it's not null...
      System.out.println("DOCTYPE is null");
    } else {                                          // ...output it
      System.out.println("DOCTYPE node:
" + doctype.getInternalSubset());
    }
 

You can see from the output that you get the complete text of the DTD from the document.

After outputting a header line showing where the document body starts, you output the contents, starting with the root element. The listNodes() method does all the work. You pass a reference to the root element that you obtain from the Document object with the following statement:

    listNodes(xmlDoc.getDocumentElement(), " ");      // Root element & children
 

The first argument to listNodes() is the node to be listed, and the second argument is the current indent for output. On each recursive call of the method, you append a couple of spaces. This results in each nested level of nodes being indented in the output by two spaces relative to the parent node output.

The first step in the listNodes() method is to get the name of the current node by calling its getNodeName() method:

String nodeName = node.getNodeName();                 // Get name of this node
 

The next statement outputs the node itself:

System.out.println(indent + " " + nodeName);
 

You then output the type of the current node with the following statement:

    System.out.println(indent + " Node Type: " + nodeType(node.getNodeType()));
 

The indent parameter defines the indentation for the current node. Calling getNodeType() for the node object returns a value of type short that identifies the node type. You then pass this value to the nodeType() helper method that you’ve added to the TryDOM class. The code for the helper method is just a switch statement with the constants from the Node interface that identify the types of nodes as case values. I just included a representative set in the code, but you can add case labels for all 18 constants if you want.

The remainder of the listNodes() code iterates through the child nodes of the current node if it has any:

NodeList list = node.getChildNodes();                 // Get the list of child nodes
if(list.getLength() > 0) {                            // As long as there are some...
  System.out.println(indent+"Child Nodes of " + nodeName + " are:");
     //...list them & their children...
     // ...by calling listNodes() for each
    for(int i = 0 ; i < list.getLength() ; ++i) {
      listNodes(list.item(i),indent + "  ");
    }
 

The for loop simply iterates through the list of child nodes obtained by calling the getChildNodes() method. Each child is passed as an argument to the listNodes() method, which lists the node and iterates through its children. In this way the method works through all the nodes in the document. You can see that you append an extra couple of spaces to indent in the second argument to the listNodes() call for a child node. The indent parameter in the next level down references a string that is two spaces longer. This ensures that the output for the next level of nodes is indented relative to the current node.

Ignorable Whitespace and Element Content

Some of the elements have multiple #text elements recorded in the output. The #text elements arise from two things: text that represents element content and ignorable whitespace that is there to present the markup in a readable fashion. If you don’t want to see the ignorable whitespace, you can get rid of it quite easily. You just need to set another parser feature in the factory object:

builderFactory.setNamespaceAware(true);        // Set namespace aware
builderFactory.setValidating(true);            // and validating parser
builderFactory.setIgnoringElementContentWhitespace(true); 
 

Calling this method results in a parser that does not report ignorable whitespace as a node, so you don’t see it in the Document object. If you run the example again with this change, the #text nodes arising from ignorable whitespace are no longer there.

That still leaves some other #text elements that represent element content, and you really do want to access that and display it. In this case you can use the getWholeText() method for a node of type Text to obtain all of the content as a single string. You could modify the code in the listNodes() method in the example to do this:

image
static void listNodes(Node node, String indent) {
    // List the current node
    String nodeName = node.getNodeName();
    System.out.println(indent + " Node: " + nodeName);
    short type =node.getNodeType();
    System.out.println(indent+" Node Type: " + nodeType(type));
    if(type == TEXT_NODE){
      System.out.println(indent + " Content is: " + ((Text)node).getWholeText());
    }
    
    // Now list the child nodes
    NodeList list = node.getChildNodes();       // Get the list of child nodes
    if(list.getLength() > 0) {                  // As long as there are some...
      //...list them & their children...
      // ...by calling listNodes() for each
     System.out.println(indent + " Child Nodes of " + nodeName + " are:");
      for(int i = 0 ; i < list.getLength() ; ++i) {  
        listNodes(list.item(i),indent + "  "); 
      }
    }         
  }
 

Directory "TryDOM 3 with node content"

Here you store the integer that identifies the node type in a variable, type, that you test to see if it is a text node. If it is, you get the contents by calling the getWholeText() method for the node. You have to cast the node reference to type Text; otherwise, you would not be able to call the getWholeText() method because it is declared in the Text interface, which is a subinterface of Node. If you run the example again with this further addition, you get the contents of the nodes displayed, too.

Even though you have set the parser feature to ignore ignorable whitespace, you could still get #text elements that contained just whitespace. The Text interface declares the isElementContentWhitespace() method that you can use to check for this — when you don’t want to display an empty line, for example.

Accessing Attributes

You usually want to access the attributes for an element, but only if it has some. You can test whether an element has attributes by calling its hasAttributes() method. This returns true if the element has attributes and false otherwise, so you might use it like this:

short type = node.getNodeType();
if(type == ELEMENT_NODE && node.hasAttributes()) {
    // Process the element with its attributes
 
} else {
    // Process the element without attributes
}
 

The getAttributes() method returns a NamedNodeMap reference that contains the attributes, the NamedNodeMap interface being defined in the org.w3c.dom package. In general, a NamedNodeMap object is a collection of Node references that can be accessed by name, or serially by iterating through the collection. Because the nodes are attributes in this instance, the nodes are actually of type Attr. In fact, you can call the getAttributes() method for any node type, and it returns null if an element has no attributes. Thus, you could omit the test for the element type in the if condition, and the code works just as well.

The NamedNodeMap interface declares the following methods for retrieving nodes from the collection:

  • Node item(int index): Returns the node at position index.
  • int getLength(): Returns the number of Node references in the collection.
  • Node getNamedItem(String name): Returns the node with the node name name.
  • Node getNamedItemNS(String uri,String localName): Returns the node with the name localName in the namespace at uri.

Obviously the last two methods apply when you know what attributes to expect. You can apply the first two methods to iterate through the collection of attributes in a NamedNodeMap:

if(node.hasAttributes()) {
  NamedNodeMap attrs = node.getAttributes();
  for(int i = 0 ; i < attrs.getLength() ; ++i) {
    Attr attribute = (Attr)attrs.item(i);
    // Process the attribute...
  }
}
 

You now are in a position to obtain each of the attributes for an element as a reference of type Attr. To get at the attribute name and value you call the getName() and getValue() methods declared in the Attr interface, respectively, both of which return a value of type String. You can put that into practice in another example.

TRY IT OUT: Listing Elements with Attributes

You can modify the listNodes() method in the previous example to include attributes with the elements. Here’s the revised version:

image
static void listNodes(Node node) {
    System.out.println(indent + " Node: " + nodeName);
    short type =node.getNodeType();
    System.out.println(indent + " Node Type: " + nodeType(type));
    if(type == TEXT_NODE){
      System.out.println(indent + " Content is: " + ((Text)node).getWholeText());
    } else if(node.hasAttributes()) {
      System.out.println(indent+" Element Attributes are:");
      NamedNodeMap attrs = node.getAttributes();      //...get the attributes
      for(int i = 0 ; i < attrs.getLength() ; ++i) {
        Attr attribute = (Attr)attrs.item(i);         // Get an attribute
        System.out.println(indent + " " + attribute.getName() +
                                       " = " + attribute.getValue());
      }
    }
  
    NodeList list = node.getChildNodes();        // Get the list of child nodes    
    if(list.getLength() > 0) {                   // If there are some...
      //...list them & their children...
      // ...by calling listNodes() for each 
      System.out.println(indent + "Child Nodes of " + nodeName + " are:");
      for(int i = 0 ; i < list.getLength() ; ++i){  
        listNodes(list.item(i), indent + "  "); 
    }
}
 

Directory "TryDOM 4 listing elements with attributes"

You can recompile the code with these changes and run the example with the circle with DTD.xml file that you created when I was discussing DTDs. The content of this file is the following:

image
<?xml version="1.0"?>
<!DOCTYPE circle 
[
   <!ELEMENT circle (position)>
   <!ATTLIST circle 
             diameter CDATA #REQUIRED
   >
 
   <!ELEMENT position EMPTY>
   <!ATTLIST position 
             x CDATA #REQUIRED
             y CDATA #REQUIRED
   >
]>
 
<circle diameter="30">
  <position x="30" y="50"/>
</circle>
 

Directory "TryDOM 4 listing elements with attributes"

The output from the example processing this file should be

DOCTYPE node:
<!ELEMENT circle (position)>
<!ATTLIST circle diameter CDATA #REQUIRED>
<!ELEMENT position EMPTY>
<!ATTLIST position x CDATA #REQUIRED>
<!ATTLIST position y CDATA #REQUIRED>
 
Document body contents are:
 Node: circle
 Node Type: Element
 Element Attributes are:
 diameter = 30
 Child Nodes of circle are:
   Node: position
   Node Type: Element
   Element Attributes are:
   x = 30
   y = 50

How It Works

All the new code to handle attributes is in the listNodes() method. After verifying that the current node does have attributes, you get the collection of attributes as a NamedNodeMap object. You then iterate through the collection extracting each node in turn. Nodes are indexed from zero, and you obtain the number of nodes in the collection by calling its getLength() method. Because an attribute node is returned by the item() method as type Node, you have to cast the return value to type Attr to call the methods in this interface. You output the attribute and its value, making use of the getName() and getValue() methods for the Attr object in the process of assembling the output string.

It isn’t used in the example, but the Attr interface also declares a getSpecified() method that returns true if the attribute value was explicitly set in the document rather than being a default value from the DTD. The Attr interface also declares a getOwnerElement() method that returns an Element reference to the element to which this attribute applies.

TRANSFORMING XML

The Extensible Stylesheet Language (XSL) is a standard language for describing how an XML document should be transformed and/or displayed. XSL has three parts to it, referred to as the XSL family:

  • XSL-FO is a standard language for formatting XML documents.
  • XSLT is a standard language for transforming an XML document into another XML document, so you could transform an XML document into HTML or XHTML, for example.
  • XPath is a language for describing how you navigate through an XML document.

XSL is a huge topic that is generally far beyond the scope of this book. Indeed, you can find whole books dedicated to the topic, so it is impossible for me to discuss it at length here. Nonetheless, the JAXP XSLT capability provided by javax.xml.transform.Transformer objects can be very helpful when you want to transfer an XML document to or from an external file, so I’m just explaining how you can use the Java support for XSLT to do that.

Transformer Objects

The Transformer class is the basis for applying Extensible Style Sheet Language Transformations (XSLT) in Java. A Transformer object transforms an XML document, the source tree, into another XML document, the result tree. What happens to the XML during the transformation depends on the XSL style sheet associated with the source tree and how you set the parameters and properties for the Transformer object. I am going to sidestep most of the details of that and just use a transformer that does nothing. Surprisingly, a transformer that does nothing can do quite a lot. First I’ll explain how you create a Transformer object and then I’ll describe how you might use it.

Creating Transformer Objects

There are no public constructors defined in the Transformer class, so you must call a method for a TransformerFactory object to create one; this type is defined in the javax.xml.transform package. The TransformerFactory class does not have any public constructors either, so you must call the static newInstance() method to get a factory object for creating transformers. After you have a factory object, you can call its newTransformer() method with no parameters to create a Transformer object that is an identity transform; in other words, it does nothing in transferring the source XML to the result XML. This is the method I use in examples.

Here’s how you create a Transformer object that does nothing:

        TransformerFactory factory = TransformerFactory.newInstance();
        Transformer transformer = factory.newTransformer();

Both methods can throw exceptions, so you should call them from within a try block. The newInstance() method can throw TransformerFactoryConfigurationError if the factory object cannot be created. The newTransformer() method throws TransformerConfigurationException if the Transformer object cannot be created.

There is a newTranformer() method overload that requires an argument of type Source that encapsulates an XSLT document that defines the transformation to be applied by the Transformer object .

Using Transformer Objects

You apply a transformation to a source document by calling the transform() method for a Transformer object with the XML source and destination objects as arguments, in that order. You specify the source as a reference of type javax.xml.transform.Source and the destination as a reference of type javax.xml.transform.Result. Both Source and Result are interface types, so what you can use as a source or destination for a transformation is determined by the classes that implement these interfaces.

There are five classes that implement the Source interface, but I use only the DOMSource class that is defined in the javax.xml.transform.dom package and the StreamSource class that is defined in the javax.xml.transform.stream package. The DOMSource object encapsulates a Document object containing an XML document, so a Document object can be a source for a transformation. You could create a DOMSource object from a Document object, xmlDoc, like this:

        DOMSource source = new DOMSource(xmlDoc.getDocumentNode());
 

A DOMSource object accesses the contents of a Document object through its XML root node. This constructor creates a DOMSource object from the document node for the Document object. Another constructor accepts a String reference as a second argument that specifies the base URI associated with the root node. You can construct a DOMSource object using the no-arg constructor and then call setNode() for the object to identify the document’s root node. If no root node is set, then a transform operation creates an empty Document object for use as the source.

A StreamSource object encapsulates an input stream that is a source of XML markup. This implies that a file containing XML can be a source for a transformation. There are StreamSource constructors that create objects from a File object, an InputStream object, a Reader object, or a String specifying a URL that identifies the input stream. Here’s how you could create a StreamSource object from an InputStream object:

    Path file = Paths.get(System.getProperty("user.home")).
                                        resolve("Beginning Java Stuff").resolve("Address.xml");
try(BufferedInputStream xmlIn = new BufferedInputStream(Files.newInputStream(file))) {
  StreamSource source = new StreamSource(xmlIn);
  // Code to use the source...
 
} catch (IOException e) {
  e.printStackTrace();
}
 

You just pass the xmlIn stream object to the StreamSource constructor to make the file the source for a transform operation. You create xmlIn from the Path object, file, in the way you have seen several times before. The try block is necessary for the stream operations, not for the StreamSource constructor.

The Result interface is implemented by six classes, but I’m only introducing javax.xml.transform.dom.DOMResult and StreamResult from the javax.xml.transform.stream package. A DOMResult object encapsulates a Document object that results from a transformation of a Source object. A StreamResult object encapsulates an output stream, which could be a file or just the command line, and the markup that results from a transformation is written to the stream. Creating DOMResult and StreamResult objects is similar to creating source objects.

Both the source and destination for a transform() operation can be either a Document object or a stream, so you can use a transform to transfer the XML contained in a Document object to a file or to create a Document object from a file that contains XML markup. Suppose that you have a Document object, xmlDoc, that you want to write to a file. Here’s how you could use a Transformer object to do it:

   Path file = Paths.get(System.getProperty("user.home")).
                    resolve("Beginning Java Stuff").resolve("MyXMLDoc.xml");
   try(BufferedOutputStream xmlOut = new BufferedOutputStream(Files.newOutputStream(file))) {
     // Create a factory object for XML transformers
     TransformerFactory factory = TransformerFactory.newInstance();
     Transformer transformer = factory.newTransformer();
 
     // Make the transformer indent the output
     transformer.setOutputProperty(OutputKeys.INDENT, "yes");
 
     // Create the source and result objects for the transform
     DOMSource source = new DOMSource(xmlDoc.getDocumentNode());            
     StreamResult xmlFile = new StreamResult(xmlOut);
 
     transformer.transform(source, xmlFile);    // Execute transform
  } catch (TransformerConfigurationException tce) {
      System.err.println("Transformer Factory error: " + tce.getMessage());
  } catch (TransformerException te) {
      System.out.println("Transformation error: " + te.getMessage());
  } catch (IOException e) {
     e.printStackTrace();
  }
 

The process is very simple. You use the TransformerFactory object you have created to create the Transformer object. You set the property value for the transformer that corresponds to the INDENT key constant in the javax.xml.transform.OutputKeys class. This class defines constants that identify property keys for Transformer objects. Setting the value for INDENT to “yes" makes the transformer insert additional whitespace in the output so that elements appear on separate lines. With the default “no" value for the property, the output would not contain any newline characters between elements and so would be difficult to read. Instead of using the OutputKeys constant, you could use the String that is the key, “indent," but using the constant is the preferred approach. With the Transformer object set up, you create the Source object from the root node in the xmlDoc object. You create the Result object from the output stream, xmlOut, that encapsulates the file.

Transformer Properties

The OutputKeys class defines the following static fields that identify property keys for a Transformer object (see Table 23-2):

TABLE 23-2: OutputKeys Constants that Identify Transformer Properties

FIELD NAME VALUE FOR KEY
INDENT "yes" causes the processor to insert whitespace in the output. Default is “no."
DOCTYPE_SYSTEM The system identifier to be used in the DOCTYPE declaration as a string.
DOCTYPE_PUBLIC The public identifier to be used in the DOCTYPE declaration as a string.
ENCODING The encoding to be used for the output.
MEDIA_TYPE The MIME content type for the result tree.
CDATA_SECTION_ELEMENTS A list of element names whose text child nodes should be output as CDATA.
OMIT_XML_DECLARATION "yes" causes the processor to omit the XML declaration in the output. The default is “no."
STANDALONE "yes" causes the processor to output a standalone document declaration.
METHOD Specifies the method to be used for the result tree, for example, “xml," “html," or “text."
VERSION The version of the output method.

I’m only using the first two OutputKeys fields from the table. The DOCTYPE_SYSTEM property value is required for a DOCTYPE declaration to be included in the output, and you use this in Sketcher.

Dealing with Transformer Errors

A Transformer object can report errors by calling methods declared by the javax.xml.transform.ErrorListener interface. You can specify an object that is an error listener for a Transformer object like this:

transformer.setErrorListener(errorListener);
 

The errorListener object that is the argument must be of a class type that implements the ErrorListener interface.

ErrorListener declares three methods, all with a void return type, and all can throw a TransformerException:

  • error(TransformerException te) is called when a recoverable error occurs. The transformer continues to process the document after this error. You can implement the method to throw a TransformerException if you want to terminate processing of the document.
  • fatalError(TransformerException te) is called when a non-recoverable error occurs. Processing the document may continue after this error but usually it won’t. Your implementation of this method should handle the error or throw a TransformerException if that is not possible, or if you want to terminate document processing.
  • void warning(TransformerException te) is called to report conditions that are not errors or fatal errors. After this method returns, processing always continues. You can terminate processing of the document by throwing a TransformerException from this method.

You are not obliged to implement an ErrorListener for a transformer to receive notification of errors. If you don’t, the default behavior is to report all errors to System.err and not to throw any exceptions. In general you should implement an ErrorListener for a transformer to deal with errors appropriately. Throwing a TransformerException from within the error handler is optional, but if the method does throw an exception, the implementation must declare that it does.

CREATING DOCUMENT OBJECTS

The simplest way to create a Document object programmatically is to call the newDocument() method for a DocumentBuilder object, and it returns a reference to a new empty Document object:

Document newDoc = builder.newDocument();
 

This is rather limited, especially because there’s no way to modify the DocumentType node to reflect a suitable DOCTYPE declaration because the DocumentType interface does not declare any.

There’s an alternative approach that provides a bit more flexibility, but it is not quite so direct. You first call the getDOMImplementation() method for the DocumentBuilder object:

DOMImplementation domImpl = builder.getDOMImplementation();
 

This returns an org.w3c.dom.DOMImplementation reference to an object that encapsulates the underlying DOM implementation.

There are three methods you can call for a DOMImplementation object:

  • Document createDocument( String namespaceURI, String qualifiedName, DocumentType doctype): Creates a Document object with the root element having the name qualifiedName in the namespace specified by namespaceURI. The third argument specifies the DOCTYPE node to be added to the document. If you don’t want to declare a DOCTYPE then doctype can be null. The method throws a DOMException if the second argument is incorrect in some way.
  • DocumentType createDocumentType( String qualifiedName, String publicID, String systemID):Creates a DocumentType node that represents a DOCTYPE declaration. The first argument is the qualified name of the root element, the second is the public ID of the external subset of the DTD, and the third is its system ID. The method also throws a DOMException if the first argument contains an illegal character or is not of the correct form.
  • boolean hasFeature(String feature, String version): Returns true if the DOM implementation has the feature with the name feature. The second argument specifies the DOM version number for the feature and can be either "1.0" or "2.0" with DOM Level 2.

You can see from the first two methods here that there is a big advantage to using a DOMImplementation object to create a document. First of all, you can create a DocumentType object by calling the createDocumentType() method:

DocumentType doctype = null;
Path dtdFile = Paths.get(System.getProperty("user.home")).
                                       resolve("Beginning Java Stuff").resolve("sketcher.dtd");
try {     
  doctype = domImpl.createDocumentType("sketch", null, dtdFile.toString());
 
} catch(DOMException e) {
  // Handle the exception...
}
 

This creates a DocumentType node for an external DOCTYPE declaration. The first argument is the name of the document type, sketch, and the third argument is the system ID — the path to the DTD as a string, which identifies the DTD for documents of this type. There is no public ID in this case because the second argument is null.

You can now use the DocumentType object in the creation of a Document object:

Document newDoc = null;
try {     
  doctype = domImpl.createDocumentType("sketch", null, "sketcher.dtd");
  newDoc = domImpl.createDocument(null, "sketch", doctype);
} catch(DOMException e) {
  // Handle the exception...
}
 

If you were creating a document without a DTD, you would just specify the third argument to the createDocument() method as null.

The DOMException that may be thrown by either the createDocumentType() or the createDocument() method has a public field of type int that has the name code. This field stores an error code that identifies the type of error that caused the exception, so you can check its value to determine the cause of the error. This exception can be thrown by a number of different methods that create nodes in a document, so the values that code can have are not limited to the two methods you have just used. There are 17 possible values for code that are defined in the DOMException class, but obviously you would check only for those that apply to the code in the try block where the exception may arise.

The possible values for code in a DOMException object are:

  • INVALID_CHARACTER_ERR: An invalid character has been specified. In the previous code fragment this would mean the second argument to createDocument() specifying the root element name contains an invalid character.
  • DOMSTRING_SIZE_ERR: The specified range of text does not fit into a DOMString value. A DOMString value is defined in the DOM Level 3 specification and is equivalent to a Java String type.
  • HIERARCHY_REQUEST_ERR: You tried to insert a node where it doesn’t belong.
  • WRONG_DOCUMENT_ERR: You tried to use a node in a different document from the one that created it.
  • NO_DATA_ALLOWED_ERR: You specified data for a node that does not support data.
  • NO_MODIFICATION_ALLOWED_ERR: You attempted to modify an object where modifications are prohibited.
  • NOT_FOUND_ERR: You tried to reference a node that does not exist.
  • NOT_SUPPORTED_ERR: The object type or operation that you requested is not supported.
  • INUSE_ATTRIBUTE_ERR: You tried to add an attribute that is in use elsewhere.
  • INVALID_STATE_ERR: You tried to use an object that is not usable.
  • SYNTAX_ERR: You specified an invalid or illegal string.
  • INVALID_MODIFICATION_ERR: You tried to modify the type of the underlying object.
  • NAMESPACE_ERR: You tried to create or modify an object such that it would be inconsistent with namespaces in the document.
  • INVALID_ACCESS_ERR: A parameter or operation is not supported by the underlying object.
  • VALIDATION_ERR: An operation to remove or insert a node relative to an existing node would make the node invalid.
  • TYPE_MISMATCH_ERR: The type of an object is not compatible with the expected type of the parameter associated with the object.
  • WRONG_DOCUMENT_ERR: The document does not support the DocumentType node specified.

The createDocument() method can throw a DOMException with code set to INVALID_CHARACTER_ERR, NAMESPACE_ERR, NOT_SUPPORTED_ERR, or WRONG_DOCUMENT_ERR. The createDocumentType() method can also throw a DOMException with code set to any of the first three values for createDocument().

You therefore might code the catch block in the previous fragment like this:

catch(DOMException e) {
  switch(e.code) {
    case DOMException.INVALID_CHARACTER_ERR:
      System.err.println("Qualified name contains an invalid character.");
      break;
    case DOMException.NAMESPACE_ERR:
      System.err.println("Qualified name is malformed or invalid.");
      break;
    case DOMException.WRONG_DOCUMENT_ERR:
      System.err.println("Document does not support this doctype");
      break;
    case DOMException.NOT_SUPPORTED_ERR:
      System.err.println("Implementation does not support XML.");
      break;
    default:
      System.err.println("Code not recognized: " + e.code);
      break;
  }
  System.err.println(e.getMessage());
}
 

Of course, you can also output the stack trace, return from the method, or even end the program here if you want.

Adding to a Document

The org.w3c.Document interface declares methods for adding nodes to a Document object. You can create nodes encapsulating elements, attributes, text, entity references, comments, CDATA sections, and processing instructions, so you can assemble a Document object representing a complete XML document. The methods declared by the Document interface are the following:

  • Element createElement(String name): Returns a reference to an object encapsulating an element with name as the tag name. The method throws a DOMException with INVALID_CHARACTER_ERR set if name contains an invalid character.
  • Element createElementNS(String nsURI, String qualifiedName): Returns a reference to an object encapsulating an element with qualifiedName as the tag name in the namespace nsURI. The method throws a DOMException with INVALID_CHARACTER_ERR set if qualifiedName contains an invalid character or NAMESPACE_ERR if it has a prefix "xml" and nsURI is not http://www.w3.org/XML/1998/namespace.
  • Attr createAttribute(String name): Returns a reference to an Attr object with name as the attribute name and its value as "". The method throws a DOMException with INVALID_CHARACTER_ERR set if name contains an invalid character.
  • Attr createAttribute(String nsURI, String qualifiedName): Returns a reference to an Attr object with qualifiedName as the attribute name in the namespace nsURI and its value as "". The method throws a DOMException with INVALID_CHARACTER_ERR set if the name contains an invalid character or NAMESPACE_ERR if the name conflicts with the namespace.
  • Text createTextNode(String text): Returns a reference to a node containing the string text.
  • Comment createComment(String comment): Returns a reference to a node containing the string comment.
  • CDATASection createCDATASection(String data): Returns a reference to a node with the value data. Throws a DOMException if you try to create this node when the Document object encapsulates an HTML document.
  • EntityReference createEntityReference(String name):Returns a reference to an node with the name specified. Throws a DOMException with the code INVALID_CHARACTER_ERR if name contains invalid characters and NOT_SUPPORTED_ERR if the Document object is an HTML document.
  • ProcessingInstruction createProcessingInstruction(String target,String name):Returns a reference to a node with the specified name and target. Throws a DOMException with the code INVALID_CHARACTER_ERR if target contains illegal characters and NOT_SUPPORTED_ERR if the Document object is an HTML document.
  • DocumentFragment createDocumentFragment(): Creates an empty object. You can insert a DocumentFragment object into a Document object using methods that the Document and DocumentFragment interfaces inherit from the Node interface. You can use the same methods to insert nodes into a DocumentFragment object.

The return types are defined in the org.w3c.dom package. The references to HTML in the preceding list arise because a Document object can be used to encapsulate an HTML document. Our interest is purely XML so I’m not discussing this aspect further.

Of course, having a collection of nodes within a document does not define any structure. To establish the structure of a document you have to associate each attribute node that you have created with the appropriate element, and you must also make sure that each element other than the root is a child of some element. Along with all the other types of node, the org.w3c.dom.Element interface inherits two methods from the Node interface that enable you to make one node a child of another:

  • Node appendChild(Node child): Appends child to the end of the list of existing child nodes and returns child. The method throws a DOMException with the code HIERARCHY_REQUEST_ERR if the current node does not allow children, WRONG_DOCUMENT_ERR if child belongs to a document other than the one that created this node, or NO_MODIFICATION_ALLOWED_ERR if the current node is read-only.
  • Node insertBefore(Node child,Node existing): Inserts child as a child node immediately before existing in the current list of child nodes and returns child. This method throws DOMException with the same error codes as the preceding method, plus the error code NOT_FOUND_ERR if existing is not a child of the current node.

The Element interface also declares four methods that you use for adding attributes:

  • Attr setAttributeNode(Attr attr): Adds attr to the element. If an attribute node with the same name already exists, attr replaces it. The method returns either a reference to an existing Attr node that has been replaced or null. The method can throw a DOMException with the following codes:
    • WRONG_DOCUMENT_ERR if attr belongs to another document.
    • NO_MODIFICATION_ALLOWED_ERR if the element is read-only.
    • INUSE_ATTRIBUTE_ERR if attr already belongs to another element.
  • Attr setAttributeNodeNS(Attr attr): Same as the previous method, but applies to an element defined within a namespace.
  • void setAttribute(String name,String value): Adds a new attribute node with the specified name and value. If the attribute has already been added, its value is changed to value. The method can throw DOMException with the following codes:
    • INVALID_CHARACTER_ERR if name contains an illegal character.
    • NO_MODIFICATION_ALLOWED_ERR if the element is read-only.
  • void setAttributeNS(String nsURI,String qualifiedName,String value) Same as the previous method, but with the attribute within the namespace nsURI. In addition, this method throws a DOMException with the code NAMESPACE_ERR if qualifiedName is invalid or not within the namespace.

You know enough about constructing a Document object to have a stab at putting together an object encapsulating a real XML document, so let’s try it in the context of the Sketcher application.

STORING A SKETCH AS XML

You have already defined a DTD in the previous chapter that is suitable for defining a sketch. The code to store a sketch as an XML document instead of as a serialized object simply maps sketch elements to the corresponding XML elements. These elements are child nodes for a document node representing the entire sketch. You can create a Document object with a DocumentType node specifying sketcher.dtd as the DTD via a DOMImplementation object from a DocumentBuilder object. You can do this with statements in a try block:

Document doc = null;
try {
      DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
      builderFactory.setNamespaceAware(true);
      builderFactory.setValidating(true);
      builderFactory.setIgnoringElementContentWhitespace(true);
      Path dtdFile = Paths.get(System.getProperty("user.home")).
                    resolve("Beginning Java Stuff").resolve("sketcher.dtd");
      DOMImplementation domImpl = builderFactory.newDocumentBuilder().getDOMImplementation();
      doc = domImpl.createDocument(null, "sketch", domImpl.createDocumentType(
                                                          "sketch", null, dtdFile.toString()));
} catch(ParserConfigurationException e) {
  e.printStackTrace();
  // Display the error and terminate the current activity...
 
} catch(DOMException e) {
  // Determine the kind of error from the error code, 
  // display the error, and terminate the current activity...
}
 

The first statement creates a DocumentBuilderFactory object. The factory object is set to be namespace-aware and validating, so any document builder object you create validates the document against the DTD. A DOMImplementation object is obtained and stored in domImpl. This is used in the next statement to create the Document object for a sketch and its DocumentType object defining the DOCTYPE declaration for sketcher.dtd. Eventually you add code like this to the SketcherFrame class, but I’m leaving that to one side for the moment and looking at how you can fill out the detail of the Document object from the objects representing elements in a sketch.

A sketch in XML is a simple two-level structure. The root node in an XML representation of a sketch is a <sketch> element, and the child elements are XML elements defining sketch elements. To define the complete XML structure you need only to add an org.w3c.dom.Element node as a child of the root node for each element in the sketch. A good way to implement this would be to add a method to each of the sketch Element inner classes that creates its own org.w3c.dom.Element node and adds it to the root node for a Document object. This makes each object that encapsulates a sketch element able to create its own XML representation.

You have to modify the Element class as well as its inner classes that define concrete Sketcher class elements. The inner classes are Element.Line, Element.Rectangle, Element.Circle, Element.Curve, and Element.Text. The nodes that you must add for each kind of geometric element derive directly from the declaration in the DTD, so it helps if you have this handy while you go through these classes. If you have it as a file from when I discussed it in the last chapter, maybe you can print a copy.

Adding Element Nodes

Polymorphism is going to be a big help in generating the XML for a sketch, so let’s first define an abstract method in the Element base class to add an element node to a document. You can add the declaration immediately after the declaration for the other abstract methods, like this:

  public abstract void draw(Graphics2D g2D);
  public abstract void modify(Point start, Point last); 
  public abstract void addElementNode(Document document);
 

The parameter for this method is a reference to a Document object that encapsulates the XML for a sketch. An implementation adds a child node and adds it to the document. Each of the inner classes to Element implement this method.

You need a couple of import statements at the beginning of the Element.java file in Sketcher:

import org.w3c.dom.Document;
import org.w3c.dom.Attr;
 

Note that you need to use org.w3c.dom.Element to reference the XML element class type to avoid a potential clash with your sketch Element type.

The XML elements that you create from sketch elements all need <position>, <color>, and <bounds> elements as children. If you define methods to create these in the Element class, they are inherited in each of the subclasses of Element. Here’s how you can define a method in the Element class to create a <color> element:

image
  // Create an XML element for color
  protected org.w3c.dom.Element createColorElement(Document doc) {
    org.w3c.dom.Element colorElement = doc.createElement("color");
 
    Attr attr = doc.createAttribute("R");
    attr.setValue(String.valueOf(color.getRed()));
    colorElement.setAttributeNode(attr);
 
    attr = doc.createAttribute("G");
    attr.setValue(String.valueOf(color.getGreen()));
    colorElement.setAttributeNode(attr);
 
    attr = doc.createAttribute("B");
    attr.setValue(String.valueOf(color.getBlue()));
    colorElement.setAttributeNode(attr);
    return colorElement;
  }
 

Directory "Sketcher reading and writing XML"

The method for creating the node for a <position> element uses essentially the same process, but you have several nodes representing points that are the same apart from their names. You can share the code by putting it into a method in the Element class that you call with the appropriate XML element name as an argument:

image
  protected org.w3c.dom.Element createPointTypeElement(Document doc,
                                                       String name,
                                                       String xValue,
                                                       String yValue) {
    org.w3c.dom.Element element = doc.createElement(name);
 
    Attr attr = doc.createAttribute("x");         // Create attribute x
    attr.setValue(xValue);                        // and set its value
    element.setAttributeNode(attr);               // Insert the x attribute
 
    attr = doc.createAttribute("y");              // Create attribute y
    attr.setValue(yValue);                        // and set its value
    element.setAttributeNode(attr);               // Insert the y attribute
    return element;              
  }
 

Directory "Sketcher reading and writing XML"

This creates an element with the name specified by the second argument, so you can use this in another method in the Element class to create a node for a <position> element:

image
  // Create the XML element for the position of a sketch element
  protected org.w3c.dom.Element createPositionElement(Document doc) {
    return createPointTypeElement(doc, "position",
                                  String.valueOf(position.x),
                                  String.valueOf(position.y));
  }
 

Directory "Sketcher reading and writing XML"

You are able to create <endpoint> or <point> nodes in the same way in methods that you implement in the subclasses of Element.

You can create a <bounds> element like this:

image
  protected org.w3c.dom.Element createBoundsElement(Document doc) {
    org.w3c.dom.Element boundsElement = doc.createElement("bounds");
 
    Attr attr = doc.createAttribute("x");
    attr.setValue(String.valueOf(bounds.x));
    boundsElement.setAttributeNode(attr);
 
    attr = doc.createAttribute("y");
    attr.setValue(String.valueOf(bounds.y));
    boundsElement.setAttributeNode(attr);
 
    attr = doc.createAttribute("width");
    attr.setValue(String.valueOf(bounds.width));
    boundsElement.setAttributeNode(attr);
 
    attr = doc.createAttribute("height");
    attr.setValue(String.valueOf(bounds.height));
    boundsElement.setAttributeNode(attr);
    return boundsElement;
  }
 

Directory "Sketcher reading and writing XML"

This method extracts the x and y coordinates of the top-left corner and the values of the width and height for the bounds member of the Sketcher Element class and sets these as attribute values for the <bounds> XML element.

Adding a Line Node

The method to add a <line> node to the Document object creates an XML <line> element with an angle attribute and then adds four child elements: <color>, <position>, <endpoint>, and <bounds>. You can add the following implementation of the addElementNode() method to the Element.Line class:

image
    // Create XML element for a line
    public void addElementNode(Document doc) {
      org.w3c.dom.Element lineElement = doc.createElement("line");
 
      // Create the angle attribute and attach it to the <line> node
      Attr attr = doc.createAttribute("angle");
      attr.setValue(String.valueOf(angle));
      lineElement.setAttributeNode(attr);
 
      // Append the <color>, <position>, and <endpoint> nodes as children
      lineElement.appendChild(createColorElement(doc));
      lineElement.appendChild(createPositionElement(doc));
      lineElement.appendChild(createBoundsElement(doc));
      lineElement.appendChild(createEndpointElement(doc));
 
      // Append the <line> node to the document root node
      doc.getDocumentElement().appendChild(lineElement);
    }
 

Directory "Sketcher reading and writing XML"

Calling this method with a reference to the Document object that represents the sketch as an argument adds a <line> child node corresponding to the Element.Line object. To complete this you must add the createEndpointElement() method to the Element.Line class:

image
    // Create XML element for the end point of a line
    private org.w3c.dom.Element createEndpointElement(Document doc) {
      return createPointTypeElement(doc, "endpoint",
                             String.valueOf(line.x2), String.valueOf(line.y2));
    }
 

Directory "Sketcher reading and writing XML"

This method creates an XML <endpoint> element by calling the createPointTypeElement() method that Line inherits from the base class.

Adding a Rectangle Node

Next you can add the method to the Rectangle class that creates a <rectangle> child element in the Document object :

image
    // Create an XML element for a rectangle
    public void addElementNode(Document doc) {
      org.w3c.dom.Element rectElement = doc.createElement("rectangle");
 
      // Create the width & height attributes and attach them to the node
      Attr attr = doc.createAttribute("width");
      attr.setValue(String.valueOf(rectangle.width));
      rectElement.setAttributeNode(attr);
      attr = doc.createAttribute("height");
      attr.setValue(String.valueOf(rectangle.height));
      rectElement.setAttributeNode(attr);
 
      // Create the angle attribute and attach it to the <rectangle> node
      attr = doc.createAttribute("angle");
      attr.setValue(String.valueOf(angle));
      rectElement.setAttributeNode(attr);
 
      // Append the <color>, <position>, and <bounds> nodes as children
      rectElement.appendChild(createColorElement(doc));
      rectElement.appendChild(createPositionElement(doc));
      rectElement.appendChild(createBoundsElement(doc));
 
      doc.getDocumentElement().appendChild(rectElement);
    }

Directory "Sketcher reading and writing XML"

After creating the node for the Rectangle object, you set the width, height, and angle as attributes. You then append the child elements for the element color, position, and bounding rectangle. Finally you append the <rectangle> element as a child for the document root node.

Adding a Circle Node

Creating the node for a <circle> element in the Element.Circle class is not very different:

image
    // Create an XML element for a circle
    public void addElementNode(Document doc) {
      org.w3c.dom.Element circleElement = doc.createElement("circle");
 
      // Create the diameter attribute and attach it to the <circle> node
      Attr attr = doc.createAttribute("diameter");
      attr.setValue(String.valueOf(circle.width));
      circleElement.setAttributeNode(attr);
 
      // Create the angle attribute and attach it to the <circle> node
      attr = doc.createAttribute("angle");
      attr.setValue(String.valueOf(angle));
      circleElement.setAttributeNode(attr);
 
      // Append the <color> and <position> nodes as children
      circleElement.appendChild(createColorElement(doc));
      circleElement.appendChild(createPositionElement(doc));
      circleElement.appendChild(createBoundsElement(doc));
 
      doc.getDocumentElement().appendChild(circleElement);
    }
 

Directory "Sketcher reading and writing XML"

There’s nothing new here. You can use either the width or the height member of the Ellipse2D.Double class object as the diameter of the circle because they have the same value.

Adding a Curve Node

Creating an XML <curve> node for an Element.Curve object is a bit more long-winded. A curve is represented by a GeneralPath object, and you have to add all the defining points after the first as child elements to the <curve> element. You can obtain a special iterator object of type java.awt.geom.PathIterator for a GeneralPath object by calling its getPathIterator() method. The PathIterator object provides access to all the information you need to re-create the GeneralPath object.

PathIterator is an interface that declares methods for retrieving details of the segments that make up a GeneralPath object, so a reference to an object of type PathIterator encapsulates all the data defining that path.

The argument to getPathIterator() is an AffineTransform object that is applied to the path. This provides for the possibility that a single GeneralPath object may be used to create a number of different appearances on the screen simply by applying different transformations to the same object. You might have a GeneralPath object that defines a complicated object, a boat, for example. You could draw several boats on the screen from the one object simply by applying a transform before you draw each boat to set its position and orientation.

In Sketcher you want an iterator for the unmodified path, so you pass a default AffineTransform object that does nothing to the getPathIterator() method.

The PathIterator interface declares five methods:

  • int currentSegment(double[] coords): coords is used to store data relating to the current segment as double values and must have six elements to record the coordinate pairs. This is to record coordinates for one, two, or three points, depending on the current segment type. Our case only uses line segments so coordinates for one point are always returned. The method returns one of the following constants defined in the PathIterator interface:
    • SEG_MOVETO if the segment corresponds to a moveTo() operation. The coordinates of the point moved to are returned as the first two elements of the array coords.
    • SEG_LINETO if the segment corresponds to a lineTo() operation. The coordinates of the end point are returned as the first two elements of the array coords.
    • SEG_QUADTO if the segment corresponds to a quadTo() operation. The coordinates of the control point for the quadratic segment are returned as the first two elements of the coords array, and the end point coordinates are returned in the third and fourth elements.
    • SEG_CUBICTO if the segment corresponds to a curveTo() operation. The coords array contains coordinates of the first control point, the second control point, and the end point of the cubic curve segment.
    • SEG_CLOSE if the segment corresponds to a closePath() operation. No values are returned in the coords array.
  • int currentSegment(float[] coords): Stores data for the current segment as float values. The value returned is the same as it is in the previous method.
  • int getWindingRule(): Returns a value identifying the winding rule in effect. The value can be WIND_EVEN_ODD or WIND_NON_ZERO.
  • void next(): Moves the iterator to the next segment as long as there is another segment.
  • boolean isDone(): Returns true if the iteration is complete and returns false otherwise.

You have all the tools you need to get the data on every segment in the path. You just need to get a PathIterator reference and use the next() method to go through the segments in the path. The case for an Element.Curve object is simple: You have only a single moveTo() segment that is always to (0, 0) followed by one or more lineTo() segments. Even though this is a fixed pattern, you still test the return value from the currentSegment() method to show how it’s done and in case there are errors.

The first segment is a special case. It is always a move to (0, 0), whereas all the others are lines. Thus the procedure is to get the first segment and discard it after verifying it is a move, and then get the remaining segments in a loop. Here’s the code to create the XML:

image
    // Create an XML element for a curve
    public void addElementNode(Document doc) {
      org.w3c.dom.Element curveElement = doc.createElement("curve");
 
      // Create the angle attribute and attach it to the <curve> node
      Attr attr = doc.createAttribute("angle");
      attr.setValue(String.valueOf(angle));
      curveElement.setAttributeNode(attr);
 
      // Append the <color> and <position> nodes as children
      curveElement.appendChild(createColorElement(doc));
      curveElement.appendChild(createPositionElement(doc));
      curveElement.appendChild(createBoundsElement(doc));
 
      // Get the defining points via a path iterator
      PathIterator iterator = curve.getPathIterator(new AffineTransform());
      int maxCoordCount = 6;                         // Maximum coordinates for a segment
      float[] temp = new float[maxCoordCount];         // Stores segment data
 
      int result = iterator.currentSegment(temp);    // Get first segment
      assert result == PathIterator.SEG_MOVETO;      // ... should be move to
 
      iterator.next();                               // Next segment
      while(!iterator.isDone())   {                  // While you have segments
        result = iterator.currentSegment(temp);      // Get the segment data
        assert result == PathIterator.SEG_LINETO;    // Should all be lines
 
        // Create a <point> node and add it to the list of children
        curveElement.appendChild(createPointTypeElement(doc, "point",
                                      String.valueOf(temp[0]),
                                      String.valueOf(temp[1])));
        iterator.next();                             // Go to next segment
      }
      doc.getDocumentElement().appendChild(curveElement);
    }
 

Directory "Sketcher reading and writing XML"

The angle attribute and the position, color, and bounds elements are added in the same way as for other elements. You use a PathIterator object to go through all the points in the path that defines the curve. You add one <point> node as a child of the Element node for a curve for each defining point after the first. The assertion in the loop verifies that each segment is a line segment.

Adding a Node for a Text Element

A node for an Element.Text object is a little different and also involves quite a lot of code. As well as the usual <color>, <position>, and <bounds> child nodes, you also have to append a <font> node to define the font and a <string> node containing the text. The <font> node has three attributes that define the font name, the font style, and the point size. There is also an attribute to record the maxAscent value. Here’s the code:

image
    // Create an XML element for a sketch Text element
    public void addElementNode(Document doc) {
      org.w3c.dom.Element textElement = doc.createElement("text");
 
      // Create the angle attribute and attach it to the <text> node
      Attr attr = doc.createAttribute("angle");
      attr.setValue(String.valueOf(angle));
      textElement.setAttributeNode(attr);
 
      // Create the maxascent attribute and attach it to the <text> node
      attr = doc.createAttribute("maxascent");
      attr.setValue(String.valueOf(maxAscent));
      textElement.setAttributeNode(attr);
 
      // Append the <color> and <position> nodes as children
      textElement.appendChild(createColorElement(doc));
      textElement.appendChild(createPositionElement(doc));
      textElement.appendChild(createBoundsElement(doc));
 
      // Create and apppend the <font> node
      org.w3c.dom.Element fontElement = doc.createElement("font");
      attr = doc.createAttribute("fontname");
      attr.setValue(font.getName());
      fontElement.setAttributeNode(attr);
 
      attr = doc.createAttribute("fontstyle");
      String style = null;
      int styleCode = font.getStyle();
      if(styleCode == Font.PLAIN) {
        style = "plain";
      } else if(styleCode == Font.BOLD) {
        style = "bold";
      } else if(styleCode == Font.ITALIC) {
        style = "italic";
      } else if(styleCode == Font.ITALIC + Font.BOLD) {
          style = "bold-italic";
      }
      assert style != null;
      attr.setValue(style);
      fontElement.setAttributeNode(attr);
 
      attr = doc.createAttribute("pointsize");
      attr.setValue(String.valueOf(font.getSize()));
      fontElement.setAttributeNode(attr);
      textElement.appendChild(fontElement);
 
      // Create the <string> node
      org.w3c.dom.Element string = doc.createElement("string");
      string.setTextContent(text);
      textElement.appendChild(string);
 
      doc.getDocumentElement().appendChild(textElement);
    }

Directory "Sketcher reading and writing XML"

Most of this code is what you have seen for other types of sketch element. Because the font style attribute value can be "plain," "bold," "bold-italic," or just "italic," you have a series of if statements to determine the attribute value. A Font object stores the style as an integer with different values for plain, bold, and italic. The values for bold and italic may be combined, in which case the attribute value is "bold-italic."

All the element objects in a sketch can now add their own XML element nodes to a Document object. You should now be able to use this capability to create a document that encapsulates the entire sketch.

Creating a Document Object for a Complete Sketch

You can add a createDocument() method to the SketcherFrame class to create a Document object and populate it with the nodes for the elements in the current sketch model. Creating the Document object uses the code fragment you saw earlier. You need to add some import statements at the beginning of the SketcherFrame.java source file for the new interfaces and classes you are using:

import javax.xml.parsers.*;
import org.w3c.dom.*;
 

Here’s the method definition you can add to the class:

image
  // Creates a DOM Document object encapsulating the current sketch
  public Document createDocument() {
    Document doc = null;
    try {
      DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
      builderFactory.setNamespaceAware(true);
      builderFactory.setValidating(true);
      builderFactory.setIgnoringElementContentWhitespace(true);
      DocumentBuilder builder = builderFactory.newDocumentBuilder();
      builder.setErrorHandler(this);
      DOMImplementation domImpl = builder.getDOMImplementation();
      Path dtdFile = Paths.get(System.getProperty("user.home")).
                                 resolve("Beginning Java Stuff").resolve("sketcher.dtd");
      doc = domImpl.createDocument(null, "sketch", domImpl.createDocumentType(
                                                    "sketch", null, dtdFile.toString()));
    } catch(ParserConfigurationException pce) {
      JOptionPane.showMessageDialog(this,
                                     "Parser configuration error while creating document",
                                     "DOM Parser Error",
                                     JOptionPane.ERROR_MESSAGE);
      System.err.println(pce.getMessage());
      pce.printStackTrace();
      return null;
    } catch(DOMException de) {
      JOptionPane.showInternalMessageDialog(null,
                                     "DOM exception thrown while creating document",
                                     "DOM Error",
                                     JOptionPane.ERROR_MESSAGE);
      System.err.println(de.getMessage());
      de.printStackTrace();
      return null;
   }
  
    // Each element in the sketch can create its own node in the document
    SketcherModel elements = theApp.getModel();  // Get the sketch
    for(Element element : elements) {            // For each element...
      element.addElementNode(doc);               // ...add its node.
    }
    return doc;
  }
 

Directory "Sketcher reading and writing XML"

Notice that this assumes that the DTD file for Sketcher should be in your Beg Java Stuff folder. If it isn’t, amend the code accordingly. You call setErrorHandler() for the DocumentBuilder object to make the SketcherFrame object the handler for parsing errors. This implies that you must make the SketcherFrame class implement the ErrorHandler interface.

You pop up a dialog and return null if something goes wrong when you are creating the Document object. In case of a DOMException being thrown, you could add a switch statement to analyze the value in the code member of the exception and provide a more specific message in the dialog.

To implement the ErrorHandler interface, first amend the first line of the SketcherFrame class definition:

public class SketcherFrame extends JFrame
                 implements ActionListener, Observer, Printable, ErrorHandler {

Now you can add the following three method definitions that are required:

image
  // Handles recoverable errors from parsing XML
  public void error(SAXParseException spe) {
      JOptionPane.showMessageDialog(SketcherFrame.this,
                        "Error at line " + spe.getLineNumber() + "
" + spe.getMessage(),
                        "DOM Parser Error",
                        JOptionPane.ERROR_MESSAGE);
  }
 
  // Handles fatal errors from parsing XML
  public void fatalError(SAXParseException spe) throws SAXParseException {
      JOptionPane.showMessageDialog(SketcherFrame.this,
                  "Fatal error at line " + spe.getLineNumber() + "
" + spe.getMessage(),
                  "DOM Parser Error",
                  JOptionPane.ERROR_MESSAGE);
      throw spe;
  }
 
  // Handles warnings from parsing XML
  public void warning(SAXParseException spe) {
      JOptionPane.showMessageDialog(SketcherFrame.this,
                       Warning at line " + spe.getLineNumber() + "
" + spe.getMessage(),
                       "DOM Parser Error",
                       JOptionPane.ERROR_MESSAGE);
  }

Directory "Sketcher reading and writing XML"

In each case you display a dialog providing information about the error. For a fatal error, you throw SAXParseException, which terminates parsing. There should not be any errors occurring because you are processing XML that was created by the code in Sketcher. You need another import statement in SketcherFrame.java:

import org.xml.sax.*;
 

The SketcherFrame object can now create a DOM Document object encapsulating the entire sketch. All you now need is some code to provide the GUI for exporting sketches as XML, and code to use the Document object to write an XML file.

Saving a Sketch as XML

Of course, you could modify Sketcher so that you could set an option to save sketches either as objects or as XML documents. However, to keep things simple you will add menu items to the File menu to export or import a sketch as XML. In this way, you keep the code for reading and writing XML files separate from the code for reading and writing .ske files.

In broad terms, here’s what you have to do to the SketcherFrame class to save a sketch as an XML file:

  • Add Import XML and Export XML menu items.
  • Implement the process of creating an XML document from the Document object that the createDocument() method creates in response to an Export XML menu item event.
  • Implement writing the XML document that is generated from a sketch to a file.

You can add new FileAction objects for the two new menu items. Clearly, a lot of the work is in the implementation of the new functionality in the actionPerformed() method in the FileAction class, so let’s start with the easy bit — adding the new menu items to the File menu. First, you can add two new fields for the menu items by changing the existing definition in the SketcherFrame class:

  private FileAction newAction,    openAction,   closeAction,
                     saveAction,   saveAsAction, printAction,
                     exportAction, importAction, exitAction;
 

You can create the Action objects for the two new menu items in the createFileMenuActions() method, following the creation of the Action item for the Print menu item:

    printAction = new FileAction("Print", 'P', CTRL_DOWN_MASK);
    exportAction = new FileAction("Export XML", 'E', CTRL_DOWN_MASK);
    importAction = new FileAction("Import XML", 'I', CTRL_DOWN_MASK);
    exitAction = new FileAction("Exit", 'X', CTRL_DOWN_MASK);
 

The new fields store references to the Action objects for the new menu items. You don’t need to add these to the actions array definition because you use the array to create toolbars for corresponding menu items and you won’t be adding toolbar buttons for the XML I/O operations.

You can add the SHORT_DESCRIPTION property values for the new Action objects in the createFileMenuActions() method, where it does this for the other Action objects:

    exportAction.putValue(SHORT_DESCRIPTION, "Export sketch as an XML file");
    importAction.putValue(SHORT_DESCRIPTION, "Import sketch from an XML file");
 

You can add the menu items to the File menu in the createFileMenu() method, immediately before the statement adding the separator that precedes the exitAction:

    fileMenu.addSeparator();                          // Add separator
    fileMenu.add(exportAction);                       // Export XML menu item
    fileMenu.add(importAction);                       // Import XML menu item
    fileMenu.addSeparator();                             // Add separator
    fileMenu.add(exitAction);                            // Print sketch menu item
    menuBar.add(fileMenu);                               // Add the file menu
 

Next you can add an extra else if block in the actionPerformed() method in the FileAction class to respond to events from the new menu items:

    public void actionPerformed(ActionEvent e) {
      // Code for if statement as before...
      } else if(this == exitAction) {
        // Code to handle exitAction event as before...
      } else if(this == exportAction) {
        exportXMLOperation();                         // Export sketch as XML
 
      } else if(this == importAction) {
        importXMLOperation();                         // Import an XML sketch
      }
    }
 

The exportXMLOperation() method you add to the SketcherFrame class takes care of handling events for the Export XML menu item, and the importXMLOperation() method deals with events for the Import XML menu item.

Exporting a Sketch as XML

To export a sketch as XML, you can use the showDialog() method that creates a file chooser dialog to identify the file for the XML markup output. The dialog uses a new file filter that you can define as a field in the SketcherFrame class:

  private ExtensionFilter xmlFileFilter = new ExtensionFilter(
                                           ".xml", "XML Sketch files (*.xml)");
 

This defines a filter for files with the extension .xml. The showDialog() method should ensure that a selected file path ends in XML when the xmlFileFilter is in effect because this is an indicator that the dialog is used for an XML file operation. You can implement this by adding a statement to the showDialog() method:

    Path selectedFile = null;
    if(file == null) {
      selectedFile = Paths.get(
               fileChooser.getCurrentDirectory().toString(), DEFAULT_FILENAME);
    } else {
      selectedFile = file;
    }
    selectedFile = setFileExtension(
                      selectedFile, filter == xmlFileFilter ? ".xml" : ".ske");
    fileChooser.setSelectedFile(new File(selectedFile.toString()));
 

The second argument to the setFileExtension() method call is ".xml" when the XML file filter is in effect and ".ske" otherwise. Note that this only guarantees that the file that is initially selected has the extension .xml. The path that is returned may not.

You can implement the method to export a sketch as XML in the SketcherFrame class like this:

image
  // Export a sketch as XML
  private void exportXMLOperation() {
    Path selectedFile = null;
    if(currentSketchFile == null) {
      selectedFile = Paths.get(
               fileChooser.getCurrentDirectory().toString(), DEFAULT_FILENAME);
    } else {
      selectedFile = currentSketchFile;
    }
    // Make extension .xml
    selectedFile = setFileExtension(selectedFile, ".xml"); 
 
    Path file  = showDialog("Export Sketch as XML", "Export",
                          "Export sketch as XML", xmlFileFilter, selectedFile);
    if(file == null) {                            // No file selected...
      return;                                    // ... so we are done.
    }
 
    if(Files.exists(file) &&                      // If the path exists and...
         JOptionPane.NO_OPTION ==                // .. NO selected in dialog...
              JOptionPane.showConfirmDialog(
                                  this,
                                  file.getFileName() + " exists. Overwrite?",
                                  "Confirm Save As",
                                  JOptionPane.YES_NO_OPTION,
                                  JOptionPane.WARNING_MESSAGE)) {
            return;                              // ...do nothing
    }
    saveXMLSketch(file);
  }
 

Directory "Sketcher reading and writing XML"

The exportXMLOperation() method uses the showDialog() method that you create for use when you are saving the sketch normally. The dialog has a filter for XML files in effect, so the user should only see those in the file list. If the dialog returns a non-null path, you first ensure the extension is .xml. Then you check whether the file exists and provide the user with the option to overwrite it if it does. Finally you call saveXMLSketch() to write the file, so you had better implement that next.

Writing the XML File

The chosen file is passed to a new saveXMLSketch() method in SketcherFrame that writes the XML document to the file. You can create a Document object encapsulating the XML markup for the current sketch and then use a Transformer object to write the file in the way that you saw earlier in this chapter. Here’s the code:

image
  // Write XML sketch to file
  private void saveXMLSketch(Path file) {
    Document document = createDocument();             // XML representation of the sketch
    Node node = document.getDocumentElement();        // Document tree base
    try(BufferedOutputStream xmlOut =
                       new BufferedOutputStream(Files.newOutputStream(file))) {
        TransformerFactory factory = TransformerFactory.newInstance();
 
        // Create transformer
        Transformer transformer = factory.newTransformer(); 
        transformer.setErrorListener(this);
 
        // Set properties - add whitespace for readability
        //                - include DOCTYPE declaration in output
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        transformer.setOutputProperty(
                  OutputKeys.DOCTYPE_SYSTEM, "D:/Beg Java Stuff/sketcher.dtd");
 
        // Source is the document object - result is the file
        DOMSource source = new DOMSource(node); 
        StreamResult xmlFile = new StreamResult(xmlOut);
        transformer.transform(source, xmlFile);       // Write XML to file
    } catch (TransformerConfigurationException tce) {
        System.err.println("Transformer Factory error: " + tce.getMessage());
    } catch (TransformerException te) {
        System.err.println("Transformation error: " + te.getMessage());
    } catch (IOException e) {
        System.err.println("I/O error writing XML file: " + e.getMessage());
    }
  }

Directory "Sketcher reading and writing XML"

You first create a Document object for the sketch by calling the createDocument() method that you added to SketcherFrame earlier in this chapter. You create an output stream corresponding to the Path object that is passed to the method in the try block. To create a Transformer object that writes the file, you first create a TransformerFactory object by calling the static newInstance() method. Calling the newTransformer() method with no arguments for the factory object creates a Transformer object that represents an identity transform. You set the SketcherFrame object to be the error listener for the Transformer object so SketcherFrame needs to implement the ErrorListener interface.

You set the values for two properties for the transformer. You set the value for INDENT key to “yes" so whitespace is inserted between elements in the output and the value for DOCTYPE_SYSTEM key to the file path for the DTD.

To write the file, you define the Document object as the source for the transform and the xmlFile output stream object as the result. Calling transform() for the transformer object with these as arguments writes the file.

To implement the ErrorListener interface in the SketcherFrame class, first amend the first line of the class definition:

public class SketcherFrame extends JFrame
  implements ActionListener, Observer, Printable, ErrorHandler, ErrorListener {
 

Next add definitions for the methods declared in the interface:

image
  // Handles recoverable errors from transforming XML
  public void error(TransformerException te) {
    System.err.println("Error transforming XML: " + te.getMessage());
  }
 
  // Handles fatal errors from transforming XML
  public void fatalError(TransformerException te) {
    System.err.println("Fatal error transforming XML: " + te.getMessage());
    System.exit(1);
  }
 
  // Handles warnings from transforming XML
  public void warning(TransformerException te) {
    System.err.println("Warning transforming XML: " + te.getMessage());
  }
 

Directory "Sketcher reading and writing XML"

Each method just outputs an error message. With a fatal error, you abort the program. You could throw TransformerException if you want to simply stop the transformer. Some more import statements are needed in SketcherFrame.java:

import javax.xml.transform.*;
import javax.xml.transform.stream.*;
import javax.xml.transform.dom.*;
import javax.xml.validation.*;
 

You should be able to recompile Sketcher at this point if you want to try out writing a sketch as XML. The file that is produced should be quite readable, and you can view it using any plain text editor, such as Notepad.

READING AN XML REPRESENTATION OF A SKETCH

Reading a sketch from an XML file is the reverse of writing it. You can use a Transformer object to create a Document object from the XML markup in the file. You can then iterate through the child nodes in the Document object and create sketch Element objects from them. You need a way to create sketch elements from the XML elements, and the obvious way to tackle this is to add constructors to the classes derived from the Element class that create an object from the Node object that encapsulates the XML for the element. These new instructors must be able to call a base class constructor as the first statement. However, the existing Element class constructors are not suitable because they require arguments that are not available when one of the new constructors starts executing. Let’s see how you can accommodate this and provide for initializing the base class fields.

Creating the Base Class Object from XML

You can add a no-arg constructor to the Sketcher Element class to solve the problem:

protected Element() {}
 

This constructor creates the object but does not initialize the base class fields. That is down to the derived class constructors.

If the Element class has methods to initialize its fields from an XML node object, these can be used by all of the derived classes when an object is to be created from XML. Here’s the method to initialize the angle field:

image
  // Set angle field value from a node
  protected void setAngleFromXML(Node node) {
    angle = Double.valueOf(((Attr)(node.getAttributes().getNamedItem("angle"))).getValue());
  }
 

Directory "Sketcher reading and writing XML"

The Node object that is passed to the method contains the value for angle as an attribute. Calling getAttributes() for node returns all the attributes as a NamedNodeMap object. Calling getNamedItem() for this object returns the attribute, with the name you pass as the argument, as a Node object. The getValue() method for a Node object returns its value as a String object, so passing this to the static valueOf() method in the Double class returns the angle value as type double.

This pattern for obtaining the value of an attribute from a Node object appears frequently. Here’s how you can initialize the position field with another method in the Element class:

image
  // Set position field from a node
  protected void setPositionFromXML(Node node) {
    NamedNodeMap attrs = node.getAttributes();
    position = new Point(
                Integer.valueOf(((Attr)(attrs.getNamedItem("x"))).getValue()),
                Integer.valueOf(((Attr)(attrs.getNamedItem("y"))).getValue()));
  }
 

Directory "Sketcher reading and writing XML"

This method closely parallels the previous method. You extract the values for the coordinates of the point from the attributes as integers. You then pass these to a Point class constructor.

The code to initialize the color field is very similar:

image
  // Set color field from a node
  protected void setColorFromXML(Node node) {
    NamedNodeMap attrs = node.getAttributes();
    color = new Color(
                Integer.valueOf(((Attr)(attrs.getNamedItem("R"))).getValue()),
                Integer.valueOf(((Attr)(attrs.getNamedItem("G"))).getValue()),
                Integer.valueOf(((Attr)(attrs.getNamedItem("B"))).getValue()));
  }
 

Directory "Sketcher reading and writing XML"

This works essentially the same way as the previous method. You extract each of the color component values as integers from the corresponding attributes and pass them to the Color class constructor.

Creating the java.awt.Rectangle object that you need to initialize the bounds field is virtually the same:

image
  // Set bounds field from a node
  protected void setBoundsFromXML(Node node) {
    NamedNodeMap attrs = node.getAttributes();
    bounds = new java.awt.Rectangle(
           Integer.valueOf(((Attr)(attrs.getNamedItem("x"))).getValue()),
           Integer.valueOf(((Attr)(attrs.getNamedItem("y"))).getValue()),
           Integer.valueOf(((Attr)(attrs.getNamedItem("width"))).getValue()),
           Integer.valueOf(((Attr)(attrs.getNamedItem("height"))).getValue()));
  }
 

Directory "Sketcher reading and writing XML"

You obtain the four attribute values in exactly the same way as in the other methods.

Creating Elements from XML Nodes

You add a constructor to each of the inner classes of Element that has a parameter of type Node. The Node object passed to each constructor encapsulates the XML child element that was created from an object of the inner class type. The subclasses of Element inherit the methods you have added that initialize the base class fields, so they can be called from the new constructor. A call to the no-arg Element class constructor is inserted automatically as the first statement in each of the new inner class constructors. Let’s start with the Element.Line class constructor.

Creating a Line Object from an XML Node

A Node object that defines a line has the angle as an attribute, and four child nodes specifying the position, color, bounds, and the line end point. Here’s the constructor to decode that lot:

image
    // Create Line object from XML node
    public Line(Node node) {
      setAngleFromXML(node);
      NodeList childNodes = node.getChildNodes();
      Node aNode = null;
      for(int i = 0 ; i < childNodes.getLength() ; ++i) {
        aNode = childNodes.item(i);
        switch(aNode.getNodeName()) {
          case "position":
            setPositionFromXML(aNode);
            break;
          case "color":
            setColorFromXML(aNode);
            break;
          case "bounds":
            setBoundsFromXML(aNode);
            break;
          case "endpoint":
            NamedNodeMap coords = aNode.getAttributes();
            line = new Line2D.Double();
            line.x2 = Double.valueOf(((Attr)(coords.getNamedItem("x"))).getValue());
            line.y2 = Double.valueOf(((Attr)(coords.getNamedItem("y"))).getValue());
            break;
          default:
            System.err.println("Invalid node in <line>: " + aNode);
            break;
        }
      }
    }
 

Directory "Sketcher reading and writing XML"

You set the angle for the line by calling the method that you added to the base class for this purpose. The getChildNode() method returns the child nodes in a NodeList object. You iterate over all the nodes in the childNodes list in the for loop. Calling getItem() for childNodes returns the Node object at the index position you pass to the method. You then process the node in the switch statement, selecting on the basis of the node name, which is a String. Each of the base class fields are set by calling one or other of the methods you have defined. For the <endpoint> node, you reconstruct the Line2D.Double object from the attributes for the node. Calling the no-arg constructor for Line2D.Double creates an object with the start and end points as (0,0), so you just have to set the end point coordinates, which are stored in line.x2 and line.y2.

Creating a Rectangle Object from an XML Node

Most of the code to reconstruct an Element.Rectangle object is the same as for a line:

image
    // Create Rectangle object from XML node
    public Rectangle(Node node) {
      setAngleFromXML(node);
      NodeList childNodes = node.getChildNodes();
      Node aNode = null;
      for(int i = 0 ; i < childNodes.getLength() ; ++i) {
        aNode = childNodes.item(i);
        switch(aNode.getNodeName()) {
          case "position":
            setPositionFromXML(aNode);
            break;
          case "color":
            setColorFromXML(aNode);
            break;
          case "bounds":
            setBoundsFromXML(aNode);
            break;
          default:
            System.err.println("Invalid node in <rectangle>: " + aNode);
            break;
        }
      }
      NamedNodeMap attrs = node.getAttributes();
      rectangle = new Rectangle2D.Double();
      rectangle.width = Double.valueOf(((Attr)(attrs.getNamedItem("width"))).getValue());
      rectangle.height =
             Double.valueOf(((Attr)(attrs.getNamedItem("height"))).getValue());
    }
 

Directory "Sketcher reading and writing XML"

The no-arg constructor for a Rectangle2D.Double object creates an object at (0,0) with a width and height of zero. The width and height of the rectangle are recorded as XML attributes, so you retrieve these and set the width and height of the object that you have created.

Creating a Circle Object from an XML Node

This constructor is almost identical to the previous constructor:

image
    // Create Circle object from XML node
    public Circle(Node node) {
      setAngleFromXML(node);
      NodeList childNodes = node.getChildNodes();
      Node aNode = null;
      for(int i = 0 ; i < childNodes.getLength() ; ++i) {
        aNode = childNodes.item(i);
        switch(aNode.getNodeName()) {
          case "position":
            setPositionFromXML(aNode);
            break;
          case "color":
            setColorFromXML(aNode);
            break;
          case "bounds":
            setBoundsFromXML(aNode);
            break;
          default:
            System.err.println("Invalid node in <circle>: " + aNode);
            break;
        }
      }
      NamedNodeMap attrs = node.getAttributes();
      circle = new Ellipse2D.Double();
      circle.width = circle.height =
           Double.valueOf(((Attr)(attrs.getNamedItem("diameter"))).getValue());
    }
 

Directory "Sketcher reading and writing XML"

There’s not a lot to say about this that is new. You reconstruct the Ellipse2D.Double object using the no-arg constructor and set its width and height to be the value you extract for the "diameter" attribute from the <circle> element.

Creating a Curve Object from an XML Node

Creating an Element.Curve object is inevitably different from previous object types, but not substantially different. The initial MOVE_TO segment for the general path is to the origin, so that requires no information from the Node object. The remaining LINE_TO segments are specified by <point> child nodes, so you just need to add a segment to the GeneralPath object corresponding to each <point> child node that is present. Here’s the code:

image
    // Create Curve object from XML node
    public Curve(Node node) {
      curve = new GeneralPath();
      curve.moveTo(origin.x, origin.y);       // Set current position as origin
      setAngleFromXML(node);
      NodeList childNodes = node.getChildNodes();
      Node aNode = null;
      for(int i = 0 ; i < childNodes.getLength() ; ++i) {
        aNode = childNodes.item(i);
        switch(aNode.getNodeName()) {
          case "position":
            setPositionFromXML(aNode);
            break;
          case "color":
            setColorFromXML(aNode);
            break;
          case "bounds":
            setBoundsFromXML(aNode);
            break;
          case "point":
            NamedNodeMap attrs = aNode.getAttributes();
            curve.lineTo(
                Double.valueOf(((Attr)(attrs.getNamedItem("x"))).getValue()),
                Double.valueOf(((Attr)(attrs.getNamedItem("y"))).getValue()));
          break;
          default:
            System.err.println("Invalid node in <curve>: " + aNode);
            break;
        }
      }
    }
 

Directory "Sketcher reading and writing XML"

The base class fields are set in the same way as for other objects. You create an empty GeneralPath object before the loop that iterates over the child nodes and set the first segment as a move to the origin. For each child node with the name "point" you add a LINE_TO segment using the values stored as attributes for the <point> node. This re-creates the original curve representation.

Creating a Text Object from an XML Node

A <text> XML node has the angle and the maxAscent field values as attributes, and you extract those first. Here’s the code for the constructor:

image
    // Create Text object from XML node
    public Text(Node node) {
      NamedNodeMap attrs = node.getAttributes();
      angle = Double.valueOf(((Attr)(attrs.getNamedItem("angle"))).getValue());
      maxAscent =
         Integer.valueOf(((Attr)(attrs.getNamedItem("maxascent"))).getValue());
 
      NodeList childNodes = node.getChildNodes();
      Node aNode = null;
      for(int i = 0 ; i < childNodes.getLength() ; ++i) {
        aNode = childNodes.item(i);
        switch(aNode.getNodeName()) {
          case "position":
            setPositionFromXML(aNode);
            break;
          case "color":
            setColorFromXML(aNode);
            break;
          case "bounds":
            setBoundsFromXML(aNode);
            break;
          case "font":
            setFontFromXML(aNode);
          break;
          case "string":
            text = aNode.getTextContent();
            break;
          default:
            System.err.println("Invalid node in <text>: " + aNode);
            break;
        }
      }
    }
 

Directory "Sketcher reading and writing XML"

The process for extracting child nodes from the Node object passed to the constructor is as you have seen for the other constructors. For the <string> child node, you call its getTextContent() method to obtain the text for the object. You call a new method that creates the font field from a node corresponding to a <font> child element. You can add the setFontFromXML() method to the Element.Text class like this:

image
    // Set the font field from an XML node
    private void setFontFromXML(Node node) {
      NamedNodeMap attrs = node.getAttributes();
      String fontName = ((Attr)(attrs.getNamedItem("fontname"))).getValue();
      String style = ((Attr)(attrs.getNamedItem("fontstyle"))).getValue();
      int fontStyle = 0;
      switch(style){
        case "plain":
          fontStyle = Font.PLAIN;
          break;
        case "bold":
          fontStyle = Font.BOLD;
          break;
        case "italic":
          fontStyle = Font.ITALIC;
          break;
        case "bold-italic":
          fontStyle = Font.ITALIC|Font.BOLD;
          break;
        default:
          System.err.println("Invalid font style code: " + style);
          break;
      }
      int pointSize =
         Integer.valueOf(((Attr)(attrs.getNamedItem("pointsize"))).getValue());
      font = new Font(fontName, fontStyle, pointSize);
    }
 

Directory "Sketcher reading and writing XML"

The font name, style, and point size are all recorded as attributes for the <font> XML node. The font style is recorded as one of four possible strings, so for each of these you set the value of the fontStyle variable to the corresponding integer constant from the Font class. Because you need to extract the node attributes directly here, you extract the angle value along with the other attributes, rather than calling the base class method.

That completes creating sketch elements from XML nodes. The final piece is to handle the Import XML menu item event.

Handling Import XML Events

You can define the ImportXMLOperation() method in SketcherFrame like this:

image
  // Handle Import XML menu item events
  private void importXMLOperation() {
    checkForSave();
 
    // Now get the destination file path
    Path file = showDialog(
                     "Open XML Sketch File",            // Dialog window title
                     "Open",                            // Button label
                     "Read a sketch from an XML file",   // Button tooltip text
                     xmlFileFilter,                     // File filter
                     null);                             // No file selected
    if(file != null) {
      openXMLSketch(file);
    }
  }
 

Directory "Sketcher reading and writing XML"

Reading an XML file containing a sketch replaces the current sketch, so you call checkForSave() in case the current sketch has not been saved. If the showDialog() method returns a non-null Path object, you pass it to the openXMLFile() method that reads the sketch from the file at the specified path.

Reading the XML File

You read the XML file and create a Document object from it using a Transformer object.

image
  // Read an XML sketch from a file
  private void openXMLSketch(Path file) {
    try (BufferedInputStream xmlIn =
                         new BufferedInputStream(Files.newInputStream(file))){
      StreamSource source = new StreamSource(xmlIn);
      DocumentBuilderFactory builderFactory =
                                      DocumentBuilderFactory.newInstance();
      builderFactory.setNamespaceAware(true);
      builderFactory.setValidating(true);
      DocumentBuilder builder = builderFactory.newDocumentBuilder();
      builder.setErrorHandler(this);
      Document xmlDoc = builder.newDocument();
      DOMResult result = new DOMResult(xmlDoc);
 
     // Create a factory object for XML transformers
     TransformerFactory factory = TransformerFactory.newInstance();
     Transformer transformer = factory.newTransformer();  // Create transformer
     transformer.setErrorListener(this);
     transformer.transform(source, result);               // Read the XML file
     theApp.insertModel(createModelFromXML(xmlDoc));      // Create the sketch
 
     // Change file extension to .ske
     currentSketchFile = setFileExtension(file, ".ske");  
     setTitle(frameTitle+currentSketchFile);              // Update the window title
     sketchChanged = false;                               // Status is unchanged
     } catch(ParserConfigurationException e) {
       e.printStackTrace();
       System.exit(1);
     } catch(Exception e) {
     System.err.println(e);
     JOptionPane.showMessageDialog(this,
                                  "Error reading a sketch file.",
                                  "File Input Error",
                                  JOptionPane.ERROR_MESSAGE);
    }
  }
 

Directory "Sketcher reading and writing XML"

The DocumentBuilder object sets the SketcherFrame object to be the error handler, so the methods that you have already added to Sketcher report any parsing errors. The same applies to the Transformer object. The SketcherFrame object is the error listener, so the methods in SketcherFrame that implement ErrorListener are invoked if transformer errors occur. The createModel() method creates a SketcherModel object from the Document object that is created from the contents of the file. You just need to implement this in SketcherFrame to complete reading a sketch from an XML file.

Creating the Model

You know that a sketch in XML is a two-level structure. There is a root element, <sketch>, that contains one XML child element for each of the elements in the original sketch. Therefore, to re-create the sketch, you just need to extract the children of the root node in the Document object and then figure out what kind of sketch element each child represents. Whatever it is, you want to create a sketch element object of that type and add it to a model. You have already added a constructor to each of the classes that define sketch elements to create an object from an XML node. Here’s the code for the SketcherFrame method that uses them to create a sketch model:

image
  private SketcherModel createModelFromXML(Document xmlDoc) {
    SketcherModel model = new SketcherModel();
     NodeList nodes = xmlDoc.getDocumentElement().getChildNodes();
    // The child nodes should be elements representing sketch elements
    if(nodes.getLength() > 0) {                      // If there are some...
      Node elementNode = null;
      for(int i = 0 ; i<nodes.getLength() ; ++i){    // ...process them
        elementNode = nodes.item(i);
        switch(elementNode.getNodeName()) {
          case "line":
            model.add(new Element.Line(elementNode));
            break;
          case "rectangle":
            model.add(new Element.Rectangle(elementNode));
            break;
          case "circle":
            model.add(new Element.Circle(elementNode));
            break;
          case "curve":
            model.add(new Element.Curve(elementNode));
            break;
          case "text":
            model.add(new Element.Text(elementNode));
            break;
          default:
            System.err.println("Invalid XML node: " + elementNode);
            break;
        }
      }
    }
    return model;
  }
 

Directory "Sketcher reading and writing XML"

This method works in a straightforward fashion. You get the child nodes of the root node as a NodeList object by calling getChildNodes() for the object returned by getDocumentElement(). You determine what kind of element each child node is by checking its name in the switch statement. You call a sketch Element constructor corresponding to the node name to create the sketch element to be added to the model. Each of these constructors creates an object from the Node object reference that is passed as the argument. If the child node name does not correspond to any of the sketch elements, you output an error message.

That’s all the code you need. If everything compiles, you are ready to try exporting and importing sketches. If it doesn’t compile, chances are good there’s an import statement missing. The import statements that I have in SketcherFrame.java now are:

import javax.swing.*;
import javax.swing.border.*;
import java.awt.event.*;
import java.awt.*;
import java.nio.file.*;
import java.io.*;
import java.util.*;
import java.awt.image.BufferedImage;
import java.awt.font.TextLayout;
import java.awt.geom.Rectangle2D;
import javax.print.PrintService;
import javax.print.attribute.HashPrintRequestAttributeSet;
import java.awt.print.*;
import javax.xml.parsers.*;
import org.w3c.dom.*;
import javax.xml.transform.*;
import javax.xml.transform.stream.*;
import javax.xml.transform.dom.*;
import org.xml.sax.*;
import javax.xml.validation.*;
 
import static java.awt.event.InputEvent.*;
import static java.awt.AWTEvent.*;
import static java.awt.Color.*;
import static Constants.SketcherConstants.*;
import static javax.swing.Action.*;
 

I have the following set of imports in Element.java:

import java.awt.*;
import java.io.Serializable;
import static Constants.SketcherConstants.*;
import java.awt.geom.*;
import org.w3c.dom.Document;
import org.w3c.dom.Attr;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.NamedNodeMap;
 

Check your version of the code to make sure you have them all. It’s easy to miss one or two!

TRY IT OUT: Sketches in XML

You can try various combinations of elements to see how they look in XML. Make sure that the sketcher.dtd file is in the directory that you identified in the code. If you don’t, you aren’t able to import XML sketches because the DTD will not be found. Don’t forget you can look at the XML using any text editor and in most browsers. I created the sketch shown in Figure 23-1.

When I exported this sketch I got an XML file with the following contents:

<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sketch SYSTEM "D:/Beg Java Stuff/sketcher.dtd">
<sketch>
<line angle="0.0">
<color B="255" G="0" R="0"/>
<position x="147" y="67"/>
<bounds height="54" width="108" x="147" y="67"/>
<endpoint x="107.0" y="53.0"/>
</line>
<rectangle angle="0.0" height="72.0" width="110.0">
<color B="255" G="0" R="0"/>
<position x="339" y="73"/>
<bounds height="73" width="111" x="339" y="73"/>
</rectangle>
<circle angle="0.0" diameter="90.0">
<color B="255" G="0" R="0"/>
<position x="136" y="124"/>
<bounds height="91" width="91" x="136" y="124"/>
</circle>
<curve angle="0.0">
<color B="255" G="0" R="0"/>
<position x="310" y="202"/>
<bounds height="24" width="94" x="310" y="182"/>
<point x="1.0" y="-1.0"/>
<point x="2.0" y="-3.0"/>
                      <!-- points cut here for the sake of brevity -->
<point x="93.0" y="-12.0"/>
<point x="93.0" y="-13.0"/>
<point x="92.0" y="-13.0"/>
</curve>
<text angle="0.0" maxascent="21">
<color B="255" G="0" R="0"/>
<position x="201" y="259"/>
<bounds height="30" width="163" x="201" y="259"/>
<font fontname="Serif" fontstyle="bold" pointsize="20"/>
<string>The Complete Set!</string>
</text>
</sketch>
 

This file is also available as Sample Sketch.xml in the code download for this book from the Wrox Press website, www.wrox.com. You could try importing it into Sketcher and see if you get the same sketch.

SUMMARY

In this chapter I discussed how you can use a DOM parser to analyze XML and how JAXP supports the synthesis and modification of XML documents using DOM. You have also seen how you can use a XSLT Transformer object to create an XML file from a Document object and vice versa.

If you managed to get to the end of this chapter having built your own working version of Sketcher, you are to be congratulated, especially if you started out as a newcomer to programming. I’m sure I don’t need to tell you that it’s quite a challenge to acquire the knowledge and understanding necessary to do this.

An important point I want to emphasize is that the Sketcher program is just a demonstration and test vehicle. It is an environment in which you have been able to try out various Java programming techniques and work with reasonably large chunks of code, but it is not a good example of how the application should be built. Because its development has been Topsy-like, without much regard for where it would finally end up, Sketcher contains many inconsistencies and inefficiencies that would not be there if the application had been designed from the ground up.

At this point you have a good knowledge of the Java language and experience with some of the basic facilities provided by the Java class libraries. I have only been able to introduce you to a small proportion of the total available in the support libraries. There are vast tracts of capability still to explore. There are packages that provide a sound API, support for cryptography, drag-and-drop capability, networking application support, and many more. It is well worth browsing the Java documentation.

Enjoy your Java programming!

EXERCISES

You can download the source code for the examples in the book and the solutions to the following exercises from www.wrox.com.

1. Write a program using DOM that counts the number of occurrences of each element type in an XML document and displays them. The document file should be identified by the first command-line argument. The program should also accept optional, additional command-line arguments that are the names of elements. When there are two or more command-line arguments, the program should count and report only on the elements identified by the second and subsequent command-line arguments.

2. Implement the XML Import capability in Sketcher using SAX, rather than DOM.

image

• WHAT YOU LEARNED IN THIS CHAPTER

TOPIC CONCEPT
DOM Parsers An object of type DocumentBuilder encapsulates a DOM parser.
Creating a DOM Parser You create an object encapsulating a DOM parser by using a DocumentBuilderFactory object that you obtain by calling the static newInstance() method that is defined in the DocumentFactoryBuilder class.
Parsing XML You can parse an XML document by passing the document as an argument to the parse() method for a DocumentBuilder object.
Document Objects A DOM parser creates a Document object that encapsulates an entire XML document as a tree of Node objects.
Analyzing XML Documents The DOM API includes the Document class methods that enable you to analyze an XML document by navigating through the nodes in a Document object.
Creating XML Documents The DOM API includes Document class methods for creating an XML document programmatically encapsulated by a Document object.
Creating XML with a DTD When you want to create a new XML document that includes a DTD, you use the createDocument() method for a DOMImplementation object, rather than the newDocument() method for a DocumentBuilder object.
Creating a DOCTYPE Element You can create a DOCTYPE element by calling the createDocumentType() method for a DOMImplementation object. You can then pass the DocumentType object to the createDocument() method for the DOMImplementation object to create a Document object that include the DOCTYPE element..
XSLT A Transformer object encapsulates an XSLT transform. You create a Transformer object by first creating a TransformerFactory object and calling its newTransformer() method. The argument to the method is a Source object encapsulating the transform. The version with no argument creates a transformer that is the identity transform that does nothing.
Reading and Writing XML Files You can use a Transformer object that encapsulates the identity transform to write a Document object as an XML file, or to read an XML file and create a Document object from it.
image
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.97.235