View Javadoc
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  
56  import org.slf4j.Logger;
57  import org.slf4j.LoggerFactory;
58  
59  import java.io.IOException;
60  import java.io.OutputStream;
61  import java.io.OutputStreamWriter;
62  import java.io.Writer;
63  import java.nio.charset.Charset;
64  import java.nio.charset.StandardCharsets;
65  import java.util.ArrayDeque;
66  import java.util.Deque;
67  
68  /**
69   * Makes writing XML much much easier. Improved from <a href=
70   * "http://builder.com.com/article.jhtml?id=u00220020318yan01.htm&page=1&vf=tt">
71   * article</a>
72   *
73   * @author <a href="mailto:bayard@apache.org">Henri Yandell</a>
74   * @author <a href="mailto:pete@fingertipsoft.com">Peter Cassetta</a>
75   * @author Last changed by: $Author$
76   * @version $Revision$ $Date$
77   * @since 1.0
78   */
79  public class XmlWriter
80  {
81      /**
82       * CDATA start tag: {@value}
83       */
84      public static final String CDATA_START = "<![CDATA[";
85      /**
86       * CDATA end tag: {@value}
87       */
88      public static final String CDATA_END = "]]>";
89  
90      /**
91       * Default encoding value which is {@value}
92       */
93      public static final String DEFAULT_ENCODING = "UTF-8";
94  
95      public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
96  
97      /**
98       * Logger for this class
99       */
100     private static final Logger logger =
101             LoggerFactory.getLogger(XmlWriter.class);
102 
103     /** Underlying writer. */
104     private Writer out;
105 
106     /** The encoding to be written into the XML header/metatag. */
107     private Charset encoding;
108 
109     /** Of xml element names. */
110     private Deque<String> stack = new ArrayDeque<>();
111 
112     /** Current attribute string. */
113     private StringBuilder attrs;
114 
115     /** Is the current node empty. */
116     private boolean empty;
117 
118     /** Is the current node closed.... */
119     private boolean closed = true;
120 
121     /** Is pretty printing enabled?. */
122     private boolean pretty = true;
123 
124     /**
125      * was text the last thing output?
126      */
127     private boolean wroteText = false;
128 
129     /**
130      * output this to indent one level when pretty printing
131      */
132     private String indent = "  ";
133 
134     /**
135      * output this to end a line when pretty printing
136      */
137     private String newline = "\n";
138 
139     /**
140      * Create an XmlWriter on top of an existing java.io.Writer.
141      */
142     public XmlWriter(final Writer writer)
143     {
144         this(writer, null);
145     }
146 
147     /**
148      * Create an XmlWriter on top of an existing java.io.Writer.
149      */
150     public XmlWriter(final Writer writer, final Charset charset)
151     {
152         setWriter(writer, charset);
153     }
154 
155     /**
156      * Create an XmlWriter on top of an existing {@link java.io.OutputStream}.
157      *
158      * @param outputStream
159      * @param charset
160      *         The charset to be used for writing to the given output stream.
161      *         Can be <code>null</code>. If it is <code>null</code> the
162      *         {@link #DEFAULT_ENCODING} is used.
163      * @since 2.4
164      */
165     public XmlWriter(final OutputStream outputStream, Charset charset)
166     {
167         final Charset writerCharset =
168                 charset == null ? DEFAULT_CHARSET : charset;
169         final OutputStreamWriter writer =
170                 new OutputStreamWriter(outputStream, writerCharset);
171         setWriter(writer, writerCharset);
172     }
173 
174     /**
175      * Turn pretty printing on or off. Pretty printing is enabled by default,
176      * but it can be turned off to generate more compact XML.
177      *
178      * @param enable
179      *            true to enable, false to disable pretty printing.
180      */
181     public void enablePrettyPrint(final boolean enable)
182     {
183         if (logger.isDebugEnabled())
184         {
185             logger.debug("enablePrettyPrint(enable={}) - start",
186                     String.valueOf(enable));
187         }
188 
189         this.pretty = enable;
190     }
191 
192     /**
193      * Specify the string to prepend to a line for each level of indent. It is 2
194      * spaces ("  ") by default. Some may prefer a single tab ("\t") or a
195      * different number of spaces. Specifying an empty string will turn off
196      * indentation when pretty printing.
197      *
198      * @param indent
199      *            representing one level of indentation while pretty printing.
200      */
201     public void setIndent(final String indent)
202     {
203         logger.debug("setIndent(indent={}) - start", indent);
204 
205         this.indent = indent;
206     }
207 
208     /**
209      * Specify the string used to terminate each line when pretty printing. It
210      * is a single newline ("\n") by default. Users who need to read generated
211      * XML documents in Windows editors like Notepad may wish to set this to a
212      * carriage return/newline sequence ("\r\n"). Specifying an empty string
213      * will turn off generation of line breaks when pretty printing.
214      *
215      * @param newline
216      *            representing the newline sequence when pretty printing.
217      */
218     public void setNewline(final String newline)
219     {
220         logger.debug("setNewline(newline={}) - start", newline);
221 
222         this.newline = newline;
223     }
224 
225     /**
226      * A helper method. It writes out an element which contains only text.
227      *
228      * @param name
229      *            String name of tag
230      * @param text
231      *            String of text to go inside the tag
232      */
233     public XmlWriter writeElementWithText(final String name, final String text)
234             throws IOException
235     {
236         logger.debug("writeElementWithText(name={}, text={}) - start", name,
237                 text);
238 
239         writeElement(name);
240         writeText(text);
241         return endElement();
242     }
243 
244     /**
245      * A helper method. It writes out empty entities.
246      *
247      * @param name
248      *            String name of tag
249      */
250     public XmlWriter writeEmptyElement(final String name) throws IOException
251     {
252         logger.debug("writeEmptyElement(name={}) - start", name);
253 
254         writeElement(name);
255         return endElement();
256     }
257 
258     /**
259      * Begin to write out an element. Unlike the helper tags, this tag will need
260      * to be ended with the endElement method.
261      *
262      * @param name
263      *            String name of tag
264      */
265     public XmlWriter writeElement(final String name) throws IOException
266     {
267         logger.debug("writeElement(name={}) - start", name);
268 
269         return openElement(name);
270     }
271 
272     /**
273      * Begin to output an element.
274      *
275      * @param name
276      *            name of element.
277      */
278     private XmlWriter openElement(final String name) throws IOException
279     {
280         logger.debug("openElement(name={}) - start", name);
281 
282         final boolean wasClosed = this.closed;
283         closeOpeningTag();
284         this.closed = false;
285         if (this.pretty)
286         {
287             // ! wasClosed separates adjacent opening tags by a newline.
288             // this.wroteText makes sure an element embedded within the text of
289             // its parent element begins on a new line, indented to the proper
290             // level. This solves only part of the problem of pretty printing
291             // entities which contain both text and child entities.
292             if (!wasClosed || this.wroteText)
293             {
294                 this.out.write(newline);
295             }
296             for (int i = 0; i < this.stack.size(); i++)
297             {
298                 this.out.write(indent); // Indent opening tag to proper level
299             }
300         }
301         this.out.write("<");
302         this.out.write(name);
303         this.stack.push(name);
304         this.empty = true;
305         this.wroteText = false;
306         return this;
307     }
308 
309     /** Close off the opening tag. **/
310     private void closeOpeningTag() throws IOException
311     {
312         logger.debug("closeOpeningTag() - start");
313 
314         if (!this.closed)
315         {
316             writeAttributes();
317             this.closed = true;
318             this.out.write(">");
319         }
320     }
321 
322     /** Write out all current attributes. */
323     private void writeAttributes() throws IOException
324     {
325         logger.debug("writeAttributes() - start");
326 
327         if (this.attrs != null)
328         {
329             this.out.write(this.attrs.toString());
330             this.attrs.setLength(0);
331             this.empty = false;
332         }
333     }
334 
335     /**
336      * Write an attribute out for the current element. Any XML characters in the
337      * value are escaped. Currently it does not actually throw the exception,
338      * but the API is set that way for future changes.
339      *
340      * @param attr
341      *            name of attribute.
342      * @param value
343      *            value of attribute.
344      * @see #writeAttribute(String, String, boolean)
345      */
346     public XmlWriter writeAttribute(final String attr, final String value)
347             throws IOException
348     {
349         logger.debug("writeAttribute(attr={}, value={}) - start", attr, value);
350         return this.writeAttribute(attr, value, false);
351     }
352 
353     /**
354      * Write an attribute out for the current element. Any XML characters in the
355      * value are escaped. Currently it does not actually throw the exception,
356      * but the API is set that way for future changes.
357      *
358      * @param attr
359      *            name of attribute.
360      * @param value
361      *            value of attribute.
362      * @param literally
363      *            If the writer should be literally on the given value which
364      *            means that meta characters will also be preserved by escaping
365      *            them. Mainly preserves newlines and tabs.
366      */
367     public XmlWriter writeAttribute(final String attr, final String value,
368             final boolean literally) throws IOException
369     {
370         if (logger.isDebugEnabled())
371         {
372             logger.debug(
373                     "writeAttribute(attr={}, value={}, literally={}) - start",
374                     new Object[] {attr, value, String.valueOf(literally)});
375         }
376 
377         if (this.wroteText == true)
378         {
379             throw new IllegalStateException(
380                     "The text for the current element has already been written. Cannot add attributes afterwards.");
381         }
382         // maintain API
383         if (false)
384         {
385             throw new IOException();
386         }
387 
388         if (this.attrs == null)
389         {
390             this.attrs = new StringBuilder();
391         }
392         this.attrs.append(" ");
393         this.attrs.append(attr);
394         this.attrs.append("=\"");
395         this.attrs.append(escapeXml(value, literally));
396         this.attrs.append("\"");
397         return this;
398     }
399 
400     /**
401      * End the current element. This will throw an exception if it is called
402      * when there is not a currently open element.
403      */
404     public XmlWriter endElement() throws IOException
405     {
406         logger.debug("endElement() - start");
407 
408         if (this.stack.isEmpty())
409         {
410             throw new IOException("Called endElement too many times. ");
411         }
412         final String name = this.stack.pop();
413         if (name != null)
414         {
415             if (this.empty)
416             {
417                 writeAttributes();
418                 this.out.write("/>");
419             } else
420             {
421                 if (this.pretty && !this.wroteText)
422                 {
423                     for (int i = 0; i < this.stack.size(); i++)
424                     {
425                         this.out.write(indent); // Indent closing tag to proper
426                                                 // level
427                     }
428                 }
429                 this.out.write("</");
430                 this.out.write(name);
431                 this.out.write(">");
432             }
433             if (this.pretty)
434             {
435                 this.out.write(newline); // Add a newline after the closing tag
436             }
437             this.empty = false;
438             this.closed = true;
439             this.wroteText = false;
440         }
441         return this;
442     }
443 
444     /**
445      * Close this writer. It does not close the underlying writer, but does
446      * throw an exception if there are as yet unclosed tags.
447      */
448     public void close() throws IOException
449     {
450         logger.debug("close() - start");
451 
452         this.out.flush();
453 
454         if (!this.stack.isEmpty())
455         {
456             throw new IOException("Tags are not all closed. " + "Possibly, "
457                     + this.stack.pop() + " is unclosed. ");
458         }
459     }
460 
461     /**
462      * Output body text. Any XML characters are escaped.
463      *
464      * @param text
465      *            The text to be written
466      * @return This writer
467      * @throws IOException
468      * @see #writeText(String, boolean)
469      */
470     public XmlWriter writeText(final String text) throws IOException
471     {
472         logger.debug("writeText(text={}) - start", text);
473         return this.writeText(text, false);
474     }
475 
476     /**
477      * Output body text. Any XML characters are escaped.
478      *
479      * @param text
480      *            The text to be written
481      * @param literally
482      *            If the writer should be literally on the given value which
483      *            means that meta characters will also be preserved by escaping
484      *            them. Mainly preserves newlines and tabs.
485      * @return This writer
486      * @throws IOException
487      */
488     public XmlWriter writeText(final String text, final boolean literally)
489             throws IOException
490     {
491         if (logger.isDebugEnabled())
492         {
493             logger.debug("writeText(text={}, literally={}) - start", text,
494                     String.valueOf(literally));
495         }
496 
497         closeOpeningTag();
498         this.empty = false;
499         this.wroteText = true;
500 
501         this.out.write(escapeXml(text, literally));
502         return this;
503     }
504 
505     /**
506      * Write out a chunk of CDATA. This helper method surrounds the passed in
507      * data with the CDATA tag.
508      *
509      * @param cdata
510      *            of CDATA text.
511      */
512     public XmlWriter writeCData(String cdata) throws IOException
513     {
514         logger.debug("writeCData(cdata={}) - start", cdata);
515 
516         closeOpeningTag();
517 
518         final boolean hasAlreadyEnclosingCdata =
519                 cdata.startsWith(CDATA_START) && cdata.endsWith(CDATA_END);
520 
521         // There may already be CDATA sections inside the data.
522         // But CDATA sections can't be nested - can't have ]]> inside a CDATA
523         // section.
524         // (See http://www.w3.org/TR/REC-xml/#NT-CDStart in the W3C specs)
525         // The solutions is to replace any occurrence of "]]>" by
526         // "]]]]><![CDATA[>",
527         // so that the top CDATA section is split into many valid CDATA sections
528         // (you
529         // can look at the "]]]]>" as if it was an escape sequence for "]]>").
530         if (!hasAlreadyEnclosingCdata)
531         {
532             cdata = cdata.replaceAll(CDATA_END, "]]]]><![CDATA[>");
533         }
534 
535         this.empty = false;
536         this.wroteText = true;
537         if (!hasAlreadyEnclosingCdata)
538         {
539             this.out.write(CDATA_START);
540         }
541         this.out.write(cdata);
542         if (!hasAlreadyEnclosingCdata)
543         {
544             this.out.write(CDATA_END);
545         }
546         return this;
547     }
548 
549     /**
550      * Write out a chunk of comment. This helper method surrounds the passed in
551      * data with the XML comment tag.
552      *
553      * @param comment
554      *            of text to comment.
555      */
556     public XmlWriter writeComment(final String comment) throws IOException
557     {
558         logger.debug("writeComment(comment={}) - start", comment);
559 
560         writeChunk("<!-- " + comment + " -->");
561         return this;
562     }
563 
564     ////////////////////////////////////////////////////////////////////////////
565     // Added for DbUnit
566 
567     private void writeChunk(final String data) throws IOException
568     {
569         logger.debug("writeChunk(data={}) - start", data);
570 
571         closeOpeningTag();
572         this.empty = false;
573         if (this.pretty && !this.wroteText)
574         {
575             for (int i = 0; i < this.stack.size(); i++)
576             {
577                 this.out.write(indent);
578             }
579         }
580 
581         this.out.write(data);
582 
583         if (this.pretty)
584         {
585             this.out.write(newline);
586         }
587     }
588 
589     // Two example methods. They should output the same XML:
590     // <person name="fred" age="12"><phone>425343</phone><bob/></person>
591     static public void main(final String[] args) throws IOException
592     {
593         logger.debug("main(args={}) - start", args);
594 
595         test1();
596         test2();
597     }
598 
599     static public void test1() throws IOException
600     {
601         logger.debug("test1() - start");
602 
603         final Writer writer = new java.io.StringWriter();
604         final XmlWriter xmlwriter = new XmlWriter(writer);
605         xmlwriter.writeElement("person").writeAttribute("name", "fred")
606                 .writeAttribute("age", "12").writeElement("phone")
607                 .writeText("4254343").endElement().writeElement("friends")
608                 .writeElement("bob").endElement().writeElement("jim")
609                 .endElement().endElement().endElement();
610         xmlwriter.close();
611         System.err.println(writer.toString());
612     }
613 
614     static public void test2() throws IOException
615     {
616         logger.debug("test2() - start");
617 
618         final Writer writer = new java.io.StringWriter();
619         final XmlWriter xmlwriter = new XmlWriter(writer);
620         xmlwriter.writeComment("Example of XmlWriter running");
621         xmlwriter.writeElement("person");
622         xmlwriter.writeAttribute("name", "fred");
623         xmlwriter.writeAttribute("age", "12");
624         xmlwriter.writeElement("phone");
625         xmlwriter.writeText("4254343");
626         xmlwriter.endElement();
627         xmlwriter.writeComment("Examples of empty tags");
628         // xmlwriter.setDefaultNamespace("test");
629         xmlwriter.writeElement("friends");
630         xmlwriter.writeEmptyElement("bob");
631         xmlwriter.writeEmptyElement("jim");
632         xmlwriter.endElement();
633         xmlwriter.writeElementWithText("foo", "This is an example.");
634         xmlwriter.endElement();
635         xmlwriter.close();
636         System.err.println(writer.toString());
637     }
638 
639     ////////////////////////////////////////////////////////////////////////////
640     // Added for DbUnit
641 
642     /**
643      * Escapes some meta characters like \n, \r that should be preserved in the
644      * XML so that a reader will not filter out those symbols. This code is
645      * modified from xmlrpc:
646      * https://svn.apache.org/repos/asf/webservices/xmlrpc/branches/
647      * XMLRPC_1_2_BRANCH/src/java/org/apache/xmlrpc/XmlWriter.java
648      *
649      * @param str
650      *            The string to be escaped
651      * @param literally
652      *            If the writer should be literally on the given value which
653      *            means that meta characters will also be preserved by escaping
654      *            them. Mainly preserves newlines and carriage returns.
655      * @return The escaped string
656      */
657     private String escapeXml(final String str, final boolean literally)
658     {
659         logger.debug("escapeXml(str={}, literally={}) - start", str,
660                 Boolean.toString(literally));
661 
662         char[] block = null;
663         int last = 0;
664         StringBuilder buffer = null;
665         final int strLength = str.length();
666         int index = 0;
667 
668         for (index = 0; index < strLength; index++)
669         {
670             final char currentChar = str.charAt(index);
671             final String entity =
672                     convertCharacterToEntity(currentChar, literally);
673 
674             // If we found something to substitute, then copy over previous
675             // data then do the substitution.
676             if (entity != null)
677             {
678                 if (block == null)
679                 {
680                     block = str.toCharArray();
681                 }
682                 if (buffer == null)
683                 {
684                     buffer = new StringBuilder();
685                 }
686                 buffer.append(block, last, index - last);
687                 buffer.append(entity);
688                 last = index + 1;
689             }
690         }
691 
692         // nothing found, just return source
693         if (last == 0)
694         {
695             return str;
696         }
697 
698         if (last < strLength)
699         {
700             if (block == null)
701             {
702                 block = str.toCharArray();
703             }
704             if (buffer == null)
705             {
706                 buffer = new StringBuilder();
707             }
708             buffer.append(block, last, index - last);
709         }
710 
711         return buffer.toString();
712     }
713 
714     protected String convertCharacterToEntity(final char currentChar,
715             final boolean literally)
716     {
717         String entity = null;
718         switch (currentChar)
719         {
720         case '\t':
721             entity = "&#09;";
722             break;
723         case '\n':
724             if (literally)
725             {
726                 entity = "&#xA;";
727             }
728             break;
729         case '\r':
730             if (literally)
731             {
732                 entity = "&#xD;";
733             }
734             break;
735         case '&':
736             entity = "&amp;";
737             break;
738         case '<':
739             entity = "&lt;";
740             break;
741         case '>':
742             entity = "&gt;";
743             break;
744         case '\"':
745             entity = "&quot;";
746             break;
747         case '\'':
748             entity = "&apos;";
749             break;
750         default:
751             if ((currentChar > 0x7f) && !isValidXmlChar(currentChar))
752             {
753                 entity = "&#" + String.valueOf((int) currentChar) + ";";
754             }
755             break;
756         }
757         return entity;
758     }
759 
760     /**
761      * Section 2.2 of the XML spec describes which Unicode code points are valid
762      * in XML:
763      *
764      * <blockquote><code>#x9 | #xA | #xD | [#x20-#xD7FF] |
765      * [#xE000-#xFFFD] | [#x10000-#x10FFFF]</code></blockquote>
766      *
767      * Code points outside this set must be entity encoded to be represented in
768      * XML.
769      *
770      * @param c The character to inspect. Type is int because unicode char value may exceed Character.MAX_VALUE.
771      * @return Whether the specified character is valid in XML.
772      */
773     private static boolean isValidXmlChar(int c)
774     {
775         switch (c)
776         {
777         case 0x9:
778         case 0xa: // line feed, '\n'
779         case 0xd: // carriage return, '\r'
780             return true;
781 
782         default:
783             return ((0x20 <= c && c <= 0xd7ff) || (0xe000 <= c && c <= 0xfffd)
784                     || (0x10000 <= c && c <= 0x10ffff));
785         }
786     }
787 
788     private String replace(final String value, final String original,
789             final String replacement)
790     {
791         if (logger.isDebugEnabled())
792         {
793             logger.debug("replace(value=" + value + ", original=" + original
794                     + ", replacement=" + replacement + ") - start");
795         }
796 
797         StringBuilder buffer = null;
798 
799         int startIndex = 0;
800         int lastEndIndex = 0;
801         for (;;)
802         {
803             startIndex = value.indexOf(original, lastEndIndex);
804             if (startIndex == -1)
805             {
806                 if (buffer != null)
807                 {
808                     buffer.append(value.substring(lastEndIndex));
809                 }
810                 break;
811             }
812 
813             if (buffer == null)
814             {
815                 buffer = new StringBuilder((int) (original.length() * 1.5));
816             }
817             buffer.append(value.substring(lastEndIndex, startIndex));
818             buffer.append(replacement);
819             lastEndIndex = startIndex + original.length();
820         }
821 
822         return buffer == null ? value : buffer.toString();
823     }
824 
825     private void setEncoding(String encoding)
826     {
827         logger.debug("setEncoding(encoding={}) - start", encoding);
828 
829         Charset charset = null;
830 
831         if (encoding == null && out instanceof OutputStreamWriter)
832         {
833             charset = Charset.forName(((OutputStreamWriter) out).getEncoding());
834         }
835 
836         if (encoding != null)
837         {
838             final String ucEncoding = encoding.toUpperCase();
839 
840             // Use official encoding names where we know them,
841             // avoiding the Java-only names. When using common
842             // encodings where we can easily tell if characters
843             // are out of range, we'll escape out-of-range
844             // characters using character refs for safety.
845 
846             // I _think_ these are all the main synonyms for these!
847             if ("UTF8".equalsIgnoreCase(ucEncoding))
848             {
849                 charset = StandardCharsets.UTF_8;
850             } else if ("US-ASCII".equalsIgnoreCase(ucEncoding) || "ASCII".equalsIgnoreCase(ucEncoding))
851             {
852                 // dangerMask = (short)0xff80;
853                 charset = StandardCharsets.US_ASCII;
854             } else if ("ISO-8859-1".equalsIgnoreCase(ucEncoding)
855                     || "8859_1".equalsIgnoreCase(ucEncoding)
856                     || "ISO8859_1".equalsIgnoreCase(ucEncoding))
857             {
858                 // dangerMask = (short)0xff00;
859                 charset = StandardCharsets.ISO_8859_1;
860             } else if ("UNICODE".equalsIgnoreCase(ucEncoding)
861                     || "UNICODE-BIG".equalsIgnoreCase(ucEncoding)
862                     || "UNICODE-LITTLE".equalsIgnoreCase(ucEncoding))
863             {
864                 charset = StandardCharsets.UTF_16;
865 
866                 // TODO: UTF-16BE, UTF-16LE ... no BOM; what
867                 // release of JDK supports those Unicode names?
868             }
869 
870             // if (dangerMask != 0)
871             // stringBuf = new StringBuffer();
872         }
873 
874         setEncoding(charset);
875     }
876 
877     private void setEncoding(Charset charset)
878     {
879         this.encoding = charset;
880     }
881 
882     /**
883      * Resets the handler to write a new text document.
884      *
885      * @param writer
886      *            XML text is written to this writer.
887      * @param encoding
888      *            if non-null, and an XML declaration is written, this is the
889      *            name that will be used for the character encoding.
890      *
891      * @exception IllegalStateException
892      *                if the current document hasn't yet ended (i.e. the output
893      *                stream {@link #out} is not null)
894      */
895     final public void setWriter(final Writer writer, final String encoding)
896     {
897         logger.debug("setWriter(writer={}, encoding={}) - start", writer,
898                 encoding);
899 
900         setWriter(writer, Charset.forName(encoding));
901     }
902 
903     final public void setWriter(final Writer writer, final Charset charset)
904     {
905         logger.debug("setWriter(writer={}, charset={}) - start", writer,
906                 charset);
907 
908         if (this.out != null)
909         {
910             throw new IllegalStateException(
911                     "can't change stream in mid course");
912         }
913         this.out = writer;
914         if (this.out != null)
915         {
916             setEncoding(charset);
917             // if (!(this.out instanceof BufferedWriter))
918             // this.out = new BufferedWriter(this.out);
919         }
920     }
921 
922     public XmlWriter writeDeclaration() throws IOException
923     {
924         logger.debug("writeDeclaration() - start");
925 
926         if (this.encoding != null)
927         {
928             this.out.write("<?xml version='1.0'");
929             this.out.write(" encoding='" + this.encoding + "'");
930             this.out.write("?>");
931             this.out.write(this.newline);
932         }
933 
934         return this;
935     }
936 
937     public XmlWriter writeDoctype(final String systemId, final String publicId)
938             throws IOException
939     {
940         logger.debug("writeDoctype(systemId={}, publicId={}) - start", systemId,
941                 publicId);
942 
943         if (systemId != null || publicId != null)
944         {
945             this.out.write("<!DOCTYPE dataset");
946 
947             if (systemId != null)
948             {
949                 this.out.write(" SYSTEM \"");
950                 this.out.write(systemId);
951                 this.out.write("\"");
952             }
953 
954             if (publicId != null)
955             {
956                 this.out.write(" PUBLIC \"");
957                 this.out.write(publicId);
958                 this.out.write("\"");
959             }
960 
961             this.out.write(">");
962             this.out.write(this.newline);
963         }
964 
965         return this;
966     }
967 }