import java.util.Map; import java.util.List; import java.util.ArrayList; import java.util.Collections; import java.io.BufferedWriter; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.IOException; /** A simple class to write XML to a stream. Does some pretty-printing. */ /* TODO: - more validation, eg of tags and attr names - namespaces - a ContentHandler adapter? - drip-feeding text (needed to allow mixed content) - proper handling of whitespace in mixed content (ie can't do it, only in element content, but how do we know where that is?) - CDATA (as a separate method? a mode? a setting attached to tags? an intelligent decision based on content?) - PIs */ public class XMLWriter { private static final String UTF8 = "UTF-8"; private BufferedWriter out; private String publicID; private String systemID; private List tagStack; public XMLWriter(BufferedWriter out, String charset, String publicID, String systemID) throws IOException { this.out = out; out.write(""); out.newLine(); this.publicID = publicID; this.systemID = systemID; tagStack = new ArrayList(); } public XMLWriter(BufferedWriter out, String publicID, String systemID) throws IOException { this(out, UTF8, publicID, systemID); } public XMLWriter(BufferedWriter out) throws IOException { this(out, null, null); } public XMLWriter(OutputStream out, String charset, String publicID, String systemID) throws IOException { this(new BufferedWriter(new OutputStreamWriter(out, charset)), charset, publicID, systemID); } public XMLWriter(OutputStream out, String publicID, String systemID) throws IOException { this(out, UTF8, publicID, systemID); } public XMLWriter(OutputStream out) throws IOException { this(out, UTF8, null, null); } public void startElement(String tag, Map attrs) throws IOException { writeTagOpen(tag, attrs); out.write(">"); out.newLine(); pushTag(tag); } public void startElement(String tag) throws IOException { startElement(tag, Collections.emptyMap()); } public void endElement() throws IOException { String tag = popTag(); indent(); out.write(""); out.newLine(); } public void element(String tag, Map attrs, String text) throws IOException { writeTagOpen(tag, attrs); if (text.equals("")) { out.write("/>"); } else { out.write(">"); writeEscaped(text); out.write(""); } out.newLine(); } public void element(String tag, String text) throws IOException { element(tag, Collections.emptyMap(), text); } public void comment(String text) throws IOException { if (text.contains("-->")) throw new IOException("bad comment text: " + text); indent(); out.write(""); out.newLine(); } private void writeTagOpen(String tag, Map attrs) throws IOException { if ((publicID != null) || (systemID != null)) { out.write(""); out.newLine(); publicID = null; systemID = null; } indent(); out.write("<"); out.write(tag); for (Map.Entry attr: attrs.entrySet()) { out.write(" "); out.write(attr.getKey()); out.write("=\""); writeEscaped(attr.getValue()); out.write("\""); } } private void writeEscaped(String str) throws IOException { for (int i = 0; i < str.length(); ++i) { char ch = str.charAt(i); switch (ch) { case '&': { out.write("&"); } break; case '<': { out.write("<"); } break; case '>': { out.write(">"); } break; case '"': { out.write("""); } break; default: { out.write(ch); } } } } private void pushTag(String tag) { tagStack.add(tag); } private String popTag() { return tagStack.remove(tagStack.size() - 1); } private void indent() throws IOException { for (int i = 0; i < tagStack.size(); ++i) out.write('\t'); } public void newLine() throws IOException { indent(); out.newLine(); } public void close() throws IOException { if (!tagStack.isEmpty()) throw new IOException("some tags are unclosed: " + tagStack); out.flush(); } }