Thursday, 5 January 2012

Approaches to XML - Part 2 - What about SAX?

My last blog introduced the idea that there are different ways to approach XML parsing and highlighted the point that XML is NOT A STRING; rather it’s an object oriented document model that can be represented using a string. Today’s blog continues this discussion using my Pete’s Perfect Pizza scenario. If you remember, Pete’s just popped his head around the door and asked you to enhance the system so that the front desk can send orders for multiple pizzas in a single XML message. You know that your simple string parsing code is flawed and won’t cut the mustard, so you Google1 more on XML and come up with the idea of using SAX.


SAX, or Simple API for XML has been around for many years and, as far as I can recollect, was originally a development lead by David Megginson before the turn of the millennium. In those days, you had to download the Java version of SAX from David’s personal web site. This developed into the SAX Project before finally being added to Java Standard Edition 1.4.

SAX is a streaming interface for XML, which means that applications using SAX receive event notifications about the XML document being processed an element, and attribute, at a time in sequential order starting at the top of the document, and ending with the closing of the ROOT element. This means that it’s extremely efficient at processing XML in linear time without placing too many demands upon system memory.

Back to Pete’s, you work hard and come up with the following SAX parser based class:

public class PizzaParser {

 
public List<PizzaOrder> order(InputStream xml) {

   
PizzaContentHandler handler = new PizzaContentHandler();

   
// do the parsing
   
try {
     
// Construct the parser by bolting together an XMLReader
      // and the ContentHandler
     
XMLReader parser = XMLReaderFactory.createXMLReader();
      parser.setContentHandler
(handler);

     
// create an input source from the XML input stream
     
InputSource source = new InputSource(xml);
     
// Do the actual work
     
parser.parse(source);

     
return handler.getPizzaOrder();
   
} catch (Exception ex) {
     
throw new RuntimeException("Exception parsing xml message. Message: " + ex.getMessage(), ex);
   
}
  }

 
static class PizzaOrder {

   
private final String pizzaName;
   
private final String base;
   
private final String quantity;

    PizzaOrder
(String pizzaName, String base, String quantity) {
     
this.pizzaName = pizzaName;
     
this.base = base;
     
this.quantity = quantity;
   
}

   
public String getPizzaName() {
     
return pizzaName;
   
}

   
public String getBase() {
     
return base;
   
}

   
public String getQuantity() {
     
return quantity;
   
}
  }

 
/**
   * Use this class the handle the SAX events
   */
 
class PizzaContentHandler extends DefaultHandler {

   
private String[] pizzaInfo;
   
private int index;
   
private List<PizzaOrder> outList;
   
private boolean capture;

   
/**
     * Set things up at the start of the document.
     */
   
@Override
   
public void startDocument() {
     
outList = new ArrayList<PizzaOrder>();
   
}

   
/**
     * Handle the startElement event
     */
   
@Override
   
public void startElement(String uri, String localName, String qName, Attributes attributes) {

     
capture = true;
     
if ("pizzas".equals(qName)) {
       
capture = false;
     
} else if ("pizza".equals(qName)) {
       
pizzaInfo = new String[3];
        capture =
false;
     
} else if ("name".equals(qName)) {
       
index = 0;
     
} else if ("base".equals(qName)) {
       
index = 1;
     
} else if ("quantity".equals(qName)) {
       
index = 2;
     
}
    }

   
/**
     * Handle the endElement event
     */
   
@Override
   
public void endElement(String uri, String localName, String qName) {

     
if ("pizza".equals(qName)) {
       
outList.add(new PizzaOrder(pizzaInfo[0], pizzaInfo[1], pizzaInfo[2]));
     
}
    }

   
/**
     * Grab hold of incoming character data
     */
   
@Override
   
public void characters(char[] ch, int start, int length) {

     
if (capture) {
       
pizzaInfo[index] = new String(ch, start, length);
        capture =
false;
     
}
    }

   
List<PizzaOrder> getPizzaOrder() {
     
return outList;
   
}
  }

}

This blog isn’t here to demonstrate how to use SAX, there are lots of the examples available if you look around, but lets take a critical look at the code and the first thing to notice is that the order(...) method now takes an input stream rather than a string as befitting a stream based API:

  public List<PizzaOrder> order(InputStream xml)

