Pliant AllianceSoftware development notes & tools

Reading and Writing XML in Java with DOM and SAX

XML often arrives as a contract between systems: a supplier sends an order, a booking service publishes timetables, and another program updates records. Java developers need to turn those characters into useful objects, then send valid XML back. The two classic approaches make different promises. DOM builds a navigable tree, while SAX reports a sequence of parsing events as the document arrives.

Neither model is automatically superior. DOM suits bounded documents that application code will inspect or alter; SAX suits large feeds that can be handled in one pass. The practical choice depends on document size, latency, validation requirements, and whether the code needs to revisit earlier data. Good Java code also treats the parser as a security boundary because an XML file can contain more than ordinary business elements.

Choosing between a tree and an event stream

A DOM parser reads the complete document and constructs a tree whose nodes represent elements, attributes, text, and comments. You can move between a customer and its address, insert an element, or pass the tree to an XSLT transformer. That flexibility comes at a cost: a large catalogue held in memory can become expensive, and the parser normally completes before application code sees the first record.

A SAX parser does not retain that tree. It calls handlers as it encounters start tags, character data and end tags, so a multi-megabyte feed can be processed with a small memory footprint. This suits an order queue or log arriving from an Australian courier, but it is awkward when later code needs the third matching item or a parent and all of its children. Choose DOM for random access and editing; choose SAX for ordered, one-pass processing.

Preparing a practical XML document

A small purchase order makes a useful test document. Australian teams commonly exchange supplier details between Windows workstations and Linux servers, and namespaces prevent a field called id in one system from colliding with another. The sample uses a NSW suburb, an Australian dollar total, GST status and an explicit +10:00 time offset, reflecting local data without hard-coding every downstream system to Australian conventions.

<?xml version="1.0" encoding="UTF-8"?>
<orders xmlns="urn:example:orders"
        generated="2006-03-18T09:30:00+10:00">
  <order id="AU-1042">
    <customer>Melbourne Office Supply Pty Ltd</customer>
    <ship-to>
      <suburb>Newtown</suburb>
      <state>NSW</state>
    </ship-to>
    <item code="PEN-BLUE" quantity="10"/>
    <total currency="AUD" includes-gst="true">48.50</total>
  </order>
</orders>

The XML declaration fixes the byte encoding, while the default namespace identifies the vocabulary. A document can be well formed without a schema, but a schema adds enforceable rules for order identifiers, quantities and totals. Keep the schema beside the Java application and version both together when an exchange format changes.

Reading XML with the DOM

javax.xml.parsers.DocumentBuilderFactory is the entry point for tree-based parsing. Namespace awareness should be enabled before parsing, and a parser receiving files from outside the organisation should reject document type declarations. The latter blocks the simplest external-entity attacks and prevents entity expansion from becoming an unexpected expense.

DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
factory.setFeature(
    "http://apache.org/xml/features/disallow-doctype-decl", true);

Document document = factory.newDocumentBuilder()
    .parse(new File("orders.xml"));

NodeList orders = document.getElementsByTagNameNS(
    "urn:example:orders", "order");

for (int i = 0; i < orders.getLength(); i++) {
    Element order = (Element) orders.item(i);
    String id = order.getAttribute("id");
    Element customer = (Element) order.getElementsByTagNameNS(
        "urn:example:orders", "customer").item(0);

    System.out.println(id + " belongs to " + customer.getTextContent());
}

Once loaded, each element is reachable through its parent, children and attributes. The namespace-aware lookup avoids confusing the local name id with similarly named elements from another vocabulary, which matters when an order is combined with billing or inventory data. DOM also supports XPath queries and direct edits: create an element with createElementNS, append it and serialise the altered tree. A production program should attach an ErrorHandler rather than silently accepting malformed input.

Reading XML with SAX

SAXParserFactory creates a streaming reader whose callbacks arrive in document order. Unlike DOM, it never presents the complete purchase order as one object. This is valuable when a Brisbane service imports a continuous supplier feed, or when a program must acknowledge each item without waiting for a closing tag on a large document.

SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true);
factory.setFeature(
    "http://apache.org/xml/features/disallow-doctype-decl", true);

XMLReader reader = factory.newSAXParser().getXMLReader();

reader.setContentHandler(new DefaultHandler() {
    private String orderId;
    private boolean inCustomer;
    private final StringBuilder text = new StringBuilder();

    public void startElement(String uri, String localName,
                             String qName, Attributes attributes) {
        if ("urn:example:orders".equals(uri)
                && "order".equals(localName)) {
            orderId = attributes.getValue("id");
        }
        if ("urn:example:orders".equals(uri)
                && "customer".equals(localName)) {
            inCustomer = true;
            text.setLength(0);
        }
    }

    public void characters(char[] ch, int start, int length) {
        if (inCustomer) {
            text.append(ch, start, length);
        }
    }

    public void endElement(String uri, String localName, String qName) {
        if (inCustomer) {
            System.out.println(orderId + ": " + text);
            inCustomer = false;
        }
        if ("order".equals(localName)) {
            orderId = null;
        }
    }
});

reader.parse(new File("orders.xml"));

