Skip to content
Closed
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 \
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'
- '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
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
63 changes: 58 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 Expand Up @@ -1087,6 +1082,64 @@ The BaseX Team. The original license statement is also included below.]]></pream
</executions>
</plugin>

<!--
Generates org/exist/util/GeneratedSchemaVersions.java from each canonical XSD's
xs:schema/@version (see ../schema/generate-schema-version.xsl), runs on every
normal build. Unlike ../schema/governance.xsl (phase=none, CI-invoked-only), this
means SchemaVersion.java's version constants can never silently drift from
schema/*.xsd.
-->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>xml-maven-plugin</artifactId>
<executions>
<execution>
<id>schema-version-codegen</id>
<phase>generate-sources</phase>
<goals>
<goal>transform</goal>
</goals>
<configuration>
<forceCreation>true</forceCreation>
<transformationSets>
<transformationSet>
<dir>${project.basedir}/../schema</dir>
<includes>
<include>GeneratedSchemaVersions.xml</include>
</includes>
<stylesheet>${project.basedir}/../schema/generate-schema-version.xsl</stylesheet>
<outputDir>${project.build.directory}/generated-sources/schema-version/org/exist/util</outputDir>
<fileMappers>
<fileMapper implementation="org.codehaus.plexus.components.io.filemappers.FileExtensionMapper">
<targetExtension>.java</targetExtension>
</fileMapper>
</fileMappers>
</transformationSet>
</transformationSets>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<id>add-schema-version-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>${project.build.directory}/generated-sources/schema-version</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
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);
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
Loading
Loading