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 }