diff --git a/exist-core/src/main/java/org/exist/resolver/XercesXmlResolverAdapter.java b/exist-core/src/main/java/org/exist/resolver/XercesXmlResolverAdapter.java
index 7c58786b6c5..10c853191fa 100644
--- a/exist-core/src/main/java/org/exist/resolver/XercesXmlResolverAdapter.java
+++ b/exist-core/src/main/java/org/exist/resolver/XercesXmlResolverAdapter.java
@@ -43,6 +43,7 @@
import org.apache.xerces.xni.parser.XMLEntityResolver;
import org.apache.xerces.xni.parser.XMLInputSource;
import org.exist.util.XMLReaderObjectFactory;
+import org.w3c.dom.ls.LSResourceResolver;
import org.xml.sax.*;
import org.xmlresolver.Resolver;
@@ -165,4 +166,45 @@ public static void setXmlReaderEntityResolver(final XMLReader xmlReader, @Nullab
public Resolver getResolver() {
return resolver;
}
+
+ /**
+ * Wraps {@code resolver} as an {@link LSResourceResolver}, falling back to {@code namespaceURI}
+ * as the system id whenever the caller doesn't supply one -- the same fallback {@link
+ * #resolveEntity} already performs for the XNI/SAX pipeline (see its {@code systemId} lookup).
+ *
+ *
This fallback matters because {@link Resolver#resolveResource} requires a non-null system
+ * id before it will consult its catalog at all; without it, an OASIS catalog's {@code }
+ * entry keyed purely by namespace (no {@code schemaLocation} hint on the instance) is invisible
+ * to the XSD 1.1 dynamic-discovery {@link javax.xml.validation.Validator}, which calls {@link
+ * LSResourceResolver#resolveResource} with the root element's namespace but no system id --
+ * even though the same catalog entry already works for the default SAX pipeline via {@link
+ * #resolveEntity}.
+ *
+ * @param resolver the resolver to wrap.
+ *
+ * @return an {@link LSResourceResolver} view of {@code resolver} with the namespace fallback applied.
+ */
+ public static LSResourceResolver asLSResourceResolver(final Resolver resolver) {
+ return (type, namespaceURI, publicId, systemId, baseURI) -> {
+ final String effectiveSystemId = systemId != null ? systemId : namespaceURI;
+ return resolver.resolveResource(type, namespaceURI, publicId, effectiveSystemId, baseURI);
+ };
+ }
+
+ /**
+ * As {@link #asLSResourceResolver(Resolver)}, but accepting whichever concrete resolver type
+ * {@code Shared#resolveCatalogArgument} returns -- the namespace fallback is only applied when
+ * {@code resolver} is an {@link Resolver} (the implementation that needs it); any other {@link
+ * LSResourceResolver} (e.g. {@link org.exist.validation.resolver.SearchResourceResolver}, which
+ * already resolves purely by namespace on its own) is returned unchanged, as is {@code null}.
+ *
+ * @param resolver the resolver to adapt, or null.
+ *
+ * @return an {@link LSResourceResolver} with the namespace fallback applied where needed, or
+ * {@code null} if {@code resolver} was {@code null}.
+ */
+ @Nullable
+ public static LSResourceResolver asLSResourceResolver(@Nullable final LSResourceResolver resolver) {
+ return resolver instanceof Resolver xmlResolver ? asLSResourceResolver(xmlResolver) : resolver;
+ }
}
diff --git a/exist-core/src/main/java/org/exist/xquery/functions/validation/Jaxp.java b/exist-core/src/main/java/org/exist/xquery/functions/validation/Jaxp.java
index c288358fa32..a71ed10c136 100644
--- a/exist-core/src/main/java/org/exist/xquery/functions/validation/Jaxp.java
+++ b/exist-core/src/main/java/org/exist/xquery/functions/validation/Jaxp.java
@@ -565,8 +565,10 @@ private GrammarPool getGrammarPool() {
* @return a {@link Validator} for the only XSD 1.1-capable pipeline this
* Xerces fork supports: {@link SchemaFactory}/{@link Schema} with no
* pre-supplied schema documents, so it dynamically discovers the schema
- * from the instance's own schemaLocation hint, mirroring how the default
- * SAXParser pipeline behaves for XSD 1.0.
+ * from the instance's own schemaLocation hint or, when {@code resolver} is
+ * an OASIS/system catalog, purely from the root element's namespace via
+ * {@link XercesXmlResolverAdapter#asLSResourceResolver}, mirroring how the
+ * default SAXParser pipeline behaves for XSD 1.0.
*/
private Validator newXsd11Validator(@Nullable final LSResourceResolver resolver, final boolean useCache) throws SAXException {
final SchemaFactory schemaFactory = SchemaFactory.newInstance(XSD_1_1_NS);
@@ -581,7 +583,12 @@ private Validator newXsd11Validator(@Nullable final LSResourceResolver resolver,
final Schema schema = schemaFactory.newSchema();
final Validator validator = schema.newValidator();
if (resolver != null) {
- validator.setResourceResolver(resolver);
+ // org.xmlresolver.Resolver#resolveResource requires a non-null systemId before it will
+ // consult its catalog at all (unlike SearchResourceResolver, which resolves purely by
+ // namespace), so an OASIS catalog's entry keyed by namespace alone is otherwise
+ // invisible to this dynamic-discovery validator when the instance carries no
+ // schemaLocation hint.
+ validator.setResourceResolver(XercesXmlResolverAdapter.asLSResourceResolver(resolver));
}
return validator;
}
diff --git a/exist-core/src/main/java/org/exist/xquery/functions/validation/Jaxv.java b/exist-core/src/main/java/org/exist/xquery/functions/validation/Jaxv.java
index 010f626955b..f881c964ae0 100644
--- a/exist-core/src/main/java/org/exist/xquery/functions/validation/Jaxv.java
+++ b/exist-core/src/main/java/org/exist/xquery/functions/validation/Jaxv.java
@@ -32,6 +32,7 @@
import org.exist.dom.QName;
import org.exist.dom.memtree.MemTreeBuilder;
import org.exist.dom.memtree.NodeImpl;
+import org.exist.resolver.XercesXmlResolverAdapter;
import org.exist.storage.BrokerPool;
import org.exist.validation.ValidationReport;
import org.exist.xquery.BasicFunction;
@@ -237,10 +238,13 @@ public Sequence eval(Sequence[] args, Sequence contextSequence) throws XPathExce
// directly, and SearchResourceResolver also implements it (in addition to the
// Xerces-specific XMLEntityResolver/XNI interface it uses for validation:jaxp()'s
// SAX pipeline), so the directory-search/collection case (a catalog URL ending
- // in '/') works here too.
+ // in '/') works here too. Wrapped via XercesXmlResolverAdapter#asLSResourceResolver
+ // so an xs:import with no schemaLocation (resolved purely by namespace) also works
+ // through an org.xmlresolver.Resolver-backed (OASIS/system) catalog -- see that
+ // method's Javadoc for why the raw Resolver alone can't do this.
if (args.length == 4) {
final LSResourceResolver resolver = Shared.resolveCatalogArgument(this, brokerPool, context.getBroker(), context.getSubject(), args[3]);
- factory.setResourceResolver(resolver);
+ factory.setResourceResolver(XercesXmlResolverAdapter.asLSResourceResolver(resolver));
}
// Create grammar -- xs:import/xs:include resolution (via the resolver se
diff --git a/exist-core/src/test/java/org/exist/xquery/functions/validate/NativeSchemaJaxpCatalogGapTest.java b/exist-core/src/test/java/org/exist/xquery/functions/validate/NativeSchemaJaxpCatalogGapTest.java
new file mode 100644
index 00000000000..dddc1359f03
--- /dev/null
+++ b/exist-core/src/test/java/org/exist/xquery/functions/validate/NativeSchemaJaxpCatalogGapTest.java
@@ -0,0 +1,207 @@
+/*
+ * eXist-db Open Source Native XML Database
+ * Copyright (C) 2001 The eXist-db Authors
+ *
+ * info@exist-db.org
+ * http://www.exist-db.org
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+package org.exist.xquery.functions.validate;
+
+import org.custommonkey.xmlunit.exceptions.XpathException;
+import org.exist.test.ExistXmldbEmbeddedServer;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+import org.xmldb.api.base.Collection;
+import org.xmldb.api.base.ResourceSet;
+import org.xmldb.api.base.XMLDBException;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import static org.custommonkey.xmlunit.XMLAssert.assertXpathEvaluatesTo;
+import static org.exist.collections.CollectionConfiguration.DEFAULT_COLLECTION_CONFIG_FILE;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Closes the integration gap left by #6528:
+ * native XSDs ship at {@code $EXIST_HOME/schema/} and {@code WEB-INF/catalog.xml} maps
+ * {@code http://exist-db.org/collection-config/1.0} to {@code collection.xconf.xsd}, but nothing
+ * asserts that {@code validation:jaxp*} can apply that grammar via an OASIS catalog the way
+ * tools (e.g. eXide) are expected to.
+ *
+ * {@link #jaxvXsd11AgainstNativeCollectionSchemaValid} /
+ * {@link #jaxvXsd11AgainstNativeCollectionSchemaEmptyFailsAssert} are the control: the schema
+ * file itself is fine when handed to {@code validation:jaxv-report} with the XSD 1.1 language URI.
+ * {@link #jaxpViaOasisCatalogAgainstNativeCollectionSchemaValid} was the missing path — it used to
+ * fail with {@code cvc-elt.1.a} (declaration not found) even when the catalog {@code } entry
+ * pointed at an absolute {@code file:} URI of the same XSD, because {@code
+ * org.xmlresolver.Resolver#resolveResource} requires a non-null system id before consulting its
+ * catalog, and the XSD 1.1 dynamic-discovery validator calls it with the root namespace but no
+ * system id when the instance carries no {@code schemaLocation} hint. Fixed by {@link
+ * org.exist.resolver.XercesXmlResolverAdapter#asLSResourceResolver}.
+ *
+ * Existing XSD 1.1 coverage in {@link JaxpXsdCatalogTest} only exercises
+ * directory-search catalogs over {@code /db/.../}; {@link
+ * org.exist.validation.CollectionConfigSchema6000Test} validates the native XSD via raw
+ * {@code SchemaFactory}, not {@code validation:jaxp}.
+ *
+ * @see #6686
+ * @see eXide#842
+ */
+public class NativeSchemaJaxpCatalogGapTest {
+
+ private static final String COLLECTION_CONFIG_NS = "http://exist-db.org/collection-config/1.0";
+ private static final String XSD_1_1 = "http://www.w3.org/XML/XMLSchema/v1.1";
+
+ private static final String VALID_COLLECTION_XCONF = """
+
+
+
+ """;
+
+ private static final String EMPTY_COLLECTION_XCONF = """
+
+ """;
+
+ private static final String NO_VALIDATION = """
+
+
+
+
+ """;
+
+ @ClassRule
+ public static final ExistXmldbEmbeddedServer existEmbeddedServer =
+ new ExistXmldbEmbeddedServer(false, true, true);
+
+ private static Path nativeSchemaPath;
+ private static String nativeSchemaFileUri;
+
+ @BeforeClass
+ public static void prepareResources() throws XMLDBException, IOException {
+ nativeSchemaPath = resolveSchemaPath();
+ assertTrue("""
+ Native schema not found at %s \
+ (run from repo root: mvn test -pl exist-core -Dtest=NativeSchemaJaxpCatalogGapTest)
+ """.formatted(nativeSchemaPath).strip(),
+ Files.exists(nativeSchemaPath));
+ nativeSchemaFileUri = nativeSchemaPath.toAbsolutePath().toUri().toString();
+
+ try (Collection conf = existEmbeddedServer.createCollection(existEmbeddedServer.getRoot(),
+ "system/config/db/native-schema-gap")) {
+ ExistXmldbEmbeddedServer.storeResource(conf, DEFAULT_COLLECTION_CONFIG_FILE,
+ NO_VALIDATION.getBytes());
+ }
+
+ try (Collection col = existEmbeddedServer.createCollection(existEmbeddedServer.getRoot(),
+ "native-schema-gap")) {
+ // Mirrors WEB-INF/catalog.xml's collection-config mapping, but with an absolute
+ // file: URI so the embedded harness does not depend on $EXIST_HOME layout.
+ final String catalog = """
+
+
+
+
+ """.formatted(COLLECTION_CONFIG_NS, escapeXmlAttribute(nativeSchemaFileUri));
+ ExistXmldbEmbeddedServer.storeResource(col, "catalog.xml", catalog.getBytes());
+ ExistXmldbEmbeddedServer.storeResource(col, "valid.xml", VALID_COLLECTION_XCONF.getBytes());
+ ExistXmldbEmbeddedServer.storeResource(col, "empty.xml", EMPTY_COLLECTION_XCONF.getBytes());
+ }
+ }
+
+ @Before
+ public void clearGrammarCache() throws XMLDBException {
+ final ResourceSet results = existEmbeddedServer.executeQuery("validation:clear-grammar-cache()");
+ results.getResource(0).getContent();
+ }
+
+ @Test
+ public void jaxvXsd11AgainstNativeCollectionSchemaValid() throws Exception {
+ final String query = """
+ validation:jaxv-report(
+ doc('/db/native-schema-gap/valid.xml'),
+ xs:anyURI('%s'),
+ '%s')
+ """.formatted(nativeSchemaFileUri, XSD_1_1);
+ assertReportStatus(query, "valid");
+ }
+
+ @Test
+ public void jaxvXsd11AgainstNativeCollectionSchemaEmptyFailsAssert() throws Exception {
+ final String query = """
+ validation:jaxv-report(
+ doc('/db/native-schema-gap/empty.xml'),
+ xs:anyURI('%s'),
+ '%s')
+ """.formatted(nativeSchemaFileUri, XSD_1_1);
+ final String report = executeOne(query);
+ assertXpathEvaluatesTo("invalid", "//status/text()", report);
+ assertTrue("expected xs:assert failure, got: " + report,
+ report.contains("cvc-assertion") || report.contains("count(*)"));
+ }
+
+ /**
+ * OASIS catalog → native {@code collection.xconf.xsd}, resolved purely by namespace (no
+ * {@code schemaLocation} hint on the instance).
+ */
+ @Test
+ public void jaxpViaOasisCatalogAgainstNativeCollectionSchemaValid() throws Exception {
+ final String query = """
+ validation:jaxp-report(
+ doc('/db/native-schema-gap/valid.xml'),
+ false(),
+ xs:anyURI('/db/native-schema-gap/catalog.xml'))
+ """;
+ assertReportStatus(query, "valid");
+ }
+
+ private static void assertReportStatus(final String query, final String expected)
+ throws XMLDBException, SAXException, IOException, XpathException {
+ assertXpathEvaluatesTo(expected, "//status/text()", executeOne(query));
+ }
+
+ private static String executeOne(final String query) throws XMLDBException {
+ final ResourceSet results = existEmbeddedServer.executeQuery(query);
+ assertEquals(1, results.getSize());
+ return (String) results.getResource(0).getContent();
+ }
+
+ private static Path resolveSchemaPath() {
+ final Path base = Path.of(System.getProperty("user.dir"));
+ Path p = base.resolve("schema").resolve("collection.xconf.xsd");
+ if (!Files.exists(p)) {
+ p = base.getParent().resolve("schema").resolve("collection.xconf.xsd");
+ }
+ return p;
+ }
+
+ /**
+ * Escapes {@code value} for use inside a double-quoted XML attribute. Needed because {@link
+ * #nativeSchemaFileUri} is derived from the checkout's own filesystem path via {@link
+ * Path#toUri()}, which does not escape XML-attribute-special characters (e.g. {@code &}) that
+ * a directory name could legally contain -- without this, such a path would produce a
+ * malformed {@code catalog.xml}.
+ */
+ private static String escapeXmlAttribute(final String value) {
+ return value.replace("&", "&").replace("<", "<").replace("\"", """);
+ }
+}