Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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).
*
* <p>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 <uri>}
* 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}.</p>
*
* @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);
Comment thread
dizzzz marked this conversation as resolved.
};
}

/**
* 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 <uri> 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <a href="https://github.com/eXist-db/exist/pull/6528">#6528</a>:
* 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.
*
* <p>{@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 <uri>} 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}.</p>
*
* <p>Existing XSD 1.1 coverage in {@link JaxpXsdCatalogTest} only exercises
* <em>directory-search</em> catalogs over {@code /db/.../}; {@link
* org.exist.validation.CollectionConfigSchema6000Test} validates the native XSD via raw
* {@code SchemaFactory}, not {@code validation:jaxp}.</p>
*
* @see <a href="https://github.com/eXist-db/exist/issues/6686">#6686</a>
* @see <a href="https://github.com/eXist-db/eXide/issues/842">eXide#842</a>
*/
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 = """
<collection xmlns="http://exist-db.org/collection-config/1.0">
<index/>
</collection>
""";

private static final String EMPTY_COLLECTION_XCONF = """
<collection xmlns="http://exist-db.org/collection-config/1.0"/>
""";

private static final String NO_VALIDATION = """
<?xml version='1.0'?>
<collection xmlns='http://exist-db.org/collection-config/1.0'>
<validation mode='no'/>
</collection>
""";

@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 = """
<?xml version="1.0"?>
<catalog xmlns="urn:oasis:names:tc:entity:xmlns:xml:catalog">
<uri name="%s" uri="%s"/>
</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("&", "&amp;").replace("<", "&lt;").replace("\"", "&quot;");
}
}
Loading