/*
* citygml4j - The Open Source Java API for CityGML
* https://github.com/citygml4j
*
* Copyright 2013-2017 Claus Nagel <claus.nagel@gmail.com>
*
* 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.citygml4j.util.xml;
import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Stack;
import javax.xml.XMLConstants;
import javax.xml.transform.stream.StreamResult;
import org.citygml4j.xml.CityGMLNamespaceContext;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.NamespaceSupport;
import org.xml.sax.helpers.XMLFilterImpl;
public class SAXWriter extends XMLFilterImpl {
private final String OPEN_COMMENT = "<!--";
private final String END_COMMENT = "-->";
private final String XML_DECL_ENCODING = " encoding=";
private final String XML_DECL_VERSION = " version=\"1.0\"";
private final String XML_DECL_STANDALONE = " standalone=\"yes\"";
private final String OPEN_XML_DECL = "<?xml";
private final String CLOSE_XML_DECL = "?>";
private final char CLOSE_START_TAG = '>';
private final char OPEN_START_TAG = '<';
private final String OPEN_END_TAG = "</";
private final char CLOSE_END_TAG = '>';
private final String OPEN_PI = "<?";
private final String CLOSE_PI = "?>";
private final String CLOSE_EMPTY_ELEMENT = "/>";
private final String SPACE = " ";
private final String LINE_SEPARATOR = System.getProperty("line.separator");
private final String SCHEMA_LOCATION_NS = XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI;
private final String SCHEMA_LOCATION = "schemaLocation";
private final String SCHEMA_LOCATION_NS_PREFIX = "xsi";
private Writer writer;
private CharsetEncoder charsetEncoder;
private String streamEncoding;
private CityGMLNamespaceContext userDefinedNS;
private NamespaceSupport reportedNS;
private LocalNamespaceContext localNS;
private boolean escapeCharacters = true;
private boolean writeEncoding = false;
private boolean writeXMLDecl = true;
private String indentString;
private String[] headerComment;
private HashMap<String, String> schemaLocations;
private int depth = 0;
private XMLContentType lastXMLContent;
private enum XMLContentType {
UNDEFINED,
START_ELEMENT,
END_ELEMENT,
CHARACTERS,
PI,
COMMENT
}
public SAXWriter() {
init();
}
public SAXWriter(StreamResult streamResult, String encoding) throws IOException {
this();
setOutput(streamResult, encoding);
}
public SAXWriter(OutputStream outputStream) throws IOException {
this(outputStream, null);
}
public SAXWriter(OutputStream outputStream, String encoding) throws IOException {
this();
setOutput(outputStream, encoding);
}
public SAXWriter(Writer writer) throws IOException {
this();
setOutput(writer);
}
private void init() {
userDefinedNS = new CityGMLNamespaceContext();
reportedNS = new NamespaceSupport();
localNS = new LocalNamespaceContext();
schemaLocations = new HashMap<String, String>();
lastXMLContent = XMLContentType.UNDEFINED;
}
public void reset() {
userDefinedNS.reset();
reportedNS.reset();
localNS.reset();
streamEncoding = null;
escapeCharacters = true;
writeEncoding = false;
writeXMLDecl = true;
indentString = null;
headerComment = null;
schemaLocations.clear();
depth = 0;
lastXMLContent = XMLContentType.UNDEFINED;
}
public void setOutput(StreamResult streamResult) throws IOException {
setOutput(streamResult, null);
}
public void setOutput(StreamResult streamResult, String encoding) throws IOException {
if (streamResult.getOutputStream() != null)
setOutput(streamResult.getOutputStream(), encoding);
else if (streamResult.getWriter() != null)
setOutput(streamResult.getWriter());
else if (streamResult.getSystemId() != null)
setOutput(new FileOutputStream(streamResult.getSystemId()), encoding);
}
public void setOutput(Writer writer) {
this.writer = writer;
if (writer instanceof OutputStreamWriter) {
this.writer = new BufferedWriter((OutputStreamWriter)writer);
String encoding = ((OutputStreamWriter)writer).getEncoding();
if (encoding != null) {
Charset charset = Charset.forName(encoding);
streamEncoding = charset.name();
writeEncoding = true;
if (!streamEncoding.equalsIgnoreCase("utf-8"))
charsetEncoder = charset.newEncoder();
}
}
}
public void setOutput(OutputStream outputStream) throws IOException {
setOutput(outputStream, null);
}
public void setOutput(OutputStream outputStream, String encoding) throws IOException {
if (encoding == null)
encoding = System.getProperty("file.encoding");
if (encoding != null) {
Charset charset = Charset.forName(encoding);
streamEncoding = charset.name();
writeEncoding = true;
writer = new BufferedWriter(new OutputStreamWriter(outputStream, encoding));
if (!streamEncoding.equalsIgnoreCase("utf-8"))
charsetEncoder = charset.newEncoder();
}
}
public Writer getOutputWriter() {
return writer;
}
public void flush() throws SAXException {
try {
if (writer != null)
writer.flush();
} catch (IOException e) {
throw new SAXException(e);
}
}
public void close() throws SAXException {
try {
if (writer != null) {
writer.flush();
writer.close();
}
} catch (IOException e) {
throw new SAXException(e);
}
writer = null;
charsetEncoder = null;
userDefinedNS.reset();
reportedNS.reset();
localNS.reset();
headerComment = null;
schemaLocations.clear();
}
public void setEscapeCharacters(boolean escapeCharacters) {
this.escapeCharacters = escapeCharacters;
}
public boolean getEscapeCharacters() {
return escapeCharacters;
}
public void setNamespaceContext(CityGMLNamespaceContext context) {
if (context == null)
throw new IllegalArgumentException("namespace context may not be null.");
userDefinedNS = context;
if (depth > 0) {
Iterator<String> iter = userDefinedNS.getNamespaceURIs();
while (iter.hasNext()) {
String userDefinedURI = iter.next();
localNS.declarePrefix(userDefinedNS.getPrefix(userDefinedURI), userDefinedURI);
}
}
}
public CityGMLNamespaceContext getNamespaceContext() {
return userDefinedNS;
}
public void setPrefix(String prefix, String uri) {
if (prefix == null)
throw new IllegalArgumentException("namespace prefix may not be null.");
if (uri == null)
throw new IllegalArgumentException("namespace URI may not be null.");
if (depth == 0)
userDefinedNS.setPrefix(prefix, uri);
localNS.declarePrefix(prefix, uri);
}
public String getPrefix(String uri) {
String prefix = userDefinedNS.getPrefix(uri);
if (prefix == null)
prefix = getReportedPrefix(uri);
return prefix;
}
public String getNamespaceURI(String prefix) {
String uri = userDefinedNS.getNamespaceURI(prefix);
if (uri.equals(XMLConstants.NULL_NS_URI))
uri = getReportedURI(prefix);
return uri;
}
public void setDefaultNamespace(String uri) {
if (uri == null)
throw new IllegalArgumentException("namespace URI may not be null.");
if (depth == 0)
userDefinedNS.setDefaultNamespace(uri);
localNS.declarePrefix(XMLConstants.DEFAULT_NS_PREFIX, uri);
}
public String getIndentString() {
return indentString;
}
public void setIndentString(String indentString) {
if (indentString == null)
throw new IllegalArgumentException("indentation string may not be null.");
this.indentString = indentString;
}
public boolean isWriteEncoding() {
return writeEncoding;
}
public void setWriteEncoding(boolean writeEncoding) {
this.writeEncoding = writeEncoding;
}
public boolean isWriteXMLDecl() {
return writeXMLDecl;
}
public void setWriteXMLDecl(boolean writeXMLDecl) {
this.writeXMLDecl = writeXMLDecl;
}
public void setHeaderComment(String... headerMessage) {
if (headerMessage == null)
throw new IllegalArgumentException("header comment may not be null.");
this.headerComment = headerMessage;
}
public String[] getHeaderComment() {
return headerComment;
}
public void unsetHeaderComment() {
headerComment = null;
}
public void setSchemaLocation(String namespaceURI, String schemaLocation) {
if (namespaceURI == null)
throw new IllegalArgumentException("namespace URI may not be null.");
if (schemaLocation == null)
throw new IllegalArgumentException("schema location may not be null.");
schemaLocations.put(namespaceURI, schemaLocation);
}
public String getSchemaLocation(String namespaceURI) {
return schemaLocations.get(namespaceURI);
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
try {
if (length > 0) {
if (lastXMLContent == XMLContentType.START_ELEMENT)
writer.write(CLOSE_START_TAG);
writeXMLContent(ch, start, length, escapeCharacters);
lastXMLContent = XMLContentType.CHARACTERS;
}
} catch (IOException e) {
throw new SAXException(e);
}
}
@Override
public void endDocument() throws SAXException {
// nothing to do
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
try {
depth--;
if (lastXMLContent == XMLContentType.START_ELEMENT)
writer.write(CLOSE_EMPTY_ELEMENT);
else {
if (lastXMLContent == XMLContentType.END_ELEMENT)
writeIndent();
writer.write(OPEN_END_TAG);
writeQName(localNS.getPrefix(uri), localName);
writer.write(CLOSE_END_TAG);
}
lastXMLContent = XMLContentType.END_ELEMENT;
localNS.popContext();
} catch (IOException e) {
throw new SAXException(e);
}
}
@Override
public void endPrefixMapping(String prefix) throws SAXException {
reportedNS.popContext();
}
@Override
public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
try {
if (lastXMLContent != XMLContentType.END_ELEMENT) {
if (length > 0 && lastXMLContent == XMLContentType.START_ELEMENT)
writer.write(CLOSE_START_TAG);
writeXMLContent(ch, start, length, escapeCharacters);
lastXMLContent = XMLContentType.CHARACTERS;
}
} catch (IOException e) {
throw new SAXException(e);
}
}
@Override
public void processingInstruction(String target, String data) throws SAXException {
try {
if (lastXMLContent == XMLContentType.START_ELEMENT) {
writer.write(CLOSE_START_TAG);
writeIndent();
}
if (target == null || data == null)
throw new SAXException("PI target cannot be null.");
writer.write(OPEN_PI);
writer.write(target);
writer.write(SPACE);
writer.write(data);
writer.write(CLOSE_PI);
writeIndent();
lastXMLContent = XMLContentType.PI;
} catch (IOException e) {
throw new SAXException(e);
}
}
@Override
public void startDocument() throws SAXException {
try {
if (depth == 0) {
if (writeXMLDecl) {
if (streamEncoding == null && writer instanceof OutputStreamWriter) {
streamEncoding = ((OutputStreamWriter)writer).getEncoding();
if (streamEncoding != null)
streamEncoding = Charset.forName(streamEncoding).name();
}
writer.write(OPEN_XML_DECL);
writer.write(XML_DECL_VERSION);
if (writeEncoding && streamEncoding != null) {
writer.write(XML_DECL_ENCODING);
writer.write("\"");
writer.write(streamEncoding);
writer.write("\"");
}
writer.write(XML_DECL_STANDALONE);
writer.write(CLOSE_XML_DECL);
writeIndent();
lastXMLContent = XMLContentType.PI;
}
if (headerComment != null) {
writeHeader(headerComment);
lastXMLContent = XMLContentType.COMMENT;
}
}
} catch (IOException e) {
throw new SAXException(e);
}
}
@Override
public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
try {
if (depth > 0) {
if (lastXMLContent == XMLContentType.START_ELEMENT)
writer.write(CLOSE_START_TAG);
writeIndent();
} else if (depth == 0) {
Iterator<String> iter = userDefinedNS.getNamespaceURIs();
while (iter.hasNext()) {
String userDefinedURI = iter.next();
localNS.declarePrefix(userDefinedNS.getPrefix(userDefinedURI), userDefinedURI);
}
}
writer.write(OPEN_START_TAG);
boolean writeLocalNS = false;
String prefix = localNS.getPrefix(uri);
if (prefix == null) {
prefix = getReportedPrefix(uri);
if (prefix == null)
throw new IllegalStateException("namespace URI " + uri + " is not bound to a prefix.");
writeLocalNS = true;
}
writeQName(prefix, localName);
if (depth == 0) {
writeDeclaredNamespace();
writeSchemaLocations();
}
if (writeLocalNS) {
localNS.declarePrefix(prefix, uri);
writeNamespace(prefix, uri);
}
writeAttributes(atts, uri);
lastXMLContent = XMLContentType.START_ELEMENT;
depth++;
} catch (IOException e) {
throw new SAXException(e);
}
}
@Override
public void startPrefixMapping(String prefix, String uri) throws SAXException {
reportedNS.pushContext();
if (getReportedPrefix(uri) == null)
reportedNS.declarePrefix(prefix, uri);
}
private String getReportedPrefix(String uri) {
String prefix = reportedNS.getPrefix(uri);
if (prefix == null && uri.equals(reportedNS.getURI(XMLConstants.DEFAULT_NS_PREFIX)))
prefix = XMLConstants.DEFAULT_NS_PREFIX;
return prefix;
}
private String getReportedURI(String prefix) {
return reportedNS.getURI(prefix);
}
private void writeAttributes(Attributes atts, String elementURI) throws SAXException {
try {
String localName, uri, prefix;
for (int i = 0; i < atts.getLength(); i++) {
localName = atts.getLocalName(i);
uri = atts.getURI(i);
prefix = null;
if (uri != null && uri.length() > 0) {
if (uri.equals(XMLConstants.XMLNS_ATTRIBUTE_NS_URI))
continue;
prefix = localNS.getPrefix(uri);
if (prefix == null) {
prefix = getReportedPrefix(uri);
localNS.declarePrefix(prefix, uri);
writeNamespace(prefix, uri);
}
}
writer.write(SPACE);
writeQName(prefix, localName);
writer.write("=\"");
writeXMLContent(atts.getValue(i), true, true);
writer.write('"');
}
} catch (IOException e) {
throw new SAXException(e);
}
}
private void writeQName(String prefix, String localName) throws SAXException {
try {
if (prefix != null && !prefix.equals(XMLConstants.DEFAULT_NS_PREFIX)) {
writer.write(prefix);
writer.write(':');
}
writer.write(localName);
} catch (IOException e) {
throw new SAXException(e);
}
}
private void writeDeclaredNamespace() throws SAXException {
Iterator<String> iter = userDefinedNS.getNamespaceURIs();
while (iter.hasNext()) {
String uri = iter.next();
String prefix = userDefinedNS.getPrefix(uri);
writeNamespace(prefix, uri);
}
}
private void writeNamespace(String prefix, String uri) throws SAXException {
if (prefix.equals(XMLConstants.XML_NS_PREFIX) && uri.equals(XMLConstants.XML_NS_URI))
return;
try {
writer.write(SPACE);
writer.write(XMLConstants.XMLNS_ATTRIBUTE);
if (prefix.length() > 0) {
writer.write(':');
writer.write(prefix);
}
writer.write("=\"");
writeXMLContent(uri, true, true);
writer.write('"');
} catch (IOException e) {
throw new SAXException(e);
}
}
public void writeSchemaLocations() throws SAXException {
if (!schemaLocations.isEmpty()) {
try {
String uri = SCHEMA_LOCATION_NS;
String prefix = localNS.getPrefix(uri);
if (prefix == null) {
prefix = SCHEMA_LOCATION_NS_PREFIX;
localNS.declarePrefix(prefix, uri);
writeNamespace(prefix, uri);
}
writer.write(SPACE);
writeQName(prefix, SCHEMA_LOCATION);
writer.write("=\"");
Iterator<Entry<String, String>> iter = schemaLocations.entrySet().iterator();
while (iter.hasNext()) {
Entry<String, String> entry = iter.next();
writeXMLContent(entry.getKey(), true, true);
writer.write(SPACE);
writeXMLContent(entry.getValue(), true, true);
if (iter.hasNext())
writer.write(SPACE);
}
writer.write('"');
} catch (IOException e) {
throw new SAXException(e);
}
}
}
public void writeHeader(String... data) throws SAXException {
try {
if (lastXMLContent == XMLContentType.START_ELEMENT) {
writer.write(CLOSE_START_TAG);
writeIndent();
}
if (data == null)
throw new SAXException("comment target cannot be null.");
for (String line : data) {
if (line == null)
continue;
writer.write(OPEN_COMMENT);
writer.write(SPACE);
writer.write(line);
writer.write(SPACE);
writer.write(END_COMMENT);
writeIndent();
}
lastXMLContent = XMLContentType.COMMENT;
} catch (IOException e) {
throw new SAXException(e);
}
}
private void writeIndent() throws SAXException {
if (indentString == null || indentString.length() == 0)
return;
if (lastXMLContent == XMLContentType.CHARACTERS)
return;
try {
writer.write(LINE_SEPARATOR);
for (int i = 0; i < depth; i++)
writer.write(indentString);
} catch (IOException e) {
throw new SAXException(e);
}
}
private void writeXMLContent(char[] content, int start, int length, boolean escapeChars) throws IOException {
if (!escapeChars) {
writer.write(content, start, length);
return;
}
int startWritePos = start;
final int end = start + length;
for (int index = start; index < end; index++) {
char ch = content[index];
if (charsetEncoder != null && !charsetEncoder.canEncode(ch)){
writer.write(content, startWritePos, index - startWritePos );
writer.write("");
writer.write(Integer.toHexString(ch));
writer.write(';');
startWritePos = index + 1;
continue;
}
switch (ch) {
case '<':
writer.write(content, startWritePos, index - startWritePos);
writer.write("<");
startWritePos = index + 1;
break;
case '&':
writer.write(content, startWritePos, index - startWritePos);
writer.write("&");
startWritePos = index + 1;
break;
case '>':
writer.write(content, startWritePos, index - startWritePos);
writer.write(">");
startWritePos = index + 1;
break;
}
}
writer.write(content, startWritePos, end - startWritePos);
}
private void writeXMLContent(String content, boolean escapeChars, boolean escapeDoubleQuotes) throws IOException {
if (!escapeChars) {
writer.write(content);
return;
}
int startWritePos = 0;
final int end = content.length();
for (int index = 0; index < end; index++) {
char ch = content.charAt(index);
if (charsetEncoder != null && !charsetEncoder.canEncode(ch)){
writer.write(content, startWritePos, index - startWritePos );
writer.write("");
writer.write(Integer.toHexString(ch));
writer.write(';');
startWritePos = index + 1;
continue;
}
switch (ch) {
case '<':
writer.write(content, startWritePos, index - startWritePos);
writer.write("<");
startWritePos = index + 1;
break;
case '&':
writer.write(content, startWritePos, index - startWritePos);
writer.write("&");
startWritePos = index + 1;
break;
case '>':
writer.write(content, startWritePos, index - startWritePos);
writer.write(">");
startWritePos = index + 1;
break;
case '"':
writer.write(content, startWritePos, index - startWritePos);
if (escapeDoubleQuotes) {
writer.write(""");
} else {
writer.write('"');
}
startWritePos = index + 1;
break;
}
}
writer.write(content, startWritePos, end - startWritePos);
}
private class LocalNamespaceContext {
Stack<LocalNamespaceMap> contexts;
LocalNamespaceContext() {
contexts = new Stack<LocalNamespaceMap>();
}
void pushContext() {
if (contexts.isEmpty() || contexts.peek().level != depth)
contexts.push(new LocalNamespaceMap(depth));
}
void popContext() {
if (!contexts.isEmpty() && contexts.peek().level == depth)
contexts.pop();
}
void reset() {
contexts.clear();
}
void declarePrefix(String prefix, String uri) {
pushContext();
contexts.peek().namespaces.put(uri, prefix);
}
String getPrefix(String uri) {
Iterator<LocalNamespaceMap> iter = contexts.iterator();
while (iter.hasNext()) {
LocalNamespaceMap context = iter.next();
String prefix = context.namespaces.get(uri);
if (prefix != null)
return prefix;
}
return null;
}
}
private class LocalNamespaceMap {
private HashMap<String, String> namespaces;
private int level;
LocalNamespaceMap(int level) {
this.level = level;
namespaces = new HashMap<String, String>();
}
}
}