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
53 changes: 53 additions & 0 deletions .github/scripts/prepare-governance-context.sh
Original file line number Diff line number Diff line change
@@ -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/<name> 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 \
Comment thread
line-o marked this conversation as resolved.
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
<?xml version="1.0" encoding="UTF-8"?>
<g:ctx xmlns:g="http://exist-db.org/ns/schema-governance"
workspace="${WORKSPACE}"
base-dir="${OUT}/base"
base-ref="${BASE}"
changed-file="${OUT}/changed.txt"/>
EOF

echo "Governance context: ${OUT}/context.xml (base ${BASE})"
60 changes: 60 additions & 0 deletions .github/workflows/ci-schema-checks.yml
Original file line number Diff line number Diff line change
@@ -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'
Comment thread
line-o marked this conversation as resolved.
- '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'
Comment thread
line-o marked this conversation as resolved.
- '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
24 changes: 24 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
5 changes: 0 additions & 5 deletions exist-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,6 @@
<dependency>
<groupId>org.exist-db.thirdparty.xerces</groupId>
<artifactId>xercesImpl</artifactId>
<version>2.12.2</version>
<classifier>jdk14-xml-schema-1.1</classifier>
<exclusions>
<exclusion> <!-- conflicts with Java 17's javax.xml module -->
Expand Down Expand Up @@ -348,7 +347,6 @@
<dependency>
<groupId>org.xmlresolver</groupId>
<artifactId>xmlresolver</artifactId>
<version>${xmlresolver.version}</version>
<exclusions>
<exclusion> <!-- conflicts with Java 17's javax.xml module -->
<groupId>xml-apis</groupId>
Expand All @@ -360,7 +358,6 @@
<dependency>
<groupId>org.xmlresolver</groupId>
<artifactId>xmlresolver</artifactId>
<version>${xmlresolver.version}</version>
<classifier>data</classifier>
<scope>runtime</scope>
<exclusions>
Expand All @@ -376,13 +373,11 @@
<dependency>
<groupId>org.exist-db.thirdparty.org.eclipse.wst.xml</groupId>
<artifactId>xpath2</artifactId>
<version>1.2.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>edu.princeton.cup</groupId>
<artifactId>java-cup</artifactId>
<version>10k</version>
<scope>runtime</scope>
</dependency>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions exist-core/src/main/java/org/exist/http/Descriptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -138,6 +139,8 @@ private Descriptor() {

final Document doc = adapter.getDocument();

SchemaVersion.logDocumentVersion(LOG, doc.getDocumentElement(), SchemaVersion.DESCRIPTOR, "descriptor.xml");

//load <xquery-app> attribue settings
if ("true".equals(doc.getDocumentElement().getAttribute("request-replay-log"))) {
final Path logFile = Path.of("request-replay-log.txt");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
3 changes: 3 additions & 0 deletions exist-core/src/main/java/org/exist/util/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,9 @@ public Configuration(@Nullable String configFilename, Optional<Path> 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
Expand Down
25 changes: 17 additions & 8 deletions exist-core/src/main/java/org/exist/util/MimeTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment thread
line-o marked this conversation as resolved.
this.src = sourceDescription;
} catch (final ParserConfigurationException | SAXException | IOException e) {
throw new IllegalStateException(FILE_LOAD_FAILED_ERR + path.toAbsolutePath(), e);
}
Expand Down Expand Up @@ -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);
Expand All @@ -275,25 +276,26 @@ 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();

reader.setFeature("http://xml.org/sax/features/external-general-entities", false);
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 {
Expand All @@ -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");
Expand Down
69 changes: 69 additions & 0 deletions exist-core/src/main/java/org/exist/util/SchemaVersion.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
and then as a classpath resource in org/exist/util .
=======================================================
-->
<mime-types xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="schema/mime-types.xsd">
<mime-types xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="schema/mime-types.xsd" schemaVersion="1.2.1">

<!-- - - Mime types stored as XML - - - - - - - - - -->

Expand Down
Loading
Loading