Monday, February 16, 2009

Java Blog

In my last post I hope I convinced you that XML parsing with XPath queries and XSD validation is the way forward:-

  • W3C Schema Definitions (XSDs) let you separate validation from logic
  • XPaths queries are a serious timesaver and make code very readable

But also that the Java XML API is possibly the ugliest and stupidest API you will ever see in your life. In this post my company is releasing a helper class as open source, which makes XML processing a pleasure. Right click on XMLHelper.java and save to disc. Please let me know if you find any bugs or come up with anything worthwhile adding! You’ll need the GuruMeditationFailure exception from my previous post unless you want to edit the code, and the latest version of the Google Collections (which everybody should be using!).

XML classes in Java are notoriously verbose and rely on various factory and builder classes, with various thread safety models that are poorly documented and lots and lots of checked exceptions that are often implementation specific. The XPath classes are especially difficult to work with as they lack fundamental support for namespace prefixes, have not been upgraded to use generics and are not thread safe on any level (making pre-compilation of queries almost useless).

A typical XML-aware Java class file will have around 49 import statements, just to deal with the framework… this helper class hides it such that the client code only needs to deal with the bare essentials (typically 3 or 4 imports plus this file), resulting in much cleaner code.

This class attempts to hide as much IO as possible by working with Strings (to allow swallowing of IOExceptions) and turns configuration-based checked exceptions into runtime IllegalArgumentExceptions… shame on the initial designers! Convenience methods are provided to allow one-line XPath queries, and all implementation specific exceptions are turned into more fitting throwables… such as ParseException and (our own) XsdValidationException.

Lets say you have loaded your XSD file into a String named xsdString, you can create a thread-safe instance of XMLHelper and define the namespace shortcuts that you intend to use in your XPath queries

XMLHelper xml = new XMLHelper(xsdString);
xml.addXPathShorthand("J", "javablog");

which can be used to parse and validate an XML file which has been read into the String named inputString as easily as

try {
Document doc = xml.getDocument(inputString);
xml.validate(doc);
String title = xml.xPathString("/J:post/title/text()", doc);
String dateString = xml.xPathString("/J:post@dateTime", doc);
URI url = new URI(xml.xPathString("/J:post@url", doc));
} catch (ParseException e) {
// problem parsing the input, it probably wasn't valid XML
} catch (XsdValidationException e) {
// problem validating the input, it was valid XML but broke our constraints
}

In 5 lines you have parsed your XML file, validated it against the XSD and read three of the elements into variables. Another 2 lines catch the parse and validation exceptions, which have very clear blocks in which you can perform failure logic. Pretty cool, eh?

The helper class has many other methods, including the ability to pre-compile XPath queries (which gives a performance benefit when you make the calls in a loop of more than 10,000 elements), obtaining un-shared versions of all the core XML classes (e.g. Document, Validator, Source) and outputting Source objects as formatted XML Strings.

Note that the following little methods may be useful to you for creating Strings from InputStreams and for silently closing Closeables

/**
* Converts a stream to a String, converting end-of-line markers into "{@code \n}".
*
*
@param stream
*
@return
* @throws IOException
*/

public static String streamToString(InputStream stream) throws IOException {
Preconditions.checkNotNull(stream);
try {
InputStreamReader reader = new InputStreamReader(stream);
BufferedReader buffered = new BufferedReader(reader);
StringBuilder builder = new StringBuilder();
String line;
while ((line = buffered.readLine()) != null) {
builder.append(line);
builder.append("\n");
}
return builder.toString();
} finally {
close(stream);
}
}
/**
* Close a reader, writer, stream or channel, swallowing (but logging)
* any exceptions. Calls {@link Flushable#flush()} if {@link Flushable}.
*
*
@param input
*/

public static void close(@Nullable Closeable input) {
if (input == null)
return;
if (input instanceof Flushable)
try {
((Flushable) input).flush();
} catch (IOException e) {
log.warning("failed to flush Flushable " + input);
}
try {
input.close();
} catch (IOException e) {
log.warning("failed to close Closeable " + input);
}
}