This is an automated email from the ASF dual-hosted git repository.

ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-text.git


The following commit(s) were added to refs/heads/master by this push:
     new 9656bdee Enable secure processing for the XML parser (#729)
9656bdee is described below

commit 9656bdeedc5fde6150b107b3a3eec2ab775b33b3
Author: Gary Gregory <[email protected]>
AuthorDate: Wed Dec 3 06:07:10 2025 -0500

    Enable secure processing for the XML parser (#729)
    
    Secure processing for XPath evaluation is already enabled but doesn't
    affect XML parsing.
---
 .../commons/text/lookup/StringLookupFactory.java   |  61 ++++++++---
 .../commons/text/lookup/XmlStringLookup.java       | 115 ++++++++++++++++-----
 .../text/lookup/StringLookupFactoryTest.java       |  13 +++
 .../commons/text/lookup/XmlStringLookupTest.java   |  65 ++++++++++--
 .../apache/commons/text/document-entity-ref.xml    |  26 +++++
 .../org/apache/commons/text/xml-entity.txt         |  17 +++
 6 files changed, 242 insertions(+), 55 deletions(-)

diff --git 
a/src/main/java/org/apache/commons/text/lookup/StringLookupFactory.java 
b/src/main/java/org/apache/commons/text/lookup/StringLookupFactory.java
index 2f163b8e..f172f588 100644
--- a/src/main/java/org/apache/commons/text/lookup/StringLookupFactory.java
+++ b/src/main/java/org/apache/commons/text/lookup/StringLookupFactory.java
@@ -30,6 +30,7 @@ import java.util.function.BiFunction;
 import java.util.function.Function;
 import java.util.function.Supplier;
 
+import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.xpath.XPathFactory;
 
 import org.apache.commons.text.StringSubstitutor;
@@ -1617,10 +1618,19 @@ public final class StringLookupFactory {
      * if a lookup causes causes a path to resolve outside of these fences. 
Otherwise, the result is unfenced to preserved behavior from previous versions.
      * </p>
      * <p>
-     * We look up the value for the key in the format "DocumentPath:XPath".
+     * We looks up values in an XML document in the format {@code 
"[secure=(true|false):]DocumentPath:XPath"}.
      * </p>
      * <p>
-     * For example: "com/domain/document.xml:/path/to/node".
+     * For example:
+     * </p>
+     * <ul>
+     * <li>{@code "com/domain/document.xml:/path/to/node"}</li>
+     * <li>{@code "secure=false:com/domain/document.xml:/path/to/node"}</li>
+     * <li>{@code "secure=true:com/domain/document.xml:/path/to/node"}</li>
+     * </ul>
+     * <p>
+     * Secure processing is enabled by default. The secure boolean String 
parsing follows the syntax defined by {@link Boolean#parseBoolean(String)}. The 
secure
+     * value in the key overrides instance settings given in the constructor.
      * </p>
      * <p>
      * Using a {@link StringLookup} from the {@link StringLookupFactory}:
@@ -1644,7 +1654,7 @@ public final class StringLookupFactory {
      * @since 1.5
      */
     public StringLookup xmlStringLookup() {
-        return fences != null ? 
xmlStringLookup(XmlStringLookup.DEFAULT_FEATURES, fences) : 
XmlStringLookup.INSTANCE;
+        return fences != null ? 
xmlStringLookup(XmlStringLookup.DEFAULT_XPATH_FEATURES, fences) : 
XmlStringLookup.INSTANCE;
     }
 
     /**
@@ -1654,10 +1664,19 @@ public final class StringLookupFactory {
      * if a lookup causes causes a path to resolve outside of these fences. 
Otherwise, the result is unfenced to preserved behavior from previous versions.
      * </p>
      * <p>
-     * We look up the value for the key in the format {@code 
"DocumentPath:XPath"}.
+     * We looks up values in an XML document in the format {@code 
"[secure=(true|false):]DocumentPath:XPath"}.
      * </p>
      * <p>
-     * For example: {@code "com/domain/document.xml:/path/to/node"}.
+     * For example:
+     * </p>
+     * <ul>
+     * <li>{@code "com/domain/document.xml:/path/to/node"}</li>
+     * <li>{@code "secure=false:com/domain/document.xml:/path/to/node"}</li>
+     * <li>{@code "secure=true:com/domain/document.xml:/path/to/node"}</li>
+     * </ul>
+     * <p>
+     * Secure processing is enabled by default. The secure boolean String 
parsing follows the syntax defined by {@link Boolean#parseBoolean(String)}. The 
secure
+     * value in the key overrides instance settings given in the constructor.
      * </p>
      * <p>
      * Using a {@link StringLookup} from the {@link StringLookupFactory}:
@@ -1677,13 +1696,14 @@ public final class StringLookupFactory {
      * The examples above convert {@code 
"com/domain/document.xml:/path/to/node"} to the value of the XPath in the XML 
document.
      * </p>
      *
-     * @param xPathFactoryFeatures XPathFactory features to set.
+     * @param factoryFeatures DocumentBuilderFactory and XPathFactory features 
to set.
      * @return An XML StringLookup instance.
+     * @see DocumentBuilderFactory#setFeature(String, boolean)
      * @see XPathFactory#setFeature(String, boolean)
      * @since 1.11.0
      */
-    public StringLookup xmlStringLookup(final Map<String, Boolean> 
xPathFactoryFeatures) {
-        return xmlStringLookup(xPathFactoryFeatures, fences);
+    public StringLookup xmlStringLookup(final Map<String, Boolean> 
factoryFeatures) {
+        return xmlStringLookup(factoryFeatures, fences);
     }
 
     /**
@@ -1693,10 +1713,19 @@ public final class StringLookupFactory {
      * if a lookup causes causes a path to resolve outside of these fences. 
Otherwise, the result is unfenced to preserved behavior from previous versions.
      * </p>
      * <p>
-     * We look up the value for the key in the format {@code 
"DocumentPath:XPath"}.
+     * We looks up values in an XML document in the format {@code 
"[secure=(true|false):]DocumentPath:XPath"}.
      * </p>
      * <p>
-     * For example: {@code "com/domain/document.xml:/path/to/node"}.
+     * For example:
+     * </p>
+     * <ul>
+     * <li>{@code "com/domain/document.xml:/path/to/node"}</li>
+     * <li>{@code "secure=false:com/domain/document.xml:/path/to/node"}</li>
+     * <li>{@code "secure=true:com/domain/document.xml:/path/to/node"}</li>
+     * </ul>
+     * <p>
+     * Secure processing is enabled by default. The secure boolean String 
parsing follows the syntax defined by {@link Boolean#parseBoolean(String)}. The 
secure
+     * value in the key overrides instance settings given in the constructor.
      * </p>
      * <p>
      * Using a {@link StringLookup} from the {@link StringLookupFactory} 
fenced by the current directory ({@code Paths.get("")}):
@@ -1711,10 +1740,8 @@ public final class StringLookupFactory {
      *
      * <pre>
      * 
StringLookupFactory.INSTANCE.xmlStringLookup(Paths.get("")).lookup("com/domain/document.xml:/path/to/node");
-     *
      * // throws IllegalArgumentException
      * 
StringLookupFactory.INSTANCE.xmlStringLookup(Paths.get("")).lookup("/rootdir/foo/document.xml:/path/to/node");
-     *
      * // throws IllegalArgumentException
      * 
StringLookupFactory.INSTANCE.xmlStringLookup(Paths.get("")).lookup("../com/domain/document.xml:/path/to/node");
      * </pre>
@@ -1726,12 +1753,14 @@ public final class StringLookupFactory {
      * resolves in a fence.
      * </p>
      *
-     * @param xPathFactoryFeatures XPathFactory features to set.
-     * @param fences               The fences guarding Path resolution.
+     * @param factoryFeatures DocumentBuilderFactory and XPathFactory features 
to set.
+     * @param fences          The fences guarding Path resolution.
      * @return An XML StringLookup instance.
+     * @see DocumentBuilderFactory#setFeature(String, boolean)
+     * @see XPathFactory#setFeature(String, boolean)
      * @since 1.12.0
      */
-    public StringLookup xmlStringLookup(final Map<String, Boolean> 
xPathFactoryFeatures, final Path... fences) {
-        return new XmlStringLookup(xPathFactoryFeatures, fences);
+    public StringLookup xmlStringLookup(final Map<String, Boolean> 
factoryFeatures, final Path... fences) {
+        return new XmlStringLookup(factoryFeatures, factoryFeatures, fences);
     }
 }
diff --git a/src/main/java/org/apache/commons/text/lookup/XmlStringLookup.java 
b/src/main/java/org/apache/commons/text/lookup/XmlStringLookup.java
index a476cc07..aa873a00 100644
--- a/src/main/java/org/apache/commons/text/lookup/XmlStringLookup.java
+++ b/src/main/java/org/apache/commons/text/lookup/XmlStringLookup.java
@@ -26,38 +26,60 @@ import java.util.Map.Entry;
 import java.util.Objects;
 
 import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.xpath.XPathFactory;
 
 import org.apache.commons.lang3.StringUtils;
-import org.xml.sax.InputSource;
+import org.w3c.dom.Document;
 
 /**
- * Looks up keys from an XML document.
+ * Looks up values in an XML document in the format {@code 
"[secure=(true|false):]DocumentPath:XPath"}.
  * <p>
- * Looks up the value for a given key in the format "Document:XPath".
+ * For example:
  * </p>
+ * <ul>
+ * <li>{@code "com/domain/document.xml:/path/to/node"}</li>
+ * <li>{@code "secure=false:com/domain/document.xml:/path/to/node"}</li>
+ * <li>{@code "secure=true:com/domain/document.xml:/path/to/node"}</li>
+ * </ul>
  * <p>
- * For example: "com/domain/document.xml:/path/to/node".
+ * Secure processing is enabled by default. The secure boolean String parsing 
follows the syntax defined by {@link Boolean#parseBoolean(String)}. The secure
+ * value in the key overrides instance settings given in the constructor.
  * </p>
  *
  * @since 1.5
  */
 final class XmlStringLookup extends AbstractPathFencedLookup {
 
+    /**
+     * Minimum number of key parts.
+     */
+    private static final int KEY_PARTS_MIN = 2;
+
+    /**
+     * Minimum number of key parts.
+     */
+    private static final int KEY_PARTS_MAX = 3;
+
     /**
      * Defines default XPath factory features.
      */
-    static final Map<String, Boolean> DEFAULT_FEATURES;
+    static final Map<String, Boolean> DEFAULT_XPATH_FEATURES;
 
+    /**
+     * Defines default XML factory features.
+     */
+    static final Map<String, Boolean> DEFAULT_XML_FEATURES;
     static {
-        DEFAULT_FEATURES = new HashMap<>(1);
-        DEFAULT_FEATURES.put(XMLConstants.FEATURE_SECURE_PROCESSING, 
Boolean.TRUE);
+        DEFAULT_XPATH_FEATURES = new HashMap<>(1);
+        DEFAULT_XPATH_FEATURES.put(XMLConstants.FEATURE_SECURE_PROCESSING, 
Boolean.TRUE);
+        DEFAULT_XML_FEATURES = new HashMap<>(1);
+        DEFAULT_XML_FEATURES.put(XMLConstants.FEATURE_SECURE_PROCESSING, 
Boolean.TRUE);
     }
-
     /**
-     * Defines the singleton for this class.
+     * Defines the singleton for this class with secure processing enabled.
      */
-    static final XmlStringLookup INSTANCE = new 
XmlStringLookup(DEFAULT_FEATURES, (Path[]) null);
+    static final XmlStringLookup INSTANCE = new 
XmlStringLookup(DEFAULT_XML_FEATURES, DEFAULT_XPATH_FEATURES, (Path[]) null);
 
     /**
      * Defines XPath factory features.
@@ -65,23 +87,40 @@ final class XmlStringLookup extends 
AbstractPathFencedLookup {
     private final Map<String, Boolean> xPathFactoryFeatures;
 
     /**
-     * No need to build instances for now.
+     * Defines XML factory features.
+     */
+    private final Map<String, Boolean> xmlFactoryFeatures;
+
+    /**
+     * Constructs a new instance.
      *
-     * @param xPathFactoryFeatures XPathFactory features to set.
+     * @param xmlFactoryFeatures   The {@link DocumentBuilderFactory} features 
to set.
+     * @param xPathFactoryFeatures The {@link XPathFactory} features to set.
+     * @see DocumentBuilderFactory#setFeature(String, boolean)
      * @see XPathFactory#setFeature(String, boolean)
      */
-    XmlStringLookup(final Map<String, Boolean> xPathFactoryFeatures, final 
Path... fences) {
+    XmlStringLookup(final Map<String, Boolean> xmlFactoryFeatures, final 
Map<String, Boolean> xPathFactoryFeatures, final Path... fences) {
         super(fences);
+        this.xmlFactoryFeatures = Objects.requireNonNull(xmlFactoryFeatures, 
"xmlFactoryFeatures");
         this.xPathFactoryFeatures = 
Objects.requireNonNull(xPathFactoryFeatures, "xPathFfactoryFeatures");
     }
 
     /**
-     * Looks up the value for the key in the format "DocumentPath:XPath".
+     * Looks up a value for the key in the format {@code 
"[secure=(true|false):]DocumentPath:XPath"}.
+     * <p>
+     * For example:
+     * </p>
+     * <ul>
+     * <li>{@code "com/domain/document.xml:/path/to/node"}</li>
+     * <li>{@code "secure=false:com/domain/document.xml:/path/to/node"}</li>
+     * <li>{@code "secure=true:com/domain/document.xml:/path/to/node"}</li>
+     * </ul>
      * <p>
-     * For example: "com/domain/document.xml:/path/to/node".
+     * Secure processing is enabled by default. The secure boolean String 
parsing follows the syntax defined by {@link Boolean#parseBoolean(String)}. The 
secure
+     * value in the key overrides instance settings given in the constructor.
      * </p>
      *
-     * @param key the key to be looked up, may be null
+     * @param key the key to be looked up, may be null.
      * @return The value associated with the key.
      */
     @Override
@@ -91,22 +130,42 @@ final class XmlStringLookup extends 
AbstractPathFencedLookup {
         }
         final String[] keys = key.split(SPLIT_STR);
         final int keyLen = keys.length;
-        if (keyLen != 2) {
-            throw IllegalArgumentExceptions.format("Bad XML key format [%s]; 
expected format is DocumentPath:XPath.",
-                    key);
+        if (keyLen != KEY_PARTS_MIN && keyLen != KEY_PARTS_MAX) {
+            throw IllegalArgumentExceptions.format("Bad XML key format '%s'; 
the expected format is [secure=(true|false):]DocumentPath:XPath.", key);
         }
-        final String documentPath = keys[0];
-        final String xpath = StringUtils.substringAfter(key, SPLIT_CH);
-        try (InputStream inputStream = 
Files.newInputStream(getPath(documentPath))) {
-            final XPathFactory factory = XPathFactory.newInstance();
-            for (final Entry<String, Boolean> p : 
xPathFactoryFeatures.entrySet()) {
-                factory.setFeature(p.getKey(), p.getValue());
+        final boolean isKeySecure = keyLen == KEY_PARTS_MAX;
+        final Boolean secure = isKeySecure ? parseSecureKey(keys, key) : null;
+        final String documentPath = isKeySecure ? keys[1] : keys[0];
+        final String xpath = StringUtils.substringAfterLast(key, SPLIT_CH);
+        final DocumentBuilderFactory dbFactory = 
DocumentBuilderFactory.newInstance();
+        try {
+            for (final Entry<String, Boolean> p : 
xmlFactoryFeatures.entrySet()) {
+                dbFactory.setFeature(p.getKey(), p.getValue());
+            }
+            if (secure != null) {
+                dbFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, 
secure.booleanValue());
+            }
+            try (InputStream inputStream = 
Files.newInputStream(getPath(documentPath))) {
+                final Document doc = 
dbFactory.newDocumentBuilder().parse(inputStream);
+                final XPathFactory factory = XPathFactory.newInstance();
+                for (final Entry<String, Boolean> p : 
xPathFactoryFeatures.entrySet()) {
+                    factory.setFeature(p.getKey(), p.getValue());
+                }
+                if (secure != null) {
+                    factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, 
secure.booleanValue());
+                }
+                return factory.newXPath().evaluate(xpath, doc);
             }
-            return factory.newXPath().evaluate(xpath, new 
InputSource(inputStream));
         } catch (final Exception e) {
-            throw IllegalArgumentExceptions.format(e, "Error looking up XML 
document [%s] and XPath [%s].",
-                    documentPath, xpath);
+            throw new IllegalArgumentException(e);
         }
     }
 
+    private Boolean parseSecureKey(final String[] args, final String key) {
+        final String[] secParts = args[0].split("=");
+        if (secParts.length != 2 && !Objects.equals(secParts[0], "secure")) {
+            throw IllegalArgumentExceptions.format("Bad XML key format '%s'; 
the expected format is [secure=(true|false):]DocumentPath:XPath.", key);
+        }
+        return Boolean.valueOf(secParts[1]);
+    }
 }
diff --git 
a/src/test/java/org/apache/commons/text/lookup/StringLookupFactoryTest.java 
b/src/test/java/org/apache/commons/text/lookup/StringLookupFactoryTest.java
index 33897a74..5ffe49f3 100644
--- a/src/test/java/org/apache/commons/text/lookup/StringLookupFactoryTest.java
+++ b/src/test/java/org/apache/commons/text/lookup/StringLookupFactoryTest.java
@@ -282,4 +282,17 @@ class StringLookupFactoryTest {
         
XmlStringLookupTest.assertLookup(stringLookupFactory.xmlStringLookup(features));
         
XmlStringLookupTest.assertLookup(stringLookupFactory.xmlStringLookup(new 
HashMap<>()));
     }
+
+    @Test
+    void testXmlStringLookupExternalEntityOff() {
+        assertThrows(IllegalArgumentException.class,
+                () -> 
StringLookupFactory.INSTANCE.xmlStringLookup().apply(XmlStringLookupTest.DOC_DIR
 + "document-entity-ref.xml:/document/content"));
+    }
+
+    @Test
+    void testXmlStringLookupExternalEntityOn() {
+        final String key = XmlStringLookupTest.DOC_DIR + 
"document-entity-ref.xml:/document/content";
+        assertEquals(XmlStringLookupTest.DATA, 
StringLookupFactory.INSTANCE.xmlStringLookup(XmlStringLookupTest.EMPTY_MAP).apply(key).trim());
+    }
+
 }
diff --git 
a/src/test/java/org/apache/commons/text/lookup/XmlStringLookupTest.java 
b/src/test/java/org/apache/commons/text/lookup/XmlStringLookupTest.java
index 40290f0b..d24c4090 100644
--- a/src/test/java/org/apache/commons/text/lookup/XmlStringLookupTest.java
+++ b/src/test/java/org/apache/commons/text/lookup/XmlStringLookupTest.java
@@ -26,11 +26,14 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
 
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.Map;
 
 import javax.xml.XMLConstants;
 
 import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.text.StringSubstitutor;
 import org.junit.jupiter.api.Test;
 
 /**
@@ -38,15 +41,18 @@ import org.junit.jupiter.api.Test;
  */
 class XmlStringLookupTest {
 
+    static final String DATA = "Hello World!";
+    static final Map<String, Boolean> EMPTY_MAP = Collections.emptyMap();
     private static final Path CURRENT_PATH = Paths.get(StringUtils.EMPTY); // 
NOT "."
     private static final Path ABSENT_PATH = Paths.get("does not exist at all");
-    private static final String DOC_RELATIVE = 
"src/test/resources/org/apache/commons/text/document.xml";
+    static final String DOC_DIR = 
"src/test/resources/org/apache/commons/text/";
+    private static final String DOC_RELATIVE = DOC_DIR + "document.xml";
     private static final String DOC_ROOT = "/document.xml";
 
     static void assertLookup(final StringLookup xmlStringLookup) {
         assertNotNull(xmlStringLookup);
         assertInstanceOf(XmlStringLookup.class, xmlStringLookup);
-        assertEquals("Hello World!", xmlStringLookup.apply(DOC_RELATIVE + 
":/root/path/to/node"));
+        assertEquals(DATA, xmlStringLookup.apply(DOC_RELATIVE + 
":/root/path/to/node"));
         assertNull(xmlStringLookup.apply(null));
     }
 
@@ -55,6 +61,44 @@ class XmlStringLookupTest {
         assertThrows(IllegalArgumentException.class, () -> 
XmlStringLookup.INSTANCE.apply("docName"));
     }
 
+    @Test
+    void testExternalEntityOff() {
+        assertThrows(IllegalArgumentException.class,
+                () -> new 
XmlStringLookup(XmlStringLookup.DEFAULT_XML_FEATURES, EMPTY_MAP).apply(DOC_DIR 
+ "document-entity-ref.xml:/document/content"));
+    }
+
+    @Test
+    void testExternalEntityOn() {
+        final String key = DOC_DIR + 
"document-entity-ref.xml:/document/content";
+        assertEquals(DATA, new XmlStringLookup(EMPTY_MAP, 
EMPTY_MAP).apply(key).trim());
+        assertEquals(DATA, new XmlStringLookup(EMPTY_MAP, 
XmlStringLookup.DEFAULT_XPATH_FEATURES).apply(key).trim());
+    }
+
+    @Test
+    void testInterpolatorExternalEntityOff() {
+        final StringSubstitutor stringSubstitutor = 
StringSubstitutor.createInterpolator();
+        assertThrows(IllegalArgumentException.class, () -> 
stringSubstitutor.replace("${xml:" + DOC_DIR + 
"document-entity-ref.xml:/document/content}"));
+    }
+
+    @Test
+    void testInterpolatorExternalEntityOffOverride() {
+        final StringSubstitutor stringSubstitutor = 
StringSubstitutor.createInterpolator();
+        assertEquals(DATA, stringSubstitutor.replace("${xml:secure=false:" + 
DOC_DIR + "document-entity-ref.xml:/document/content}").trim());
+    }
+
+    @Test
+    void testInterpolatorExternalEntityOn() {
+        final StringSubstitutor stringSubstitutor = 
StringSubstitutor.createInterpolator();
+        assertThrows(IllegalArgumentException.class, () -> 
stringSubstitutor.replace("${xml:" + DOC_DIR + 
"document-entity-ref.xml:/document/content}"));
+    }
+
+    @Test
+    void testInterpolatorExternalEntityOnOverride() {
+        final StringSubstitutor stringSubstitutor = 
StringSubstitutor.createInterpolator();
+        assertThrows(IllegalArgumentException.class,
+                () -> stringSubstitutor.replace("${xml:secure=true:" + DOC_DIR 
+ "document-entity-ref.xml:/document/content}"));
+    }
+
     @Test
     void testMissingXPath() {
         assertThrows(IllegalArgumentException.class, () -> 
XmlStringLookup.INSTANCE.apply(DOC_RELATIVE + ":!JUNK!"));
@@ -63,20 +107,20 @@ class XmlStringLookupTest {
     @Test
     void testNoFeatures() {
         final String xpath = "/root/path/to/node";
-        assertEquals("Hello World!", new XmlStringLookup(new 
HashMap<>()).apply(DOC_RELATIVE + ":" + xpath));
-        assertEquals("Hello World!", new XmlStringLookup(new HashMap<>(), 
CURRENT_PATH).apply(DOC_RELATIVE + ":" + xpath));
-        assertEquals("Hello World!", new XmlStringLookup(new HashMap<>(), 
CURRENT_PATH, ABSENT_PATH).apply(DOC_RELATIVE + ":" + xpath));
-        assertEquals("Hello World!", new XmlStringLookup(new HashMap<>(), 
ABSENT_PATH, CURRENT_PATH).apply(DOC_RELATIVE + ":" + xpath));
-        assertThrows(IllegalArgumentException.class, () -> new 
XmlStringLookup(new HashMap<>(), ABSENT_PATH).apply(DOC_ROOT + ":" + xpath));
-        assertThrows(IllegalArgumentException.class, () -> new 
XmlStringLookup(new HashMap<>(), CURRENT_PATH).apply(DOC_ROOT + ":" + xpath));
-        assertThrows(IllegalArgumentException.class, () -> new 
XmlStringLookup(new HashMap<>(), ABSENT_PATH, CURRENT_PATH).apply(DOC_ROOT + 
":" + xpath));
+        assertEquals(DATA, new XmlStringLookup(EMPTY_MAP, 
EMPTY_MAP).apply(DOC_RELATIVE + ":" + xpath));
+        assertEquals(DATA, new XmlStringLookup(EMPTY_MAP, 
EMPTY_MAP).apply(DOC_RELATIVE + ":" + xpath));
+        assertEquals(DATA, new XmlStringLookup(EMPTY_MAP, EMPTY_MAP, 
CURRENT_PATH, ABSENT_PATH).apply(DOC_RELATIVE + ":" + xpath));
+        assertEquals(DATA, new XmlStringLookup(EMPTY_MAP, EMPTY_MAP, 
ABSENT_PATH, CURRENT_PATH).apply(DOC_RELATIVE + ":" + xpath));
+        assertThrows(IllegalArgumentException.class, () -> new 
XmlStringLookup(EMPTY_MAP, EMPTY_MAP, ABSENT_PATH).apply(DOC_ROOT + ":" + 
xpath));
+        assertThrows(IllegalArgumentException.class, () -> new 
XmlStringLookup(EMPTY_MAP, EMPTY_MAP, CURRENT_PATH).apply(DOC_ROOT + ":" + 
xpath));
+        assertThrows(IllegalArgumentException.class, () -> new 
XmlStringLookup(EMPTY_MAP, EMPTY_MAP, ABSENT_PATH, CURRENT_PATH).apply(DOC_ROOT 
+ ":" + xpath));
     }
 
     @Test
     void testNoFeaturesDefault() {
         final HashMap<String, Boolean> features = new HashMap<>(1);
         features.put(XMLConstants.FEATURE_SECURE_PROCESSING, Boolean.TRUE);
-        assertLookup(new XmlStringLookup(features));
+        assertLookup(new XmlStringLookup(EMPTY_MAP, features));
     }
 
     @Test
@@ -94,5 +138,4 @@ class XmlStringLookupTest {
         // does not blow up and gives some kind of string.
         assertFalse(XmlStringLookup.INSTANCE.toString().isEmpty());
     }
-
 }
diff --git a/src/test/resources/org/apache/commons/text/document-entity-ref.xml 
b/src/test/resources/org/apache/commons/text/document-entity-ref.xml
new file mode 100644
index 00000000..bfd36098
--- /dev/null
+++ b/src/test/resources/org/apache/commons/text/document-entity-ref.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+     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
+
+          https://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.
+  -->
+<!DOCTYPE document [
+  <!ENTITY ext SYSTEM 
"src/test/resources/org/apache/commons/text/xml-entity.txt">
+]>
+<document>
+  <title>Example of an External Entity</title>
+  <content>
+    &ext; 
+  </content>
+</document>
diff --git a/src/test/resources/org/apache/commons/text/xml-entity.txt 
b/src/test/resources/org/apache/commons/text/xml-entity.txt
new file mode 100644
index 00000000..4f2fa758
--- /dev/null
+++ b/src/test/resources/org/apache/commons/text/xml-entity.txt
@@ -0,0 +1,17 @@
+<!--
+     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
+
+          https://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.
+  -->
+Hello World!

Reply via email to