1package org.testng.reporters;
2
3import java.io.Writer;
4import java.util.Properties;
5import java.util.Stack;
6import java.util.regex.Pattern;
7
8import 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 */
16public 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
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   */
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   */
51  public XMLStringBuffer(IBuffer buffer, String start) {
52    init(buffer, start);
53  }
54
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  */
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   */
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 "&lt;!DOCTYPE " "&gt;"
89   */
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   */
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   */
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   */
126  public void push(String tagName, @Nullable Properties attributes) {
127    push(tagName, "", attributes);
128  }
129
130  public void push(String tagName, String... attributes) {
131    push(tagName, createProperties(attributes));
132  }
133
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   */
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   */
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   */
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   */
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   */
203  public void addRequired(String tagName, @Nullable String value, @Nullable Properties attributes) {
204    XMLUtils.xmlRequired(m_buffer, m_currentIndent, tagName, value, attributes);
205  }
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   */
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
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   */
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   */
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   */
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   */
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   */
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
283  public void addEmptyElement(String tagName, String... attributes) {
284    addEmptyElement(tagName, createProperties(attributes));
285  }
286
287  public void addComment(String comment) {
288    m_buffer.append(m_currentIndent).append("<!-- " + comment.replaceAll("[-]{2,}", "-") + " -->\n");
289  }
290
291  public void addString(String s) {
292    m_buffer.append(s);
293  }
294
295  private static void ppp(String s) {
296    System.out.println("[XMLStringBuffer] " + s);
297  }
298
299  /**
300   * Add a CDATA tag.
301   */
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   */
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   */
336  public String toXML() {
337    return INVALID_XML_CHARS.matcher(m_buffer.toString()).replaceAll("");
338  }
339
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
359  public String getCurrentIndent() {
360    return m_currentIndent;
361  }
362
363  public void toWriter(Writer fw) {
364    m_buffer.toWriter(fw);
365  }
366}
367
368
369////////////////////////
370
371class Tag {
372  public final String tagName;
373  public final String indent;
374  public final Properties properties;
375
376  public Tag(String ind, String n, Properties p) {
377    tagName = n;
378    indent = ind;
379    properties = p;
380  }
381
382  @Override
383  public String toString() {
384    return tagName;
385  }
386}
387