1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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
133 print(entry.getKey().toString());
134
135
136
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
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
255
256
257 i += 4;
258 n += 4;
259 break;
260
261 case '<':
262 buffer.replace(i, i + 1, "<");
263
264
265
266
267 i += 3;
268 n += 3;
269 break;
270
271 case '>':
272 buffer.replace(i, i + 1, ">");
273
274
275
276
277 i += 3;
278 n += 3;
279 break;
280
281 case '"':
282
283
284
285 if (isAttrValue && this.useDoubleQuotes) {
286 buffer.replace(i, i + 1, """);
287
288
289
290
291 i += 5;
292 n += 5;
293 }
294 break;
295
296 case '\'':
297
298
299
300
301 if (isAttrValue && !this.useDoubleQuotes){
302 buffer.replace(i, i + 1, "'");
303
304
305
306
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 }