diff --git a/exist-core/src/main/java/org/exist/xquery/ElementConstructor.java b/exist-core/src/main/java/org/exist/xquery/ElementConstructor.java index 20b94537797..eea4cdb5032 100644 --- a/exist-core/src/main/java/org/exist/xquery/ElementConstructor.java +++ b/exist-core/src/main/java/org/exist/xquery/ElementConstructor.java @@ -275,24 +275,32 @@ public Sequence eval(final Sequence contextSequence, final Item contextItem) thr if (qnitem instanceof QNameValue) { qn = ((QNameValue) qnitem).getQName(); } else { - //Do we have the same result than Atomize there ? -pb - try { - qn = QName.parse(context, qnitem.getStringValue()); - } catch (final QName.IllegalQNameException e) { - throw new XPathException(this, ErrorCodes.XPTY0004, "'" + qnitem.getStringValue() + "' is not a valid element name"); - } catch (final XPathException e) { - e.setLocation(getLine(), getColumn(), getSource()); - throw e; + // Element constructors must resolve namespace prefixes using the full + // inherited namespace context, regardless of declare copy-namespaces no-inherit. + // The no-inherit option governs how namespaces propagate from copied source + // nodes, not how constructor names are resolved (XQuery 3.1 §3.9.3.4). + final boolean savedInherit = context.inheritNamespaces(); + if (!savedInherit) { + context.setInheritNamespaces(true); } + try { + //Do we have the same result than Atomize there ? -pb + try { + qn = QName.parse(context, qnitem.getStringValue()); + } catch (final QName.IllegalQNameException e) { + throw new XPathException(this, ErrorCodes.XPTY0004, "'" + qnitem.getStringValue() + "' is not a valid element name"); + } catch (final XPathException e) { + e.setLocation(getLine(), getColumn(), getSource()); + throw e; + } - //Use the default namespace if specified - /* - if (qn.getPrefix() == null && context.inScopeNamespaces.get("xmlns") != null) { - qn.setNamespaceURI((String)context.inScopeNamespaces.get("xmlns")); - } - */ - if (qn.getPrefix() == null && context.getInScopeNamespace(XMLConstants.DEFAULT_NS_PREFIX) != null) { - qn = new QName(qn.getLocalPart(), context.getInScopeNamespace(XMLConstants.DEFAULT_NS_PREFIX), qn.getPrefix()); + if (qn.getPrefix() == null && context.getInScopeNamespace(XMLConstants.DEFAULT_NS_PREFIX) != null) { + qn = new QName(qn.getLocalPart(), context.getInScopeNamespace(XMLConstants.DEFAULT_NS_PREFIX), qn.getPrefix()); + } + } finally { + if (!savedInherit) { + context.setInheritNamespaces(false); + } } } diff --git a/exist-core/src/main/java/org/exist/xquery/EnclosedExpr.java b/exist-core/src/main/java/org/exist/xquery/EnclosedExpr.java index b11a10e06f4..d818271e6ff 100644 --- a/exist-core/src/main/java/org/exist/xquery/EnclosedExpr.java +++ b/exist-core/src/main/java/org/exist/xquery/EnclosedExpr.java @@ -21,20 +21,30 @@ */ package org.exist.xquery; +import org.exist.dom.QName; import org.exist.dom.memtree.DocumentBuilderReceiver; +import org.exist.dom.memtree.DocumentImpl; import org.exist.dom.memtree.MemTreeBuilder; +import org.exist.dom.memtree.NodeImpl; import org.exist.dom.memtree.TextImpl; +import org.exist.util.serializer.AttrList; import org.exist.xquery.functions.array.ArrayType; import org.exist.xquery.util.ExpressionDumper; import org.exist.xquery.value.*; import org.w3c.dom.DOMException; import org.xml.sax.SAXException; +import javax.xml.XMLConstants; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + /** * Represents an enclosed expression {expr} inside element * content. Enclosed expressions within attribute values are processed by * {@link org.exist.xquery.AttributeConstructor}. - * + * * @author Wolfgang Meier */ public class EnclosedExpr extends PathExpr { @@ -42,7 +52,7 @@ public class EnclosedExpr extends PathExpr { public EnclosedExpr(XQueryContext context) { super(context); } - + public void analyze(AnalyzeContextInfo contextInfo) throws XPathException { final AnalyzeContextInfo newContextInfo = new AnalyzeContextInfo(contextInfo); newContextInfo.removeFlag(IN_NODE_CONSTRUCTOR); @@ -74,14 +84,41 @@ public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathExc Sequence result; context.enterEnclosedExpr(); try { + // Check copy-namespaces mode before evaluation so we can lazily capture + // innerBuilder only when no-inherit is active. In the default (inherit) case + // this avoids the peekDocumentBuilder() call entirely. + final boolean noInherit = !context.inheritNamespaces(); + context.pushDocumentContext(); + MemTreeBuilder innerBuilder = null; try { result = super.eval(contextSequence, null); + // Only capture the inner builder when no-inherit is active — it is used + // solely to distinguish pre-existing nodes (variable references / copies) + // from nodes constructed inside this enclosed expression. + if (noInherit) { + innerBuilder = context.getCurrentDocumentBuilder(); + } } finally { context.popDocumentContext(); } + // Compute ancestor namespace context for no-inherit copy handling. + // This is the union of inherited namespaces (from outer constructors) and + // in-scope namespaces (from the immediately enclosing constructor), i.e. all + // namespace bindings that an ancestor traversal from this element would find. + Map ancestorNS = null; + if (noInherit) { + final Map inherited = context.getAllInheritedNamespaces(); + final Map inScope = context.getInScopeNamespaces(); + if ((inherited != null && !inherited.isEmpty()) || (inScope != null && !inScope.isEmpty())) { + ancestorNS = new HashMap<>(); + if (inherited != null) { ancestorNS.putAll(inherited); } + if (inScope != null) { ancestorNS.putAll(inScope); } + } + } + // create the output final MemTreeBuilder builder = context.getDocumentBuilder(); final DocumentBuilderReceiver receiver = new DocumentBuilderReceiver(this, builder); @@ -130,7 +167,25 @@ public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathExc } try { receiver.setCheckNS(false); - next.copyTo(context.getBroker(), receiver); + // When copy-namespaces no-inherit is active, pre-existing element nodes + // (i.e. nodes from variable references, not direct constructors) must have + // namespace undeclarations injected so that ancestor-traversal in + // in-scope-prefixes() is neutralized for inherited namespace bindings. + if (noInherit && ancestorNS != null + && next.getType() == Type.ELEMENT + && next instanceof NodeImpl) { + final NodeImpl nodeImpl = (NodeImpl) next; + final boolean isPreExisting = (innerBuilder == null) + || (nodeImpl.getOwnerDocument() != innerBuilder.getDocument()); + if (isPreExisting) { + next.copyTo(context.getBroker(), + new NoInheritCopyReceiver(this, builder, ancestorNS)); + } else { + next.copyTo(context.getBroker(), receiver); + } + } else { + next.copyTo(context.getBroker(), receiver); + } receiver.setCheckNS(true); } catch (DOMException e) { if (e.code == DOMException.NAMESPACE_ERR) { @@ -194,4 +249,104 @@ public Expression simplify() { public boolean evalNextExpressionOnEmptyContextSequence() { return true; } + + /** + * A {@link DocumentBuilderReceiver} that injects namespace undeclarations onto the + * root element of a copied pre-existing node when {@code declare copy-namespaces no-inherit} + * is active. The undeclarations neutralize ancestor namespace bindings so that + * {@code fn:in-scope-prefixes()} traversing the ancestor chain returns the correct result. + * + *

Only prefixes present in {@code ancestorNS} but absent from the root element's own + * namespace declarations are undeclared (i.e. recorded as {@code xmlns:prefix=""}).

+ */ + private static final class NoInheritCopyReceiver extends DocumentBuilderReceiver { + + private final Map ancestorNS; + /** True once the root element's startElement event has been seen. */ + private boolean rootSeen = false; + /** True once undeclarations have been flushed (happens before first non-namespace event). */ + private boolean undeclsFlushed = false; + /** Prefixes that the root element itself declares (element prefix + xmlns:* nodes). */ + private final Set rootOwnPrefixes = new HashSet<>(); + + NoInheritCopyReceiver(final Expression expr, final MemTreeBuilder builder, + final Map ancestorNS) { + super(expr, builder); + this.ancestorNS = ancestorNS; + } + + @Override + public void startElement(final QName qname, final AttrList attribs) { + if (!rootSeen) { + rootSeen = true; + // Track the element's own namespace prefix so we don't undeclare it. + final String prefix = qname.getPrefix(); + if (prefix != null && !prefix.isEmpty()) { + rootOwnPrefixes.add(prefix); + } + // Note: namespace declaration nodes for this element come via addNamespaceNode + // after startElement; they are collected in rootOwnPrefixes there. + } else { + maybeFlushUndeclarations(); + } + super.startElement(qname, attribs); + } + + @Override + public void addNamespaceNode(final QName qname) throws SAXException { + if (rootSeen && !undeclsFlushed) { + // Collect the prefix that this namespace node declares on the root element. + rootOwnPrefixes.add(qname.getLocalPart()); + } + super.addNamespaceNode(qname); + } + + @Override + public void characters(final CharSequence seq) throws SAXException { + maybeFlushUndeclarations(); + super.characters(seq); + } + + @Override + public void characters(final char[] ch, final int start, final int len) throws SAXException { + maybeFlushUndeclarations(); + super.characters(ch, start, len); + } + + @Override + public void endElement(final String ns, final String local, final String qname) throws SAXException { + maybeFlushUndeclarations(); + super.endElement(ns, local, qname); + } + + @Override + public void endElement(final QName qname) throws SAXException { + maybeFlushUndeclarations(); + super.endElement(qname); + } + + /** + * Emit namespace undeclarations for every ancestor namespace binding whose prefix is + * not already declared by the root element itself. Called before the first non-namespace + * event on the root element (child, text, endElement) to ensure the nodes attach to the + * root element node number in the MemTree. + */ + private void maybeFlushUndeclarations() { + if (rootSeen && !undeclsFlushed) { + undeclsFlushed = true; + for (final Map.Entry entry : ancestorNS.entrySet()) { + final String prefix = entry.getKey(); + if (!rootOwnPrefixes.contains(prefix)) { + try { + super.addNamespaceNode( + new QName(prefix, XMLConstants.NULL_NS_URI, XMLConstants.XMLNS_ATTRIBUTE)); + } catch (final SAXException e) { + // Silently skip — undeclaration is best-effort; worst case + // in-scope-prefixes() returns a superset which is handled by cleanup. + } + } + } + } + } + } } diff --git a/exist-core/src/main/java/org/exist/xquery/ModuleContext.java b/exist-core/src/main/java/org/exist/xquery/ModuleContext.java index fd63f4f0b6c..da028882b1e 100644 --- a/exist-core/src/main/java/org/exist/xquery/ModuleContext.java +++ b/exist-core/src/main/java/org/exist/xquery/ModuleContext.java @@ -357,6 +357,11 @@ public MemTreeBuilder getDocumentBuilder(final boolean explicitCreation) { return parentContext.getDocumentBuilder(explicitCreation); } + @Override + public MemTreeBuilder getCurrentDocumentBuilder() { + return parentContext.getCurrentDocumentBuilder(); + } + @Override public void pushDocumentContext() { parentContext.pushDocumentContext(); @@ -523,6 +528,11 @@ public String getInheritedNamespace(final String prefix) { return parentContext.getInheritedNamespace(prefix); } + @Override + public Map getAllInheritedNamespaces() { + return parentContext.getAllInheritedNamespaces(); + } + @Override public String getInheritedPrefix(final String uri) { return parentContext.getInheritedPrefix(uri); diff --git a/exist-core/src/main/java/org/exist/xquery/XQueryContext.java b/exist-core/src/main/java/org/exist/xquery/XQueryContext.java index b3721c34179..1b15f7702a9 100644 --- a/exist-core/src/main/java/org/exist/xquery/XQueryContext.java +++ b/exist-core/src/main/java/org/exist/xquery/XQueryContext.java @@ -969,6 +969,18 @@ public String getInheritedNamespace(final String prefix) { return inheritedInScopeNamespaces == null ? null : inheritedInScopeNamespaces.get(prefix); } + public Map getAllInheritedNamespaces() { + return inheritedInScopeNamespaces; + } + + public Map getInScopeNamespaces() { + return inScopeNamespaces; + } + + public MemTreeBuilder getCurrentDocumentBuilder() { + return documentBuilder; + } + @Override public String getInheritedPrefix(final String uri) { return inheritedInScopePrefixes == null ? null : inheritedInScopePrefixes.get(uri); diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunInScopePrefixes.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunInScopePrefixes.java index a0a248dface..b8d46a82ff8 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunInScopePrefixes.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunInScopePrefixes.java @@ -154,25 +154,20 @@ public static Map collectPrefixes(XQueryContext context, NodeVal Node node = nodeValue.getNode(); if (context.preserveNamespaces()) { //Horrible hacks to work-around bad in-scope NS : we reconstruct a NS context ! - if (context.inheritNamespaces()) { - //Grab ancestors' NS - final Deque stack = new ArrayDeque<>(); - do { - if (node.getNodeType() == Node.ELEMENT_NODE) { - stack.add((Element) node); - } - node = node.getParentNode(); - } while (node != null && node.getNodeType() == Node.ELEMENT_NODE); - - while (!stack.isEmpty()) { - collectNamespacePrefixes(stack.pop(), prefixes); - } - - } else { - //Grab self's NS + // Always traverse ancestors for in-memory nodes. + // When copy-namespaces no-inherit is active, pre-existing nodes (variable copies) + // carry explicit namespace undeclarations (xmlns:prefix="") so that ancestor + // namespace entries are neutralized by the cleanup pass below. + final Deque stack = new ArrayDeque<>(); + do { if (node.getNodeType() == Node.ELEMENT_NODE) { - collectNamespacePrefixes((Element) node, prefixes); + stack.add((Element) node); } + node = node.getParentNode(); + } while (node != null && node.getNodeType() == Node.ELEMENT_NODE); + + while (!stack.isEmpty()) { + collectNamespacePrefixes(stack.pop(), prefixes); } } else { if (context.inheritNamespaces()) { @@ -192,17 +187,16 @@ public static Map collectPrefixes(XQueryContext context, NodeVal } } - //clean up - String key = null; - String value = null; - for (final Entry entry : prefixes.entrySet()) { - key = entry.getKey(); - value = entry.getValue(); - - if ((key == null || key.isEmpty()) && (value == null || value.isEmpty())) { - prefixes.remove(key); + // clean up: remove namespace undeclarations (entries with empty URI). + // With copy-namespaces no-inherit, pre-existing nodes carry explicit undeclarations + // (xmlns:prefix="") that neutralize ancestor namespace bindings; those must not appear + // in the in-scope-prefixes() result. + final Iterator> cleanupIt = prefixes.entrySet().iterator(); + while (cleanupIt.hasNext()) { + final Entry entry = cleanupIt.next(); + if (entry.getValue() == null || entry.getValue().isEmpty()) { + cleanupIt.remove(); } - } return prefixes;