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 = "	";
722 break;
723 case '\n':
724 if (literally)
725 {
726 entity = "
";
727 }
728 break;
729 case '\r':
730 if (literally)
731 {
732 entity = "
";
733 }
734 break;
735 case '&':
736 entity = "&";
737 break;
738 case '<':
739 entity = "<";
740 break;
741 case '>':
742 entity = ">";
743 break;
744 case '\"':
745 entity = """;
746 break;
747 case '\'':
748 entity = "'";
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 }