package xapi.dev.source; import xapi.collect.X_Collect; import xapi.collect.api.StringTo; import xapi.util.api.ConvertsValue; public class XmlBuffer extends PrintBuffer { public static final String QUOTE = "\"", QUOTE_ENTITY = """; private static final String COMMENT_NAME = "!--"; private String tagName; private PrintBuffer attributes; private final PrintBuffer comment; private final PrintBuffer before; private StringTo<StringBuilder> attributeMap; protected boolean printNewline = false; private boolean abbr = false; private ConvertsValue<String, String> escaper; protected boolean trimWhitespace; @SuppressWarnings("unchecked") public XmlBuffer() { comment = new PrintBuffer(); before = new PrintBuffer(); init(); } public XmlBuffer(final String tagName) { this(); setTagName(tagName); } XmlBuffer(StringBuilder suffix) { super(suffix); comment = new PrintBuffer(); before = new PrintBuffer(); init(); } protected void init() { indent = INDENT; escaper = ConvertsValue.PASS_THRU; } public XmlBuffer setTagName(final String name) { if (tagName != null) { indent(); } tagName = name; return this; } public XmlBuffer setAttribute(final String name, final String value) { ensureAttributes(); final String val = escapeAttribute(value); setRawAttribute(name, val); return this; } protected String escapeAttribute(final String value) { if (value == null) { return " "; } else { if (value.startsWith("//")) { return "=" + escape(value.substring(2)); } else { return "=\"" + value.replaceAll(QUOTE, QUOTE_ENTITY) + "\" "; } } } protected void setRawAttribute(final String name, final String val) { StringBuilder attr = attributeMap.get(name); if (attr == null) { attributes.print(name); attr = new StringBuilder(val); attributeMap.put(name, attr); final PrintBuffer attrBuf = new PrintBuffer(attr); attributes.addToEnd(attrBuf); } else { attr.setLength(0); attr.append(val); } } protected void ensureAttributes() { if (attributes == null) { attributes = new PrintBuffer(); attributeMap = X_Collect.newStringMapInsertionOrdered(StringBuilder.class); } } public XmlBuffer makeTag(final String name) { final XmlBuffer buffer = new XmlBuffer(name); buffer.setTrimWhitespace(true); buffer.indent = indent + INDENT; addToEnd(buffer); return buffer; } public XmlBuffer makeTagNoIndent(final String name) { final XmlBuffer buffer = new XmlBuffer(name); buffer.setTrimWhitespace(trimWhitespace); buffer.indent = indent + INDENT; buffer.setNewLine(false); addToEnd(buffer); return buffer; } public XmlBuffer makeTagAtBeginning(final String name) { final XmlBuffer buffer = new XmlBuffer(name); buffer.setTrimWhitespace(trimWhitespace); buffer.indent = indent + INDENT; addToBeginning(buffer); return buffer; } public XmlBuffer makeTagAtBeginningNoIndent(final String name) { final XmlBuffer buffer = new XmlBuffer(name); buffer.setNewLine(false); buffer.setTrimWhitespace(trimWhitespace); buffer.indent = indent + INDENT; addToBeginning(buffer); return buffer; } public String toSource() { if (tagName == null) { assert attributes == null || attributes.isEmpty() : "Cannot add attributes to an XmlBuffer with no tag name: " + "\nAttributes: " + attributes + "\nBody: " + super.toSource(); return super.toSource(); } String indent = trimWhitespace ? "" : this.indent; final String origIndent = indent.replaceFirst(INDENT, ""); final StringBuilder b = new StringBuilder(origIndent); String text; text = this.before.toSource(); if (text.length() > 0) { b.append(escape(text)); } text = this.comment.toSource(); if (text.length() > 0) { if (!text.startsWith("<!--")) { b.append("<!--\n"); } b.append(indent); b.append(escape(text)); if (!text.endsWith("-->")) { b.append("\n-->"); } b.append(origIndent); } b.append("<"); String tag; if (tagName.startsWith("//")) { tag = escape(tagName.substring(2)); } else { tag = tagName; } b.append(tag); if (attributes != null && !attributes.isEmpty()) { b.append(" ").append(attributes); } final String body = super.toSource(); if (abbr && body.length() == 0) { if (shouldShortenEmptyTag(tagName)) { newline(b.append("/>")); } else { newline(b.append("> </").append(tag).append(">")); } } else { newline(newline(b.append(">")).append(escape(body)).append(printNewline ? origIndent : "").append("</") .append(tag).append(">")); } return b.toString(); } public String escape(final String text) { return escaper.convert(text); } public XmlBuffer setEscaper(final ConvertsValue<String, String> escaper) { this.escaper = escaper; return this; } private StringBuilder newline(final StringBuilder append) { if (printNewline && !trimWhitespace) { append.append("\n"); } return append; } public XmlBuffer setNewLine(final boolean useNewLine) { printNewline = useNewLine; return this; } protected boolean shouldShortenEmptyTag(final String tag) { return !"script".equals(tag); } @Override public XmlBuffer append(final Object obj) { super.append(obj); return this; } @Override public XmlBuffer print(final String str) { super.print(str); return this; } @Override public XmlBuffer clearIndent() { indent = ""; return this; } @Override public XmlBuffer append(final String str) { super.append(str); return this; } @Override public XmlBuffer append(final CharSequence s) { super.append(s); return this; } @Override public XmlBuffer append(final CharSequence s, final int start, final int end) { super.append(s, start, end); return this; } @Override public XmlBuffer append(final char[] str) { super.append(str); return this; } @Override public XmlBuffer append(final char[] str, final int offset, final int len) { super.append(str, offset, len); return this; } @Override public XmlBuffer append(final boolean b) { super.append(b); return this; } @Override public XmlBuffer append(final char c) { super.append(c); return this; } @Override public XmlBuffer append(final int i) { super.append(i); return this; } @Override public XmlBuffer append(final long lng) { super.append(lng); return this; } @Override public XmlBuffer append(final float f) { super.append(f); return this; } @Override public XmlBuffer append(final double d) { super.append(d); return this; } @Override public XmlBuffer indent() { super.indent(); return this; } @Override public XmlBuffer indentln(final Object obj) { super.indentln(obj); return this; } @Override public XmlBuffer indentln(final String str) { super.indentln(str); return this; } @Override public XmlBuffer indentln(final CharSequence s) { super.indentln(s); return this; } @Override public XmlBuffer indentln(final char[] str) { super.indentln(str); return this; } @Override public XmlBuffer outdent() { super.outdent(); return this; } @Override public XmlBuffer println() { super.println(); return this; } @Override public XmlBuffer println(final Object obj) { super.println(obj); return this; } @Override public XmlBuffer println(final String str) { super.println(str); return this; } @Override public XmlBuffer println(final CharSequence s) { super.println(s); return this; } @Override public XmlBuffer println(final char[] str) { super.println(str); return this; } @Override public PrintBuffer printBefore(final String prefix) { return before.printBefore(prefix); } public boolean isNoTagName() { return tagName == null; } public boolean hasComment() { return comment.isEmpty(); } /** * final so implementors must override addComment. * * We pay for the ugly unsafe generic to make it final, * so perhaps a nice @DoNotOverride annotation is in order * or @MustCallSuper (which would make the infinite recursion obvious), * or @NeverCallFrom() (which would check all subtype's method bodies) */ @SuppressWarnings("unchecked") public final <X extends XmlBuffer> X setComment(String comment) { this.comment.clear(); addComment(comment); return (X) this; } public XmlBuffer addComment(String comment) { this.comment.append(comment); return this; } @Override public boolean isEmpty() { return super.isEmpty() && isNoTagName() && comment.isEmpty() && before.isEmpty(); } public XmlBuffer setId(final String id) { setAttribute("id", id); return this; } public String getId() { if (!attributeMap.containsKey("id")) { setId("x-"+hashCode()); } final StringBuilder id = attributeMap.get("id"); return id.substring(2, id.length()-2); } public boolean hasAttribute(final String name) { ensureAttributes(); return attributeMap.containsKey(name); } public String getAttribute(final String name) { if (hasAttribute(name)) { final StringBuilder attr = attributeMap.get(name); if (isRemoveQuotes(attr)) { return attr.substring(2, attr.length()-2); } return attr.substring(1, attr.length()); } return null; } public XmlBuffer add(Object ... values) { super.add(values); return this; } @Override public XmlBuffer ln() { super.ln(); return this; } protected boolean isRemoveQuotes(final StringBuilder attr) { return attr.charAt(1) == '"'; } public XmlBuffer allowAbbreviation(final boolean abbr) { this.abbr = abbr; return this; } public XmlBuffer setTrimWhitespace(boolean trimWhitespace) { this.trimWhitespace = trimWhitespace; return this; } }