/* * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software * Foundation. * * You should have received a copy of the GNU Lesser General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program 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 Lesser General Public License for more details. * * Copyright (c) 2001 - 2016 Object Refinery Ltd, Pentaho Corporation and Contributors.. All rights reserved. */ package org.pentaho.reporting.libraries.xmlns.writer; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.nio.charset.Charset; import java.nio.charset.CharsetEncoder; import java.util.HashMap; import java.util.Properties; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.pentaho.reporting.libraries.base.util.FastStack; import org.pentaho.reporting.libraries.base.util.ObjectUtilities; import org.pentaho.reporting.libraries.base.util.StringUtils; import org.pentaho.reporting.libraries.xmlns.common.AttributeList; import org.pentaho.reporting.libraries.xmlns.common.AttributeList.AttributeEntry; /** * A support class for writing XML files. * * @author Thomas Morgner */ public class XmlWriterSupport { private static final Log LOGGER = LogFactory.getLog( XmlWriterSupport.class ); /** * An internal state-management class containing the state for nested tags. */ private static class ElementLevel { private String namespace; private String prefix; private final String tagName; private final DeclaredNamespaces namespaces; /** * Creates a new ElementLevel object. * * @param namespace the namespace of the current tag (can be null). * @param prefix the namespace prefix of the current tag (can be null). * @param tagName the tagname (never null). * @param namespaces the collection of all currently known namespaces (never null). */ protected ElementLevel( final String namespace, final String prefix, final String tagName, final DeclaredNamespaces namespaces ) { if ( tagName == null ) { throw new NullPointerException(); } if ( namespaces == null ) { throw new NullPointerException(); } this.prefix = prefix; this.namespace = namespace; this.tagName = tagName; this.namespaces = namespaces; } /** * Creates a new ElementLevel with no namespace information. * * @param tagName the xml-tagname. * @param namespaces the currently known namespaces. */ protected ElementLevel( final String tagName, final DeclaredNamespaces namespaces ) { if ( tagName == null ) { throw new NullPointerException(); } if ( namespaces == null ) { throw new NullPointerException(); } this.namespaces = namespaces; this.tagName = tagName; } /** * Returns the defined namespace prefix for this entry. * * @return the namespace prefix. */ public String getPrefix() { return prefix; } /** * Returns the defined namespace uri for this entry. * * @return the namespace uri. */ public String getNamespace() { return namespace; } /** * Returns the tagname for this entry. * * @return the tagname. */ public String getTagName() { return tagName; } /** * Returns the map of defined namespace for this entry. * * @return the namespaces. */ public DeclaredNamespaces getNamespaces() { return namespaces; } } /** * A constant for controlling the indent function. */ public static final int OPEN_TAG_INCREASE = 1; /** * A constant for controlling the indent function. */ public static final int CLOSE_TAG_DECREASE = 2; /** * A constant for controlling the indent function. */ public static final int INDENT_ONLY = 3; /** * A constant for close. */ public static final boolean CLOSE = true; /** * A constant for open. */ public static final boolean OPEN = false; /** * A list of safe tags. */ private final TagDescription safeTags; /** * The indent level for that writer. */ private final FastStack openTags; /** * The indent string. */ private final String indentString; private boolean lineEmpty; private int additionalIndent; private boolean alwaysAddNamespace; private boolean assumeDefaultNamespace; private HashMap impliedNamespaces; private boolean writeFinalLinebreak; private boolean htmlCompatiblityMode; private final String lineSeparator; private CharsetEncoder charsetEncoder; /** * Default Constructor. The created XMLWriterSupport will not have no safe tags and starts with an indention level of * 0. */ public XmlWriterSupport() { this( new DefaultTagDescription(), " " ); } /** * Creates a new support instance. * * @param safeTags the tags that are safe for line breaks. * @param indentString the indent string. */ public XmlWriterSupport( final TagDescription safeTags, final String indentString ) { this( safeTags, indentString, StringUtils.getLineSeparator() ); } /** * Create a new XmlWriterSupport instance. * * @param safeTags the tags that are safe for line breaks. * @param indentString the indent string. * @param lineseparator the lineseparator that should be used for writing XML files. */ public XmlWriterSupport( final TagDescription safeTags, final String indentString, final String lineseparator ) { if ( indentString == null ) { throw new NullPointerException( "IndentString must not be null" ); } if ( safeTags == null ) { throw new NullPointerException( "SafeTags must not be null" ); } if ( lineseparator == null ) { throw new NullPointerException( "LineSeparator must not be null" ); } this.safeTags = safeTags; openTags = new FastStack(); this.indentString = indentString; lineEmpty = true; writeFinalLinebreak = true; lineSeparator = lineseparator; addImpliedNamespace( "http://www.w3.org/XML/1998/namespace", "xml" ); } /** * Checks, whether the HTML compatibility mode is enabled. In HTML compatibility mode, closed empty tags will have a * space between the tagname and the close-indicator. * * @return true, if the HTML compatiblity mode is enabled, false otherwise. */ public boolean isHtmlCompatiblityMode() { return htmlCompatiblityMode; } /** * Enables or disables the HTML Compatibility mode. In HTML compatibility mode, closed empty tags will have a space * between the tagname and the close-indicator. * * @param htmlCompatiblityMode true, if the HTML compatiblity mode is enabled, false otherwise. */ public void setHtmlCompatiblityMode( final boolean htmlCompatiblityMode ) { this.htmlCompatiblityMode = htmlCompatiblityMode; } /** * Checks, whether the XML writer should always add a namespace prefix to the attributes. The XML specification leaves * it up to the application on how to handle unqualified attributes. If this mode is enabled, all attributes will * always be fully qualified - which removed the ambugity but may not be compatible with simple, non namespace aware * parsers. * * @return true, if all attributes should be qualified, false otherwise. */ public boolean isAlwaysAddNamespace() { return alwaysAddNamespace; } /** * Defines, whether the XML writer should always add a namespace prefix to the attributes. The XML specification * leaves it up to the application on how to handle unqualified attributes. If this mode is enabled, all attributes * will always be fully qualified - which removed the ambuigity but may not be compatible with simple, non namespace * aware parsers. * * @param alwaysAddNamespace set to true, if all attributes should be qualified, false otherwise. */ public void setAlwaysAddNamespace( final boolean alwaysAddNamespace ) { this.alwaysAddNamespace = alwaysAddNamespace; } /** * Returns the indent level that should be added to the automaticly computed indentation. * * @return the indent level. */ public int getAdditionalIndent() { return additionalIndent; } /** * Defines the indent level that should be added to the automaticly computed indentation. * * @param additionalIndent the indent level. */ public void setAdditionalIndent( final int additionalIndent ) { this.additionalIndent = additionalIndent; } /** * Returns the line separator. * * @return the line separator. */ public String getLineSeparator() { return lineSeparator; } /** * Writes the XML declaration that usually appears at the top of every XML file. * * @param encoding the encoding that should be declared (this has to match the encoding of the writer, or funny things * may happen when parsing the xml file later). * @throws java.io.IOException if there is a problem writing to the character stream. */ public void writeXmlDeclaration( final Writer writer, final String encoding ) throws IOException { if ( encoding == null ) { writer.write( "<?xml version=\"1.0\"?>" ); writer.write( getLineSeparator() ); return; } writer.write( "<?xml version=\"1.0\" encoding=\"" ); writer.write( encoding ); writer.write( "\"?>" ); writer.write( getLineSeparator() ); setEncoding( encoding ); } public void setEncoding( final String encoding ) { final Charset charset = Charset.forName( encoding ); charsetEncoder = charset.newEncoder(); } /** * Writes an opening XML tag that has no attributes. * * @param w the writer. * @param namespaceUri the namespace URI for the element. * @param name the tag name. * @throws java.io.IOException if there is an I/O problem. */ public void writeTag( final Writer w, final String namespaceUri, final String name ) throws IOException { writeTag( w, namespaceUri, name, null, XmlWriterSupport.OPEN ); } /** * Writes a closing XML tag. * * @param w the writer. * @throws java.io.IOException if there is an I/O problem. */ public void writeCloseTag( final Writer w ) throws IOException { indentForClose( w ); final ElementLevel level = (ElementLevel) openTags.pop(); setLineEmpty( false ); w.write( "</" ); final String prefix = level.getPrefix(); if ( prefix != null ) { w.write( prefix ); w.write( ":" ); w.write( level.getTagName() ); } else { w.write( level.getTagName() ); } w.write( ">" ); doEndOfLine( w ); } /** * Writes a linebreak to the writer. * * @param writer the writer. * @throws IOException if there is a problem writing to the character stream. */ public void writeNewLine( final Writer writer ) throws IOException { if ( isLineEmpty() == false ) { writer.write( lineSeparator ); setLineEmpty( true ); } } /** * Checks, whether the currently generated line of text is empty. * * @return true, if the line is empty, false otherwise. */ public boolean isLineEmpty() { return lineEmpty; } /** * A marker flag to track, wether the current line is empty. This influences the indention. * * @param lineEmpty defines, whether the current line should be treated as empty line. */ public void setLineEmpty( final boolean lineEmpty ) { this.lineEmpty = lineEmpty; } /** * Writes an opening XML tag with an attribute/value pair. * * @param w the writer. * @param namespace the namespace URI for the element * @param name the tag name. * @param attributeName the attribute name. * @param attributeValue the attribute value. * @param close controls whether the tag is closed. * @throws java.io.IOException if there is an I/O problem. */ public void writeTag( final Writer w, final String namespace, final String name, final String attributeName, final String attributeValue, final boolean close ) throws IOException { if ( attributeName != null ) { final AttributeList attr = new AttributeList(); attr.setAttribute( namespace, attributeName, attributeValue ); writeTag( w, namespace, name, attr, close ); } else { writeTag( w, namespace, name, null, close ); } } /** * Adds an implied namespace to the document. Such a namespace is not explicitly declared, it is assumed that the * xml-parser knows the prefix by some other means. Using implied namespaces for standalone documents is almost always * a bad idea. * * @param uri the uri of the namespace. * @param prefix the defined prefix. */ public void addImpliedNamespace( final String uri, final String prefix ) { if ( openTags.isEmpty() == false ) { throw new IllegalStateException( "Cannot modify the implied namespaces in the middle of the processing" ); } if ( prefix == null ) { if ( impliedNamespaces == null ) { return; } impliedNamespaces.remove( uri ); } else { if ( impliedNamespaces == null ) { impliedNamespaces = new HashMap(); } impliedNamespaces.put( uri, prefix ); } } /** * Copies all currently declared namespaces of the given XmlWriterSupport instance as new implied namespaces into this * instance. * * @param writerSupport the Xml-writer from where to copy the declared namespaces. */ public void copyNamespaces( final XmlWriterSupport writerSupport ) { if ( openTags.isEmpty() == false ) { throw new IllegalStateException( "Cannot modify the implied namespaces in the middle of the processing" ); } if ( impliedNamespaces == null ) { impliedNamespaces = new HashMap(); } if ( writerSupport.openTags.isEmpty() == false ) { final ElementLevel parent = (ElementLevel) writerSupport.openTags.peek(); impliedNamespaces.putAll( parent.getNamespaces().getNamespaces() ); } if ( writerSupport.impliedNamespaces != null ) { impliedNamespaces.putAll( writerSupport.impliedNamespaces ); } } /** * Checks, whether the given URI is defined as valid namespace. * * @param uri the uri of the namespace. * @return true, if there's a namespace defined, false otherwise. */ public boolean isNamespaceDefined( final String uri ) { if ( impliedNamespaces != null ) { if ( impliedNamespaces.containsKey( uri ) ) { return true; } } if ( openTags.isEmpty() ) { return false; } final ElementLevel parent = (ElementLevel) openTags.peek(); return parent.getNamespaces().isNamespaceDefined( uri ); } /** * Checks, whether the given namespace prefix is defined. * * @param prefix the namespace prefix. * @return true, if the prefix is defined, false otherwise. */ public boolean isNamespacePrefixDefined( final String prefix ) { if ( impliedNamespaces != null ) { if ( impliedNamespaces.containsValue( prefix ) ) { return true; } } if ( openTags.isEmpty() ) { return false; } final ElementLevel parent = (ElementLevel) openTags.peek(); return parent.getNamespaces().isPrefixDefined( prefix ); } /** * Returns all namespaces as properties-collection. This reflects the currently defined namespaces, therefore calls to * writeOpenTag(..) might cause this method to return different collections. * * @return the defined namespaces. */ public Properties getNamespaces() { final Properties namespaces = new Properties(); if ( openTags.isEmpty() ) { if ( impliedNamespaces != null ) { //noinspection UseOfPropertiesAsHashtable namespaces.putAll( impliedNamespaces ); } return namespaces; } final ElementLevel parent = (ElementLevel) openTags.peek(); //noinspection UseOfPropertiesAsHashtable namespaces.putAll( parent.getNamespaces().getNamespaces() ); return namespaces; } /** * Computes the current collection of defined namespaces. * * @return the namespaces declared at this writing position. */ protected DeclaredNamespaces computeNamespaces() { if ( openTags.isEmpty() ) { final DeclaredNamespaces namespaces = new DeclaredNamespaces(); if ( impliedNamespaces != null ) { return namespaces.add( impliedNamespaces ); } return namespaces; } final ElementLevel parent = (ElementLevel) openTags.peek(); return parent.getNamespaces(); } /** * Writes an opening XML tag along with a list of attribute/value pairs. * * @param w the writer. * @param namespaceUri the namespace uri for the element (can be null). * @param name the tag name. * @param attributes the attributes. * @param close controls whether the tag is closed. * @throws java.io.IOException if there is an I/O problem. */ public void writeTag( final Writer w, final String namespaceUri, final String name, final AttributeList attributes, final boolean close ) throws IOException { if ( name == null ) { throw new NullPointerException(); } indent( w ); setLineEmpty( false ); DeclaredNamespaces namespaces = computeNamespaces(); if ( attributes != null ) { namespaces = namespaces.add( attributes ); } w.write( "<" ); if ( namespaceUri == null ) { w.write( name ); openTags.push( new ElementLevel( name, namespaces ) ); } else { final String nsPrefix = namespaces.getPrefix( namespaceUri ); if ( nsPrefix == null ) { throw new IllegalArgumentException( "Namespace " + namespaceUri + " is not defined." ); } if ( "".equals( nsPrefix ) ) { w.write( name ); openTags.push( new ElementLevel( namespaceUri, null, name, namespaces ) ); } else { w.write( nsPrefix ); w.write( ":" ); w.write( name ); openTags.push( new ElementLevel( namespaceUri, nsPrefix, name, namespaces ) ); } } if ( attributes != null ) { final AttributeList.AttributeEntry[] entries = attributes.toArray(); for ( final AttributeEntry entry : entries ) { w.write( " " ); buildAttributeName( entry, namespaces, w ); w.write( "=\"" ); writeTextNormalized( w, entry.getValue(), true ); w.write( "\"" ); } } if ( close ) { if ( isHtmlCompatiblityMode() ) { w.write( " />" ); } else { w.write( "/>" ); } openTags.pop(); doEndOfLine( w ); } else { w.write( ">" ); doEndOfLine( w ); } } /** * Conditionally writes an end-of-line character. The End-Of-Line is only written, if the tag description indicates * that the currently open element does not expect any CDATA inside. Writing a newline for CDATA-elements may have * sideeffects. * * @param w the writer. * @throws java.io.IOException if there is an I/O problem. */ private void doEndOfLine( final Writer w ) throws IOException { if ( openTags.isEmpty() ) { if ( isWriteFinalLinebreak() ) { writeNewLine( w ); } } else { final ElementLevel level = (ElementLevel) openTags.peek(); if ( getTagDescription().hasCData( level.getNamespace(), level.getTagName() ) == false ) { writeNewLine( w ); } } } /** * Processes a single attribute and searches for namespace declarations. If a namespace declaration is found, it is * returned in a normalized way. If namespace processing is active, the attribute name will be fully qualified with * the prefix registered for the attribute's namespace URI. * * @param entry the attribute enty. * @param namespaces the currently known namespaces. * @param writer the writer that should receive the formatted attribute name. * @throws IOException if an IO error occured. */ private void buildAttributeName( final AttributeList.AttributeEntry entry, final DeclaredNamespaces namespaces, final Writer writer ) throws IOException { final ElementLevel currentElement = (ElementLevel) openTags.peek(); final String name = entry.getName(); final String namespaceUri = entry.getNamespace(); if ( ( isAlwaysAddNamespace() == false ) && ObjectUtilities.equal( currentElement.getNamespace(), namespaceUri ) ) { writer.write( name ); return; } if ( namespaceUri == null ) { writer.write( name ); return; } if ( AttributeList.XMLNS_NAMESPACE.equals( namespaceUri ) ) { // its a namespace declaration. if ( "".equals( name ) ) { writer.write( "xmlns" ); return; } writer.write( "xmlns:" ); writer.write( name ); return; } final String namespacePrefix = namespaces.getPrefix( namespaceUri ); if ( ( namespacePrefix != null ) && ( "".equals( namespacePrefix ) == false ) ) { writer.write( namespacePrefix ); writer.write( ':' ); writer.write( name ); } else { writer.write( name ); } } /** * Normalizes the given string using a shared buffer. * * @param s the string that should be XML-Encoded. * @param transformNewLine a flag controling whether to transform newlines into character-entities. * @return the transformed string. */ public String normalizeLocal( final String s, final boolean transformNewLine ) throws IOException { return normalize( s, transformNewLine ); } /** * Normalizes the given string and writes the result directly to the stream. * * @param writer the writer that should receive the normalized content. * @param s the string that should be XML-Encoded. * @param transformNewLine a flag controling whether to transform newlines into character-entities. * @throws IOException if writing to the stream failed. */ public void writeTextNormalized( final Writer writer, final String s, final boolean transformNewLine ) throws IOException { writeTextNormalized( writer, s, charsetEncoder, transformNewLine ); } private static void writeTextNormalized( final Writer writer, final String s, final CharsetEncoder encoder, final boolean transformNewLine ) throws IOException { if ( s == null ) { return; } final StringBuilder strB = new StringBuilder( s.length() ); for ( int offset = 0; offset < s.length(); ) { final int cp = s.codePointAt( offset ); switch ( cp ) { case 9: // \t strB.appendCodePoint( cp ); break; case 10: // \n if ( transformNewLine ) { strB.append( " " ); break; } strB.appendCodePoint( cp ); break; case 13: // \r if ( transformNewLine ) { strB.append( " " ); break; } strB.appendCodePoint( cp ); break; case 60: // < strB.append( "<" ); break; case 62: // > strB.append( ">" ); break; case 34: // " strB.append( """ ); break; case 38: // & strB.append( "&" ); break; case 39: // ' strB.append( "'" ); break; default: if ( cp >= 0x20 ) { final String cpStr = new String( new int[] { cp }, 0, 1 ); if ( ( encoder != null ) && !encoder.canEncode( cpStr ) ) { strB.append( "&#x" + Integer.toHexString( cp ) ); } else { strB.appendCodePoint( cp ); } } } offset += Character.charCount( cp ); } writer.write( strB.toString() ); } /** * Normalises a string, replacing certain characters with their escape sequences so that the XML text is not * corrupted. * * @param s the string. * @param transformNewLine true, if a newline in the string should be converted into a character entity. * @return the normalised string. */ public static String normalize( final String s, final boolean transformNewLine ) { if ( s == null ) { return ""; } final StringWriter writer = new StringWriter( s.length() ); try { writeTextNormalized( writer, s, null, transformNewLine ); } catch ( final IOException e ) { LOGGER.error( e ); return s; } return writer.toString(); } /** * Indent the line. Called for proper indenting in various places. * * @param writer the writer which should receive the indentention. * @throws java.io.IOException if writing the stream failed. */ public void indent( final Writer writer ) throws IOException { if ( openTags.isEmpty() ) { for ( int i = 0; i < additionalIndent; i++ ) { writer.write( indentString ); } return; } final ElementLevel level = (ElementLevel) openTags.peek(); if ( getTagDescription().hasCData( level.getNamespace(), level.getTagName() ) == false ) { doEndOfLine( writer ); for ( int i = 0; i < openTags.size(); i++ ) { writer.write( indentString ); } for ( int i = 0; i < additionalIndent; i++ ) { writer.write( indentString ); } } } /** * Indent the line. Called for proper indenting in various places. * * @param writer the writer which should receive the indentention. * @throws java.io.IOException if writing the stream failed. */ public void indentForClose( final Writer writer ) throws IOException { if ( openTags.isEmpty() ) { for ( int i = 0; i < additionalIndent; i++ ) { writer.write( indentString ); } return; } final ElementLevel level = (ElementLevel) openTags.peek(); if ( getTagDescription().hasCData( level.getNamespace(), level.getTagName() ) == false ) { doEndOfLine( writer ); for ( int i = 1; i < openTags.size(); i++ ) { writer.write( indentString ); } for ( int i = 0; i < additionalIndent; i++ ) { writer.write( indentString ); } } } /** * Returns the list of safe tags. * * @return The list. */ public TagDescription getTagDescription() { return safeTags; } /** * Writes a comment into the generated xml file. * * @param writer the writer. * @param comment the comment text * @throws IOException if there is a problem writing to the character stream. */ public void writeComment( final Writer writer, final String comment ) throws IOException { if ( openTags.isEmpty() == false ) { final ElementLevel level = (ElementLevel) openTags.peek(); if ( getTagDescription().hasCData( level.getNamespace(), level.getTagName() ) == false ) { indent( writer ); } } setLineEmpty( false ); writer.write( "<!-- " ); writeTextNormalized( writer, comment, false ); writer.write( " -->" ); doEndOfLine( writer ); } /** * Checks, whether attributes of the same namespace as the current element should be written without a prefix. * Attributes without a prefix are considered to be not in any namespace at all. How to treat such attributes is * implementation dependent. (Appendix A; Section 6.2 of the XmlNamespaces recommendation) * * @return true, if attributes in the element's namespace should be written without a prefix, false to write all * attributes with a prefix. */ public boolean isAssumeDefaultNamespace() { return assumeDefaultNamespace; } /** * Defines, whether attributes of the same namespace as the current element should be written without a prefix. * Attributes without a prefix are considered to be not in any namespace at all. How to treat such attributes is * implementation dependent. (Appendix A; Section 6.2 of the XmlNamespaces recommendation) * * @param assumeDefaultNamespace true, if attributes in the element's namespace should be written without a prefix, * false to write all attributes with a prefix. */ public void setAssumeDefaultNamespace( final boolean assumeDefaultNamespace ) { this.assumeDefaultNamespace = assumeDefaultNamespace; } /** * Returns the current indention level. * * @return the indention level. */ public int getCurrentIndentLevel() { return additionalIndent + openTags.size(); } /** * Defines, whether the written XML file should end with an empty line. * * @param writeFinalLinebreak true, if an linebreak should be added at the end of the file, false otherwise. */ public void setWriteFinalLinebreak( final boolean writeFinalLinebreak ) { this.writeFinalLinebreak = writeFinalLinebreak; } /** * Checks, whether the written XML file should end with an empty line. * * @return true, if an linebreak should be added at the end of the file, false otherwise. */ public boolean isWriteFinalLinebreak() { return writeFinalLinebreak; } }