    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}