com.wavemaker.commons.util.XMLWriter Maven / Gradle / Ivy
/**
* Copyright (C) 2020 WaveMaker, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.wavemaker.commons.util;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import org.apache.commons.lang3.StringEscapeUtils;
/**
* API for writing XML.
*
* @author Simon Toens
*/
public class XMLWriter {
private static final String SEPARATOR = "\n";
private static final int DEFAULT_INDENT = 2;
private static final int DEFAULT_MAX_ATTRS_ON_SAME_LINE = 2;
private int startIndent;
private static final String ENCODING = "utf-8";
private String currentShortNS;
private boolean wroteFirstElement;
private boolean hasElements;
private boolean incompleteOpenTag;
private boolean hasAttributes;
private final SortedMap namespaces = new TreeMap<>();
// decides if we write closing element on same line as opening element
// or on new line. this applies for both and .../>
private boolean closeOnNewLine;
private final Stack elementStack = new Stack();
private final PrintWriter pw;
// configurable at construction time
private final int indent;
private final int maxAttrsOnSameLine;
// configurable with setter method
// a
// vs.
//
// a
//
private boolean textOnSameLineAsParentElement = true;
public XMLWriter(PrintWriter pw) {
this(pw, DEFAULT_INDENT);
}
public XMLWriter(PrintWriter pw, int indent) {
this(pw, indent, DEFAULT_MAX_ATTRS_ON_SAME_LINE);
}
public XMLWriter(PrintWriter pw, int indent, int maxAttrsOnSameLine) {
this.indent = indent;
this.maxAttrsOnSameLine = maxAttrsOnSameLine;
this.pw = pw;
}
/**
* Calls flush on underlying PrintWriter.
*/
public void flush() {
this.pw.flush();
}
/**
* Writes DOCTYPE, publicID, privateID. Can only be called before adding any elements.
*
* @param publicID
* @param privateID
*/
public void addDoctype(String doctypeName, String publicID, String systemID) {
if (this.hasElements) {
throw new MalformedXMLRuntimeException("Cannot init document after elements have been added");
}
StringBuilder dtdString = new StringBuilder();
dtdString.append("").append(SEPARATOR);
writeGeneric(dtdString.toString());
}
/**
* Add version. Can only be called before adding any elements.
*/
public void addVersion() {
addVersion(false);
}
public void addVersion(boolean standalone) {
if (this.hasElements) {
throw new MalformedXMLRuntimeException("Cannot write version after elements have been added");
}
this.pw.print("");
this.pw.print(SEPARATOR);
}
public void setCurrentShortNS(String s) {
if (!this.namespaces.containsKey(s)) {
throw new MalformedXMLRuntimeException("Short NS \"" + s + "\" has not been declared. Known short NS: " + this.namespaces.keySet());
}
this.currentShortNS = s;
}
public void unsetCurrentShortNS() {
this.currentShortNS = null;
}
/**
* The current element will be closed on a new line, and attributes added will each be on a new line.
*/
public void forceCloseOnNewLine() {
this.closeOnNewLine = true;
}
public boolean willCloseOnNewLine() {
return this.closeOnNewLine;
}
public void setStartIndent(int startIndent) {
this.startIndent = startIndent;
}
/**
* Switches the behavior for addElementWithTextChild. a vs.
* a
*
*/
public void setTextOnSameLineAsParentElement(boolean b) {
this.textOnSameLineAsParentElement = b;
}
public void addNamespace(String shortNS, String longNS) {
this.namespaces.put(shortNS, longNS);
}
public void addComment(String comment) {
StringBuilder sb = new StringBuilder();
if (this.hasElements) {
finishIncompleteTag();
sb.append(SEPARATOR);
}
sb.append(getIndent()).append("");
if (!this.hasElements) {
sb.append(SEPARATOR);
}
writeGeneric(sb.toString());
}
/**
* Adds attribute (name and value) to current XML element.
*/
public void addAttribute(String name, String value) {
if (!this.incompleteOpenTag) {
throw new MalformedXMLRuntimeException("Illegal call to addAttribute");
}
this.hasAttributes = true;
if (this.closeOnNewLine) {
this.pw.print(SEPARATOR);
this.pw.print(getIndent());
} else {
this.pw.print(" ");
}
this.pw.print(name);
this.pw.print("=\"");
this.pw.print(value);
this.pw.print("\"");
}
/**
* Adds attributes to current XML element, represented as Map. Uses the keys as attribute names and corresponding
* elements as attribute values. Calls String.valueOf(...) on keys and values.
*/
public void addAttribute(Map attributes) {
String[] attributesArray = new String[attributes.size() * 2];
int index = 0;
for (Map.Entry entry : attributes.entrySet()) {
attributesArray[index] = entry.getKey();
attributesArray[index + 1] = entry.getValue();
index += 2;
}
addAttribute(attributesArray);
}
/**
* Adds attributes to current XML element. Attribute names and values are passed in as String Array, using the
* following format: {n1, v1, n2, v2, n3, v3, ...}
*/
public void addAttribute(String... attributes) {
// if no attributes yet for this element, we can decided
// on the formatting (all attributes on same line or not)
if (!this.hasAttributes && attributes.length / 2 > this.maxAttrsOnSameLine) {
this.closeOnNewLine = true;
}
for (int i = 0; i < attributes.length; i += 2) {
String key = attributes[i];
String value = attributes[i + 1];
addAttribute(key, value);
}
}
/**
* Adds nested closed elements.
*/
public void addNestedElements(String... elementNames) {
for (String elementName : elementNames) {
addElement(elementName);
}
}
/**
* Adds many closed elements.
*/
public void addClosedElements(String... elementNames) {
for (String elementName : elementNames) {
addClosedElement(elementName);
}
}
/**
* Adds a single closed element.
*/
public void addClosedElement(String elementName) {
addElement(elementName);
closeElement();
}
/**
* Adds a closed element with attributes
*/
public void addClosedElement(String elementName, String... attributes) {
addElement(elementName, attributes);
closeElement();
}
/**
* Writes a new XML element to PrintWriter. If another XML element has been written and not closed, writes this
* element as a child.
*/
public void addElement(String elementName) {
this.hasElements = true;
finishIncompleteTag();
boolean addNamespaces = !this.wroteFirstElement;
if (this.wroteFirstElement) {
this.pw.print(SEPARATOR);
} else {
this.wroteFirstElement = true;
}
this.pw.print(getIndent());
this.pw.print("<");
if (this.currentShortNS != null) {
elementName = qualify(elementName, this.currentShortNS);
}
this.pw.print(elementName);
this.elementStack.push(elementName);
this.incompleteOpenTag = true;
this.hasAttributes = false;
this.closeOnNewLine = false;
if (addNamespaces && !this.namespaces.isEmpty()) {
// prepend short NS with "xmlns:", then add as attributes
String[] ns = new String[this.namespaces.size() * 2];
int i = 0;
for (Map.Entry e : this.namespaces.entrySet()) {
ns[i] = qualify(e.getKey(), "xmlns");
ns[i + 1] = e.getValue();
i += 2;
}
// for short->long ns mappings, put each attr on a new line
this.closeOnNewLine = true;
addAttribute(ns);
}
}
/**
* Convenience method for passing parent and child text element. The result is
* <elementName>textChild</elementName>
* if setTextOnSameLineAsParentElement is set to true on this instance of XMLWriter.
*
* Otherwise the result is:
* <elementName>
* textChild
* </elementName>
*/
public void addElement(String elementName, String textChild) {
addElement(elementName);
addText(textChild, !this.textOnSameLineAsParentElement);
closeElement(!this.textOnSameLineAsParentElement);
}
/**
* Writes a new XML element to PrintWriter. If another XML element has been written and not closed, writes this
* element as a child. Also adds passed attributes.
*/
public void addElement(String elementName, String... attributes) {
addElement(elementName);
addAttribute(attributes);
}
/**
* Writes a new, closed, XML element to PrintWriter. If another XML element has been written and not closed, writes
* this element as a child. Adds passed attributes and character data.
*/
public void addClosedTextElement(String elementName, String text, String... attributes) {
addElement(elementName);
addAttribute(attributes);
addText(text, false);
closeElement(false);
}
/**
* Writes a new XML element to PrintWriter. If another XML element has been written and not closed, writes this
* element as a child. Also adds passed attributes.
*/
public void addElement(String elementName, Map attributes) {
addElement(elementName);
addAttribute(attributes);
}
/**
* Closes the last XML element that has been written.
*/
public void closeElement() {
closeElement(true);
}
public void closeElement(boolean elementOnNewLine) {
if (this.elementStack.isEmpty()) {
throw new MalformedXMLRuntimeException("Illegal call to closeElement");
}
String element = this.elementStack.pop();
if (this.incompleteOpenTag) {
if (this.closeOnNewLine) {
this.pw.print(SEPARATOR);
this.pw.print(getIndent());
}
this.pw.print("/>");
this.incompleteOpenTag = false;
} else {
if (elementOnNewLine) {
this.pw.print(SEPARATOR);
this.pw.print(getIndent());
}
this.pw.print("");
this.pw.print(element);
this.pw.print(">");
}
this.hasAttributes = false;
}
/**
* Writes Text as child element to the current element.
*/
public void addText(String in) {
addText(in, true);
}
public void addText(String in, boolean onNewLine) {
if (in.trim().length() == 0) {
return;
}
finishIncompleteTag();
if (onNewLine) {
this.pw.print(SEPARATOR);
this.pw.print(getIndent());
}
this.pw.print(StringEscapeUtils.escapeXml(in.trim()));
}
public void addCDATA(String in) {
if (in.trim().length() == 0) {
return;
}
finishIncompleteTag();
StringBuilder sb = new StringBuilder();
sb.append(SEPARATOR).append(getIndent()).append("");
writeGeneric(sb.toString());
}
/**
* Closes all XML elements that have been added, and not yet closed, and flushes the underlying PrintWriter.
*/
public void finish() {
closeAll();
this.pw.flush();
}
/**
* @return The current indentation
*/
public String getIndent() {
return getIndent(getStackSize());
}
public String getLineSep() {
return SEPARATOR;
}
private void closeAll() {
while (!this.elementStack.isEmpty()) {
closeElement();
}
}
private void finishIncompleteTag() {
if (this.incompleteOpenTag) {
this.pw.print(">");
this.incompleteOpenTag = false;
}
}
private void writeGeneric(String in) {
this.pw.print(in);
}
private String getIndent(int numUnits) {
StringBuilder indentString = new StringBuilder();
int max = this.startIndent + numUnits * this.indent;
for (int i = 0; i < max; i++) {
indentString.append(" ");
}
return indentString.toString();
}
private String getDefaultIndent() {
return getIndent(1);
}
private int getStackSize() {
return this.elementStack.size();
}
private String qualify(String name, String ns) {
return ns + ":" + name;
}
private static class MalformedXMLRuntimeException extends RuntimeException {
private static final long serialVersionUID = 1L;
private MalformedXMLRuntimeException(String message) {
super(message);
}
}
private static class Stack extends ArrayList {
private static final long serialVersionUID = 1L;
public void push(String s) {
super.add(0, s);
}
public String pop() {
return super.remove(0);
}
}
}