001/*
002// $Id: //open/util/resgen/src/org/eigenbase/xom/XMLOutput.java#5 $
003// Package org.eigenbase.xom is an XML Object Mapper.
004// Copyright (C) 2005-2008 The Eigenbase Project
005// Copyright (C) 2005-2008 Disruptive Tech
006// Copyright (C) 2005-2008 LucidEra, Inc.
007// Portions Copyright (C) 2000-2005 Kana Software, Inc. and others.
008//
009// This library is free software; you can redistribute it and/or modify it
010// under the terms of the GNU Lesser General Public License as published by the
011// Free Software Foundation; either version 2 of the License, or (at your
012// option) any later version approved by The Eigenbase Project.
013//
014// This library is distributed in the hope that it will be useful, 
015// but WITHOUT ANY WARRANTY; without even the implied warranty of
016// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
017// GNU Lesser General Public License for more details.
018// 
019// You should have received a copy of the GNU Lesser General Public License
020// along with this library; if not, write to the Free Software
021// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
022//
023// dsommerfield, 12 December, 2000
024*/
025
026package org.eigenbase.xom;
027
028import java.io.*;
029import java.util.Vector;
030
031/**
032 * XMLOutput is a class which implements streaming XML output.  Use this class
033 * to write XML to any streaming source.  While the class itself is
034 * unstructured and doesn't enforce any DTD specification, use of the class
035 * does ensure that the output is syntactically valid XML.
036 */
037public class XMLOutput {
038
039    // This Writer is the underlying output stream to which all XML is
040    // written.
041    private PrintWriter out;
042
043    // The tagStack is maintained to check that tags are balanced.
044    private Vector tagStack;
045
046    // The class maintains an indentation level to improve output quality.
047    private int indent;
048
049    // The class also maintains the total number of tags written.  This
050    // is used to monitor changes to the output
051    private int tagsWritten;
052
053    // This flag is set to true if the output should be compacted.
054    // Compacted output is free of extraneous whitespace and is designed
055    // for easier transport.
056    private boolean compact;
057
058    /** @see setIndentString **/
059    private String indentString = "\t";
060
061    /** @see setGlob **/
062    private boolean glob;
063
064    /**
065     * Whether we have started but not finished a start tag. This only happens
066     * if <code>glob</code> is true. The start tag is automatically closed
067     * when we start a child node. If there are no child nodes, {@link #endTag}
068     * creates an empty tag.
069     **/
070    private boolean inTag;
071
072    /** @see #setAlwaysQuoteCData */
073    private boolean alwaysQuoteCData;
074
075    /** @see #setIgnorePcdata **/
076    private boolean ignorePcdata;
077
078    /**
079     * Private helper function to display a degree of indentation
080     * @param out the PrintWriter to which to display output.
081     * @param indent the degree of indentation.
082     */
083    private void displayIndent(PrintWriter out, int indent)
084    {
085        if(!compact) {
086            for (int i = 0; i < indent; i++) {
087                out.print(indentString);
088            }
089        }
090    }
091
092    /**
093     * Constructs a new XMLOutput based on any Writer.
094     * @param out the writer to which this XMLOutput generates results.
095     */
096    public XMLOutput(Writer out)
097    {
098        this.out = new PrintWriter(out, true);
099        indent = 0;
100        tagsWritten = 0;
101        tagStack = new Vector();
102    }
103
104    /**
105     * Sets or unsets the compact mode.  Compact mode causes the generated
106     * XML to be free of extraneous whitespace and other unnecessary
107     * characters.
108     *
109     * @param compact true to turn on compact mode, or false to turn it off.
110     */
111    public void setCompact(boolean compact)
112    {
113        this.compact = compact;
114    }
115
116    public boolean getCompact()
117    {
118        return compact;
119    }
120
121    /**
122     * Sets the string to print for each level of indentation. The default is a
123     * tab. The value must not be <code>null</code>. Set this to the empty
124     * string to achieve no indentation (note that <code>{@link
125     * #setCompact}(true)</code> removes indentation <em>and</em> newlines).
126     **/
127    public void setIndentString(String indentString)
128    {
129        this.indentString = indentString;
130    }
131
132    /**
133     * Sets whether to detect that tags are empty.
134     **/
135    public void setGlob(boolean glob)
136    {
137        this.glob = glob;
138    }
139
140    /**
141     * Sets whether to always quote cdata segments (even if they don't contain
142     * special characters).
143     **/
144    public void setAlwaysQuoteCData(boolean alwaysQuoteCData)
145    {
146        this.alwaysQuoteCData = alwaysQuoteCData;
147    }
148
149    /**
150     * Sets whether to ignore unquoted text, such as whitespace.
151     **/
152    public void setIgnorePcdata(boolean ignorePcdata)
153    {
154        this.ignorePcdata = ignorePcdata;
155    }
156
157    public boolean getIgnorePcdata()
158    {
159        return ignorePcdata;
160    }
161
162    /**
163     * Sends a string directly to the output stream, without escaping any
164     * characters.  Use with caution!
165     **/
166    public void print(String s)
167    {
168        out.print(s);
169    }
170
171    /**
172     * Start writing a new tag to the stream.  The tag's name must be given and
173     * its attributes should be specified by a fully constructed AttrVector
174     * object.
175     * @param tagName the name of the tag to write.
176     * @param attributes an XMLAttrVector containing the attributes to include
177     * in the tag.
178     */
179    public void beginTag(String tagName, XMLAttrVector attributes)
180    {
181        beginBeginTag(tagName);
182        if (attributes != null) {
183            attributes.display(out, indent);
184        }
185        endBeginTag(tagName);
186    }
187
188    public void beginBeginTag(String tagName)
189    {
190        if (inTag) {
191            // complete the parent's start tag
192            if (compact) {
193                out.print(">");
194            } else {
195                out.println(">");
196            }
197            inTag = false;
198        }
199        displayIndent(out, indent);
200        out.print("<");
201        out.print(tagName);
202    }
203
204    public void endBeginTag(String tagName)
205    {
206        if (glob) {
207            inTag = true;
208        } else if (compact) {
209            out.print(">");
210        } else {
211            out.println(">");
212        }
213        out.flush();
214        tagStack.addElement(tagName);
215        indent++;
216        tagsWritten++;
217    }
218
219    /**
220     * Write an attribute.
221     **/
222    public void attribute(String name, String value)
223    {
224        XMLUtil.printAtt(out, name, value);
225    }
226
227    /**
228     * If we are currently inside the start tag, finish it off.
229     **/
230    public void beginNode()
231    {
232        if (inTag) {
233            // complete the parent's start tag
234            if (compact) {
235                out.print(">");
236            } else {
237                out.println(">");
238            }
239            inTag = false;
240        }
241    }
242
243    /**
244     * Complete a tag.  This outputs the end tag corresponding to the
245     * last exposed beginTag.  The tag name must match the name of the
246     * corresponding beginTag.
247     * @param tagName the name of the end tag to write.
248     */
249    public void endTag(String tagName)
250    {
251        // Check that the end tag matches the corresponding start tag
252        int stackSize = tagStack.size();
253        String matchTag = (String)(tagStack.elementAt(stackSize-1));
254        if(!tagName.equalsIgnoreCase(matchTag))
255            throw new AssertFailure(
256                "End tag <" + tagName + "> does not match " +
257                " start tag <" + matchTag + ">");
258        tagStack.removeElementAt(stackSize-1);
259
260        // Lower the indent and display the end tag
261        indent--;
262        if (inTag) {
263            // we're still in the start tag -- this element had no children
264            if (compact) {
265                out.print("/>");
266            } else {
267                out.println("/>");
268            }
269            inTag = false;
270        } else {
271            displayIndent(out, indent);
272            out.print("</");
273            out.print(tagName);
274            if (compact) {
275                out.print(">");
276            } else {
277                out.println(">");
278            }
279        }
280        out.flush();
281    }
282
283    /**
284     * Write an empty tag to the stream.  An empty tag is one with no
285     * tags inside it, although it may still have attributes.
286     * @param tagName the name of the empty tag.
287     * @param attributes an XMLAttrVector containing the attributes to
288     * include in the tag.
289     */
290    public void emptyTag(String tagName, XMLAttrVector attributes)
291    {
292        if (inTag) {
293            // complete the parent's start tag
294            if (compact) {
295                out.print(">");
296            } else {
297                out.println(">");
298            }
299            inTag = false;
300        }
301        displayIndent(out, indent);
302        out.print("<");
303        out.print(tagName);
304        if(attributes != null) {
305            out.print(" ");
306            attributes.display(out, indent);
307        }
308
309        if(compact)
310            out.print("/>");
311        else
312            out.println("/>");
313        out.flush();
314        tagsWritten++;
315    }
316
317    /**
318     * Write a CDATA section.  Such sections always appear on their own line.
319     * The nature in which the CDATA section is written depends on the actual
320     * string content with respect to these special characters/sequences:
321     * <ul>
322     * <li><code>&amp;</code>
323     * <li><code>&quot;</code>
324     * <li><code>&apos;</code>
325     * <li><code>&lt;</code>
326     * <li><code>&gt;</code>
327     * </ul>
328     * Additionally, the sequence <code>]]&gt;</code> is special.
329     * <ul>
330     * <li>Content containing no special characters will be left as-is.
331     * <li>Content containing one or more special characters but not the
332     * sequence <code>]]&gt;</code> will be enclosed in a CDATA section.
333     * <li>Content containing special characters AND at least one
334     * <code>]]&gt;</code> sequence will be left as-is but have all of its
335     * special characters encoded as entities.
336     * </ul>
337     * These special treatment rules are required to allow cdata sections
338     * to contain XML strings which may themselves contain cdata sections.
339     * Traditional CDATA sections <b>do not nest</b>.
340     */
341    public void cdata(String data)
342    {
343        cdata(data, false);
344    }
345
346    /**
347     * Writes a CDATA section (as {@link #cdata(String)}).
348     *
349     * @param data string to write
350     * @param quote if true, quote in a <code>&lt;![CDATA[</code>
351     *        ... <code>]]&gt;</code> regardless of the content of
352     *        <code>data</code>; if false, quote only if the content needs it
353     **/
354    public void cdata(String data, boolean quote)
355    {
356        if (inTag) {
357            // complete the parent's start tag
358            if (compact) {
359                out.print(">");
360            } else {
361                out.println(">");
362            }
363            inTag = false;
364        }
365        if (data == null) {
366            data = "";
367        }
368        boolean specials = false;
369        boolean cdataEnd = false;
370
371        // Scan the string for special characters
372        // If special characters are found, scan the string for ']]>'
373        if(XOMUtil.stringHasXMLSpecials(data)) {
374            specials = true;
375            if(data.indexOf("]]>") > -1)
376                cdataEnd = true;
377        }
378
379        // Display the result
380        displayIndent(out, indent);
381        if (quote || alwaysQuoteCData) {
382            out.print("<![CDATA[");
383            out.print(data);
384            out.println("]]>");
385        } else if (!specials && !cdataEnd) {
386            out.print(data);
387        } else {
388            XMLUtil.stringEncodeXML(data, out);
389        }
390
391        out.flush();
392        tagsWritten++;
393    }
394
395    /**
396     * Write a String tag; a tag containing nothing but a CDATA section.
397     */
398    public void stringTag(String name, String data)
399    {
400        beginTag(name, null);
401        cdata(data);
402        endTag(name);
403    }
404
405    /**
406     * Write content.
407     */
408    public void content(String content)
409    {
410        if(content != null) {
411            indent++;
412            LineNumberReader in = new LineNumberReader(new StringReader(content));
413            try {
414                String line;
415                while((line = in.readLine()) != null) {
416                    displayIndent(out, indent);
417                    out.println(line);
418                }
419            } catch (IOException ex) {
420                throw new AssertFailure(ex);
421            }
422            indent--;
423            out.flush();
424        }
425        tagsWritten++;
426    }
427
428    /**
429     *  Write header. Use default version 1.0.
430     */
431    public void header()
432    {
433        out.println("<?xml version=\"1.0\" ?>");
434        out.flush();
435        tagsWritten++;
436    }
437
438    /**
439     * Write header, take version as input.
440     */
441    public void header(String version)
442    {
443        out.print("<?xml version=\"");
444        out.print(version);
445        out.println("\" ?>");
446        out.flush();
447        tagsWritten++;
448    }
449
450    /**
451     * Get the total number of tags written
452     * @return the total number of tags written to the XML stream.
453     */
454    public int numTagsWritten()
455    {
456        return tagsWritten;
457    }
458
459}
460
461
462// End XMLOutput.java