You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@velocity.apache.org by cb...@apache.org on 2026/06/17 16:40:51 UTC

(velocity-tools) branch 3.x updated: VELTOOLS-188 - Proper XmlTools namespaces handling

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

asf-gitbox-commits pushed a commit to branch 3.x
in repository https://gitbox.apache.org/repos/asf/velocity-tools.git


The following commit(s) were added to refs/heads/3.x by this push:
     new eef78f3e VELTOOLS-188 - Proper XmlTools namespaces handling
eef78f3e is described below

commit eef78f3e8984cb4707801398bef455a946d79e09
Author: Claude Brisson <cl...@renegat.net>
AuthorDate: Wed Jun 17 18:40:20 2026 +0200

    VELTOOLS-188 - Proper XmlTools namespaces handling
---
 .../java/org/apache/velocity/tools/XmlUtils.java   | 76 ++++++++++++++++++++--
 .../velocity/tools/generic/XmlToolTests.java       | 23 +++++++
 2 files changed, 93 insertions(+), 6 deletions(-)

diff --git a/velocity-tools-generic/src/main/java/org/apache/velocity/tools/XmlUtils.java b/velocity-tools-generic/src/main/java/org/apache/velocity/tools/XmlUtils.java
index 87c9052d..d44e2c9d 100644
--- a/velocity-tools-generic/src/main/java/org/apache/velocity/tools/XmlUtils.java
+++ b/velocity-tools-generic/src/main/java/org/apache/velocity/tools/XmlUtils.java
@@ -36,11 +36,14 @@ import java.io.StringReader;
 import java.io.StringWriter;
 import java.lang.ref.SoftReference;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Stack;
 import java.util.concurrent.LinkedBlockingDeque;
 
 import javax.xml.XMLConstants;
+import javax.xml.namespace.NamespaceContext;
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
@@ -401,12 +404,13 @@ public final class XmlUtils
      */
     public static NodeList search(String xpath, Node context) throws XPathExpressionException
     {
-        NodeList ret = null;
-            XPath xp = XPathFactory.newInstance().newXPath();
-            XPathExpression exp = xp.compile(xpath);
-            ret = (NodeList)exp.evaluate(context, XPathConstants.NODESET);
-        return ret;
-        }
+        XPath xp = XPathFactory.newInstance().newXPath();
+        // resolve namespace prefixes from the document so prefixed XPath steps
+        // (e.g. "b:author/a:name") work with multiple namespaces (VELTOOLS-188)
+        xp.setNamespaceContext(new NodeNamespaceContext(context));
+        XPathExpression exp = xp.compile(xpath);
+        return (NodeList)exp.evaluate(context, XPathConstants.NODESET);
+    }
 
     /**
      * Search for nodes using an XPath expression
@@ -426,6 +430,66 @@ public final class XmlUtils
         return ret;
     }
 
+    /**
+     * A {@link NamespaceContext} resolving prefixes against a DOM node's in-scope namespace
+     * declarations, so that namespace-prefixed XPath expressions resolve (VELTOOLS-188).
+     */
+    private static class NodeNamespaceContext implements NamespaceContext
+    {
+        private static final String UNBOUND_PREFIX_NAMESPACE = "urn:org.apache.velocity.tools:unbound-prefix";
+
+        private final Node node;
+
+        NodeNamespaceContext(Node node)
+        {
+            this.node = node;
+        }
+
+        @Override
+        public String getNamespaceURI(String prefix)
+        {
+            if (prefix == null)
+            {
+                throw new IllegalArgumentException("prefix cannot be null");
+            }
+            if (XMLConstants.XML_NS_PREFIX.equals(prefix))
+            {
+                return XMLConstants.XML_NS_URI;
+            }
+            if (XMLConstants.XMLNS_ATTRIBUTE.equals(prefix))
+            {
+                return XMLConstants.XMLNS_ATTRIBUTE_NS_URI;
+            }
+            if (prefix.isEmpty())
+            {
+                String def = node.lookupNamespaceURI(null);
+                return def == null ? XMLConstants.NULL_NS_URI : def;
+            }
+            String uri = node.lookupNamespaceURI(prefix);
+            // permissive: an unbound prefix resolves to a synthetic namespace, so the expression
+            // matches nothing rather than the engine throwing "Prefix must resolve to a namespace"
+            return uri != null ? uri : UNBOUND_PREFIX_NAMESPACE;
+        }
+
+        @Override
+        public String getPrefix(String namespaceURI)
+        {
+            if (namespaceURI == null)
+            {
+                throw new IllegalArgumentException("namespaceURI cannot be null");
+            }
+            return node.lookupPrefix(namespaceURI);
+        }
+
+        @Override
+        public Iterator<String> getPrefixes(String namespaceURI)
+        {
+            String prefix = getPrefix(namespaceURI);
+            return (prefix == null ? Collections.<String>emptyList()
+                                   : Collections.singletonList(prefix)).iterator();
+        }
+    }
+
 
     /**
      * Search for elements using an XPath expression
diff --git a/velocity-tools-generic/src/test/java/org/apache/velocity/tools/generic/XmlToolTests.java b/velocity-tools-generic/src/test/java/org/apache/velocity/tools/generic/XmlToolTests.java
index b56fffd1..ef3ed67c 100644
--- a/velocity-tools-generic/src/test/java/org/apache/velocity/tools/generic/XmlToolTests.java
+++ b/velocity-tools-generic/src/test/java/org/apache/velocity/tools/generic/XmlToolTests.java
@@ -333,6 +333,29 @@ public class XmlToolTests {
         assertEquals("  x  ", xml.find("./text()").toString());
     }
 
+    // VELTOOLS-188: prefixed XPath resolves with multiple namespaces
+    public @Test void multiNamespaceXPath() throws Exception
+    {
+        XmlTool xml = new XmlTool();
+        xml.configure(new ValueParser());
+        xml.parse("<b:book xmlns:b=\"http://book\" xmlns:a=\"http://author\">"
+                + "<b:author><a:name>Jane</a:name></b:author></b:book>");
+        XmlTool name = xml.find("./b:author/a:name");
+        assertNotNull(name);
+        assertEquals("Jane", name.getText());
+    }
+
+    // VELTOOLS-188: undeclared prefixes must stay permissive — no exception, empty result
+    public @Test void undeclaredPrefixIsPermissive() throws Exception
+    {
+        org.w3c.dom.Element root = org.apache.velocity.tools.XmlUtils.parse(
+            "<b:book xmlns:b=\"http://book\"><b:t>x</b:t></b:book>");
+        // 'z' is not declared in the document: search must not throw, just match nothing
+        org.w3c.dom.NodeList nl = org.apache.velocity.tools.XmlUtils.search("//z:t", root);
+        assertNotNull(nl);
+        assertEquals(0, nl.getLength());
+    }
+
 
 }