/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see .
Copyright 2009 (C) Sindre Mehus
*/
package net.sourceforge.subsonic.util;
import org.apache.commons.lang.StringEscapeUtils;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.XML;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Stack;
/**
* Simplifies building of XML documents.
*
* Example:
* The following code:
*
* XMLBuilder builder = XMLBuilder.createXMLBuilder();
* builder.add("foo").add("bar");
* builder.add("zonk", 42);
* builder.end().end();
* System.out.println(builder.toString());
*
* produces the following XML:
*
* <foo>
* <bar>
* <zonk>42</zonk>
* </bar>
* </foo>
*
* This class is not thread safe.
*
* Also supports JSON and JSONP formats.
*
* @author Sindre Mehus
*/
public class XMLBuilder {
private static final String INDENTATION = " ";
private static final String NEWLINE = "\n";
private final Writer writer = new StringWriter();
private final Stack elementStack = new Stack();
private final boolean json;
private final String jsonpCallback;
public static XMLBuilder createXMLBuilder() {
return new XMLBuilder(false, null);
}
public static XMLBuilder createJSONBuilder() {
return new XMLBuilder(true, null);
}
public static XMLBuilder createJSONPBuilder(String callback) {
return new XMLBuilder(true, callback);
}
/**
* Creates a new instance.
*
* @param json Whether to produce JSON rather than XML.
* @param jsonpCallback Name of javascript callback for JSONP.
*/
private XMLBuilder(boolean json, String jsonpCallback) {
this.json = json;
this.jsonpCallback = jsonpCallback;
}
/**
* Adds an XML preamble, with the given encoding. The preamble will typically
* look like this:
*
* <?xml version="1.0" encoding="UTF-8"?>
*
* @param encoding The encoding to put in the preamble.
* @return A reference to this object.
*/
public XMLBuilder preamble(String encoding) throws IOException {
writer.write("");
newline();
return this;
}
/**
* Adds an element with the given name and a single attribute.
*
* @param element The element name.
* @param attributeKey The attributes key.
* @param attributeValue The attributes value.
* @param close Whether to close the element.
* @return A reference to this object.
*/
public XMLBuilder add(String element, String attributeKey, Object attributeValue, boolean close) throws IOException {
return add(element, close, new Attribute(attributeKey, attributeValue));
}
/**
* Adds an element with the given name and attributes.
*
* @param element The element name.
* @param close Whether to close the element.
* @param attributes The element attributes.
* @return A reference to this object.
*/
public XMLBuilder add(String element, boolean close, Attribute... attributes) throws IOException {
return add(element, Arrays.asList(attributes), close);
}
/**
* Adds an element with the given name and attributes.
*
* @param element The element name.
* @param attributes The element attributes.
* @param close Whether to close the element.
* @return A reference to this object.
*/
public XMLBuilder add(String element, Iterable attributes, boolean close) throws IOException {
return add(element, attributes, null, close);
}
/**
* Adds an element with the given name, attributes and character data.
*
* @param element The element name.
* @param attributes The element attributes.
* @param text The character data.
* @param close Whether to close the element.
* @return A reference to this object.
*/
public XMLBuilder add(String element, Iterable attributes, String text, boolean close) throws IOException {
indent();
elementStack.push(element);
writer.write('<');
writer.write(element);
if (attributes == null) {
attributes = Collections.emptyList();
}
Iterator iterator = attributes.iterator();
if (iterator.hasNext()) {
writer.write(' ');
}
while (iterator.hasNext()) {
Attribute attribute = iterator.next();
attribute.append(writer);
if (iterator.hasNext()) {
writer.write(' ');
}
}
if (close && text == null) {
elementStack.pop();
writer.write("/>");
} else {
writer.write('>');
}
if (text != null) {
writer.write(text);
if (close) {
elementStack.pop();
writer.write("");
writer.write(element);
writer.write('>');
}
}
newline();
return this;
}
/**
* Closes the current element.
*
* @return A reference to this object.
* @throws IllegalStateException If there are no unclosed elements.
*/
public XMLBuilder end() throws IllegalStateException, IOException {
if (elementStack.isEmpty()) {
throw new IllegalStateException("There are no unclosed elements.");
}
String element = elementStack.pop();
indent();
writer.write("");
writer.write(element);
writer.write('>');
newline();
return this;
}
/**
* Closes all unclosed elements.
*
* @return A reference to this object.
*/
public XMLBuilder endAll() throws IOException {
while (!elementStack.isEmpty()) {
end();
}
return this;
}
/**
* Returns the XML document as a string.
*/
@Override
public String toString() {
String xml = writer.toString();
if (!json) {
return xml;
}
try {
JSONObject jsonObject = XML.toJSONObject(xml);
if (jsonpCallback != null) {
return jsonpCallback + "(" + jsonObject.toString(1) + ");";
}
return jsonObject.toString(1);
} catch (JSONException x) {
throw new RuntimeException("Failed to convert from XML to JSON.", x);
}
}
private void indent() throws IOException {
int depth = elementStack.size();
for (int i = 0; i < depth; i++) {
writer.write(INDENTATION);
}
}
private void newline() throws IOException {
writer.write(NEWLINE);
}
/**
* An XML element attribute.
*/
public static class Attribute {
private final String key;
private final Object value;
public Attribute(String key, Object value) {
this.key = key;
this.value = value;
}
public String getKey() {
return key;
}
public Object getValue() {
return value;
}
private void append(Writer writer) throws IOException {
if (key != null && value != null) {
writer.write(key);
writer.write("=\"");
writer.write(StringEscapeUtils.escapeXml(value.toString()));
writer.write("\"");
}
}
}
/**
* A set of attributes.
*/
public static class AttributeSet implements Iterable {
private final Map attributes = new LinkedHashMap();
public void add(Attribute attribute) {
attributes.put(attribute.getKey(), attribute);
}
public void add(String key, Object value) {
if (key != null && value != null) {
add(new Attribute(key, value));
}
}
public void addAll(Iterable attributes) {
for (Attribute attribute : attributes) {
add(attribute);
}
}
public Iterator iterator() {
return attributes.values().iterator();
}
}
}