org.beanio.stream.xml.XmlWriter Maven / Gradle / Ivy
Show all versions of beanio Show documentation
/*
* Copyright 2011-2013 Kevin Seim
*
* 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 org.beanio.stream.xml;
import java.io.*;
import java.util.*;
import javax.xml.XMLConstants;
import javax.xml.stream.*;
import org.beanio.internal.util.*;
import org.beanio.stream.RecordWriter;
import org.w3c.dom.*;
/**
* A XmlWriter is used to write records to a XML output stream. A document
* object model (DOM) is used to represent a record. Group elements, as indicated
* by a user data key (see below), are not closed when a record is written. When
* write(null) is called, an open group element is closed. Finally, calling
* flush() will close all remaining group elements and complete the document.
*
* A XmlWriter makes use of the DOM user data feature to pass additional
* information to and from the parser. The IS_GROUP_ELEMENT user data is
* a Boolean value added to an element to indicate the element is group.
* And the IS_NAMESPACE_IGNORED user data is a Boolean value set on
* elements where the XML namespace should be ignored when writing to the output stream.
*
* @author Kevin Seim
* @since 1.1
*/
public class XmlWriter implements RecordWriter, StatefulWriter {
/**
* The DOM user data key to indicate whether the namespace of a DOM element
* should be ignored when writing to the output stream. The value must be
* of type {@link Boolean}.
*/
public static final String IS_NAMESPACE_IGNORED = "isNamespaceIgnored";
/**
* The DOM user data key to indicate whether the declared namespace should
* override the default namespace. The value must be of type {@link Boolean}.
*/
public static final String IS_DEFAULT_NAMESPACE = "isDefaultNamespace";
/**
* The DOM user data key to indicate a DOM element is a group element and should
* be left "open" when the record is written to the output stream. The value must
* of type java.lang.Boolean.
*/
public static final String IS_GROUP_ELEMENT = "isGroup";
private static final boolean DELTA_ENABLED = "true".equals(Settings.getInstance().getProperty(
Settings.XML_WRITER_UPDATE_STATE_USING_DELTA));
private static final String DEFAULT_LINE_SEPARATOR = System.getProperty("line.separator");
private static final XMLOutputFactory xmlOutputFactory;
static {
xmlOutputFactory = XMLOutputFactory.newInstance();
}
/* map keys for storing state information for implementing StatefulWriter */
private static final String OUTPUT_HEADER_KEY = "header";
private static final String NAMESPACE_MAP_KEY = "nsMap";
private static final String LEVEL_KEY = "level";
private static final String STACK_ELEMENT_KEY = "xml";
private static final String STACK_NS_MAP_KEY = "nsMap";
/* The underlying writer */
private Writer writer;
/* The XML stream writer to write to */
private XMLStreamWriter out;
/* XML parser configuration */
private XmlParserConfiguration config;
/* String used to indent new lines of XML */
private String indentation = "";
private int level = 0;
private ElementStack elementStack;
/* whether a XML header needs to be output before writing a record */
private boolean outputHeader = false;
/* the next index to try when auto generating a namespace prefix */
private int namespaceCount = 0;
/* Map of auto-generated namespace prefixes to namespaces */
private Map namespaceMap = new HashMap();
/* the minimum level last stored when the state was updated */
private int dirtyLevel = 0;
/* flag used to suppress output during state restoration */
private boolean suppressOutput = false;
/**
* Constructs a new XmlWriter.
* @param writer the output stream to write to
*/
public XmlWriter(Writer writer) {
this(writer, null);
}
/**
* Constructs a new XmlWriter.
* @param writer the output stream to write to
* @param config the XML writer configuration
*/
public XmlWriter(Writer writer, XmlParserConfiguration config) {
if (writer == null) {
throw new IllegalArgumentException("writer is null");
}
this.writer = new FilterWriter(writer) {
@Override
public void write(int c) throws IOException {
if (!suppressOutput) {
super.write(c);
}
}
@Override
public void write(char[] cbuf, int off, int len) throws IOException {
if (!suppressOutput) {
super.write(cbuf, off, len);
}
}
@Override
public void write(String str, int off, int len) throws IOException {
if (!suppressOutput) {
super.write(str, off, len);
}
}
};
if (config == null) {
// create a default configuration
this.config = new XmlParserConfiguration();
}
else {
// the configuration is cloned to prevent changes during execution
this.config = config.clone();
}
init();
try {
out = xmlOutputFactory.createXMLStreamWriter(this.writer);
}
catch (XMLStreamException e) {
throw new IllegalArgumentException("Failed to create XMLStreamWriter: " + e.getMessage(), e);
}
}
/**
* Initializes this writer after the configuration has been set.
*/
private void init() {
if (config.getLineSeparator() == null) {
config.setLineSeparator(DEFAULT_LINE_SEPARATOR);
}
if (config.isIndentationEnabled()) {
StringBuilder b = new StringBuilder();
for (int i=0; itrue if indentation is enabled
* @throws XMLStreamException
*/
private void write(Element element, boolean indentationEnabled) throws XMLStreamException {
String name = element.getLocalName();
String prefix = element.getPrefix();
String namespace = element.getNamespaceURI();
boolean ignoreNamespace = false;
if (namespace == null) {
if (Boolean.TRUE.equals(element.getUserData(IS_NAMESPACE_IGNORED))) {
prefix = null;
ignoreNamespace = true;
}
namespace = "";
}
boolean setDefaultNamespace = false;
if (prefix == null && !ignoreNamespace) {
if (Boolean.TRUE.equals(element.getUserData(IS_DEFAULT_NAMESPACE))) {
setDefaultNamespace = true;
}
}
// flag indicating if the element is empty or not
boolean empty = false;
// flag for lazily appending to stack
boolean pendingStackUpdate = true;
// start the element
if (elementStack == null) {
if (ignoreNamespace) {
out.writeStartElement(name);
}
else if (prefix != null) {
out.writeStartElement(prefix, name, namespace);
out.writeNamespace(prefix, namespace);
}
else {
out.writeStartElement(name);
out.writeDefaultNamespace(namespace);
}
push(namespace, prefix, name);
for (Map.Entry ns : config.getNamespaceMap().entrySet()) {
out.writeNamespace(ns.getKey(), ns.getValue());
elementStack.addNamespace(ns.getKey(), ns.getValue());
}
pendingStackUpdate = false;
}
else {
if (indentationEnabled) {
newLine();
}
empty = !element.hasChildNodes();
if (ignoreNamespace || (elementStack.isDefaultNamespace(namespace)) && prefix == null) {
if (empty) {
out.writeEmptyElement(name);
}
else {
out.writeStartElement(name);
}
namespace = elementStack.getDefaultNamespace();
prefix = null;
}
else {
String p = elementStack.findPrefix(namespace);
boolean declareNamespace = false;
if (p == null) {
declareNamespace = true;
}
else if (prefix == null && !setDefaultNamespace) {
prefix = p;
}
if (prefix == null) {
if (empty) {
out.writeEmptyElement(name);
}
else {
out.writeStartElement(name);
}
}
else {
if (empty) {
out.writeEmptyElement(prefix, name, namespace);
}
else {
out.writeStartElement(prefix, name, namespace);
}
}
if (setDefaultNamespace) {
out.writeDefaultNamespace(namespace);
}
else if (declareNamespace) {
out.writeNamespace(prefix, namespace);
}
}
}
// write attributes
Set attPrefixSet = null;
NamedNodeMap map = element.getAttributes();
if (map.getLength() > 0) {
if (pendingStackUpdate) {
push(namespace, prefix, name);
pendingStackUpdate = false;
}
}
for (int i=0,j=map.getLength(); i();
}
attPrefixSet.add(attPrefix);
}
out.writeAttribute(attPrefix, attNamespace, attName, att.getValue());
}
}
// if the element contains text, we disable indentation
if (indentationEnabled) {
Node child = element.getFirstChild();
while (child != null) {
if (child.getNodeType() == Node.TEXT_NODE) {
indentationEnabled = false;
break;
}
child = child.getNextSibling();
}
}
boolean isParent = false;
// write children
Node child = element.getFirstChild();
while (child != null) {
switch (child.getNodeType()) {
case Node.ELEMENT_NODE:
if (pendingStackUpdate) {
push(namespace, prefix, name);
pendingStackUpdate = false;
}
write((Element) child, indentationEnabled);
isParent = true;
break;
case Node.TEXT_NODE:
out.writeCharacters(((Text)child).getData());
break;
default:
break;
}
child = child.getNextSibling();
}
// end the element if it is not a group
if (!Boolean.TRUE.equals(element.getUserData(IS_GROUP_ELEMENT))) {
if (!pendingStackUpdate) {
pop();
}
if (!empty) {
if (isParent && indentationEnabled) {
newLine();
}
out.writeEndElement();
}
}
}
/*
* (non-Javadoc)
* @see org.beanio.stream.RecordWriter#flush()
*/
public void flush() throws IOException {
try {
out.flush();
}
catch (XMLStreamException e) {
throw (IOException) new IOException(e.getMessage()).initCause(e);
}
}
/*
* (non-Javadoc)
* @see org.beanio.stream.RecordWriter#close()
*/
public void close() throws IOException {
try {
while (elementStack != null) {
endElement();
}
out.writeEndDocument();
out.flush();
out.close();
// closing the XMLStreamWriter does not automatically
// close the underlying writer
writer.flush();
writer.close();
}
catch (XMLStreamException e) {
throw (IOException) new IOException(e.getMessage()).initCause(e);
}
}
/**
* Auto generates a prefix for a given namespace uri.
* @param uri the namespace uri
* @return the unique auto generated namespace prefix
*/
private String createNamespace(String uri) {
String prefix;
if (XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI.equals(uri)) {
prefix = Settings.getInstance().getProperty(Settings.DEFAULT_XSI_NAMESPACE_PREFIX);
}
else {
prefix = "ns" + (++namespaceCount);
}
while (namespaceMap.containsValue(prefix)) {
prefix = "ns" + (++namespaceCount);
}
namespaceMap.put(uri, prefix);
return prefix;
}
/**
* Terminates the current line and indents the start of the next line.
* @throws XMLStreamException
*/
private void newLine() throws XMLStreamException {
if (config.isIndentationEnabled()) {
out.writeCharacters(config.getLineSeparator());
for (int i=0,j=level; i state) {
state.put(getKey(namespace, OUTPUT_HEADER_KEY), outputHeader);
state.put(getKey(namespace, NAMESPACE_MAP_KEY), toToken(namespaceMap));
Integer n = (Integer) state.get(getKey(namespace, LEVEL_KEY));
int lastLevel = (n == null) ? 0 : n;
// remove previous stack items beyond the current level
for (int i=lastLevel; i>level; i--) {
String stackPrefix = namespace + ".s" + i;
state.remove(getKey(stackPrefix, STACK_ELEMENT_KEY));
state.remove(getKey(stackPrefix, STACK_NS_MAP_KEY));
}
int to = DELTA_ENABLED ? dirtyLevel : 0;
// update dirtied stack items up to the current level
ElementStack e = elementStack;
for (int i=level; i>to; i--) {
String stackPrefix = namespace + ".s" + i;
state.put(getKey(stackPrefix, STACK_ELEMENT_KEY), e.toToken());
String nsMapKey = getKey(stackPrefix, STACK_NS_MAP_KEY);
String token = toToken(e.getNamespaces());
if (token == null) {
state.remove(nsMapKey);
}
else {
state.put(nsMapKey, token);
}
e = elementStack.getParent();
}
dirtyLevel = level;
state.put(getKey(namespace, LEVEL_KEY), level);
}
/*
* (non-Javadoc)
* @see org.beanio.util.StatefulWriter#restoreState(java.lang.String, java.util.Map)
*/
public void restoreState(String namespace, Map state) throws IllegalStateException {
this.outputHeader = (Boolean) getRequired(namespace, OUTPUT_HEADER_KEY, state);
String key = getKey(namespace, NAMESPACE_MAP_KEY);
String token = (String) state.get(key);
if (token != null) {
this.namespaceMap = toMap(token, key);
this.namespaceCount = this.namespaceMap.size();
}
else {
this.namespaceCount = 0;
}
this.level = 0;
this.elementStack = null;
try {
out.flush();
this.suppressOutput = true;
int level = (Integer) getRequired(namespace, LEVEL_KEY, state);
for (int i=0; i state) {
key = getKey(namespace, key);
Object value = state.get(key);
if (value == null) {
throw new IllegalStateException("Missing state information for key '" + key + "'");
}
return value;
}
private String getKey(String namespace, String name) {
return namespace + "." + name;
}
/*
* Constructs a Map from a String of space delimited key-values pairings.
*/
private Map toMap(String token, String key) {
if (token == null) {
return null;
}
String[] s = token.trim().split(" ");
if (s.length % 2 != 0) {
throw new IllegalStateException("Invalid state information for key '" + key + "'");
}
Map map = new HashMap();
for (int n=0; n map) {
if (map == null || map.isEmpty()) {
return null;
}
boolean first = true;
StringBuilder token = new StringBuilder();
for (Map.Entry entry : map.entrySet()) {
if (first) {
first = false;
}
else {
token.append(" ");
}
token.append(entry.getKey());
token.append(" ");
token.append(entry.getValue());
}
return token.toString();
}
}