diff --git a/basex-core/src/main/java/org/basex/query/QueryParser.java b/basex-core/src/main/java/org/basex/query/QueryParser.java index 90e3c3d195..279a8c6d8c 100644 --- a/basex-core/src/main/java/org/basex/query/QueryParser.java +++ b/basex-core/src/main/java/org/basex/query/QueryParser.java @@ -95,7 +95,7 @@ public class QueryParser extends InputParser { /** Output declarations. */ private final HashMap sparams = new HashMap<>(); /** QName cache. */ - private final QNmCache qnames = new QNmCache(); + private final QNmResolver qnames = new QNmResolver(); /** Local variable. */ private final LocalVars localVars = new LocalVars(this); @@ -261,7 +261,7 @@ private void finish(final MainModule mm) throws QueryException { } // completes the parsing step - qnames.assignURI(this, 0); + qnames.resolve(this, 0, sc.elemNS); if(sc.elemNS != null) sc.ns.add(EMPTY, sc.elemNS, null); RecordType.resolveRefs(recordTypeRefs, namedRecordTypes); } @@ -329,8 +329,10 @@ private void prolog1() throws QueryException { while(true) { final int p = pos; if(wsConsumeWs(DECLARE)) { - if(wsConsumeWs(DEFAULT)) { - if(!defaultNamespaceDecl() && !defaultCollationDecl() && !emptyOrderDecl() && + if(wsConsumeWs(FIXED)) { + if(!wsConsumeWs(DEFAULT) || !defaultNamespaceDecl(true)) throw error(DECLINCOMPLETE); + } else if(wsConsumeWs(DEFAULT)) { + if(!defaultNamespaceDecl(false) && !defaultCollationDecl() && !emptyOrderDecl() && !decimalFormatDecl(true)) throw error(DECLINCOMPLETE); } else if(wsConsumeWs(BOUNDARY_SPACE)) { boundarySpaceDecl(); @@ -529,10 +531,11 @@ private void boundarySpaceDecl() throws QueryException { /** * Parses the "DefaultNamespaceDecl" rule. + * @param fixed fixed default namespace flag * @return true if declaration was found * @throws QueryException query exception */ - private boolean defaultNamespaceDecl() throws QueryException { + private boolean defaultNamespaceDecl(final boolean fixed) throws QueryException { final boolean elem = wsConsumeWs(ELEMENT); if(!elem && !wsConsumeWs(FUNCTION)) return false; wsCheck(NAMESPACE); @@ -542,7 +545,9 @@ private boolean defaultNamespaceDecl() throws QueryException { if(elem) { if(!decl.add(ELEMENT)) throw error(DUPLNS); + sc.elemNsFixed = fixed; sc.elemNS = uri.length == 0 ? null : uri; + sc.dirNS = sc.elemNS; } else { if(!decl.add(FUNCTION)) throw error(DUPLNS); sc.funcNS = uri; @@ -682,9 +687,13 @@ private void schemaImport() throws QueryException { prefix = ncName(NONAME_X, false); if(eq(prefix, XML, XMLNS)) throw error(BINDXML_X, prefix); wsCheck("="); - } else if(wsConsumeWs(DEFAULT)) { - wsCheck(ELEMENT); - wsCheck(NAMESPACE); + } else { + final boolean fixed = wsConsumeWs(FIXED); + if(fixed || wsConsumeWs(DEFAULT)) { + if(fixed) wsCheck(DEFAULT); + wsCheck(ELEMENT); + wsCheck(NAMESPACE); + } } final byte[] uri = uriLiteral(); if(prefix != null && uri.length == 0) throw error(NSEMPTY); @@ -2080,7 +2089,7 @@ private void validate() throws QueryException { if(consume(TYPE)) { final InputInfo ii = info(); - qnames.add(eQName(SKIPCHECK, QNAME_X), ii); + qnames.add(eQName(SKIPCHECK, QNAME_X), sc, ii); } consume(STRICT); consume(LAX); @@ -2462,7 +2471,7 @@ private ExprInfo simpleNodeTest(final Kind kind, final boolean all) throws Query } } // name test: prefix:name, name, Q{uri}name - qnames.add(name, kind == Kind.ELEMENT, ii); + if(kind == Kind.ELEMENT) qnames.add(name, sc, ii); else qnames.add(name, false, ii); return nameTest.apply(name, scope); } } @@ -3077,10 +3086,11 @@ private Expr dirElement(final boolean root) throws QueryException { // cache namespace information final int size = sc.ns.size(); final byte[] nse = sc.elemNS; + final byte[] nsd = sc.dirNS; final int npos = qnames.size(); final QNm name = new QNm(qnm); - qnames.add(name, ii); + qnames.add(name, true, ii); final Atts ns = new Atts(); final ExprList cont = new ExprList(); @@ -3170,7 +3180,8 @@ private Expr dirElement(final boolean root) throws QueryException { sc.ns.add(prefix, uri); } else { if(eq(uri, XML_URI)) throw error(XMLNSDEF_X, uri); - sc.elemNS = uri; + sc.dirNS = uri; + if(!sc.elemNsFixed) sc.elemNS = sc.dirNS; } if(ns.contains(prefix)) throw error(DUPLNSDEF_X, prefix); ns.add(prefix, uri); @@ -3201,7 +3212,7 @@ private Expr dirElement(final boolean root) throws QueryException { if(!eq(name.string(), close)) throw error(TAGWRONG_X_X, name.string(), close); } - qnames.assignURI(this, npos); + qnames.resolve(this, npos, sc.dirNS); // check for duplicate attribute names if(atts != null) { @@ -3215,6 +3226,7 @@ private Expr dirElement(final boolean root) throws QueryException { sc.ns.size(size); sc.elemNS = nse; + sc.dirNS = nsd; return new CElem(info(), false, name, ns, cont.finish()); } @@ -3356,7 +3368,7 @@ private Expr compConstructor() throws QueryException { private Expr compElement() throws QueryException { final Expr name = compName(NOELEMNAME, true); if(name == null) return null; - if(name instanceof final QNm qnm) qnames.add(qnm, info()); + if(name instanceof final QNm qnm) qnames.add(qnm, sc, info()); skipWs(); return current('{') ? new CElem(info(), true, name, new Atts(), enclosedExpr()) : null; } diff --git a/basex-core/src/main/java/org/basex/query/QueryText.java b/basex-core/src/main/java/org/basex/query/QueryText.java index ae04b07ba4..53866ae098 100644 --- a/basex-core/src/main/java/org/basex/query/QueryText.java +++ b/basex-core/src/main/java/org/basex/query/QueryText.java @@ -69,6 +69,7 @@ public interface QueryText { /** Parser token. */ String FALSE = "false"; /** Parser token. */ String FINALLY = "finally"; /** Parser token. */ String FIRST = "first"; + /** Parser token. */ String FIXED = "fixed"; /** Parser token. */ String FN = "fn"; /** Parser token. */ String FOR = "for"; /** Parser token. */ String FROM = "from"; diff --git a/basex-core/src/main/java/org/basex/query/StaticContext.java b/basex-core/src/main/java/org/basex/query/StaticContext.java index 9a6d95a532..b416fc8f69 100644 --- a/basex-core/src/main/java/org/basex/query/StaticContext.java +++ b/basex-core/src/main/java/org/basex/query/StaticContext.java @@ -36,8 +36,12 @@ public final class StaticContext { /** Default collation (default collection ({@link QueryText#COLLATION_URI}): {@code null}). */ public Collation collation; + /** Default element/type namespace fixed flag. */ + public boolean elemNsFixed; /** Default element/type namespace. */ public byte[] elemNS; + /** Direct element constructor default element/type namespace (differs from above if "fixed"). */ + public byte[] dirNS; /** Default function namespace. */ public byte[] funcNS; /** Name of module (not assigned for main module). */ @@ -90,6 +94,7 @@ public StaticContext(final QueryContext qc) { void namespace(final String prefix, final String uri) throws QueryException { if(prefix.isEmpty()) { elemNS = uri.isEmpty() ? null : token(uri); + dirNS = elemNS; } else if(uri.isEmpty()) { ns.delete(token(prefix)); } else { diff --git a/basex-core/src/main/java/org/basex/query/util/parse/QNmCache.java b/basex-core/src/main/java/org/basex/query/util/parse/QNmCache.java deleted file mode 100644 index 5fb21260d6..0000000000 --- a/basex-core/src/main/java/org/basex/query/util/parse/QNmCache.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.basex.query.util.parse; - -import java.util.*; - -import org.basex.query.*; -import org.basex.query.value.item.*; -import org.basex.util.*; - -/** - * QName cache. - * - * @author BaseX Team, BSD License - * @author Christian Gruen - */ -public final class QNmCache { - /** Cached QNames. */ - private final ArrayList names = new ArrayList<>(); - - /** - * Adds a QName to the cache. - * @param name QName - * @param info input info (can be {@code null}) - */ - public void add(final QNm name, final InputInfo info) { - add(name, true, info); - } - - /** - * Constructor. - * @param name qname - * @param nsElem default check - * @param info input info (can be {@code null}) - */ - public void add(final QNm name, final boolean nsElem, final InputInfo info) { - names.add(new QNmCheck(name, nsElem, info)); - } - - /** - * Finalizes the QNames by assigning namespace URIs. - * @param qp query parser - * @param npos first entry to be checked - * @throws QueryException query exception - */ - public void assignURI(final QueryParser qp, final int npos) throws QueryException { - for(int i = npos; i < names.size(); i++) { - if(names.get(i).assign(qp, npos == 0)) names.remove(i--); - } - } - - /** - * Returns the number of caches QNames. - * @return number - */ - public int size() { - return names.size(); - } -} diff --git a/basex-core/src/main/java/org/basex/query/util/parse/QNmCheck.java b/basex-core/src/main/java/org/basex/query/util/parse/QNmCheck.java deleted file mode 100644 index f746989194..0000000000 --- a/basex-core/src/main/java/org/basex/query/util/parse/QNmCheck.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.basex.query.util.parse; - -import static org.basex.query.QueryError.*; - -import org.basex.query.*; -import org.basex.query.value.item.*; -import org.basex.util.*; - -/** - * Cache for checking QNames after their construction. - * - * @author BaseX Team, BSD License - * @author Christian Gruen - */ -final class QNmCheck { - /** QName to be checked. */ - private final QNm name; - /** Flag for assigning default element namespace. */ - private final boolean nsElem; - /** Input info (can be {@code null}). */ - private final InputInfo info; - - /** - * Constructor. - * @param name qname - * @param nsElem default check - * @param info input info (can be {@code null}) - */ - QNmCheck(final QNm name, final boolean nsElem, final InputInfo info) { - this.name = name; - this.nsElem = nsElem; - this.info = info; - } - - /** - * Assigns the namespace URI that is currently in scope. - * @param parser query parser - * @param check check if prefix URI was assigned - * @return true if URI has a URI - * @throws QueryException query exception - */ - boolean assign(final QueryParser parser, final boolean check) throws QueryException { - if(name.hasURI()) return true; - - if(name.hasPrefix()) { - name.uri(parser.sc.ns.uri(name.prefix())); - if(check && !name.hasURI()) throw parser.error(NOURI_X, info, name.prefix()); - } else if(nsElem) { - name.uri(parser.sc.elemNS); - } - return name.hasURI(); - } -} diff --git a/basex-core/src/main/java/org/basex/query/util/parse/QNmResolver.java b/basex-core/src/main/java/org/basex/query/util/parse/QNmResolver.java new file mode 100644 index 0000000000..028c3d9370 --- /dev/null +++ b/basex-core/src/main/java/org/basex/query/util/parse/QNmResolver.java @@ -0,0 +1,81 @@ +package org.basex.query.util.parse; + +import static org.basex.query.QueryError.*; + +import java.util.*; + +import org.basex.query.*; +import org.basex.query.value.item.*; +import org.basex.util.*; + +/** + * Resolves namespace URIs of QNames whose resolution must be deferred until the surrounding + * namespace context is known. + * + * @author BaseX Team, BSD License + * @author Christian Gruen + */ +public final class QNmResolver { + /** + * Entry for a QName whose namespace URI still needs to be resolved. + * @param name QName to be resolved + * @param nsElem flag for assigning default element namespace + * @param info input info (can be {@code null}) + */ + private record Entry(QNm name, boolean nsElem, InputInfo info) { } + /** QNames to be resolved. */ + private final ArrayList entries = new ArrayList<>(); + + /** + * Adds a QName unless it already has a namespace URI or it can be immediately assigned the fixed + * default namespace URI. This method must not be called for the element names of direct element + * constructors. + * @param name QName + * @param sc static context + * @param info input info (can be {@code null}) + */ + public void add(final QNm name, final StaticContext sc, final InputInfo info) { + if(sc.elemNsFixed && !name.hasPrefix() && !name.hasURI()) name.uri(sc.elemNS); + else add(name, true, info); + } + + /** + * Adds a QName unless it already has a namespace URI. + * @param name qname + * @param nsElem default check + * @param info input info (can be {@code null}) + */ + public void add(final QNm name, final boolean nsElem, final InputInfo info) { + if(!name.hasURI()) entries.add(new Entry(name, nsElem, info)); + } + + /** + * Finalizes the QNames by assigning namespace URIs. + * @param qp query parser + * @param npos first entry to be checked + * @param elemNS default element namespace + * @throws QueryException query exception + */ + public void resolve(final QueryParser qp, final int npos, final byte[] elemNS) + throws QueryException { + for(int i = entries.size() - 1; i >= npos; --i) { + final Entry entry = entries.get(i); + if(entry.name.hasPrefix()) { + entry.name.uri(qp.sc.ns.uri(entry.name.prefix())); + if(npos == 0 && !entry.name.hasURI()) + throw qp.error(NOURI_X, entry.info, entry.name.prefix()); + } else if(entry.nsElem) { + entry.name.uri(elemNS); + } + if(entry.name.hasURI()) entries.remove(i); + } + } + + /** + * Returns the number of remaining QNames. + * @return number + */ + public int size() { + return entries.size(); + } +} diff --git a/basex-core/src/test/java/org/basex/query/NamespaceTest.java b/basex-core/src/test/java/org/basex/query/NamespaceTest.java index 6098832466..d460d9ca27 100644 --- a/basex-core/src/test/java/org/basex/query/NamespaceTest.java +++ b/basex-core/src/test/java/org/basex/query/NamespaceTest.java @@ -848,6 +848,46 @@ public void copyPreserveNoInheritPersistent() { query("db:get('db')/*:B", ""); } + /** Tests fixed default element namespaces. */ + @Test public void fixed() { + // non-fixed: constructor xmlns="" changes the default namespace seen by enclosed expressions + query("declare default element namespace 'foo';" + + "{ element b {} }", + ""); + // fixed: constructor xmlns="" does not change the default namespace of enclosed expressions + query("declare fixed default element namespace 'foo';" + + "{ element b {} }", + ""); + // fixed does not affect direct element constructors themselves + query("declare fixed default element namespace 'foo';" + + "", + ""); + // direct constructor in enclosed expression still uses constructor namespace, not fixed default + query("declare fixed default element namespace 'foo';" + + "{ }", + ""); + // computed constructor uses fixed default; nested direct constructor uses constructor namespace + query("declare fixed default element namespace 'foo';" + + "{ element c { } }", + ""); + // non-fixed version: both computed and direct constructors end up in no namespace + query("declare default element namespace 'foo';" + + "{ element c { } }", + ""); + // prefixed names are unaffected + query("declare fixed default element namespace 'foo';" + + "declare namespace p = 'bar';" + + "{ element p:b {} }", + ""); + // restoration after leaving direct constructor scope + query("declare fixed default element namespace 'foo';" + + "({ element b {} }, element c {})", + "\n"); + + error("import schema fixed default element namespace 'http://www.w3.org/2003/05/soap-envelope' " + + "at 'http://www.w3.org/2003/05/soap-envelope/'; 42", IMPLSCHEMA); + } + /** * Creates the specified test databases. * @param db database numbers