/* Copyright 2014 The jeo project. All rights reserved.
*
* 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 io.jeo.util;
import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.util.Properties;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamResult;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
/**
* Produces an XML document.
* <p>
* Usage:
* <pre><code>
* XMLWriter w = new XMLWriter();
* w.init(output).start("message").text("Hello World").end("message").close();
* </code></pre>
*
* The above would produce the following XML document:
* <pre> <message>Hello World<message></pre>.
* </p>
*/
public final class XMLWriter implements Closeable {
private final Properties outputProps;
private final AttributesImpl atts;
private TransformerHandler tx;
/**
* Create an uninitialized WMLWriter.
*
* @see #init
*/
public XMLWriter() {
outputProps = new Properties();
outputProps.put(OutputKeys.METHOD, "XML");
atts = new AttributesImpl();
}
/**
* Create a XMLWriter initialized with the given Writer.
* @param out non-null Writer
*/
public XMLWriter(Writer out) {
this();
init(out);
}
private TransformerHandler createTransformer(Writer out) throws TransformerConfigurationException {
//create the document seriaizer
SAXTransformerFactory txFactory
= (SAXTransformerFactory) SAXTransformerFactory.newInstance();
TransformerHandler tx = txFactory.newTransformerHandler();
//tx.getTransformer().setOutputProperties(outputProps);
//tx.getTransformer().setOutputProperty(OutputKeys.METHOD, "XML");
tx.getTransformer().setOutputProperties(outputProps);
tx.setResult(new StreamResult(out));
return tx;
}
/**
* Turns on indentation and sets the indent size.
* <p>
* This method needs to be called prior to {@link #init}.
* </p>
* @param size The number of spaces to indent.
*/
public XMLWriter indent(int size) {
if (size > 0) {
String INDENT_AMOUNT_KEY = "{http://xml.apache.org/xslt}indent-amount";
outputProps.put(OutputKeys.INDENT, "yes");
outputProps.put(INDENT_AMOUNT_KEY, String.valueOf(size));
}
return this;
}
/**
* One form of {@link #init} must be invoked before use.
* @param writer non-null Writer to output to
*/
public XMLWriter init(Writer writer) {
if (tx != null) {
throw new IllegalStateException("init");
}
try {
tx = createTransformer(writer);
tx.startDocument();
} catch (Exception ex) {
throw new RuntimeException(ex);
}
return this;
}
/**
* One form of {@link #init} must be invoked before use.
* @param out non-null OutputStream to output to
*/
public XMLWriter init(OutputStream out) {
try {
init(new OutputStreamWriter(out, "utf-8"));
} catch (UnsupportedEncodingException ex) {
throw new RuntimeException(ex);
}
return this;
}
/**
* Starts an element with the provided name and attribute key-value pairs.
* <p>
* Whatever attributes that have been set via calling {@link #atts(Object...)}
* will also be written out. The list of attributes will be cleared after this
* method is called.
* </p>
*
* @param name The name of the element to encode.
* @param kv key-value pairs representing attributes, must be an even number.
* @return this
*/
public XMLWriter start(String name, Object... kv) {
atts(kv);
try {
// prevent potential NPE by passing empty strings
tx.startElement("", "", name, atts);
atts.clear();
} catch (SAXException e) {
throw new RuntimeException(e);
}
return this;
}
/**
* Encodes a full element (start and end) with the specified name.
* <p>
* This method is short hand for the following:
* <pre><code>
* start(name, kv);
* text(value);
* end(name);
* </code></pre>
* </p>
* @param name The name of the element.
* @param value The text value of the element, <tt>null</tt> means no text content.
* @param kv Attribute key-value pairs.
*/
public XMLWriter element(String name, Object value, Object... kv) {
start(name, kv);
text(value);
end(name);
return this;
}
/**
* Encodes an element with no text content or children.
*
* @param name The name of the element.
* @param kv The attribute key value pairs.
*/
public XMLWriter emptyElement(String name, Object... kv) {
return element(name, null, kv);
}
/**
* Encodes text content.
* <p>
* This method is called between {@link #start(String, Object...)} and
* {@link #end()} invocations.
* </p>
* @param value The value to encode as text, <tt>null</tt> makes this method a
* no-op.
*
*/
public XMLWriter text(Object value) {
if (value != null) {
String text = value.toString();
try {
tx.characters(text.toCharArray(), 0, text.length());
} catch (SAXException e) {
throw new RuntimeException(e);
}
}
return this;
}
/**
* Ends the current element.
* <p>
* This method must be called after {@link #start(String, Object...)}.
* </p>
*/
public XMLWriter end(String name) {
try {
// prevent potential NPE by passing empty strings
tx.endElement("", "", name);
return this;
} catch (SAXException e) {
throw new RuntimeException(e);
}
}
/**
* Add any attributes to use on the next invocation of {@link #start(String, Object...)}.
*
* @param kv An even number of key value attribute pairs.
*/
public XMLWriter atts(Object... kv) {
if (kv.length % 2 != 0) {
throw new IllegalArgumentException("non even number of key value pairs");
}
for (int i = 0; i < kv.length; i += 2) {
if (kv[i] != null) {
atts.addAttribute(null, null, String.valueOf(kv[i]), null, String.valueOf(kv[i + 1]));
}
}
return this;
}
@Override
public void close() throws IOException {
try {
tx.endDocument();
} catch (Exception ioe) {
throw new IOException(ioe);
}
}
}