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    
044    package org.jfree.xml.writer;
045    
046    import java.io.IOException;
047    import java.io.Writer;
048    import java.util.Enumeration;
049    import java.util.Iterator;
050    import java.util.Properties;
051    
052    /**
053     * A support class for writing XML files.
054     *
055     * @author Thomas Morgner
056     */
057    public 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    }