diff --git a/.github/scripts/prepare-governance-context.sh b/.github/scripts/prepare-governance-context.sh new file mode 100755 index 00000000000..43b48759644 --- /dev/null +++ b/.github/scripts/prepare-governance-context.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Pure git plumbing for schema governance — everything else (pairing, +# version comparison, the GitHub annotations, failing the build) happens in +# a single Saxon XSLT 2.0 transform (schema/governance.xsl) driven by +# `mvn xml:transform@schema-governance`. This script's only job is to put +# the git state that transform needs onto disk as plain files/XML, so the +# stylesheet never has to shell out itself. +set -euo pipefail +cd "$(git rev-parse --show-toplevel)" + +WORKSPACE="${1:?workspace required}" +OUT="${2:-${WORKSPACE}/target/governance}" +mkdir -p "${OUT}/base" + +if [[ -n "${GITHUB_BASE_REF:-}" ]]; then + git fetch --depth=1 origin "${GITHUB_BASE_REF}" 2>/dev/null || true + BASE="$(git merge-base HEAD "origin/${GITHUB_BASE_REF}")" +elif [[ -n "${GITHUB_EVENT_BEFORE:-}" && "${GITHUB_EVENT_BEFORE}" != "0000000000000000000000000000000000000000" ]]; then + BASE="${GITHUB_EVENT_BEFORE}" +else + BASE="$(git rev-parse HEAD~1 2>/dev/null || git rev-parse HEAD)" +fi + +# Every tracked schema/*.xsd's content as it existed at BASE, one file per +# schema named after its basename. A missing file at $OUT/base/ means +# "didn't exist at BASE" (new schema) — governance.xsl checks for that with +# doc-available() rather than this script trying to distinguish "new file" +# from "tool failure" itself. +git ls-tree -r --name-only HEAD -- schema | grep '\.xsd$' | while read -r f; do + out="${OUT}/base/$(basename "${f}")" + git show "${BASE}:${f}" > "${out}" 2>/dev/null || rm -f "${out}" +done + +# One path per line; governance.xsl reads this with unparsed-text() + tokenize(), +# so no XML-escaping of path characters is needed anywhere in this pipeline. +git diff --name-only "${BASE}" -- \ + schema \ + exist-distribution/src/main/config \ + exist-jetty-config/src/main/resources/webapp/WEB-INF/controller-config.xml \ + exist-core/src/main/resources/org/exist/util/mime-types.xml \ + exist-core/src/main/java/org/exist/util/SchemaVersion.java \ + > "${OUT}/changed.txt" 2>/dev/null || true + +cat > "${OUT}/context.xml" < + +EOF + +echo "Governance context: ${OUT}/context.xml (base ${BASE})" diff --git a/.github/workflows/ci-schema-checks.yml b/.github/workflows/ci-schema-checks.yml new file mode 100644 index 00000000000..bed3744ff75 --- /dev/null +++ b/.github/workflows/ci-schema-checks.yml @@ -0,0 +1,60 @@ +name: Schema checks + +on: + pull_request: + paths: + - 'schema/**' + - 'exist-distribution/src/main/config/**' + - 'exist-jetty-config/src/main/resources/webapp/WEB-INF/controller-config.xml' + - 'exist-core/src/main/resources/org/exist/util/mime-types.xml' + - 'exist-core/src/main/java/org/exist/util/SchemaVersion.java' + push: + branches: [develop] + paths: + - 'schema/**' + - 'exist-distribution/src/main/config/**' + - 'exist-jetty-config/src/main/resources/webapp/WEB-INF/controller-config.xml' + - 'exist-core/src/main/resources/org/exist/util/mime-types.xml' + - 'exist-core/src/main/java/org/exist/util/SchemaVersion.java' + workflow_dispatch: + +permissions: + contents: read + +env: + MAVEN_OPTS: -DtrimStackTrace=false + DEV_JDK: '21' + +jobs: + schema: + name: Native XSD checks + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: ${{ env.DEV_JDK }} + + - uses: ./.github/actions/maven-cache + + - name: Validate canonical templates against XSD + run: mvn -V -B --no-transfer-progress validate -Ddependency-check.skip=true -Ddocker=false + + - name: Prepare governance context + env: + GITHUB_BASE_REF: ${{ github.base_ref }} + GITHUB_EVENT_BEFORE: ${{ github.event.before }} + run: | + chmod +x .github/scripts/prepare-governance-context.sh + .github/scripts/prepare-governance-context.sh "${{ github.workspace }}" + + - name: Run schema governance (XSLT 2.0 / Saxon) + run: mvn -N -B --no-transfer-progress xml:transform@schema-governance -Ddependency-check.skip=true -Ddocker=false + + - name: Verify SchemaVersion.java matches XSD versions + run: mvn -B --no-transfer-progress test -pl exist-core -Dtest=org.exist.util.SchemaVersionSyncTest -Ddependency-check.skip=true -Ddocker=false diff --git a/AGENTS.md b/AGENTS.md index b742b4da0e5..f75437ddace 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -144,6 +144,30 @@ ANTLR generates `XQueryParser.java`, `XQueryLexer.java`, `XQueryTreeParser.java` | `org.exist.dom.persistent` | Persistent DOM implementation | | `org.exist.dom.memtree` | In-memory DOM (for constructed nodes) | +### Native config schemas (`schema/`) + +eXist-db's own config-file XSDs (`conf.xsd`, `collection.xconf.xsd`, `descriptor.xsd`, +`controller-config.xsd`, `mime-types.xsd`, plus `users.xsd`/`server.xsd`/`security-manager.xsd`/ +`expath-pkg.xsd` and its extensions) live in [`schema/`](schema/) at the repo root, and are shipped +in every distribution layout as `$EXIST_HOME/schema/` — a sibling of `etc/`, `bin/`, `lib/` (tarball, +zip, Docker image, and the IzPack installer all include it; see `exist-distribution`/`exist-docker`/ +`exist-installer`). External tools (eXide, IDE plugins) can resolve a config file's grammar from +this fixed location instead of vendoring their own copy. + +- Each XSD's `xs:schema/@version` is an independent semver line — see [`schema/README.md`](schema/README.md) + for the versioning policy (CI enforces a version bump on any semantic schema edit, via + `mvn -N xml:transform@schema-governance`, see [`schema/governance.xsl`](schema/governance.xsl)). +- `org.exist.util.SchemaVersion`'s version constants are generated at build time from the XSDs + themselves (`generate-sources` phase, see `exist-core/pom.xml`'s `schema-version-codegen` + execution and [`schema/generate-schema-version.xsl`](schema/generate-schema-version.xsl)) — never + hand-edit `SchemaVersion`'s constants; bump the XSD's `xs:schema/@version` instead and the + constant follows automatically on the next build. +- The 5 canonical instances (the files `pom.xml`'s `validate-canonical-instances` execution + validates on every `mvn validate`) are the only ones checked for drift; the ~39 test/sample + fixture copies scattered across module test resources (e.g. `extensions/*/src/test/resources*/conf.xml`) + are intentionally hand-trimmed per-module subsets, not literal copies — don't try to regenerate + them from canonical. + ### Adding a new `fn:` function 1. Create the class in `org.exist.xquery.functions.fn` extending `BasicFunction` diff --git a/exist-core/pom.xml b/exist-core/pom.xml index 94287e0e7e1..2fcdb5ca616 100644 --- a/exist-core/pom.xml +++ b/exist-core/pom.xml @@ -301,7 +301,6 @@ org.exist-db.thirdparty.xerces xercesImpl - 2.12.2 jdk14-xml-schema-1.1 @@ -348,7 +347,6 @@ org.xmlresolver xmlresolver - ${xmlresolver.version} xml-apis @@ -360,7 +358,6 @@ org.xmlresolver xmlresolver - ${xmlresolver.version} data runtime @@ -376,13 +373,11 @@ org.exist-db.thirdparty.org.eclipse.wst.xml xpath2 - 1.2.0 runtime edu.princeton.cup java-cup - 10k runtime diff --git a/exist-core/src/main/java/org/exist/collections/CollectionConfiguration.java b/exist-core/src/main/java/org/exist/collections/CollectionConfiguration.java index 379eb2e6b90..ffaa06de747 100644 --- a/exist-core/src/main/java/org/exist/collections/CollectionConfiguration.java +++ b/exist-core/src/main/java/org/exist/collections/CollectionConfiguration.java @@ -40,6 +40,7 @@ import org.exist.storage.IndexSpec; import org.exist.util.DatabaseConfigurationException; import org.exist.util.ParametersExtractor; +import org.exist.util.SchemaVersion; import org.exist.util.XMLReaderObjectFactory; import org.exist.xmldb.XmldbURI; import org.w3c.dom.Document; @@ -129,6 +130,8 @@ protected void read(final DBBroker broker, final Document doc, final boolean che "' in configuration document. Got '" + root.getNamespaceURI() + "'", checkOnly); return; } + SchemaVersion.logDocumentVersion(LOG, root, SchemaVersion.COLLECTION_XCONF, + "collection.xconf" + (docName != null ? " (" + docName + ")" : "")); final NodeList childNodes = root.getChildNodes(); for (int i = 0; i < childNodes.getLength(); i++) { Node node = childNodes.item(i); diff --git a/exist-core/src/main/java/org/exist/http/Descriptor.java b/exist-core/src/main/java/org/exist/http/Descriptor.java index 571aa558276..1f284d76b15 100644 --- a/exist-core/src/main/java/org/exist/http/Descriptor.java +++ b/exist-core/src/main/java/org/exist/http/Descriptor.java @@ -28,6 +28,7 @@ import org.exist.dom.memtree.SAXAdapter; import org.exist.util.ConfigurationHelper; import org.exist.util.ExistSAXParserFactory; +import org.exist.util.SchemaVersion; import org.exist.util.SingleInstanceConfiguration; import org.exist.xquery.Expression; import org.w3c.dom.Document; @@ -138,6 +139,8 @@ private Descriptor() { final Document doc = adapter.getDocument(); + SchemaVersion.logDocumentVersion(LOG, doc.getDocumentElement(), SchemaVersion.DESCRIPTOR, "descriptor.xml"); + //load attribue settings if ("true".equals(doc.getDocumentElement().getAttribute("request-replay-log"))) { final Path logFile = Path.of("request-replay-log.txt"); diff --git a/exist-core/src/main/java/org/exist/http/urlrewrite/RewriteConfig.java b/exist-core/src/main/java/org/exist/http/urlrewrite/RewriteConfig.java index 745dd98d106..a08a016c20b 100644 --- a/exist-core/src/main/java/org/exist/http/urlrewrite/RewriteConfig.java +++ b/exist-core/src/main/java/org/exist/http/urlrewrite/RewriteConfig.java @@ -35,6 +35,7 @@ import net.sf.saxon.str.StringView; import net.sf.saxon.trans.XPathException; import org.exist.util.XMLReaderPool; +import org.exist.util.SchemaVersion; import org.exist.xmldb.XmldbURI; import org.exist.xquery.Constants; import org.exist.xquery.Expression; @@ -199,6 +200,7 @@ private void configure(final String controllerConfig) throws ServletException { private void parse(final Document doc) throws ServletException { final Element root = doc.getDocumentElement(); + SchemaVersion.logDocumentVersion(LOG, root, SchemaVersion.CONTROLLER_CONFIG, "controller-config.xml"); Node child = root.getFirstChild(); while (child != null) { final String ns = child.getNamespaceURI(); diff --git a/exist-core/src/main/java/org/exist/util/Configuration.java b/exist-core/src/main/java/org/exist/util/Configuration.java index 0927f76fc89..bbead109e73 100644 --- a/exist-core/src/main/java/org/exist/util/Configuration.java +++ b/exist-core/src/main/java/org/exist/util/Configuration.java @@ -345,6 +345,9 @@ public Configuration(@Nullable String configFilename, Optional existHomeDi final Document doc = adapter.getDocument(); + SchemaVersion.logDocumentVersion(LOG, doc.getDocumentElement(), SchemaVersion.CONF, + configFilePath.map(p -> "conf.xml (" + p + ")").orElse("conf.xml")); + //indexer settings configureElement(doc, Indexer.CONFIGURATION_ELEMENT_NAME, element -> configureIndexer(doc, element)); //scheduler settings diff --git a/exist-core/src/main/java/org/exist/util/MimeTable.java b/exist-core/src/main/java/org/exist/util/MimeTable.java index 09d303e9ae9..87252ceee43 100644 --- a/exist-core/src/main/java/org/exist/util/MimeTable.java +++ b/exist-core/src/main/java/org/exist/util/MimeTable.java @@ -143,8 +143,9 @@ public MimeTable(final Path path) { } try (final InputStream is = Files.newInputStream(path)) { LOG.info("Loading mime table from file: {}", path.toAbsolutePath()); - loadMimeTypes(is); - this.src = path.toUri().toString(); + final String sourceDescription = path.toUri().toString(); + loadMimeTypes(is, sourceDescription); + this.src = sourceDescription; } catch (final ParserConfigurationException | SAXException | IOException e) { throw new IllegalStateException(FILE_LOAD_FAILED_ERR + path.toAbsolutePath(), e); } @@ -264,7 +265,7 @@ private void load(final InputStream stream, final String src) { private void loadFromStream(final InputStream stream, final String sourceDescription) { try (stream) { - loadMimeTypes(stream); + loadMimeTypes(stream, sourceDescription); this.src = sourceDescription; } catch (final ParserConfigurationException | SAXException | IOException e) { throw new IllegalStateException("Failed to load mime-type table from " + sourceDescription, e); @@ -275,16 +276,17 @@ private void loadFromStream(final InputStream stream, final String sourceDescrip * Load Mime Types * * @param stream input stream. + * @param sourceDescription description of the stream's origin, for diagnostic messages. * * @throws SAXException if an error occurs whilst reading the XML stream * @throws ParserConfigurationException if an error occurs whilst parsing the stream * @throws IOException if an error occurs whilst reading the stream */ - private void loadMimeTypes(final InputStream stream) throws ParserConfigurationException, SAXException, IOException { + private void loadMimeTypes(final InputStream stream, final String sourceDescription) throws ParserConfigurationException, SAXException, IOException { final SAXParserFactory factory = ExistSAXParserFactory.getSAXParserFactory(); factory.setNamespaceAware(true); factory.setValidating(false); - final InputSource src = new InputSource(stream); + final InputSource inputSource = new InputSource(stream); final SAXParser parser = factory.newSAXParser(); final XMLReader reader = parser.getXMLReader(); @@ -292,8 +294,8 @@ private void loadMimeTypes(final InputStream stream) throws ParserConfigurationE reader.setFeature("http://xml.org/sax/features/external-parameter-entities", false); reader.setFeature(FEATURE_SECURE_PROCESSING, true); - reader.setContentHandler(new MimeTableHandler()); - reader.parse(src); + reader.setContentHandler(new MimeTableHandler(sourceDescription)); + reader.parse(inputSource); } private class MimeTableHandler extends DefaultHandler { @@ -302,16 +304,23 @@ private class MimeTableHandler extends DefaultHandler { private static final String DESCRIPTION = "description"; private static final String MIME_TYPE = "mime-type"; private static final String MIME_TYPES = "mime-types"; - + + private final String sourceDescription; private MimeType mime = null; private final StringBuilder charBuf = new StringBuilder(64); + MimeTableHandler(final String sourceDescription) { + this.sourceDescription = sourceDescription; + } + @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { if (MIME_TYPES.equals(qName)) { + SchemaVersion.logDocumentVersion(LOG, attributes.getValue(SchemaVersion.ATTRIBUTE), + SchemaVersion.MIME_TYPES, sourceDescription != null ? "mime-types.xml (" + sourceDescription + ")" : "mime-types.xml"); // Check for a default mime type settings final String defaultMimeAttr = attributes.getValue("default-mime-type"); final String defaultTypeAttr = attributes.getValue("default-resource-type"); diff --git a/exist-core/src/main/java/org/exist/util/SchemaVersion.java b/exist-core/src/main/java/org/exist/util/SchemaVersion.java new file mode 100644 index 00000000000..607f227816b --- /dev/null +++ b/exist-core/src/main/java/org/exist/util/SchemaVersion.java @@ -0,0 +1,69 @@ +/* + * 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.util; + +import org.apache.logging.log4j.Logger; +import org.w3c.dom.Element; + +/** + * Optional {@code schemaVersion} on native config instance documents — mirrors + * {@code xs:schema/@version} on the paired XSD (native schema semver, not eXist product version). + */ +public final class SchemaVersion { + + public static final String ATTRIBUTE = "schemaVersion"; + + /** Paired {@code xs:schema/@version} values for canonical templates (keep in sync with {@code schema/*.xsd}). */ + public static final String CONF = "2.1.1"; + public static final String COLLECTION_XCONF = "1.2.1"; + public static final String DESCRIPTOR = "1.2.1"; + public static final String MIME_TYPES = "1.2.1"; + public static final String CONTROLLER_CONFIG = "1.1.1"; + + private SchemaVersion() { + } + + /** + * Log when {@code schemaVersion} is missing (legacy) or differs from the schema version this build expects. + */ + public static void logDocumentVersion(final Logger log, final Element root, + final String expectedVersion, final String documentDescription) { + logDocumentVersion(log, root != null ? root.getAttribute(ATTRIBUTE) : "", expectedVersion, documentDescription); + } + + /** + * SAX variant when only the attribute value is available. + */ + public static void logDocumentVersion(final Logger log, final String declaredVersion, + final String expectedVersion, final String documentDescription) { + if (declaredVersion == null || declaredVersion.isEmpty()) { + log.debug("{} has no {} attribute (legacy document)", documentDescription, ATTRIBUTE); + return; + } + if (!declaredVersion.equals(expectedVersion)) { + log.warn("{} declares {}=\"{}\" but this eXist build expects \"{}\"", + documentDescription, ATTRIBUTE, declaredVersion, expectedVersion); + } else { + log.debug("{} {}=\"{}\"", documentDescription, ATTRIBUTE, declaredVersion); + } + } +} diff --git a/exist-core/src/main/resources/org/exist/util/mime-types.xml b/exist-core/src/main/resources/org/exist/util/mime-types.xml index 58ba03cf203..142f6a2318b 100644 --- a/exist-core/src/main/resources/org/exist/util/mime-types.xml +++ b/exist-core/src/main/resources/org/exist/util/mime-types.xml @@ -37,7 +37,7 @@ and then as a classpath resource in org/exist/util . ======================================================= --> - + diff --git a/exist-core/src/test/java/org/exist/util/SchemaVersionSyncTest.java b/exist-core/src/test/java/org/exist/util/SchemaVersionSyncTest.java new file mode 100644 index 00000000000..8c77bf0a902 --- /dev/null +++ b/exist-core/src/test/java/org/exist/util/SchemaVersionSyncTest.java @@ -0,0 +1,86 @@ +/* + * 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.util; + +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; + +import javax.xml.parsers.DocumentBuilderFactory; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The CI schema-governance workflow (.github/workflows/ci-schema-checks.yml) + * enforces that {@code xs:schema/@version} is bumped whenever a native XSD or + * its canonical template changes — but it diffs files via git path filters + * and has no visibility into Java source, so a forgotten update to the + * hand-copied constants in {@link SchemaVersion} would otherwise drift + * silently. This test closes that gap directly: it runs on every {@code mvn + * test}, independent of which files a PR happens to touch, and fails loudly + * the moment a constant disagrees with its paired XSD. + */ +public class SchemaVersionSyncTest { + + private static final Map SCHEMA_FILE_TO_CONSTANT = Map.of( + "conf.xsd", SchemaVersion.CONF, + "collection.xconf.xsd", SchemaVersion.COLLECTION_XCONF, + "descriptor.xsd", SchemaVersion.DESCRIPTOR, + "mime-types.xsd", SchemaVersion.MIME_TYPES, + "controller-config.xsd", SchemaVersion.CONTROLLER_CONFIG); + + @Test + public void schemaVersionConstantsMatchXsds() throws Exception { + final Path schemaDir = resolveSchemaDir(); + assertTrue(Files.isDirectory(schemaDir), + "schema/ directory not found at " + schemaDir + " (run from repo root: mvn test -pl exist-core)"); + + final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + + for (final Map.Entry entry : SCHEMA_FILE_TO_CONSTANT.entrySet()) { + final String fileName = entry.getKey(); + final Path xsdPath = schemaDir.resolve(fileName); + assertTrue(Files.exists(xsdPath), "Missing XSD: " + xsdPath); + + final Document doc = factory.newDocumentBuilder().parse(xsdPath.toFile()); + final String xsdVersion = doc.getDocumentElement().getAttribute("version"); + + assertEquals(xsdVersion, entry.getValue(), + "SchemaVersion.java is out of sync with schema/" + fileName + + " — update the matching constant in SchemaVersion.java" + + " whenever you bump xs:schema/@version"); + } + } + + private Path resolveSchemaDir() { + final Path base = Path.of(System.getProperty("user.dir")); + Path p = base.resolve("schema"); + if (!Files.isDirectory(p)) { + p = base.getParent().resolve("schema"); + } + return p; + } +} diff --git a/exist-core/src/test/java/org/exist/util/SchemaVersionTest.java b/exist-core/src/test/java/org/exist/util/SchemaVersionTest.java new file mode 100644 index 00000000000..c908bd967e8 --- /dev/null +++ b/exist-core/src/test/java/org/exist/util/SchemaVersionTest.java @@ -0,0 +1,34 @@ +/* + * 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.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class SchemaVersionTest { + + @Test + public void attributeNameIsSchemaVersion() { + assertEquals("schemaVersion", SchemaVersion.ATTRIBUTE); + } +} diff --git a/exist-distribution/pom.xml b/exist-distribution/pom.xml index 8e4ccf29f7f..37b3267843d 100644 --- a/exist-distribution/pom.xml +++ b/exist-distribution/pom.xml @@ -516,12 +516,21 @@ + + + + + + + + + diff --git a/exist-distribution/src/main/config/collection.xconf.init b/exist-distribution/src/main/config/collection.xconf.init index 8933b884e33..ccef02b7aa2 100644 --- a/exist-distribution/src/main/config/collection.xconf.init +++ b/exist-distribution/src/main/config/collection.xconf.init @@ -1,5 +1,5 @@ - + - + - + - + + + + + + diff --git a/exist-jetty-config/src/main/resources/webapp/WEB-INF/controller-config.xml b/exist-jetty-config/src/main/resources/webapp/WEB-INF/controller-config.xml index cd91242753d..8be7eb767ae 100644 --- a/exist-jetty-config/src/main/resources/webapp/WEB-INF/controller-config.xml +++ b/exist-jetty-config/src/main/resources/webapp/WEB-INF/controller-config.xml @@ -6,7 +6,7 @@ ++ The order of elements within this configuration file is significant. --> + xsi:schemaLocation="http://exist.sourceforge.net/NS/exist file:../../../schema/controller-config.xsd" schemaVersion="1.1.1"> diff --git a/exist-parent/pom.xml b/exist-parent/pom.xml index 570df3da812..4cab338385e 100644 --- a/exist-parent/pom.xml +++ b/exist-parent/pom.xml @@ -137,6 +137,9 @@ 10.5.0 2.1.3 12.5 + 2.12.2 + 1.2.0 + 10k 6.0.23 2.12.0 4.13.2 @@ -229,6 +232,56 @@ ${saxon.version} + + org.exist-db.thirdparty.xerces + xercesImpl + ${xerces.version} + jdk14-xml-schema-1.1 + + + xml-apis + xml-apis + + + + + + org.exist-db.thirdparty.org.eclipse.wst.xml + xpath2 + ${xpath2.version} + + + + edu.princeton.cup + java-cup + ${java-cup.version} + + + + org.xmlresolver + xmlresolver + ${xmlresolver.version} + + + xml-apis + xml-apis + + + + + + org.xmlresolver + xmlresolver + ${xmlresolver.version} + data + + + xml-apis + xml-apis + + + + com.evolvedbinary.j8fu j8fu @@ -912,6 +965,28 @@ Saxon-HE ${saxon.version} + + + org.exist-db.thirdparty.xerces + xercesImpl + ${xerces.version} + jdk14-xml-schema-1.1 + + + org.exist-db.thirdparty.org.eclipse.wst.xml + xpath2 + ${xpath2.version} + + + edu.princeton.cup + java-cup + ${java-cup.version} + + + org.xmlresolver + xmlresolver + ${xmlresolver.version} + diff --git a/pom.xml b/pom.xml index 9566e6abc41..ea267445539 100644 --- a/pom.xml +++ b/pom.xml @@ -67,6 +67,99 @@ true + + + + org.codehaus.mojo + xml-maven-plugin + + + + schema-governance + none + + transform + + + + + ${project.build.directory}/governance + + context.xml + + ${project.basedir}/schema/governance.xsl + ${project.build.directory}/governance + + + .report.xml + + + + + + + + validate-canonical-instances + validate + + validate + + + + + ${project.basedir}/exist-distribution/src/main/config + + conf.xml + + ${project.basedir}/schema/conf.xsd + http://www.w3.org/XML/XMLSchema/v1.1 + + + ${project.basedir}/exist-distribution/src/main/config + + collection.xconf.init + + ${project.basedir}/schema/collection.xconf.xsd + http://www.w3.org/XML/XMLSchema/v1.1 + + + ${project.basedir}/exist-distribution/src/main/config + + descriptor.xml + + ${project.basedir}/schema/descriptor.xsd + http://www.w3.org/XML/XMLSchema/v1.1 + + + ${project.basedir}/exist-jetty-config/src/main/resources/webapp/WEB-INF + + controller-config.xml + + ${project.basedir}/schema/controller-config.xsd + http://www.w3.org/XML/XMLSchema/v1.1 + + + ${project.basedir}/exist-core/src/main/resources/org/exist/util + + mime-types.xml + + ${project.basedir}/schema/mime-types.xsd + http://www.w3.org/XML/XMLSchema/v1.1 + + + + + + diff --git a/schema/README.md b/schema/README.md new file mode 100644 index 00000000000..6fd76e13942 --- /dev/null +++ b/schema/README.md @@ -0,0 +1,55 @@ +# eXist-db native XML schemas + +XSD schemas for eXist-db configuration and descriptor files. + +## Versioning policy + +Each native schema declares an independent semver on ``. Schema version is **not** tied to the eXist-db product release. + +| Change type | Version bump | +|-------------|--------------| +| Breaking change for existing instance documents | **MAJOR** | +| Backward-compatible addition (new optional element/attribute) | **MINOR** | +| Documentation or non-semantics XSD-only change | **PATCH** (optional) | + +CI fails if a schema or canonical template changes without bumping the paired `xs:schema/@version`. + +Canonical templates may declare an optional **`schemaVersion`** attribute on the root element. When present, its value should match the paired `xs:schema/@version` (native schema semver — not the eXist product release, and not expath package `@version`). Legacy documents without the attribute remain valid; runtime code logs a debug message when it is missing and warns when it differs from the version this build expects. + +| Schema `@version` | `schemaVersion` on template | +|-------------------|----------------------------| +| `conf.xsd` | `` | +| `collection.xconf.xsd` | `` | +| `descriptor.xsd` | `` | +| `mime-types.xsd` | `` | +| `controller-config.xsd` | `` | + +All five schemas `xs:include` the `schemaVersionType` simple type from [`schema-version-type.xsd`](schema-version-type.xsd) rather than each declaring their own copy (it has no `targetNamespace`, so it is pulled in as a chameleon component and inherits each includer's namespace). + +[`SchemaVersion.java`](../exist-core/src/main/java/org/exist/util/SchemaVersion.java)'s version constants are generated at build time from `xs:schema/@version` on the paired XSDs (see [`generate-schema-version.xsl`](generate-schema-version.xsl), wired as `exist-core/pom.xml`'s `schema-version-codegen` execution) — never hand-edit them. [`SchemaVersionSyncTest`](../exist-core/src/test/java/org/exist/util/SchemaVersionSyncTest.java) is a tautology now that the constants can't drift by construction, kept as a guard against the codegen wiring itself silently breaking. [`ci-schema-checks.yml`](../.github/workflows/ci-schema-checks.yml) still triggers on edits to `SchemaVersion.java` itself. + +## Validation + +**Templates vs schemas** — root [`pom.xml`](../pom.xml) binds `xml-maven-plugin:validate` at the `validate` phase. This also runs on every full build via [`ci-test.yml`](../.github/workflows/ci-test.yml) (`mvn test` runs `validate` first). + +[`ci-schema-checks.yml`](../.github/workflows/ci-schema-checks.yml) re-runs `mvn validate` on PRs that touch schemas or canonical templates (fast, path-filtered). + +**Version bumps** — a single Saxon XSLT 2.0 transform, [`governance.xsl`](governance.xsl), does the whole check in one pass: it reads the schema/template pairs straight from `pom.xml`'s `validate-canonical-instances` validationSets, reads changed paths and BASE-revision copies of each XSD via [`unparsed-text()`](https://www.w3.org/TR/xpath-functions-30/#func-unparsed-text)/[`document()`](https://www.w3.org/TR/xslt-30/#document)/[`doc-available()`](https://www.w3.org/TR/xpath-functions-30/#func-doc-available), and fails the build directly with `xsl:message terminate="yes"` (which also prints the GitHub Actions `::error::` annotations) when a paired schema/template changed without its `xs:schema/@version` moving. [`.github/scripts/prepare-governance-context.sh`](../.github/scripts/prepare-governance-context.sh) is pure git plumbing — it resolves the diff base, dumps each schema's BASE-revision content to disk, and writes a small `context.xml` — everything else is XSLT, run via `mvn -N xml:transform@schema-governance`. + +That execution is bound to `phase=none` in [`pom.xml`](../pom.xml), so it never runs on an ordinary `mvn install`/`mvn test`/`mvn validate` — only [`ci-schema-checks.yml`](../.github/workflows/ci-schema-checks.yml) invokes it directly, after running the shim script. Saxon-HE (XSLT 2.0/3.0) is already a `xml-maven-plugin` dependency via `exist-parent/pom.xml`'s `pluginManagement` — no extra CI dependency installation needed (no more `xmllint`/`xsltproc`). + +## Canonical templates + +| Schema | Template | +|--------|----------| +| [`conf.xsd`](conf.xsd) | [`exist-distribution/src/main/config/conf.xml`](../exist-distribution/src/main/config/conf.xml) | +| [`collection.xconf.xsd`](collection.xconf.xsd) | [`collection.xconf.init`](../exist-distribution/src/main/config/collection.xconf.init) | +| [`descriptor.xsd`](descriptor.xsd) | [`descriptor.xml`](../exist-distribution/src/main/config/descriptor.xml) | +| [`controller-config.xsd`](controller-config.xsd) | [`controller-config.xml`](../exist-jetty-config/src/main/resources/webapp/WEB-INF/controller-config.xml) | +| [`mime-types.xsd`](mime-types.xsd) | [`mime-types.xml`](../exist-core/src/main/resources/org/exist/util/mime-types.xml) | + +Other schemas (`users.xsd`, `server.xsd`, `expath-pkg.xsd`, …) apply to runtime or package files, not shipped templates. + +## Distribution + +Schemas ship at `$EXIST_HOME/schema/` ([#6189](https://github.com/eXist-db/exist/issues/6189)). diff --git a/schema/collection.xconf.xsd b/schema/collection.xconf.xsd index fcd8eb16ede..8e0cd261f31 100644 --- a/schema/collection.xconf.xsd +++ b/schema/collection.xconf.xsd @@ -6,7 +6,9 @@ xmlns:dcterms="http://purl.org/dc/terms/" elementFormDefault="qualified" targetNamespace="http://exist-db.org/collection-config/1.0" - version="1.0.0"> + version="1.2.1"> + + Schema for eXist-db Collection Configuration files /db/system/config/db/**/collection.xconf @@ -28,6 +30,7 @@ + @@ -267,7 +270,7 @@ Trigger Configuration - + diff --git a/schema/conf.xsd b/schema/conf.xsd index 5b1c0b363d9..fe7b7e65922 100644 --- a/schema/conf.xsd +++ b/schema/conf.xsd @@ -8,8 +8,10 @@ - + version="2.1.1"> + + + @@ -1413,6 +1415,7 @@ + diff --git a/schema/controller-config.xsd b/schema/controller-config.xsd index 7ed505eee78..8bb533c9cae 100644 --- a/schema/controller-config.xsd +++ b/schema/controller-config.xsd @@ -4,13 +4,15 @@ xmlns:exist="http://exist.sourceforge.net/NS/exist" elementFormDefault="qualified" targetNamespace="http://exist.sourceforge.net/NS/exist" - version="1.0.0"> + version="1.1.1"> + + diff --git a/schema/descriptor.xsd b/schema/descriptor.xsd index 30d9432d5b6..f5e160a87f3 100644 --- a/schema/descriptor.xsd +++ b/schema/descriptor.xsd @@ -6,7 +6,8 @@ + version="1.2.1"> + @@ -35,6 +36,8 @@ + + \ No newline at end of file diff --git a/schema/expath-pkg-extensions/cxan.xsd b/schema/expath-pkg-extensions/cxan.xsd index dc2864d613c..613b09d961e 100644 --- a/schema/expath-pkg-extensions/cxan.xsd +++ b/schema/expath-pkg-extensions/cxan.xsd @@ -5,7 +5,8 @@ xmlns:cxan="http://cxan.org/ns/package" xmlns:dcterms="http://purl.org/dc/terms/" elementFormDefault="qualified" - targetNamespace="http://cxan.org/ns/package"> + targetNamespace="http://cxan.org/ns/package" + version="1.0.0"> A schema for the EXPath Packaging CXAN concept. diff --git a/schema/expath-pkg-extensions/exist.xsd b/schema/expath-pkg-extensions/exist.xsd index b72e130e3d3..eb53fa7f4d7 100644 --- a/schema/expath-pkg-extensions/exist.xsd +++ b/schema/expath-pkg-extensions/exist.xsd @@ -5,7 +5,8 @@ xmlns:eepkg="http://exist-db.org/ns/expath-pkg" xmlns:dcterms="http://purl.org/dc/terms/" elementFormDefault="qualified" - targetNamespace="http://exist-db.org/ns/expath-pkg"> + targetNamespace="http://exist-db.org/ns/expath-pkg" + version="1.0.0"> A schema for eXist-db extensions to EXPath Packaging. diff --git a/schema/expath-pkg-extensions/repo.xsd b/schema/expath-pkg-extensions/repo.xsd index 14ee73383a7..c5000cd65cf 100644 --- a/schema/expath-pkg-extensions/repo.xsd +++ b/schema/expath-pkg-extensions/repo.xsd @@ -5,7 +5,8 @@ xmlns:repo="http://exist-db.org/xquery/repo" xmlns:dcterms="http://purl.org/dc/terms/" elementFormDefault="qualified" - targetNamespace="http://exist-db.org/xquery/repo"> + targetNamespace="http://exist-db.org/xquery/repo" + version="1.0.0"> A schema for eXist-db Package Repository extensions to EXPath Packaging. diff --git a/schema/expath-pkg.xsd b/schema/expath-pkg.xsd index c62f50cc315..a0248bd47ee 100644 --- a/schema/expath-pkg.xsd +++ b/schema/expath-pkg.xsd @@ -5,7 +5,8 @@ xmlns:pkg="http://expath.org/ns/pkg" xmlns:dcterms="http://purl.org/dc/terms/" elementFormDefault="qualified" - targetNamespace="http://expath.org/ns/pkg"> + targetNamespace="http://expath.org/ns/pkg" + version="1.1.0"> A schema for EXPath Packaging (i.e. expath-pkg.xml) file as per the EXPath Packaging System - Candidate Module 9 May 2012' specification. @@ -41,7 +42,7 @@ The name of the package. A package is named using an IRI, as defined by [RFC 3987], excepted any IRI using the file: scheme (most frequent choices are http: and urn: scheme URIs). Note that the definition of IRI excludes relative references. - + diff --git a/schema/governance.xsl b/schema/governance.xsl new file mode 100644 index 00000000000..f1648fbb6f8 --- /dev/null +++ b/schema/governance.xsl @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + governance.xsl: no xml-maven-plugin execution with + id='validate-canonical-instances' found in — the execution id + has drifted, or $pom-uri is wrong. Schema governance cannot run blind. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Schema governance failed ( error(s) above). + + + + diff --git a/schema/mime-types.xsd b/schema/mime-types.xsd index 7c1b78b18eb..be05282158b 100644 --- a/schema/mime-types.xsd +++ b/schema/mime-types.xsd @@ -6,7 +6,8 @@ + version="1.2.1"> + @@ -17,7 +18,7 @@ - + @@ -55,6 +56,7 @@ + \ No newline at end of file diff --git a/schema/schema-version-type.xsd b/schema/schema-version-type.xsd new file mode 100644 index 00000000000..ef64c96a97b --- /dev/null +++ b/schema/schema-version-type.xsd @@ -0,0 +1,22 @@ + + + + + + + + + Native XSD semver; mirrors the paired schema's xs:schema/@version (not eXist product version). See schema/README.md. + + + + + + +