1 package org.testng.reporters; 2 3 import java.io.Writer; 4 import java.util.Properties; 5 import java.util.Stack; 6 import java.util.regex.Pattern; 7 8 import org.testng.internal.Nullable; 9 10 /** 11 * This class allows you to generate an XML text document by pushing 12 * and popping tags from a stack maintained internally. 13 * 14 * @author <a href="mailto:cedric@beust.com">Cedric Beust</a> Jul 21, 2003 15 */ 16 public class XMLStringBuffer { 17 /** End of line, value of 'line.separator' system property or '\n' */ 18 private static final String EOL = System.getProperty("line.separator", "\n"); 19 20 /** Tab space indent for XML document */ 21 private static final String DEFAULT_INDENT_INCREMENT = " "; 22 23 /** The buffer to hold the xml document */ 24 private IBuffer m_buffer; 25 26 /** The stack of tags to make sure XML document is well formed. */ 27 private final Stack<Tag> m_tagStack = new Stack<>(); 28 29 /** A string of space character representing the current indentation. */ 30 private String m_currentIndent = ""; 31 XMLStringBuffer()32 public XMLStringBuffer() { 33 init(Buffer.create(), "", "1.0", "UTF-8"); 34 } 35 36 /** 37 * @param start A string of spaces indicating the indentation at which 38 * to start the generation. This constructor will not insert an <?xml 39 * prologue. 40 */ XMLStringBuffer(String start)41 public XMLStringBuffer(String start) { 42 init(Buffer.create(), start); 43 } 44 45 /** 46 * @param buffer The StringBuffer to use internally to represent the 47 * document. 48 * @param start A string of spaces indicating the indentation at which 49 * to start the generation. 50 */ XMLStringBuffer(IBuffer buffer, String start)51 public XMLStringBuffer(IBuffer buffer, String start) { 52 init(buffer, start); 53 } 54 init(IBuffer buffer, String start)55 private void init(IBuffer buffer, String start) { 56 init(buffer, start, null, null); 57 } 58 59 /** 60 * 61 * @param start A string of spaces indicating the indentation at which 62 * to start the generation. 63 */ init(IBuffer buffer, String start, @Nullable String version, @Nullable String encoding)64 private void init(IBuffer buffer, String start, @Nullable String version, @Nullable String encoding) { 65 m_buffer = buffer; 66 m_currentIndent = start; 67 if (version != null) { 68 setXmlDetails(version, encoding); 69 } 70 } 71 72 /** 73 * Set the xml version and encoding for this document. 74 * 75 * @param v the XML version 76 * @param enc the XML encoding 77 */ setXmlDetails(String v, String enc)78 public void setXmlDetails(String v, String enc) { 79 if (m_buffer.toString().length() != 0) { 80 throw new IllegalStateException("Buffer should be empty: '" + m_buffer.toString() + "'"); 81 } 82 m_buffer.append("<?xml version=\"" + v + "\" encoding=\"" + enc + "\"?>").append(EOL); 83 } 84 85 /** 86 * Set the doctype for this document. 87 * 88 * @param docType The DOCTYPE string, without the "<!DOCTYPE " ">" 89 */ setDocType(String docType)90 public void setDocType(String docType) { 91 m_buffer.append("<!DOCTYPE " + docType + ">" + EOL); 92 } 93 94 /** 95 * Push a new tag. Its value is stored and will be compared against the parameter 96 * passed to pop(). 97 * 98 * @param tagName The name of the tag. 99 * @param schema The schema to use (can be null or an empty string). 100 * @param attributes A Properties file representing the attributes (or null) 101 */ push(String tagName, @Nullable String schema, @Nullable Properties attributes)102 public void push(String tagName, @Nullable String schema, @Nullable Properties attributes) { 103 XMLUtils.xmlOpen(m_buffer, m_currentIndent, tagName + schema, attributes); 104 m_tagStack.push(new Tag(m_currentIndent, tagName, attributes)); 105 m_currentIndent += DEFAULT_INDENT_INCREMENT; 106 } 107 108 /** 109 * Push a new tag. Its value is stored and will be compared against the parameter 110 * passed to pop(). 111 * 112 * @param tagName The name of the tag. 113 * @param schema The schema to use (can be null or an empty string). 114 */ push(String tagName, @Nullable String schema)115 public void push(String tagName, @Nullable String schema) { 116 push(tagName, schema, null); 117 } 118 119 /** 120 * Push a new tag. Its value is stored and will be compared against the parameter 121 * passed to pop(). 122 * 123 * @param tagName The name of the tag. 124 * @param attributes A Properties file representing the attributes (or null) 125 */ push(String tagName, @Nullable Properties attributes)126 public void push(String tagName, @Nullable Properties attributes) { 127 push(tagName, "", attributes); 128 } 129 push(String tagName, String... attributes)130 public void push(String tagName, String... attributes) { 131 push(tagName, createProperties(attributes)); 132 } 133 createProperties(String[] attributes)134 private Properties createProperties(String[] attributes) { 135 Properties result = new Properties(); 136 if (attributes == null) { 137 return result; 138 } 139 if (attributes.length % 2 != 0) { 140 throw new IllegalArgumentException("Arguments 'attributes' length must be even. Actual: " + attributes.length); 141 } 142 for (int i = 0; i < attributes.length; i += 2) { 143 result.put(attributes[i], attributes[i + 1]); 144 } 145 return result; 146 } 147 148 /** 149 * Push a new tag. Its value is stored and will be compared against the parameter 150 * passed to pop(). 151 * 152 * @param tagName The name of the tag. 153 */ push(String tagName)154 public void push(String tagName) { 155 push(tagName, ""); 156 } 157 158 /** 159 * Pop the last pushed element without verifying it if matches the previously 160 * pushed tag. 161 */ pop()162 public void pop() { 163 pop(null); 164 } 165 166 /** 167 * Pop the last pushed element and throws an AssertionError if it doesn't 168 * match the corresponding tag that was pushed earlier. 169 * 170 * @param tagName The name of the tag this pop() is supposed to match. 171 */ pop(String tagName)172 public void pop(String tagName) { 173 m_currentIndent = m_currentIndent.substring(DEFAULT_INDENT_INCREMENT.length()); 174 Tag t = m_tagStack.pop(); 175 if (null != tagName) { 176 if (!tagName.equals(t.tagName)) { 177 // TODO Is it normal to throw an Error here? 178 throw new AssertionError( 179 "Popping the wrong tag: " + t.tagName + " but expected " + tagName); 180 } 181 } 182 XMLUtils.xmlClose(m_buffer, m_currentIndent, t.tagName, 183 XMLUtils.extractComment(tagName, t.properties)); 184 } 185 186 /** 187 * Add a required element to the current tag. An opening and closing tag 188 * will be generated even if value is null. 189 * @param tagName The name of the tag 190 * @param value The value for this tag 191 */ addRequired(String tagName, @Nullable String value)192 public void addRequired(String tagName, @Nullable String value) { 193 addRequired(tagName, value, (Properties) null); 194 } 195 196 /** 197 * Add a required element to the current tag. An opening and closing tag 198 * will be generated even if value is null. 199 * @param tagName The name of the tag 200 * @param value The value for this tag 201 * @param attributes A Properties file containing the attributes (or null) 202 */ addRequired(String tagName, @Nullable String value, @Nullable Properties attributes)203 public void addRequired(String tagName, @Nullable String value, @Nullable Properties attributes) { 204 XMLUtils.xmlRequired(m_buffer, m_currentIndent, tagName, value, attributes); 205 } addRequired(String tagName, @Nullable String value, String... attributes)206 public void addRequired(String tagName, @Nullable String value, String... attributes) { 207 addRequired(tagName, value, createProperties(attributes)); 208 } 209 210 /** 211 * Add an optional String element to the current tag. If value is null, nothing is 212 * added. 213 * @param tagName The name of the tag 214 * @param value The value for this tag 215 * @param attributes A Properties file containing the attributes (or null) 216 */ addOptional(String tagName, @Nullable String value, @Nullable Properties attributes)217 public void addOptional(String tagName, @Nullable String value, @Nullable Properties attributes) { 218 if (value != null) { 219 XMLUtils.xmlOptional(m_buffer, m_currentIndent, tagName, value, attributes); 220 } 221 } 222 addOptional(String tagName, @Nullable String value, String... attributes)223 public void addOptional(String tagName, @Nullable String value, String... attributes) { 224 if (value != null) { 225 XMLUtils.xmlOptional(m_buffer, m_currentIndent, tagName, value, createProperties(attributes)); 226 } 227 } 228 229 /** 230 * Add an optional String element to the current tag. If value is null, nothing is 231 * added. 232 * @param tagName The name of the tag 233 * @param value The value for this tag 234 */ addOptional(String tagName, @Nullable String value)235 public void addOptional(String tagName, @Nullable String value) { 236 addOptional(tagName, value, (Properties) null); 237 } 238 239 /** 240 * Add an optional Boolean element to the current tag. If value is null, nothing is 241 * added. 242 * @param tagName The name of the tag 243 * @param value The value for this tag 244 * @param attributes A Properties file containing the attributes (or null) 245 */ addOptional(String tagName, @Nullable Boolean value, @Nullable Properties attributes)246 public void addOptional(String tagName, @Nullable Boolean value, @Nullable Properties attributes) { 247 if (null != value) { 248 XMLUtils.xmlOptional(m_buffer, m_currentIndent, tagName, value.toString(), attributes); 249 } 250 } 251 252 /** 253 * Add an optional Boolean element to the current tag. If value is null, nothing is 254 * added. 255 * @param tagName The name of the tag 256 * @param value The value for this tag 257 */ addOptional(String tagName, @Nullable Boolean value)258 public void addOptional(String tagName, @Nullable Boolean value) { 259 addOptional(tagName, value, null); 260 } 261 262 /** 263 * Add an empty element tag (e.g. <foo/>) 264 * 265 * @param tagName The name of the tag 266 * 267 */ addEmptyElement(String tagName)268 public void addEmptyElement(String tagName) { 269 addEmptyElement(tagName, (Properties) null); 270 } 271 272 /** 273 * Add an empty element tag (e.g. <foo/>) 274 * @param tagName The name of the tag 275 * @param attributes A Properties file containing the attributes (or null) 276 */ addEmptyElement(String tagName, @Nullable Properties attributes)277 public void addEmptyElement(String tagName, @Nullable Properties attributes) { 278 m_buffer.append(m_currentIndent).append("<").append(tagName); 279 XMLUtils.appendAttributes(m_buffer, attributes); 280 m_buffer.append("/>").append(EOL); 281 } 282 addEmptyElement(String tagName, String... attributes)283 public void addEmptyElement(String tagName, String... attributes) { 284 addEmptyElement(tagName, createProperties(attributes)); 285 } 286 addComment(String comment)287 public void addComment(String comment) { 288 m_buffer.append(m_currentIndent).append("<!-- " + comment.replaceAll("[-]{2,}", "-") + " -->\n"); 289 } 290 addString(String s)291 public void addString(String s) { 292 m_buffer.append(s); 293 } 294 ppp(String s)295 private static void ppp(String s) { 296 System.out.println("[XMLStringBuffer] " + s); 297 } 298 299 /** 300 * Add a CDATA tag. 301 */ addCDATA(String content)302 public void addCDATA(String content) { 303 if (content == null) { 304 content = "null"; 305 } 306 if (content.contains("]]>")) { 307 String[] subStrings = content.split("]]>"); 308 m_buffer.append(m_currentIndent).append("<![CDATA[").append(subStrings[0]).append("]]]]>"); 309 for (int i = 1; i < subStrings.length - 1; i++) { 310 m_buffer.append("<![CDATA[>").append(subStrings[i]).append("]]]]>"); 311 } 312 m_buffer.append("<![CDATA[>").append(subStrings[subStrings.length - 1]).append("]]>"); 313 if (content.endsWith("]]>")) { 314 m_buffer.append("<![CDATA[]]]]>").append("<![CDATA[>]]>"); 315 } 316 m_buffer.append(EOL); 317 } else { 318 m_buffer.append(m_currentIndent).append("<![CDATA[").append(content).append("]]>" + EOL); 319 } 320 } 321 322 /** 323 * 324 * @return The StringBuffer used to create the document. 325 */ getStringBuffer()326 public IBuffer getStringBuffer() { 327 return m_buffer; 328 } 329 330 private static final Pattern INVALID_XML_CHARS = 331 Pattern.compile("[^\\u0009\\u000A\\u000D\\u0020-\\uD7FF\\uE000-\\uFFFD\uD800\uDC00-\uDBFF\uDFFF]"); 332 333 /** 334 * @return The String representation of the XML for this XMLStringBuffer. 335 */ toXML()336 public String toXML() { 337 return INVALID_XML_CHARS.matcher(m_buffer.toString()).replaceAll(""); 338 } 339 main(String[] argv)340 public static void main(String[] argv) { 341 IBuffer result = Buffer.create(); 342 XMLStringBuffer sb = new XMLStringBuffer(result, ""); 343 344 sb.push("family"); 345 Properties p = new Properties(); 346 p.setProperty("prop1", "value1"); 347 p.setProperty("prop2", "value2"); 348 sb.addRequired("cedric", "true", p); 349 sb.addRequired("alois", "true"); 350 sb.addOptional("anne-marie", (String) null); 351 sb.pop(); 352 353 System.out.println(result.toString()); 354 355 assert ("<family>" + EOL + "<cedric>true</cedric>" + EOL + "<alois>true</alois>" + EOL + "</family>" + EOL) 356 .equals(result.toString()); 357 } 358 getCurrentIndent()359 public String getCurrentIndent() { 360 return m_currentIndent; 361 } 362 toWriter(Writer fw)363 public void toWriter(Writer fw) { 364 m_buffer.toWriter(fw); 365 } 366 } 367 368 369 //////////////////////// 370 371 class Tag { 372 public final String tagName; 373 public final String indent; 374 public final Properties properties; 375 Tag(String ind, String n, Properties p)376 public Tag(String ind, String n, Properties p) { 377 tagName = n; 378 indent = ind; 379 properties = p; 380 } 381 382 @Override toString()383 public String toString() { 384 return tagName; 385 } 386 } 387