Index: src/test/groovy/xml/MarkupBuilderTest.groovy =================================================================== --- src/test/groovy/xml/MarkupBuilderTest.groovy (revision 4346) +++ src/test/groovy/xml/MarkupBuilderTest.groovy (working copy) @@ -15,14 +15,21 @@ * @author Pilho Kim */ class MarkupBuilderTest extends GroovyTestCase { - - StringWriter writer = new StringWriter() - MarkupBuilder chars = new MarkupBuilder(writer) - XmlParser parser = new XmlParser() + private StringWriter writer + private MarkupBuilder xml + protected void setUp() { + writer = new StringWriter() + xml = new MarkupBuilder(writer) + } + + /** + * Main test method. Checks that well-formed XML is generated + * and that the appropriate characters are escaped with the + * correct entities. + */ void testBuilder() { - String expectedXml = -""" + String expectedXml = ''' & " ' @@ -30,14 +37,14 @@ chars: & < > " in middle > -""" +''' // Generate the markup. - chars.chars { + xml.chars { ampersand(a: "&", "&") quote(attr: "\"", "\"") apostrophe(attr: "'", "'") - lessthan(attr: "value", "chars: & < > '") + lessthan(attr: "value", "chars: & < > '") element(attr: "value 1 & 2", "chars: & < > \" in middle") greaterthan(">") emptyElement() @@ -45,37 +52,99 @@ // Compare the MarkupBuilder generated XML with the 'expectedXml' // string. - def outputValue = writer.toString() - if (expectedXml.indexOf("\r\n") >= 0) expectedXml = expectedXml.replaceAll("\r\n", "\n"); - if (outputValue.indexOf("\r\n") >= 0) outputValue = outputValue.replaceAll("\r\n", "\n"); - assertEquals(expectedXml, outputValue) + assertEquals(expectedXml, fixEOLs(writer.toString())) } /** + * Tests the builder with double quotes for attribute values. + */ + void testBuilderWithDoubleQuotes() { + String expectedXml = ''' + & + " + ' + chars: & < > ' + chars: & < > " in middle + > + +''' + + // Generate the markup. + xml.doubleQuotes = true + xml.chars { + ampersand(a: "&", "&") + quote(attr: "\"", "\"") + apostrophe(attr: "'", "'") + lessthan(attr: "value", "chars: & < > '") + element(attr: "value 1 & 2", "chars: & < > \" in middle") + greaterthan(">") + emptyElement() + } + + // Compare the MarkupBuilder generated XML with the 'expectedXml' + // string. + assertEquals(expectedXml, fixEOLs(writer.toString())) + } + + /** * Tests that MarkupBuilder escapes element content correctly, even * when the content contains line-endings. */ void testEscapingMultiLineContent() { def expectedXml = -"""This is multi-line content with characters, such as <, that +'''This is multi-line content with characters, such as <, that require escaping. The other characters consist of: * > - greater than * & - ampersand -""" +''' // Generate the markup. - chars.element("""This is multi-line content with characters, such as <, that + xml.element('''This is multi-line content with characters, such as <, that require escaping. The other characters consist of: * > - greater than * & - ampersand -""") +''') // Compare the generated markup with the 'expectedXml' string. - def outputValue = writer.toString() - if (expectedXml.indexOf("\r\n") >= 0) expectedXml = expectedXml.replaceAll("\r\n", "\n"); - if (outputValue.indexOf("\r\n") >= 0) outputValue = outputValue.replaceAll("\r\n", "\n"); - assertEquals(expectedXml, outputValue) + assertEquals(expectedXml, fixEOLs(writer.toString())) } + + /** + * Checks against a regression bug whereby some empty elements were + * not closed. + */ + void testMarkupForClosingTags() { + def expectedXml = +''' + + + text + + + + text + + + + text + +''' + + // Generate the XML. + def list = ['first', 'second', 'third'] + + xml.ELEM1() { + list.each(){ r -> + xml.ELEM2(id:r, type:'2') { + xml.ELEM3A(id:r) + xml.ELEM3B(type:'3', 'text') + } + } + } + + // Check that the MarkupBuilder has generated the expected XML. + assertEquals(expectedXml, fixEOLs(writer.toString())) + } } Index: src/main/groovy/xml/MarkupBuilder.java =================================================================== --- src/main/groovy/xml/MarkupBuilder.java (revision 4346) +++ src/main/groovy/xml/MarkupBuilder.java (working copy) @@ -66,6 +66,7 @@ private boolean nospace; private int state; private boolean nodeIsEmpty = true; + private boolean useDoubleQuotes = false; public MarkupBuilder() { this(new IndentPrinter()); @@ -83,6 +84,25 @@ this.out = out; } + /** + * Returns true if attribute values are output with + * double quotes; false if single quotes are used. + * By default, single quotes are used. + */ + public boolean getDoubleQuotes() { + return this.useDoubleQuotes; + } + + /** + * Sets whether the builder outputs attribute values in double + * quotes or single quotes. + * @param useDoubleQuotes If this parameter is true, + * double quotes are used; otherwise, single quotes are. + */ + public void setDoubleQuotes(boolean useDoubleQuotes) { + this.useDoubleQuotes = useDoubleQuotes; + } + protected IndentPrinter getPrinter() { return this.out; } @@ -97,6 +117,7 @@ protected Object createNode(Object name, Object value) { toState(2, name); + this.nodeIsEmpty = false; out.print(">"); out.print(escapeElementContent(value.toString())); return name; @@ -107,16 +128,25 @@ for (Iterator iter = attributes.entrySet().iterator(); iter.hasNext();) { Map.Entry entry = (Map.Entry) iter.next(); out.print(" "); + + // Output the attribute name, print(entry.getKey().toString()); - out.print("='"); + + // Output the attribute value within quotes. Use whichever + // type of quotes are currently configured. + out.print(this.useDoubleQuotes ? "=\"" : "='"); print(escapeAttributeValue(entry.getValue().toString())); - out.print("'"); + out.print(this.useDoubleQuotes ? "\"" : "'"); } - if (value != null) - { + + if (value != null) { nodeIsEmpty = false; out.print(">" + escapeElementContent(value.toString()) + ""); } + else { + nodeIsEmpty = true; + } + return name; } @@ -134,8 +164,8 @@ } protected Object getName(String methodName) { - return super.getName(methodName); - } + return super.getName(methodName); + } /** * Returns a String with special XML characters escaped as entities so that @@ -214,10 +244,10 @@ * @return A new string in which all characters that require escaping * have been replaced with the corresponding XML entities. */ - private String escapeXmlValue(String value, boolean isAttrValue){ + private String escapeXmlValue(String value, boolean isAttrValue) { StringBuffer buffer = new StringBuffer(value); - for (int i = 0, n = buffer.length(); i < n; i++){ - switch (buffer.charAt(i)){ + for (int i = 0, n = buffer.length(); i < n; i++) { + switch (buffer.charAt(i)) { case '&': buffer.replace(i, i + 1, "&"); @@ -248,10 +278,27 @@ n += 3; break; + case '"': + // The double quote is only escaped if the value is for + // an attribute and the builder is configured to output + // attribute values inside double quotes. + if (isAttrValue && this.useDoubleQuotes) { + buffer.replace(i, i + 1, """); + + // We're replacing a single character by a string of + // length 6, so we need to update the index variable + // and the total length. + i += 5; + n += 5; + } + break; + case '\'': // The apostrophe is only escaped if the value is for an - // attribute, as opposed to element content. - if (isAttrValue){ + // attribute, as opposed to element content, and if the + // builder is configured to surround attribute values with + // single quotes. + if (isAttrValue && !this.useDoubleQuotes){ buffer.replace(i, i + 1, "'"); // We're replacing a single character by a string of @@ -347,5 +394,4 @@ } state = next; } - }