11 December, 2010

StAX - Streaming API for XML

StAX, is a Java processing XML API which allow software to processing XML stream in a push streaming style. In certain scenario, StAX will be the most efficient approach for prcoessing XML stream. This article aims to provide an overview on this API as well as comparison with two other commonly used APIs: DOM and SAX.

Background

StAX, is defined by JSR 173. The API is initially provided by some industry vendor including Oracle, and BEA and finally becomes a JSR. Starting from JDK 6, it is already included in JDK as a part of the standard Java XML processing API (JAXP).

With the advent of all kind of XML API, high level (and developer friendly) tools like JAXB and XStream can be very handy for XML writing / reading. In certain situation, however, it would be desirable to implement the processing logic with lower level XML processing API. For example, consider implementing a XML message router, there is no need to process the whole message in order to perform the routing. Unmarshalling a XML message with JAXB or XStream, or even DOM will not be an efficient approach. In contrary, the best handling may be only process the header, decide the routing and dispatch the message without processing the whole XML message.

Push Streaming XML Processing

So StAX is a "Push" and "Streaming" API for XML processing. What does it actually means. Example below can help to illustrate these two idea. Let's say there is simple XML message looks like this:
  <words a="123">
    <word/>
    <word>Apple&amp;Orange</word>
    <!-- comment -->
  </words>
  
And below is a simple code snipplet demostrate how StAX API can be used:
  XMLEventReader reader = XMLInputFactory.newFactory().createXMLEventReader(inputStream);
  while (reader.hasNext()) {
   XMLEvent e = reader.nextEvent();
   String eventType = "N/A";
   switch (e.getEventType()) {
    case XMLEvent.START_DOCUMENT:
     eventType = "START_DOCUMENT";
     break;
    case XMLEvent.END_DOCUMENT:
     eventType = "END_DOCUMENT";
     break;
    case XMLEvent.START_ELEMENT:
     eventType = "START_ELEMENT";
     break;
    case XMLEvent.END_ELEMENT:
     eventType = "END_ELEMENT";
     break;
    case XMLEvent.CHARACTERS:
     eventType = "CHARACTERS";
     break;
    case XMLEvent.COMMENT:
     eventType = "COMMENT";
     break;
   }
   System.out.println(eventType + ":" + e.toString());
  }
  
Executing the sniplet above will generate be following output below:
  START_DOCUMENT:<?xml version="1.0" encoding='null' standalone='no'?>
  START_ELEMENT:<words a='123'>
  CHARACTERS:
    
  START_ELEMENT:<word>
  END_ELEMENT:</word>
  CHARACTERS:
    
  START_ELEMENT:<word>
  CHARACTERS:Apple
  CHARACTERS:&
  CHARACTERS:Orange
  END_ELEMENT:</word>
  CHARACTERS:
    
  COMMENT:<!-- comment -->
  CHARACTERS:

  END_ELEMENT:</words>
  END_DOCUMENT:ENDDOCUMENT

  

In this example, XmlEventReader, one of primary interface in StAX is used. XmlEventReader provides the nextNext() and next() method. XMLEventReader can be viewed as a reader, which will parse the XML stream. XMLEventReader will parse and returnn an XMLEventReader upon each next() call. The API is similar to the Java collections API. However, in StAX, what is being iterate is not a collection of items, but a series of XML processing Event.

XMLEvent in StAX refers to the various event when a parser is paring a XML stream. When the XML parser start parsing an XML document, it will return an Event to represent the start of document (START_DOCUMENT). When it proceed parsing, it will encounter the start of an XML element (START_ELEMENT), that is the <words> node. Similarlly, when the parser encounters some TEXT data (including whitespace and new line between two XML tag), it will report it as a Character event. When it recognize an end of tag (like </words> it will report it as an end of element event (END_ELEMENT). And finally, when it found that the parsing is finished, it will report an End of Document event (END_DOCUMENT).

In other words, in StAX, application will handle the stream of XML event. In additions, with StAX, application will proactively control the parsing. XMLEventReader will not do the parsing unless the "next()" is called. Everything "next()" is called, the XML parsing continue and stop when XML event is created and the event will then returned to the application. Application using StAX can decide whether it should read up the whole document, or pause after traversing the beginging portion of the XML stream.

XMLStreamReader and XMLEventReader

In StAX, two interface is provided: XMLStreamReader and XMLEventReader. The conceptual use of this two are basically the same. Software will use the "hasNext()" and "next()" to control the parsing. The main key difference is, with XMLEventReader each next() will return an event which encapsulate the XML parsing event. With XMLStreamReader, next() call will return the event type. To access the details (like the XML element name and attribute contents), we need to use various accessor methods on the XMLStreamReader to retrieve the information. The sample code below will try to parse an XML message and return a Message object to represent the message. The code below will demostrate how XMLEventReader and XMLStreamReader can be used to implements MessageReader interface. Consider the XML message below:
  <?xml version="1.0" encoding="UTF-8"?>
  <mf:message xmlns:mf="http://www.nixstyle.net/lab/xml/message-router">
    <mf:header>
      <mf:source>z33220011z&amp;gmail.com</mf:source>
      <mf:destination>a77662233a&amp;gmail.com</mf:destination>
    </mf:header>
    <mf:content>
      <xyz:message xmlns:xyz="http://www.nixstyle.net/lab/xml/message-sample">Yo!</xyz:message>
    </mf:content>
  </mf:message>
  
Below is a MessageReader interface
  public interface MessageReader {
    Message next() throws XMLStreamException;
  }
  
Below is an implementation of MessageReader using XMLEventReader
    public Message next() throws XMLStreamException {
      XMLEvent event = reader.nextTag();
      expectStartOfElement(event, "message");
      return parseMessage();
    }

    private Message parseMessage() throws XMLStreamException {
      Message message = new Message();
      
      XMLEvent event = reader.nextTag();
      parseMessageHeader(event, message);
      
      event = reader.nextTag();
      parseMessageContent(event, message);
      
      return message;
    }

    private void parseMessageContent(XMLEvent event, Message message) throws XMLStreamException {
      expectStartOfElement(event, "content");
      while (!isEndOfElement(event, "content")) {
        event = reader.nextEvent();
      }
    }

    private void parseMessageHeader(XMLEvent event, Message message) throws XMLStreamException {

      String source = null;
      String destination = null;

      expectStartOfElement(event, "header");

      while (source == null || destination == null) {

        XMLEvent nextTag = reader.nextTag();
        if (nextTag.isStartElement()) {

          if ("source".equals(nextTag.asStartElement().getName().getLocalPart())) {
            source = reader.getElementText();
          }

          if ("destination".equals(nextTag.asStartElement().getName().getLocalPart())) {
            destination = reader.getElementText();
          }
        }

      }

      message.setSource(source);
      message.setDestination(destination);

      XMLEvent next = reader.nextTag();
      moveToNextEndElement(next, "header");

    }

    private void moveToNextEndElement(XMLEvent event, String elementName) throws XMLStreamException {
      while (!isEndOfElement(event, elementName)) {
        if (reader.hasNext()) {
          event = reader.nextTag();
        } else {
          throw new RuntimeException("Unexpected stream");    
        }
      }
    }
    
    private void expectStartOfElement(XMLEvent event, String elementName) throws XMLStreamException {
      if (!isStartOfElement(event, elementName)) {
        throw new RuntimeException("Unexpected stream");
      }
    }
    
    private boolean isStartOfElement(XMLEvent event, String elementName) throws XMLStreamException {
      if (event.isStartElement()) {
        return event.asStartElement().getName().getLocalPart().equals(elementName);
      }
      return false;
    }

    private boolean isEndOfElement(XMLEvent event, String elementName) throws XMLStreamException {
      if (event.isEndElement()) {
        return event.asEndElement().getName().getLocalPart().equals(elementName);
      }
      return false;
    }
  
Below is an implementation of MessageReader using XMLStreamReader
    public Message next() throws XMLStreamException {
      Message message = null;
      while (reader.hasNext()) {
        int eventCode = reader.next();
        if (eventCode == XMLStreamConstants.START_ELEMENT) {
          String elementName = reader.getLocalName();
          if ("message".equals(elementName)) {
            message = parseMessage();
          }
        }
      }
      return message;
    }

    private Message parseMessage() throws XMLStreamException {
      Message message = new Message();
      int nextTag = reader.nextTag();
      if (nextTag == XMLStreamConstants.START_ELEMENT) {
        if ("header".equals(reader.getLocalName()) == false) {
          throw new RuntimeException("Unexpected stream");
        }
        parseMessageHeader(message);
      }
      
      nextTag = reader.nextTag();
      if (nextTag == XMLStreamConstants.START_ELEMENT) {
        if ("content".equals(reader.getLocalName()) == false) {
          throw new RuntimeException("Unexpected stream");
        }
        parseMessageContent(message);
      }
      return message;
    }

    private void parseMessageContent(Message message) throws XMLStreamException {
      if (reader.getLocalName().equals("content") == false) {
        throw new RuntimeException("Unexpected stream");
      }

      while (!isEndOfElement("content")) {
        reader.next();
      }
    }

    private void parseMessageHeader(Message message) throws XMLStreamException {

      String source = null;
      String destination = null;

      expectStartOfElement("header");

      while (source == null || destination == null) {

        int nextTag = reader.nextTag();

        if (nextTag == XMLStreamConstants.START_ELEMENT) {

          if ("source".equals(reader.getLocalName())) {
            source = extractText();
            moveToNextEndElement("source");
          }

          if ("destination".equals(reader.getLocalName())) {
            destination = extractText();
            moveToNextEndElement("destination");
          }

        }
      }

      if (reader.nextTag() != XMLStreamConstants.END_ELEMENT) {
        throw new RuntimeException("Unexpected stream");
      }

      message.setSource(source);
      message.setDestination(destination);

      moveToNextEndElement("header");

    }


    private String extractText() throws XMLStreamException {
      while (!isStartOfText()) {
        reader.next();
      }
      return reader.getText();
    }

    private void moveToNextEndElement(String elementName) throws XMLStreamException {
      while (!isEndOfElement(elementName)) {
        if (reader.hasNext()) {
          reader.next();
        } else {
          throw new RuntimeException("Unexpected stream");    
        }
      }
    }

    private void expectStartOfElement(String elementName) {
      if (!isStartOfElement(elementName)) {
        throw new RuntimeException("Unexpected stream");
      }
    }

    private boolean isStartOfElement(String elementName) {
      if (reader.getEventType() == XMLStreamConstants.START_ELEMENT) {
        return reader.getLocalName().equals(elementName);
      }
      return false;
    }

    private boolean isEndOfElement(String elementName) {
      if (reader.getEventType() == XMLStreamConstants.END_ELEMENT) {
        return reader.getLocalName().equals(elementName);
      }
      return false;
    }

    private boolean isStartOfText() {
      switch (reader.getEventType()) {
        case XMLStreamConstants.CDATA:
        case XMLStreamConstants.CHARACTERS:
          return true;
      }
      return false;
    }
  

In the sniplets shown above, XMLEventReader and XMLStreamReader is both used. The key difference is, XMLEventReader return an XMLEvent which provide a little bit Object oriented interface for accessing the XML event details. With XMLStreamReader, the accessor method like getText() and getLocalName() is only available on the XMLStreamReader interface. As a result, XMLEventReader is usually the preferred approach.

Compare with SAX and DOM

When comparing StAX with SAX, the most notable difference will be the control of the parsing. With SAX, the application can only provide a callback to handle the event. Once the parsing is started, parser will continue parsing the whole stream. The application will have no control on the parsing, for example, terminte the parsing after processing a message header.

When compare with DOM, the difference is more significant. When processing XML using DOM, the object model representing the whole XML structure will be constructed. When the XML message size is huge, it may be inefficient for building the whole DOM tree. Especially, most of the time, application will convert an XML into some kind of POJO. In contrary, using streaming API like StAX can eliminate the needs to building the DOM tree in memory.

On the other hand, if all the application need is performing some analysis on the XML stream, only building a DOM and manipulate with DOM tree will make more sense.

No comments: