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:
113: protected Object createNode(Object name) {
114: this .nodeIsEmpty = true;
115: toState(1, name);
116: return name;
117: }
118:
119: protected Object createNode(Object name, Object value) {
120: toState(2, name);
121: this .nodeIsEmpty = false;
122: out.print(">");
123: out.print(escapeElementContent(value.toString()));
124: return name;
125: }
126:
127: protected Object createNode(Object name, Map attributes,
128: Object value) {
129: toState(1, name);
130: for (Iterator iter = attributes.entrySet().iterator(); iter
131: .hasNext();) {
132: Map.Entry entry = (Map.Entry) iter.next();
133: out.print(" ");
134:
135: // Output the attribute name,
136: print(entry.getKey().toString());
137:
138: // Output the attribute value within quotes. Use whichever
139: // type of quotes are currently configured.
140: out.print(this .useDoubleQuotes ? "=\"" : "='");
141: print(escapeAttributeValue(entry.getValue().toString()));
142: out.print(this .useDoubleQuotes ? "\"" : "'");
143: }
144:
145: if (value != null) {
146: nodeIsEmpty = false;
147: out.print(">" + escapeElementContent(value.toString())
148: + "</" + name + ">");
149: } else {
150: nodeIsEmpty = true;
151: }
152:
153: return name;
154: }
155:
156: protected Object createNode(Object name, Map attributes) {
157: return createNode(name, attributes, null);
158: }
159:
160: protected void nodeCompleted(Object parent, Object node) {
161: toState(3, node);
162: out.flush();
163: }
164:
165: protected void print(Object node) {
166: out.print(node == null ? "null" : node.toString());
167: }
168:
169: protected Object getName(String methodName) {
170: return super .getName(methodName);
171: }
172:
173: /**
174: * Returns a String with special XML characters escaped as entities so that
175: * output XML is valid. Escapes the following characters as corresponding
176: * entities:
177: * <ul>
178: * <li>\' as &apos;</li>
179: * <li>& as &amp;</li>
180: * <li>< as &lt;</li>
181: * <li>> as &gt;</li>
182: * </ul>
183: *
184: * @param value to be searched and replaced for XML special characters.
185: * @return value with XML characters escaped
186: * @deprecated
187: * @see #escapeXmlValue(String, boolean)
188: */
189: protected String transformValue(String value) {
190: // & has to be checked and replaced before others
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: if (value.matches(".*>.*")) {
201: value = value.replaceAll(">", ">");
202: }
203: return value;
204: }
205:
206: /**
207: * Escapes a string so that it can be used directly as an XML
208: * attribute value.
209: * @param value The string to escape.
210: * @return A new string in which all characters that require escaping
211: * have been replaced with the corresponding XML entities.
212: * @see #escapeXmlValue(String, boolean)
213: */
214: private String escapeAttributeValue(String value) {
215: return escapeXmlValue(value, true);
216: }
217:
218: /**
219: * Escapes a string so that it can be used directly in XML element
220: * content.
221: * @param value The string to escape.
222: * @return A new string in which all characters that require escaping
223: * have been replaced with the corresponding XML entities.
224: * @see #escapeXmlValue(String, boolean)
225: */
226: private String escapeElementContent(String value) {
227: return escapeXmlValue(value, false);
228: }
229:
230: /**
231: * Escapes a string so that it can be used in XML text successfully.
232: * It replaces the following characters with the corresponding XML
233: * entities:
234: * <ul>
235: * <li>& as &amp;</li>
236: * <li>< as &lt;</li>
237: * <li>> as &gt;</li>
238: * </ul>
239: * If the string is to be added as an attribute value, these
240: * characters are also escaped:
241: * <ul>
242: * <li>' as &apos;</li>
243: * </ul>
244: * @param value The string to escape.
245: * @param isAttrValue <code>true</code> if the string is to be used
246: * as an attribute value, otherwise <code>false</code>.
247: * @return A new string in which all characters that require escaping
248: * have been replaced with the corresponding XML entities.
249: */
250: private String escapeXmlValue(String value, boolean isAttrValue) {
251: StringBuffer buffer = new StringBuffer(value);
252: for (int i = 0, n = buffer.length(); i < n; i++) {
253: switch (buffer.charAt(i)) {
254: case '&':
255: buffer.replace(i, i + 1, "&");
256:
257: // We're replacing a single character by a string of
258: // length 5, so we need to update the index variable
259: // and the total length.
260: i += 4;
261: n += 4;
262: break;
263:
264: case '<':
265: buffer.replace(i, i + 1, "<");
266:
267: // We're replacing a single character by a string of
268: // length 4, so we need to update the index variable
269: // and the total length.
270: i += 3;
271: n += 3;
272: break;
273:
274: case '>':
275: buffer.replace(i, i + 1, ">");
276:
277: // We're replacing a single character by a string of
278: // length 4, so we need to update the index variable
279: // and the total length.
280: i += 3;
281: n += 3;
282: break;
283:
284: case '"':
285: // The double quote is only escaped if the value is for
286: // an attribute and the builder is configured to output
287: // attribute values inside double quotes.
288: if (isAttrValue && this .useDoubleQuotes) {
289: buffer.replace(i, i + 1, """);
290:
291: // We're replacing a single character by a string of
292: // length 6, so we need to update the index variable
293: // and the total length.
294: i += 5;
295: n += 5;
296: }
297: break;
298:
299: case '\'':
300: // The apostrophe is only escaped if the value is for an
301: // attribute, as opposed to element content, and if the
302: // builder is configured to surround attribute values with
303: // single quotes.
304: if (isAttrValue && !this .useDoubleQuotes) {
305: buffer.replace(i, i + 1, "'");
306:
307: // We're replacing a single character by a string of
308: // length 6, so we need to update the index variable
309: // and the total length.
310: i += 5;
311: n += 5;
312: }
313: break;
314:
315: default:
316: break;
317: }
318: }
319:
320: return buffer.toString();
321: }
322:
323: private void toState(int next, Object name) {
324: switch (state) {
325: case 0:
326: switch (next) {
327: case 1:
328: case 2:
329: out.print("<");
330: print(name);
331: break;
332: case 3:
333: throw new Error();
334: }
335: break;
336: case 1:
337: switch (next) {
338: case 1:
339: case 2:
340: out.print(">");
341: if (nospace) {
342: nospace = false;
343: } else {
344: out.println();
345: out.incrementIndent();
346: out.printIndent();
347: }
348: out.print("<");
349: print(name);
350: break;
351: case 3:
352: if (nodeIsEmpty) {
353: out.print(" />");
354: }
355: break;
356: }
357: break;
358: case 2:
359: switch (next) {
360: case 1:
361: case 2:
362: throw new Error();
363: case 3:
364: out.print("</");
365: print(name);
366: out.print(">");
367: break;
368: }
369: break;
370: case 3:
371: switch (next) {
372: case 1:
373: case 2:
374: if (nospace) {
375: nospace = false;
376: } else {
377: out.println();
378: out.printIndent();
379: }
380: out.print("<");
381: print(name);
382: break;
383: case 3:
384: if (nospace) {
385: nospace = false;
386: } else {
387: out.println();
388: out.decrementIndent();
389: out.printIndent();
390: }
391: out.print("</");
392: print(name);
393: out.print(">");
394: break;
395: }
396: break;
397: }
398: state = next;
399: }
400: }
|