View Javadoc

    1   /*
    2    $Id: MarkupBuilder.java 4350 2006-12-11 19:21:50Z tug $
    3
    4    Copyright 2003 (C) James Strachan and Bob Mcwhirter. All Rights Reserved.
    5
    6    Redistribution and use of this software and associated documentation
    7    ("Software"), with or without modification, are permitted provided
    8    that the following conditions are met:
    9
    10   1. Redistributions of source code must retain copyright
    11      statements and notices.  Redistributions must also contain a
    12      copy of this document.
    13
    14   2. Redistributions in binary form must reproduce the
    15      above copyright notice, this list of conditions and the
    16      following disclaimer in the documentation and/or other
    17      materials provided with the distribution.
    18
    19   3. The name "groovy" must not be used to endorse or promote
    20      products derived from this Software without prior written
    21      permission of The Codehaus.  For written permission,
    22      please contact info@codehaus.org.
    23
    24   4. Products derived from this Software may not be called "groovy"
    25      nor may "groovy" appear in their names without prior written
    26      permission of The Codehaus. "groovy" is a registered
    27      trademark of The Codehaus.
    28
    29   5. Due credit should be given to The Codehaus -
    30      http://groovy.codehaus.org/
    31
    32   THIS SOFTWARE IS PROVIDED BY THE CODEHAUS AND CONTRIBUTORS
    33   ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
    34   NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
    35   FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
    36   THE CODEHAUS OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
    37   INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
    38   (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
    39   SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
    40   HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
    41   STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
    42   ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
    43   OF THE POSSIBILITY OF SUCH DAMAGE.
    44
    45   */
    46  package groovy.xml;
    47
    48  import groovy.util.BuilderSupport;
    49  import groovy.util.IndentPrinter;
    50
    51  import java.io.PrintWriter;
    52  import java.io.Writer;
    53  import java.util.Iterator;
    54  import java.util.Map;
    55
    56  /***
    57   * A helper class for creating XML or HTML markup
    58   *
    59   * @author <a href="mailto:james@coredevelopers.net">James Strachan</a>
    60   * @author Stefan Matthias Aust
    61   * @author <a href="mailto:scottstirling@rcn.com">Scott Stirling</a>
    62   * @version $Revision: 4350 $
    63   */
    64  public class MarkupBuilder extends BuilderSupport {
    65      private IndentPrinter out;
    66      private boolean nospace;
    67      private int state;
    68      private boolean nodeIsEmpty = true;
    69      private boolean useDoubleQuotes = false;
    70
    71      public MarkupBuilder() {
    72          this(new IndentPrinter());
    73      }
    74
    75      public MarkupBuilder(PrintWriter writer) {
    76          this(new IndentPrinter(writer));
    77      }
    78
    79      public MarkupBuilder(Writer writer) {
    80          this(new IndentPrinter(new PrintWriter(writer)));
    81      }
    82
    83      public MarkupBuilder(IndentPrinter out) {
    84          this.out = out;
    85      }
    86
    87      /***
    88       * Returns <code>true</code> if attribute values are output with
    89       * double quotes; <code>false</code> if single quotes are used.
    90       * By default, single quotes are used.
    91       */
    92      public boolean getDoubleQuotes() {
    93          return this.useDoubleQuotes;
    94      }
    95
    96      /***
    97       * Sets whether the builder outputs attribute values in double
    98       * quotes or single quotes.
    99       * @param useDoubleQuotes If this parameter is <code>true</code>,
    100      * double quotes are used; otherwise, single quotes are.
    101      */
    102     public void setDoubleQuotes(boolean useDoubleQuotes) {
    103         this.useDoubleQuotes = useDoubleQuotes;
    104     }
    105
    106     protected IndentPrinter getPrinter() {
    107         return this.out;
    108     }
    109
    110     protected void setParent(Object parent, Object child) { }
    111
    112     protected Object createNode(Object name) {
    113         this.nodeIsEmpty = true;
    114         toState(1, name);
    115         return name;
    116     }
    117
    118     protected Object createNode(Object name, Object value) {
    119         toState(2, name);
    120         this.nodeIsEmpty = false;
    121         out.print(">");
    122         out.print(escapeElementContent(value.toString()));
    123         return name;
    124     }
    125
    126     protected Object createNode(Object name, Map attributes, Object value) {
    127         toState(1, name);
    128         for (Iterator iter = attributes.entrySet().iterator(); iter.hasNext();) {
    129             Map.Entry entry = (Map.Entry) iter.next();
    130             out.print(" ");
    131
    132             // Output the attribute name,
    133             print(entry.getKey().toString());
    134
    135             // Output the attribute value within quotes. Use whichever
    136             // type of quotes are currently configured.
    137             out.print(this.useDoubleQuotes ? "=\"" : "='");
    138             print(escapeAttributeValue(entry.getValue().toString()));
    139             out.print(this.useDoubleQuotes ? "\"" : "'");
    140         }
    141
    142         if (value != null) {
    143             nodeIsEmpty = false;
    144             out.print(">" + escapeElementContent(value.toString()) + "</" + name + ">");
    145         }
    146         else {
    147             nodeIsEmpty = true;
    148         }
    149
    150         return name;
    151     }
    152
    153     protected Object createNode(Object name, Map attributes) {
    154         return createNode(name, attributes, null);
    155     }
    156
    157     protected void nodeCompleted(Object parent, Object node) {
    158         toState(3, node);
    159         out.flush();
    160     }
    161
    162     protected void print(Object node) {
    163         out.print(node == null ? "null" : node.toString());
    164     }
    165
    166     protected Object getName(String methodName) {
    167         return super.getName(methodName);
    168     }
    169
    170     /***
    171      * Returns a String with special XML characters escaped as entities so that
    172      * output XML is valid. Escapes the following characters as corresponding
    173      * entities:
    174      * <ul>
    175      *   <li>\' as &amp;apos;</li>
    176      *   <li>&amp; as &amp;amp;</li>
    177      *   <li>&lt; as &amp;lt;</li>
    178      *   <li>&gt; as &amp;gt;</li>
    179      * </ul>
    180      *
    181      * @param value to be searched and replaced for XML special characters.
    182      * @return value with XML characters escaped
    183      * @deprecated
    184      * @see #escapeXmlValue(String, boolean)
    185      */
    186     protected String transformValue(String value) {
    187         // & has to be checked and replaced before others
    188         if (value.matches(".*&.*")) {
    189             value = value.replaceAll("&", "&amp;");
    190         }
    191         if (value.matches(".*//'.*")) {
    192             value = value.replaceAll("//'", "&apos;");
    193         }
    194         if (value.matches(".*<.*")) {
    195             value = value.replaceAll("<", "&lt;");
    196         }
    197         if (value.matches(".*>.*")) {
    198             value = value.replaceAll(">", "&gt;");
    199         }
    200         return value;
    201     }
    202
    203     /***
    204      * Escapes a string so that it can be used directly as an XML
    205      * attribute value.
    206      * @param value The string to escape.
    207      * @return A new string in which all characters that require escaping
    208      * have been replaced with the corresponding XML entities.
    209      * @see #escapeXmlValue(String, boolean)
    210      */
    211     private String escapeAttributeValue(String value) {
    212         return escapeXmlValue(value, true);
    213     }
    214
    215     /***
    216      * Escapes a string so that it can be used directly in XML element
    217      * content.
    218      * @param value The string to escape.
    219      * @return A new string in which all characters that require escaping
    220      * have been replaced with the corresponding XML entities.
    221      * @see #escapeXmlValue(String, boolean)
    222      */
    223     private String escapeElementContent(String value) {
    224         return escapeXmlValue(value, false);
    225     }
    226
    227     /***
    228      * Escapes a string so that it can be used in XML text successfully.
    229      * It replaces the following characters with the corresponding XML
    230      * entities:
    231      * <ul>
    232      *   <li>&amp; as &amp;amp;</li>
    233      *   <li>&lt; as &amp;lt;</li>
    234      *   <li>&gt; as &amp;gt;</li>
    235      * </ul>
    236      * If the string is to be added as an attribute value, these
    237      * characters are also escaped:
    238      * <ul>
    239      *   <li>' as &amp;apos;</li>
    240      * </ul>
    241      * @param value The string to escape.
    242      * @param isAttrValue <code>true</code> if the string is to be used
    243      * as an attribute value, otherwise <code>false</code>.
    244      * @return A new string in which all characters that require escaping
    245      * have been replaced with the corresponding XML entities.
    246      */
    247     private String escapeXmlValue(String value, boolean isAttrValue) {
    248         StringBuffer buffer = new StringBuffer(value);
    249         for (int i = 0, n = buffer.length(); i < n; i++) {
    250             switch (buffer.charAt(i)) {
    251             case '&':
    252                 buffer.replace(i, i + 1, "&amp;");
    253
    254                 // We're replacing a single character by a string of
    255                 // length 5, so we need to update the index variable
    256                 // and the total length.
    257                 i += 4;
    258                 n += 4;
    259                 break;
    260
    261             case '<':
    262                 buffer.replace(i, i + 1, "&lt;");
    263
    264                 // We're replacing a single character by a string of
    265                 // length 4, so we need to update the index variable
    266                 // and the total length.
    267                 i += 3;
    268                 n += 3;
    269                 break;
    270
    271             case '>':
    272                 buffer.replace(i, i + 1, "&gt;");
    273
    274                 // We're replacing a single character by a string of
    275                 // length 4, so we need to update the index variable
    276                 // and the total length.
    277                 i += 3;
    278                 n += 3;
    279                 break;
    280
    281             case '"':
    282                 // The double quote is only escaped if the value is for
    283                 // an attribute and the builder is configured to output
    284                 // attribute values inside double quotes.
    285                 if (isAttrValue && this.useDoubleQuotes) {
    286                     buffer.replace(i, i + 1, "&quot;");
    287
    288                     // We're replacing a single character by a string of
    289                     // length 6, so we need to update the index variable
    290                     // and the total length.
    291                     i += 5;
    292                     n += 5;
    293                 }
    294                 break;
    295
    296             case '\'':
    297                 // The apostrophe is only escaped if the value is for an
    298                 // attribute, as opposed to element content, and if the
    299                 // builder is configured to surround attribute values with
    300                 // single quotes.
    301                 if (isAttrValue && !this.useDoubleQuotes){
    302                     buffer.replace(i, i + 1, "&apos;");
    303
    304                     // We're replacing a single character by a string of
    305                     // length 6, so we need to update the index variable
    306                     // and the total length.
    307                     i += 5;
    308                     n += 5;
    309                 }
    310                 break;
    311
    312             default:
    313                 break;
    314             }
    315         }
    316
    317         return buffer.toString();
    318     }
    319
    320     private void toState(int next, Object name) {
    321         switch (state) {
    322             case 0:
    323                 switch (next) {
    324                     case 1:
    325                     case 2:
    326                         out.print("<");
    327                         print(name);
    328                         break;
    329                     case 3:
    330                         throw new Error();
    331                 }
    332                 break;
    333             case 1:
    334                 switch (next) {
    335                     case 1:
    336                     case 2:
    337                         out.print(">");
    338                         if (nospace) {
    339                             nospace = false;
    340                         } else {
    341                             out.println();
    342                             out.incrementIndent();
    343                             out.printIndent();
    344                         }
    345                         out.print("<");
    346                         print(name);
    347                         break;
    348                     case 3:
    349                         if (nodeIsEmpty) {
    350                             out.print(" />");
    351                         }
    352                         break;
    353                 }
    354                 break;
    355             case 2:
    356                 switch (next) {
    357                     case 1:
    358                     case 2:
    359                         throw new Error();
    360                     case 3:
    361                         out.print("</");
    362                         print(name);
    363                         out.print(">");
    364                         break;
    365                 }
    366                 break;
    367             case 3:
    368                 switch (next) {
    369                     case 1:
    370                     case 2:
    371                         if (nospace) {
    372                             nospace = false;
    373                         } else {
    374                             out.println();
    375                             out.printIndent();
    376                         }
    377                         out.print("<");
    378                         print(name);
    379                         break;
    380                     case 3:
    381                         if (nospace) {
    382                             nospace = false;
    383                         } else {
    384                             out.println();
    385                             out.decrementIndent();
    386                             out.printIndent();
    387                         }
    388                         out.print("</");
    389                         print(name);
    390                         out.print(">");
    391                         break;
    392                 }
    393                 break;
    394         }
    395         state = next;
    396     }
    397 }