The handler stores only the current order identifier and customer text. characters can be called several times for one text node, so a robust handler accumulates into a buffer and interprets the value in endElement. Namespace-aware SAX supplies the URI and local name; qName may be empty and should not be the primary identity. An ErrorHandler can reject, continue or log parsing and validation problems with a line and column.

The trade-off is visible in a live import. A SAX handler can calculate a running total or write each accepted order to a database immediately without retaining earlier records. It cannot later jump back to the first order unless the application saved what it needed. For repeated navigation, transformation or validation against related documents, DOM or a hybrid cache is usually clearer.

Writing XML with the DOM

Writing with DOM begins by creating a Document, adding namespaced elements and attributes, and then handing the completed tree to a JAXP Transformer. The transformer controls the XML declaration, character escaping and indentation.

DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
Document document = factory.newDocumentBuilder().newDocument();

Element orders = document.createElementNS(
    "urn:example:orders", "orders");
document.appendChild(orders);

Element order = document.createElementNS(
    "urn:example:orders", "order");
order.setAttribute("id", "AU-1042");
orders.appendChild(order);

Element customer = document.createElementNS(
    "urn:example:orders", "customer");
customer.appendChild(
    document.createTextNode("Melbourne Office Supply Pty Ltd"));
order.appendChild(customer);

Transformer transformer =
    TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.transform(
    new DOMSource(document),
    new StreamResult(new File("orders.xml")));

createElementNS keeps the customer in the same vocabulary as its order and avoids accidental collisions with other XML namespaces. appendChild establishes the tree structure, while text nodes carry values that the transformer will escape correctly. This is safer than concatenating strings, where a customer name containing an ampersand could produce invalid XML.

A transformer should perform the final serialisation rather than hand-built string concatenation. When the source document contains extension elements that must survive a round trip, parse it into a DOM and transform or edit that tree; rebuilding only selected fields can silently discard data another participant depends on.

Writing XML through SAX-style events

SAX is primarily a parsing interface, and the JDK has no direct equivalent of a push-style XMLWriter. Fresh documents are often generated more simply with Java 5’s StAX XMLStreamWriter, while existing XML can be copied or transformed through a SAXSource. Another standard option is to send SAX events to a TransformerHandler. That handler accepts startElement, characters and endElement callbacks and serialises them, which is useful when application logic already produces events.

OutputStream output = new FileOutputStream("orders.xml");
TransformerHandler events =
    TransformerFactory.newInstance().newTransformerHandler();
events.getTransformer().setOutputStream(output);
events.getTransformer().setOutputProperty(OutputKeys.INDENT, "yes");

AttributesImpl orderAttributes = new AttributesImpl();
orderAttributes.addAttribute("", "id", "", "CDATA", "AU-1042");

events.startDocument();
events.startPrefixMapping("", "urn:example:orders");
events.startElement("urn:example:orders", "orders", "orders");
events.startElement("urn:example:orders", "order", "order",
    orderAttributes);
events.startElement("urn:example:orders", "customer", "customer");
events.characters(
    "Melbourne Office Supply Pty Ltd".toCharArray(), 0, 33);
events.endElement("urn:example:orders", "customer", "customer");
events.endElement("urn:example:orders", "order", "order");
events.endElement("urn:example:orders", "orders", "orders");
events.endPrefixMapping("");
events.endDocument();
output.close();

The direct calls follow the SAX event contract, while the transformer performs the actual serialisation. Namespace declarations, attributes and character data remain separate events, and the output receives the usual XML escaping and declaration. A SAXSource combined with StreamResult provides the same division of labour when an existing document is being transformed rather than created.

StAX offers a useful middle path for newly generated XML. Its XMLStreamWriter is a pull-style API that writes elements and attributes directly, while StAX readers consume the same stream format without building a tree. DOM and SAX remained attractive because their APIs were already established. The selection follows the shape of the work: DOM for mutation, SAX for callbacks and StAX when producing and consuming streams in a modern pipeline.

Validation, namespaces and operational choices

Both approaches need a namespace-aware parser, an error handler and explicit decisions about trust. For document type validation, DOM can receive a Schema through setSchema; SAX can use a schema-aware SAXParser and report failures through its ValidationErrorHandler. DTD validation remains available, but XML Schema is usually easier to share with an Australian trading partner. External entities should stay disabled for uploaded documents, and XML should be treated as UTF-8 unless its declaration states otherwise.

Australian deployments add operational and legal context. Where the Privacy Act 1988 (Cth) applies, customer names, delivery addresses and order histories can be personal information. A service should collect only necessary fields, protect raw XML as it would protect a database, and apply access controls and retention rules. The Electronic Transactions Act 1999 (Cth) recognises electronic transactions and signatures, but a tag named signature does not create one; genuine cryptographic signing and key protection are separate technical requirements and warrant legal review for a particular implementation.

For Australian software vendors, the JDK’s bundled DOM and SAX implementations are often attractive to small businesses because they avoid another runtime licence and run consistently across Windows and Linux servers. The dependable operating pattern is DOM for bounded documents that will be edited, SAX for large one-pass feeds, schema checks at every trust boundary and a clear decision about which Java XML API owns each stage of processing.