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 java.io.IOException;
57  import java.io.OutputStream;
58  import java.io.OutputStreamWriter;
59  import java.io.UnsupportedEncodingException;
60  import java.io.Writer;
61  import java.util.Stack;
62  
63  import org.slf4j.Logger;
64  import org.slf4j.LoggerFactory;
65  
66  /**
67   * Makes writing XML much much easier. Improved from <a href=
68   * "http://builder.com.com/article.jhtml?id=u00220020318yan01.htm&page=1&vf=tt">
69   * article</a>
70   *
71   * @author <a href="mailto:bayard@apache.org">Henri Yandell</a>
72   * @author <a href="mailto:pete@fingertipsoft.com">Peter Cassetta</a>
73   * @author Last changed by: $Author$
74   * @version $Revision$ $Date$
75   * @since 1.0
76   */
77  public class XmlWriter
78  {
79      /**
80       * CDATA start tag: {@value}
81       */
82      public static final String CDATA_START = "<![CDATA[";
83      /**
84       * CDATA end tag: {@value}
85       */
86      public static final String CDATA_END = "]]>";
87  
88      /**
89       * Default encoding value which is {@value}
90       */
91      public static final String DEFAULT_ENCODING = "UTF-8";
92  
93      /**
94       * Logger for this class
95       */
96      private static final Logger logger =
97              LoggerFactory.getLogger(XmlWriter.class);
98  
99      /** Underlying writer. */
100     private Writer out;
101 
102     /** The encoding to be written into the XML header/metatag. */
103     private String encoding;
104 
105     /** Of xml element names. */
106     private Stack stack = new Stack();
107 
108     /** Current attribute string. */
109     private StringBuffer attrs;
110 
111     /** Is the current node empty. */
112     private boolean empty;
113 
114     /** Is the current node closed.... */
115     private boolean closed = true;
116 
117     /** Is pretty printing enabled?. */
118     private boolean pretty = true;
119 
120     /**
121      * was text the last thing output?
122      */
123     private boolean wroteText = false;
124 
125     /**
126      * output this to indent one level when pretty printing
127      */
128     private String indent = "  ";
129 
130     /**
131      * output this to end a line when pretty printing
132      */
133     private String newline = "\n";
134 
135     /**
136      * Create an XmlWriter on top of an existing java.io.Writer.
137      */
138     public XmlWriter(final Writer writer)
139     {
140         this(writer, null);
141     }
142 
143     /**
144      * Create an XmlWriter on top of an existing java.io.Writer.
145      */
146     public XmlWriter(final Writer writer, final String encoding)
147     {
148         setWriter(writer, encoding);
149     }
150 
151     /**
152      * Create an XmlWriter on top of an existing {@link java.io.OutputStream}.
153      *
154      * @param outputStream
155      * @param encoding
156      *            The encoding to be used for writing to the given output
157      *            stream. Can be <code>null</code>. If it is <code>null</code>
158      *            the {@link #DEFAULT_ENCODING} is used.
159      * @throws UnsupportedEncodingException
160      * @since 2.4
161      */
162     public XmlWriter(final OutputStream outputStream, String encoding)
163             throws UnsupportedEncodingException
164     {
165         if (encoding == null)
166         {
167             encoding = DEFAULT_ENCODING;
168         }
169         final OutputStreamWriter writer =
170                 new OutputStreamWriter(outputStream, encoding);
171         setWriter(writer, encoding);
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         stack.add(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 StringBuffer();
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.empty())
409         {
410             throw new IOException("Called endElement too many times. ");
411         }
412         final String name = (String) 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.empty())
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     private void writeChunk(final String data) throws IOException
565     {
566         logger.debug("writeChunk(data={}) - start", data);
567 
568         closeOpeningTag();
569         this.empty = false;
570         if (this.pretty && !this.wroteText)
571         {
572             for (int i = 0; i < this.stack.size(); i++)
573             {
574                 this.out.write(indent);
575             }
576         }
577 
578         this.out.write(data);
579 
580         if (this.pretty)
581         {
582             this.out.write(newline);
583         }
584     }
585 
586     // Two example methods. They should output the same XML:
587     // <person name="fred" age="12"><phone>425343</phone><bob/></person>
588     static public void main(final String[] args) throws IOException
589     {
590         logger.debug("main(args={}) - start", args);
591 
592         test1();
593         test2();
594     }
595 
596     static public void test1() throws IOException
597     {
598         logger.debug("test1() - start");
599 
600         final Writer writer = new java.io.StringWriter();
601         final XmlWriter xmlwriter = new XmlWriter(writer);
602         xmlwriter.writeElement("person").writeAttribute("name", "fred")
603                 .writeAttribute("age", "12").writeElement("phone")
604                 .writeText("4254343").endElement().writeElement("friends")
605                 .writeElement("bob").endElement().writeElement("jim")
606                 .endElement().endElement().endElement();
607         xmlwriter.close();
608         System.err.println(writer.toString());
609     }
610 
611     static public void test2() throws IOException
612     {
613         logger.debug("test2() - start");
614 
615         final Writer writer = new java.io.StringWriter();
616         final XmlWriter xmlwriter = new XmlWriter(writer);
617         xmlwriter.writeComment("Example of XmlWriter running");
618         xmlwriter.writeElement("person");
619         xmlwriter.writeAttribute("name", "fred");
620         xmlwriter.writeAttribute("age", "12");
621         xmlwriter.writeElement("phone");
622         xmlwriter.writeText("4254343");
623         xmlwriter.endElement();
624         xmlwriter.writeComment("Examples of empty tags");
625         // xmlwriter.setDefaultNamespace("test");
626         xmlwriter.writeElement("friends");
627         xmlwriter.writeEmptyElement("bob");
628         xmlwriter.writeEmptyElement("jim");
629         xmlwriter.endElement();
630         xmlwriter.writeElementWithText("foo", "This is an example.");
631         xmlwriter.endElement();
632         xmlwriter.close();
633         System.err.println(writer.toString());
634     }
635 
636     ////////////////////////////////////////////////////////////////////////////
637     // Added for DbUnit
638 
639     /**
640      * Escapes some meta characters like \n, \r that should be preserved in the
641      * XML so that a reader will not filter out those symbols. This code is
642      * modified from xmlrpc:
643      * https://svn.apache.org/repos/asf/webservices/xmlrpc/branches/
644      * XMLRPC_1_2_BRANCH/src/java/org/apache/xmlrpc/XmlWriter.java
645      *
646      * @param str
647      *            The string to be escaped
648      * @param literally
649      *            If the writer should be literally on the given value which
650      *            means that meta characters will also be preserved by escaping
651      *            them. Mainly preserves newlines and carriage returns.
652      * @return The escaped string
653      */
654     private String escapeXml(final String str, final boolean literally)
655     {
656         logger.debug("escapeXml(str={}, literally={}) - start", str,
657                 Boolean.toString(literally));
658 
659         char[] block = null;
660         int last = 0;
661         StringBuffer buffer = null;
662         final int strLength = str.length();
663         int index = 0;
664 
665         for (index = 0; index < strLength; index++)
666         {
667             final char currentChar = str.charAt(index);
668             final String entity =
669                     convertCharacterToEntity(currentChar, literally);
670 
671             // If we found something to substitute, then copy over previous
672             // data then do the substitution.
673             if (entity != null)
674             {
675                 if (block == null)
676                 {
677                     block = str.toCharArray();
678                 }
679                 if (buffer == null)
680                 {
681                     buffer = new StringBuffer();
682                 }
683                 buffer.append(block, last, index - last);
684                 buffer.append(entity);
685                 last = index + 1;
686             }
687         }
688 
689         // nothing found, just return source
690         if (last == 0)
691         {
692             return str;
693         }
694 
695         if (last < strLength)
696         {
697             if (block == null)
698             {
699                 block = str.toCharArray();
700             }
701             if (buffer == null)
702             {
703                 buffer = new StringBuffer();
704             }
705             buffer.append(block, last, index - last);
706         }
707 
708         return buffer.toString();
709     }
710 
711     protected String convertCharacterToEntity(final char currentChar,
712             final boolean literally)
713     {
714         String entity = null;
715         switch (currentChar)
716         {
717         case '\t':
718             entity = "&#09;";
719             break;
720         case '\n':
721             if (literally)
722             {
723                 entity = "&#xA;";
724             }
725             break;
726         case '\r':
727             if (literally)
728             {
729                 entity = "&#xD;";
730             }
731             break;
732         case '&':
733             entity = "&amp;";
734             break;
735         case '<':
736             entity = "&lt;";
737             break;
738         case '>':
739             entity = "&gt;";
740             break;
741         case '\"':
742             entity = "&quot;";
743             break;
744         case '\'':
745             entity = "&apos;";
746             break;
747         default:
748             if ((currentChar > 0x7f) && !isValidXmlChar(currentChar))
749             {
750                 entity = "&#" + String.valueOf((int) currentChar) + ";";
751             }
752             break;
753         }
754         return entity;
755     }
756 
757     /**
758      * Section 2.2 of the XML spec describes which Unicode code points are valid
759      * in XML:
760      *
761      * <blockquote><code>#x9 | #xA | #xD | [#x20-#xD7FF] |
762      * [#xE000-#xFFFD] | [#x10000-#x10FFFF]</code></blockquote>
763      *
764      * Code points outside this set must be entity encoded to be represented in
765      * XML.
766      *
767      * @param c The character to inspect. Type is int because unicode char value may exceed Character.MAX_VALUE.
768      * @return Whether the specified character is valid in XML.
769      */
770     private static boolean isValidXmlChar(int c)
771     {
772         switch (c)
773         {
774         case 0x9:
775         case 0xa: // line feed, '\n'
776         case 0xd: // carriage return, '\r'
777             return true;
778 
779         default:
780             return ((0x20 <= c && c <= 0xd7ff) || (0xe000 <= c && c <= 0xfffd)
781                     || (0x10000 <= c && c <= 0x10ffff));
782         }
783     }
784 
785     private String replace(final String value, final String original,
786             final String replacement)
787     {
788         if (logger.isDebugEnabled())
789         {
790             logger.debug("replace(value=" + value + ", original=" + original
791                     + ", replacement=" + replacement + ") - start");
792         }
793 
794         StringBuffer buffer = null;
795 
796         int startIndex = 0;
797         int lastEndIndex = 0;
798         for (;;)
799         {
800             startIndex = value.indexOf(original, lastEndIndex);
801             if (startIndex == -1)
802             {
803                 if (buffer != null)
804                 {
805                     buffer.append(value.substring(lastEndIndex));
806                 }
807                 break;
808             }
809 
810             if (buffer == null)
811             {
812                 buffer = new StringBuffer((int) (original.length() * 1.5));
813             }
814             buffer.append(value.substring(lastEndIndex, startIndex));
815             buffer.append(replacement);
816             lastEndIndex = startIndex + original.length();
817         }
818 
819         return buffer == null ? value : buffer.toString();
820     }
821 
822     private void setEncoding(String encoding)
823     {
824         logger.debug("setEncoding(encoding={}) - start", encoding);
825 
826         if (encoding == null && out instanceof OutputStreamWriter)
827         {
828             encoding = ((OutputStreamWriter) out).getEncoding();
829         }
830 
831         if (encoding != null)
832         {
833             encoding = encoding.toUpperCase();
834 
835             // Use official encoding names where we know them,
836             // avoiding the Java-only names. When using common
837             // encodings where we can easily tell if characters
838             // are out of range, we'll escape out-of-range
839             // characters using character refs for safety.
840 
841             // I _think_ these are all the main synonyms for these!
842             if ("UTF8".equals(encoding))
843             {
844                 encoding = "UTF-8";
845             } else if ("US-ASCII".equals(encoding) || "ASCII".equals(encoding))
846             {
847                 // dangerMask = (short)0xff80;
848                 encoding = "US-ASCII";
849             } else if ("ISO-8859-1".equals(encoding)
850                     || "8859_1".equals(encoding)
851                     || "ISO8859_1".equals(encoding))
852             {
853                 // dangerMask = (short)0xff00;
854                 encoding = "ISO-8859-1";
855             } else if ("UNICODE".equals(encoding)
856                     || "UNICODE-BIG".equals(encoding)
857                     || "UNICODE-LITTLE".equals(encoding))
858             {
859                 encoding = "UTF-16";
860 
861                 // TODO: UTF-16BE, UTF-16LE ... no BOM; what
862                 // release of JDK supports those Unicode names?
863             }
864 
865             // if (dangerMask != 0)
866             // stringBuf = new StringBuffer();
867         }
868 
869         this.encoding = encoding;
870     }
871 
872     /**
873      * Resets the handler to write a new text document.
874      *
875      * @param writer
876      *            XML text is written to this writer.
877      * @param encoding
878      *            if non-null, and an XML declaration is written, this is the
879      *            name that will be used for the character encoding.
880      *
881      * @exception IllegalStateException
882      *                if the current document hasn't yet ended (i.e. the output
883      *                stream {@link #out} is not null)
884      */
885     final public void setWriter(final Writer writer, final String encoding)
886     {
887         logger.debug("setWriter(writer={}, encoding={}) - start", writer,
888                 encoding);
889 
890         if (this.out != null)
891         {
892             throw new IllegalStateException(
893                     "can't change stream in mid course");
894         }
895         this.out = writer;
896         if (this.out != null)
897         {
898             setEncoding(encoding);
899             // if (!(this.out instanceof BufferedWriter))
900             // this.out = new BufferedWriter(this.out);
901         }
902     }
903 
904     public XmlWriter writeDeclaration() throws IOException
905     {
906         logger.debug("writeDeclaration() - start");
907 
908         if (this.encoding != null)
909         {
910             this.out.write("<?xml version='1.0'");
911             this.out.write(" encoding='" + this.encoding + "'");
912             this.out.write("?>");
913             this.out.write(this.newline);
914         }
915 
916         return this;
917     }
918 
919     public XmlWriter writeDoctype(final String systemId, final String publicId)
920             throws IOException
921     {
922         logger.debug("writeDoctype(systemId={}, publicId={}) - start", systemId,
923                 publicId);
924 
925         if (systemId != null || publicId != null)
926         {
927             this.out.write("<!DOCTYPE dataset");
928 
929             if (systemId != null)
930             {
931                 this.out.write(" SYSTEM \"");
932                 this.out.write(systemId);
933                 this.out.write("\"");
934             }
935 
936             if (publicId != null)
937             {
938                 this.out.write(" PUBLIC \"");
939                 this.out.write(publicId);
940                 this.out.write("\"");
941             }
942 
943             this.out.write(">");
944             this.out.write(this.newline);
945         }
946 
947         return this;
948     }
949 }