/*
* Copyright 2004-2005 Revolution Systems 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.revolsys.record.io.format.xml;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.xml.namespace.QName;
import com.revolsys.datatype.DataTypes;
import com.revolsys.io.FileUtil;
import com.revolsys.util.Exceptions;
import com.revolsys.util.Property;
import com.revolsys.util.number.Doubles;
import com.revolsys.util.number.Numbers;
/**
* <p>
* The XmlWriter class is a subclass of {@link Writer} that provides additional
* methods to write XML documents.
* </p>
*
* @author Paul Austin
*/
public class XmlWriter extends Writer {
/**
* The TagConfiguration class is used to record the XML Namespace URIs defined
* for an XML element.
*
* @author Paul Austin
*/
private class TagConfiguration {
/** The namespaces defined on the element. */
private final List<String> attributeDefinedNamespaces = new ArrayList<>();
/** The QName of the current element. */
private final QName element;
private String tagDefinedNamespace;
/**
* Construct a new TagConfiguration
*
* @param element The QName of the current element.
*/
TagConfiguration(final QName element) {
this.element = element;
}
/**
* Add the namespace URI to the list of namespaces for the element.
*
* @param namespaceUri The namespace URI to add.
*/
public void addFieldDefinedNamespace(final String namespaceUri) {
this.attributeDefinedNamespaces.add(namespaceUri);
}
/**
* Get the current element.
*
* @return The current element.
*/
public QName getElement() {
return this.element;
}
/**
* Get the namespaces defined on the element.
*
* @return The namespaces defined on the element.
*/
public List<String> getFieldDefinedNamespaces() {
return this.attributeDefinedNamespaces;
}
public String getTagDefinedNamespace() {
return this.tagDefinedNamespace;
}
public void setTagDefinedNamespace(final String tagDefinedNamespace) {
this.tagDefinedNamespace = tagDefinedNamespace;
}
/**
* Return a string representation.
*
* @return The string representation.
*/
@Override
public String toString() {
return this.element.toString();
}
}
public static void writeAttributeContent(final Writer out, final String buffer) {
try {
final int lastIndex = buffer.length();
int index = 0;
String escapeString = null;
for (int i = 0; i < lastIndex; i++) {
final char ch = buffer.charAt(index);
switch (ch) {
case '&':
escapeString = "&";
break;
case '<':
escapeString = "<";
break;
case '>':
escapeString = ">";
break;
case '"':
escapeString = """;
break;
case 9:
escapeString = " ";
break;
case 10:
escapeString = "
";
break;
case 13:
escapeString = "
";
break;
default:
// Reject all other control characters
if (ch < 32) {
throw new IllegalStateException(
"character " + Integer.toString(ch) + " is not allowed in output");
}
break;
}
if (escapeString != null) {
if (i > index) {
out.write(buffer, index, i - index);
}
out.write(escapeString);
escapeString = null;
index = i + 1;
}
}
if (lastIndex > index) {
out.write(buffer, index, lastIndex - index);
}
} catch (final IOException e) {
throw Exceptions.wrap(e);
}
}
public static void writeElementContent(final Writer out, final char[] buffer, final int offest,
final int length) {
try {
int index = offest;
final int lastIndex = index + length;
String escapeString = null;
for (int i = index; i < lastIndex; i++) {
final char ch = buffer[i];
switch (ch) {
case '&':
escapeString = "&";
break;
case '<':
escapeString = "<";
break;
case '>':
escapeString = ">";
break;
case 9:
case 10:
case 13:
// Accept these control characters
break;
default:
// Reject all other control characters
if (ch < 32) {
throw new IllegalStateException(
"character " + Integer.toString(ch) + " is not allowed in output");
}
break;
}
if (escapeString != null) {
if (i > index) {
out.write(buffer, index, i - index);
}
out.write(escapeString);
escapeString = null;
index = i + 1;
}
}
if (lastIndex > index) {
out.write(buffer, index, lastIndex - index);
}
} catch (final IOException e) {
throw Exceptions.wrap(e);
}
}
public static void writeElementContent(final Writer out, final String buffer) {
if (buffer != null) {
writeElementContent(out, buffer, 0, buffer.length());
}
}
public static void writeElementContent(final Writer out, final String buffer, final int offest,
final int length) {
try {
int index = offest;
final int lastIndex = index + length;
String escapeString = null;
for (int i = index; i < lastIndex; i++) {
final char ch = buffer.charAt(i);
switch (ch) {
case '&':
escapeString = "&";
break;
case '<':
escapeString = "<";
break;
case '>':
escapeString = ">";
break;
case 9:
case 10:
case 13:
// Accept these control characters
break;
default:
// Reject all other control characters
if (ch < 32) {
throw new IllegalStateException(
"character " + Integer.toString(ch) + " is not allowed in output");
}
break;
}
if (escapeString != null) {
if (i > index) {
out.write(buffer, index, i - index);
}
out.write(escapeString);
escapeString = null;
index = i + 1;
}
}
if (lastIndex > index) {
out.write(buffer, index, lastIndex - index);
}
} catch (final IOException e) {
throw Exceptions.wrap(e);
}
}
/** True if an XML declaration can be written. */
private boolean canWriteXmlDeclaration = true;
/** Flag indicating if a DOCTYPE has been written. */
private boolean docTypeWritten = false;
/** Flag indicating if endDocument has been called. */
private boolean documentFinished = false;
/** Flag indicating content has been written for the current element. */
private boolean elementHasContent = false;
/** Flag indicating if an element has been started. */
private boolean elementsStarted = false;
/** The stack of open XML elements. */
private final LinkedList<TagConfiguration> elementStack = new LinkedList<>();
/** Flag indicating if endDocument is running has been called. */
private boolean endingDocument = false;
/** Flag indicating that the xml elements should be indented. */
private boolean indent;
private final Map<String, String> namespaceAliasMap = new LinkedHashMap<>();
/** The map of XML Namespace URIs to prefixes. */
private final Map<String, String> namespacePrefixMap = new LinkedHashMap<>();
/** The string of characters to use for a new line. */
private final String newLine = "\n";
/** The underlying writer to write to. */
private final Writer out;
private int prefixNum;
/** Flag indicating that XML namespaces should be written to the output. */
private final boolean useNamespaces;
private boolean writeNewLine = true;
/** Flag indicating that a start tag has been written by not closed. */
private boolean writingStartTag = false;
/** Flag indicating that an XML declaration has been written. */
private boolean xmlDeclarationWritten = false;
/**
* Construct a new XmlWriter.
*
* @param out The output stream to write to.
*/
public XmlWriter(final OutputStream out) {
this(out, true);
}
/**
* Construct a new XmlWriter that optionally ignores namespaces.
*
* @param out The output stream to write to.
* @param useNamespaces True if namespaces should be written, false if they
* should be ignored.
*/
public XmlWriter(final OutputStream out, final boolean useNamespaces) {
this(FileUtil.newUtf8Writer(out), useNamespaces);
}
/**
* Construct a new XmlWriter.
*
* @param out The writer to write to.
*/
public XmlWriter(final Writer out) {
this(out, true);
}
/**
* Construct a new XmlWriter that optionally ignores namespaces.
*
* @param out The writer to write to.
* @param useNamespaces True if namespaces should be written, false if they
* should be ignored.
*/
public XmlWriter(final Writer out, final boolean useNamespaces) {
this.out = out;
this.useNamespaces = useNamespaces;
}
/**
* Add an attribute to the current open tag.
*
* @param attribute The QName of the attribute.
* @param value The attribute value.
* @throws IOException If there was an error writing.
* @throws IllegalStateException If a start tag is not open.
*/
public void attribute(final QName attribute, final boolean value) {
attribute(attribute, String.valueOf(value));
}
/**
* Add an attribute to the current open tag.
*
* @param attribute The QName of the attribute.
* @param value The attribute value.
* @throws IOException If there was an error writing.
* @throws IllegalStateException If a start tag is not open.
*/
public void attribute(final QName attribute, final byte value) {
attribute(attribute, String.valueOf(value));
}
/**
* Add an attribute to the current open tag.
*
* @param attribute The QName of the attribute.
* @param value The attribute value.
* @throws IOException If there was an error writing.
* @throws IllegalStateException If a start tag is not open.
*/
public void attribute(final QName attribute, final double value) {
attribute(attribute, String.valueOf(value));
}
/**
* Add an attribute to the current open tag.
*
* @param attribute The QName of the attribute.
* @param value The attribute value.
* @throws IOException If there was an error writing.
* @throws IllegalStateException If a start tag is not open.
*/
public void attribute(final QName attribute, final float value) {
attribute(attribute, String.valueOf(value));
}
/**
* Add an attribute to the current open tag.
*
* @param attribute The QName of the attribute.
* @param value The attribute value.
* @throws IOException If there was an error writing.
* @throws IllegalStateException If a start tag is not open.
*/
public void attribute(final QName attribute, final int value) {
attribute(attribute, String.valueOf(value));
}
/**
* Add an attribute to the current open tag.
*
* @param attribute The QName of the attribute.
* @param value The attribute value.
* @throws IOException If there was an error writing.
* @throws IllegalStateException If a start tag is not open.
*/
public void attribute(final QName attribute, final long value) {
attribute(attribute, String.valueOf(value));
}
/**
* Add an attribute to the current open tag.
*
* @param attribute The QName of the attribute.
* @param value The attribute value.
* @throws IOException If there was an error writing.
* @throws IllegalStateException If a start tag is not open.
*/
public void attribute(final QName attribute, final Object value) {
if (value != null) {
final String string = DataTypes.toString(value);
attribute(attribute, string);
}
}
/**
* Add an attribute to the current open tag.
*
* @param attribute The QName of the attribute.
* @param value The attribute value.
* @throws IOException If there was an error writing.
* @throws IllegalStateException If a start tag is not open.
*/
public void attribute(final QName attribute, final String value) {
try {
if (value != null) {
checkWriteAttribute();
final String namespaceUri = attribute.getNamespaceURI();
if (namespaceUri.length() > 0) {
String prefix = this.namespacePrefixMap.get(namespaceUri);
if (prefix == null) {
prefix = attribute.getPrefix();
if (prefix == null || this.namespacePrefixMap.containsValue(prefix)) {
prefix = "p" + ++this.prefixNum;
}
this.namespacePrefixMap.put(namespaceUri, prefix);
this.elementStack.getFirst().addFieldDefinedNamespace(namespaceUri);
writeNamespaceAttribute(namespaceUri, prefix);
}
}
this.out.write(' ');
writeName(attribute, true);
this.out.write("=\"");
writeAttributeValue(value);
this.out.write('"');
}
} catch (final IOException e) {
throw Exceptions.wrap(e);
}
}
public void attribute(final String name, final Object value) {
if (value != null) {
final String string = DataTypes.toString(value);
attribute(name, string);
}
}
public void attribute(final String name, final String value) {
try {
if (Property.hasValue(value)) {
checkWriteAttribute();
this.out.write(' ');
this.out.write(name);
this.out.write("=\"");
writeAttributeValue(value);
this.out.write('"');
}
} catch (final IOException e) {
throw Exceptions.wrap(e);
}
}
/**
* Write the contents in a CDATA section. No escaping will be done on the data
* the text must not contain "]]>".
*
* @param text The text to wrap in a CDATA section
* @throws IOException If there was a problem writing the text
*/
public void cdata(final String text) {
try {
closeStartTag();
this.out.write("<![CDATA[");
this.out.write(text);
this.out.write("]]>");
setElementHasContent();
} catch (final IOException e) {
throw Exceptions.wrap(e);
}
}
/**
* Check that the document is not finished and can be written to.
*/
private void checkNotFinished() {
if (this.documentFinished) {
throw new IllegalStateException("Cannot write to a document after it has been finished");
}
}
/**
* Check that it is valid to write an attribute. A attribute declaration can
* only be written if there is a start tag that is open.
*
* @throws IllegalStateException If a start tag is not open.
*/
private void checkWriteAttribute() {
checkNotFinished();
if (!this.writingStartTag) {
throw new IllegalStateException("A start tag must be open to write an attribute");
}
}
/**
* Check that it is valid to write a DOCTYPE declaration. A DOCTYPE
* declaration can only be written if there has not been a DOCTYPE declaraion,
* an element has not been written and the document has not been finished.
*
* @throws IllegalStateException If a DOCTYPE cannot be written.
*/
private void checkWriteDocType() {
checkNotFinished();
if (this.elementsStarted) {
throw new IllegalStateException("Cannot create doc type after elements have been created");
}
if (this.docTypeWritten) {
throw new IllegalStateException("A document can only have one DOCTYPE declaration");
}
}
/**
* Check that it is valid to write an XML declaration. An XML declaration can
* only be written if there has not been a DOCTYPE declaraion or an element
* written.
*
* @throws IllegalStateException If an XML declaration cannot be written.
*/
private void checkWriteXmlDeclaration() {
checkNotFinished();
if (!this.canWriteXmlDeclaration) {
throw new IllegalStateException("An XML declaration must be the first item in a document");
}
if (this.xmlDeclarationWritten) {
throw new IllegalStateException("A document can only have one XML declaration");
}
}
/**
* Close the underlying output stream or writer.
*
* @throws IOException If the output stream or writer could not be closed.
*/
@Override
public void close() {
try {
this.out.flush();
this.out.close();
} catch (final IOException e) {
throw Exceptions.wrap(e);
}
}
/**
* Close the current open start tag. If a start tag is not open then no action
* will be taken.
*
* @throws IOException If there was an error writing.
*/
public void closeStartTag() {
try {
if (this.writingStartTag) {
checkNotFinished();
writeNamespaces();
this.out.write('>');
this.writingStartTag = false;
}
} catch (final IOException e) {
throw Exceptions.wrap(e);
}
}
public void closeStartTagLn() {
closeStartTag();
newLine();
}
/**
* Write an XML comment. The comment should not contain the string '--'.
*
* @param comment The comment to write
* @throws IOException If there was a problem writing the comment
*/
public void comment(final String comment) {
try {
closeStartTag();
this.out.write("<!--");
writeIndent();
this.out.write(comment);
writeEndIndent();
this.out.write("-->");
this.canWriteXmlDeclaration = false;
setElementHasContent();
} catch (final IOException e) {
throw Exceptions.wrap(e);
}
}
/**
* Write a DTD section. This string represents the entire doctypedecl
* production from the XML 1.0 specification.
*
* @param dtd The DTD to be written
* @throws IOException If there was a problem writing the declaration
*/
public void docType(final String dtd) {
try {
checkWriteDocType();
this.out.write(dtd);
this.canWriteXmlDeclaration = false;
this.docTypeWritten = true;
} catch (final IOException e) {
throw Exceptions.wrap(e);
}
}
/**
* Write a DOCTYPE declaration using a SYSTEM identifier.
*
* @param name The root element name
* @param systemId The system id
* @throws IOException If there was a problem writing the declaration
*/
public void docType(final String name, final String systemId) {
try {
checkWriteDocType();
this.out.write("<!DOCTYPE ");
this.out.write(name);
if (systemId != null) {
this.out.write(" SYSTEM \"");
this.out.write(systemId);
this.out.write('"');
}
this.out.write(">");
newLine();
this.canWriteXmlDeclaration = false;
this.docTypeWritten = true;
} catch (final IOException e) {
throw Exceptions.wrap(e);
}
}
/**
* Write a DOCTYPE declaration using a PUBLIC identifier.
*
* @param name The root element name.
* @param publicId The public id.
* @param systemId The system id.
* @throws IOException If there was a problem writing the declaration.
*/
public void docType(final String name, final String publicId, final String systemId) {
try {
checkWriteDocType();
this.out.write("<!DOCTYPE ");
this.out.write(name);
this.out.write(" PUBLIC \"");
this.out.write(publicId);
this.out.write("\" \"");
this.out.write(systemId);
this.out.write("\">");
this.canWriteXmlDeclaration = false;
this.docTypeWritten = true;
} catch (final IOException e) {
throw Exceptions.wrap(e);
}
}
/**
* Write the element with the specified content.
*
* @param element The QName of the tag.
* @param content The body context for the element.
* @throws IOException If there was a problem writing the element.
*/
public void element(final QName element, final Object content) {
startTag(element);
if (content != null) {
text(content.toString());
}
endTag(element);
}
public void element(final String local, final Object content) {
element(new QName(local), content);
}
public void elementLn(final QName element, final Object content) {
element(element, content);
newLine();
}
/**
* Write an empty tag.
*
* @param element The QName of the tag.
* @throws IOException If there was a problem writing the tag.
*/
public void emptyTag(final QName element) {
startTag(element);
endTag(element);
}
/**
* End a document, closing all tags and flushing the output.
*
* @throws IOException If the document could not be ended
*/
public void endDocument() {
this.endingDocument = true;
for (final Iterator<TagConfiguration> elements = this.elementStack.iterator(); elements
.hasNext();) {
final TagConfiguration tag = elements.next();
final QName element = tag.getElement();
endTag(element);
elements.remove();
}
this.documentFinished = true;
flush();
}
/**
* Write the end tag for the current element. If the element has no content it
* will be written as an empty tag.
*
* @throws IOException If there was a problem writing the element.
*/
public void endTag() {
endTag(getCurrentTag().getElement());
}
/**
* Write the end tag for an element. If the element has no content it will be
* written as an empty tag.
*
* @param element The QName of the tag.
* @throws IOException If there was a problem writing the element.
*/
public void endTag(final QName element) {
try {
checkNotFinished();
final TagConfiguration currentTag = getCurrentTag();
if (currentTag == null) {
throw new IllegalArgumentException("Cannot end tag " + element + " no open tag");
} else {
final QName currentElement = currentTag.getElement();
if (!element.equals(currentElement)) {
throw new IllegalArgumentException(
"Cannot end tag " + element + " expecting " + currentElement);
}
if (this.writingStartTag) {
writeNamespaces();
this.out.write(" />");
this.writingStartTag = false;
} else {
writeEndIndent();
this.out.write("</");
writeName(element, false);
this.out.write('>');
}
removeCurrentTag();
this.elementHasContent = false;
}
} catch (final IOException e) {
throw Exceptions.wrap(e);
}
}
public void endTag(final String localPart) {
endTag(new QName(localPart));
}
public void endTagLn(final QName element) {
endTag(element);
newLine();
}
/**
* Write the entity reference for the specified character.
*
* @param ch The character to write the entity for
* @throws IOException If there was a problem writing the entity
*/
public void entityRef(final char ch) {
try {
closeStartTag();
this.out.write("");
this.out.write(ch);
this.out.write(';');
setElementHasContent();
} catch (final IOException e) {
throw Exceptions.wrap(e);
}
}
/**
* Write the entity reference with the specified text.
*
* @param name The name of the entity
* @throws IOException If there was a problem writing the entity
*/
public void entityRef(final String name) {
try {
closeStartTag();
this.out.write('&');
this.out.write(name);
this.out.write(';');
setElementHasContent();
} catch (final IOException e) {
throw Exceptions.wrap(e);
}
}
/**
* Flush the output closing the current start tag.
*
* @throws IOException If there was an exception flushing the writer.
*/
@Override
public void flush() {
try {
if (!this.documentFinished) {
closeStartTag();
}
this.out.flush();
} catch (final IOException e) {
throw Exceptions.wrap(e);
}
}
/**
* Get the TagConfiguration of the current open tag.
*
* @return The TagConfiguration of the current open tag.
*/
private TagConfiguration getCurrentTag() {
if (this.elementStack.isEmpty()) {
return null;
} else {
return this.elementStack.getFirst();
}
}
/**
* Get the prefix for the XML Namespace URI.
*
* @param namespaceUri The XML Namespace URI.
* @return The prefix.
*/
public String getPrefix(final String namespaceUri) {
if (namespaceUri == null || namespaceUri.equals("")) {
return null;
} else {
return this.namespacePrefixMap.get(namespaceUri);
}
}
/**
* Get a new QName for the specified QName with the defined prefix for that
* XML Namespace.
*
* @param qName The QName without the prefix.
* @param attribute
* @return The QName with the prefix.
*/
private QName getQNameWithPrefix(final QName qName, final boolean attribute) {
final String namespaceUri = qName.getNamespaceURI();
if (namespaceUri.equals("")) {
return new QName(qName.getLocalPart());
}
String prefix = this.namespacePrefixMap.get(namespaceUri);
if (prefix == null) {
prefix = qName.getPrefix();
if (!attribute) {
getCurrentTag().setTagDefinedNamespace(namespaceUri);
}
this.namespacePrefixMap.put(namespaceUri, prefix);
return new QName(namespaceUri, qName.getLocalPart(), prefix);
}
if (prefix == qName.getPrefix()) {
return qName;
} else {
return new QName(namespaceUri, qName.getLocalPart(), prefix);
}
}
/**
* Get the flag indicating that the xml elements should be indented.
*
* @return The flag indicating that the xml elements should be indented.
*/
public boolean isIndent() {
return this.indent;
}
public boolean isWriteNewLine() {
return this.writeNewLine;
}
/**
* Write a newLine to the writer. If an XML start tag is open it will be
* closed before the new line is written.
*
* @throws IOException If there was an exception writing the new line.
*/
public void newLine() {
try {
closeStartTag();
this.out.write(this.newLine);
} catch (final IOException e) {
throw Exceptions.wrap(e);
}
}
/**
* Write the element with the specified content, if null xsi:nil attribute
* will be set.
*
* @param element The QName of the tag.
* @param content The body context for the element.
* @throws IOException If there was a problem writing the element.
*/
public void nillableElement(final QName element, final Object content) {
startTag(element);
if (content == null) {
attribute(XsiConstants.NIL, "true");
} else {
text(content.toString());
}
endTag(element);
}
/**
* Write an XML processing instruction.
*
* @param target The PI Target (must not be xml)
* @param value The value of the processing instruction
* @throws IOException If there was a problem writing the comment
*/
public void processingInstruction(final String target, final String value) {
try {
closeStartTag();
this.out.write("<?");
this.out.write(target);
if (value != null) {
this.out.write(" ");
this.out.write(value);
}
this.out.write("?>");
} catch (final IOException e) {
throw Exceptions.wrap(e);
}
}
/**
* Remove the current tag from the stack of open tags.
*/
private void removeCurrentTag() {
if (!this.endingDocument) {
final TagConfiguration tag = this.elementStack.removeFirst();
final Iterator<String> namespaceUris = tag.getFieldDefinedNamespaces().iterator();
while (namespaceUris.hasNext()) {
final String namespaceUri = namespaceUris.next();
this.namespacePrefixMap.remove(namespaceUri);
}
}
}
/**
* @param element
*/
private void setCurrentTag(final QName element) {
this.elementStack.addFirst(new TagConfiguration(element));
}
public void setElementHasContent() {
this.elementHasContent = true;
}
/**
* Set the flag indicating that the xml elements should be indented.
*
* @param indent The flag indicating that the xml elements should be indented.
*/
public void setIndent(final boolean indent) {
this.indent = indent;
}
public void setNamespaceAlias(final String namespaceUri, final String alias) {
this.namespaceAliasMap.put(namespaceUri, alias);
}
public void setPrefix(final QName typePath) {
setPrefix(typePath.getPrefix(), typePath.getNamespaceURI());
}
/**
* Set the prefix for the XML Namespace URI.
*
* @param prefix The prefix.
* @param namespaceUri The XML Namespace URI.
*/
public void setPrefix(final String prefix, final String namespaceUri) {
if (getPrefix(namespaceUri) == null) {
this.namespacePrefixMap.put(namespaceUri, prefix);
}
final TagConfiguration currentTag = getCurrentTag();
if (currentTag != null) {
currentTag.addFieldDefinedNamespace(namespaceUri);
}
}
public void setWriteNewLine(final boolean writeNewLine) {
this.writeNewLine = writeNewLine;
}
/**
* Start a document with an empty XML Declaration.
*
* @throws IOException If there was a problem writing the XML Declaration.
*/
public void startDocument() {
startDocument(null, null, null);
}
/**
* Start a document with an XML Declaration for the specified encoding.
*
* @param encoding The encoding for the document
* @throws IOException If there was a problem writing the XML Declaration.
*/
public void startDocument(final String encoding) {
startDocument(encoding, null, null);
}
/**
* Start a document with an XML Declaration for the specified encoding and the
* standalone flag.
*
* @param encoding The encoding for the document
* @param standalone The standalone flag
* @throws IOException If there was a problem writing the XML Declaration.
*/
public void startDocument(final String encoding, final boolean standalone) {
startDocument(encoding, Boolean.valueOf(standalone));
}
/**
* Start a document with an XML Declaration for the specified encoding and the
* standalone flag (null will omit the flag).
*
* @param encoding The encoding for the document
* @param standalone The standalone flag
* @throws IOException If there was a problem writing the XML Declaration.
*/
public void startDocument(final String encoding, final Boolean standalone) {
}
/**
* Start a document with an XML Declaration for the specified encoding.
*
* @param encoding The encoding for the document
* @param version The XML version.
* @throws IOException If there was a problem writing the XML Declaration.
*/
public void startDocument(final String encoding, final String version) {
startDocument(encoding, version, null);
}
/**
* Start a document with an XML Declaration for the specified encoding and the
* standalone flag (null will omit the flag).
*
* @param encoding The encoding for the document
* @param version The XML version.
* @param standalone The standalone flag
* @throws IOException If there was a problem writing the XML Declaration.
*/
public void startDocument(final String encoding, final String version, final Boolean standalone) {
try {
checkWriteXmlDeclaration();
if (version == null) {
this.out.write("<?xml version=\"1.0\"");
} else {
this.out.write("<?xml version=\"" + version + '"');
}
if (encoding != null) {
this.out.write(" encoding=\"");
this.out.write(encoding);
this.out.write('"');
}
if (standalone != null) {
if (standalone.booleanValue()) {
this.out.write(" standalone=\"yes\"");
} else {
this.out.write(" standalone=\"no\"");
}
}
this.out.write("?>\n");
this.xmlDeclarationWritten = true;
this.canWriteXmlDeclaration = false;
} catch (final IOException e) {
throw Exceptions.wrap(e);
}
}
/**
* Write the start tag for an element.
*
* @param element The QName of the tag.
* @throws IOException If there was a problem writing the element.
*/
public void startTag(final QName element) {
try {
checkNotFinished();
closeStartTag();
writeIndent();
this.writingStartTag = true;
setCurrentTag(element);
this.out.write('<');
writeName(element, false);
this.elementHasContent = false;
} catch (final IOException e) {
throw Exceptions.wrap(e);
}
}
public void startTag(final String localPart) {
startTag(new QName(localPart));
}
/**
* Write the start tag for an element.
*
* @param namespaceUri The namespace URI.
* @param localPart The local name.
* @throws IOException If there was a problem writing the element.
*/
public void startTag(final String namespaceUri, final String localPart) {
startTag(new QName(namespaceUri, localPart));
}
public void startTagLn(final QName element) {
startTag(element);
newLine();
}
public void text() {
closeStartTag();
setElementHasContent();
}
/**
* Write the boolean value as the content of a tag with special characters
* escaped.
*
* @param value The value.
*/
public void text(final boolean value) {
text(String.valueOf(value));
}
/**
* Write the int value as the content of a tag with special characters
* escaped.
*
* @param value The value.
*/
public void text(final char value) {
text(String.valueOf(value));
}
/**
* Write a portion of the character buffer to the outpu, escaping special
* characters.
*
* @param buffer The buffer to write.
* @param offset The starting offset in the buffer.
* @param length The number of characters to write.
*/
public void text(final char[] buffer, final int offset, final int length) {
closeStartTag();
writeElementContent(buffer, offset, length);
setElementHasContent();
}
/**
* Write the double value as the content of a tag with special characters
* escaped.
*
* @param value The value.
*/
public void text(final double value) {
final String text = Doubles.toString(value);
text(text);
}
/**
* Write the float value as the content of a tag with special characters
* escaped.
*
* @param value The value.
*/
public void text(final float value) {
final String text = Numbers.toString(value);
text(text);
}
/**
* Write the int value as the content of a tag with special characters
* escaped.
*
* @param value The value.
*/
public void text(final int value) {
text(String.valueOf(value));
}
/**
* Write the long value as the content of a tag with special characters
* escaped.
*
* @param value The value.
*/
public void text(final long value) {
text(String.valueOf(value));
}
/**
* Write the object value as the content of a tag with special characters
* escaped. If the value is null it will not be written.
*
* @param value The value.
*/
public void text(final Object value) {
if (value != null) {
if (value instanceof Number) {
final Number number = (Number)value;
final String text = Numbers.toString(number);
text(text);
} else {
text(value.toString());
}
}
}
/**
* Write the text string to the output, escaping special characters.
*
* @param text The text to write
*/
public void text(final String text) {
if (text != null) {
text(text.toCharArray(), 0, text.length());
}
}
public void textLn(final String text) {
text(text);
newLine();
}
/**
* Write an array of characters.
*
* @param buffer The character buffer to write.
* @throws IOException If an I/O exception occurs.
*/
@Override
public void write(final char[] buffer) {
write(buffer, 0, buffer.length);
}
/**
* Write a portion of an array of characters.
*
* @param buffer The character buffer to write.
* @param offset The starting offset in the buffer.
* @param length The number of characters to write.
* @throws IOException If an I/O exception occurs.
*/
@Override
public void write(final char[] buffer, final int offset, final int length) {
try {
closeStartTag();
this.out.write(buffer, offset, length);
setElementHasContent();
} catch (final IOException e) {
throw Exceptions.wrap(e);
}
}
/**
* Write a single character.
*
* @param character The character to write.
* @throws IOException If an I/O exception occurs.
*/
@Override
public void write(final int character) {
try {
closeStartTag();
this.out.write(character);
setElementHasContent();
} catch (final IOException e) {
throw Exceptions.wrap(e);
}
}
/**
* Write a String.
*
* @param string The String to write.
* @throws IOException If an I/O exception occurs.
*/
@Override
public void write(final String string) {
write(string.toCharArray(), 0, string.length());
}
/**
* Write a portion of a String.
*
* @param string The String to write.
* @param offset The starting offset in the buffer.
* @param length The number of characters to write.
* @throws IOException If an I/O exception occurs.
*/
@Override
public void write(final String string, final int offset, final int length) {
write(string.toCharArray(), 0, length);
}
/**
* Write content for an attribute value to the output, escaping the characters
* that are used within markup. This method will escape characters ' <', '>',
* '&', 9, 10, 13 and '"'. Note the XML 1.0 standard does allow '>' to be used
* unless it is part of "]]>" for simplicity it is allways escaped in this
* implementation.
*
* @param buffer The character buffer to write
* @param offset The offset in the character data to write
* @param length The number of characters to write.
* @throws IOException If an I/O exception occurs.
*/
protected void writeAttributeContent(final char[] buffer, final int offset, final int length) {
try {
final int lastIndex = offset + length;
int index = offset;
String escapeString = null;
for (int i = offset; i < lastIndex; i++) {
final char ch = buffer[i];
switch (ch) {
case '&':
escapeString = "&";
break;
case '<':
escapeString = "<";
break;
case '>':
escapeString = ">";
break;
case '"':
escapeString = """;
break;
case 9:
escapeString = " ";
break;
case 10:
escapeString = "
";
break;
case 13:
escapeString = "
";
break;
default:
// Reject all other control characters
if (ch < 32) {
throw new IllegalStateException(
"character " + Integer.toString(ch) + " is not allowed in output");
}
break;
}
if (escapeString != null) {
if (i > index) {
this.out.write(buffer, index, i - index);
}
this.out.write(escapeString);
escapeString = null;
index = i + 1;
}
}
if (lastIndex > index) {
this.out.write(buffer, index, lastIndex - index);
}
} catch (final IOException e) {
throw Exceptions.wrap(e);
}
}
/**
* Write the value of an attribute.
*
* @param value The value to write.
* @throws IOException If an I/O exception occurs.
*/
protected void writeAttributeValue(final String value) {
writeAttributeContent(value.toCharArray(), 0, value.length());
}
/**
* Write content for an element to the output, escaping the characters that
* are used within markup. This method will escape characters ' <', '>' and
* '&'. Note the XML 1.0 standard does allow '>' to be used unless it is part
* of "]]>" for simplicity it is allways escaped in this implementation.
*
* @param buffer The character buffer to write
* @param offest The offset in the character data to write
* @param length The number of characters to write.
* @throws IOException If an I/O exception occurs.
*/
protected void writeElementContent(final char[] buffer, final int offest, final int length) {
try {
int index = offest;
final int lastIndex = index + length;
String escapeString = null;
for (int i = index; i < lastIndex; i++) {
final char ch = buffer[i];
switch (ch) {
case '&':
escapeString = "&";
break;
case '<':
escapeString = "<";
break;
case '>':
escapeString = ">";
break;
case 9:
case 10:
case 13:
// Accept these control characters
break;
default:
// Reject all other control characters
if (ch < 32) {
throw new IllegalStateException(
"character " + Integer.toString(ch) + " is not allowed in output");
}
break;
}
if (escapeString != null) {
if (i > index) {
this.out.write(buffer, index, i - index);
}
this.out.write(escapeString);
escapeString = null;
index = i + 1;
}
}
if (lastIndex > index) {
this.out.write(buffer, index, lastIndex - index);
}
} catch (final IOException e) {
throw Exceptions.wrap(e);
}
}
/**
* Write the indent for the end of an element.
*
* @throws IOException If an I/O exception occurs.
*/
private void writeEndIndent() {
try {
if (!this.elementHasContent) {
if (this.writeNewLine) {
this.out.write(this.newLine);
}
if (this.indent) {
final int depth = this.elementStack.size() - 1;
for (int i = 0; i < depth; i++) {
this.out.write(" ");
}
}
}
} catch (final IOException e) {
throw Exceptions.wrap(e);
}
}
/**
* Write the indent for a child of an element.
*
* @throws IOException If an I/O exception occurs.
*/
private void writeIndent() {
try {
if (this.elementsStarted) {
if (this.writeNewLine) {
this.out.write(this.newLine);
}
if (this.indent) {
final int depth = this.elementStack.size();
for (int i = 0; i < depth; i++) {
this.out.write(" ");
}
}
} else {
this.elementsStarted = true;
}
} catch (final IOException e) {
throw Exceptions.wrap(e);
}
}
/**
* Write out a QName, if namespaces are used the name will be written with the
* prefix of the namespace.
*
* @param qName The QName to write
* @param attribute TODO
* @throws IOException If an I/O exception occurs.
*/
private void writeName(final QName qName, final boolean attribute) {
try {
if (this.useNamespaces) {
final String namespaceUri = qName.getNamespaceURI();
String prefix = this.namespacePrefixMap.get(namespaceUri);
final QName prefixedQName = getQNameWithPrefix(qName, attribute);
prefix = prefixedQName.getPrefix();
if (prefix.length() != 0) {
this.out.write(prefix);
this.out.write(':');
}
}
final String name = qName.getLocalPart();
this.out.write(name);
} catch (final IOException e) {
throw Exceptions.wrap(e);
}
}
public void writeNamespaceAttribute(final String namespaceUri, final String prefix) {
try {
if (prefix.length() == 0) {
this.out.write(" xmlns");
} else {
this.out.write(" xmlns:");
this.out.write(prefix);
}
this.out.write("=\"");
writeAttributeValue(namespaceUri);
this.out.write('"');
} catch (final IOException e) {
throw Exceptions.wrap(e);
}
}
/**
* Write the XML namespace declarations for an element.
*
* @throws IOException If an I/O exception occurs.
*/
private void writeNamespaces() {
if (this.useNamespaces) {
final TagConfiguration tag = getCurrentTag();
final Collection<String> namespaceUris;
if (this.elementStack.size() == 1) {
namespaceUris = this.namespacePrefixMap.keySet();
} else {
final String tagNamespace = tag.getTagDefinedNamespace();
if (tagNamespace == null) {
namespaceUris = Collections.emptyList();
} else {
namespaceUris = Collections.singletonList(tagNamespace);
}
}
for (final String namespaceUri : namespaceUris) {
final String prefix = this.namespacePrefixMap.get(namespaceUri);
final String alias = this.namespaceAliasMap.get(namespaceUri);
if (alias == null) {
writeNamespaceAttribute(namespaceUri, prefix);
} else {
writeNamespaceAttribute(alias, prefix);
}
}
}
}
public void xsiTypeAttribute(final QName xsiTagName) {
final String namespaceUri = xsiTagName.getNamespaceURI();
final String xsiName = xsiTagName.getLocalPart();
if (namespaceUri.length() > 0) {
String prefix = this.namespacePrefixMap.get(namespaceUri);
if (prefix == null) {
prefix = xsiTagName.getPrefix();
if (prefix == null || this.namespacePrefixMap.containsValue(prefix)) {
prefix = "p" + ++this.prefixNum;
}
this.namespacePrefixMap.put(namespaceUri, prefix);
writeNamespaceAttribute(namespaceUri, prefix);
}
attribute(XsiConstants.TYPE, prefix + ":" + xsiName);
} else {
attribute(XsiConstants.TYPE, xsiName);
}
}
}