XmlWriter.java

  1. /* ====================================================================
  2.  * The Apache Software License, Version 1.1
  3.  *
  4.  * Copyright (c) 2001 The Apache Software Foundation.  All rights
  5.  * reserved.
  6.  *
  7.  * Redistribution and use in source and binary forms, with or without
  8.  * modification, are permitted provided that the following conditions
  9.  * are met:
  10.  *
  11.  * 1. Redistributions of source code must retain the above copyright
  12.  *    notice, this list of conditions and the following disclaimer.
  13.  *
  14.  * 2. Redistributions in binary form must reproduce the above copyright
  15.  *    notice, this list of conditions and the following disclaimer in
  16.  *    the documentation and/or other materials provided with the
  17.  *    distribution.
  18.  *
  19.  * 3. The end-user documentation included with the redistribution,
  20.  *    if any, must include the following acknowledgment:
  21.  *       "This product includes software developed by the
  22.  *        Apache Software Foundation (http://www.apache.org /)."
  23.  *    Alternately, this acknowledgment may appear in the software itself,
  24.  *    if and wherever such third-party acknowledgments normally appear.
  25.  *
  26.  * 4. The names "Apache" and "Apache Software Foundation" and
  27.  *    "Apache Commons" must not be used to endorse or promote products
  28.  *    derived from this software without prior written permission. For
  29.  *    written permission, please contact apache@apache.org.
  30.  *
  31.  * 5. Products derived from this software may not be called "Apache",
  32.  *    "Apache Turbine", nor may "Apache" appear in their name, without
  33.  *    prior written permission of the Apache Software Foundation.
  34.  *
  35.  * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
  36.  * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  37.  * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  38.  * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
  39.  * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  40.  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  41.  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
  42.  * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  43.  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  44.  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
  45.  * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  46.  * SUCH DAMAGE.
  47.  * ====================================================================
  48.  *
  49.  * This software consists of voluntary contributions made by many
  50.  * individuals on behalf of the Apache Software Foundation.  For more
  51.  * information on the Apache Software Foundation, please see
  52.  * <http://www.apache.org />.
  53.  */
  54. package org.dbunit.util.xml;

  55. import org.slf4j.Logger;
  56. import org.slf4j.LoggerFactory;

  57. import java.io.IOException;
  58. import java.io.OutputStream;
  59. import java.io.OutputStreamWriter;
  60. import java.io.Writer;
  61. import java.nio.charset.Charset;
  62. import java.nio.charset.StandardCharsets;
  63. import java.util.ArrayDeque;
  64. import java.util.Deque;

  65. /**
  66.  * Makes writing XML much much easier. Improved from <a href=
  67.  * "http://builder.com.com/article.jhtml?id=u00220020318yan01.htm&page=1&vf=tt">
  68.  * article</a>
  69.  *
  70.  * @author <a href="mailto:bayard@apache.org">Henri Yandell</a>
  71.  * @author <a href="mailto:pete@fingertipsoft.com">Peter Cassetta</a>
  72.  * @author Last changed by: $Author$
  73.  * @version $Revision$ $Date$
  74.  * @since 1.0
  75.  */
  76. public class XmlWriter
  77. {
  78.     /**
  79.      * CDATA start tag: {@value}
  80.      */
  81.     public static final String CDATA_START = "<![CDATA[";
  82.     /**
  83.      * CDATA end tag: {@value}
  84.      */
  85.     public static final String CDATA_END = "]]>";

  86.     /**
  87.      * Default encoding value which is {@value}
  88.      */
  89.     public static final String DEFAULT_ENCODING = "UTF-8";

  90.     public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;

  91.     /**
  92.      * Logger for this class
  93.      */
  94.     private static final Logger logger =
  95.             LoggerFactory.getLogger(XmlWriter.class);

  96.     /** Underlying writer. */
  97.     private Writer out;

  98.     /** The encoding to be written into the XML header/metatag. */
  99.     private Charset encoding;

  100.     /** Of xml element names. */
  101.     private Deque<String> stack = new ArrayDeque<>();

  102.     /** Current attribute string. */
  103.     private StringBuilder attrs;

  104.     /** Is the current node empty. */
  105.     private boolean empty;

  106.     /** Is the current node closed.... */
  107.     private boolean closed = true;

  108.     /** Is pretty printing enabled?. */
  109.     private boolean pretty = true;

  110.     /**
  111.      * was text the last thing output?
  112.      */
  113.     private boolean wroteText = false;

  114.     /**
  115.      * output this to indent one level when pretty printing
  116.      */
  117.     private String indent = "  ";

  118.     /**
  119.      * output this to end a line when pretty printing
  120.      */
  121.     private String newline = "\n";

  122.     /**
  123.      * Create an XmlWriter on top of an existing java.io.Writer.
  124.      */
  125.     public XmlWriter(final Writer writer)
  126.     {
  127.         this(writer, null);
  128.     }

  129.     /**
  130.      * Create an XmlWriter on top of an existing java.io.Writer.
  131.      */
  132.     public XmlWriter(final Writer writer, final Charset charset)
  133.     {
  134.         setWriter(writer, charset);
  135.     }

  136.     /**
  137.      * Create an XmlWriter on top of an existing {@link java.io.OutputStream}.
  138.      *
  139.      * @param outputStream
  140.      * @param charset
  141.      *         The charset to be used for writing to the given output stream.
  142.      *         Can be <code>null</code>. If it is <code>null</code> the
  143.      *         {@link #DEFAULT_ENCODING} is used.
  144.      * @since 2.4
  145.      */
  146.     public XmlWriter(final OutputStream outputStream, Charset charset)
  147.     {
  148.         final Charset writerCharset =
  149.                 charset == null ? DEFAULT_CHARSET : charset;
  150.         final OutputStreamWriter writer =
  151.                 new OutputStreamWriter(outputStream, writerCharset);
  152.         setWriter(writer, writerCharset);
  153.     }

  154.     /**
  155.      * Turn pretty printing on or off. Pretty printing is enabled by default,
  156.      * but it can be turned off to generate more compact XML.
  157.      *
  158.      * @param enable
  159.      *            true to enable, false to disable pretty printing.
  160.      */
  161.     public void enablePrettyPrint(final boolean enable)
  162.     {
  163.         if (logger.isDebugEnabled())
  164.         {
  165.             logger.debug("enablePrettyPrint(enable={}) - start",
  166.                     String.valueOf(enable));
  167.         }

  168.         this.pretty = enable;
  169.     }

  170.     /**
  171.      * Specify the string to prepend to a line for each level of indent. It is 2
  172.      * spaces ("  ") by default. Some may prefer a single tab ("\t") or a
  173.      * different number of spaces. Specifying an empty string will turn off
  174.      * indentation when pretty printing.
  175.      *
  176.      * @param indent
  177.      *            representing one level of indentation while pretty printing.
  178.      */
  179.     public void setIndent(final String indent)
  180.     {
  181.         logger.debug("setIndent(indent={}) - start", indent);

  182.         this.indent = indent;
  183.     }

  184.     /**
  185.      * Specify the string used to terminate each line when pretty printing. It
  186.      * is a single newline ("\n") by default. Users who need to read generated
  187.      * XML documents in Windows editors like Notepad may wish to set this to a
  188.      * carriage return/newline sequence ("\r\n"). Specifying an empty string
  189.      * will turn off generation of line breaks when pretty printing.
  190.      *
  191.      * @param newline
  192.      *            representing the newline sequence when pretty printing.
  193.      */
  194.     public void setNewline(final String newline)
  195.     {
  196.         logger.debug("setNewline(newline={}) - start", newline);

  197.         this.newline = newline;
  198.     }

  199.     /**
  200.      * A helper method. It writes out an element which contains only text.
  201.      *
  202.      * @param name
  203.      *            String name of tag
  204.      * @param text
  205.      *            String of text to go inside the tag
  206.      */
  207.     public XmlWriter writeElementWithText(final String name, final String text)
  208.             throws IOException
  209.     {
  210.         logger.debug("writeElementWithText(name={}, text={}) - start", name,
  211.                 text);

  212.         writeElement(name);
  213.         writeText(text);
  214.         return endElement();
  215.     }

  216.     /**
  217.      * A helper method. It writes out empty entities.
  218.      *
  219.      * @param name
  220.      *            String name of tag
  221.      */
  222.     public XmlWriter writeEmptyElement(final String name) throws IOException
  223.     {
  224.         logger.debug("writeEmptyElement(name={}) - start", name);

  225.         writeElement(name);
  226.         return endElement();
  227.     }

  228.     /**
  229.      * Begin to write out an element. Unlike the helper tags, this tag will need
  230.      * to be ended with the endElement method.
  231.      *
  232.      * @param name
  233.      *            String name of tag
  234.      */
  235.     public XmlWriter writeElement(final String name) throws IOException
  236.     {
  237.         logger.debug("writeElement(name={}) - start", name);

  238.         return openElement(name);
  239.     }

  240.     /**
  241.      * Begin to output an element.
  242.      *
  243.      * @param name
  244.      *            name of element.
  245.      */
  246.     private XmlWriter openElement(final String name) throws IOException
  247.     {
  248.         logger.debug("openElement(name={}) - start", name);

  249.         final boolean wasClosed = this.closed;
  250.         closeOpeningTag();
  251.         this.closed = false;
  252.         if (this.pretty)
  253.         {
  254.             // ! wasClosed separates adjacent opening tags by a newline.
  255.             // this.wroteText makes sure an element embedded within the text of
  256.             // its parent element begins on a new line, indented to the proper
  257.             // level. This solves only part of the problem of pretty printing
  258.             // entities which contain both text and child entities.
  259.             if (!wasClosed || this.wroteText)
  260.             {
  261.                 this.out.write(newline);
  262.             }
  263.             for (int i = 0; i < this.stack.size(); i++)
  264.             {
  265.                 this.out.write(indent); // Indent opening tag to proper level
  266.             }
  267.         }
  268.         this.out.write("<");
  269.         this.out.write(name);
  270.         this.stack.push(name);
  271.         this.empty = true;
  272.         this.wroteText = false;
  273.         return this;
  274.     }

  275.     /** Close off the opening tag. **/
  276.     private void closeOpeningTag() throws IOException
  277.     {
  278.         logger.debug("closeOpeningTag() - start");

  279.         if (!this.closed)
  280.         {
  281.             writeAttributes();
  282.             this.closed = true;
  283.             this.out.write(">");
  284.         }
  285.     }

  286.     /** Write out all current attributes. */
  287.     private void writeAttributes() throws IOException
  288.     {
  289.         logger.debug("writeAttributes() - start");

  290.         if (this.attrs != null)
  291.         {
  292.             this.out.write(this.attrs.toString());
  293.             this.attrs.setLength(0);
  294.             this.empty = false;
  295.         }
  296.     }

  297.     /**
  298.      * Write an attribute out for the current element. Any XML characters in the
  299.      * value are escaped. Currently it does not actually throw the exception,
  300.      * but the API is set that way for future changes.
  301.      *
  302.      * @param attr
  303.      *            name of attribute.
  304.      * @param value
  305.      *            value of attribute.
  306.      * @see #writeAttribute(String, String, boolean)
  307.      */
  308.     public XmlWriter writeAttribute(final String attr, final String value)
  309.             throws IOException
  310.     {
  311.         logger.debug("writeAttribute(attr={}, value={}) - start", attr, value);
  312.         return this.writeAttribute(attr, value, false);
  313.     }

  314.     /**
  315.      * Write an attribute out for the current element. Any XML characters in the
  316.      * value are escaped. Currently it does not actually throw the exception,
  317.      * but the API is set that way for future changes.
  318.      *
  319.      * @param attr
  320.      *            name of attribute.
  321.      * @param value
  322.      *            value of attribute.
  323.      * @param literally
  324.      *            If the writer should be literally on the given value which
  325.      *            means that meta characters will also be preserved by escaping
  326.      *            them. Mainly preserves newlines and tabs.
  327.      */
  328.     public XmlWriter writeAttribute(final String attr, final String value,
  329.             final boolean literally) throws IOException
  330.     {
  331.         if (logger.isDebugEnabled())
  332.         {
  333.             logger.debug(
  334.                     "writeAttribute(attr={}, value={}, literally={}) - start",
  335.                     new Object[] {attr, value, String.valueOf(literally)});
  336.         }

  337.         if (this.wroteText == true)
  338.         {
  339.             throw new IllegalStateException(
  340.                     "The text for the current element has already been written. Cannot add attributes afterwards.");
  341.         }
  342.         // maintain API
  343.         if (false)
  344.         {
  345.             throw new IOException();
  346.         }

  347.         if (this.attrs == null)
  348.         {
  349.             this.attrs = new StringBuilder();
  350.         }
  351.         this.attrs.append(" ");
  352.         this.attrs.append(attr);
  353.         this.attrs.append("=\"");
  354.         this.attrs.append(escapeXml(value, literally));
  355.         this.attrs.append("\"");
  356.         return this;
  357.     }

  358.     /**
  359.      * End the current element. This will throw an exception if it is called
  360.      * when there is not a currently open element.
  361.      */
  362.     public XmlWriter endElement() throws IOException
  363.     {
  364.         logger.debug("endElement() - start");

  365.         if (this.stack.isEmpty())
  366.         {
  367.             throw new IOException("Called endElement too many times. ");
  368.         }
  369.         final String name = this.stack.pop();
  370.         if (name != null)
  371.         {
  372.             if (this.empty)
  373.             {
  374.                 writeAttributes();
  375.                 this.out.write("/>");
  376.             } else
  377.             {
  378.                 if (this.pretty && !this.wroteText)
  379.                 {
  380.                     for (int i = 0; i < this.stack.size(); i++)
  381.                     {
  382.                         this.out.write(indent); // Indent closing tag to proper
  383.                                                 // level
  384.                     }
  385.                 }
  386.                 this.out.write("</");
  387.                 this.out.write(name);
  388.                 this.out.write(">");
  389.             }
  390.             if (this.pretty)
  391.             {
  392.                 this.out.write(newline); // Add a newline after the closing tag
  393.             }
  394.             this.empty = false;
  395.             this.closed = true;
  396.             this.wroteText = false;
  397.         }
  398.         return this;
  399.     }

  400.     /**
  401.      * Close this writer. It does not close the underlying writer, but does
  402.      * throw an exception if there are as yet unclosed tags.
  403.      */
  404.     public void close() throws IOException
  405.     {
  406.         logger.debug("close() - start");

  407.         this.out.flush();

  408.         if (!this.stack.isEmpty())
  409.         {
  410.             throw new IOException("Tags are not all closed. " + "Possibly, "
  411.                     + this.stack.pop() + " is unclosed. ");
  412.         }
  413.     }

  414.     /**
  415.      * Output body text. Any XML characters are escaped.
  416.      *
  417.      * @param text
  418.      *            The text to be written
  419.      * @return This writer
  420.      * @throws IOException
  421.      * @see #writeText(String, boolean)
  422.      */
  423.     public XmlWriter writeText(final String text) throws IOException
  424.     {
  425.         logger.debug("writeText(text={}) - start", text);
  426.         return this.writeText(text, false);
  427.     }

  428.     /**
  429.      * Output body text. Any XML characters are escaped.
  430.      *
  431.      * @param text
  432.      *            The text to be written
  433.      * @param literally
  434.      *            If the writer should be literally on the given value which
  435.      *            means that meta characters will also be preserved by escaping
  436.      *            them. Mainly preserves newlines and tabs.
  437.      * @return This writer
  438.      * @throws IOException
  439.      */
  440.     public XmlWriter writeText(final String text, final boolean literally)
  441.             throws IOException
  442.     {
  443.         if (logger.isDebugEnabled())
  444.         {
  445.             logger.debug("writeText(text={}, literally={}) - start", text,
  446.                     String.valueOf(literally));
  447.         }

  448.         closeOpeningTag();
  449.         this.empty = false;
  450.         this.wroteText = true;

  451.         this.out.write(escapeXml(text, literally));
  452.         return this;
  453.     }

  454.     /**
  455.      * Write out a chunk of CDATA. This helper method surrounds the passed in
  456.      * data with the CDATA tag.
  457.      *
  458.      * @param cdata
  459.      *            of CDATA text.
  460.      */
  461.     public XmlWriter writeCData(String cdata) throws IOException
  462.     {
  463.         logger.debug("writeCData(cdata={}) - start", cdata);

  464.         closeOpeningTag();

  465.         final boolean hasAlreadyEnclosingCdata =
  466.                 cdata.startsWith(CDATA_START) && cdata.endsWith(CDATA_END);

  467.         // There may already be CDATA sections inside the data.
  468.         // But CDATA sections can't be nested - can't have ]]> inside a CDATA
  469.         // section.
  470.         // (See http://www.w3.org/TR/REC-xml/#NT-CDStart in the W3C specs)
  471.         // The solutions is to replace any occurrence of "]]>" by
  472.         // "]]]]><![CDATA[>",
  473.         // so that the top CDATA section is split into many valid CDATA sections
  474.         // (you
  475.         // can look at the "]]]]>" as if it was an escape sequence for "]]>").
  476.         if (!hasAlreadyEnclosingCdata)
  477.         {
  478.             cdata = cdata.replaceAll(CDATA_END, "]]]]><![CDATA[>");
  479.         }

  480.         this.empty = false;
  481.         this.wroteText = true;
  482.         if (!hasAlreadyEnclosingCdata)
  483.         {
  484.             this.out.write(CDATA_START);
  485.         }
  486.         this.out.write(cdata);
  487.         if (!hasAlreadyEnclosingCdata)
  488.         {
  489.             this.out.write(CDATA_END);
  490.         }
  491.         return this;
  492.     }

  493.     /**
  494.      * Write out a chunk of comment. This helper method surrounds the passed in
  495.      * data with the XML comment tag.
  496.      *
  497.      * @param comment
  498.      *            of text to comment.
  499.      */
  500.     public XmlWriter writeComment(final String comment) throws IOException
  501.     {
  502.         logger.debug("writeComment(comment={}) - start", comment);

  503.         writeChunk("<!-- " + comment + " -->");
  504.         return this;
  505.     }

  506.     ////////////////////////////////////////////////////////////////////////////
  507.     // Added for DbUnit

  508.     private void writeChunk(final String data) throws IOException
  509.     {
  510.         logger.debug("writeChunk(data={}) - start", data);

  511.         closeOpeningTag();
  512.         this.empty = false;
  513.         if (this.pretty && !this.wroteText)
  514.         {
  515.             for (int i = 0; i < this.stack.size(); i++)
  516.             {
  517.                 this.out.write(indent);
  518.             }
  519.         }

  520.         this.out.write(data);

  521.         if (this.pretty)
  522.         {
  523.             this.out.write(newline);
  524.         }
  525.     }

  526.     // Two example methods. They should output the same XML:
  527.     // <person name="fred" age="12"><phone>425343</phone><bob/></person>
  528.     static public void main(final String[] args) throws IOException
  529.     {
  530.         logger.debug("main(args={}) - start", args);

  531.         test1();
  532.         test2();
  533.     }

  534.     static public void test1() throws IOException
  535.     {
  536.         logger.debug("test1() - start");

  537.         final Writer writer = new java.io.StringWriter();
  538.         final XmlWriter xmlwriter = new XmlWriter(writer);
  539.         xmlwriter.writeElement("person").writeAttribute("name", "fred")
  540.                 .writeAttribute("age", "12").writeElement("phone")
  541.                 .writeText("4254343").endElement().writeElement("friends")
  542.                 .writeElement("bob").endElement().writeElement("jim")
  543.                 .endElement().endElement().endElement();
  544.         xmlwriter.close();
  545.         System.err.println(writer.toString());
  546.     }

  547.     static public void test2() throws IOException
  548.     {
  549.         logger.debug("test2() - start");

  550.         final Writer writer = new java.io.StringWriter();
  551.         final XmlWriter xmlwriter = new XmlWriter(writer);
  552.         xmlwriter.writeComment("Example of XmlWriter running");
  553.         xmlwriter.writeElement("person");
  554.         xmlwriter.writeAttribute("name", "fred");
  555.         xmlwriter.writeAttribute("age", "12");
  556.         xmlwriter.writeElement("phone");
  557.         xmlwriter.writeText("4254343");
  558.         xmlwriter.endElement();
  559.         xmlwriter.writeComment("Examples of empty tags");
  560.         // xmlwriter.setDefaultNamespace("test");
  561.         xmlwriter.writeElement("friends");
  562.         xmlwriter.writeEmptyElement("bob");
  563.         xmlwriter.writeEmptyElement("jim");
  564.         xmlwriter.endElement();
  565.         xmlwriter.writeElementWithText("foo", "This is an example.");
  566.         xmlwriter.endElement();
  567.         xmlwriter.close();
  568.         System.err.println(writer.toString());
  569.     }

  570.     ////////////////////////////////////////////////////////////////////////////
  571.     // Added for DbUnit

  572.     /**
  573.      * Escapes some meta characters like \n, \r that should be preserved in the
  574.      * XML so that a reader will not filter out those symbols. This code is
  575.      * modified from xmlrpc:
  576.      * https://svn.apache.org/repos/asf/webservices/xmlrpc/branches/
  577.      * XMLRPC_1_2_BRANCH/src/java/org/apache/xmlrpc/XmlWriter.java
  578.      *
  579.      * @param str
  580.      *            The string to be escaped
  581.      * @param literally
  582.      *            If the writer should be literally on the given value which
  583.      *            means that meta characters will also be preserved by escaping
  584.      *            them. Mainly preserves newlines and carriage returns.
  585.      * @return The escaped string
  586.      */
  587.     private String escapeXml(final String str, final boolean literally)
  588.     {
  589.         logger.debug("escapeXml(str={}, literally={}) - start", str,
  590.                 Boolean.toString(literally));

  591.         char[] block = null;
  592.         int last = 0;
  593.         StringBuilder buffer = null;
  594.         final int strLength = str.length();
  595.         int index = 0;

  596.         for (index = 0; index < strLength; index++)
  597.         {
  598.             final char currentChar = str.charAt(index);
  599.             final String entity =
  600.                     convertCharacterToEntity(currentChar, literally);

  601.             // If we found something to substitute, then copy over previous
  602.             // data then do the substitution.
  603.             if (entity != null)
  604.             {
  605.                 if (block == null)
  606.                 {
  607.                     block = str.toCharArray();
  608.                 }
  609.                 if (buffer == null)
  610.                 {
  611.                     buffer = new StringBuilder();
  612.                 }
  613.                 buffer.append(block, last, index - last);
  614.                 buffer.append(entity);
  615.                 last = index + 1;
  616.             }
  617.         }

  618.         // nothing found, just return source
  619.         if (last == 0)
  620.         {
  621.             return str;
  622.         }

  623.         if (last < strLength)
  624.         {
  625.             if (block == null)
  626.             {
  627.                 block = str.toCharArray();
  628.             }
  629.             if (buffer == null)
  630.             {
  631.                 buffer = new StringBuilder();
  632.             }
  633.             buffer.append(block, last, index - last);
  634.         }

  635.         return buffer.toString();
  636.     }

  637.     protected String convertCharacterToEntity(final char currentChar,
  638.             final boolean literally)
  639.     {
  640.         String entity = null;
  641.         switch (currentChar)
  642.         {
  643.         case '\t':
  644.             entity = "&#09;";
  645.             break;
  646.         case '\n':
  647.             if (literally)
  648.             {
  649.                 entity = "&#xA;";
  650.             }
  651.             break;
  652.         case '\r':
  653.             if (literally)
  654.             {
  655.                 entity = "&#xD;";
  656.             }
  657.             break;
  658.         case '&':
  659.             entity = "&amp;";
  660.             break;
  661.         case '<':
  662.             entity = "&lt;";
  663.             break;
  664.         case '>':
  665.             entity = "&gt;";
  666.             break;
  667.         case '\"':
  668.             entity = "&quot;";
  669.             break;
  670.         case '\'':
  671.             entity = "&apos;";
  672.             break;
  673.         default:
  674.             if ((currentChar > 0x7f) && !isValidXmlChar(currentChar))
  675.             {
  676.                 entity = "&#" + String.valueOf((int) currentChar) + ";";
  677.             }
  678.             break;
  679.         }
  680.         return entity;
  681.     }

  682.     /**
  683.      * Section 2.2 of the XML spec describes which Unicode code points are valid
  684.      * in XML:
  685.      *
  686.      * <blockquote><code>#x9 | #xA | #xD | [#x20-#xD7FF] |
  687.      * [#xE000-#xFFFD] | [#x10000-#x10FFFF]</code></blockquote>
  688.      *
  689.      * Code points outside this set must be entity encoded to be represented in
  690.      * XML.
  691.      *
  692.      * @param c The character to inspect. Type is int because unicode char value may exceed Character.MAX_VALUE.
  693.      * @return Whether the specified character is valid in XML.
  694.      */
  695.     private static boolean isValidXmlChar(int c)
  696.     {
  697.         switch (c)
  698.         {
  699.         case 0x9:
  700.         case 0xa: // line feed, '\n'
  701.         case 0xd: // carriage return, '\r'
  702.             return true;

  703.         default:
  704.             return ((0x20 <= c && c <= 0xd7ff) || (0xe000 <= c && c <= 0xfffd)
  705.                     || (0x10000 <= c && c <= 0x10ffff));
  706.         }
  707.     }

  708.     private String replace(final String value, final String original,
  709.             final String replacement)
  710.     {
  711.         if (logger.isDebugEnabled())
  712.         {
  713.             logger.debug("replace(value=" + value + ", original=" + original
  714.                     + ", replacement=" + replacement + ") - start");
  715.         }

  716.         StringBuilder buffer = null;

  717.         int startIndex = 0;
  718.         int lastEndIndex = 0;
  719.         for (;;)
  720.         {
  721.             startIndex = value.indexOf(original, lastEndIndex);
  722.             if (startIndex == -1)
  723.             {
  724.                 if (buffer != null)
  725.                 {
  726.                     buffer.append(value.substring(lastEndIndex));
  727.                 }
  728.                 break;
  729.             }

  730.             if (buffer == null)
  731.             {
  732.                 buffer = new StringBuilder((int) (original.length() * 1.5));
  733.             }
  734.             buffer.append(value.substring(lastEndIndex, startIndex));
  735.             buffer.append(replacement);
  736.             lastEndIndex = startIndex + original.length();
  737.         }

  738.         return buffer == null ? value : buffer.toString();
  739.     }

  740.     private void setEncoding(String encoding)
  741.     {
  742.         logger.debug("setEncoding(encoding={}) - start", encoding);

  743.         Charset charset = null;

  744.         if (encoding == null && out instanceof OutputStreamWriter)
  745.         {
  746.             charset = Charset.forName(((OutputStreamWriter) out).getEncoding());
  747.         }

  748.         if (encoding != null)
  749.         {
  750.             final String ucEncoding = encoding.toUpperCase();

  751.             // Use official encoding names where we know them,
  752.             // avoiding the Java-only names. When using common
  753.             // encodings where we can easily tell if characters
  754.             // are out of range, we'll escape out-of-range
  755.             // characters using character refs for safety.

  756.             // I _think_ these are all the main synonyms for these!
  757.             if ("UTF8".equalsIgnoreCase(ucEncoding))
  758.             {
  759.                 charset = StandardCharsets.UTF_8;
  760.             } else if ("US-ASCII".equalsIgnoreCase(ucEncoding) || "ASCII".equalsIgnoreCase(ucEncoding))
  761.             {
  762.                 // dangerMask = (short)0xff80;
  763.                 charset = StandardCharsets.US_ASCII;
  764.             } else if ("ISO-8859-1".equalsIgnoreCase(ucEncoding)
  765.                     || "8859_1".equalsIgnoreCase(ucEncoding)
  766.                     || "ISO8859_1".equalsIgnoreCase(ucEncoding))
  767.             {
  768.                 // dangerMask = (short)0xff00;
  769.                 charset = StandardCharsets.ISO_8859_1;
  770.             } else if ("UNICODE".equalsIgnoreCase(ucEncoding)
  771.                     || "UNICODE-BIG".equalsIgnoreCase(ucEncoding)
  772.                     || "UNICODE-LITTLE".equalsIgnoreCase(ucEncoding))
  773.             {
  774.                 charset = StandardCharsets.UTF_16;

  775.                 // TODO: UTF-16BE, UTF-16LE ... no BOM; what
  776.                 // release of JDK supports those Unicode names?
  777.             }

  778.             // if (dangerMask != 0)
  779.             // stringBuf = new StringBuffer();
  780.         }

  781.         setEncoding(charset);
  782.     }

  783.     private void setEncoding(Charset charset)
  784.     {
  785.         this.encoding = charset;
  786.     }

  787.     /**
  788.      * Resets the handler to write a new text document.
  789.      *
  790.      * @param writer
  791.      *            XML text is written to this writer.
  792.      * @param encoding
  793.      *            if non-null, and an XML declaration is written, this is the
  794.      *            name that will be used for the character encoding.
  795.      *
  796.      * @exception IllegalStateException
  797.      *                if the current document hasn't yet ended (i.e. the output
  798.      *                stream {@link #out} is not null)
  799.      */
  800.     final public void setWriter(final Writer writer, final String encoding)
  801.     {
  802.         logger.debug("setWriter(writer={}, encoding={}) - start", writer,
  803.                 encoding);

  804.         setWriter(writer, Charset.forName(encoding));
  805.     }

  806.     final public void setWriter(final Writer writer, final Charset charset)
  807.     {
  808.         logger.debug("setWriter(writer={}, charset={}) - start", writer,
  809.                 charset);

  810.         if (this.out != null)
  811.         {
  812.             throw new IllegalStateException(
  813.                     "can't change stream in mid course");
  814.         }
  815.         this.out = writer;
  816.         if (this.out != null)
  817.         {
  818.             setEncoding(charset);
  819.             // if (!(this.out instanceof BufferedWriter))
  820.             // this.out = new BufferedWriter(this.out);
  821.         }
  822.     }

  823.     public XmlWriter writeDeclaration() throws IOException
  824.     {
  825.         logger.debug("writeDeclaration() - start");

  826.         if (this.encoding != null)
  827.         {
  828.             this.out.write("<?xml version='1.0'");
  829.             this.out.write(" encoding='" + this.encoding + "'");
  830.             this.out.write("?>");
  831.             this.out.write(this.newline);
  832.         }

  833.         return this;
  834.     }

  835.     public XmlWriter writeDoctype(final String systemId, final String publicId)
  836.             throws IOException
  837.     {
  838.         logger.debug("writeDoctype(systemId={}, publicId={}) - start", systemId,
  839.                 publicId);

  840.         if (systemId != null || publicId != null)
  841.         {
  842.             this.out.write("<!DOCTYPE dataset");

  843.             if (systemId != null)
  844.             {
  845.                 this.out.write(" SYSTEM \"");
  846.                 this.out.write(systemId);
  847.                 this.out.write("\"");
  848.             }

  849.             if (publicId != null)
  850.             {
  851.                 this.out.write(" PUBLIC \"");
  852.                 this.out.write(publicId);
  853.                 this.out.write("\"");
  854.             }

  855.             this.out.write(">");
  856.             this.out.write(this.newline);
  857.         }

  858.         return this;
  859.     }
  860. }