A Gradle plugin providing Saxon-backed XSLT/XQuery transforms and SVRL-based XML validation tasks with an orthogonal, Gradle-style DSL.
Define and execute XPath/XSLT/XQuery transformations and XML validations as Gradle tasks with:
- File-tree input matching (include/exclude patterns)
- Explicit single-file mode via Ant-like
input(...)/output(...) - Output file generation with configurable extension mapping
- External parameter passing to transforms
- Optional parallel processing using virtual threads
- SVRL and optional JUnit XML reporting for validation
The plugin contributes four task types:
name.jurgenei.gradle.xml.XsltTask— XSLT 3.0 transformationsname.jurgenei.gradle.xml.XQueryTask— XQuery transformationsname.jurgenei.gradle.xml.SchematronTask— Schematron to SVRL validationname.jurgenei.gradle.xml.XsdTask— XSD validation normalized to SVRLname.jurgenei.gradle.xml.SchematronBootstrapTask— bootstrap Schematron from XSDname.jurgenei.gradle.xml.SchematronObservationCompileTask— compileobs:*annotated Schematron into grouped observation stylesheet skeletonname.jurgenei.gradle.xml.SchematronExtractTask— execute runtime observation extraction and emit grouped observation XML
Both share a near-orthogonal API for unified Gradle-style configuration.
- Saxon HE XSLT 3.0 and XQuery execution
- Schematron validation via SchXslt2 transpiler (
name.dmaus.schxslt:schxslt2) - XSD validation with AUTO engine resolution (Saxon PE/EE when available, JAXP fallback on HE)
- Orthogonal task API — both task types inherit the same base configuration
- File-tree DSL — Ant-like include/exclude filtering via Gradle's native
fileTree - Single-file DSL — explicit one-to-one transforms via
input(...)andoutput(...) - Flexible output mapping — custom extension and output directory per task
- Parameter passing — externalize stylesheet/query variables
- Virtual-thread parallelism — optional worker pool for concurrent file processing (default: serial)
- Comprehensive testing — JUnit 4 integration tests with mirrored XSLT/XQuery scenarios
- Security automation — CodeQL, OWASP Dependency-Check, SpotBugs + FindSecBugs, Dependabot
- S-expression I/O —
.sexprinput and output routing for XSLT/XQuery tasks - Canonical JSON I/O — optional
.jsoninput/output routing with reversible element mapping
S-expression support provides a compact, human- and AI-friendly representation of XML and XDM-based technologies. Rather than introducing new semantics, it offers an alternative serialization syntax for established standards such as XML, XDM, XPath, XSLT, and XML Schema.
By reducing serialization overhead while preserving structure, typing, and validation capabilities, S-expressions make it easier to work with existing XML assets in modern development and AI workflows. All processing continues to rely on the same mature standards and implementations that have evolved within the XML ecosystem for more than two decades.
XsltTask and XQueryTask support .sexpr files in file-tree mode and explicit mode.
S-expression runtime ships inside gradle-xml-plugin artifact.
-
Internal package:
name.jurgenei.gradle.xml.sexpr -
No separate
name.jurgenei.xml:xml-sexprdependency required -
S-expression parser/serializer runtime is Saxon-agnostic (
java.xmlSAX/JAXP APIs) -
Input
.sexpris parsed as SAX source. -
XSLT stylesheet may also be
.sexpr(forXsltTask.style(...)). -
Output
.sexpris serialized from XML result events through SAX/JAXP pipeline. -
Saxon URI dereferencing routes
.sexprresources through the same SAX parser path fordoc()andcollection()calls. -
sexprFormatcontrols output style:compact(default) orbeautified.
S-expression format details:
()= nodes{}= associative structures[]= sequences.= document node head?= processing instruction head!= comment node head
Canonical examples:
- Element node:
(book (title "XML")) - Element associative block (attributes + namespaces):
(book { id "b1" xmlns:m "urn:math" } (m:title "XML")) - Document with XML declaration map:
(. { version "1.0" encoding "UTF-8" } (book)) - Map node:
(xdm:map { name "John" age 42 }) - Array node:
(xdm:array [ "A" "B" "C" ]) - Typed atomics:
(xs:boolean true),(xs:date "2026-09-06") - Comment:
(! "text") - Processing instruction:
(?xml-stylesheet { href "main.xsl" type "text/xsl" })
Disambiguation:
(map ...)and(array ...)are XML elements namedmap/array.- XDM map/array nodes use explicit heads:
xdm:mapandxdm:array.
Serializer compatibility modes:
- Canonical mode (default): canonical token classes and forms shown above
- Legacy mode: retained for compatibility output only
Internal bridge note:
- SAX cannot represent XDM map/array/typed-atomic/xml-declaration directly.
- Runtime uses internal
xdm:*helper elements in URIurn:name.jurgenei.gradle.xml:xdmas lossless bridge between parser and serializer.
sexprFormat is also reused for canonical JSON output formatting.
Format conventions:
; compact
(book {id "b1"} (title "XML"))
; beautified
(book
{id "b1"}
(title "XML"))tasks.register('xmlToSexpr', name.jurgenei.gradle.xml.XsltTask) {
style 'src/main/xslt/identity.xsl'
source 'src/main/xml/input.xml'
outputDir.set(layout.buildDirectory.dir('out/xslt'))
outputExtension.set('.sexpr')
}
tasks.register('sexprToXml', name.jurgenei.gradle.xml.XsltTask) {
style 'src/main/xslt/identity.xsl'
input 'build/out/xslt/input.sexpr'
output 'build/out/xml/result.xml'
}XsltTask and XQueryTask support optional canonical JSON parsing/serialization.
- Canonical JSON maps XML element trees to JSON objects with
type,name,attributes,children. - Canonical JSON mode is reversible for XML -> JSON -> XML roundtrips.
sexprFormatcontrols canonical JSON output style too:compactorbeautified.
Set JSON routing mode with jsonMode:
auto(default): canonical parser for.jsoninput; for.jsonoutput, try canonical hierarchical JSON first and fall back to native Saxon JSON when canonical serialization is not applicable (for example map/array results)native: no canonical JSON parser for input; for.jsonoutput, same canonical-first behavior with native fallbackcanonical: canonical parser + canonical serializer for.jsoninput/output (no fallback)
tasks.register('xmlToJsonCanonical', name.jurgenei.gradle.xml.XsltTask) {
style 'src/main/xslt/identity.xsl'
source 'src/main/xml/input.xml'
outputDir.set(layout.buildDirectory.dir('out/json'))
outputExtension.set('.json')
jsonMode.set('canonical')
sexprFormat.set('beautified')
}
tasks.register('jsonCanonicalToXml', name.jurgenei.gradle.xml.XsltTask) {
style 'src/main/xslt/identity.xsl'
input 'build/out/json/input.json'
output 'build/out/xml/result.xml'
jsonMode.set('canonical')
}Kotlin DSL:
tasks.register<name.jurgenei.gradle.xml.XsltTask>("xmlToSexpr") {
style("src/main/xslt/identity.xsl")
source("src/main/xml/input.xml")
outputDir.set(layout.buildDirectory.dir("out/xslt"))
outputExtension.set(".sexpr")
sexprFormat.set("beautified")
}Groovy DSL:
tasks.register('xmlToSexpr', name.jurgenei.gradle.xml.XsltTask) {
style 'src/main/xslt/identity.xsl'
source 'src/main/xml/input.xml'
outputDir.set(layout.buildDirectory.dir('out/xslt'))
outputExtension.set('.sexpr')
sexprFormat.set('beautified')
}XsltTask and XQueryTask support two equivalent execution modes:
- File-tree mode: set
source(...)andoutputDir - Explicit single-file mode: set
input(...)andoutput(...)
Notes:
- In explicit mode,
input(...)andoutput(...)must be set together. - In file-tree mode,
outputDiris required. - Both modes support
param(...); file-tree mode additionally supportsworkersand extension-based mapping.
Validation tasks share a common contract (ValidationTaskSpec) and defaults:
outputExtension = '.svrl.xml'workers = 1reportFormat = SVRLfailOnError = truejunitOutputDir = build/reports/xml-validation/junit
ReportFormat values:
SVRLJUNITSVRL_AND_JUNIT
XsdTask supports XsdEngine values:
AUTO(default; prefers Saxon schema-aware, otherwise JAXP)SAXONJAXP
- Supported plugin ID:
name.jurgenei.gradle.xml - Maven artifact for legacy
buildscriptusage:name.jurgenei.gradle:gradle-xml-transform:<version> - Obsolete/legacy IDs from earlier docs are no longer supported.
Add to build.gradle.kts:
plugins {
id("name.jurgenei.gradle.xml")
}Or build.gradle:
plugins {
id 'name.jurgenei.gradle.xml'
}Legacy buildscript usage:
buildscript {
repositories {
mavenCentral()
gradlePluginPortal()
}
dependencies {
classpath("name.jurgenei.gradle:gradle-xml-transform:0.1.1")
}
}
apply(plugin = "name.jurgenei.gradle.xml")plugins {
id("name.jurgenei.gradle.xml")
}
tasks.register<name.jurgenei.gradle.xml.XsltTask>("transformDocs") {
style("src/main/xslt/main.xsl")
source(fileTree("src/main/xml") {
include("**/*.xml")
exclude("**/legacy/**")
})
outputDir.set(layout.buildDirectory.dir("generated/xslt"))
outputExtension.set(".html")
workers.set(4)
param("env", "dev")
}
tasks.register<name.jurgenei.gradle.xml.XQueryTask>("queryDocs") {
query("src/main/xquery/main.xq")
source("src/main/xml/single.xml")
outputDir.set(layout.buildDirectory.dir("generated/xquery"))
outputExtension.set(".xml")
workers.set(1)
param("tenant", "acme")
}
tasks.register<name.jurgenei.gradle.xml.XsltTask>("transformOne") {
style("src/main/xslt/main.xsl")
input("src/main/xml/a.xml")
output("build/custom/b.xml")
}
tasks.register<name.jurgenei.gradle.xml.XQueryTask>("queryOne") {
query("src/main/xquery/main.xq")
input("src/main/xml/a.xml")
output("build/custom/b.xml")
}plugins {
id 'name.jurgenei.gradle.xml'
}
tasks.register('transformDocs', name.jurgenei.gradle.xml.XsltTask) {
style 'src/main/xslt/main.xsl'
source(fileTree('src/main/xml') {
include '**/*.xml'
exclude '**/legacy/**'
})
outputDir.set(layout.buildDirectory.dir('generated/xslt'))
outputExtension.set('.html')
workers.set(4)
param 'env', 'dev'
}
tasks.register('queryDocs', name.jurgenei.gradle.xml.XQueryTask) {
query 'src/main/xquery/main.xq'
source 'src/main/xml/single.xml'
outputDir.set(layout.buildDirectory.dir('generated/xquery'))
outputExtension.set('.xml')
workers.set(1)
param 'tenant', 'acme'
}
tasks.register('transformOne', name.jurgenei.gradle.xml.XsltTask) {
style 'src/main/xslt/main.xsl'
input 'src/main/xml/a.xml'
output 'build/custom/b.xml'
}
tasks.register('queryOne', name.jurgenei.gradle.xml.XQueryTask) {
query 'src/main/xquery/main.xq'
input 'src/main/xml/a.xml'
output 'build/custom/b.xml'
}tasks.register('validateSchematron', name.jurgenei.gradle.xml.SchematronTask) {
schema 'src/main/schematron/rules.sch'
// Optional persistent compiled stylesheet cache.
style 'build/generated/schematron/rules.compiled.xsl'
source(fileTree('src/main/xml') { include '**/*.xml' })
outputDir.set(layout.buildDirectory.dir('reports/schematron'))
reportFormat.set(name.jurgenei.gradle.xml.validation.ReportFormat.SVRL_AND_JUNIT)
// Optional SchXslt transpiler parameters.
phase.set('#ALL')
severityThreshold.set('warning')
workers.set(4)
failOnError.set(false)
}
tasks.register('validateXsd', name.jurgenei.gradle.xml.XsdTask) {
schema 'src/main/xsd/schema.xsd'
source(fileTree('src/main/xml') { include '**/*.xml' })
outputDir.set(layout.buildDirectory.dir('reports/xsd'))
reportFormat.set(name.jurgenei.gradle.xml.validation.ReportFormat.SVRL_AND_JUNIT)
engine.set(name.jurgenei.gradle.xml.validation.XsdEngine.AUTO)
}Schematron-specific options:
style(...)/style.set(...)(optional): persistent location for compiled Schematron XSLT.- When unset, a temp compiled stylesheet is used per validation run.
- When set, recompilation is skipped if the compiled stylesheet is newer than inputs and transpiler parameters are unchanged.
transpilerStylesheet(...)(optional): override bundled SchXslt transpiler.- Optional SchXslt transpiler parameter properties (only passed when explicitly set):
debug,phase,expandText,streamable,locationFunction,failEarlyterminateValidationOnError,reportActivePattern,reportFiredRule,reportSuppressedRulereportSkippedAssertion,compactReport,severityThreshold,defaultSeverity,defaultFromcheckAssembledSchema,handleDynamicErrors
Use SchematronBootstrapTask to create an initial observation Schematron from an XSD.
The generated file is comprehensive (captures required children/attributes as observations)
but intentionally passing (bootstrap-safe) until you tighten rules manually.
Safety behavior:
- If output
.schalready exists, bootstrap does not overwrite it. - The task logs a lifecycle warning and exits.
Cross-plugin workflow (OOXML + XML plugins):
plugins {
id 'name.jurgenei.gradle.ooxml'
id 'name.jurgenei.gradle.xml'
}
tasks.register('bootstrapCanonicalSchematron', name.jurgenei.gradle.xml.SchematronBootstrapTask) {
def ooxmlExt = project.extensions.getByType(name.jurgenei.gradle.ooxml.OoXmlExtension)
schemaUrl(ooxmlExt.canonicalSchemaUrl.get())
output 'src/main/schematron/canonical-observation.sch'
}
tasks.register('copyCanonicalXsd') {
doLast {
def ooxmlExt = project.extensions.getByType(name.jurgenei.gradle.ooxml.OoXmlExtension)
def target = file('src/main/xsd/canonical.local.xsd')
if (!target.exists()) {
target.parentFile.mkdirs()
target.text = new URL(ooxmlExt.canonicalSchemaUrl.get()).getText('UTF-8')
}
}
}
tasks.register('bootstrapFromLocalXsd', name.jurgenei.gradle.xml.SchematronBootstrapTask) {
dependsOn tasks.named('copyCanonicalXsd')
schemaFile.set(layout.projectDirectory.file('src/main/xsd/canonical.local.xsd'))
output 'src/main/schematron/canonical-local.sch'
}
tasks.register('validateCanonicalSchematron', name.jurgenei.gradle.xml.SchematronTask) {
dependsOn tasks.named('bootstrapCanonicalSchematron')
schema.set(layout.projectDirectory.file('src/main/schematron/canonical-observation.sch'))
source 'src/main/xml/canonical.xml'
outputDir.set(layout.buildDirectory.dir('reports/schematron'))
}SchematronObservationCompileTask compiles obs:* rule metadata into an extraction stylesheet skeleton
with grouped xsl:result-document outputs.
tasks.register('compileObservation', name.jurgenei.gradle.xml.SchematronObservationCompileTask) {
schema 'src/main/schematron/observations.sch'
output 'build/generated/observation/observations.xsl'
groupOutput 'knowledge', 'observations/knowledge.xml'
groupOutput 'terminology', 'observations/terminology.xml'
groupOutput 'architecture', 'observations/architecture.xml'
}SchematronExtractTask executes observation extraction against canonical XML and emits grouped outputs.
It can either:
- compile extraction style on the fly from
schema, or - consume a precompiled style via
style.
tasks.register('extractObservations', name.jurgenei.gradle.xml.SchematronExtractTask) {
schema 'src/main/schematron/observations.sch'
// Optional if precompiled by SchematronObservationCompileTask:
// style 'build/generated/observation/observations.xsl'
source(fileTree('src/main/xml') { include '**/*.xml' })
outputDir.set(layout.buildDirectory.dir('reports/observations'))
groupOutput 'knowledge', 'observations/knowledge.xml'
groupOutput 'terminology', 'observations/terminology.xml'
groupOutput 'architecture', 'observations/architecture.xml'
failOnError.set(true)
}./gradlew testGenerate coverage report and enforce the current minimum line coverage baseline (>= 0%):
./gradlew coverageCoverage report outputs:
- XML:
build/reports/jacoco/test/jacocoTestReport.xml - HTML:
build/reports/jacoco/test/html/index.html
CI coverage workflow: .github/workflows/coverage.yml
To enable Codecov upload/badge, add repository secret CODECOV_TOKEN.
Security automation runs in GitHub Actions:
- CodeQL static analysis:
.github/workflows/codeql.yml - OWASP Dependency-Check:
.github/workflows/dependency-check.yml - SpotBugs + FindSecBugs:
.github/workflows/spotbugs-security.yml - Dependabot updates:
.github/dependabot.yml
Set repository secret NVD_API_KEY for faster/more reliable Dependency-Check NVD lookups.
Run locally:
./gradlew dependencyCheckAnalyze --no-configuration-cache
./gradlew spotbugsMain -PspotbugsIgnoreFailures=false --no-configuration-cache
./gradlew allSecurityChecks./gradlew buildRequired Java version: 21+
AbstractXmlTransformTask (shared base)
├── XsltTask (XSLT transformations)
└── XQueryTask (XQuery transformations)
AbstractXmlValidationTask (shared base)
├── SchematronTask (Schematron validation)
└── XsdTask (XSD validation)
- Resolve input files from
source/fileset - Sort files deterministically
- Optionally parallelize using virtual-thread worker pool (if
workers > 1) - For each input file:
- Skip when output is newer than transform dependencies (source + style/query/schema)
- Derive output file path using
outputExtensionmapping - Create output directories (thread-safe via
Files.createDirectories) - Compile and execute transform (XSLT or XQuery)
- Log success or collect failure
workers = 1(default): Sequential processingworkers > 1: Fixed virtual-thread pool with concurrent file processing
Virtual threads are used to maximize throughput with minimal memory overhead for I/O-bound XML transformations.
Runnable minimal examples are available under samples/:
samples/xslt-basicsamples/s-xslt-sexpr-identitysamples/xquery-basicsamples/s-xquery-sexpr-identitysamples/s-xsdsamples/s-schematronsamples/validation-basic
See samples/README.md for run commands.
JUnit 4 with Gradle TestKit for functional integration testing:
./gradlew test --tests '*XsltTaskIntegrationTest'
./gradlew test --tests '*XQueryTaskIntegrationTest'
./gradlew test --tests '*SchematronTaskIntegrationTest'
./gradlew test --tests '*XsdTaskIntegrationTest'
./gradlew test --tests '*SchematronBootstrapTaskIntegrationTest'
./gradlew test --tests '*SchematronObservationCompileTaskIntegrationTest'
./gradlew test --tests '*SchematronExtractTaskIntegrationTest'- Java 21+ source
- Javadoc on all public APIs and classes
- Text blocks for multiline strings (Java 15+)
Contribution workflow and coding expectations are documented in CONTRIBUTING.md.