001    /*
002     * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons-sandbox//xmlio/src/java/org/apache/commons/xmlio/out/XMLWriter.java,v 1.1 2004/10/08 11:56:20 ozeigermann Exp $
003     * $Revision: 155476 $
004     * $Date: 2005-02-26 13:31:24 +0000 (Sat, 26 Feb 2005) $
005     *
006     * ====================================================================
007     *
008     * Copyright 2004 The Apache Software Foundation 
009     *
010     * Licensed under the Apache License, Version 2.0 (the "License");
011     * you may not use this file except in compliance with the License.
012     * You may obtain a copy of the License at
013     *
014     *     http://www.apache.org/licenses/LICENSE-2.0
015     *
016     * Unless required by applicable law or agreed to in writing, software
017     * distributed under the License is distributed on an "AS IS" BASIS,
018     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
019     * See the License for the specific language governing permissions and
020     * limitations under the License.
021     *
022     */
023    
024    package org.apache.commons.xmlio.out;
025    
026    import java.io.*;
027    
028    import org.xml.sax.Attributes;
029    
030    /**
031     * {@link FilterWriter} adding formatted and encoded XML export 
032     * functionality to the underlying writer. Formatting and
033     * encoding is done as straight forward as possible. <br>
034     * Everything you know better than this class must be done by you, e.g. you will
035     * have to tell <code>XMLWriter</code> where you wish to have
036     * newlines.In effect, no unexpected so called
037     * <em>intelligent</em> behavior is to be feared. Another effect is high speed.
038     * <br>
039     * <br>
040     * A simple example: Suppose your <code>XMLWriter</code> object is xmlWriter.
041     * The following sequence of code <br><br>
042     * <code>
043     * &nbsp;&nbsp;xmlWriter.writeStartTag("&lt;root>");<br>
044     * &nbsp;&nbsp;xmlWriter.writeStartTag("&lt;next1>", false);<br>
045     * &nbsp;&nbsp;xmlWriter.writeEmptyTag("&lt;emptyTag/>", false);<br>
046     * &nbsp;&nbsp;xmlWriter.writeEndTag("&lt;/next1>");<br>
047     * &nbsp;&nbsp;xmlWriter.writeStartTag("&lt;/root>");<br>
048     * </code>
049     * <br>
050     * will write this to the underlying writer<br><br>
051     * <code>
052     * &lt;root><br>
053     * &nbsp;&nbsp;&lt;next1>&lt;emptyTag/>&lt;/next1><br>
054     * &lt;/root><br>
055     *</code>
056     * <br>
057     * <br>
058     * <em>Caution</em>: Do not forget to call {@link #flush} at the end of your
059     * exporting process as otherwise no data might be written.
060     *
061     */
062    public class XMLWriter extends FilterWriter {
063    
064        public final static boolean NEWLINE = true;
065        public final static boolean NO_NEWLINE = false;
066    
067        protected int tabWidth = 2;
068    
069        /** Current depth of the tree. Do not know what this is good for, but
070         * who knows...
071         */
072        protected int depth = 0;
073    
074        /** Current indentation. Depth does not contain sufficient information as 
075         * tabWidth may change during output (should not).
076         */
077        protected int indent = 0;
078    
079        protected boolean prettyPrintMode = true;
080    
081        protected boolean nlAfterEmptyTag = true;
082        protected boolean nlAfterStartTag = true;
083        protected boolean nlAfterEndTag = true;
084    
085        /** Flag indicating if the XML declaration has already been writter.
086         * Check this using {@link #isXMLDeclarationWritten()}. 
087         * It might be useful to 
088         * avoid writing twice or more times in different contexts writing
089         * to same writer. 
090         * <br>
091         * <em>Caution</em>: If you subclass, be sure to set this in
092         * {@link #writeXMLDeclaration()}.
093         */
094        protected boolean xmlDeclWritten = false;
095    
096        private boolean needsIndent = false;
097        private boolean indentStringCacheValid = true;
098        private String indentStringCache = "";
099    
100        /** Convenience method for creating an end tag.
101         * @param tagName name of the end tag
102         */
103        public final static String createEndTag(String tagName) {
104            return "</" + tagName + ">";
105        }
106    
107        /** Convenience method for creating a start tag having no attributes.
108         * @param tagName name of the start tag
109         */
110        public final static String createStartTag(String tagName) {
111            return "<" + tagName + ">";
112        }
113    
114        /** Convenience method for creating an <em>empty</em> tag 
115         * having no attributes. E.g. <code>&lt;tagName/></code>. 
116         * @param tagName name of the tag
117         */
118        public final static String createEmptyTag(String tagName) {
119            return "<" + tagName + "/>";
120        }
121    
122        /** Convenience method for creating a start tag.
123         * @param tagName name of the start tag
124         * @param attrNames names of attributes to be included into start tag
125         * @param attrValues values of attributes to be included into start tag -
126         * there should be just as many entries as in <code>attrNames</code>,
127         * if a value is <code>null</code> corresponding attribute will not be included
128         * @param isEmpty decides wheter this is start tag is for an empty element
129         */
130        public final static String createStartTag(
131            String tagName,
132            String[] attrNames,
133            String[] attrValues,
134            boolean isEmpty) {
135            return createStartTag(tagName, attrNames, attrValues, isEmpty, true, '"');
136        }
137    
138        /** Convenience method for creating a <em>non empty</em> start tag.
139         * @param tagName name of the start tag
140         * @param attrNames names of attributes to be included into start tag
141         * @param attrValues values of attributes to be included into start tag -
142         * there should be just as many entries as in <code>attrNames</code>,
143         * if a value is <code>null</code> corresponding attribute will not be included
144         */
145        public final static String createStartTag(String tagName, String[] attrNames, String[] attrValues) {
146            return createStartTag(tagName, attrNames, attrValues, false);
147        }
148    
149        /** Convenience method for creating an <em>empty</em> tag.
150         * @param tagName name of the tag
151         * @param attrNames names of attributes to be included into tag
152         * @param attrValues values of attributes to be included into tag -
153         * there should be just as many entries as in <code>attrNames</code>,
154         * if a value is <code>null</code> corresponding attribute will not be included
155         * @see #createEmptyTag(String)
156         */
157        public final static String createEmptyTag(String tagName, String[] attrNames, String[] attrValues) {
158            return createStartTag(tagName, attrNames, attrValues, true);
159        }
160    
161        /** Convenience method for creating a start tag.
162         * @param tagName name of the start tag
163         * @param attrName name of attribute to be included into start tag
164         * @param attrValue value of attribute to be included into start tag,
165         * if attrValue is <code>null</code> attribute will not be included
166         * @param isEmpty decides wheter this is start tag is for an empty element
167         */
168        public final static String createStartTag(String tagName, String attrName, String attrValue, boolean isEmpty) {
169            return createStartTag(tagName, new String[] { attrName }, new String[] { attrValue }, isEmpty);
170        }
171    
172        /** Convenience method for creating a <em>non empty</em> start tag.
173         * @param tagName name of the start tag
174         * @param attrName name of attribute to be included into start tag
175         * @param attrValue value of attribute to be included into start tag,
176         * if attrValue is <code>null</code> attribute will not be included
177         */
178        public final static String createStartTag(String tagName, String attrName, String attrValue) {
179            return createStartTag(tagName, attrName, attrValue, false);
180        }
181    
182        /** Convenience method for creating an <em>empty</em> tag.
183         * @param tagName name of the tag
184         * @param attrName name of attribute to be included into tag
185         * @param attrValue value of attribute to be included into tag,
186         * if attrValue is <code>null</code> attribute will not be included
187         * @see #createEmptyTag(String)
188         */
189        public final static String createEmptyTag(String tagName, String attrName, String attrValue) {
190            return createStartTag(tagName, attrName, attrValue, true);
191        }
192    
193        /** Convenience method for creating a start tag.
194         * @param tagName name of the start tag
195         * @param attrNames names of attributes to be included into start tag
196         * @param attrValues values of attributes to be included into start tag -
197         * there should be just as many entries as in <code>attrNames</code>,
198         * if a value is <code>null</code> corresponding attribute will not be included
199         * @param isEmpty decides wheter this is start tag is for an empty element
200         * @param encodeAttrs set this to have your attribute values encoded for XML
201         * @param quoteChar if you choose encoding this is the char that quotes
202         * your attributes
203         */
204        public final static String createStartTag(
205            String tagName,
206            String[] attrNames,
207            String[] attrValues,
208            boolean isEmpty,
209            boolean encodeAttrs,
210            char quoteChar) {
211            // estimate buffer size
212            StringBuffer buf = new StringBuffer((attrNames.length + 1) * 15);
213            buf.append('<').append(tagName);
214    
215            if (attrNames.length != 0 && (attrNames.length <= attrValues.length)) {
216                for (int i = 0; i < attrNames.length; i++) {
217                    String name = attrNames[i];
218                    String value = attrValues[i];
219                    if (value == null)
220                        continue;
221                    if (encodeAttrs)
222                        value = XMLEncode.xmlEncodeTextForAttribute(value, quoteChar);
223                    buf.append(' ').append(name).append('=').append(value);
224                }
225            }
226    
227            if (isEmpty) {
228                buf.append("/>");
229            } else {
230                buf.append('>');
231            }
232            return buf.toString();
233        }
234    
235        /** Convenience method for creating a start tag.
236         * @param tagName name of the start tag
237         * @param attrPairs name/value pairs of attributes to be included into start tag -
238         * if a value is <code>null</code> corresponding attribute will not be included
239         * @param isEmpty decides wheter this is start tag is for an empty element
240         */
241        public final static String createStartTag(String tagName, String[][] attrPairs, boolean isEmpty) {
242            return createStartTag(tagName, attrPairs, isEmpty, true, '"');
243        }
244    
245        /** Convenience method for creating a <em>non empty</em> start tag.
246         * @param tagName name of the start tag
247         * @param attrPairs name/value pairs of attributes to be included into start tag -
248         * if a value is <code>null</code> corresponding attribute will not be included
249         */
250        public final static String createStartTag(String tagName, String[][] attrPairs) {
251            return createStartTag(tagName, attrPairs, false);
252        }
253    
254        /** Convenience method for creating an <em>empty</em> tag.
255         * @param tagName name of the tag
256         * @param attrPairs name/value pairs of attributes to be included into tag -
257         * if a value is <code>null</code> corresponding attribute will not be included
258         * @see #createEmptyTag(String)
259         */
260        public final static String createEmptyTag(String tagName, String[][] attrPairs) {
261            return createStartTag(tagName, attrPairs, true);
262        }
263    
264        /** Convenience method for creating a start tag.
265         * @param tagName name of the start tag
266         * @param attrPairs name/value pairs of attributes to be included into start tag -
267         * if a value is <code>null</code> corresponding attribute will not be included
268         * @param isEmpty decides wheter this is start tag is for an empty element
269         * @param encodeAttrs set this to have your attribute values encoded for XML
270         * @param quoteChar if you choose encoding this is the char that quotes
271         * your attributes
272         */
273        public final static String createStartTag(
274            String tagName,
275            String[][] attrPairs,
276            boolean isEmpty,
277            boolean encodeAttrs,
278            char quoteChar) {
279            // estimate buffer size
280            StringBuffer buf = new StringBuffer((attrPairs.length + 1) * 15);
281            buf.append('<').append(tagName);
282    
283            for (int i = 0; i < attrPairs.length; i++) {
284                String name = attrPairs[i][0];
285                String value = attrPairs[i][1];
286                if (value == null)
287                    continue;
288                if (encodeAttrs)
289                    value = XMLEncode.xmlEncodeTextForAttribute(value, quoteChar);
290                buf.append(' ').append(name).append('=').append(value);
291            }
292    
293            if (isEmpty) {
294                buf.append("/>");
295            } else {
296                buf.append('>');
297            }
298            return buf.toString();
299        }
300    
301        /** Convenience method for creating an <em>empty</em> tag.
302         * @param tagName name of the tag
303         * @param attributes SAX attributes to be included into start tag
304         * @see #createEmptyTag(String)
305         */
306        public final static String createEmptyTag(String tagName, Attributes attributes) {
307            return createStartTag(tagName, attributes, true);
308        }
309    
310        /** Convenience method for creating a start tag.
311         * @param tagName name of the start tag
312         * @param attributes SAX attributes to be included into start tag
313         */
314        public final static String createStartTag(String tagName, Attributes attributes) {
315            return createStartTag(tagName, attributes, false);
316        }
317    
318        /** Convenience method for creating a start tag.
319         * @param tagName name of the start tag
320         * @param attributes SAX attributes to be included into start tag
321         * @param isEmpty decides wheter this is start tag is for an empty element
322         */
323        public final static String createStartTag(String tagName, Attributes attributes, boolean isEmpty) {
324            return createStartTag(tagName, attributes, isEmpty, true, '"');
325        }
326    
327        /** Convenience method for creating a start tag.
328         * @param tagName name of the start tag
329         * @param attributes SAX attributes to be included into start tag
330         * @param isEmpty decides wheter this is start tag is for an empty element
331         * @param encodeAttrs set this to have your attribute values encoded for XML
332         * @param quoteChar if you choose encoding this is the char that quotes
333         * your attributes
334         */
335        public final static String createStartTag(
336            String tagName,
337            Attributes attributes,
338            boolean isEmpty,
339            boolean encodeAttrs,
340            char quoteChar) {
341            // estimate buffer size
342            StringBuffer buf = new StringBuffer((attributes.getLength() + 1) * 15);
343            buf.append('<').append(tagName);
344    
345            for (int i = 0; i < attributes.getLength(); i++) {
346                String name = attributes.getQName(i);
347                String value = attributes.getValue(i);
348                if (encodeAttrs)
349                    value = XMLEncode.xmlEncodeTextForAttribute(value, quoteChar);
350                buf.append(' ').append(name).append('=').append(value);
351            }
352    
353            if (isEmpty) {
354                buf.append("/>");
355            } else {
356                buf.append('>');
357            }
358            return buf.toString();
359        }
360    
361        /** Convenience method for creating <em>and writing</em> a whole element. 
362         * Added to normal non-static write methods purely for my own laziness.<br>
363         * It is non-static as it differs from all other write methods as it
364         * combines generating and writing. This is normally avoided to keep every 
365         * everything simple, clear and fast.<br>
366         * <br>
367         * You can write<br>
368         * <code>XMLOutputStreamWriter.generateAndWriteElementWithCData(writer, "tag", "cdata");
369         * </code><br>
370         * <br>
371         * to generate<br>
372         * <code>&lt;tag>cdata&lt;/tag>
373         * </code><br>
374         * 
375         * @param xmlWriter writer to write generated stuff to
376         * @param tagName name of the element
377         * @param attrPairs name/value pairs of attributes to be included into start tag -
378         * if a value is <code>null</code> corresponding attribute will not be included
379         * @param cData the character data of the element
380         * @see #writeElementWithCData(String, String, String)
381         * @see #createStartTag(String, String[][])
382         * @see #createEndTag(String)
383         */
384        public final static void generateAndWriteElementWithCData(
385            XMLWriter xmlWriter,
386            String tagName,
387            String[][] attrPairs,
388            String cData)
389            throws IOException {
390            String startTag = createStartTag(tagName, attrPairs);
391            String endTag = createEndTag(tagName);
392            xmlWriter.writeElementWithCData(startTag, cData, endTag);
393        }
394    
395        /** Convenience method for creating <em>and writing</em> a whole element. 
396         * @param xmlWriter writer to write generated stuff to
397         * @param tagName name of the element
398         * @param attrNames names of attributes to be included into start tag
399         * @param attrValues values of attributes to be included into start tag -
400         * there should be just as many entries as in <code>attrNames</code>,
401         * if a value is <code>null</code> corresponding attribute will not be included
402         * @param cData the character data of the element
403         * @see #generateAndWriteElementWithCData(XMLWriter, String, String[][], String)
404         * @see #writeElementWithCData(String, String, String)
405         * @see #createStartTag(String, String[], String[])
406         * @see #createEndTag(String)
407         */
408        public final static void generateAndWriteElementWithCData(
409            XMLWriter xmlWriter,
410            String tagName,
411            String[] attrNames,
412            String[] attrValues,
413            String cData)
414            throws IOException {
415            String startTag = createStartTag(tagName, attrNames, attrValues);
416            String endTag = createEndTag(tagName);
417            xmlWriter.writeElementWithCData(startTag, cData, endTag);
418        }
419    
420        /** Creates a new filter writer for XML export.
421         * @param writer the underlying writer the formatted XML is exported to
422         */
423        public XMLWriter(Writer writer) {
424            super(writer);
425        }
426    
427        /** Switches on/off pretty print mode.
428         * <br>
429         * Having it switched on (which is the default) makes output
430         * pretty as newlines after tags and indentataion is done. Unfortunately,
431         * if your application is sensible to whitespace in CDATA this might lead
432         * to unwanted additional spaces and newlines.
433         * <br>
434         * If it is switched off the output is guaranteed to be correct, but looks
435         * pretty funny. After before markup close (> or />) a newline is inserted
436         * as otherwise you may get extremely long output lines.
437         */
438        public void setPrettyPrintMode(boolean prettyPrintMode) {
439            this.prettyPrintMode = prettyPrintMode;
440        }
441    
442        /** Gets property described in {@link #setPrettyPrintMode}. */
443        public boolean getPrettyPrintMode() {
444            return prettyPrintMode;
445        }
446    
447        /** Sets the amount of spaces to increase indentation with element level.
448         * <br>
449         * This only takes effect when {@link #setPrettyPrintMode} is set to true.
450         * <br>
451         * <em>Caution</em>: You should better avoid to change this property while
452         * exporting as this may result in unexpected output.
453         */
454        public void setTabWidth(int tabWidth) {
455            this.tabWidth = tabWidth;
456        }
457    
458        /** Gets property described in {@link #setTabWidth}. */
459        public int getTabWidth() {
460            return tabWidth;
461        }
462    
463        /** Sets if a newline is inserted after an empty start element 
464         * by default. 
465         */
466        public void setNlAfterEmptyTag(boolean nlAfterEmptyTag) {
467            this.nlAfterEmptyTag = nlAfterEmptyTag;
468        }
469    
470        /** Gets property described in {@link #setNlAfterEmptyTag}. */
471        public boolean getNlAfterEmptyTag() {
472            return nlAfterEmptyTag;
473        }
474    
475        /** Sets if a newline is inserted after an end tag 
476         * by default. */
477        public void setNlAfterEndTag(boolean nlAfterEndTag) {
478            this.nlAfterEndTag = nlAfterEndTag;
479        }
480    
481        /** Gets property described in {@link #setNlAfterEndTag}. */
482        public boolean getNlAfterEndTag() {
483            return nlAfterEndTag;
484        }
485    
486        /** Sets if a newline is inserted after a non empty start tag 
487         * by default. */
488        public void setNlAfterStartTag(boolean nlAfterStartTag) {
489            this.nlAfterStartTag = nlAfterStartTag;
490        }
491    
492        /** Gets property described in {@link #setNlAfterStartTag}. */
493        public boolean getNlAfterStartTag() {
494            return nlAfterStartTag;
495        }
496    
497        /** Writes XML declaration. 
498         * XML declaration will be written 
499         * using version 1.0 and no encoding defaulting
500         * to standard encoding (supports UTF-8 and UTF-16):<br>
501         * <code>&lt;?xml version="1.0"?></code>
502         * <br>
503         * If you want to have a different encoding or the standalone declaration
504         * use {@link #writeProlog(String)}.<br>
505         * This sets {@link #setXMLDeclarationWritten xmlDeclWritten} to 
506         * <code>true</code>.
507         * 
508         */
509        public void writeXMLDeclaration() throws IOException {
510            xmlDeclWritten = true;
511            needsIndent = false;
512            write("<?xml version=\"1.0\"?>\n");
513        }
514    
515        /** Indicates whether the XML declaration has been written, yet.
516         * As it may only be written once, you can check this when writing 
517         * in different contexts to same writer.
518         */
519        public boolean isXMLDeclarationWritten() {
520            return xmlDeclWritten;
521        }
522    
523        /** Manually sets or resets whether XML declaration has been written. 
524         * This is done implicly by {@link #writeXMLDeclaration}, but to give you
525         * the full freedom, this can be done here as well. 
526         * Use {@link #isXMLDeclarationWritten} to check it.
527         */
528        public void setXMLDeclarationWritten(boolean xmlDeclWritten) {
529            this.xmlDeclWritten = xmlDeclWritten;
530        }
531    
532        /** Writes prolog data like doctype delcaration and 
533         * DTD parts followed by a newline.
534         * <br>
535         * Do not misuse this to write plain text, but rather - if you really
536         * have to - use the standard {@link #write} methods.
537         */
538        public void writeProlog(String prolog) throws IOException {
539            needsIndent = false;
540            write(prolog);
541            writeNl();
542        }
543    
544        /** Writes a single newline. */
545        public void writeNl() throws IOException {
546            needsIndent = true;
547            write('\n');
548        }
549    
550        /** Writes <code>comment</code> encoded as comment. */
551        public void writeComment(String comment) throws IOException {
552            needsIndent = false;
553            write("<!-- ");
554            write(comment);
555            write(" -->");
556        }
557    
558        /** Writes a processing instruction. */
559        public void writePI(String target, String data) throws IOException {
560            needsIndent = false;
561            write("<?" + target + " " + data + "?>");
562        }
563    
564        /** Writes a start tag.
565         * @param startTag the complete start tag, e.g. <code>&lt;start></code>
566         * @param nl decides whether there should be a newline after the tag
567         */
568        public void writeStartTag(String startTag, boolean nl) throws IOException {
569            writeTag(startTag, nl);
570            depthPlus();
571        }
572    
573        /** Writes a start tag.
574         * @param startTag the complete start tag, e.g. <code>&lt;start></code>
575         * @see #setNlAfterStartTag
576         */
577        public void writeStartTag(String startTag) throws IOException {
578            writeStartTag(startTag, nlAfterStartTag);
579        }
580    
581        /** Writes an end tag.
582         * @param endTag the complete end tag, e.g. <code>&lt;/end></code>
583         * @param nl decides whether there should be a newline after the tag
584         */
585        public void writeEndTag(String endTag, boolean nl) throws IOException {
586            depthMinus();
587            writeTag(endTag, nl);
588        }
589    
590        /** Writes an end tag.
591         * @param endTag the complete end tag, e.g. <code>&lt;/end></code>
592         * @see #setNlAfterEndTag
593         */
594        public void writeEndTag(String endTag) throws IOException {
595            writeEndTag(endTag, nlAfterEndTag);
596        }
597    
598        /** Writes an empty element.
599         * @param emptyTag the complete tag for an empty element, e.g. <code>&lt;empty/></code>
600         * @param nl decides whether there should be a newline after the tag
601         */
602        public void writeEmptyElement(String emptyTag, boolean nl) throws IOException {
603            writeTag(emptyTag, nl);
604        }
605    
606        /** Writes an empty element.
607         * @param emptyTag the complete tag for an empty element, e.g. <code>&lt;start/></code>
608         * @see #setNlAfterEmptyTag
609         */
610        public void writeEmptyElement(String emptyTag) throws IOException {
611            writeEmptyElement(emptyTag, nlAfterEmptyTag);
612        }
613    
614        /** Writes character data with encoding.
615         * @param cData the character data to write
616         */
617        public void writeCData(String cData) throws IOException {
618            String encoded = XMLEncode.xmlEncodeText(cData);
619            writePCData(encoded);
620        }
621    
622        /** Writes character data <em>without</em> encoding.
623         * @param pcData the <em>parseable</em> character data to write
624         */
625        public void writePCData(String pcData) throws IOException {
626            needsIndent = false;
627            write(pcData);
628        }
629    
630        /** Writes a full element consisting of a start tag, character data and
631         * an end tag. There will be no newline after start tag, so character data
632         * is literally preserved.
633         * <br>
634         * The character data will be encoded.
635         *
636         * @param startTag the complete start tag, e.g. <code>&lt;element></code>
637         * @param cData the character data to write
638         * @param endTag the complete end tag, e.g. <code>&lt;/element></code>
639         */
640        public void writeElementWithCData(String startTag, String cData, String endTag) throws IOException {
641            writeStartTag(startTag, false);
642            writeCData(cData);
643            writeEndTag(endTag);
644        }
645    
646        /** Writes a full element consisting of a start tag, character data and
647         * an end tag. There will be no newline after start tag, so character data
648         * is literally preserved.
649         * <br>
650         * The character data will <em>not</em> be encoded.
651         *
652         * @param startTag the complete start tag, e.g. <code>&lt;element></code>
653         * @param pcData the <em>parseable</em> character data to write
654         * @param endTag the complete end tag, e.g. <code>&lt;/element></code>
655         */
656        public void writeElementWithPCData(String startTag, String pcData, String endTag) throws IOException {
657            writeStartTag(startTag, false);
658            writePCData(pcData);
659            writeEndTag(endTag);
660        }
661    
662        private void writeTag(String tag, boolean nl) throws IOException {
663            writeIndent();
664            needsIndent = false;
665            if (nl) {
666                if (getPrettyPrintMode()) {
667                    write(tag);
668                    writeNl();
669                } else {
670                    // in correct mode we need to break tag before closing > resp. />
671                    int length = tag.length();
672                    int pos;
673                    if ((pos = tag.indexOf("/>")) != -1) {
674                        write(tag, 0, pos);
675                        write('\n');
676                        write(tag, pos, length - pos);
677                    } else if ((pos = tag.indexOf(">")) != -1) {
678                        write(tag, 0, pos);
679                        write('\n');
680                        write(tag, pos, length - pos);
681                    } else {
682                        write(tag);
683                        write('\n');
684                    }
685                }
686            } else {
687                write(tag);
688            }
689        }
690    
691        private void writeIndent() throws IOException {
692            // indentation is only needed after a newline in pretty print mode
693            if (!needsIndent)
694                return;
695    
696            // every indentation destroys literal write
697            if (!getPrettyPrintMode())
698                return;
699    
700            // shortcut
701            if (indent == 0)
702                return;
703    
704            // save some computation time when indent does not change
705            if (!indentStringCacheValid) {
706                StringBuffer buf = new StringBuffer(indent);
707                for (int i = 0; i < indent; i++) {
708                    buf.append(' ');
709                }
710                indentStringCache = buf.toString();
711                indentStringCacheValid = true;
712            }
713    
714            write(indentStringCache);
715        }
716    
717        private void depthPlus() {
718            indent += tabWidth;
719            depth++;
720            indentStringCacheValid = false;
721        }
722    
723        private void depthMinus() {
724            indent -= tabWidth;
725            if (indent < 0)
726                indent = 0;
727            depth--;
728            indentStringCacheValid = false;
729        }
730    }