package jeql.command.io.xml; import java.io.IOException; import java.io.Writer; import java.util.Stack; import jeql.util.SystemUtil; /** * An adapter which supports * writing data-structured XML documents. * Data-structured means that elements are not interleaved * with text; * or equivalently that text entities * occur only at leaves in the XML DOM tree; * or equivalently that no text occurs between two start tags or two end tags. * <p> * Features of the writer include: * <ul> * <li>emission of correct end tags * <li>indenting * <li>generation of CDATA escaping, with CDATA end tags escaped also * <li>comments * </ul> * * @author Martin Davis * */ public class XmlDataWriter { private Writer writer; private boolean documentStarted = false; private Stack elementStack = new Stack(); private int indentLevel = 0; private static final String INDENT_STR = " "; public XmlDataWriter(Writer writer) { this.writer = writer; } public Writer getWriter() { return writer; } private void indentPush() { indentLevel++; } private void indentPop() { if (indentLevel > 0) indentLevel -= 1; } private void writeIndent() throws IOException { for (int i = 0; i < indentLevel; i++) { writer.write(INDENT_STR); } } public int getIndentLevel() { return indentLevel; } public void prolog() throws IOException { if (documentStarted) throw new IllegalStateException("XML declaration must occur at start of document"); writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); writer.write(SystemUtil.LINE_TERM); documentStarted = true; } public void comment(String comment) throws IOException { comment(comment, true); } public void comment(String comment, boolean indent) throws IOException { if (indent) writeIndent(); writer.write("<!-- "); writer.write(comment); writer.write(" -->"); writer.write(SystemUtil.LINE_TERM); } public void elementStart(String tagName) throws IOException { elementStart(tagName, null); } public void elementStart(String tagName, String attributes) throws IOException { writeIndent(); writeStartTag(tagName, attributes); writer.write(SystemUtil.LINE_TERM); elementStack.push(tagName); indentPush(); } /** * Writes end tag for current element * * @throws IOException */ public void elementEnd() throws IOException { indentPop(); writeIndent(); writeEndTag((String) elementStack.pop()); } /** * Writes end tags for elements nested n deep. * * @param n * @throws IOException */ public void elementEnd(int n) throws IOException { for (int i = 0; i < n; i++) { elementEnd(); } } /** * Writes an element containing a data string, without XML-encoding it. * The data should be known to not contain any XML reserved characters. * This is more efficient, since it avoids the translation of these characters. * * @param tagName * @param value * @throws IOException */ public void elementWithDataRaw(String tagName, String data) throws IOException { elementWithDataRaw(tagName, null, data); } /** * Writes an element containing a data string, without XML-encoding it. * The data should be known to not contain any XML reserved characters. * This is more efficient, since it avoids the translation of these characters. * * @param tagName * @param value * @throws IOException */ public void elementWithDataRaw(String tagName, String attributes, String data) throws IOException { writeIndent(); writeStartTag(tagName, attributes); writer.write(data); writeEndTag(tagName); } /** * Writes an element containing a data string, * XML-encoding the string if necessary. * * @param tagName * @param value * @throws IOException */ public void elementWithData(String tagName, String value) throws IOException { elementWithData(tagName, null, value); } /** * Writes an element with the given attributes and containing a data string, * XML-encoding the string if necessary. * * @param tagName * @param attributes * @param value * @throws IOException */ public void elementWithData(String tagName, String attributes, String value) throws IOException { writeIndent(); writeStartTag(tagName, attributes); writeEncoded(value); writeEndTag(tagName); } /** * Writes markup, with optional indenting. * Markup is assumed to be correctly XML-encoded. * * @param markup * @param indent * @throws IOException */ public void markup(String markup, boolean indent) throws IOException { int start = 0; int len = markup.length(); while (start < len) { int end = start; // write indent, if chars remaining if (end < len) { writeIndent(); } // find next EOL, or EOS while (end < len && markup.charAt(end) != '\n') { end++; } // include EOL in written block if (end < len && markup.charAt(end) == '\n') end++; // write the text block (if any) if (end > start) { writer.write(markup, start, end - start); } start = end; } } private void writeStartTag(String tagName, String attributes) throws IOException { writer.write("<"); writer.write(tagName); if (attributes != null) { writer.write(" "); writer.write(attributes); } writer.write(">"); } private void writeEndTag(String tagName) throws IOException { writer.write("</"); writer.write(tagName); writer.write(">"); writer.write(SystemUtil.LINE_TERM); } private void writeEncoded(String text) throws IOException { if (hasReservedTextChars(text)) { writeCdata(text); } else writer.write(text); } private void writeCdata(String text) throws IOException { // if text contains CDATA terminator, it must be encoded String encText = text.replaceAll("]]>", "]]>"); writer.write("<![CDATA["); writer.write(encText); writer.write("]]>"); } /** * Tests whether a string contains any characters which * are reserved characters in XML text content. * These include '&', '<' and '>' * These characters must be escaped to be represented correctly * in a valid XML document. * * @param text * @return true if the input contains any reserved characters */ public static boolean hasReservedTextChars(String text) { return text.indexOf('&') >= 0 || text.indexOf('<') >= 0 || text.indexOf('>') >= 0; } /** * Closes this writer and outputs any non-ended element tags. * Does NOT close the underlying Writer. * * @throws IOException */ public void close() throws IOException { while (! elementStack.empty()) { elementEnd(); } } }