001/* ========================================================================
002 * JCommon : a free general purpose class library for the Java(tm) platform
003 * ========================================================================
004 *
005 * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
006 * 
007 * Project Info:  http://www.jfree.org/jcommon/index.html
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 
011 * the Free Software Foundation; either version 2.1 of the License, or 
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but 
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
022 * USA.  
023 *
024 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
025 * in the United States and other countries.]
026 * 
027 * ---------------------
028 * XMLWriterSupport.java
029 * ---------------------
030 * (C)opyright 2003-2005, by Thomas Morgner and Contributors.
031 *
032 * Original Author:  Thomas Morgner;
033 * Contributor(s):   David Gilbert (for Object Refinery Limited);
034 *
035 * $Id: XMLWriterSupport.java,v 1.6 2005/11/08 14:35:52 mungady Exp $
036 *
037 * Changes
038 * -------
039 * 21-Jun-2003 : Initial version (TM);
040 * 26-Nov-2003 : Updated Javadocs (DG);
041 *
042 */
043
044package org.jfree.xml.writer;
045
046import java.io.IOException;
047import java.io.Writer;
048import java.util.Enumeration;
049import java.util.Iterator;
050import java.util.Properties;
051
052/**
053 * A support class for writing XML files.
054 *
055 * @author Thomas Morgner
056 */
057public class XMLWriterSupport {
058
059    /** A constant for controlling the indent function. */
060    public static final int OPEN_TAG_INCREASE = 1;
061
062    /** A constant for controlling the indent function. */
063    public static final int CLOSE_TAG_DECREASE = 2;
064
065    /** A constant for controlling the indent function. */
066    public static final int INDENT_ONLY = 3;
067
068    /** A constant for close. */
069    public static final boolean CLOSE = true;
070
071    /** A constant for open. */
072    public static final boolean OPEN = false;
073
074    /** The line separator. */
075    private static String lineSeparator;
076
077    /** A list of safe tags. */
078    private SafeTagList safeTags;
079
080    /** The indent level for that writer. */
081    private int indentLevel;
082
083    /** The indent string. */
084    private String indentString;
085
086    /** 
087     * A flag indicating whether to force a linebreak before printing the next 
088     * tag. 
089     */
090    private boolean newLineOk;
091
092    /**
093     * Default Constructor. The created XMLWriterSupport will not have no safe 
094     * tags and starts with an indention level of 0.  
095     */
096    public XMLWriterSupport() {
097        this(new SafeTagList(), 0);
098    }
099
100    /**
101     * Creates a new support instance.
102     *
103     * @param safeTags  tags that are safe for line breaks.
104     * @param indentLevel  the index level.
105     */
106    public XMLWriterSupport(final SafeTagList safeTags, final int indentLevel) {
107        this(safeTags, indentLevel, "    ");
108    }
109
110    /**
111     * Creates a new support instance.
112     *
113     * @param safeTags  the tags that are safe for line breaks.
114     * @param indentLevel  the indent level.
115     * @param indentString  the indent string.
116     */
117    public XMLWriterSupport(final SafeTagList safeTags, final int indentLevel, 
118            final String indentString) {
119        if (indentString == null) {
120            throw new NullPointerException("IndentString must not be null");
121        }
122
123        this.safeTags = safeTags;
124        this.indentLevel = indentLevel;
125        this.indentString = indentString;
126    }
127
128    /**
129     * Starts a new block by increasing the indent level.
130     *
131     * @throws IOException if an IO error occurs.
132     */
133    public void startBlock() throws IOException {
134        this.indentLevel++;
135        allowLineBreak();
136    }
137
138    /**
139     * Ends the current block by decreasing the indent level.
140     *
141     * @throws IOException if an IO error occurs.
142     */
143    public void endBlock() throws IOException {
144        this.indentLevel--;
145        allowLineBreak();
146    }
147
148    /**
149     * Forces a linebreak on the next call to writeTag or writeCloseTag.
150     *
151     * @throws IOException if an IO error occurs.
152     */
153    public void allowLineBreak() throws IOException {
154        this.newLineOk = true;
155    }
156
157    /**
158     * Returns the line separator.
159     *
160     * @return the line separator.
161     */
162    public static String getLineSeparator() {
163        if (lineSeparator == null) {
164            try {
165                lineSeparator = System.getProperty("line.separator", "\n");
166            }
167            catch (SecurityException se) {
168                lineSeparator = "\n";
169            }
170        }
171        return lineSeparator;
172    }
173
174    /**
175     * Writes an opening XML tag that has no attributes.
176     *
177     * @param w  the writer.
178     * @param name  the tag name.
179     *
180     * @throws java.io.IOException if there is an I/O problem.
181     */
182    public void writeTag(final Writer w, final String name) throws IOException {
183        if (this.newLineOk) {
184            w.write(getLineSeparator());
185        }
186        indent(w, OPEN_TAG_INCREASE);
187
188        w.write("<");
189        w.write(name);
190        w.write(">");
191        if (getSafeTags().isSafeForOpen(name)) {
192            w.write(getLineSeparator());
193        }
194    }
195
196    /**
197     * Writes a closing XML tag.
198     *
199     * @param w  the writer.
200     * @param tag  the tag name.
201     *
202     * @throws java.io.IOException if there is an I/O problem.
203     */
204    public void writeCloseTag(final Writer w, final String tag) 
205            throws IOException {
206        // check whether the tag contains CData - we ma not indent such tags
207        if (this.newLineOk || getSafeTags().isSafeForOpen(tag)) {
208            if (this.newLineOk) {
209                w.write(getLineSeparator());
210            }
211            indent(w, CLOSE_TAG_DECREASE);
212        }
213        else {
214            decreaseIndent();
215        }
216        w.write("</");
217        w.write(tag);
218        w.write(">");
219        if (getSafeTags().isSafeForClose(tag)) {
220            w.write(getLineSeparator());
221        }
222        this.newLineOk = false;
223    }
224
225    /**
226     * Writes an opening XML tag with an attribute/value pair.
227     *
228     * @param w  the writer.
229     * @param name  the tag name.
230     * @param attributeName  the attribute name.
231     * @param attributeValue  the attribute value.
232     * @param close  controls whether the tag is closed.
233     *
234     * @throws java.io.IOException if there is an I/O problem.
235     */
236    public void writeTag(final Writer w, final String name, 
237            final String attributeName, final String attributeValue,
238            final boolean close) throws IOException {
239        final AttributeList attr = new AttributeList();
240        if (attributeName != null) {
241            attr.setAttribute(attributeName, attributeValue);
242        }
243        writeTag(w, name, attr, close);
244    }
245
246    /**
247     * Writes an opening XML tag along with a list of attribute/value pairs.
248     *
249     * @param w  the writer.
250     * @param name  the tag name.
251     * @param attributes  the attributes.
252     * @param close  controls whether the tag is closed.
253     *
254     * @throws java.io.IOException if there is an I/O problem.
255     * @deprecated use the attribute list instead of the properties.
256     */
257    public void writeTag(final Writer w, final String name, 
258            final Properties attributes, final boolean close)
259            throws IOException {
260        final AttributeList attList = new AttributeList();
261        final Enumeration keys = attributes.keys();
262        while (keys.hasMoreElements()) {
263            final String key = (String) keys.nextElement();
264            attList.setAttribute(key, attributes.getProperty(key));
265        }
266        writeTag(w, name, attList, close);
267    }
268
269    /**
270     * Writes an opening XML tag along with a list of attribute/value pairs.
271     *
272     * @param w  the writer.
273     * @param name  the tag name.
274     * @param attributes  the attributes.
275     * @param close  controls whether the tag is closed.
276     *
277     * @throws java.io.IOException if there is an I/O problem.     
278     */
279    public void writeTag(final Writer w, final String name, 
280            final AttributeList attributes, final boolean close)
281            throws IOException {
282
283        if (this.newLineOk) {
284            w.write(getLineSeparator());
285            this.newLineOk = false;
286        }
287        indent(w, OPEN_TAG_INCREASE);
288
289        w.write("<");
290        w.write(name);
291        final Iterator keys = attributes.keys();
292        while (keys.hasNext()) {
293            final String key = (String) keys.next();
294            final String value = attributes.getAttribute(key);
295            w.write(" ");
296            w.write(key);
297            w.write("=\"");
298            w.write(normalize(value));
299            w.write("\"");
300        }
301        if (close) {
302            w.write("/>");
303            if (getSafeTags().isSafeForClose(name)) {
304                w.write(getLineSeparator());
305            }
306            decreaseIndent();
307        }
308        else {
309            w.write(">");
310            if (getSafeTags().isSafeForOpen(name)) {
311                w.write(getLineSeparator());
312            }
313        }
314    }
315
316    /**
317     * Normalises a string, replacing certain characters with their escape 
318     * sequences so that the XML text is not corrupted.
319     *
320     * @param s  the string.
321     *
322     * @return the normalised string.
323     */
324    public static String normalize(final String s) {
325        if (s == null) {
326            return "";
327        }
328        final StringBuffer str = new StringBuffer();
329        final int len = s.length();
330
331        for (int i = 0; i < len; i++) {
332            final char ch = s.charAt(i);
333
334            switch (ch) {
335                case '<':
336                    {
337                        str.append("&lt;");
338                        break;
339                    }
340                case '>':
341                    {
342                        str.append("&gt;");
343                        break;
344                    }
345                case '&':
346                    {
347                        str.append("&amp;");
348                        break;
349                    }
350                case '"':
351                    {
352                        str.append("&quot;");
353                        break;
354                    }
355                case '\n':
356                    {
357                        if (i > 0) {
358                            final char lastChar = str.charAt(str.length() - 1);
359
360                            if (lastChar != '\r') {
361                                str.append(getLineSeparator());
362                            }
363                            else {
364                                str.append('\n');
365                            }
366                        }
367                        else {
368                            str.append(getLineSeparator());
369                        }
370                        break;
371                    }
372                default :
373                    {
374                        str.append(ch);
375                    }
376            }
377        }
378
379        return (str.toString());
380    }
381
382    /**
383     * Indent the line. Called for proper indenting in various places.
384     *
385     * @param writer the writer which should receive the indentention.
386     * @param increase the current indent level.
387     * @throws java.io.IOException if writing the stream failed.
388     */
389    public void indent(final Writer writer, final int increase) 
390            throws IOException {
391        if (increase == CLOSE_TAG_DECREASE) {
392            decreaseIndent();
393        }
394        for (int i = 0; i < this.indentLevel; i++) {
395            writer.write(this.indentString); // 4 spaces, we could also try tab,
396            // but I do not know whether this works
397            // with our XML edit pane
398        }
399        if (increase == OPEN_TAG_INCREASE) {
400            increaseIndent();
401        }
402    }
403
404    /**
405     * Returns the current indent level.
406     *
407     * @return the current indent level.
408     */
409    public int getIndentLevel() {
410        return this.indentLevel;
411    }
412
413    /**
414     * Increases the indention by one level.
415     */
416    protected void increaseIndent() {
417        this.indentLevel++;
418    }
419
420    /**
421     * Decreates the indention by one level.
422     */
423    protected void decreaseIndent() {
424        this.indentLevel--;
425    }
426
427    /**
428     * Returns the list of safe tags.
429     *
430     * @return The list.
431     */
432    public SafeTagList getSafeTags() {
433        return this.safeTags;
434    }
435}