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