/*
* Copyright 2013 Guidewire Software, Inc.
*/
package gw.internal.xml;
import gw.util.Stack;
import gw.xml.XmlSerializationOptions;
import java.awt.Color;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CodingErrorAction;
import java.util.Map;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
/**
* Writes XML to a stream.
*
*
*/
public class XMLWriter implements IXMLWriter {
private final OutputStreamWriter _out;
private final XmlSerializationOptions _options;
private boolean gotRoot = false; // got root tag already?
// element name stack for tracking element recursion
private final Stack<String> _elementNameStack = new Stack<String>();
private String _elementName = null; // the name of the current element
private boolean _gotValue = false; // does the current element have a value set
private boolean _gotSubelements = false; // does the current element have any subelements
private boolean _writeNewLine = false;
private static final SimpleAttributeSet PI_ATTRIBUTES = new SimpleAttributeSet();
private static final SimpleAttributeSet COMMENT_ATTRIBUTES = new SimpleAttributeSet();
private static final SimpleAttributeSet ATTRIBUTE_NAME_ATTRIBUTES = new SimpleAttributeSet();
private static final SimpleAttributeSet ATTRIBUTE_VALUE_ATTRIBUTES = new SimpleAttributeSet();
private static final SimpleAttributeSet ELEMENT_NAME_ATTRIBUTES = new SimpleAttributeSet();
static {
PI_ATTRIBUTES.addAttribute( StyleConstants.Italic, true );
ATTRIBUTE_NAME_ATTRIBUTES.addAttribute( StyleConstants.Foreground, Color.GREEN );
ATTRIBUTE_VALUE_ATTRIBUTES.addAttribute( StyleConstants.Foreground, Color.PINK );
COMMENT_ATTRIBUTES.addAttribute( StyleConstants.Italic, true );
ELEMENT_NAME_ATTRIBUTES.addAttribute( StyleConstants.Foreground, Color.CYAN );
}
XMLWriter(OutputStream out, XmlSerializationOptions options) throws IOException {
Charset charset = options.getEncoding() == null ? Charset.forName("UTF-8") : options.getEncoding();
CharsetEncoder encoder = charset.newEncoder();
encoder.onUnmappableCharacter(CodingErrorAction.REPORT);
encoder.onMalformedInput(CodingErrorAction.REPORT);
_out = new OutputStreamWriter(out, encoder);
_options = options;
try {
setAttributes( PI_ATTRIBUTES );
if (options.getXmlDeclaration()) {
if (options.getEncoding() != null) {
if (options.getEncoding().name().startsWith("UTF-16") && !options.getEncoding().name().equals("UTF-16")) {
_out.write(0xFEFF);
}
}
write("<?xml ");
setAttributes( ATTRIBUTE_NAME_ATTRIBUTES );
write("version=");
setAttributes( ATTRIBUTE_VALUE_ATTRIBUTES );
write(xmlEncode( "1.0", true ));
if (_options.getEncoding() != null) {
write(" ");
setAttributes( ATTRIBUTE_NAME_ATTRIBUTES );
write("encoding=");
setAttributes( ATTRIBUTE_VALUE_ATTRIBUTES );
if (options.getEncoding().name().startsWith("UTF-16")) {
write(xmlEncode("UTF-16", true));
}
else {
write(xmlEncode(options.getEncoding().name(), true));
}
}
setAttributes( PI_ATTRIBUTES );
write("?>");
}
}
finally {
setAttributes( null );
}
}
private void write(String str) throws IOException {
try {
_out.write(str);
}
catch (CharacterCodingException e) {
throw new RuntimeException("attempting to encode '" + str +"'", e);
}
}
private void setAttributes( SimpleAttributeSet attributes ) {
}
/**
* Writes the stylesheet element. This should be called immediately after the constructor.
*
* @param type The stylesheet type such as text/xsl.
* @param href The stylesheet location.
* @throws IOException if an I/O error occurs.
*/
public void writeStyleSheet(String type, String href) throws IOException {
if (_options.getPretty()) {
writeNewLine();
}
write("<?xml-stylesheet type=");
write(xmlEncode(type, true));
write(" href=");
write(xmlEncode(href, true));
write("?>");
}
/**
* Appends a newLine at the next opportunity.
* @throws IOException if an I/O error occurs.
*/
public void newLine() throws IOException {
_writeNewLine = true;
}
/**
* Adds a comment to the XML.
* @param comment The comment text to add to the XML.
* @throws IOException if an I/O error occurs.
*/
public void writeComment( String comment ) throws IOException {
if ( _options.getComments() && comment != null ) {
if (gotRoot) {
finishStartElement();
}
if (_options.getPretty()) {
writeNewLine();
writeIndent(0);
}
try {
setAttributes( COMMENT_ATTRIBUTES );
StringBuilder sb = new StringBuilder();
for ( int i = 0; i < comment.length(); i++ ) {
char ch = comment.charAt( i );
sb.append( ch );
if ( ch == '-' ) {
if ( i == comment.length() - 1 || comment.charAt( i + 1 ) == '-' ) {
sb.append( " " );
}
}
}
write("<!--" + sb + "-->");
}
finally {
setAttributes( null );
}
}
}
private void writeNewLine() throws IOException {
write(_options.getLineSeparator());
}
private void writeExplicitNewLineIfNecessary() throws IOException {
if (_writeNewLine) {
_writeNewLine = false;
write(_options.getLineSeparator());
}
}
/**
* Properly encodes user input for inclusion in an XML document.
* @param input the input to encode
* @param attribute is this for an attribute? (returned value will be pre-quoted)
* @return the XML-encoded input
*/
static String xmlEncode(String input, boolean attribute) {
if (input == null || input.length() == 0) {
return attribute ? "\"\"" : input;
}
StringBuilder output = new StringBuilder();
if (attribute) {
output.append(0); // reserve space for leading quote
}
char quoteChar = 0;
for (int i = 0; i < input.length(); i++) {
char ch = input.charAt(i);
switch (ch) {
case '<':
output.append("<");
break;
case '>':
output.append(">");
break;
case '&':
output.append("&");
break;
case '"':
if (attribute && quoteChar == '"') {
output.append(""");
}
else {
output.append(ch);
quoteChar = '\'';
}
break;
case '\'':
if (attribute && quoteChar == '\'') {
output.append("'");
}
else {
output.append(ch);
quoteChar = '"';
}
break;
case 0x0009: // tab
case 0x000A: // linefeed
case 0x000D: // carriage return
if (attribute) {
output.append("");
output.append((int) ch);
output.append(";");
}
else {
output.append(ch);
}
break;
default:
if (ch < 32 || ch >= 0xFFFE) {
// TODO dlank - allow the option of stripping invalid characters rather than throwing an exception
throw new IllegalArgumentException("UTF-16 Codepoint 0x" + Integer.toString(ch, 16) + " is not valid for XML content");
}
output.append(ch);
}
}
if (attribute) {
if (quoteChar == 0) {
quoteChar = '"';
}
output.setCharAt(0, quoteChar);
output.append(quoteChar);
}
return output.toString();
}
/**
* Starts a new element in the output document.
* @param name the element name
* @throws IOException if an I/O error occurs while writing to the stream
*/
public void startElement(String name) throws IOException {
if (name == null) {
throw new NullPointerException("name");
}
if (_elementName == null) {
if (gotRoot) {
throw new IllegalStateException("XML only allows one root element");
}
gotRoot = true;
writeExplicitNewLineIfNecessary();
}
else {
finishStartElement();
}
if (_options.getPretty()) {
writeNewLine();
writeIndent(0);
}
try {
setAttributes( ELEMENT_NAME_ATTRIBUTES );
write("<");
write(xmlEncode(name, false));
}
finally {
setAttributes( null );
}
_elementNameStack.push(_elementName);
_elementName = name;
_gotValue = false;
_gotSubelements = false;
}
private void writeIndent(int additionalLevels) throws IOException {
write(copy(_options.getIndent(), _elementNameStack.size() + additionalLevels));
}
/**
* Ends the most recently added element.
* @throws IOException if an I/O error occurs while writing to the stream
*/
public void endElement() throws IOException {
try {
setAttributes( ELEMENT_NAME_ATTRIBUTES );
if (_elementName == null) {
throw new IllegalStateException("No enclosing element for endElement()");
}
if (!_gotValue) {
write("/>");
}
else {
writeExplicitNewLineIfNecessary();
if (_options.getPretty() && _gotSubelements) {
writeNewLine();
// write indentation
write(copy(_options.getIndent(), (_elementNameStack.size() - 1)));
}
write("</");
write(xmlEncode(_elementName, false));
write(">");
}
_elementName = _elementNameStack.pop();
_gotValue = true;
_gotSubelements = true;
}
finally {
setAttributes( null );
}
}
/**
* Adds text to the current element.
* @param text the text to add
* @throws IOException if an I/O error occurs while writing to the stream
*/
public void addText(String text) throws IOException {
if (_elementName == null) {
throw new IllegalStateException("No enclosing element for addText()");
}
if ( text == null || text.length() == 0 ) {
return; // ignore call
}
finishStartElement();
write(xmlEncode(String.valueOf(text), false));
}
/**
* Adds an attribute pair to the current element. Cannot be called once any values or subelements have
* been added.
* @param attrName the attribute name
* @param attrValue the attribute value
* @throws IOException if an I/O error occurs while writing to the stream
*/
public void addAttribute(String attrName, String attrValue) throws IOException {
if (_elementName == null) {
throw new IllegalStateException("No enclosing element for addAttribute()");
}
if (_gotValue) {
throw new IllegalStateException("Attributes cannot be added once an element contains data");
}
if (_writeNewLine || _options.getAttributeNewLine() ) {
_writeNewLine = false;
write(_options.getLineSeparator());
writeIndent(_options.getAttributeIndent());
}
else {
write(" ");
}
try {
setAttributes( ATTRIBUTE_NAME_ATTRIBUTES );
write(xmlEncode(attrName, false) + "=");
setAttributes( ATTRIBUTE_VALUE_ATTRIBUTES );
write(xmlEncode(attrValue, true));
}
finally {
setAttributes( null );
}
}
/**
* Convenience method for starting an element, adding attributes and text,
* and ending the element in one call.
*
* @param name the element name
* @param attributes a Map of element attributes, can be null
* @param body the element text, can be null
* @throws IOException if an I/O error occurs while writing to the stream
*/
public void writeElement(String name, Map<String, String> attributes, String body) throws IOException {
assert name != null;
startElement(name);
if (attributes != null) {
for (Map.Entry<String, String> attribute : attributes.entrySet()) {
addAttribute(attribute.getKey(), attribute.getValue());
}
}
if (body != null) addText(body);
endElement();
}
/**
* Completes writing of the XML document. All started elements must be ended before calling this method.
* @throws IOException if an I/O error occurs while writing to the stream
*/
public void finish() throws IOException {
_out.flush();
if (_elementName != null) {
throw new IllegalStateException("Elements must be balanced before calling finish");
}
if (!gotRoot) {
throw new IllegalStateException("An XML document must have a root element");
}
}
/**
* Flushes the writing process by calling finish(), then closes the underlying output stream. Even if
* finish() fails, the outputstream will be closed. If this is not the desired behaviour, then make
* a call to finish() manually in order to ensure it succeeds.
* @throws IOException if an I/O error occurs while writing to the stream
*/
public void close() throws IOException {
try {
finish();
}
finally {
_out.close();
}
}
private void finishStartElement() throws IOException {
if (!_gotValue) {
try {
setAttributes( ELEMENT_NAME_ATTRIBUTES );
write(">");
}
finally {
setAttributes( null );
}
_gotValue = true;
}
}
/**
* Makes a concatenated copy of the input string <i>count</i> number of times.
* @param input the input string
* @param count the copy count
* @return the copied output
*/
public static String copy(String input, int count) {
assert input != null;
switch (count) {
case 0: return "";
case 1: return input;
default:
assert count >= 0;
StringBuilder output = new StringBuilder();
for (int i = 0; i < count; i++) {
output.append(input);
}
return output.toString();
}
}
/**
* Shorthand for if ( value != null ) { startElement( name ); addText( value ); endElement(); }.
* @param name the element name
* @param value the element value
* @throws IOException if an I/O error occurs
*/
public void addElement( String name, String value ) throws IOException {
startElement( name );
addText( value );
endElement();
}
/**
* Returns the XML writer options
* @return XML writer options
*/
@Override
public XmlSerializationOptions getWriterOptions() {
return _options;
}
}