001 /*
002 $Id: MarkupBuilder.java 4350 2006-12-11 19:21:50Z tug $
003
004 Copyright 2003 (C) James Strachan and Bob Mcwhirter. All Rights Reserved.
005
006 Redistribution and use of this software and associated documentation
007 ("Software"), with or without modification, are permitted provided
008 that the following conditions are met:
009
010 1. Redistributions of source code must retain copyright
011 statements and notices. Redistributions must also contain a
012 copy of this document.
013
014 2. Redistributions in binary form must reproduce the
015 above copyright notice, this list of conditions and the
016 following disclaimer in the documentation and/or other
017 materials provided with the distribution.
018
019 3. The name "groovy" must not be used to endorse or promote
020 products derived from this Software without prior written
021 permission of The Codehaus. For written permission,
022 please contact info@codehaus.org.
023
024 4. Products derived from this Software may not be called "groovy"
025 nor may "groovy" appear in their names without prior written
026 permission of The Codehaus. "groovy" is a registered
027 trademark of The Codehaus.
028
029 5. Due credit should be given to The Codehaus -
030 http://groovy.codehaus.org/
031
032 THIS SOFTWARE IS PROVIDED BY THE CODEHAUS AND CONTRIBUTORS
033 ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
034 NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
035 FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
036 THE CODEHAUS OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
037 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
038 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
039 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
040 HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
041 STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
042 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
043 OF THE POSSIBILITY OF SUCH DAMAGE.
044
045 */
046 package groovy.xml;
047
048 import groovy.util.BuilderSupport;
049 import groovy.util.IndentPrinter;
050
051 import java.io.PrintWriter;
052 import java.io.Writer;
053 import java.util.Iterator;
054 import java.util.Map;
055
056 /**
057 * A helper class for creating XML or HTML markup
058 *
059 * @author <a href="mailto:james@coredevelopers.net">James Strachan</a>
060 * @author Stefan Matthias Aust
061 * @author <a href="mailto:scottstirling@rcn.com">Scott Stirling</a>
062 * @version $Revision: 4350 $
063 */
064 public class MarkupBuilder extends BuilderSupport {
065 private IndentPrinter out;
066 private boolean nospace;
067 private int state;
068 private boolean nodeIsEmpty = true;
069 private boolean useDoubleQuotes = false;
070
071 public MarkupBuilder() {
072 this(new IndentPrinter());
073 }
074
075 public MarkupBuilder(PrintWriter writer) {
076 this(new IndentPrinter(writer));
077 }
078
079 public MarkupBuilder(Writer writer) {
080 this(new IndentPrinter(new PrintWriter(writer)));
081 }
082
083 public MarkupBuilder(IndentPrinter out) {
084 this.out = out;
085 }
086
087 /**
088 * Returns <code>true</code> if attribute values are output with
089 * double quotes; <code>false</code> if single quotes are used.
090 * By default, single quotes are used.
091 */
092 public boolean getDoubleQuotes() {
093 return this.useDoubleQuotes;
094 }
095
096 /**
097 * Sets whether the builder outputs attribute values in double
098 * quotes or single quotes.
099 * @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 &apos;</li>
176 * <li>& as &amp;</li>
177 * <li>< as &lt;</li>
178 * <li>> as &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("&", "&");
190 }
191 if (value.matches(".*\\'.*")) {
192 value = value.replaceAll("\\'", "'");
193 }
194 if (value.matches(".*<.*")) {
195 value = value.replaceAll("<", "<");
196 }
197 if (value.matches(".*>.*")) {
198 value = value.replaceAll(">", ">");
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>& as &amp;</li>
233 * <li>< as &lt;</li>
234 * <li>> as &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 &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, "&");
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, "<");
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, ">");
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, """);
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, "'");
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 }