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    
026    package org.eigenbase.xom;
027    
028    import java.io.*;
029    import 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     */
037    public 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