1/**
2 * Copyright (c) 2004-2006 Regents of the University of California.
3  All rights reserved.
4
5  Redistribution and use in source and binary forms, with or without
6  modification, are permitted provided that the following conditions
7  are met:
8
9  1. Redistributions of source code must retain the above copyright
10  notice, this list of conditions and the following disclaimer.
11
12  2. Redistributions in binary form must reproduce the above copyright
13  notice and this list of conditions.
14
15  3. The name of the University may not be used to endorse or promote products
16  derived from this software without specific prior written permission.
17
18  THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
19  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21  ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
22  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
24  OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25  HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26  LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
27  OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
28  SUCH DAMAGE.
29 */
30
31package org.jheer;
32
33//import java.io.PrintWriter;
34import java.io.FileWriter;
35import java.io.IOException;
36import java.util.ArrayList;
37
38/**
39 * Utility class for writing XML files. This class provides convenience
40 * methods for creating XML documents, such as starting and ending
41 * tags, and adding content and comments. This class handles correct
42 * XML formatting and will properly escape text to ensure that the
43 * text remains valid XML.
44 *
45 * <p>To use this class, create a new instance with the desired
46 * [Print]FileWriter to write the XML to. Call the {@link #begin()} or
47 * {@link #begin(String, int)} method when ready to start outputting
48 * XML. Then use the provided methods to generate the XML file.
49 * Finally, call either the {@link #finish()} or {@link #finish(String)}
50 * methods to signal the completion of the file.</p>
51 *
52 * @author <a href="http://jheer.org">jeffrey heer</a>
53 *
54 * Modified to take a FileWriter and now throws IOException.
55 */
56
57public class XMLWriter {
58
59//    private PrintWriter m_out;
60    private FileWriter m_out;
61    private int m_bias = 0;
62    private int m_tab;
63    private ArrayList m_tagStack = new ArrayList();
64
65    /**
66     * Create a new XMLWriter.
67     * @param out the  FileWriter to write the XML to
68     */
69//    public XMLWriter(PrintWriter out) {
70    public XMLWriter(FileWriter out) {
71        this(out, 2);
72    }
73
74    /**
75     * Create a new XMLWriter.
76     * @param out the FileWriter to write the XML to
77     * @param tabLength the number of spaces to use for each
78     *  level of indentation in the XML file
79     */
80//    public XMLWriter(PrintWriter out, int tabLength) {
81    public XMLWriter(FileWriter out, int tabLength) {
82        m_out = out;
83        m_tab = 2;
84    }
85
86    /**
87     * Write <em>unescaped</em> text into the XML file. To write
88     * escaped text, use the {@link #content(String)} method instead.
89     * @param s the text to write. This String will not be escaped.
90     */
91    public void write(String s) throws IOException {
92        m_out.write(s);
93    }
94
95    /**
96     * Write <em>unescaped</em> text into the XML file, followed by
97     * a newline. To write escaped text, use the {@link #content(String)}
98     * method instead.
99     * @param s the text to write. This String will not be escaped.
100     */
101    public void writeln(String s) throws IOException {
102        m_out.write(s);
103        m_out.write("\n");
104    }
105
106    /**
107     * Write a newline into the XML file.
108     */
109    public void writeln() throws IOException {
110        m_out.write("\n");
111    }
112
113    /**
114     * Begin the XML document. This must be called before any other
115     * formatting methods. This method writes an XML header into
116     * the top of the output stream.
117     */
118    public void begin() throws IOException {
119        m_out.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
120        writeln();
121    }
122
123    /**
124     * Begin the XML document. This must be called before any other
125     * formatting methods. This method writes an XML header into
126     * the top of the output stream, plus additional header text
127     * provided by the client
128     * @param header header text to insert into the document
129     * @param bias the spacing bias to use for all subsequent indenting
130     */
131    public void begin(String header, int bias) throws IOException {
132        begin();
133        m_out.write(header);
134        m_bias = bias;
135    }
136
137    /**
138     * Write a comment in the XML document. The comment will be written
139     * according to the current spacing and followed by a newline.
140     * @param comment the comment text
141     */
142    public void comment(String comment) throws IOException {
143        spacing();
144        m_out.write("<!-- ");
145        m_out.write(comment);
146        m_out.write(" -->");
147        writeln();
148    }
149
150    /**
151     * Internal method for writing a tag with attributes.
152     * @param tag the tag name
153     * @param names the names of the attributes
154     * @param values the values of the attributes
155     * @param nattr the number of attributes
156     * @param close true to close the tag, false to leave it
157     * open and adjust the spacing
158     */
159    protected void tag(String tag, String[] names, String[] values,
160            int nattr, boolean close) throws IOException
161    {
162        spacing();
163        m_out.write('<');
164        m_out.write(tag);
165        for ( int i=0; i<nattr; ++i ) {
166            m_out.write(' ');
167            m_out.write(names[i]);
168            m_out.write('=');
169            m_out.write('\"');
170            escapeString(values[i]);
171            m_out.write('\"');
172        }
173        if ( close ) m_out.write('/');
174        m_out.write('>');
175        writeln();
176
177        if ( !close ) {
178            m_tagStack.add(tag);
179        }
180    }
181
182    /**
183     * Write a closed tag with attributes. The tag will be followed by a
184     * newline.
185     * @param tag the tag name
186     * @param names the names of the attributes
187     * @param values the values of the attributes
188     * @param nattr the number of attributes
189     */
190    public void tag(String tag, String[] names, String[] values, int nattr) throws IOException
191    {
192        tag(tag, names, values, nattr, true);
193    }
194
195    /**
196     * Write a start tag with attributes. The tag will be followed by a
197     * newline, and the indentation level will be increased.
198     * @param tag the tag name
199     * @param names the names of the attributes
200     * @param values the values of the attributes
201     * @param nattr the number of attributes
202     */
203    public void start(String tag, String[] names, String[] values, int nattr) throws IOException
204    {
205        tag(tag, names, values, nattr, false);
206    }
207
208    /**
209     * Write a new attribut to an existing tag.  The attribute will be followed by a newline.
210     * @param name the name of the attribute
211     * @param value the value of the attribute
212     */
213     public void addAttribute(String name, String value) throws IOException {
214        spacing();
215        m_out.write(name);
216        m_out.write('=');
217        m_out.write('\"');
218        escapeString(value);
219        m_out.write('\"');
220        writeln();
221     }
222
223     /**
224     * Internal method for writing a tag with a single attribute.
225     * @param tag the tag name
226     * @param name the name of the attribute
227     * @param value the value of the attribute
228     * @param close true to close the tag, false to leave it
229     * open and adjust the spacing
230     */
231    protected void tag(String tag, String name, String value, boolean close) throws IOException {
232        spacing();
233        m_out.write('<');
234        m_out.write(tag);
235        m_out.write(' ');
236        m_out.write(name);
237        m_out.write('=');
238        m_out.write('\"');
239        escapeString(value);
240        m_out.write('\"');
241        if ( close ) m_out.write('/');
242        m_out.write('>');
243        writeln();
244
245        if ( !close ) {
246            m_tagStack.add(tag);
247        }
248    }
249
250    /**
251     * Write a closed tag with one attribute. The tag will be followed by a
252     * newline.
253     * @param tag the tag name
254     * @param name the name of the attribute
255     * @param value the value of the attribute
256     */
257    public void tag(String tag, String name, String value) throws IOException
258    {
259        tag(tag, name, value, true);
260    }
261
262    /**
263     * Write a start tag with one attribute. The tag will be followed by a
264     * newline, and the indentation level will be increased.
265     * @param tag the tag name
266     * @param name the name of the attribute
267     * @param value the value of the attribute
268     */
269    public void start(String tag, String name, String value) throws IOException
270    {
271        tag(tag, name, value, false);
272    }
273
274    /**
275     * Internal method for writing a tag with attributes.
276     * @param tag the tag name
277     * @param names the names of the attributes
278     * @param values the values of the attributes
279     * @param nattr the number of attributes
280     * @param close true to close the tag, false to leave it
281     * open and adjust the spacing
282     */
283    protected void tag(String tag, ArrayList names, ArrayList values,
284            int nattr, boolean close) throws IOException
285    {
286        spacing();
287        m_out.write('<');
288        m_out.write(tag);
289        for ( int i=0; i<nattr; ++i ) {
290            m_out.write(' ');
291            m_out.write((String)names.get(i));
292            m_out.write('=');
293            m_out.write('\"');
294            escapeString((String)values.get(i));
295            m_out.write('\"');
296        }
297        if ( close ) m_out.write('/');
298        m_out.write('>');
299        writeln();
300
301        if ( !close ) {
302            m_tagStack.add(tag);
303        }
304    }
305
306    /**
307     * Write a closed tag with attributes. The tag will be followed by a
308     * newline.
309     * @param tag the tag name
310     * @param names the names of the attributes
311     * @param values the values of the attributes
312     * @param nattr the number of attributes
313     */
314    public void tag(String tag, ArrayList names, ArrayList values, int nattr) throws IOException
315    {
316        tag(tag, names, values, nattr, true);
317    }
318
319    /**
320     * Write a start tag with attributes. The tag will be followed by a
321     * newline, and the indentation level will be increased.
322     * @param tag the tag name
323     * @param names the names of the attributes
324     * @param values the values of the attributes
325     * @param nattr the number of attributes
326     */
327    public void start(String tag, ArrayList names, ArrayList values, int nattr) throws IOException
328    {
329        tag(tag, names, values, nattr, false);
330    }
331
332    /**
333     * Write a start tag without attributes. The tag will be followed by a
334     * newline, and the indentation level will be increased.
335     * @param tag the tag name
336     */
337    public void start(String tag) throws IOException {
338        tag(tag, (String[])null, null, 0, false);
339    }
340
341    /**
342     * Close the most recently opened tag. The tag will be followed by a
343     * newline, and the indentation level will be decreased.
344     */
345    public void end() throws IOException {
346        String tag = (String)m_tagStack.remove(m_tagStack.size()-1);
347        spacing();
348        m_out.write('<');
349        m_out.write('/');
350        m_out.write(tag);
351        m_out.write('>');
352        writeln();
353    }
354
355    /**
356     * Write a new content tag with a single attribute, consisting of an
357     * open tag, content text, and a closing tag, all on one line.
358     * @param tag the tag name
359     * @param name the name of the attribute
360     * @param value the value of the attribute, this text will be escaped
361     * @param content the text content, this text will be escaped
362     */
363    public void contentTag(String tag, String name, String value, String content) throws IOException
364    {
365        spacing();
366        m_out.write('<'); m_out.write(tag); m_out.write(' ');
367        m_out.write(name); m_out.write('=');
368        m_out.write('\"'); escapeString(value); m_out.write('\"');
369        m_out.write('>');
370        escapeString(content);
371        m_out.write('<'); m_out.write('/'); m_out.write(tag); m_out.write('>');
372        writeln();
373    }
374
375    /**
376     * Write a new content tag with no attributes, consisting of an
377     * open tag, content text, and a closing tag, all on one line.
378     * @param tag the tag name
379     * @param content the text content, this text will be escaped
380     */
381    public void contentTag(String tag, String content) throws IOException {
382        spacing();
383        m_out.write('<'); m_out.write(tag); m_out.write('>');
384        escapeString(content);
385        m_out.write('<'); m_out.write('/'); m_out.write(tag); m_out.write('>');
386        writeln();
387    }
388
389    /**
390     * Write content text.
391     * @param content the content text, this text will be escaped
392     */
393    public void content(String content) throws IOException {
394        escapeString(content);
395    }
396
397    /**
398     * Finish the XML document.
399     */
400    public void finish() throws IOException {
401        m_bias = 0;
402        m_out.flush();
403    }
404
405    /**
406     * Finish the XML document, writing the given footer text at the
407     * end of the document.
408     * @param footer the footer text, this will not be escaped
409     */
410    public void finish(String footer) throws IOException {
411        m_bias = 0;
412        m_out.write(footer);
413        m_out.flush();
414    }
415
416    /**
417     * Write the current spacing (determined by the indentation level)
418     * into the document. This method is used by many of the other
419     * formatting methods, and so should only need to be called in
420     * the case of custom text writing outside the mechanisms
421     * provided by this class.
422     */
423    public void spacing() throws IOException {
424        int len = m_bias + m_tagStack.size() * m_tab;
425        for ( int i=0; i<len; ++i )
426            m_out.write(' ');
427    }
428
429    // ------------------------------------------------------------------------
430    // Escape Text
431
432    // unicode ranges and valid/invalid characters
433    private static final char   LOWER_RANGE = 0x20;
434    private static final char   UPPER_RANGE = 0x7f;
435    private static final char[] VALID_CHARS = { 0x9, 0xA, 0xD };
436
437    private static final char[] INVALID = { '<', '>', '"', '\'', '&' };
438    private static final String[] VALID =
439        { "&lt;", "&gt;", "&quot;", "&apos;", "&amp;" };
440
441    /**
442     * Escape a string such that it is safe to use in an XML document.
443     * @param str the string to escape
444     */
445    protected void escapeString(String str) throws IOException {
446        if ( str == null ) {
447            m_out.write("null");
448            return;
449        }
450
451        int len = str.length();
452        for (int i = 0; i < len; ++i) {
453            char c = str.charAt(i);
454
455            if ( (c < LOWER_RANGE     && c != VALID_CHARS[0] &&
456                  c != VALID_CHARS[1] && c != VALID_CHARS[2])
457                 || (c > UPPER_RANGE) )
458            {
459                // character out of range, escape with character value
460                m_out.write("&#");
461                m_out.write(Integer.toString(c));
462                m_out.write(';');
463            } else {
464                boolean valid = true;
465                // check for invalid characters (e.g., "<", "&", etc)
466                for (int j=INVALID.length-1; j >= 0; --j )
467                {
468                    if ( INVALID[j] == c) {
469                        valid = false;
470                        m_out.write(VALID[j]);
471                        break;
472                    }
473                }
474                // if character is valid, don't escape
475                if (valid) {
476                    m_out.write(c);
477                }
478            }
479        }
480    }
481
482} // end of class XMLWriter
483
484