org.codehaus.plexus.util.xml.pull.MXSerializer Maven / Gradle / Ivy
/* -*- c-basic-offset: 4; indent-tabs-mode: nil; -*- //------100-columns-wide------>|*/
// for license please see accompanying LICENSE.txt file (available also at http://www.xmlpull.org/)
package org.codehaus.plexus.util.xml.pull;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
/**
* Implementation of XmlSerializer interface from XmlPull V1 API. This implementation is optimized for performance and
* low memory footprint.
*
* Implemented features:
*
* - FEATURE_NAMES_INTERNED - when enabled all returned names (namespaces, prefixes) will be interned and it is
* required that all names passed as arguments MUST be interned
*
- FEATURE_SERIALIZER_ATTVALUE_USE_APOSTROPHE
*
*
* Implemented properties:
*
* - PROPERTY_SERIALIZER_INDENTATION
*
- PROPERTY_SERIALIZER_LINE_SEPARATOR
*
*/
public class MXSerializer
implements XmlSerializer
{
protected final static String XML_URI = "http://www.w3.org/XML/1998/namespace";
protected final static String XMLNS_URI = "http://www.w3.org/2000/xmlns/";
private static final boolean TRACE_SIZING = false;
protected final String FEATURE_SERIALIZER_ATTVALUE_USE_APOSTROPHE =
"http://xmlpull.org/v1/doc/features.html#serializer-attvalue-use-apostrophe";
protected final String FEATURE_NAMES_INTERNED = "http://xmlpull.org/v1/doc/features.html#names-interned";
protected final String PROPERTY_SERIALIZER_INDENTATION =
"http://xmlpull.org/v1/doc/properties.html#serializer-indentation";
protected final String PROPERTY_SERIALIZER_LINE_SEPARATOR =
"http://xmlpull.org/v1/doc/properties.html#serializer-line-separator";
protected final static String PROPERTY_LOCATION = "http://xmlpull.org/v1/doc/properties.html#location";
// properties/features
protected boolean namesInterned;
protected boolean attributeUseApostrophe;
protected String indentationString = null; // " ";
protected String lineSeparator = "\n";
protected String location;
protected Writer out;
protected int autoDeclaredPrefixes;
protected int depth = 0;
// element stack
protected String elNamespace[] = new String[2];
protected String elName[] = new String[elNamespace.length];
protected int elNamespaceCount[] = new int[elNamespace.length];
// namespace stack
protected int namespaceEnd = 0;
protected String namespacePrefix[] = new String[8];
protected String namespaceUri[] = new String[namespacePrefix.length];
protected boolean finished;
protected boolean pastRoot;
protected boolean setPrefixCalled;
protected boolean startTagIncomplete;
protected boolean doIndent;
protected boolean seenTag;
protected boolean seenBracket;
protected boolean seenBracketBracket;
// buffer output if needed to write escaped String see text(String)
private static final int BUF_LEN = Runtime.getRuntime().freeMemory() > 1000000L ? 8 * 1024 : 256;
protected char buf[] = new char[BUF_LEN];
protected static final String precomputedPrefixes[];
static
{
precomputedPrefixes = new String[32]; // arbitrary number ...
for ( int i = 0; i < precomputedPrefixes.length; i++ )
{
precomputedPrefixes[i] = ( "n" + i ).intern();
}
}
private boolean checkNamesInterned = false;
private void checkInterning( String name )
{
if ( namesInterned && name != name.intern() )
{
throw new IllegalArgumentException( "all names passed as arguments must be interned"
+ "when NAMES INTERNED feature is enabled" );
}
}
protected void reset()
{
location = null;
out = null;
autoDeclaredPrefixes = 0;
depth = 0;
// nullify references on all levels to allow it to be GCed
for ( int i = 0; i < elNamespaceCount.length; i++ )
{
elName[i] = null;
elNamespace[i] = null;
elNamespaceCount[i] = 2;
}
namespaceEnd = 0;
// NOTE: no need to intern() as all literal strings and string-valued constant expressions
// are interned. String literals are defined in 3.10.5 of the Java Language Specification
// just checking ...
// assert "xmlns" == "xmlns".intern();
// assert XMLNS_URI == XMLNS_URI.intern();
// TODO: how to prevent from reporting this namespace?
// this is special namespace declared for consistency with XML infoset
namespacePrefix[namespaceEnd] = "xmlns";
namespaceUri[namespaceEnd] = XMLNS_URI;
++namespaceEnd;
namespacePrefix[namespaceEnd] = "xml";
namespaceUri[namespaceEnd] = XML_URI;
++namespaceEnd;
finished = false;
pastRoot = false;
setPrefixCalled = false;
startTagIncomplete = false;
// doIntent is not changed
seenTag = false;
seenBracket = false;
seenBracketBracket = false;
}
protected void ensureElementsCapacity()
{
final int elStackSize = elName.length;
// assert (depth + 1) >= elName.length;
// we add at least one extra slot ...
final int newSize = ( depth >= 7 ? 2 * depth : 8 ) + 2; // = lucky 7 + 1 //25
if ( TRACE_SIZING )
{
System.err.println( getClass().getName() + " elStackSize " + elStackSize + " ==> " + newSize );
}
final boolean needsCopying = elStackSize > 0;
String[] arr = null;
// reuse arr local variable slot
arr = new String[newSize];
if ( needsCopying )
System.arraycopy( elName, 0, arr, 0, elStackSize );
elName = arr;
arr = new String[newSize];
if ( needsCopying )
System.arraycopy( elNamespace, 0, arr, 0, elStackSize );
elNamespace = arr;
final int[] iarr = new int[newSize];
if ( needsCopying )
{
System.arraycopy( elNamespaceCount, 0, iarr, 0, elStackSize );
}
else
{
// special initialization
iarr[0] = 0;
}
elNamespaceCount = iarr;
}
protected void ensureNamespacesCapacity()
{ // int size) {
// int namespaceSize = namespacePrefix != null ? namespacePrefix.length : 0;
// assert (namespaceEnd >= namespacePrefix.length);
// if(size >= namespaceSize) {
// int newSize = size > 7 ? 2 * size : 8; // = lucky 7 + 1 //25
final int newSize = namespaceEnd > 7 ? 2 * namespaceEnd : 8;
if ( TRACE_SIZING )
{
System.err.println( getClass().getName() + " namespaceSize " + namespacePrefix.length + " ==> " + newSize );
}
final String[] newNamespacePrefix = new String[newSize];
final String[] newNamespaceUri = new String[newSize];
if ( namespacePrefix != null )
{
System.arraycopy( namespacePrefix, 0, newNamespacePrefix, 0, namespaceEnd );
System.arraycopy( namespaceUri, 0, newNamespaceUri, 0, namespaceEnd );
}
namespacePrefix = newNamespacePrefix;
namespaceUri = newNamespaceUri;
// TODO use hashes for quick namespace->prefix lookups
// if( ! allStringsInterned ) {
// int[] newNamespacePrefixHash = new int[newSize];
// if(namespacePrefixHash != null) {
// System.arraycopy(
// namespacePrefixHash, 0, newNamespacePrefixHash, 0, namespaceEnd);
// }
// namespacePrefixHash = newNamespacePrefixHash;
// }
// prefixesSize = newSize;
// ////assert nsPrefixes.length > size && nsPrefixes.length == newSize
// }
}
@Override
public void setFeature( String name, boolean state )
throws IllegalArgumentException, IllegalStateException
{
if ( name == null )
{
throw new IllegalArgumentException( "feature name can not be null" );
}
if ( FEATURE_NAMES_INTERNED.equals( name ) )
{
namesInterned = state;
}
else if ( FEATURE_SERIALIZER_ATTVALUE_USE_APOSTROPHE.equals( name ) )
{
attributeUseApostrophe = state;
}
else
{
throw new IllegalStateException( "unsupported feature " + name );
}
}
@Override
public boolean getFeature( String name )
throws IllegalArgumentException
{
if ( name == null )
{
throw new IllegalArgumentException( "feature name can not be null" );
}
if ( FEATURE_NAMES_INTERNED.equals( name ) )
{
return namesInterned;
}
else if ( FEATURE_SERIALIZER_ATTVALUE_USE_APOSTROPHE.equals( name ) )
{
return attributeUseApostrophe;
}
else
{
return false;
}
}
// precomputed variables to simplify writing indentation
protected int offsetNewLine;
protected int indentationJump;
protected char[] indentationBuf;
protected int maxIndentLevel;
protected boolean writeLineSeparator; // should end-of-line be written
protected boolean writeIndentation; // is indentation used?
/**
* For maximum efficiency when writing indents the required output is pre-computed This is internal function that
* recomputes buffer after user requested changes.
*/
protected void rebuildIndentationBuf()
{
if ( doIndent == false )
return;
final int maxIndent = 65; // hardcoded maximum indentation size in characters
int bufSize = 0;
offsetNewLine = 0;
if ( writeLineSeparator )
{
offsetNewLine = lineSeparator.length();
bufSize += offsetNewLine;
}
maxIndentLevel = 0;
if ( writeIndentation )
{
indentationJump = indentationString.length();
maxIndentLevel = maxIndent / indentationJump;
bufSize += maxIndentLevel * indentationJump;
}
if ( indentationBuf == null || indentationBuf.length < bufSize )
{
indentationBuf = new char[bufSize + 8];
}
int bufPos = 0;
if ( writeLineSeparator )
{
for ( int i = 0; i < lineSeparator.length(); i++ )
{
indentationBuf[bufPos++] = lineSeparator.charAt( i );
}
}
if ( writeIndentation )
{
for ( int i = 0; i < maxIndentLevel; i++ )
{
for ( int j = 0; j < indentationString.length(); j++ )
{
indentationBuf[bufPos++] = indentationString.charAt( j );
}
}
}
}
// if(doIndent) writeIndent();
protected void writeIndent()
throws IOException
{
final int start = writeLineSeparator ? 0 : offsetNewLine;
final int level = ( depth > maxIndentLevel ) ? maxIndentLevel : depth;
out.write( indentationBuf, start, ( level * indentationJump ) + offsetNewLine );
}
@Override
public void setProperty( String name, Object value )
throws IllegalArgumentException, IllegalStateException
{
if ( name == null )
{
throw new IllegalArgumentException( "property name can not be null" );
}
if ( PROPERTY_SERIALIZER_INDENTATION.equals( name ) )
{
indentationString = (String) value;
}
else if ( PROPERTY_SERIALIZER_LINE_SEPARATOR.equals( name ) )
{
lineSeparator = (String) value;
}
else if ( PROPERTY_LOCATION.equals( name ) )
{
location = (String) value;
}
else
{
throw new IllegalStateException( "unsupported property " + name );
}
writeLineSeparator = lineSeparator != null && lineSeparator.length() > 0;
writeIndentation = indentationString != null && indentationString.length() > 0;
// optimize - do not write when nothing to write ...
doIndent = indentationString != null && ( writeLineSeparator || writeIndentation );
// NOTE: when indentationString == null there is no indentation
// (even though writeLineSeparator may be true ...)
rebuildIndentationBuf();
seenTag = false; // for consistency
}
@Override
public Object getProperty( String name )
throws IllegalArgumentException
{
if ( name == null )
{
throw new IllegalArgumentException( "property name can not be null" );
}
if ( PROPERTY_SERIALIZER_INDENTATION.equals( name ) )
{
return indentationString;
}
else if ( PROPERTY_SERIALIZER_LINE_SEPARATOR.equals( name ) )
{
return lineSeparator;
}
else if ( PROPERTY_LOCATION.equals( name ) )
{
return location;
}
else
{
return null;
}
}
private String getLocation()
{
return location != null ? " @" + location : "";
}
// this is special method that can be accessed directly to retrieve Writer serializer is using
public Writer getWriter()
{
return out;
}
@Override
public void setOutput( Writer writer )
{
reset();
out = writer;
}
@Override
public void setOutput( OutputStream os, String encoding )
throws IOException
{
if ( os == null )
throw new IllegalArgumentException( "output stream can not be null" );
reset();
if ( encoding != null )
{
out = new OutputStreamWriter( os, encoding );
}
else
{
out = new OutputStreamWriter( os );
}
}
@Override
public void startDocument( String encoding, Boolean standalone )
throws IOException
{
char apos = attributeUseApostrophe ? '\'' : '"';
if ( attributeUseApostrophe )
{
out.write( "" );
if ( writeLineSeparator )
{
out.write( lineSeparator );
}
}
@Override
public void endDocument()
throws IOException
{
// close all unclosed tag;
while ( depth > 0 )
{
endTag( elNamespace[depth], elName[depth] );
}
if ( writeLineSeparator )
{
out.write( lineSeparator );
}
// assert depth == 0;
// assert startTagIncomplete == false;
finished = pastRoot = startTagIncomplete = true;
out.flush();
}
@Override
public void setPrefix( String prefix, String namespace )
throws IOException
{
if ( startTagIncomplete )
closeStartTag();
// assert prefix != null;
// assert namespace != null;
if ( prefix == null )
{
prefix = "";
}
if ( !namesInterned )
{
prefix = prefix.intern(); // will throw NPE if prefix==null
}
else if ( checkNamesInterned )
{
checkInterning( prefix );
}
else if ( prefix == null )
{
throw new IllegalArgumentException( "prefix must be not null" + getLocation() );
}
// check that prefix is not duplicated ...
for ( int i = elNamespaceCount[depth]; i < namespaceEnd; i++ )
{
if ( prefix == namespacePrefix[i] )
{
throw new IllegalStateException( "duplicated prefix " + printable( prefix ) + getLocation() );
}
}
if ( !namesInterned )
{
namespace = namespace.intern();
}
else if ( checkNamesInterned )
{
checkInterning( namespace );
}
else if ( namespace == null )
{
throw new IllegalArgumentException( "namespace must be not null" + getLocation() );
}
if ( namespaceEnd >= namespacePrefix.length )
{
ensureNamespacesCapacity();
}
namespacePrefix[namespaceEnd] = prefix;
namespaceUri[namespaceEnd] = namespace;
++namespaceEnd;
setPrefixCalled = true;
}
protected String lookupOrDeclarePrefix( String namespace )
{
return getPrefix( namespace, true );
}
@Override
public String getPrefix( String namespace, boolean generatePrefix )
{
// assert namespace != null;
if ( !namesInterned )
{
// when String is interned we can do much faster namespace stack lookups ...
namespace = namespace.intern();
}
else if ( checkNamesInterned )
{
checkInterning( namespace );
// assert namespace != namespace.intern();
}
if ( namespace == null )
{
throw new IllegalArgumentException( "namespace must be not null" + getLocation() );
}
else if ( namespace.length() == 0 )
{
throw new IllegalArgumentException( "default namespace cannot have prefix" + getLocation() );
}
// first check if namespace is already in scope
for ( int i = namespaceEnd - 1; i >= 0; --i )
{
if ( namespace == namespaceUri[i] )
{
final String prefix = namespacePrefix[i];
// now check that prefix is still in scope
for ( int p = namespaceEnd - 1; p > i; --p )
{
if ( prefix == namespacePrefix[p] )
continue; // too bad - prefix is redeclared with different namespace
}
return prefix;
}
}
// so not found it ...
if ( !generatePrefix )
{
return null;
}
return generatePrefix( namespace );
}
private String generatePrefix( String namespace )
{
// assert namespace == namespace.intern();
while ( true )
{
++autoDeclaredPrefixes;
// fast lookup uses table that was pre-initialized in static{} ....
final String prefix =
autoDeclaredPrefixes < precomputedPrefixes.length ? precomputedPrefixes[autoDeclaredPrefixes]
: ( "n" + autoDeclaredPrefixes ).intern();
// make sure this prefix is not declared in any scope (avoid hiding in-scope prefixes)!
for ( int i = namespaceEnd - 1; i >= 0; --i )
{
if ( prefix == namespacePrefix[i] )
{
continue; // prefix is already declared - generate new and try again
}
}
// declare prefix
if ( namespaceEnd >= namespacePrefix.length )
{
ensureNamespacesCapacity();
}
namespacePrefix[namespaceEnd] = prefix;
namespaceUri[namespaceEnd] = namespace;
++namespaceEnd;
return prefix;
}
}
@Override
public int getDepth()
{
return depth;
}
@Override
public String getNamespace()
{
return elNamespace[depth];
}
@Override
public String getName()
{
return elName[depth];
}
@Override
public XmlSerializer startTag( String namespace, String name )
throws IOException
{
if ( startTagIncomplete )
{
closeStartTag();
}
seenBracket = seenBracketBracket = false;
if ( doIndent && depth > 0 && seenTag )
{
writeIndent();
}
seenTag = true;
setPrefixCalled = false;
startTagIncomplete = true;
++depth;
if ( ( depth + 1 ) >= elName.length )
{
ensureElementsCapacity();
}
//// assert namespace != null;
if ( checkNamesInterned && namesInterned )
checkInterning( namespace );
elNamespace[depth] = ( namesInterned || namespace == null ) ? namespace : namespace.intern();
// assert name != null;
// elName[ depth ] = name;
if ( checkNamesInterned && namesInterned )
checkInterning( name );
elName[depth] = ( namesInterned || name == null ) ? name : name.intern();
if ( out == null )
{
throw new IllegalStateException( "setOutput() must called set before serialization can start" );
}
out.write( '<' );
if ( namespace != null )
{
if ( namespace.length() > 0 )
{
// ALEK: in future make it as feature on serializer
String prefix = null;
if ( depth > 0 && ( namespaceEnd - elNamespaceCount[depth - 1] ) == 1 )
{
// if only one prefix was declared un-declare it if prefix is already declared on parent el with the
// same URI
String uri = namespaceUri[namespaceEnd - 1];
if ( uri == namespace || uri.equals( namespace ) )
{
String elPfx = namespacePrefix[namespaceEnd - 1];
// 2 == to skip predefined namespaces (xml and xmlns ...)
for ( int pos = elNamespaceCount[depth - 1] - 1; pos >= 2; --pos )
{
String pf = namespacePrefix[pos];
if ( pf == elPfx || pf.equals( elPfx ) )
{
String n = namespaceUri[pos];
if ( n == uri || n.equals( uri ) )
{
--namespaceEnd; // un-declare namespace
prefix = elPfx;
}
break;
}
}
}
}
if ( prefix == null )
{
prefix = lookupOrDeclarePrefix( namespace );
}
// assert prefix != null;
// make sure that default ("") namespace to not print ":"
if ( prefix.length() > 0 )
{
out.write( prefix );
out.write( ':' );
}
}
else
{
// make sure that default namespace can be declared
for ( int i = namespaceEnd - 1; i >= 0; --i )
{
if ( namespacePrefix[i] == "" )
{
final String uri = namespaceUri[i];
if ( uri == null )
{
// declare default namespace
setPrefix( "", "" );
}
else if ( uri.length() > 0 )
{
throw new IllegalStateException( "start tag can not be written in empty default namespace "
+ "as default namespace is currently bound to '" + uri + "'" + getLocation() );
}
break;
}
}
}
}
out.write( name );
return this;
}
@Override
public XmlSerializer attribute( String namespace, String name, String value )
throws IOException
{
if ( !startTagIncomplete )
{
throw new IllegalArgumentException( "startTag() must be called before attribute()" + getLocation() );
}
// assert setPrefixCalled == false;
out.write( ' ' );
//// assert namespace != null;
if ( namespace != null && namespace.length() > 0 )
{
// namespace = namespace.intern();
if ( !namesInterned )
{
namespace = namespace.intern();
}
else if ( checkNamesInterned )
{
checkInterning( namespace );
}
String prefix = lookupOrDeclarePrefix( namespace );
// assert( prefix != null);
if ( prefix.length() == 0 )
{
// needs to declare prefix to hold default namespace
// NOTE: attributes such as a='b' are in NO namespace
prefix = generatePrefix( namespace );
}
out.write( prefix );
out.write( ':' );
// if(prefix.length() > 0) {
// out.write(prefix);
// out.write(':');
// }
}
// assert name != null;
out.write( name );
out.write( '=' );
// assert value != null;
out.write( attributeUseApostrophe ? '\'' : '"' );
writeAttributeValue( value, out );
out.write( attributeUseApostrophe ? '\'' : '"' );
return this;
}
protected void closeStartTag()
throws IOException
{
if ( finished )
{
throw new IllegalArgumentException( "trying to write past already finished output" + getLocation() );
}
if ( seenBracket )
{
seenBracket = seenBracketBracket = false;
}
if ( startTagIncomplete || setPrefixCalled )
{
if ( setPrefixCalled )
{
throw new IllegalArgumentException( "startTag() must be called immediately after setPrefix()"
+ getLocation() );
}
if ( !startTagIncomplete )
{
throw new IllegalArgumentException( "trying to close start tag that is not opened" + getLocation() );
}
// write all namespace declarations!
writeNamespaceDeclarations();
out.write( '>' );
elNamespaceCount[depth] = namespaceEnd;
startTagIncomplete = false;
}
}
private void writeNamespaceDeclarations()
throws IOException
{
// int start = elNamespaceCount[ depth - 1 ];
for ( int i = elNamespaceCount[depth - 1]; i < namespaceEnd; i++ )
{
if ( doIndent && namespaceUri[i].length() > 40 )
{
writeIndent();
out.write( " " );
}
if ( namespacePrefix[i] != "" )
{
out.write( " xmlns:" );
out.write( namespacePrefix[i] );
out.write( '=' );
}
else
{
out.write( " xmlns=" );
}
out.write( attributeUseApostrophe ? '\'' : '"' );
// NOTE: escaping of namespace value the same way as attributes!!!!
writeAttributeValue( namespaceUri[i], out );
out.write( attributeUseApostrophe ? '\'' : '"' );
}
}
@Override
public XmlSerializer endTag( String namespace, String name )
throws IOException
{
// check that level is valid
//// assert namespace != null;
// if(namespace != null) {
// namespace = namespace.intern();
// }
seenBracket = seenBracketBracket = false;
if ( namespace != null )
{
if ( !namesInterned )
{
namespace = namespace.intern();
}
else if ( checkNamesInterned )
{
checkInterning( namespace );
}
}
if ( namespace != elNamespace[depth] )
{
throw new IllegalArgumentException( "expected namespace " + printable( elNamespace[depth] ) + " and not "
+ printable( namespace ) + getLocation() );
}
if ( name == null )
{
throw new IllegalArgumentException( "end tag name can not be null" + getLocation() );
}
if ( checkNamesInterned && namesInterned )
{
checkInterning( name );
}
if ( ( !namesInterned && !name.equals( elName[depth] ) ) || ( namesInterned && name != elName[depth] ) )
{
throw new IllegalArgumentException( "expected element name " + printable( elName[depth] ) + " and not "
+ printable( name ) + getLocation() );
}
if ( startTagIncomplete )
{
writeNamespaceDeclarations();
out.write( " />" ); // space is added to make it easier to work in XHTML!!!
--depth;
}
else
{
--depth;
// assert startTagIncomplete == false;
if ( doIndent && seenTag )
{
writeIndent();
}
out.write( "" );
if ( namespace != null && namespace.length() > 0 )
{
// TODO prefix should be already known from matching start tag ...
final String prefix = lookupOrDeclarePrefix( namespace );
// assert( prefix != null);
if ( prefix.length() > 0 )
{
out.write( prefix );
out.write( ':' );
}
}
out.write( name );
out.write( '>' );
}
namespaceEnd = elNamespaceCount[depth];
startTagIncomplete = false;
seenTag = true;
return this;
}
@Override
public XmlSerializer text( String text )
throws IOException
{
// assert text != null;
if ( startTagIncomplete || setPrefixCalled )
closeStartTag();
if ( doIndent && seenTag )
seenTag = false;
writeElementContent( text, out );
return this;
}
@Override
public XmlSerializer text( char[] buf, int start, int len )
throws IOException
{
if ( startTagIncomplete || setPrefixCalled )
closeStartTag();
if ( doIndent && seenTag )
seenTag = false;
writeElementContent( buf, start, len, out );
return this;
}
@Override
public void cdsect( String text )
throws IOException
{
if ( startTagIncomplete || setPrefixCalled || seenBracket )
closeStartTag();
if ( doIndent && seenTag )
seenTag = false;
out.write( "" );
}
@Override
public void entityRef( String text )
throws IOException
{
if ( startTagIncomplete || setPrefixCalled || seenBracket )
closeStartTag();
if ( doIndent && seenTag )
seenTag = false;
out.write( '&' );
out.write( text ); // escape?
out.write( ';' );
}
@Override
public void processingInstruction( String text )
throws IOException
{
if ( startTagIncomplete || setPrefixCalled || seenBracket )
closeStartTag();
if ( doIndent && seenTag )
seenTag = false;
out.write( "" );
out.write( text ); // escape?
out.write( "?>" );
}
@Override
public void comment( String text )
throws IOException
{
if ( startTagIncomplete || setPrefixCalled || seenBracket )
closeStartTag();
if ( doIndent && seenTag )
seenTag = false;
out.write( "" );
}
@Override
public void docdecl( String text )
throws IOException
{
if ( startTagIncomplete || setPrefixCalled || seenBracket )
closeStartTag();
if ( doIndent && seenTag )
seenTag = false;
out.write( "" );
}
@Override
public void ignorableWhitespace( String text )
throws IOException
{
if ( startTagIncomplete || setPrefixCalled || seenBracket )
closeStartTag();
if ( doIndent && seenTag )
seenTag = false;
if ( text.length() == 0 )
{
throw new IllegalArgumentException( "empty string is not allowed for ignorable whitespace"
+ getLocation() );
}
out.write( text ); // no escape?
}
@Override
public void flush()
throws IOException
{
if ( !finished && startTagIncomplete )
closeStartTag();
out.flush();
}
// --- utility methods
protected void writeAttributeValue( String value, Writer out )
throws IOException
{
// .[apostrophe and <, & escaped],
final char quot = attributeUseApostrophe ? '\'' : '"';
final String quotEntity = attributeUseApostrophe ? "'" : """;
int pos = 0;
for ( int i = 0; i < value.length(); i++ )
{
char ch = value.charAt( i );
if ( ch == '&' )
{
if ( i > pos )
out.write( value.substring( pos, i ) );
out.write( "&" );
pos = i + 1;
}
if ( ch == '<' )
{
if ( i > pos )
out.write( value.substring( pos, i ) );
out.write( "<" );
pos = i + 1;
}
else if ( ch == quot )
{
if ( i > pos )
out.write( value.substring( pos, i ) );
out.write( quotEntity );
pos = i + 1;
}
else if ( ch < 32 )
{
// in XML 1.0 only legal character are #x9 | #xA | #xD
// and they must be escaped otherwise in attribute value they are normalized to spaces
if ( ch == 13 || ch == 10 || ch == 9 )
{
if ( i > pos )
out.write( value.substring( pos, i ) );
out.write( "" );
out.write( Integer.toString( ch ) );
out.write( ';' );
pos = i + 1;
}
else
{
throw new IllegalStateException( "character " + Integer.toString( ch ) + " is not allowed in output"
+ getLocation() );
// in XML 1.1 legal are [#x1-#xD7FF]
// if(ch > 0) {
// if(i > pos) out.write(text.substring(pos, i));
// out.write("");
// out.write(Integer.toString(ch));
// out.write(';');
// pos = i + 1;
// } else {
// throw new IllegalStateException(
// "character zero is not allowed in XML 1.1 output"+getLocation());
// }
}
}
}
if ( pos > 0 )
{
out.write( value.substring( pos ) );
}
else
{
out.write( value ); // this is shortcut to the most common case
}
}
protected void writeElementContent( String text, Writer out )
throws IOException
{
// escape '<', '&', ']]>', <32 if necessary
int pos = 0;
for ( int i = 0; i < text.length(); i++ )
{
// TODO: check if doing char[] text.getChars() would be faster than getCharAt(i) ...
char ch = text.charAt( i );
if ( ch == ']' )
{
if ( seenBracket )
{
seenBracketBracket = true;
}
else
{
seenBracket = true;
}
}
else
{
if ( ch == '&' )
{
if ( i > pos )
out.write( text.substring( pos, i ) );
out.write( "&" );
pos = i + 1;
}
else if ( ch == '<' )
{
if ( i > pos )
out.write( text.substring( pos, i ) );
out.write( "<" );
pos = i + 1;
}
else if ( seenBracketBracket && ch == '>' )
{
if ( i > pos )
out.write( text.substring( pos, i ) );
out.write( ">" );
pos = i + 1;
}
else if ( ch < 32 )
{
// in XML 1.0 only legal character are #x9 | #xA | #xD
if ( ch == 9 || ch == 10 || ch == 13 )
{
// pass through
// } else if(ch == 13) { //escape
// if(i > pos) out.write(text.substring(pos, i));
// out.write("");
// out.write(Integer.toString(ch));
// out.write(';');
// pos = i + 1;
}
else
{
throw new IllegalStateException( "character " + Integer.toString( ch )
+ " is not allowed in output" + getLocation() );
// in XML 1.1 legal are [#x1-#xD7FF]
// if(ch > 0) {
// if(i > pos) out.write(text.substring(pos, i));
// out.write("");
// out.write(Integer.toString(ch));
// out.write(';');
// pos = i + 1;
// } else {
// throw new IllegalStateException(
// "character zero is not allowed in XML 1.1 output"+getLocation());
// }
}
}
if ( seenBracket )
{
seenBracketBracket = seenBracket = false;
}
}
}
if ( pos > 0 )
{
out.write( text.substring( pos ) );
}
else
{
out.write( text ); // this is shortcut to the most common case
}
}
protected void writeElementContent( char[] buf, int off, int len, Writer out )
throws IOException
{
// escape '<', '&', ']]>'
final int end = off + len;
int pos = off;
for ( int i = off; i < end; i++ )
{
final char ch = buf[i];
if ( ch == ']' )
{
if ( seenBracket )
{
seenBracketBracket = true;
}
else
{
seenBracket = true;
}
}
else
{
if ( ch == '&' )
{
if ( i > pos )
{
out.write( buf, pos, i - pos );
}
out.write( "&" );
pos = i + 1;
}
else if ( ch == '<' )
{
if ( i > pos )
{
out.write( buf, pos, i - pos );
}
out.write( "<" );
pos = i + 1;
}
else if ( seenBracketBracket && ch == '>' )
{
if ( i > pos )
{
out.write( buf, pos, i - pos );
}
out.write( ">" );
pos = i + 1;
}
else if ( ch < 32 )
{
// in XML 1.0 only legal character are #x9 | #xA | #xD
if ( ch == 9 || ch == 10 || ch == 13 )
{
// pass through
// } else if(ch == 13 ) { //if(ch == '\r') {
// if(i > pos) {
// out.write(buf, pos, i - pos);
// }
// out.write("");
// out.write(Integer.toString(ch));
// out.write(';');
// pos = i + 1;
}
else
{
throw new IllegalStateException( "character " + Integer.toString( ch )
+ " is not allowed in output" + getLocation() );
// in XML 1.1 legal are [#x1-#xD7FF]
// if(ch > 0) {
// if(i > pos) out.write(text.substring(pos, i));
// out.write("");
// out.write(Integer.toString(ch));
// out.write(';');
// pos = i + 1;
// } else {
// throw new IllegalStateException(
// "character zero is not allowed in XML 1.1 output"+getLocation());
// }
}
}
if ( seenBracket )
{
seenBracketBracket = seenBracket = false;
}
// assert seenBracketBracket == seenBracket == false;
}
}
if ( end > pos )
{
out.write( buf, pos, end - pos );
}
}
// simple utility method -- good for debugging
protected static final String printable( String s )
{
if ( s == null )
return "null";
StringBuilder retval = new StringBuilder( s.length() + 16 );
retval.append( "'" );
char ch;
for ( int i = 0; i < s.length(); i++ )
{
addPrintable( retval, s.charAt( i ) );
}
retval.append( "'" );
return retval.toString();
}
protected static final String printable( char ch )
{
StringBuilder retval = new StringBuilder();
addPrintable( retval, ch );
return retval.toString();
}
private static void addPrintable( StringBuilder retval, char ch )
{
switch ( ch )
{
case '\b':
retval.append( "\\b" );
break;
case '\t':
retval.append( "\\t" );
break;
case '\n':
retval.append( "\\n" );
break;
case '\f':
retval.append( "\\f" );
break;
case '\r':
retval.append( "\\r" );
break;
case '\"':
retval.append( "\\\"" );
break;
case '\'':
retval.append( "\\\'" );
break;
case '\\':
retval.append( "\\\\" );
break;
default:
if ( ch < 0x20 || ch > 0x7e )
{
final String ss = "0000" + Integer.toString( ch, 16 );
retval.append( "\\u" ).append( ss, ss.length() - 4, ss.length() );
}
else
{
retval.append( ch );
}
}
}
}