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 }