Tuesday, October 20, 2009

Comparing XML payloads during testing (Java)

Update: This solution assumes both payloads have same (deep) order of attributes and elements.

Writing tests often requires XML payloads to be compared for equality. As such, XML payloads cannot be compared using String.equals() method due to possible differences in canonical representations of similar payloads.

For an example, these are equal XML payloads but not equal strings:


<person>
<name>Neil</name>
<age>31</age>
</person>

<person><name>Neil</name><age>31</age></person>


In this post I am listing a solution I wrote some time ago to compare XML payloads.

import java.io.IOException;
import java.io.StringReader;

import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;

public class XMLCompare extends DefaultHandler {

private StringBuilder accumulator = new StringBuilder();

public static boolean equal(String xml1, String xml2)
throws SAXException, IOException {
return new XMLCompare().compareEqual(xml1, xml2);
}

private boolean compareEqual(String actualXML, String expectedXML)
throws IOException {

XMLReader xr = null;
try {
xr = XMLReaderFactory.createXMLReader();
} catch (SAXException e) {
throw new IllegalStateException("Unable to get XMLReader object");
}
xr.setContentHandler(this);
xr.setErrorHandler(this);

accumulator = new StringBuilder("");
try {
xr.parse(new InputSource(new StringReader(actualXML)));
} catch (SAXException e) {
throw new IllegalStateException(getInvalidXMLExceptionMessage(
"ActualXML", actualXML));
}
String xmlString1 = accumulator.toString();
accumulator = new StringBuilder("");
try {
xr.parse(new InputSource(new StringReader(expectedXML)));
} catch (SAXException e) {
throw new IllegalStateException(getInvalidXMLExceptionMessage(
"ExpectedXML", expectedXML));
}
String xmlString2 = accumulator.toString();
return xmlString1.equals(xmlString2);
}

private String getInvalidXMLExceptionMessage(String prefix, String xml) {
return "" + prefix + " is not valid XML: **" + xml + "**";
}

/* --- Event handlers --- */

@Override
public void startDocument () { }

@Override
public void endDocument () { }

// Every time the parser encounters the beginning of a new element, it
// calls this method, which resets the string buffer
@Override
public void startElement (String uri, String name,
String qName, Attributes atts) {
appendStartElement(accumulator, uri, name, qName, atts);
}

private static void appendStartElement(StringBuilder string,
String uri, String name, String qName,
Attributes attributes) {
if ("".equals(uri)) {
string.append("<" + qName);
} else {
string.append("<{" + uri + "}" + name);
}
for (int i=0; i < attributes.getLength(); i++) {
string.append(" " + attributes.getLocalName(i) + "=\"" +
attributes.getValue(i) + "\"");
}
string.append(">");
}

// When the parser encounters the end of an element, it calls this method
@Override
public void endElement (String uri, String name, String qName) {
accumulator.append("</");
if ("".equals (uri)) {
accumulator.append(qName);
} else {
accumulator.append("{" + uri + "}" + name);
}
accumulator.append(">");
}

// When the parser encounters plain text (not XML elements), it calls
// this method, which accumulates them in a string buffer
@Override
public void characters (char ch[], int start, int length) {
for (int i = start; i < start + length; i++) {
if (!Character.isWhitespace(ch[i])) {
accumulator.append(ch[i]);
}
}
}

/** This method is called when warnings occur */
@Override
public void warning(SAXParseException exception) throws SAXException {
System.err.println("WARNING: line " + exception.getLineNumber() + ": "+
exception.getMessage());
throw(exception);
}

/** This method is called when errors occur */
@Override
public void error(SAXParseException exception) throws SAXException {
System.err.println("ERROR: line " + exception.getLineNumber() + ": " +
exception.getMessage());
throw(exception);
}

/** This method is called when non-recoverable errors occur. */
@Override
public void fatalError(SAXParseException exception) throws SAXException {
System.err.println("FATAL: line " + exception.getLineNumber() + ": " +
exception.getMessage());
throw(exception);
}

}



Thanks for reading through here -- your feedback is most welcome.

2 comments:

  1. So "parse XML document and build a string and then compare consistently formatted strings"? What about when you've got an XSD with an "any" tag in it where ordering is unimportant? Documents using that schema should still be equal if the elements are the same but in different orders, but won't necessarily be if you flatten them to the same formatted strings.

    Also, a little less junk content (like the commented out lines) might help readability of your example. The switch statement doesn't appear to do anything, for example.

    ReplyDelete
  2. > What about when you've got an XSD with an "any" tag in it where ordering is unimportant?

    Yes, that is a gross error in this solution. Will try to fix it in future.

    > Also, a little less junk content (like the commented out lines) might help readability of your example.

    I have updated the code listing. Thanks!

    ReplyDelete

Disqus for Char Sequence