The next thing to note is that the PizzaParser uses a nested class, PizzaContentHandler that extends the SAX helper class DefaultHandler. The PizzaContentHandler class captures a list of PizzaOrder beans and passes them back to the enclosing class for return to the caller. This means that all you need to do to get hold of the SAX events is to override handler methods such as startElement(...), endElement(...) etc.

If you take a closer look a the code, you’ll realise that it’s pretty complex. All it has to do is to create an output list, yet there are multiple if() statements, temporary arrays and boolean switches that are used to grab hold of the right bit of information from the right point in the document. This is downside to SAX: it’s complexity places more of burden on the programmer and makes your code more error prone.

It is, however, more resilient than the previous string based attempt as the unit tests below demonstrate:

public class PizzaParserTest {

 
private static final String ORDER_XML = //
 
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + //
     
"<pizza>\n" + // 8
     
"    <name>Capricciosa</name>\n" + //
     
"    <base>thin</base>\n" + //
     
"    <quantity>2</quantity>\n" + //
     
"</pizza>\n";

 
private static final String ORDER_XML_2 = //
 
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><pizza><name>Capricciosa</name><base>thin</base><quantity>2</quantity></pizza>";

 
private static final String ORDER_XML_3 = //
 
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + //
     
"<pizzas>\n" + //
     
"    <pizza>\n" + //
     
"        <name>Capricciosa</name>\n" + //
     
"        <base>thin</base>\n" + //
     
"        <quantity>2</quantity>\n" + //
     
"    </pizza>\n" + //
     
"    <pizza>\n" + //
     
"        <name>Margherita</name>\n" + //
     
"        <base>thin</base>\n" + //
     
"        <quantity>1</quantity>\n" + //
     
"    </pizza>\n" + //
     
"</pizzas>";

 
private PizzaParser instance;

 
@Before
 
public void setUp() {
   
instance = new PizzaParser();
 
}

 
@Test
 
public void readOrderFromXML() {

   
List<PizzaOrder> results = instance.order(new ByteArrayInputStream(ORDER_XML.getBytes()));

    assertEquals
(1, results.size());

    PizzaOrder result = results.get
(0);
    assertEquals
("Capricciosa", result.getPizzaName());
    assertEquals
("thin", result.getBase());
    assertEquals
("2", result.getQuantity());
 
}

 
@Test
 
public void readOrderFromModifiedXML() {

   
List<PizzaOrder> results = instance.order(new ByteArrayInputStream(ORDER_XML_2.getBytes()));

    assertEquals
(1, results.size());

    PizzaOrder result = results.get
(0);
    assertEquals
("Capricciosa", result.getPizzaName());
    assertEquals
("thin", result.getBase());
    assertEquals
("2", result.getQuantity());
 
}

 
@Test
 
public void readOrderForMultiplePizza() {

   
List<PizzaOrder> results = instance.order(new ByteArrayInputStream(ORDER_XML_3.getBytes()));

    PizzaOrder result = results.get
(0);
    assertEquals
("Capricciosa", result.getPizzaName());
    assertEquals
("thin", result.getBase());
    assertEquals
("2", result.getQuantity());

    result = results.get
(1);
    assertEquals
("Margherita", result.getPizzaName());
    assertEquals
("thin", result.getBase());
    assertEquals
("1", result.getQuantity());
 
}
}

These tests demonstrate the scenarios of processing XML messages with and without white-space (fixing yesterday’s problem) together with a message that includes an order for multiple pizzas.

It’s all working really well, but Pete’s big ideas are coming fruition. He’s now expanding into a world wide concern with multiple kitchens around the world and an online presence. Pete hires some rinky-dinky business consultants who create a new pizza order XML schema and combine it with their existing customer schema. This is dropped into your email inbox and you wonder what to do next...

1Other search engines are available.

Other blogs in this series...
  1. XML is not a String
  2. What about Sax
  3. JAXB
  4. XMLBeans

The source code is available from GitHub at:

git://github.com/roghughe/captaindebug.git

No comments: