You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2020/09/18 01:27:26 UTC
[groovy] branch GROOVY_3_0_X updated (4409282 -> a6fe071)
This is an automated email from the ASF dual-hosted git repository.
paulk pushed a change to branch GROOVY_3_0_X
in repository https://gitbox.apache.org/repos/asf/groovy.git.
from 4409282 GROOVY-9739: check knownFields for accessedVariable is FieldNode case (closes #1369)
new 638bd40 Add character filter to MarkupBuilder
new 31bd9ad Reduce non-Characters down to java relevant size
new fdae025 Prettify MarkupBuilderTest code indentation
new f77b660 GROOVY-9745: Provide a way to filter illegal/discouraged characters when using MarkupBuilder (closes #1366)
new a6fe071 GROOVY-9745: Remove wrong extra comments (closes #1372)
The 5 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../groovy/runtime/StringGroovyMethods.java | 55 +++++++++
.../vmplugin/v8/PluginDefaultGroovyMethods.java | 11 ++
subprojects/groovy-xml/build.gradle | 7 ++
.../src/main/java/groovy/xml/MarkupBuilder.java | 85 +++++++-------
.../xml/markupsupport/AllControlToUndefined.java | 20 ++--
.../xml/markupsupport/DoubleQuoteFilter.java | 15 +--
.../xml/markupsupport/SingleQuoteFilter.java | 15 +--
.../markupsupport/StandardControlToUndefined.java | 21 ++--
.../markupsupport/StandardXmlAttributeFilter.java | 25 ++--
.../xml/markupsupport/StandardXmlFilter.java | 25 ++--
.../xml/markupsupport/SurrogateToUndefined.java | 16 ++-
.../StreamingMarkupWriter.java | 51 ++++----
.../xml/MarkupBuilderIllegalCharactersSpec.groovy | 129 +++++++++++++++++++++
.../groovy/groovy/xml/MarkupBuilderTest.groovy | 125 +++++++++++---------
14 files changed, 435 insertions(+), 165 deletions(-)
copy src/test/org/codehaus/groovy/classgen/asm/sc/LoopsStaticCompileTest.groovy => subprojects/groovy-xml/src/main/java/groovy/xml/markupsupport/AllControlToUndefined.java (65%)
copy src/test/org/codehaus/groovy/classgen/asm/sc/LoopsStaticCompileTest.groovy => subprojects/groovy-xml/src/main/java/groovy/xml/markupsupport/DoubleQuoteFilter.java (74%)
copy src/test/org/codehaus/groovy/classgen/asm/sc/LoopsStaticCompileTest.groovy => subprojects/groovy-xml/src/main/java/groovy/xml/markupsupport/SingleQuoteFilter.java (74%)
copy src/test/org/codehaus/groovy/classgen/asm/sc/LoopsStaticCompileTest.groovy => subprojects/groovy-xml/src/main/java/groovy/xml/markupsupport/StandardControlToUndefined.java (64%)
copy src/test/org/codehaus/groovy/classgen/asm/sc/LoopsStaticCompileTest.groovy => subprojects/groovy-xml/src/main/java/groovy/xml/markupsupport/StandardXmlAttributeFilter.java (61%)
copy src/test/org/codehaus/groovy/classgen/asm/sc/LoopsStaticCompileTest.groovy => subprojects/groovy-xml/src/main/java/groovy/xml/markupsupport/StandardXmlFilter.java (61%)
copy src/test/org/codehaus/groovy/classgen/asm/sc/LoopsStaticCompileTest.groovy => subprojects/groovy-xml/src/main/java/groovy/xml/markupsupport/SurrogateToUndefined.java (68%)
create mode 100644 subprojects/groovy-xml/src/test/groovy/groovy/xml/MarkupBuilderIllegalCharactersSpec.groovy
[groovy] 03/05: Prettify MarkupBuilderTest code indentation
Posted by pa...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
paulk pushed a commit to branch GROOVY_3_0_X
in repository https://gitbox.apache.org/repos/asf/groovy.git
commit fdae025096e0cef6c2cddc0bbc090c90a5f088b1
Author: Simon Tost <si...@tngtech.com>
AuthorDate: Fri Sep 11 22:53:49 2020 +0200
Prettify MarkupBuilderTest code indentation
---
.../groovy/groovy/xml/MarkupBuilderTest.groovy | 125 ++++++++++++---------
1 file changed, 69 insertions(+), 56 deletions(-)
diff --git a/subprojects/groovy-xml/src/test/groovy/groovy/xml/MarkupBuilderTest.groovy b/subprojects/groovy-xml/src/test/groovy/groovy/xml/MarkupBuilderTest.groovy
index eeadaae..916ba58 100644
--- a/subprojects/groovy-xml/src/test/groovy/groovy/xml/MarkupBuilderTest.groovy
+++ b/subprojects/groovy-xml/src/test/groovy/groovy/xml/MarkupBuilderTest.groovy
@@ -75,35 +75,48 @@ class MarkupBuilderTest extends BuilderTestSupport {
}
}
assertExpectedXml m, '''\
-<root1 a='5' b='7'>hello1<elem1 c='4'>hello2<elem2 d='4'>hello3</elem2>
-</elem1>
-<elem1 c='4'>hello2<elem2>hello3</elem2>
-<elem2 d='4'>hello3</elem2>
-</elem1>
-<elem1 c='4'>hello2<elem2 d='4'>hello3</elem2>
-<elem2>hello3</elem2>
-</elem1>
-<elem1 c='4'>hello2<elem2 d='4' />
-<elem2 d='4'>hello3</elem2>
-</elem1>
-<elem1 c='4'>hello2<elem2 d='4'>hello3</elem2>
-<elem2 d='4' />
-</elem1>
-<elem1>hello2<elem2 d='4'>hello3</elem2>
-<elem2 d='4' />
-</elem1>
-</root1>'''
+ <root1 a='5' b='7'>
+ hello1
+ <elem1 c='4'>
+ hello2
+ <elem2 d='4'>hello3</elem2>
+ </elem1>
+ <elem1 c='4'>
+ hello2
+ <elem2>hello3</elem2>
+ <elem2 d='4'>hello3</elem2>
+ </elem1>
+ <elem1 c='4'>
+ hello2
+ <elem2 d='4'>hello3</elem2>
+ <elem2>hello3</elem2>
+ </elem1>
+ <elem1 c='4'>
+ hello2
+ <elem2 d='4' />
+ <elem2 d='4'>hello3</elem2>
+ </elem1>
+ <elem1 c='4'>
+ hello2
+ <elem2 d='4'>hello3</elem2>
+ <elem2 d='4' />
+ </elem1>
+ <elem1>hello2<elem2 d='4'>hello3</elem2>
+ <elem2 d='4' />
+ </elem1>
+ </root1>
+ '''
}
void testMarkupWithColonsAndNamespaces() {
def expectedXml = '''\
-<ns1:customer-description>
- <last-name>Laforge</last-name>
- <first-name>
- <first>Guillaume</first>
- <initial-letters>A.J.</initial-letters>
- </first-name>
-</ns1:customer-description>'''
+ <ns1:customer-description>
+ <last-name>Laforge</last-name>
+ <first-name>
+ <first>Guillaume</first>
+ <initial-letters>A.J.</initial-letters>
+ </first-name>
+ </ns1:customer-description>'''.stripIndent()
xml."ns1:customer-description"{
"last-name"("Laforge")
"first-name"{
@@ -121,23 +134,23 @@ class MarkupBuilderTest extends BuilderTestSupport {
*/
void testBuilder() {
String expectedXml = '''\
-<chars>
- <ampersand a='&'>&</ampersand>
- <quote attr='"'>"</quote>
- <apostrophe attr='''>'</apostrophe>
- <lessthan attr='value'>chars: & < > '</lessthan>
- <element attr='value 1 & 2'>chars: & < > " in middle</element>
- <greaterthan>></greaterthan>
- <emptyElement />
- <null />
- <nullAttribute t1='' />
- <emptyWithAttributes attr1='set' />
- <emptyAttribute t1='' />
- <parent key='value'>
- <label for='usernameId'>Username: </label>
- <input name='test' id='1' />
- </parent>
-</chars>'''
+ <chars>
+ <ampersand a='&'>&</ampersand>
+ <quote attr='"'>"</quote>
+ <apostrophe attr='''>'</apostrophe>
+ <lessthan attr='value'>chars: & < > '</lessthan>
+ <element attr='value 1 & 2'>chars: & < > " in middle</element>
+ <greaterthan>></greaterthan>
+ <emptyElement />
+ <null />
+ <nullAttribute t1='' />
+ <emptyWithAttributes attr1='set' />
+ <emptyAttribute t1='' />
+ <parent key='value'>
+ <label for='usernameId'>Username: </label>
+ <input name='test' id='1' />
+ </parent>
+ </chars>'''.stripIndent()
// Generate the markup.
xml.chars {
@@ -166,15 +179,15 @@ class MarkupBuilderTest extends BuilderTestSupport {
*/
void testBuilderWithDoubleQuotes() {
String expectedXml = '''\
-<chars>
- <ampersand a="&">&</ampersand>
- <quote attr=""">"</quote>
- <apostrophe attr="'">'</apostrophe>
- <lessthan attr="value">chars: & < > '</lessthan>
- <element attr="value 1 & 2">chars: & < > " in middle</element>
- <greaterthan>></greaterthan>
- <emptyElement />
-</chars>'''
+ <chars>
+ <ampersand a="&">&</ampersand>
+ <quote attr=""">"</quote>
+ <apostrophe attr="'">'</apostrophe>
+ <lessthan attr="value">chars: & < > '</lessthan>
+ <element attr="value 1 & 2">chars: & < > " in middle</element>
+ <greaterthan>></greaterthan>
+ <emptyElement />
+ </chars>'''.stripIndent()
// Generate the markup.
xml.doubleQuotes = true
@@ -250,11 +263,11 @@ class MarkupBuilderTest extends BuilderTestSupport {
mkp.yieldUnescaped("\n <inside>wow</inside>")
}
String expectedXml = '''\
-<?xml version='1.0' encoding='UTF-8'?>
-<element><>
- <inner>foobar</inner>
- <inside>wow</inside>
-</element>'''
+ <?xml version='1.0' encoding='UTF-8'?>
+ <element><>
+ <inner>foobar</inner>
+ <inside>wow</inside>
+ </element>'''.stripIndent()
assertEquals expectedXml, fixEOLs(writer.toString())
}
[groovy] 02/05: Reduce non-Characters down to java relevant size
Posted by pa...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
paulk pushed a commit to branch GROOVY_3_0_X
in repository https://gitbox.apache.org/repos/asf/groovy.git
commit 31bd9ad700c069c1085dbaee2977667978eac481
Author: Simon Tost <si...@tngtech.com>
AuthorDate: Fri Sep 11 23:03:32 2020 +0200
Reduce non-Characters down to java relevant size
- Java Strings are UTF-16
---
subprojects/groovy-xml/src/main/java/groovy/xml/MarkupBuilder.java | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/subprojects/groovy-xml/src/main/java/groovy/xml/MarkupBuilder.java b/subprojects/groovy-xml/src/main/java/groovy/xml/MarkupBuilder.java
index bdbde82..163a4d0 100644
--- a/subprojects/groovy-xml/src/main/java/groovy/xml/MarkupBuilder.java
+++ b/subprojects/groovy-xml/src/main/java/groovy/xml/MarkupBuilder.java
@@ -495,8 +495,7 @@ public class MarkupBuilder extends BuilderSupport {
}
private boolean isNonCharacter(char ch) {
- return 0xFDD0 <= ch && ch <= 0xFDEF
- || ((ch % 0x10000 ^ 0xFFFE) == 0 || (ch % 0x10000 ^ 0xFFFF) == 0) && ch >> 16 <= 0x10;
+ return 0xFDD0 <= ch && ch <= 0xFDEF || ((ch ^ 0xFFFE) == 0 || (ch ^ 0xFFFF) == 0);
}
}
[groovy] 05/05: GROOVY-9745: Remove wrong extra comments (closes
#1372)
Posted by pa...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
paulk pushed a commit to branch GROOVY_3_0_X
in repository https://gitbox.apache.org/repos/asf/groovy.git
commit a6fe071c6eff4cd57ceebe17f55025322d5ad111
Author: Simon Tost <si...@tngtech.com>
AuthorDate: Thu Sep 17 19:12:06 2020 +0200
GROOVY-9745: Remove wrong extra comments (closes #1372)
---
.../groovy/groovy/xml/MarkupBuilderIllegalCharactersSpec.groovy | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/subprojects/groovy-xml/src/test/groovy/groovy/xml/MarkupBuilderIllegalCharactersSpec.groovy b/subprojects/groovy-xml/src/test/groovy/groovy/xml/MarkupBuilderIllegalCharactersSpec.groovy
index bd5e368..46bec48 100644
--- a/subprojects/groovy-xml/src/test/groovy/groovy/xml/MarkupBuilderIllegalCharactersSpec.groovy
+++ b/subprojects/groovy-xml/src/test/groovy/groovy/xml/MarkupBuilderIllegalCharactersSpec.groovy
@@ -60,7 +60,7 @@ class MarkupBuilderIllegalCharactersSpec extends Specification {
'ext control I' | true | extControl1 // Discouraged XML, not in HTML char references
'Next line NEL' | true | nextLine // Not in HTML char references
'ext control II' | true | extControl2 // Discouraged XML, not in HTML char references
- 'Surrogates' | true | surrogates // Not necessarily XML, not in HTML char references
+ 'Surrogates' | true | surrogates // Not necessarily XML
'Non-characters I' | true | nonCharacters1 // Discouraged XML, not in HTML char references
'Non-characters II' | true | nonCharacters2 // Discouraged XML, not in HTML char references
}
@@ -92,7 +92,7 @@ class MarkupBuilderIllegalCharactersSpec extends Specification {
'ext control I' | false | extControl1 // Discouraged XML, not in HTML char references
'Next line NEL' | false | nextLine // Not in HTML char references
'ext control II' | false | extControl2 // Discouraged XML, not in HTML char references
- 'Surrogates' | true | surrogates // Not necessarily XML, not in HTML char references
+ 'Surrogates' | true | surrogates // Not necessarily XML
'Non-characters I' | false | nonCharacters1 // Discouraged XML, not in HTML char references
'Non-characters II' | false | nonCharacters2 // Discouraged XML, not in HTML char references
}
@@ -122,7 +122,7 @@ class MarkupBuilderIllegalCharactersSpec extends Specification {
'ext control I' | false | extControl1 // Discouraged XML, not in HTML char references
'Next line NEL' | false | nextLine // Not in HTML char references
'ext control II' | false | extControl2 // Discouraged XML, not in HTML char references
- 'Surrogates' | false | surrogates // Not necessarily XML, not in HTML char references
+ 'Surrogates' | false | surrogates // Not necessarily XML
'Non-characters I' | false | nonCharacters1 // Discouraged XML, not in HTML char references
'Non-characters II' | false | nonCharacters2 // Discouraged XML, not in HTML char references
}
[groovy] 04/05: GROOVY-9745: Provide a way to filter
illegal/discouraged characters when using MarkupBuilder (closes #1366)
Posted by pa...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
paulk pushed a commit to branch GROOVY_3_0_X
in repository https://gitbox.apache.org/repos/asf/groovy.git
commit f77b6608ed3b77eb78384e3624ecf8ed9be14bc0
Author: Paul King <pa...@asert.com.au>
AuthorDate: Thu Sep 17 23:28:11 2020 +1000
GROOVY-9745: Provide a way to filter illegal/discouraged characters when using MarkupBuilder (closes #1366)
---
.../groovy/runtime/StringGroovyMethods.java | 55 ++++++++
.../vmplugin/v8/PluginDefaultGroovyMethods.java | 11 ++
.../src/main/java/groovy/xml/MarkupBuilder.java | 141 +++++++--------------
.../xml/markupsupport/AllControlToUndefined.java | 34 +++++
.../xml/markupsupport/DoubleQuoteFilter.java | 29 +++++
.../xml/markupsupport/SingleQuoteFilter.java | 29 +++++
.../markupsupport/StandardControlToUndefined.java | 35 +++++
.../markupsupport/StandardXmlAttributeFilter.java | 39 ++++++
.../xml/markupsupport/StandardXmlFilter.java | 39 ++++++
.../xml/markupsupport/SurrogateToUndefined.java | 34 +++++
.../StreamingMarkupWriter.java | 51 ++++----
.../xml/MarkupBuilderIllegalCharactersSpec.groovy | 31 ++---
12 files changed, 393 insertions(+), 135 deletions(-)
diff --git a/src/main/java/org/codehaus/groovy/runtime/StringGroovyMethods.java b/src/main/java/org/codehaus/groovy/runtime/StringGroovyMethods.java
index 73faf6e..9baf1f8 100644
--- a/src/main/java/org/codehaus/groovy/runtime/StringGroovyMethods.java
+++ b/src/main/java/org/codehaus/groovy/runtime/StringGroovyMethods.java
@@ -49,8 +49,10 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
+import java.util.Optional;
import java.util.Set;
import java.util.StringTokenizer;
+import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -678,6 +680,59 @@ public class StringGroovyMethods extends DefaultGroovyMethodsSupport {
}
/**
+ * Iterates through this String a character at a time collecting either the
+ * original character or a transformed replacement String.
+ * The return value is an {@code Optional} either having a value equal to the transformed replacement String
+ * or {@code empty()} to indicate that no transformation is required.
+ * <p>
+ * <pre class="groovyTestCase">
+ * import java.util.function.Function
+ * import static java.util.Optional.*
+ *
+ * Function<Character, Optional<String>> xform1 = s -> s == 'o' ? of('_O') : empty()
+ * Function<Character, Optional<String>> xform2 = { it == 'G' ? of('G_') : empty() }
+ * assert "Groovy".collectReplacements([xform1, xform2]) == 'G_r_O_Ovy'
+ * </pre>
+ *
+ * @param self the original String
+ * @param transforms one or more transforms which potentially convert a single character to a transformed string
+ * @return A new string in which all characters that require escaping
+ * have been replaced with the corresponding replacements
+ * as determined by the {@code transform} function.
+ *
+ * @since 3.0.6
+ */
+ public static String collectReplacements(final String self, final List<Function<Character, Optional<String>>> transforms) {
+ if (self == null) return self;
+
+ StringBuilder sb = null; // lazy create for edge-case efficiency
+ for (int i = 0, len = self.length(); i < len; i++) {
+ final char ch = self.charAt(i);
+ Optional<String> replacement = Optional.empty();
+ for (Function<Character, Optional<String>> next : transforms) {
+ replacement = next.apply(ch);
+ if (replacement.isPresent()) {
+ break;
+ }
+ }
+
+ if (replacement.isPresent()) {
+ // output differs from input; we write to our local buffer
+ if (sb == null) {
+ sb = new StringBuilder((int) (1.1 * len));
+ sb.append(self, 0, i);
+ }
+ sb.append(replacement.get());
+ } else if (sb != null) {
+ // earlier output differs from input; we write to our local buffer
+ sb.append(ch);
+ }
+ }
+
+ return sb == null ? self : sb.toString();
+ }
+
+ /**
* Processes each regex group matched substring of the given CharSequence. If the closure
* parameter takes one argument, an array with all match groups is passed to it.
* If the closure takes as many arguments as there are match groups, then each
diff --git a/src/main/java/org/codehaus/groovy/vmplugin/v8/PluginDefaultGroovyMethods.java b/src/main/java/org/codehaus/groovy/vmplugin/v8/PluginDefaultGroovyMethods.java
index 701cd72..33252f6 100644
--- a/src/main/java/org/codehaus/groovy/vmplugin/v8/PluginDefaultGroovyMethods.java
+++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/PluginDefaultGroovyMethods.java
@@ -54,6 +54,7 @@ import java.util.function.IntFunction;
import java.util.function.IntPredicate;
import java.util.function.LongFunction;
import java.util.function.LongPredicate;
+import java.util.function.Supplier;
import java.util.function.ToDoubleFunction;
import java.util.function.ToIntFunction;
import java.util.function.ToLongFunction;
@@ -872,4 +873,14 @@ public class PluginDefaultGroovyMethods extends DefaultGroovyMethodsSupport {
}
return DoubleStream.of(self.getAsDouble());
}
+
+ /**
+ * Provide JDK9 {@code or} on JDK8.
+ */
+ public static <T> Optional<T> orElse(Optional<T> self, Supplier<? extends Optional<? extends T>> supplier) {
+ if (self.isPresent()) {
+ return self;
+ }
+ return (Optional<T>) supplier.get();
+ }
}
diff --git a/subprojects/groovy-xml/src/main/java/groovy/xml/MarkupBuilder.java b/subprojects/groovy-xml/src/main/java/groovy/xml/MarkupBuilder.java
index 163a4d0..f11cf55 100644
--- a/subprojects/groovy-xml/src/main/java/groovy/xml/MarkupBuilder.java
+++ b/subprojects/groovy-xml/src/main/java/groovy/xml/MarkupBuilder.java
@@ -18,16 +18,25 @@
*/
package groovy.xml;
-import groovy.lang.Closure;
import groovy.namespace.QName;
import groovy.util.BuilderSupport;
import groovy.util.IndentPrinter;
+import groovy.xml.markupsupport.DoubleQuoteFilter;
+import groovy.xml.markupsupport.SingleQuoteFilter;
+import groovy.xml.markupsupport.StandardXmlAttributeFilter;
+import groovy.xml.markupsupport.StandardXmlFilter;
import org.codehaus.groovy.runtime.StringGroovyMethods;
import java.io.PrintWriter;
import java.io.Writer;
+import java.util.ArrayList;
import java.util.Iterator;
+import java.util.List;
import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+
+import static org.codehaus.groovy.vmplugin.v8.PluginDefaultGroovyMethods.orElse;
/**
* A helper class for creating XML or HTML markup.
@@ -72,7 +81,15 @@ public class MarkupBuilder extends BuilderSupport {
private boolean omitEmptyAttributes = false;
private boolean expandEmptyElements = false;
private boolean escapeAttributes = true;
- private CharFilter characterFilter = CharFilter.NONE;
+ private List<Function<Character, Optional<String>>> additionalFilters = null;
+
+ public List<Function<Character, Optional<String>>> getAdditionalFilters() {
+ return additionalFilters;
+ }
+
+ public void setAdditionalFilters(List<Function<Character, Optional<String>>> additionalFilters) {
+ this.additionalFilters = additionalFilters;
+ }
/**
* Returns the escapeAttributes property value.
@@ -225,45 +242,6 @@ public class MarkupBuilder extends BuilderSupport {
this.expandEmptyElements = expandEmptyElements;
}
- /**
- * Returns the current character filter.
- *
- * @return the character filter used by this builder.
- */
- public CharFilter getCharacterFilter() { return this.characterFilter; }
-
- /**
- * Set a filter to limit the characters, that can appear in attribute values and text nodes.
- * <p>
- * Some unicode character are either not allowed, discouraged or not referenceable with an escape sequence
- * by specification. Especially XML parsers might have trouble dealing with some of those characters.
- * Since HTML strives for closeness to XML, filtering might be helpful there, too, albeit to a lesser degree.
- * </p>
- * <p>
- * Examples include null bytes (0x0), control characters (0x1C "file separator"), surrogates or non-characters.
- * If a filter policy is used, characters that fail to pass will be replaced by 0xFFFD (�) in the output.
- * </p>
- * <p>
- * Available policies are:
- * <dl>
- * <dt>NONE (Default)</dt>
- * <dd>No filter is applied to the output</dd>
- * <dt>XML_ALL</dt>
- * <dd>
- * Allow all characters, that are neccessarily supported. According to the XML spec.<br>
- * Given as #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] ( | [#x10000-#x10FFFF] )<br>
- * (as of Aug. 2020)
- * </dd>
- * <dt>XML_STRICT</dt>
- * <dd>
- * Filter out none-supported <it>and</it> discouraged characters, according to XML spec.
- * </dd>
- * </dl>
- * </p>
- * @param characterFilter character policy to use
- */
- public void setCharacterFilter(CharFilter characterFilter) { this.characterFilter = characterFilter; }
-
protected IndentPrinter getPrinter() {
return this.out;
}
@@ -431,71 +409,38 @@ public class MarkupBuilder extends BuilderSupport {
* have been replaced with the corresponding XML entities.
*/
private String escapeXmlValue(String value, boolean isAttrValue) {
- if (value == null)
+ if (value == null) {
throw new IllegalArgumentException();
- return StringGroovyMethods.collectReplacements(value, new ReplacingClosure(isAttrValue, useDoubleQuotes, characterFilter));
+ }
+ List<Function<Character, Optional<String>>> transforms = new ArrayList<>();
+ transforms.add(new DefaultXmlEscapingFunction(isAttrValue, useDoubleQuotes));
+ if (additionalFilters != null) {
+ transforms.addAll(additionalFilters);
+ }
+ return StringGroovyMethods.collectReplacements(value, transforms);
}
- private static class ReplacingClosure extends Closure<String> {
+ public static class DefaultXmlEscapingFunction implements Function<Character, Optional<String>> {
private final boolean isAttrValue;
- private final boolean useDoubleQuotes;
- private final CharFilter characterFilter;
- public ReplacingClosure(boolean isAttrValue, boolean useDoubleQuotes, CharFilter characterFilter) {
- super(null);
- this.isAttrValue = isAttrValue;
- this.useDoubleQuotes = useDoubleQuotes;
- this.characterFilter = characterFilter;
- }
+ private final Function<Character, Optional<String>> stdFilter = new StandardXmlFilter();
+ private final Function<Character, Optional<String>> attrFilter = new StandardXmlAttributeFilter();
+ private final Function<Character, Optional<String>> quoteFilter;
- public String doCall(Character ch) {
- switch (ch) {
- case 0:
- if (characterFilter != CharFilter.NONE) return "\uFFFD";
- break;
- case '&':
- return "&";
- case '<':
- return "<";
- case '>':
- return ">";
- case '\n':
- if (isAttrValue) return " ";
- break;
- case '\r':
- if (isAttrValue) return " ";
- break;
- case '\t':
- if (isAttrValue) return "	";
- 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 && useDoubleQuotes) return """;
- break;
- case '\'':
- // The apostrophe is only escaped if the value is for an
- // attribute, as opposed to element content, and if the
- // builder is configured to surround attribute values with
- // single quotes.
- if (isAttrValue && !useDoubleQuotes) return "'";
- break;
- }
- if (characterFilter != CharFilter.NONE) {
- if (Character.isSurrogate(ch)
- || ch < 127 && ch != 9 && ch != 10 && ch != 12 && ch != 13) {
- return "\uFFFD";
- }
- }
- if (characterFilter == CharFilter.XML_STRICT) {
- if (Character.isISOControl(ch) || isNonCharacter(ch)) return "\uFFFD";
- }
- return null;
+ public DefaultXmlEscapingFunction(boolean isAttrValue, boolean useDoubleQuotes) {
+ this.isAttrValue = isAttrValue;
+ this.quoteFilter = useDoubleQuotes ? new DoubleQuoteFilter() : new SingleQuoteFilter();
}
- private boolean isNonCharacter(char ch) {
- return 0xFDD0 <= ch && ch <= 0xFDEF || ((ch ^ 0xFFFE) == 0 || (ch ^ 0xFFFF) == 0);
+ public Optional<String> apply(Character ch) {
+ return orElse(stdFilter.apply(ch),
+ () -> {
+ if (isAttrValue) {
+ return orElse(attrFilter.apply(ch), () -> quoteFilter.apply(ch));
+ }
+ return Optional.empty();
+ }
+ );
}
}
diff --git a/subprojects/groovy-xml/src/main/java/groovy/xml/markupsupport/AllControlToUndefined.java b/subprojects/groovy-xml/src/main/java/groovy/xml/markupsupport/AllControlToUndefined.java
new file mode 100644
index 0000000..d85bf06
--- /dev/null
+++ b/subprojects/groovy-xml/src/main/java/groovy/xml/markupsupport/AllControlToUndefined.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.xml.markupsupport;
+
+import java.util.Optional;
+
+public class AllControlToUndefined extends StandardControlToUndefined {
+ public Optional<String> apply(Character ch) {
+ if (Character.isISOControl(ch) || isNonCharacter(ch)) {
+ return Optional.of("\uFFFD");
+ }
+ return super.apply(ch);
+ }
+
+ private boolean isNonCharacter(char ch) {
+ return 0xFDD0 <= ch && ch <= 0xFDEF || ((ch ^ 0xFFFE) == 0 || (ch ^ 0xFFFF) == 0);
+ }
+}
diff --git a/subprojects/groovy-xml/src/main/java/groovy/xml/markupsupport/DoubleQuoteFilter.java b/subprojects/groovy-xml/src/main/java/groovy/xml/markupsupport/DoubleQuoteFilter.java
new file mode 100644
index 0000000..2545f5b
--- /dev/null
+++ b/subprojects/groovy-xml/src/main/java/groovy/xml/markupsupport/DoubleQuoteFilter.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.xml.markupsupport;
+
+import java.util.Optional;
+import java.util.function.Function;
+
+public class DoubleQuoteFilter implements Function<Character, Optional<String>> {
+ public Optional<String> apply(Character ch) {
+ if (ch == '"') return Optional.of(""");
+ else return Optional.empty();
+ }
+}
diff --git a/subprojects/groovy-xml/src/main/java/groovy/xml/markupsupport/SingleQuoteFilter.java b/subprojects/groovy-xml/src/main/java/groovy/xml/markupsupport/SingleQuoteFilter.java
new file mode 100644
index 0000000..fee3cce
--- /dev/null
+++ b/subprojects/groovy-xml/src/main/java/groovy/xml/markupsupport/SingleQuoteFilter.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.xml.markupsupport;
+
+import java.util.Optional;
+import java.util.function.Function;
+
+public class SingleQuoteFilter implements Function<Character, Optional<String>> {
+ public Optional<String> apply(Character ch) {
+ if (ch == '\'') return Optional.of("'");
+ else return Optional.empty();
+ }
+}
diff --git a/subprojects/groovy-xml/src/main/java/groovy/xml/markupsupport/StandardControlToUndefined.java b/subprojects/groovy-xml/src/main/java/groovy/xml/markupsupport/StandardControlToUndefined.java
new file mode 100644
index 0000000..d71f035
--- /dev/null
+++ b/subprojects/groovy-xml/src/main/java/groovy/xml/markupsupport/StandardControlToUndefined.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.xml.markupsupport;
+
+import java.util.Optional;
+import java.util.function.Function;
+
+public class StandardControlToUndefined implements Function<Character, Optional<String>> {
+ public Optional<String> apply(Character ch) {
+ if (ch < 32 && !isXmlAllowedControl(ch)) {
+ return Optional.of("\uFFFD");
+ }
+ return Optional.empty();
+ }
+
+ private boolean isXmlAllowedControl(char ch) {
+ return ch == 9 || ch == 10 || ch == 12 || ch == 13;
+ }
+}
diff --git a/subprojects/groovy-xml/src/main/java/groovy/xml/markupsupport/StandardXmlAttributeFilter.java b/subprojects/groovy-xml/src/main/java/groovy/xml/markupsupport/StandardXmlAttributeFilter.java
new file mode 100644
index 0000000..f612ef5
--- /dev/null
+++ b/subprojects/groovy-xml/src/main/java/groovy/xml/markupsupport/StandardXmlAttributeFilter.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.xml.markupsupport;
+
+import java.util.Optional;
+import java.util.function.Function;
+
+public class StandardXmlAttributeFilter implements Function<Character, Optional<String>> {
+ public Optional<String> apply(Character ch) {
+ String result = null;
+ switch (ch) {
+ case '\n':
+ result = " ";
+ break;
+ case '\r':
+ result = " ";
+ break;
+ case '\t':
+ result = "	";
+ }
+ return Optional.ofNullable(result);
+ }
+}
diff --git a/subprojects/groovy-xml/src/main/java/groovy/xml/markupsupport/StandardXmlFilter.java b/subprojects/groovy-xml/src/main/java/groovy/xml/markupsupport/StandardXmlFilter.java
new file mode 100644
index 0000000..771964a
--- /dev/null
+++ b/subprojects/groovy-xml/src/main/java/groovy/xml/markupsupport/StandardXmlFilter.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.xml.markupsupport;
+
+import java.util.Optional;
+import java.util.function.Function;
+
+public class StandardXmlFilter implements Function<Character, Optional<String>> {
+ public Optional<String> apply(Character ch) {
+ String result = null;
+ switch (ch) {
+ case '&':
+ result = "&";
+ break;
+ case '<':
+ result = "<";
+ break;
+ case '>':
+ result = ">";
+ }
+ return Optional.ofNullable(result);
+ }
+}
diff --git a/subprojects/groovy-xml/src/main/java/groovy/xml/markupsupport/SurrogateToUndefined.java b/subprojects/groovy-xml/src/main/java/groovy/xml/markupsupport/SurrogateToUndefined.java
new file mode 100644
index 0000000..2e8cf8a
--- /dev/null
+++ b/subprojects/groovy-xml/src/main/java/groovy/xml/markupsupport/SurrogateToUndefined.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.xml.markupsupport;
+
+import java.util.Optional;
+import java.util.function.Function;
+
+/**
+ * Convert surrogate chars to XML undefined character.
+ */
+public class SurrogateToUndefined implements Function<Character, Optional<String>> {
+ public Optional<String> apply(Character ch) {
+ if (Character.isSurrogate(ch)) {
+ return Optional.of("\uFFFD");
+ }
+ return Optional.empty();
+ }
+}
diff --git a/subprojects/groovy-xml/src/main/java/groovy/xml/streamingmarkupsupport/StreamingMarkupWriter.java b/subprojects/groovy-xml/src/main/java/groovy/xml/streamingmarkupsupport/StreamingMarkupWriter.java
index 6f35a52..ec73b79 100644
--- a/subprojects/groovy-xml/src/main/java/groovy/xml/streamingmarkupsupport/StreamingMarkupWriter.java
+++ b/subprojects/groovy-xml/src/main/java/groovy/xml/streamingmarkupsupport/StreamingMarkupWriter.java
@@ -19,12 +19,18 @@
package groovy.xml.streamingmarkupsupport;
import groovy.io.EncodingAwareBufferedWriter;
+import groovy.xml.markupsupport.DoubleQuoteFilter;
+import groovy.xml.markupsupport.SingleQuoteFilter;
+import groovy.xml.markupsupport.StandardXmlAttributeFilter;
+import groovy.xml.markupsupport.StandardXmlFilter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
+import java.util.Optional;
+import java.util.function.Function;
public class StreamingMarkupWriter extends Writer {
protected final Writer writer;
@@ -34,7 +40,9 @@ public class StreamingMarkupWriter extends Writer {
protected boolean writingAttribute = false;
protected boolean haveHighSurrogate = false;
protected StringBuilder surrogatePair = new StringBuilder(2);
- private boolean useDoubleQuotes;
+ private final Function<Character, Optional<String>> stdFilter = new StandardXmlFilter();
+ private final Function<Character, Optional<String>> attrFilter = new StandardXmlAttributeFilter();
+ private final Function<Character, Optional<String>> quoteFilter;
private final Writer escapedWriter = new Writer() {
/* (non-Javadoc)
* @see java.io.Writer#close()
@@ -54,15 +62,12 @@ public class StreamingMarkupWriter extends Writer {
* @see java.io.Writer#write(int)
*/
public void write(final int c) throws IOException {
- if (c == '<') {
- StreamingMarkupWriter.this.writer.write("<");
- } else if (c == '>') {
- StreamingMarkupWriter.this.writer.write(">");
- } else if (c == '&') {
- StreamingMarkupWriter.this.writer.write("&");
- } else {
- StreamingMarkupWriter.this.write(c);
+ Optional<String> transformed = stdFilter.apply((char) c);
+ if (transformed.isPresent()) {
+ StreamingMarkupWriter.this.writer.write(transformed.get());
+ return;
}
+ StreamingMarkupWriter.this.write(c);
}
/* (non-Javadoc)
@@ -92,7 +97,7 @@ public class StreamingMarkupWriter extends Writer {
}
public StreamingMarkupWriter(final Writer writer, final String encoding, boolean useDoubleQuotes) {
- this.useDoubleQuotes = useDoubleQuotes;
+ this.quoteFilter = useDoubleQuotes ? new DoubleQuoteFilter() : new SingleQuoteFilter();
this.writer = writer;
if (encoding != null) {
@@ -168,19 +173,21 @@ public class StreamingMarkupWriter extends Writer {
this.writer.write("&#x");
this.writer.write(Integer.toHexString(c));
this.writer.write(';');
- } else if (c == '\'' && this.writingAttribute && !useDoubleQuotes) {
- this.writer.write("'");
- } else if (c == '"' && this.writingAttribute && useDoubleQuotes) {
- this.writer.write(""");
- } else if (c == '\n' && this.writingAttribute) {
- this.writer.write(" ");
- } else if (c == '\r' && this.writingAttribute) {
- this.writer.write(" ");
- } else if (c == '\t' && this.writingAttribute) {
- this.writer.write("	");
- } else {
- this.writer.write(c);
+ return;
+ }
+ if (this.writingAttribute) {
+ Optional<String> transformed = attrFilter.apply((char) c);
+ if (transformed.isPresent()) {
+ this.writer.write(transformed.get());
+ return;
+ }
+ transformed = quoteFilter.apply((char) c);
+ if (transformed.isPresent()) {
+ this.writer.write(transformed.get());
+ return;
+ }
}
+ this.writer.write(c);
}
}
diff --git a/subprojects/groovy-xml/src/test/groovy/groovy/xml/MarkupBuilderIllegalCharactersSpec.groovy b/subprojects/groovy-xml/src/test/groovy/groovy/xml/MarkupBuilderIllegalCharactersSpec.groovy
index d978d9c..bd5e368 100644
--- a/subprojects/groovy-xml/src/test/groovy/groovy/xml/MarkupBuilderIllegalCharactersSpec.groovy
+++ b/subprojects/groovy-xml/src/test/groovy/groovy/xml/MarkupBuilderIllegalCharactersSpec.groovy
@@ -18,6 +18,9 @@
*/
package groovy.xml
+import groovy.xml.markupsupport.AllControlToUndefined
+import groovy.xml.markupsupport.StandardControlToUndefined
+import groovy.xml.markupsupport.SurrogateToUndefined
import spock.lang.Specification
class MarkupBuilderIllegalCharactersSpec extends Specification {
@@ -34,13 +37,13 @@ class MarkupBuilderIllegalCharactersSpec extends Specification {
given:
def writer
def xml
- def characterFilter = MarkupBuilder.CharFilter.XML_STRICT
+ def additionalFilters = [new SurrogateToUndefined(), new AllControlToUndefined()]
expect:
characters.each {
writer = new StringWriter()
xml = new MarkupBuilder(writer)
- xml.characterFilter = characterFilter
+ xml.additionalFilters = additionalFilters
def encoded = shouldFilter ? '\uFFFD' : it
xml.tag(attr: it, it)
@@ -52,12 +55,12 @@ class MarkupBuilderIllegalCharactersSpec extends Specification {
where:
characterRange | shouldFilter | characters
- 'Null' | true | nullCharacter // Not neccessarily XML, not allowed in HTML
- 'C0 control w/o whitespace' | true | c0Controls // Not neccessarily XML, not in HTML char references
+ 'Null' | true | nullCharacter // Not necessarily XML, not allowed in HTML
+ 'C0 control w/o whitespace' | true | c0Controls // Not necessarily XML, not in HTML char references
'ext control I' | true | extControl1 // Discouraged XML, not in HTML char references
'Next line NEL' | true | nextLine // Not in HTML char references
'ext control II' | true | extControl2 // Discouraged XML, not in HTML char references
- 'Surrogates' | true | surrogates // Not neccessarily XML, not in HTML char references
+ 'Surrogates' | true | surrogates // Not necessarily XML, not in HTML char references
'Non-characters I' | true | nonCharacters1 // Discouraged XML, not in HTML char references
'Non-characters II' | true | nonCharacters2 // Discouraged XML, not in HTML char references
}
@@ -66,13 +69,13 @@ class MarkupBuilderIllegalCharactersSpec extends Specification {
given:
def writer
def xml
- def characterFilter = MarkupBuilder.CharFilter.XML_ALL
+ def additionalFilters = [new SurrogateToUndefined(), new StandardControlToUndefined()]
expect:
characters.each {
writer = new StringWriter()
xml = new MarkupBuilder(writer)
- xml.characterFilter = characterFilter
+ xml.additionalFilters = additionalFilters
def encoded = shouldFilter ? '\uFFFD' : it
xml.tag(attr: it, it)
@@ -84,12 +87,12 @@ class MarkupBuilderIllegalCharactersSpec extends Specification {
where:
characterRange | shouldFilter | characters
- 'Null' | true | nullCharacter // Not neccessarily XML, not allowed in HTML
- 'C0 control w/o whitespace' | true | c0Controls // Not neccessarily XML, not in HTML char references
+ 'Null' | true | nullCharacter // Not necessarily XML, not allowed in HTML
+ 'C0 control w/o whitespace' | true | c0Controls // Not necessarily XML, not in HTML char references
'ext control I' | false | extControl1 // Discouraged XML, not in HTML char references
'Next line NEL' | false | nextLine // Not in HTML char references
'ext control II' | false | extControl2 // Discouraged XML, not in HTML char references
- 'Surrogates' | true | surrogates // Not neccessarily XML, not in HTML char references
+ 'Surrogates' | true | surrogates // Not necessarily XML, not in HTML char references
'Non-characters I' | false | nonCharacters1 // Discouraged XML, not in HTML char references
'Non-characters II' | false | nonCharacters2 // Discouraged XML, not in HTML char references
}
@@ -98,13 +101,11 @@ class MarkupBuilderIllegalCharactersSpec extends Specification {
given:
def writer
def xml
- def characterFilter = MarkupBuilder.CharFilter.NONE
expect:
characters.each {
writer = new StringWriter()
xml = new MarkupBuilder(writer)
- xml.characterFilter = characterFilter
def encoded = shouldFilter ? '\uFFFD' : it
xml.tag(attr: it, it)
@@ -116,12 +117,12 @@ class MarkupBuilderIllegalCharactersSpec extends Specification {
where:
characterRange | shouldFilter | characters
- 'Null' | false | nullCharacter // Not neccessarily XML, not allowed in HTML
- 'C0 control w/o whitespace' | false | c0Controls // Not neccessarily XML, not in HTML char references
+ 'Null' | false | nullCharacter // Not necessarily XML, not allowed in HTML
+ 'C0 control w/o whitespace' | false | c0Controls // Not necessarily XML, not in HTML char references
'ext control I' | false | extControl1 // Discouraged XML, not in HTML char references
'Next line NEL' | false | nextLine // Not in HTML char references
'ext control II' | false | extControl2 // Discouraged XML, not in HTML char references
- 'Surrogates' | false | surrogates // Not neccessarily XML, not in HTML char references
+ 'Surrogates' | false | surrogates // Not necessarily XML, not in HTML char references
'Non-characters I' | false | nonCharacters1 // Discouraged XML, not in HTML char references
'Non-characters II' | false | nonCharacters2 // Discouraged XML, not in HTML char references
}
[groovy] 01/05: Add character filter to MarkupBuilder
Posted by pa...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
paulk pushed a commit to branch GROOVY_3_0_X
in repository https://gitbox.apache.org/repos/asf/groovy.git
commit 638bd40170e93c51655c398474d4366928543a23
Author: Simon Tost <si...@tngtech.com>
AuthorDate: Sun Aug 30 20:40:21 2020 +0200
Add character filter to MarkupBuilder
---
subprojects/groovy-xml/build.gradle | 7 ++
.../src/main/java/groovy/xml/MarkupBuilder.java | 65 ++++++++++-
.../xml/MarkupBuilderIllegalCharactersSpec.groovy | 128 +++++++++++++++++++++
3 files changed, 198 insertions(+), 2 deletions(-)
diff --git a/subprojects/groovy-xml/build.gradle b/subprojects/groovy-xml/build.gradle
index 2528544..c37fdd3 100644
--- a/subprojects/groovy-xml/build.gradle
+++ b/subprojects/groovy-xml/build.gradle
@@ -21,6 +21,13 @@ dependencies {
testImplementation rootProject.sourceSets.test.runtimeClasspath
testImplementation "xmlunit:xmlunit:$xmlunitVersion"
testImplementation project(':groovy-test')
+ testImplementation ("org.spockframework:spock-core:$spockVersion") {
+ exclude group: 'org.codehaus.groovy'
+ }
+}
+
+test {
+ useJUnitPlatform()
}
task moduleDescriptor(type: org.codehaus.groovy.gradle.WriteExtensionDescriptorTask) {
diff --git a/subprojects/groovy-xml/src/main/java/groovy/xml/MarkupBuilder.java b/subprojects/groovy-xml/src/main/java/groovy/xml/MarkupBuilder.java
index b7d028e..bdbde82 100644
--- a/subprojects/groovy-xml/src/main/java/groovy/xml/MarkupBuilder.java
+++ b/subprojects/groovy-xml/src/main/java/groovy/xml/MarkupBuilder.java
@@ -61,6 +61,8 @@ import java.util.Map;
* </ul>
*/
public class MarkupBuilder extends BuilderSupport {
+ public enum CharFilter { XML_STRICT, XML_ALL, NONE }
+
private IndentPrinter out;
private boolean nospace;
private int state;
@@ -70,6 +72,7 @@ public class MarkupBuilder extends BuilderSupport {
private boolean omitEmptyAttributes = false;
private boolean expandEmptyElements = false;
private boolean escapeAttributes = true;
+ private CharFilter characterFilter = CharFilter.NONE;
/**
* Returns the escapeAttributes property value.
@@ -222,6 +225,45 @@ public class MarkupBuilder extends BuilderSupport {
this.expandEmptyElements = expandEmptyElements;
}
+ /**
+ * Returns the current character filter.
+ *
+ * @return the character filter used by this builder.
+ */
+ public CharFilter getCharacterFilter() { return this.characterFilter; }
+
+ /**
+ * Set a filter to limit the characters, that can appear in attribute values and text nodes.
+ * <p>
+ * Some unicode character are either not allowed, discouraged or not referenceable with an escape sequence
+ * by specification. Especially XML parsers might have trouble dealing with some of those characters.
+ * Since HTML strives for closeness to XML, filtering might be helpful there, too, albeit to a lesser degree.
+ * </p>
+ * <p>
+ * Examples include null bytes (0x0), control characters (0x1C "file separator"), surrogates or non-characters.
+ * If a filter policy is used, characters that fail to pass will be replaced by 0xFFFD (�) in the output.
+ * </p>
+ * <p>
+ * Available policies are:
+ * <dl>
+ * <dt>NONE (Default)</dt>
+ * <dd>No filter is applied to the output</dd>
+ * <dt>XML_ALL</dt>
+ * <dd>
+ * Allow all characters, that are neccessarily supported. According to the XML spec.<br>
+ * Given as #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] ( | [#x10000-#x10FFFF] )<br>
+ * (as of Aug. 2020)
+ * </dd>
+ * <dt>XML_STRICT</dt>
+ * <dd>
+ * Filter out none-supported <it>and</it> discouraged characters, according to XML spec.
+ * </dd>
+ * </dl>
+ * </p>
+ * @param characterFilter character policy to use
+ */
+ public void setCharacterFilter(CharFilter characterFilter) { this.characterFilter = characterFilter; }
+
protected IndentPrinter getPrinter() {
return this.out;
}
@@ -391,21 +433,26 @@ public class MarkupBuilder extends BuilderSupport {
private String escapeXmlValue(String value, boolean isAttrValue) {
if (value == null)
throw new IllegalArgumentException();
- return StringGroovyMethods.collectReplacements(value, new ReplacingClosure(isAttrValue, useDoubleQuotes));
+ return StringGroovyMethods.collectReplacements(value, new ReplacingClosure(isAttrValue, useDoubleQuotes, characterFilter));
}
private static class ReplacingClosure extends Closure<String> {
private final boolean isAttrValue;
private final boolean useDoubleQuotes;
+ private final CharFilter characterFilter;
- public ReplacingClosure(boolean isAttrValue, boolean useDoubleQuotes) {
+ public ReplacingClosure(boolean isAttrValue, boolean useDoubleQuotes, CharFilter characterFilter) {
super(null);
this.isAttrValue = isAttrValue;
this.useDoubleQuotes = useDoubleQuotes;
+ this.characterFilter = characterFilter;
}
public String doCall(Character ch) {
switch (ch) {
+ case 0:
+ if (characterFilter != CharFilter.NONE) return "\uFFFD";
+ break;
case '&':
return "&";
case '<':
@@ -435,8 +482,22 @@ public class MarkupBuilder extends BuilderSupport {
if (isAttrValue && !useDoubleQuotes) return "'";
break;
}
+ if (characterFilter != CharFilter.NONE) {
+ if (Character.isSurrogate(ch)
+ || ch < 127 && ch != 9 && ch != 10 && ch != 12 && ch != 13) {
+ return "\uFFFD";
+ }
+ }
+ if (characterFilter == CharFilter.XML_STRICT) {
+ if (Character.isISOControl(ch) || isNonCharacter(ch)) return "\uFFFD";
+ }
return null;
}
+
+ private boolean isNonCharacter(char ch) {
+ return 0xFDD0 <= ch && ch <= 0xFDEF
+ || ((ch % 0x10000 ^ 0xFFFE) == 0 || (ch % 0x10000 ^ 0xFFFF) == 0) && ch >> 16 <= 0x10;
+ }
}
private void toState(int next, Object name) {
diff --git a/subprojects/groovy-xml/src/test/groovy/groovy/xml/MarkupBuilderIllegalCharactersSpec.groovy b/subprojects/groovy-xml/src/test/groovy/groovy/xml/MarkupBuilderIllegalCharactersSpec.groovy
new file mode 100644
index 0000000..d978d9c
--- /dev/null
+++ b/subprojects/groovy-xml/src/test/groovy/groovy/xml/MarkupBuilderIllegalCharactersSpec.groovy
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.xml
+
+import spock.lang.Specification
+
+class MarkupBuilderIllegalCharactersSpec extends Specification {
+ static final char[] nullCharacter = [0x0]
+ static final char[] c0Controls = ((0x0..0x8) + [0xB] + (0xE..0x1F))
+ static final char[] extControl1 = (0x7F..0x84)
+ static final char[] nextLine = [0x85]
+ static final char[] extControl2 = (0x86..0x9F)
+ static final char[] surrogates = (0xD800..0xDFFF)
+ static final char[] nonCharacters1 = (0xFDD0..0xFDEF)
+ static final char[] nonCharacters2 = [0xFFFE, 0xFFFF]
+
+ def "Default MarkupBuilder character filter for #characterRange"(String characterRange, boolean shouldFilter, char[] characters) {
+ given:
+ def writer
+ def xml
+ def characterFilter = MarkupBuilder.CharFilter.XML_STRICT
+
+ expect:
+ characters.each {
+ writer = new StringWriter()
+ xml = new MarkupBuilder(writer)
+ xml.characterFilter = characterFilter
+ def encoded = shouldFilter ? '\uFFFD' : it
+
+ xml.tag(attr: it, it)
+ def actual = writer.toString()
+
+ assert actual == "<tag attr='$encoded'>$encoded</tag>",
+ "Character (${it as int}) is encoded correctly"
+ }
+
+ where:
+ characterRange | shouldFilter | characters
+ 'Null' | true | nullCharacter // Not neccessarily XML, not allowed in HTML
+ 'C0 control w/o whitespace' | true | c0Controls // Not neccessarily XML, not in HTML char references
+ 'ext control I' | true | extControl1 // Discouraged XML, not in HTML char references
+ 'Next line NEL' | true | nextLine // Not in HTML char references
+ 'ext control II' | true | extControl2 // Discouraged XML, not in HTML char references
+ 'Surrogates' | true | surrogates // Not neccessarily XML, not in HTML char references
+ 'Non-characters I' | true | nonCharacters1 // Discouraged XML, not in HTML char references
+ 'Non-characters II' | true | nonCharacters2 // Discouraged XML, not in HTML char references
+ }
+
+ def "MarkupBuilder ALL_XML character filter for #characterRange"(String characterRange, boolean shouldFilter, char[] characters) {
+ given:
+ def writer
+ def xml
+ def characterFilter = MarkupBuilder.CharFilter.XML_ALL
+
+ expect:
+ characters.each {
+ writer = new StringWriter()
+ xml = new MarkupBuilder(writer)
+ xml.characterFilter = characterFilter
+ def encoded = shouldFilter ? '\uFFFD' : it
+
+ xml.tag(attr: it, it)
+ def actual = writer.toString()
+
+ assert actual == "<tag attr='$encoded'>$encoded</tag>",
+ "Character (${it as int}) is encoded correctly"
+ }
+
+ where:
+ characterRange | shouldFilter | characters
+ 'Null' | true | nullCharacter // Not neccessarily XML, not allowed in HTML
+ 'C0 control w/o whitespace' | true | c0Controls // Not neccessarily XML, not in HTML char references
+ 'ext control I' | false | extControl1 // Discouraged XML, not in HTML char references
+ 'Next line NEL' | false | nextLine // Not in HTML char references
+ 'ext control II' | false | extControl2 // Discouraged XML, not in HTML char references
+ 'Surrogates' | true | surrogates // Not neccessarily XML, not in HTML char references
+ 'Non-characters I' | false | nonCharacters1 // Discouraged XML, not in HTML char references
+ 'Non-characters II' | false | nonCharacters2 // Discouraged XML, not in HTML char references
+ }
+
+ def "MarkupBuilder NONE character filter for #characterRange"(String characterRange, boolean shouldFilter, char[] characters) {
+ given:
+ def writer
+ def xml
+ def characterFilter = MarkupBuilder.CharFilter.NONE
+
+ expect:
+ characters.each {
+ writer = new StringWriter()
+ xml = new MarkupBuilder(writer)
+ xml.characterFilter = characterFilter
+ def encoded = shouldFilter ? '\uFFFD' : it
+
+ xml.tag(attr: it, it)
+ def actual = writer.toString()
+
+ assert actual == "<tag attr='$encoded'>$encoded</tag>",
+ "Character (${it as int}) is encoded correctly"
+ }
+
+ where:
+ characterRange | shouldFilter | characters
+ 'Null' | false | nullCharacter // Not neccessarily XML, not allowed in HTML
+ 'C0 control w/o whitespace' | false | c0Controls // Not neccessarily XML, not in HTML char references
+ 'ext control I' | false | extControl1 // Discouraged XML, not in HTML char references
+ 'Next line NEL' | false | nextLine // Not in HTML char references
+ 'ext control II' | false | extControl2 // Discouraged XML, not in HTML char references
+ 'Surrogates' | false | surrogates // Not neccessarily XML, not in HTML char references
+ 'Non-characters I' | false | nonCharacters1 // Discouraged XML, not in HTML char references
+ 'Non-characters II' | false | nonCharacters2 // Discouraged XML, not in HTML char references
+ }
+}