StAX pretty printer

Using StAX to write XML is a lot easier than either using DOM or SAX. There is however no option to indent the generated XML, unlike with SAX or DOM. When faced with this problem, I came out with a simple yet generic solution: I would intercept all write calls and preprend the necessary whitespace according to the current depth in the XML. To achieve this easily an InvocationHandler can be used that will decorate the XMLStreamWriter.

Here is a sample usage

XMLStreamWriter wstxWriter = null;
XMLStreamWriter prettyPrintWriter = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();

wstxWriter = factory.createXMLStreamWriter(baos, "UTF-8"); // specify encoding

// Wrap with pretty print proxy
PrettyPrintHandler handler = new PrettyPrintHandler( wstxWriter );
prettyPrintWriter = (XMLStreamWriter) Proxy.newProxyInstance(
XMLStreamWriter.class.getClassLoader(),
new Class[]{XMLStreamWriter.class},
handler );

prettyPrintWriter.writeStartDocument();

And the InvocationHandler looks like this (see this gist):

public class PrettyPrintHandler implements InvocationHandler {

  private static Logger LOGGER = Logger.getLogger(PrettyPrintHandler.class.getName());
  private final XMLStreamWriter target;
  private int depth = 0;
  private final Map<Integer, Boolean> hasChildElement = new HashMap<Integer, Boolean>();
  private static final String INDENT_CHAR = " ";
  private static final String LINEFEED_CHAR = "\n";

  public PrettyPrintHandler(XMLStreamWriter target) {
    this.target = target;
  }

  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    String m = method.getName();
    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("XML event: " + m);
    }
    // Needs to be BEFORE the actual event, so that for instance the
    // sequence writeStartElem, writeAttr, writeStartElem, writeEndElem, writeEndElem
    // is correctly handled
    if ("writeStartElement".equals(m)) {
      // update state of parent node
      if (depth > 0) {
        hasChildElement.put(depth - 1, true);
      }
      // reset state of current node
      hasChildElement.put(depth, false);
      // indent for current depth
      target.writeCharacters(LINEFEED_CHAR);
      target.writeCharacters(repeat(depth, INDENT_CHAR));
      depth++;
    }
    else if ("writeEndElement".equals(m)) {
      depth--;
      if (hasChildElement.get(depth) == true) {
        target.writeCharacters(LINEFEED_CHAR);
        target.writeCharacters(repeat(depth, INDENT_CHAR));
      }
    }
    else if ("writeEmptyElement".equals(m)) {
      // update state of parent node
      if (depth > 0) {
        hasChildElement.put(depth - 1, true);
      }
      // indent for current depth
      target.writeCharacters(LINEFEED_CHAR);
      target.writeCharacters(repeat(depth, INDENT_CHAR));
    }
    method.invoke(target, args);
    return null;
  }

  private String repeat(int d, String s) {
    String _s = "";
    while (d-- > 0) {
      _s += s;
    }
    return _s;
  }
}

The repeat method is quite ugly. You can use StringUtil form commons-lang instead of check one of the other repeat implementation on stackoverflow.

Advertisements

4 thoughts on “StAX pretty printer

  1. Very nice code!
    I added the following piece in my version to add a line feed before the last element:
    else if (“writeEndDocument”.equals(m)) {
    target.writeCharacters(LINEFEED_CHAR);
    }

  2. Thank you so much for sharing the code! It helps a lot.

    I have added a couple of lines for another method:

    } else if (“writeComment”.equals(m)) {
    // indent for current depth
    target.writeCharacters(LINEFEED_CHAR);
    target.writeCharacters(repeat(depth));
    }

    If a marshaller has a listener, which implements beforeMarshal() and afterMarshal() via XMLStreamWriter.writeComment() you will have nicely formatted XML anyway:

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s