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.
Thanks for the help.
A side note though: the code above put an empty line if no document start is given.
I fixed it with a boolean “firstLine” put to true by default and then used this way https://gist.github.com/cluelessjoe/5928925162fc4041c630
I also made the indentation a constructor parameter, to be able to set it up.
Thanks again
Cheers
Thank you very much!
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);
}
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: