blob: 1dc26905c12208dacb51be7ac9128efe467fdb6f [file] [log] [blame]
/*
* Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code 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
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.internal.util.xml.impl;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.UnsupportedCharsetException;
import jdk.internal.util.xml.XMLStreamException;
import jdk.internal.util.xml.XMLStreamWriter;
/**
* Implementation of a reduced version of XMLStreamWriter
*
* @author Joe Wang
*/
public class XMLStreamWriterImpl implements XMLStreamWriter {
//Document state
static final int STATE_XML_DECL = 1;
static final int STATE_PROLOG = 2;
static final int STATE_DTD_DECL = 3;
static final int STATE_ELEMENT = 4;
//Element state
static final int ELEMENT_STARTTAG_OPEN = 10;
static final int ELEMENT_STARTTAG_CLOSE = 11;
static final int ELEMENT_ENDTAG_OPEN = 12;
static final int ELEMENT_ENDTAG_CLOSE = 13;
public static final char CLOSE_START_TAG = '>';
public static final char OPEN_START_TAG = '<';
public static final String OPEN_END_TAG = "</";
public static final char CLOSE_END_TAG = '>';
public static final String START_CDATA = "<![CDATA[";
public static final String END_CDATA = "]]>";
public static final String CLOSE_EMPTY_ELEMENT = "/>";
public static final String ENCODING_PREFIX = "&#x";
public static final char SPACE = ' ';
public static final char AMPERSAND = '&';
public static final char DOUBLEQUOT = '"';
public static final char SEMICOLON = ';';
//current state
private int _state = 0;
private Element _currentEle;
private XMLWriter _writer;
private String _encoding;
/**
* This flag can be used to turn escaping off for content. It does
* not apply to attribute content.
*/
boolean _escapeCharacters = true;
//pretty print by default
private boolean _doIndent = true;
//The system line separator for writing out line breaks.
private char[] _lineSep =
System.getProperty("line.separator").toCharArray();
public XMLStreamWriterImpl(OutputStream os) throws XMLStreamException {
this(os, XMLStreamWriter.DEFAULT_ENCODING);
}
public XMLStreamWriterImpl(OutputStream os, String encoding)
throws XMLStreamException
{
Charset cs = null;
if (encoding == null) {
_encoding = XMLStreamWriter.DEFAULT_ENCODING;
} else {
try {
cs = getCharset(encoding);
} catch (UnsupportedEncodingException e) {
throw new XMLStreamException(e);
}
this._encoding = encoding;
}
_writer = new XMLWriter(os, encoding, cs);
}
/**
* Write the XML Declaration. Defaults the XML version to 1.0, and the
* encoding to utf-8.
*
* @throws XMLStreamException
*/
public void writeStartDocument() throws XMLStreamException {
writeStartDocument(_encoding, XMLStreamWriter.DEFAULT_XML_VERSION);
}
/**
* Write the XML Declaration. Defaults the encoding to utf-8
*
* @param version version of the xml document
* @throws XMLStreamException
*/
public void writeStartDocument(String version) throws XMLStreamException {
writeStartDocument(_encoding, version, null);
}
/**
* Write the XML Declaration. Note that the encoding parameter does not set
* the actual encoding of the underlying output. That must be set when the
* instance of the XMLStreamWriter is created
*
* @param encoding encoding of the xml declaration
* @param version version of the xml document
* @throws XMLStreamException If given encoding does not match encoding of the
* underlying stream
*/
public void writeStartDocument(String encoding, String version) throws XMLStreamException {
writeStartDocument(encoding, version, null);
}
/**
* Write the XML Declaration. Note that the encoding parameter does not set
* the actual encoding of the underlying output. That must be set when the
* instance of the XMLStreamWriter is created
*
* @param encoding encoding of the xml declaration
* @param version version of the xml document
* @param standalone indicate if the xml document is standalone
* @throws XMLStreamException If given encoding does not match encoding of the
* underlying stream
*/
public void writeStartDocument(String encoding, String version, String standalone)
throws XMLStreamException
{
if (_state > 0) {
throw new XMLStreamException("XML declaration must be as the first line in the XML document.");
}
_state = STATE_XML_DECL;
String enc = encoding;
if (enc == null) {
enc = _encoding;
} else {
//check if the encoding is supported
try {
getCharset(encoding);
} catch (UnsupportedEncodingException e) {
throw new XMLStreamException(e);
}
}
if (version == null) {
version = XMLStreamWriter.DEFAULT_XML_VERSION;
}
_writer.write("<?xml version=\"");
_writer.write(version);
_writer.write(DOUBLEQUOT);
if (enc != null) {
_writer.write(" encoding=\"");
_writer.write(enc);
_writer.write(DOUBLEQUOT);
}
if (standalone != null) {
_writer.write(" standalone=\"");
_writer.write(standalone);
_writer.write(DOUBLEQUOT);
}
_writer.write("?>");
writeLineSeparator();
}
/**
* 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 XMLStreamException
*/
public void writeDTD(String dtd) throws XMLStreamException {
if (_currentEle != null && _currentEle.getState() == ELEMENT_STARTTAG_OPEN) {
closeStartTag();
}
_writer.write(dtd);
writeLineSeparator();
}
/**
* Writes a start tag to the output.
* @param localName local name of the tag, may not be null
* @throws XMLStreamException
*/
public void writeStartElement(String localName) throws XMLStreamException {
if (localName == null || localName.length() == 0) {
throw new XMLStreamException("Local Name cannot be null or empty");
}
_state = STATE_ELEMENT;
if (_currentEle != null && _currentEle.getState() == ELEMENT_STARTTAG_OPEN) {
closeStartTag();
}
_currentEle = new Element(_currentEle, localName, false);
openStartTag();
_writer.write(localName);
}
/**
* Writes an empty element tag to the output
* @param localName local name of the tag, may not be null
* @throws XMLStreamException
*/
public void writeEmptyElement(String localName) throws XMLStreamException {
if (_currentEle != null && _currentEle.getState() == ELEMENT_STARTTAG_OPEN) {
closeStartTag();
}
_currentEle = new Element(_currentEle, localName, true);
openStartTag();
_writer.write(localName);
}
/**
* Writes an attribute to the output stream without a prefix.
* @param localName the local name of the attribute
* @param value the value of the attribute
* @throws IllegalStateException if the current state does not allow Attribute writing
* @throws XMLStreamException
*/
public void writeAttribute(String localName, String value) throws XMLStreamException {
if (_currentEle.getState() != ELEMENT_STARTTAG_OPEN) {
throw new XMLStreamException(
"Attribute not associated with any element");
}
_writer.write(SPACE);
_writer.write(localName);
_writer.write("=\"");
writeXMLContent(
value,
true, // true = escapeChars
true); // true = escapeDoubleQuotes
_writer.write(DOUBLEQUOT);
}
public void writeEndDocument() throws XMLStreamException {
if (_currentEle != null && _currentEle.getState() == ELEMENT_STARTTAG_OPEN) {
closeStartTag();
}
/**
* close unclosed elements if any
*/
while (_currentEle != null) {
if (!_currentEle.isEmpty()) {
_writer.write(OPEN_END_TAG);
_writer.write(_currentEle.getLocalName());
_writer.write(CLOSE_END_TAG);
}
_currentEle = _currentEle.getParent();
}
}
public void writeEndElement() throws XMLStreamException {
if (_currentEle != null && _currentEle.getState() == ELEMENT_STARTTAG_OPEN) {
closeStartTag();
}
if (_currentEle == null) {
throw new XMLStreamException("No element was found to write");
}
if (_currentEle.isEmpty()) {
return;
}
_writer.write(OPEN_END_TAG);
_writer.write(_currentEle.getLocalName());
_writer.write(CLOSE_END_TAG);
writeLineSeparator();
_currentEle = _currentEle.getParent();
}
public void writeCData(String cdata) throws XMLStreamException {
if (cdata == null) {
throw new XMLStreamException("cdata cannot be null");
}
if (_currentEle != null && _currentEle.getState() == ELEMENT_STARTTAG_OPEN) {
closeStartTag();
}
_writer.write(START_CDATA);
_writer.write(cdata);
_writer.write(END_CDATA);
}
public void writeCharacters(String data) throws XMLStreamException {
if (_currentEle != null && _currentEle.getState() == ELEMENT_STARTTAG_OPEN) {
closeStartTag();
}
writeXMLContent(data);
}
public void writeCharacters(char[] data, int start, int len)
throws XMLStreamException {
if (_currentEle != null && _currentEle.getState() == ELEMENT_STARTTAG_OPEN) {
closeStartTag();
}
writeXMLContent(data, start, len, _escapeCharacters);
}
/**
* Close this XMLStreamWriter by closing underlying writer.
*/
public void close() throws XMLStreamException {
if (_writer != null) {
_writer.close();
}
_writer = null;
_currentEle = null;
_state = 0;
}
/**
* Flush this XMLStreamWriter by flushing underlying writer.
*/
public void flush() throws XMLStreamException {
if (_writer != null) {
_writer.flush();
}
}
/**
* Set the flag to indicate if the writer should add line separator
* @param doIndent
*/
public void setDoIndent(boolean doIndent) {
_doIndent = doIndent;
}
/**
* Writes XML content to underlying writer. Escapes characters unless
* escaping character feature is turned off.
*/
private void writeXMLContent(char[] content, int start, int length, boolean escapeChars)
throws XMLStreamException
{
if (!escapeChars) {
_writer.write(content, start, length);
return;
}
// Index of the next char to be written
int startWritePos = start;
final int end = start + length;
for (int index = start; index < end; index++) {
char ch = content[index];
if (!_writer.canEncode(ch)) {
_writer.write(content, startWritePos, index - startWritePos);
// Escape this char as underlying encoder cannot handle it
_writer.write(ENCODING_PREFIX);
_writer.write(Integer.toHexString(ch));
_writer.write(SEMICOLON);
startWritePos = index + 1;
continue;
}
switch (ch) {
case OPEN_START_TAG:
_writer.write(content, startWritePos, index - startWritePos);
_writer.write("&lt;");
startWritePos = index + 1;
break;
case AMPERSAND:
_writer.write(content, startWritePos, index - startWritePos);
_writer.write("&amp;");
startWritePos = index + 1;
break;
case CLOSE_START_TAG:
_writer.write(content, startWritePos, index - startWritePos);
_writer.write("&gt;");
startWritePos = index + 1;
break;
}
}
// Write any pending data
_writer.write(content, startWritePos, end - startWritePos);
}
private void writeXMLContent(String content) throws XMLStreamException {
if ((content != null) && (content.length() > 0)) {
writeXMLContent(content,
_escapeCharacters, // boolean = escapeChars
false); // false = escapeDoubleQuotes
}
}
/**
* Writes XML content to underlying writer. Escapes characters unless
* escaping character feature is turned off.
*/
private void writeXMLContent(
String content,
boolean escapeChars,
boolean escapeDoubleQuotes)
throws XMLStreamException
{
if (!escapeChars) {
_writer.write(content);
return;
}
// Index of the next char to be written
int startWritePos = 0;
final int end = content.length();
for (int index = 0; index < end; index++) {
char ch = content.charAt(index);
if (!_writer.canEncode(ch)) {
_writer.write(content, startWritePos, index - startWritePos);
// Escape this char as underlying encoder cannot handle it
_writer.write(ENCODING_PREFIX);
_writer.write(Integer.toHexString(ch));
_writer.write(SEMICOLON);
startWritePos = index + 1;
continue;
}
switch (ch) {
case OPEN_START_TAG:
_writer.write(content, startWritePos, index - startWritePos);
_writer.write("&lt;");
startWritePos = index + 1;
break;
case AMPERSAND:
_writer.write(content, startWritePos, index - startWritePos);
_writer.write("&amp;");
startWritePos = index + 1;
break;
case CLOSE_START_TAG:
_writer.write(content, startWritePos, index - startWritePos);
_writer.write("&gt;");
startWritePos = index + 1;
break;
case DOUBLEQUOT:
_writer.write(content, startWritePos, index - startWritePos);
if (escapeDoubleQuotes) {
_writer.write("&quot;");
} else {
_writer.write(DOUBLEQUOT);
}
startWritePos = index + 1;
break;
}
}
// Write any pending data
_writer.write(content, startWritePos, end - startWritePos);
}
/**
* marks open of start tag and writes the same into the writer.
*/
private void openStartTag() throws XMLStreamException {
_currentEle.setState(ELEMENT_STARTTAG_OPEN);
_writer.write(OPEN_START_TAG);
}
/**
* marks close of start tag and writes the same into the writer.
*/
private void closeStartTag() throws XMLStreamException {
if (_currentEle.isEmpty()) {
_writer.write(CLOSE_EMPTY_ELEMENT);
} else {
_writer.write(CLOSE_START_TAG);
}
if (_currentEle.getParent() == null) {
writeLineSeparator();
}
_currentEle.setState(ELEMENT_STARTTAG_CLOSE);
}
/**
* Write a line separator
* @throws XMLStreamException
*/
private void writeLineSeparator() throws XMLStreamException {
if (_doIndent) {
_writer.write(_lineSep, 0, _lineSep.length);
}
}
/**
* Returns a charset object for the specified encoding
* @param encoding
* @return a charset object
* @throws UnsupportedEncodingException if the encoding is not supported
*/
private Charset getCharset(String encoding) throws UnsupportedEncodingException {
if (encoding.equalsIgnoreCase("UTF-32")) {
throw new UnsupportedEncodingException("The basic XMLWriter does "
+ "not support " + encoding);
}
Charset cs;
try {
cs = Charset.forName(encoding);
} catch (IllegalCharsetNameException | UnsupportedCharsetException ex) {
throw new UnsupportedEncodingException(encoding);
}
return cs;
}
/*
* Start of Internal classes.
*
*/
protected class Element {
/**
* the parent element
*/
protected Element _parent;
/**
* The size of the stack.
*/
protected short _Depth;
/**
* indicate if an element is an empty one
*/
boolean _isEmptyElement = false;
String _localpart;
int _state;
/**
* Default constructor.
*/
public Element() {
}
/**
* @param parent the parent of the element
* @param localpart name of the element
* @param isEmpty indicate if the element is an empty one
*/
public Element(Element parent, String localpart, boolean isEmpty) {
_parent = parent;
_localpart = localpart;
_isEmptyElement = isEmpty;
}
public Element getParent() {
return _parent;
}
public String getLocalName() {
return _localpart;
}
/**
* get the state of the element
*/
public int getState() {
return _state;
}
/**
* Set the state of the element
*
* @param state the state of the element
*/
public void setState(int state) {
_state = state;
}
public boolean isEmpty() {
return _isEmptyElement;
}
}
}