View Javadoc

1   /*
2    $Id: MarkupBuilder.java,v 1.11 2006/06/16 10:48:56 galleon Exp $
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: 1.11 $
63   */
64  public class MarkupBuilder extends BuilderSupport {
65  
66      private IndentPrinter out;
67      private boolean nospace;
68      private int state;
69      private boolean nodeIsEmpty = true;
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      protected IndentPrinter getPrinter() {
88          return this.out;
89      }
90  
91      protected void setParent(Object parent, Object child) {
92      }
93  
94      /*
95      public Object getProperty(String property) {
96          if (property.equals("_")) {
97              nospace = true;
98              return null;
99          } else {
100             Object node = createNode(property);
101             nodeCompleted(getCurrent(), node);
102             return node;
103         }
104     }
105     */
106 
107     protected Object createNode(Object name) {
108         toState(1, name);
109         return name;
110     }
111 
112     protected Object createNode(Object name, Object value) {
113         toState(2, name);
114         out.print(">");
115         out.print(escapeElementContent(value.toString()));
116         return name;
117     }
118 
119     protected Object createNode(Object name, Map attributes, Object value) {
120         toState(1, name);
121         for (Iterator iter = attributes.entrySet().iterator(); iter.hasNext();) {
122             Map.Entry entry = (Map.Entry) iter.next();
123             out.print(" ");
124             print(transformName(entry.getKey().toString()));
125             out.print("='");
126             print(escapeAttributeValue(entry.getValue().toString()));
127             out.print("'");
128         }
129         if (value != null)
130         {
131             nodeIsEmpty = false;
132             out.print(">" + escapeElementContent(value.toString()) + "</" + name + ">");
133         }
134         return name;
135     }
136 
137     protected Object createNode(Object name, Map attributes) {
138         return createNode(name, attributes, null);
139     }
140     
141     protected void nodeCompleted(Object parent, Object node) {
142         toState(3, node);
143         out.flush();
144     }
145 
146     protected void print(Object node) {
147         out.print(node == null ? "null" : node.toString());
148     }
149 
150     protected Object getName(String methodName) {
151 		return super.getName(transformName(methodName));
152 	}
153 
154     protected String transformName(String name) {
155     	if (name.startsWith("_")) name = name.substring(1);
156     	return name.replace('_', '-');
157     }
158 
159     /***
160      * Returns a String with special XML characters escaped as entities so that
161      * output XML is valid. Escapes the following characters as corresponding 
162      * entities:
163      * <ul>
164      *   <li>\' as &amp;apos;</li>
165      *   <li>&amp; as &amp;amp;</li>
166      *   <li>&lt; as &amp;lt;</li>
167      *   <li>&gt; as &amp;gt;</li>
168      * </ul>
169      * 
170      * @param value to be searched and replaced for XML special characters.
171      * @return value with XML characters escaped
172      */
173     protected String transformValue(String value) {
174         // & has to be checked and replaced before others
175         if (value.matches(".*&.*")) {
176             value = value.replaceAll("&", "&amp;");
177         }
178         if (value.matches(".*//'.*")) {
179             value = value.replaceAll("//'", "&apos;");
180         }
181         if (value.matches(".*<.*")) {
182             value = value.replaceAll("<", "&lt;");
183         }
184         if (value.matches(".*>.*")) {
185             value = value.replaceAll(">", "&gt;");
186         }
187         return value;
188     }
189 
190     /***
191      * Escapes a string so that it can be used directly as an XML
192      * attribute value.
193      * @param value The string to escape.
194      * @return A new string in which all characters that require escaping
195      * have been replaced with the corresponding XML entities.
196      * @see #escapeXmlValue(String, boolean)
197      */
198     private String escapeAttributeValue(String value) {
199         return escapeXmlValue(value, true);
200     }
201 
202     /***
203      * Escapes a string so that it can be used directly in XML element
204      * content.
205      * @param value The string to escape.
206      * @return A new string in which all characters that require escaping
207      * have been replaced with the corresponding XML entities.
208      * @see #escapeXmlValue(String, boolean)
209      */
210     private String escapeElementContent(String value) {
211         return escapeXmlValue(value, false);
212     }
213 
214     /***
215      * Escapes a string so that it can be used in XML text successfully.
216      * It replaces the following characters with the corresponding XML
217      * entities:
218      * <ul>
219      *   <li>&amp; as &amp;amp;</li>
220      *   <li>&lt; as &amp;lt;</li>
221      *   <li>&gt; as &amp;gt;</li>
222      * </ul>
223      * If the string is to be added as an attribute value, these
224      * characters are also escaped:
225      * <ul>
226      *   <li>' as &amp;apos;</li>
227      * </ul>
228      * @param value The string to escape.
229      * @param isAttrValue <code>true</code> if the string is to be used
230      * as an attribute value, otherwise <code>false</code>.
231      * @return A new string in which all characters that require escaping
232      * have been replaced with the corresponding XML entities.
233      */
234     private String escapeXmlValue(String value, boolean isAttrValue){
235         // & has to be checked and replaced before others
236         if (value.matches(".*&.*")) {
237             value = value.replaceAll("&", "&amp;");
238         }
239         if (value.matches(".*<.*")) {
240             value = value.replaceAll("<", "&lt;");
241         }
242         if (value.matches(".*>.*")) {
243             value = value.replaceAll(">", "&gt;");
244         }
245         if (isAttrValue){
246             // The apostrophe is only escaped if the value is for an
247             // attribute, as opposed to element content.
248             if (value.matches(".*//'.*")) {
249                 value = value.replaceAll("//'", "&apos;");
250             }
251         }
252         return value;
253     }
254 
255     private void toState(int next, Object name) {
256         switch (state) {
257             case 0:
258                 switch (next) {
259                     case 1:
260                     case 2:
261                         out.print("<");
262                         print(name);
263                         break;
264                     case 3:
265                         throw new Error();
266                 }
267                 break;
268             case 1:
269                 switch (next) {
270                     case 1:
271                     case 2:
272                         out.print(">");
273                         if (nospace) {
274                             nospace = false;
275                         } else {
276                             out.println();
277                             out.incrementIndent();
278                             out.printIndent();
279                         }
280                         out.print("<");
281                         print(name);
282                         break;
283                     case 3:
284                         if (nodeIsEmpty) {
285                             out.print(" />");
286                         }
287                         break;
288                 }
289                 break;
290             case 2:
291                 switch (next) {
292                     case 1:
293                     case 2:
294                         throw new Error();
295                     case 3:
296                         out.print("</");
297                         print(name);
298                         out.print(">");
299                         break;
300                 }
301                 break;
302             case 3:
303                 switch (next) {
304                     case 1:
305                     case 2:
306                         if (nospace) {
307                             nospace = false;
308                         } else {
309                             out.println();
310                             out.printIndent();
311                         }
312                         out.print("<");
313                         print(name);
314                         break;
315                     case 3:
316                         if (nospace) {
317                             nospace = false;
318                         } else {
319                             out.println();
320                             out.decrementIndent();
321                             out.printIndent();
322                         }
323                         out.print("</");
324                         print(name);
325                         out.print(">");
326                         break;
327                 }
328                 break;
329         }
330         state = next;
331     }
332 
333 }