001: package org.dbunit.util.xml;
002:
003: import org.slf4j.Logger;
004: import org.slf4j.LoggerFactory;
005:
006: /* ====================================================================
007: * The Apache Software License, Version 1.1
008: *
009: * Copyright (c) 2001 The Apache Software Foundation. All rights
010: * reserved.
011: *
012: * Redistribution and use in source and binary forms, with or without
013: * modification, are permitted provided that the following conditions
014: * are met:
015: *
016: * 1. Redistributions of source code must retain the above copyright
017: * notice, this list of conditions and the following disclaimer.
018: *
019: * 2. Redistributions in binary form must reproduce the above copyright
020: * notice, this list of conditions and the following disclaimer in
021: * the documentation and/or other materials provided with the
022: * distribution.
023: *
024: * 3. The end-user documentation included with the redistribution,
025: * if any, must include the following acknowledgment:
026: * "This product includes software developed by the
027: * Apache Software Foundation (http://www.apache.org /)."
028: * Alternately, this acknowledgment may appear in the software itself,
029: * if and wherever such third-party acknowledgments normally appear.
030: *
031: * 4. The names "Apache" and "Apache Software Foundation" and
032: * "Apache Commons" must not be used to endorse or promote products
033: * derived from this software without prior written permission. For
034: * written permission, please contact apache@apache.org.
035: *
036: * 5. Products derived from this software may not be called "Apache",
037: * "Apache Turbine", nor may "Apache" appear in their name, without
038: * prior written permission of the Apache Software Foundation.
039: *
040: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
041: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
042: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
043: * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
044: * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
045: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
046: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
047: * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
048: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
049: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
050: * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
051: * SUCH DAMAGE.
052: * ====================================================================
053: *
054: * This software consists of voluntary contributions made by many
055: * individuals on behalf of the Apache Software Foundation. For more
056: * information on the Apache Software Foundation, please see
057: * <http://www.apache.org />.
058: */
059:
060: import java.io.IOException;
061: import java.io.OutputStreamWriter;
062: import java.io.Writer;
063: import java.util.Stack;
064:
065: /**
066: * Makes writing XML much much easier.
067: * Improved from
068: * <a href="http://builder.com.com/article.jhtml?id=u00220020318yan01.htm&page=1&vf=tt">article</a>
069: *
070: * @author <a href="mailto:bayard@apache.org">Henri Yandell</a>
071: * @author <a href="mailto:pete@fingertipsoft.com">Peter Cassetta</a>
072: * @version 1.0
073: */
074: public class XmlWriter {
075:
076: /**
077: * Logger for this class
078: */
079: private static final Logger logger = LoggerFactory
080: .getLogger(XmlWriter.class);
081:
082: private Writer out; // underlying writer
083: private String encoding;
084: private Stack stack = new Stack(); // of xml element names
085: private StringBuffer attrs; // current attribute string
086: private boolean empty; // is the current node empty
087: private boolean closed = true; // is the current node closed...
088:
089: private boolean pretty = true; // is pretty printing enabled?
090: private boolean wroteText = false; // was text the last thing output?
091: private String indent = " "; // output this to indent one level when pretty printing
092: private String newline = "\n"; // output this to end a line when pretty printing
093:
094: /**
095: * Create an XmlWriter on top of an existing java.io.Writer.
096: */
097: public XmlWriter(Writer writer) {
098: this (writer, null);
099: }
100:
101: /**
102: * Create an XmlWriter on top of an existing java.io.Writer.
103: */
104: public XmlWriter(Writer writer, String encoding) {
105: setWriter(writer, encoding);
106: }
107:
108: /**
109: * Turn pretty printing on or off.
110: * Pretty printing is enabled by default, but it can be turned off
111: * to generate more compact XML.
112: *
113: * @param enable true to enable, false to disable pretty printing.
114: */
115: public void enablePrettyPrint(boolean enable) {
116: logger
117: .debug("enablePrettyPrint(enable=" + enable
118: + ") - start");
119:
120: this .pretty = enable;
121: }
122:
123: /**
124: * Specify the string to prepend to a line for each level of indent.
125: * It is 2 spaces (" ") by default. Some may prefer a single tab ("\t")
126: * or a different number of spaces. Specifying an empty string will turn
127: * off indentation when pretty printing.
128: *
129: * @param indent representing one level of indentation while pretty printing.
130: */
131: public void setIndent(String indent) {
132: logger.debug("setIndent(indent=" + indent + ") - start");
133:
134: this .indent = indent;
135: }
136:
137: /**
138: * Specify the string used to terminate each line when pretty printing.
139: * It is a single newline ("\n") by default. Users who need to read
140: * generated XML documents in Windows editors like Notepad may wish to
141: * set this to a carriage return/newline sequence ("\r\n"). Specifying
142: * an empty string will turn off generation of line breaks when pretty
143: * printing.
144: *
145: * @param newline representing the newline sequence when pretty printing.
146: */
147: public void setNewline(String newline) {
148: logger.debug("setNewline(newline=" + newline + ") - start");
149:
150: this .newline = newline;
151: }
152:
153: /**
154: * A helper method. It writes out an element which contains only text.
155: *
156: * @param name String name of tag
157: * @param text String of text to go inside the tag
158: */
159: public XmlWriter writeElementWithText(String name, String text)
160: throws IOException {
161: logger.debug("writeElementWithText(name=" + name + ", text="
162: + text + ") - start");
163:
164: writeElement(name);
165: writeText(text);
166: return endElement();
167: }
168:
169: /**
170: * A helper method. It writes out empty entities.
171: *
172: * @param name String name of tag
173: */
174: public XmlWriter writeEmptyElement(String name) throws IOException {
175: logger.debug("writeEmptyElement(name=" + name + ") - start");
176:
177: writeElement(name);
178: return endElement();
179: }
180:
181: /**
182: * Begin to write out an element. Unlike the helper tags, this tag
183: * will need to be ended with the endElement method.
184: *
185: * @param name String name of tag
186: */
187: public XmlWriter writeElement(String name) throws IOException {
188: logger.debug("writeElement(name=" + name + ") - start");
189:
190: return openElement(name);
191: }
192:
193: /**
194: * Begin to output an element.
195: *
196: * @param name name of element.
197: */
198: private XmlWriter openElement(String name) throws IOException {
199: logger.debug("openElement(name=" + name + ") - start");
200:
201: boolean wasClosed = this .closed;
202: closeOpeningTag();
203: this .closed = false;
204: if (this .pretty) {
205: // ! wasClosed separates adjacent opening tags by a newline.
206: // this.wroteText makes sure an element embedded within the text of
207: // its parent element begins on a new line, indented to the proper
208: // level. This solves only part of the problem of pretty printing
209: // entities which contain both text and child entities.
210: if (!wasClosed || this .wroteText) {
211: this .out.write(newline);
212: }
213: for (int i = 0; i < this .stack.size(); i++) {
214: this .out.write(indent); // Indent opening tag to proper level
215: }
216: }
217: this .out.write("<");
218: this .out.write(name);
219: stack.add(name);
220: this .empty = true;
221: this .wroteText = false;
222: return this ;
223: }
224:
225: // close off the opening tag
226: private void closeOpeningTag() throws IOException {
227: logger.debug("closeOpeningTag() - start");
228:
229: if (!this .closed) {
230: writeAttributes();
231: this .closed = true;
232: this .out.write(">");
233: }
234: }
235:
236: // write out all current attributes
237: private void writeAttributes() throws IOException {
238: logger.debug("writeAttributes() - start");
239:
240: if (this .attrs != null) {
241: this .out.write(this .attrs.toString());
242: this .attrs.setLength(0);
243: this .empty = false;
244: }
245: }
246:
247: /**
248: * Write an attribute out for the current element.
249: * Any xml characters in the value are escaped.
250: * Currently it does not actually throw the exception, but
251: * the api is set that way for future changes.
252: *
253: * @param attr name of attribute.
254: * @param value value of attribute.
255: */
256: public XmlWriter writeAttribute(String attr, String value)
257: throws IOException {
258: logger.debug("writeAttribute(attr=" + attr + ", value=" + value
259: + ") - start");
260:
261: // maintain api
262: if (false)
263: throw new IOException();
264:
265: if (this .attrs == null) {
266: this .attrs = new StringBuffer();
267: }
268: this .attrs.append(" ");
269: this .attrs.append(attr);
270: this .attrs.append("=\"");
271: this .attrs.append(escapeXml(value));
272: this .attrs.append("\"");
273: return this ;
274: }
275:
276: /**
277: * End the current element. This will throw an exception
278: * if it is called when there is not a currently open
279: * element.
280: */
281: public XmlWriter endElement() throws IOException {
282: logger.debug("endElement() - start");
283:
284: if (this .stack.empty()) {
285: throw new IOException("Called endElement too many times. ");
286: }
287: String name = (String) this .stack.pop();
288: if (name != null) {
289: if (this .empty) {
290: writeAttributes();
291: this .out.write("/>");
292: } else {
293: if (this .pretty && !this .wroteText) {
294: for (int i = 0; i < this .stack.size(); i++) {
295: this .out.write(indent); // Indent closing tag to proper level
296: }
297: }
298: this .out.write("</");
299: this .out.write(name);
300: this .out.write(">");
301: }
302: if (this .pretty)
303: this .out.write(newline); // Add a newline after the closing tag
304: this .empty = false;
305: this .closed = true;
306: this .wroteText = false;
307: }
308: return this ;
309: }
310:
311: /**
312: * Close this writer. It does not close the underlying
313: * writer, but does throw an exception if there are
314: * as yet unclosed tags.
315: */
316: public void close() throws IOException {
317: logger.debug("close() - start");
318:
319: this .out.flush();
320:
321: if (!this .stack.empty()) {
322: throw new IOException("Tags are not all closed. "
323: + "Possibly, " + this .stack.pop()
324: + " is unclosed. ");
325: }
326: }
327:
328: /**
329: * Output body text. Any xml characters are escaped.
330: */
331: public XmlWriter writeText(String text) throws IOException {
332: logger.debug("writeText(text=" + text + ") - start");
333:
334: closeOpeningTag();
335: this .empty = false;
336: this .wroteText = true;
337: this .out.write(escapeXml(text));
338: return this ;
339: }
340:
341: /**
342: * Write out a chunk of CDATA. This helper method surrounds the
343: * passed in data with the CDATA tag.
344: *
345: * @param cdata of CDATA text.
346: */
347: public XmlWriter writeCData(String cdata) throws IOException {
348: logger.debug("writeCData(cdata=" + cdata + ") - start");
349:
350: closeOpeningTag();
351: this .empty = false;
352: this .wroteText = true;
353: this .out.write("<![CDATA[");
354: this .out.write(cdata);
355: this .out.write("]]>");
356: return this ;
357: }
358:
359: /**
360: * Write out a chunk of comment. This helper method surrounds the
361: * passed in data with the xml comment tag.
362: *
363: * @param comment of text to comment.
364: */
365: public XmlWriter writeComment(String comment) throws IOException {
366: logger.debug("writeComment(comment=" + comment + ") - start");
367:
368: writeChunk("<!-- " + comment + " -->");
369: return this ;
370: }
371:
372: private void writeChunk(String data) throws IOException {
373: logger.debug("writeChunk(data=" + data + ") - start");
374:
375: closeOpeningTag();
376: this .empty = false;
377: if (this .pretty && !this .wroteText) {
378: for (int i = 0; i < this .stack.size(); i++) {
379: this .out.write(indent);
380: }
381: }
382:
383: this .out.write(data);
384:
385: if (this .pretty) {
386: this .out.write(newline);
387: }
388: }
389:
390: // Two example methods. They should output the same XML:
391: // <person name="fred" age="12"><phone>425343</phone><bob/></person>
392: static public void main(String[] args) throws IOException {
393: logger.debug("main(args=" + args + ") - start");
394:
395: test1();
396: test2();
397: }
398:
399: static public void test1() throws IOException {
400: logger.debug("test1() - start");
401:
402: Writer writer = new java.io.StringWriter();
403: XmlWriter xmlwriter = new XmlWriter(writer);
404: xmlwriter.writeElement("person").writeAttribute("name", "fred")
405: .writeAttribute("age", "12").writeElement("phone")
406: .writeText("4254343").endElement().writeElement(
407: "friends").writeElement("bob").endElement()
408: .writeElement("jim").endElement().endElement()
409: .endElement();
410: xmlwriter.close();
411: System.err.println(writer.toString());
412: }
413:
414: static public void test2() throws IOException {
415: logger.debug("test2() - start");
416:
417: Writer writer = new java.io.StringWriter();
418: XmlWriter xmlwriter = new XmlWriter(writer);
419: xmlwriter.writeComment("Example of XmlWriter running");
420: xmlwriter.writeElement("person");
421: xmlwriter.writeAttribute("name", "fred");
422: xmlwriter.writeAttribute("age", "12");
423: xmlwriter.writeElement("phone");
424: xmlwriter.writeText("4254343");
425: xmlwriter.endElement();
426: xmlwriter.writeComment("Examples of empty tags");
427: // xmlwriter.setDefaultNamespace("test");
428: xmlwriter.writeElement("friends");
429: xmlwriter.writeEmptyElement("bob");
430: xmlwriter.writeEmptyElement("jim");
431: xmlwriter.endElement();
432: xmlwriter.writeElementWithText("foo", "This is an example.");
433: xmlwriter.endElement();
434: xmlwriter.close();
435: System.err.println(writer.toString());
436: }
437:
438: ////////////////////////////////////////////////////////////////////////////
439: // Added for DbUnit
440:
441: private String escapeXml(String str) {
442: logger.debug("escapeXml(str=" + str + ") - start");
443:
444: str = replace(str, "&", "&");
445: str = replace(str, "<", "<");
446: str = replace(str, ">", ">");
447: str = replace(str, "\"", """);
448: str = replace(str, "'", "'");
449: str = replace(str, " ", "	");
450: return str;
451: }
452:
453: private String replace(String value, String original,
454: String replacement) {
455: logger.debug("replace(value=" + value + ", original="
456: + original + ", replacement=" + replacement
457: + ") - start");
458:
459: StringBuffer buffer = null;
460:
461: int startIndex = 0;
462: int lastEndIndex = 0;
463: for (;;) {
464: startIndex = value.indexOf(original, lastEndIndex);
465: if (startIndex == -1) {
466: if (buffer != null) {
467: buffer.append(value.substring(lastEndIndex));
468: }
469: break;
470: }
471:
472: if (buffer == null) {
473: buffer = new StringBuffer(
474: (int) (original.length() * 1.5));
475: }
476: buffer.append(value.substring(lastEndIndex, startIndex));
477: buffer.append(replacement);
478: lastEndIndex = startIndex + original.length();
479: }
480:
481: return buffer == null ? value : buffer.toString();
482: }
483:
484: private void setEncoding(String encoding) {
485: logger.debug("setEncoding(encoding=" + encoding + ") - start");
486:
487: if (encoding == null && out instanceof OutputStreamWriter)
488: encoding = ((OutputStreamWriter) out).getEncoding();
489:
490: if (encoding != null) {
491: encoding = encoding.toUpperCase();
492:
493: // Use official encoding names where we know them,
494: // avoiding the Java-only names. When using common
495: // encodings where we can easily tell if characters
496: // are out of range, we'll escape out-of-range
497: // characters using character refs for safety.
498:
499: // I _think_ these are all the main synonyms for these!
500: if ("UTF8".equals(encoding)) {
501: encoding = "UTF-8";
502: } else if ("US-ASCII".equals(encoding)
503: || "ASCII".equals(encoding)) {
504: // dangerMask = (short)0xff80;
505: encoding = "US-ASCII";
506: } else if ("ISO-8859-1".equals(encoding)
507: || "8859_1".equals(encoding)
508: || "ISO8859_1".equals(encoding)) {
509: // dangerMask = (short)0xff00;
510: encoding = "ISO-8859-1";
511: } else if ("UNICODE".equals(encoding)
512: || "UNICODE-BIG".equals(encoding)
513: || "UNICODE-LITTLE".equals(encoding)) {
514: encoding = "UTF-16";
515:
516: // TODO: UTF-16BE, UTF-16LE ... no BOM; what
517: // release of JDK supports those Unicode names?
518: }
519:
520: // if (dangerMask != 0)
521: // stringBuf = new StringBuffer();
522: }
523:
524: this .encoding = encoding;
525: }
526:
527: /**
528: * Resets the handler to write a new text document.
529: *
530: * @param writer XML text is written to this writer.
531: * @param encoding if non-null, and an XML declaration is written,
532: * this is the name that will be used for the character encoding.
533: *
534: * @exception IllegalStateException if the current
535: * document hasn't yet ended (with {@link #endDocument})
536: */
537: final public void setWriter(Writer writer, String encoding) {
538: logger.debug("setWriter(writer=" + writer + ", encoding="
539: + encoding + ") - start");
540:
541: if (this .out != null)
542: throw new IllegalStateException(
543: "can't change stream in mid course");
544: this .out = writer;
545: if (this .out != null)
546: setEncoding(encoding);
547: // if (!(this.out instanceof BufferedWriter))
548: // this.out = new BufferedWriter(this.out);
549: }
550:
551: public XmlWriter writeDeclaration() throws IOException {
552: logger.debug("writeDeclaration() - start");
553:
554: if (this .encoding != null) {
555: this .out.write("<?xml version='1.0'");
556: this .out.write(" encoding='" + this .encoding + "'");
557: this .out.write("?>");
558: this .out.write(this .newline);
559: }
560:
561: return this ;
562: }
563:
564: public XmlWriter writeDoctype(String systemId, String publicId)
565: throws IOException {
566: logger.debug("writeDoctype(systemId=" + systemId
567: + ", publicId=" + publicId + ") - start");
568:
569: if (systemId != null || publicId != null) {
570: this .out.write("<!DOCTYPE dataset");
571:
572: if (systemId != null) {
573: this .out.write(" SYSTEM \"");
574: this .out.write(systemId);
575: this .out.write("\"");
576: }
577:
578: if (publicId != null) {
579: this .out.write(" PUBLIC \"");
580: this .out.write(publicId);
581: this .out.write("\"");
582: }
583:
584: this .out.write(">");
585: this.out.write(this.newline);
586: }
587:
588: return this;
589: }
590:
591: }
|