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());
+ }
+
}