diff --git a/README.md b/README.md index b7670cd..cda6edd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Gradle XML Transform Plugin +# Gradle XML Validate and Transform Plugin ![Conformance](https://img.shields.io/badge/Conformance-Check--All%20Passing-brightgreen) @@ -16,702 +16,440 @@ [![Java](https://img.shields.io/badge/java-21+-green.svg)](https://www.oracle.com/java/) [![Gradle](https://img.shields.io/badge/gradle-9.5+-blue.svg)](https://gradle.org/) -A Gradle plugin providing **Saxon**-backed XSLT/XQuery transforms and SVRL-based XML validation tasks with an orthogonal, Gradle-style DSL. +Plugin gives Gradle-native tasks for XML transformations and validations with Saxon-backed execution. +It supports XML, XIR, and canonical JSON routing, with one orthogonal DSL across task types. -## Overview +## Introduction and Overview -Define and execute XPath/XSLT/XQuery transformations and XML validations as Gradle tasks with: +Use this plugin when project needs repeatable XML processing in build pipeline: -- 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 +- Transform XML with XSLT or XQuery +- Validate XML with XSD or Schematron +- Bootstrap Schematron from XSD +- Compile and run SEL/SHACL extraction flows +- Compile and run XSpec test suites -The plugin contributes task types: +Main task types: -- `name.jurgenei.gradle.xml.XsltTask` — XSLT 3.0 transformations -- `name.jurgenei.gradle.xml.XQueryTask` — XQuery transformations -- `name.jurgenei.gradle.xml.SchematronTask` — Schematron to SVRL validation -- `name.jurgenei.gradle.xml.XsdTask` — XSD validation normalized to SVRL -- `name.jurgenei.gradle.xml.SchematronBootstrapTask` — bootstrap Schematron from XSD -- `name.jurgenei.gradle.xml.SchematronSelCompileTask` — compile `sel:*` annotated Schematron into grouped SEL stylesheet skeleton -- `name.jurgenei.gradle.xml.SchematronSelExtractTask` — execute runtime SEL extraction and emit grouped SEL XML -- `name.jurgenei.gradle.xml.ShaclSelCompileTask` — compile SHACL relation shapes into grouped SEL stylesheet skeleton (+ generated Schematron bridge) -- `name.jurgenei.gradle.xml.ShaclSelExtractTask` — execute runtime SEL extraction from SHACL-compiled stylesheet -- `name.jurgenei.gradle.xml.XSpecCompileTask` — compile XSLT XSpec (`.xspec`) into executable runner stylesheet -- `name.jurgenei.gradle.xml.XSpecTask` — run XSpec (from `.xspec` or precompiled runner) and emit XML + JUnit reports +- `name.jurgenei.gradle.xml.XsltTask` +- `name.jurgenei.gradle.xml.XQueryTask` +- `name.jurgenei.gradle.xml.SchematronTask` +- `name.jurgenei.gradle.xml.XsdTask` +- `name.jurgenei.gradle.xml.SchematronBootstrapTask` +- `name.jurgenei.gradle.xml.SchematronSelCompileTask` +- `name.jurgenei.gradle.xml.SchematronSelExtractTask` +- `name.jurgenei.gradle.xml.ShaclSelCompileTask` +- `name.jurgenei.gradle.xml.ShaclSelExtractTask` +- `name.jurgenei.gradle.xml.XSpecCompileTask` +- `name.jurgenei.gradle.xml.XSpecTask` -Both share a near-orthogonal API for unified Gradle-style configuration. +Plugin ID: `name.jurgenei.gradle.xml` + +```kotlin +plugins { + id("name.jurgenei.gradle.xml") +} +``` ## Features -- **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(...)` and `output(...)` -- **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 -- **XIR I/O** — `.xir` input and output routing for XSLT/XQuery tasks -- **Canonical JSON I/O** — optional `.json` input/output routing with reversible element mapping -- **Native XSpec support (XSLT v1)** — two-phase compile/run with XML + JUnit reporting +- Saxon HE execution for XSLT and XQuery +- Validation outputs normalized to SVRL (+ optional JUnit for validation/XSpec) +- File-tree mode and explicit single-file mode +- Parallel workers with virtual threads +- Parameter passing via `param(name, value)` +- XIR input/output support +- Canonical JSON input/output support +- Orthogonal output naming: + - `outputFormat` (`compact`, `beautified`) + - `outputMode` (`auto`, `native`, `canonical`) +- Validation format naming unchanged: + - `format(...)` / `reportFormat` (`SVRL`, `JUNIT`, `SVRL_AND_JUNIT`) ## XIR Support -XIR 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. +XIR is alternate syntax for XML/XDM structures, not new data model. +Think: same information as XML, different representation optimized for compactness and readability in diffs and generated artifacts. -By reducing serialization overhead while preserving structure, typing, and validation capabilities, XIR makes 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. +### Why XIR -`XsltTask` and `XQueryTask` support `.xir` files in file-tree mode and explicit mode. +- Less punctuation noise vs XML +- Easier nested-structure scanning in code review +- Roundtrip-friendly with existing XML tooling path +- Works with same transformation and validation pipeline -S-expression runtime ships inside `gradle-xml-plugin` artifact. +### XIR syntax relation to XML and XDM -- Internal package: `name.jurgenei.gradle.xml.xir` -- No separate `name.jurgenei.xml:xml-xir` dependency required -- S-expression parser/serializer runtime is Saxon-agnostic (`java.xml` SAX/JAXP APIs) +| XIR token | Meaning | XML/XDM equivalent | +|---|---|---| +| `(...)` | node expression | element/document node structure | +| `{...}` | associative payload | attributes + namespace bindings, or XDM map value | +| `[...]` | sequence payload | XDM sequence / array value | +| `.` | document node head | XML document root wrapper | +| `?` | PI node head | processing instruction | +| `!` | comment node head | XML comment | -- Input `.xir` is parsed as SAX source. -- XSLT stylesheet may also be `.xir` (for `XsltTask.style(...)`). -- Output `.xir` is serialized from XML result events through SAX/JAXP pipeline. -- Saxon URI dereferencing routes `.xir` resources through the same SAX parser path for `doc()` and `collection()` calls. -- `xirFormat` controls output style: `compact` (default) or `beautified`. +Disambiguation: -XIR format details: +- `{...}` denotes XDM map value. +- `[...]` denotes XDM sequence/array value. +- `(map ...)` and `(array ...)` are XML elements unless mapped from XPath Functions XML namespace bridge. +- If first child of element is map value, explicit empty attribute block can disambiguate: + - `(element {} { key value })` -- `()` = nodes -- `{}` = associative structures -- `[]` = sequences -- `.` = document node head -- `?` = processing instruction head -- `!` = comment node head +
+XML and XIR side-by-side example -Canonical examples: +```xml + + + XML + +``` -- 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" })` +```lisp +(. { version "1.0" encoding "UTF-8" } + (book { id "b1" xmlns:m "urn:math" } + (title "XML"))) +``` +
-Disambiguation: +Runtime note: -- `(map ...)` and `(array ...)` are XML elements named `map`/`array`. -- XDM map/array nodes use explicit heads: `xdm:map` and `xdm:array`. +- XIR parser/serializer ships inside plugin artifact. +- No extra runtime dependency needed. -Serializer compatibility modes: +## Transformations -- Canonical mode (default): canonical token classes and forms shown above -- Legacy mode: retained for compatibility output only +Transformations use `XsltTask` and `XQueryTask`. -Internal bridge note: +### Ways to work with files -- SAX cannot represent XDM map/array/typed-atomic/xml-declaration directly. -- Runtime uses internal `xdm:*` helper elements in URI `urn:name.jurgenei.gradle.xml:xdm` as lossless bridge between parser and serializer. +1. File-tree mode: `source(...)` + `outputDir` +2. Explicit mode: `input(...)` + `output(...)` -`xirFormat` is also reused for canonical JSON output formatting. +### Common transformation options -Format conventions: +- `outputExtension` +- `outputMethod` (`xml`, `json`, `text`) +- `outputFormat` (`compact`, `beautified`) for XIR/canonical JSON style +- `outputMode` (`auto`, `native`, `canonical`) for JSON routing +- `workers` +- `failOnError` +- `param(name, value)` + +### XSLT transformation + +
+Input/output example in XML and XIR + +```xml +XML +``` ```lisp -; compact (book {id "b1"} (title "XML")) - -; beautified -(book - {id "b1"} - (title "XML")) ``` +
-### XSLT Example +
+Groovy Task Def (XSLT) ```groovy tasks.register('xmlToXir', name.jurgenei.gradle.xml.XsltTask) { style 'src/main/xslt/identity.xsl' - source 'src/main/xml/input.xml' + source(fileTree('src/main/xml') { include '**/*.xml' }) outputDir.set(layout.buildDirectory.dir('out/xslt')) outputExtension.set('.xir') + outputFormat.set('beautified') + outputMode('auto') + workers.set(4) + param 'tenant', 'acme' } +``` +
-tasks.register('xirToXml', name.jurgenei.gradle.xml.XsltTask) { - style 'src/main/xslt/identity.xsl' - input 'build/out/xslt/input.xir' - output 'build/out/xml/result.xml' +
+Kotlin Task Def (XSLT) + +```kotlin +tasks.register("xmlToXir") { + style("src/main/xslt/identity.xsl") + source(fileTree("src/main/xml") { include("**/*.xml") }) + outputDir.set(layout.buildDirectory.dir("out/xslt")) + outputExtension.set(".xir") + outputFormat.set("beautified") + outputMode("auto") + workers.set(4) + param("tenant", "acme") } ``` +
-## Canonical JSON Support - -`XsltTask` and `XQueryTask` support optional canonical JSON parsing/serialization. +### XQuery transformation -- Canonical JSON maps XML element trees to JSON objects with `type`, `name`, `attributes`, `children`. -- Canonical JSON mode is reversible for XML -> JSON -> XML roundtrips. -- `xirFormat` controls canonical JSON output style too: `compact` or `beautified`. +
+Input/output example in XML and XIR -Set JSON routing mode with `jsonMode`: +```xml +XML +``` -- `auto` (default): canonical parser for `.json` input; for `.json` output, 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 `.json` output, same canonical-first behavior with native fallback -- `canonical`: canonical parser + canonical serializer for `.json` input/output (no fallback) +```lisp +(book {id "b1"} (title "XML")) +``` +
-### XSLT Canonical JSON Example +
+Groovy Task Def (XQuery) ```groovy -tasks.register('xmlToJsonCanonical', name.jurgenei.gradle.xml.XsltTask) { - style 'src/main/xslt/identity.xsl' +tasks.register('runXQuery', name.jurgenei.gradle.xml.XQueryTask) { + query 'src/main/xquery/main.xq' source 'src/main/xml/input.xml' - outputDir.set(layout.buildDirectory.dir('out/json')) + outputDir.set(layout.buildDirectory.dir('out/xquery')) outputExtension.set('.json') - jsonMode.set('canonical') - xirFormat.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') + outputMode('canonical') + outputFormat.set('beautified') } ``` +
-### Beautified Output Switch - -Kotlin DSL: +
+Kotlin Task Def (XQuery) ```kotlin -tasks.register("xmlToXir") { - style("src/main/xslt/identity.xsl") +tasks.register("runXQuery") { + query("src/main/xquery/main.xq") source("src/main/xml/input.xml") - outputDir.set(layout.buildDirectory.dir("out/xslt")) - outputExtension.set(".xir") - xirFormat.set("beautified") + outputDir.set(layout.buildDirectory.dir("out/xquery")) + outputExtension.set(".json") + outputMode("canonical") + outputFormat.set("beautified") } ``` +
-Groovy DSL: +### Canonical JSON mode details -```groovy -tasks.register('xmlToXir', 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('.xir') - xirFormat.set('beautified') -} -``` +- `outputMode('auto')`: parse JSON canonically on input; output tries canonical-first with native fallback. +- `outputMode('native')`: no canonical input parser; output still canonical-first with native fallback. +- `outputMode('canonical')`: canonical input and canonical output only. -## Input/Output Modes +Legacy aliases still work: -`XsltTask` and `XQueryTask` support two equivalent execution modes: +- `jsonMode` / `mode` +- `xirFormat` / `xformat` -- **File-tree mode**: set `source(...)` and `outputDir` -- **Explicit single-file mode**: set `input(...)` and `output(...)` +### SEL and SHACL transformation pipelines -Notes: +- `SchematronSelCompileTask`: compile `sel:*` metadata to extraction stylesheet +- `SchematronSelExtractTask`: run extraction and emit grouped outputs +- `ShaclSelCompileTask` + `ShaclSelExtractTask`: same compile/extract shape for SHACL-driven flows -- In explicit mode, `input(...)` and `output(...)` must be set together. -- In file-tree mode, `outputDir` is required. -- Both modes support `param(...)`; file-tree mode additionally supports `workers` and extension-based mapping. +## Validations -## Validation API Contract +Validation tasks use shared contract: -Validation tasks share a common contract (`ValidationTaskSpec`) and defaults: +- `outputExtension` (default `.svrl.xml`) +- `workers` (default `1`) +- `failOnError` (default `true`) +- `format(...)` / `reportFormat` values: + - `SVRL` + - `JUNIT` + - `SVRL_AND_JUNIT` -- `outputExtension = '.svrl.xml'` -- `workers = 1` -- `reportFormat = SVRL` -- `failOnError = true` -- `junitOutputDir = build/reports/xml-validation/junit` +### XSD validation (`XsdTask`) -`ReportFormat` values: +Engine options: -- `SVRL` -- `JUNIT` -- `SVRL_AND_JUNIT` - -`XsdTask` supports `XsdEngine` values: - -- `AUTO` (default; prefers Saxon schema-aware, otherwise JAXP) +- `AUTO` (default; chooses best available engine) - `SAXON` - `JAXP` -## Plugin ID and Coordinates - -- Supported plugin ID: `name.jurgenei.gradle.xml` -- Maven artifact for legacy `buildscript` usage: `name.jurgenei.gradle:gradle-xml-transform:` -- Obsolete/legacy IDs from earlier docs are no longer supported. - -## Installation +
+XML and XIR validation inputs -Add to `build.gradle.kts`: - -```kotlin -plugins { - id("name.jurgenei.gradle.xml") -} +```xml +bad ``` -Or `build.gradle`: - -```groovy -plugins { - id 'name.jurgenei.gradle.xml' -} +```lisp +(root (wrong "bad")) ``` +
-Legacy `buildscript` usage: +
+Groovy Task Def (XSD validation) -```kotlin -buildscript { - repositories { - mavenCentral() - gradlePluginPortal() - } - dependencies { - classpath("name.jurgenei.gradle:gradle-xml-transform:0.1.1") - } +```groovy +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')) + format(name.jurgenei.gradle.xml.validation.ReportFormat.SVRL_AND_JUNIT) + engine.set(name.jurgenei.gradle.xml.validation.XsdEngine.AUTO) + failOnError.set(false) } - -apply(plugin = "name.jurgenei.gradle.xml") ``` +
-## Example (Kotlin DSL) +
+Kotlin Task Def (XSD validation) ```kotlin -plugins { - id("name.jurgenei.gradle.xml") -} - -tasks.register("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("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("transformOne") { - style("src/main/xslt/main.xsl") - input("src/main/xml/a.xml") - output("build/custom/b.xml") -} - -tasks.register("queryOne") { - query("src/main/xquery/main.xq") - input("src/main/xml/a.xml") - output("build/custom/b.xml") +tasks.register("validateXsd") { + schema("src/main/xsd/schema.xsd") + source(fileTree("src/main/xml") { include("**/*.xml") }) + outputDir.set(layout.buildDirectory.dir("reports/xsd")) + format(name.jurgenei.gradle.xml.validation.ReportFormat.SVRL_AND_JUNIT) + engine.set(name.jurgenei.gradle.xml.validation.XsdEngine.AUTO) + failOnError.set(false) } ``` +
-## Example (Groovy DSL) - -```groovy -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' -} +### Schematron validation (`SchematronTask`) -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' -} +Schematron options include: -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' -} -``` +- `style(...)` for persistent compiled stylesheet cache +- optional `transpilerStylesheet(...)` +- SchXslt parameter set: `phase`, `severityThreshold`, `streamable`, `compactReport`, and others -## Validation Examples (Groovy DSL) +
+Groovy Task Def (Schematron validation) ```groovy 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. + format(name.jurgenei.gradle.xml.validation.ReportFormat.SVRL_AND_JUNIT) 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`, `failEarly` - - `terminateValidationOnError`, `reportActivePattern`, `reportFiredRule`, `reportSuppressedRule` - - `reportSkippedAssertion`, `compactReport`, `severityThreshold`, `defaultSeverity`, `defaultFrom` - - `checkAssembledSchema`, `handleDynamicErrors` - -## Schematron Bootstrap From XSD - -Use `SchematronBootstrapTask` to create an initial SEL Schematron from an XSD. -The generated file is comprehensive (captures required children/attributes as SEL profiles) -but intentionally passing (bootstrap-safe) until you tighten rules manually. - -Safety behavior: +
+Kotlin Task Def (Schematron validation) -- If output `.sch` already exists, bootstrap does **not** overwrite it. -- The task logs a lifecycle warning and exits. - -Cross-plugin workflow (OOXML + XML plugins): - -```groovy -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-sel.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-sel.sch')) - source 'src/main/xml/canonical.xml' - outputDir.set(layout.buildDirectory.dir('reports/schematron')) -} -``` - -## SEL Compiler Skeleton (Phase 2) - -`SchematronSelCompileTask` compiles `sel:*` rule metadata into an extraction stylesheet skeleton -with grouped `xsl:result-document` outputs. - -Reusable SEL metadata can be defined once and referenced from many rules (SQF-style): - -```xml - - - - - - - Person must have id. - - -``` - -Presets can also provide reusable output template fragments. Template text/attributes support value templates -evaluated in the current matched rule context: - -```xml - - - - - {concat(local-name(), ':', normalize-space(.))} - {ancestor::c:Section[1]/c:Title} - - - - -``` - -Preset merge order is deterministic: defaults -> referenced preset(s) in declared order -> inline `sel:*` attributes on the assert/report node (inline wins). - -```groovy -tasks.register('compileSel', name.jurgenei.gradle.xml.SchematronSelCompileTask) { - schema 'src/main/schematron/sel.sch' - output 'build/generated/sel/sel.xsl' - phase '#DEFAULT' // '#DEFAULT' | '#ALL' | explicit phase id - outputNamespaceUri 'http://jurgenei.name/sel' // optional task-level override - outputNamespacePrefix 'sel' // optional; '' => default namespace output - groupOutput 'knowledge', 'sel/knowledge.xml' - groupOutput 'terminology', 'sel/terminology.xml' - groupOutput 'architecture', 'sel/architecture.xml' +```kotlin +tasks.register("validateSchematron") { + schema("src/main/schematron/rules.sch") + style("build/generated/schematron/rules.compiled.xsl") + source(fileTree("src/main/xml") { include("**/*.xml") }) + outputDir.set(layout.buildDirectory.dir("reports/schematron")) + format(name.jurgenei.gradle.xml.validation.ReportFormat.SVRL_AND_JUNIT) + phase.set("#ALL") + severityThreshold.set("warning") + workers.set(4) + failOnError.set(false) } ``` +
-Schema-level defaults can be declared once and are used unless task-level override is set: - -```xml - -``` - -## SEL Runtime Extraction (Phase 3) - -`SchematronSelExtractTask` executes SEL extraction against canonical XML, XIR (`.xir`), or canonical JSON (`.json`) inputs and emits grouped outputs. -It can either: +### Schematron bootstrap from XSD -- compile extraction style on the fly from `schema`, or -- consume a precompiled style via `style`. +`SchematronBootstrapTask` can generate initial Schematron from XSD and does not overwrite existing target file. -```groovy -tasks.register('extractSel', name.jurgenei.gradle.xml.SchematronSelExtractTask) { - schema 'src/main/schematron/sel.sch' - // Optional if precompiled by SchematronSelCompileTask: - // style 'build/generated/sel/sel.xsl' - phase 'knowledge-phase' // used when style is compiled on-the-fly from schema - outputNamespaceUri 'http://jurgenei.name/sel' // optional task-level override - outputNamespacePrefix 'sel' // optional; '' => default namespace output - source(fileTree('src/main/xml') { include '**/*.xml' }) - outputDir.set(layout.buildDirectory.dir('reports/sel')) - groupOutput 'knowledge', 'sel/knowledge.xml' - groupOutput 'terminology', 'sel/terminology.xml' - groupOutput 'architecture', 'sel/architecture.xml' - // XIR targets are supported by using .xir output paths: - // groupOutput 'knowledge', 'sel/knowledge.xir' - // xirFormat.set('beautified') - jsonMode.set('auto') - failOnError.set(true) -} -``` +## XSpec testing -Phase behavior for SEL compile/extract tasks: +### Current native scope -- `#DEFAULT` (default): uses `sch:schema/@defaultPhase`; if absent, all patterns are active. -- `#ALL`: all SEL-annotated rules are compiled. -- explicit phase id: only rules whose owning pattern is activated via ``. +- `XSpecCompileTask`: compile `.xspec` to executable runner +- `XSpecTask`: run XSpec and emit XML + JUnit reports +- Current built-in scope: XSLT XSpec -## SHACL-Sel Compile + Extract +### Scenarios -`ShaclSelCompileTask` and `ShaclSelExtractTask` follow same compile/extract shape as Schematron SEL tasks. +1. **XSLT scenario**: direct `.xspec` compile/run flow. +2. **XQuery scenario**: validate XQuery outputs through integration tasks, then assert results in test layer (JUnit/TestKit). +3. **XSD scenario**: run `XsdTask`, assert SVRL/JUnit outputs in pipeline. +4. **Schematron scenario**: run `SchematronTask`, assert SVRL/JUnit outputs, optionally combine with XSpec around downstream XSLT assets. -Current SHACL compile input is RDF/XML and targets relation-oriented SEL extraction. - -```groovy -tasks.register('compileShaclSel', name.jurgenei.gradle.xml.ShaclSelCompileTask) { - schema 'src/main/shacl/collibra-model.shacl.xml' - output 'build/generated/sel/shacl-sel.xsl' - outputSchematron 'build/generated/sel/shacl-sel.sch' - groupOutput 'relations', 'sel/relations.xml' -} - -tasks.register('extractShaclSel', name.jurgenei.gradle.xml.ShaclSelExtractTask) { - dependsOn tasks.named('compileShaclSel') - schema 'build/generated/sel/shacl-sel.sch' - style 'build/generated/sel/shacl-sel.xsl' - source(fileTree('src/main/xml') { include '*.xml' }) - outputDir.set(layout.buildDirectory.dir('reports/shacl-sel')) - groupOutput 'relations', 'sel/relations.xml' - failOnError.set(true) -} -``` - -Sample `samples/transformation/shacl-sel` also commits snapshot artifacts for discoverability: - -- `expected/shacl-sel.sch` -- `expected/shacl-sel.xsl` - -## XSpec Compile (Phase 2) - -`XSpecCompileTask` transpiles XSLT XSpec files (`.xspec`) into executable runner stylesheets. +
+Groovy Task Def (XSpec compile + run) ```groovy tasks.register('compileXSpec', name.jurgenei.gradle.xml.XSpecCompileTask) { source(fileTree('src/main/xspec') { include '**/*.xspec' }) outputDir.set(layout.buildDirectory.dir('generated/xspec')) - outputExtension.set('.xspec.xsl') // default - failOnError.set(true) + outputExtension.set('.xspec.xsl') } -``` - -## XSpec Run (Phase 3) - -`XSpecTask` executes XSpec and emits XML + JUnit reports. - -v1 scope is **XSLT XSpec only**. -Runtime inputs: - -- `.xspec` (auto-compiles internally, then runs), or -- precompiled runner stylesheet (`.xsl`) from `XSpecCompileTask`. - -```groovy tasks.register('runXSpec', name.jurgenei.gradle.xml.XSpecTask) { - // Either source .xspec files (auto-compile)... + dependsOn tasks.named('compileXSpec') source(fileTree('src/main/xspec') { include '**/*.xspec' }) outputDir.set(layout.buildDirectory.dir('reports/xspec')) - - // ...or explicit precompiled runner input: - // input 'build/generated/xspec/sample.xspec.xsl' - // output 'build/reports/xspec/sample-report.xml' - junitOutputDir.set(layout.buildDirectory.dir('reports/xspec/junit')) failOnError.set(true) } ``` +
-## Run tests - -```bash -./gradlew test -``` - -## Test Coverage +
+Kotlin Task Def (XSpec compile + run) -Generate coverage report and enforce the current minimum line coverage baseline (>= 0%): +```kotlin +tasks.register("compileXSpec") { + source(fileTree("src/main/xspec") { include("**/*.xspec") }) + outputDir.set(layout.buildDirectory.dir("generated/xspec")) + outputExtension.set(".xspec.xsl") +} -```bash -./gradlew coverage +tasks.register("runXSpec") { + dependsOn(tasks.named("compileXSpec")) + source(fileTree("src/main/xspec") { include("**/*.xspec") }) + outputDir.set(layout.buildDirectory.dir("reports/xspec")) + junitOutputDir.set(layout.buildDirectory.dir("reports/xspec/junit")) + failOnError.set(true) +} ``` +
-Coverage 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 Scanning - -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` +## Code Style -Set repository secret `NVD_API_KEY` for faster/more reliable Dependency-Check NVD lookups. +- Java 21+ +- Public API/classes documented with Javadoc +- Multiline literals use text blocks where practical +- Build scripts follow standard Gradle Groovy/Kotlin DSL idioms -Run locally: - -```bash -./gradlew dependencyCheckAnalyze --no-configuration-cache -./gradlew spotbugsMain -PspotbugsIgnoreFailures=false --no-configuration-cache -./gradlew allSecurityChecks -``` - -## Building +## Build, Test, Security ```bash ./gradlew build +./gradlew test +./gradlew coverage +./gradlew allSecurityChecks ``` -Required Java version: **21+** - -## Architecture +Coverage outputs: -### Task Hierarchy +- `build/reports/jacoco/test/jacocoTestReport.xml` +- `build/reports/jacoco/test/html/index.html` -``` -AbstractXmlTransformTask (shared base) - ├── XsltTask (XSLT transformations) - └── XQueryTask (XQuery transformations) +## Architecture (quick view) -AbstractXmlValidationTask (shared base) - ├── SchematronTask (Schematron validation) - └── XsdTask (XSD validation) +```text +AbstractXmlTransformTask + ├── XsltTask + ├── XQueryTask + ├── XSpecCompileTask + └── XSpecTask -AbstractXmlTransformTask (XSpec additions) - ├── XSpecCompileTask (compile .xspec to runner stylesheet) - └── XSpecTask (execute XSpec runner, emit XML + JUnit) +AbstractXmlValidationTask + ├── SchematronTask + └── XsdTask ``` -### Execution Flow - -1. Resolve input files from `source` / `fileset` -2. Sort files deterministically -3. Optionally parallelize using virtual-thread worker pool (if `workers > 1`) -4. For each input file: - - Skip when output is newer than transform dependencies (source + style/query/schema) - - Derive output file path using `outputExtension` mapping - - Create output directories (thread-safe via `Files.createDirectories`) - - Compile and execute transform (XSLT or XQuery) - - Log success or collect failure - -### Parallelism - -- `workers = 1` (default): Sequential processing -- `workers > 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. +## Samples -## Development - -### Samples - -Runnable minimal examples are available under `samples/`: +See runnable examples under `samples/`: - `samples/transformation/xslt` - `samples/transformation/xquery` @@ -721,47 +459,9 @@ Runnable minimal examples are available under `samples/`: - `samples/validation/schematron` - `samples/schematron-bootstrap-ooxml` -See `samples/README.md` for run commands. - -### Testing - -JUnit 4 with Gradle TestKit for functional integration testing: - -```bash -./gradlew test --tests '*XsltTaskIntegrationTest' -./gradlew test --tests '*XQueryTaskIntegrationTest' -./gradlew test --tests '*SchematronTaskIntegrationTest' -./gradlew test --tests '*XsdTaskIntegrationTest' -./gradlew test --tests '*SchematronBootstrapTaskIntegrationTest' -./gradlew test --tests '*SchematronSelCompileTaskIntegrationTest' -./gradlew test --tests '*SchematronSelExtractTaskIntegrationTest' -./gradlew test --tests '*ShaclSelCompileTaskIntegrationTest' -./gradlew test --tests '*ShaclSelExtractTaskIntegrationTest' -./gradlew test --tests '*XSpecCompileTaskIntegrationTest' -./gradlew test --tests '*XSpecTaskIntegrationTest' -``` - -Sample module self-tests (XSpec-driven): - -```bash -./gradlew -p samples/transformation/xslt runSelfTest -./gradlew -p samples/transformation/xquery runSelfTest -./gradlew -p samples/validation/schematron runSelfTest -./gradlew -p samples/validation/xsd runSelfTest -./gradlew -p samples/transformation/sel runSelfTest -./gradlew -p samples/transformation/shacl-sel runSelfTest -./gradlew -p samples/schematron-bootstrap-ooxml runSelfTest -``` - -### Code Style - -- Java 21+ source -- Javadoc on all public APIs and classes -- Text blocks for multiline strings (Java 15+) - ## Contributing -Contribution workflow and coding expectations are documented in `CONTRIBUTING.md`. +Workflow and expectations: `CONTRIBUTING.md` ## License diff --git a/src/main/java/name/jurgenei/gradle/xml/AbstractXmlTransformTask.java b/src/main/java/name/jurgenei/gradle/xml/AbstractXmlTransformTask.java index 80bef6f..83f0820 100644 --- a/src/main/java/name/jurgenei/gradle/xml/AbstractXmlTransformTask.java +++ b/src/main/java/name/jurgenei/gradle/xml/AbstractXmlTransformTask.java @@ -1,3 +1,4 @@ +/* (C)2026 */ package name.jurgenei.gradle.xml; import java.io.File; @@ -23,9 +24,10 @@ import org.gradle.api.provider.Property; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.Optional; -import org.gradle.api.tasks.OutputFile; import org.gradle.api.tasks.OutputDirectory; +import org.gradle.api.tasks.OutputFile; import org.gradle.api.tasks.PathSensitive; import org.gradle.api.tasks.PathSensitivity; import org.gradle.api.tasks.SourceTask; @@ -126,6 +128,26 @@ public enum JsonMode { @Optional public abstract Property getXirFormat(); + /** + * Canonical output-format alias for {@link #getXirFormat()}. + * + * @return S-expression output format property + */ + @Internal + public Property getOutputFormat() { + return getXirFormat(); + } + + /** + * Backward-compatible alias for {@link #getOutputFormat()}. + * + * @return S-expression output format property + */ + @Internal + public Property getXformat() { + return getOutputFormat(); + } + /** * Optional JSON mode controlling how {@code .json} input/output is routed. * @@ -137,6 +159,26 @@ public enum JsonMode { @Optional public abstract Property getJsonMode(); + /** + * Canonical output-mode alias for {@link #getJsonMode()}. + * + * @return JSON routing mode property + */ + @Internal + public Property getOutputMode() { + return getJsonMode(); + } + + /** + * Backward-compatible alias for {@link #getOutputMode()}. + * + * @return JSON routing mode property + */ + @Internal + public Property getMode() { + return getOutputMode(); + } + /** * Transform parameters exposed to the execution engine. * @@ -236,6 +278,42 @@ public void xirFormat(String format) { getXirFormat().set(format); } + /** + * Sets explicit S-expression output format using canonical DSL name. + * + * @param format one of {@code compact} or {@code beautified} + */ + public void outputFormat(String format) { + getOutputFormat().set(format); + } + + /** + * Groovy/Kotlin assignment-style alias for {@link #outputFormat(String)}. + * + * @param format one of {@code compact} or {@code beautified} + */ + public void setOutputFormat(String format) { + outputFormat(format); + } + + /** + * Backward-compatible alias for {@link #outputFormat(String)}. + * + * @param format one of {@code compact} or {@code beautified} + */ + public void xformat(String format) { + outputFormat(format); + } + + /** + * Groovy/Kotlin assignment-style alias for {@link #xformat(String)}. + * + * @param format one of {@code compact} or {@code beautified} + */ + public void setXformat(String format) { + xformat(format); + } + /** * Sets JSON routing mode (Gradle DSL friendly). * @@ -245,6 +323,42 @@ public void jsonMode(String mode) { getJsonMode().set(mode); } + /** + * Sets JSON routing mode using canonical DSL name. + * + * @param mode one of {@code auto}, {@code native}, or {@code canonical} + */ + public void outputMode(String mode) { + getOutputMode().set(mode); + } + + /** + * Groovy/Kotlin assignment-style alias for {@link #outputMode(String)}. + * + * @param mode one of {@code auto}, {@code native}, or {@code canonical} + */ + public void setOutputMode(String mode) { + outputMode(mode); + } + + /** + * Backward-compatible alias for {@link #outputMode(String)}. + * + * @param mode one of {@code auto}, {@code native}, or {@code canonical} + */ + public void mode(String mode) { + outputMode(mode); + } + + /** + * Groovy/Kotlin assignment-style alias for {@link #mode(String)}. + * + * @param mode one of {@code auto}, {@code native}, or {@code canonical} + */ + public void setMode(String mode) { + mode(mode); + } + @Override @PathSensitive(PathSensitivity.RELATIVE) public org.gradle.api.file.FileTree getSource() { @@ -262,10 +376,12 @@ public void transformAll() { boolean hasExplicitInput = getInputFile().isPresent(); boolean hasExplicitOutput = getOutputFile().isPresent(); if (hasExplicitInput != hasExplicitOutput) { - throw new GradleException("Both inputFile and outputFile must be set together for single-file mode"); + throw new GradleException( + "Both inputFile and outputFile must be set together for single-file mode"); } - Map params = Collections.unmodifiableMap(new HashMap<>(getParams().getOrElse(Map.of()))); + Map params = + Collections.unmodifiableMap(new HashMap<>(getParams().getOrElse(Map.of()))); if (hasExplicitInput) { transformExplicit(params); @@ -302,7 +418,9 @@ public void transformAll() { } if (!failures.isEmpty()) { - throw new GradleException("Transformation failed for " + failures.size() + " input file(s)", failures.get(0)); + throw new GradleException( + "Transformation failed for " + failures.size() + " input file(s)", + failures.get(0)); } } @@ -314,8 +432,7 @@ private void transformExplicit(Map params) { } long newestDependencyTimestamp = latestDependencyTimestamp(inputFile); - if (outputFile.exists() - && outputFile.lastModified() >= newestDependencyTimestamp) { + if (outputFile.exists() && outputFile.lastModified() >= newestDependencyTimestamp) { getLogger().lifecycle("[SKIP] {}", inputFile); return; } @@ -336,11 +453,27 @@ private void transformExplicit(Map params) { } } - private void runParallel(List inputFiles, File outputRoot, Map relativePaths, Map params, int workers, List failures) { - try (ExecutorService executor = Executors.newFixedThreadPool(workers, Thread.ofVirtual().name(getName() + "-vt-", 0).factory())) { + private void runParallel( + List inputFiles, + File outputRoot, + Map relativePaths, + Map params, + int workers, + List failures) { + try (ExecutorService executor = + Executors.newFixedThreadPool( + workers, Thread.ofVirtual().name(getName() + "-vt-", 0).factory())) { List> futures = new ArrayList<>(); for (File inputFile : inputFiles) { - futures.add(executor.submit(() -> transformOne(inputFile, outputRoot, relativePaths, params, failures))); + futures.add( + executor.submit( + () -> + transformOne( + inputFile, + outputRoot, + relativePaths, + params, + failures))); } for (java.util.concurrent.Future future : futures) { try { @@ -360,12 +493,16 @@ private void runParallel(List inputFiles, File outputRoot, Map relativePaths, Map params, List failures) { + private void transformOne( + File inputFile, + File outputRoot, + Map relativePaths, + Map params, + List failures) { File outputFile = outputFileFor(inputFile, outputRoot, relativePaths); long newestDependencyTimestamp = latestDependencyTimestamp(inputFile); - if (outputFile.exists() - && outputFile.lastModified() >= newestDependencyTimestamp) { + if (outputFile.exists() && outputFile.lastModified() >= newestDependencyTimestamp) { getLogger().lifecycle("[SKIP] {}", inputFile); return; } @@ -428,12 +565,14 @@ protected XirSerializer.OutputFormat resolveXirOutputFormat() { normalized = "beautified"; } if (!SUPPORTED_SEXPR_FORMATS.contains(normalized)) { - throw new GradleException("Unsupported xirFormat '" + configured - + "'. Supported values: compact, beautified"); + throw new GradleException( + "Unsupported xirFormat '" + + configured + + "'. Supported values: compact, beautified"); } return "beautified".equals(normalized) - ? XirSerializer.OutputFormat.BEAUTIFIED - : XirSerializer.OutputFormat.COMPACT; + ? XirSerializer.OutputFormat.BEAUTIFIED + : XirSerializer.OutputFormat.COMPACT; } /** @@ -445,8 +584,10 @@ protected JsonMode resolveJsonMode() { String configured = getJsonMode().getOrElse("auto"); String normalized = configured.trim().toLowerCase(Locale.ROOT); if (!SUPPORTED_JSON_MODES.contains(normalized)) { - throw new GradleException("Unsupported jsonMode '" + configured - + "'. Supported values: auto, native, canonical"); + throw new GradleException( + "Unsupported jsonMode '" + + configured + + "'. Supported values: auto, native, canonical"); } return switch (normalized) { case "native" -> JsonMode.NATIVE; @@ -531,46 +672,60 @@ private String inferSerializerMethod(File outputFile) { } private String normalizeAndValidateMethod(String configuredMethod) { - String normalized = configuredMethod == null ? "" : configuredMethod.trim().toLowerCase(Locale.ROOT); + String normalized = + configuredMethod == null ? "" : configuredMethod.trim().toLowerCase(Locale.ROOT); if ("txt".equals(normalized)) { normalized = "text"; } if (!SUPPORTED_OUTPUT_METHODS.contains(normalized)) { - throw new GradleException("Unsupported outputMethod '" + configuredMethod - + "'. Supported values: xml, json, text"); + throw new GradleException( + "Unsupported outputMethod '" + + configuredMethod + + "'. Supported values: xml, json, text"); } return normalized; } - private File outputFileFor(File inputFile, File outputRoot, Map relativePaths) { Path inputPath = inputFile.toPath().toAbsolutePath().normalize(); Path projectPath = getProject().getProjectDir().toPath().toAbsolutePath().normalize(); String relative = relativePaths.get(inputPath); if (relative == null) { - relative = inputPath.startsWith(projectPath) - ? projectPath.relativize(inputPath).toString() - : inputFile.getName(); + relative = + inputPath.startsWith(projectPath) + ? projectPath.relativize(inputPath).toString() + : inputFile.getName(); } int extensionIndex = relative.lastIndexOf('.'); String extension = getOutputExtension().get(); - String replaced = extensionIndex >= 0 ? relative.substring(0, extensionIndex) + extension : relative + extension; + String replaced = + extensionIndex >= 0 + ? relative.substring(0, extensionIndex) + extension + : relative + extension; return new File(outputRoot, replaced); } private Map resolveRelativePaths() { Map relativePaths = new HashMap<>(); - getSource().visit(details -> { - if (details.isDirectory()) { - return; - } - Path absolutePath = details.getFile().toPath().toAbsolutePath().normalize(); - String relativePath = details.getRelativePath().getPathString(); - relativePaths.merge(absolutePath, relativePath, - (existing, candidate) -> existing.length() <= candidate.length() ? existing : candidate); - }); + getSource() + .visit( + details -> { + if (details.isDirectory()) { + return; + } + Path absolutePath = + details.getFile().toPath().toAbsolutePath().normalize(); + String relativePath = details.getRelativePath().getPathString(); + relativePaths.merge( + absolutePath, + relativePath, + (existing, candidate) -> + existing.length() <= candidate.length() + ? existing + : candidate); + }); return relativePaths; } @@ -582,6 +737,6 @@ private Map resolveRelativePaths() { * @param params immutable task parameter view * @throws Exception any transform exception raised by the engine implementation */ - protected abstract void transform(File inputFile, File outputFile, Map params) throws Exception; + protected abstract void transform(File inputFile, File outputFile, Map params) + throws Exception; } - diff --git a/src/main/java/name/jurgenei/gradle/xml/AbstractXmlValidationTask.java b/src/main/java/name/jurgenei/gradle/xml/AbstractXmlValidationTask.java index 992a00f..d15d6aa 100644 --- a/src/main/java/name/jurgenei/gradle/xml/AbstractXmlValidationTask.java +++ b/src/main/java/name/jurgenei/gradle/xml/AbstractXmlValidationTask.java @@ -1,3 +1,4 @@ +/* (C)2026 */ package name.jurgenei.gradle.xml; import java.io.File; @@ -39,7 +40,8 @@ /** * Shared base task for XSD and Schematron validations producing SVRL and optional JUnit reports. */ -@DisableCachingByDefault(because = "Validation is I/O heavy and depends on external schema resources") +@DisableCachingByDefault( + because = "Validation is I/O heavy and depends on external schema resources") public abstract class AbstractXmlValidationTask extends SourceTask implements ValidationTaskSpec { @OutputDirectory @@ -60,6 +62,16 @@ public abstract class AbstractXmlValidationTask extends SourceTask implements Va @Input public abstract Property getReportFormat(); + /** + * Orthogonal alias for {@link #getReportFormat()}. + * + * @return report format property + */ + @Internal + public Property getFormat() { + return getReportFormat(); + } + @OutputDirectory public abstract DirectoryProperty getJunitOutputDir(); @@ -88,7 +100,30 @@ public AbstractXmlValidationTask() { getReportFormat().convention(ReportFormat.SVRL); getJunitSuiteName().convention(getName()); getProjectDir().convention(getProject().getLayout().getProjectDirectory()); - getJunitOutputDir().convention(getProject().getLayout().getBuildDirectory().dir("reports/xml-validation/junit")); + getJunitOutputDir() + .convention( + getProject() + .getLayout() + .getBuildDirectory() + .dir("reports/xml-validation/junit")); + } + + /** + * Sets validation report format using orthogonal DSL alias. + * + * @param format report format selection + */ + public void format(ReportFormat format) { + getReportFormat().set(format); + } + + /** + * Groovy/Kotlin assignment-style alias for {@link #format(ReportFormat)}. + * + * @param format report format selection + */ + public void setFormat(ReportFormat format) { + format(format); } /** @@ -154,15 +189,25 @@ public void validateAll() { } if (!failures.isEmpty() && getFailOnError().get()) { - throw new GradleException("Validation failed for " + failures.size() + " input file(s)", failures.get(0)); + throw new GradleException( + "Validation failed for " + failures.size() + " input file(s)", failures.get(0)); } } - private void runParallel(List inputFiles, File outputRoot, Map relativePaths, int workers, List failures) { - try (ExecutorService executor = Executors.newFixedThreadPool(workers, Thread.ofVirtual().name(getName() + "-vt-", 0).factory())) { + private void runParallel( + List inputFiles, + File outputRoot, + Map relativePaths, + int workers, + List failures) { + try (ExecutorService executor = + Executors.newFixedThreadPool( + workers, Thread.ofVirtual().name(getName() + "-vt-", 0).factory())) { List> futures = new ArrayList<>(); for (File inputFile : inputFiles) { - futures.add(executor.submit(() -> validateOne(inputFile, outputRoot, relativePaths, failures))); + futures.add( + executor.submit( + () -> validateOne(inputFile, outputRoot, relativePaths, failures))); } for (java.util.concurrent.Future future : futures) { try { @@ -182,15 +227,20 @@ private void runParallel(List inputFiles, File outputRoot, Map relativePaths, List failures) { + private void validateOne( + File inputFile, + File outputRoot, + Map relativePaths, + List failures) { File svrlFile = svrlFileFor(inputFile, outputRoot, relativePaths); mkdirs(svrlFile.getParentFile()); try { ValidationResult result = validate(inputFile, getParams().get()); - String svrlXml = result.svrlXml() == null || result.svrlXml().isBlank() - ? SvrlSupport.renderSvrl(inputFile.getPath(), result.issues()) - : result.svrlXml(); + String svrlXml = + result.svrlXml() == null || result.svrlXml().isBlank() + ? SvrlSupport.renderSvrl(inputFile.getPath(), result.issues()) + : result.svrlXml(); if (getReportFormat().get().writesSvrl()) { Files.writeString(svrlFile.toPath(), svrlXml, StandardCharsets.UTF_8); @@ -199,7 +249,9 @@ private void validateOne(File inputFile, File outputRoot, Map rela if (getReportFormat().get().writesJunit()) { File junitFile = junitFileFor(inputFile, relativePaths); mkdirs(junitFile.getParentFile()); - String junitXml = SvrlSupport.renderJunit(getJunitSuiteName().get(), inputFile.getName(), result.issues()); + String junitXml = + SvrlSupport.renderJunit( + getJunitSuiteName().get(), inputFile.getName(), result.issues()); Files.writeString(junitFile.toPath(), junitXml, StandardCharsets.UTF_8); } @@ -217,15 +269,17 @@ private File svrlFileFor(File inputFile, File outputRoot, Map rela String relative = relativePaths.get(inputPath); if (relative == null) { - relative = inputPath.startsWith(projectPath) - ? projectPath.relativize(inputPath).toString() - : inputFile.getName(); + relative = + inputPath.startsWith(projectPath) + ? projectPath.relativize(inputPath).toString() + : inputFile.getName(); } int extensionIndex = relative.lastIndexOf('.'); - String replaced = extensionIndex >= 0 - ? relative.substring(0, extensionIndex) + getOutputExtension().get() - : relative + getOutputExtension().get(); + String replaced = + extensionIndex >= 0 + ? relative.substring(0, extensionIndex) + getOutputExtension().get() + : relative + getOutputExtension().get(); return new File(outputRoot, replaced); } @@ -235,15 +289,17 @@ private File junitFileFor(File inputFile, Map relativePaths) { String relative = relativePaths.get(inputPath); if (relative == null) { - relative = inputPath.startsWith(projectPath) - ? projectPath.relativize(inputPath).toString() - : inputFile.getName(); + relative = + inputPath.startsWith(projectPath) + ? projectPath.relativize(inputPath).toString() + : inputFile.getName(); } int extensionIndex = relative.lastIndexOf('.'); - String replaced = extensionIndex >= 0 - ? relative.substring(0, extensionIndex) + ".junit.xml" - : relative + ".junit.xml"; + String replaced = + extensionIndex >= 0 + ? relative.substring(0, extensionIndex) + ".junit.xml" + : relative + ".junit.xml"; return new File(getJunitOutputDir().get().getAsFile(), replaced); } @@ -257,15 +313,23 @@ private static void mkdirs(File dir) { private Map resolveRelativePaths() { Map relativePaths = new HashMap<>(); - getSource().visit(details -> { - if (details.isDirectory()) { - return; - } - Path absolutePath = details.getFile().toPath().toAbsolutePath().normalize(); - String relativePath = details.getRelativePath().getPathString(); - relativePaths.merge(absolutePath, relativePath, - (existing, candidate) -> existing.length() <= candidate.length() ? existing : candidate); - }); + getSource() + .visit( + details -> { + if (details.isDirectory()) { + return; + } + Path absolutePath = + details.getFile().toPath().toAbsolutePath().normalize(); + String relativePath = details.getRelativePath().getPathString(); + relativePaths.merge( + absolutePath, + relativePath, + (existing, candidate) -> + existing.length() <= candidate.length() + ? existing + : candidate); + }); return relativePaths; } @@ -277,7 +341,8 @@ private Map resolveRelativePaths() { * @return normalized validation result * @throws Exception when validation cannot be performed */ - protected abstract ValidationResult validate(File inputFile, Map params) throws Exception; + protected abstract ValidationResult validate(File inputFile, Map params) + throws Exception; /** * Resolves a transform source for XML-like validation inputs. diff --git a/src/main/java/name/jurgenei/gradle/xml/SaxXmlWriter.java b/src/main/java/name/jurgenei/gradle/xml/SaxXmlWriter.java index 4e3ff46..37ece4e 100644 --- a/src/main/java/name/jurgenei/gradle/xml/SaxXmlWriter.java +++ b/src/main/java/name/jurgenei/gradle/xml/SaxXmlWriter.java @@ -40,6 +40,11 @@ private SaxXmlWriter(StringWriter output, TransformerHandler handler) { this.elementPrefixMappings = new ArrayDeque<>(); } + /** + * Creates pretty-printing SAX writer with secure transformer defaults. + * + * @return initialized XML writer instance + */ public static SaxXmlWriter createPretty() { try { StringWriter output = new StringWriter(); @@ -60,14 +65,34 @@ public static SaxXmlWriter createPretty() { } } + /** + * Starts XML document emission. + * + * @throws SAXException if downstream handler fails + */ public void startDocument() throws SAXException { handler.startDocument(); } + /** + * Completes XML document emission. + * + * @throws SAXException if downstream handler fails + */ public void endDocument() throws SAXException { handler.endDocument(); } + /** + * Starts element with optional namespace declarations and attributes. + * + * @param namespaceUri namespace URI or empty string + * @param localName local element name + * @param qName qualified element name + * @param attributes optional element attributes + * @param namespaceMappings optional prefix to URI mappings declared on this element + * @throws SAXException if downstream handler fails + */ public void startElement( String namespaceUri, String localName, @@ -92,6 +117,14 @@ public void startElement( toAttributes(attributes)); } + /** + * Ends element and closes namespace declarations opened for it. + * + * @param namespaceUri namespace URI or empty string + * @param localName local element name + * @param qName qualified element name + * @throws SAXException if downstream handler fails + */ public void endElement(String namespaceUri, String localName, String qName) throws SAXException { handler.endElement(safeNamespace(namespaceUri), safeLocal(localName, qName), qName); @@ -104,6 +137,16 @@ public void endElement(String namespaceUri, String localName, String qName) } } + /** + * Emits empty element with optional attributes and namespace mappings. + * + * @param namespaceUri namespace URI or empty string + * @param localName local element name + * @param qName qualified element name + * @param attributes optional element attributes + * @param namespaceMappings optional prefix to URI mappings declared on this element + * @throws SAXException if downstream handler fails + */ public void emptyElement( String namespaceUri, String localName, @@ -115,6 +158,12 @@ public void emptyElement( endElement(namespaceUri, localName, qName); } + /** + * Emits text node. + * + * @param text text content; blank values are ignored + * @throws SAXException if downstream handler fails + */ public void text(String text) throws SAXException { if (text == null || text.isEmpty()) { return; @@ -123,6 +172,12 @@ public void text(String text) throws SAXException { handler.characters(chars, 0, chars.length); } + /** + * Emits XML comment. + * + * @param text comment text; blank values are ignored + * @throws SAXException if downstream handler fails + */ public void comment(String text) throws SAXException { if (text == null || text.isEmpty()) { return; @@ -131,6 +186,12 @@ public void comment(String text) throws SAXException { handler.comment(chars, 0, chars.length); } + /** + * Parses and appends XML fragment under secure parser settings. + * + * @param fragment XML fragment to append + * @param namespaceHints optional namespace hints used while wrapping fragment root + */ public void appendFragment(String fragment, Map namespaceHints) { if (fragment == null || fragment.isBlank()) { return; @@ -159,14 +220,35 @@ public void appendFragment(String fragment, Map namespaceHints) } } + /** + * Returns rendered XML output. + * + * @return serialized XML string + */ public String build() { return output.toString(); } + /** + * Creates non-namespaced attribute. + * + * @param name attribute name + * @param value attribute value + * @return attribute descriptor + */ public static XmlAttribute attr(String name, String value) { return new XmlAttribute("", name, name, value == null ? "" : value); } + /** + * Creates namespaced attribute. + * + * @param namespaceUri namespace URI or null + * @param localName local name or null + * @param qName qualified name + * @param value attribute value + * @return attribute descriptor + */ public static XmlAttribute attr( String namespaceUri, String localName, String qName, String value) { return new XmlAttribute( @@ -301,5 +383,13 @@ private String escapeAttribute(String value) { .replace("\"", """); } + /** + * Attribute descriptor used by element helper methods. + * + * @param namespaceUri attribute namespace URI + * @param localName attribute local name + * @param qName attribute qualified name + * @param value attribute value + */ public record XmlAttribute(String namespaceUri, String localName, String qName, String value) {} } diff --git a/src/main/java/name/jurgenei/gradle/xml/SchematronSelExtractTask.java b/src/main/java/name/jurgenei/gradle/xml/SchematronSelExtractTask.java index 602d4fc..8459288 100644 --- a/src/main/java/name/jurgenei/gradle/xml/SchematronSelExtractTask.java +++ b/src/main/java/name/jurgenei/gradle/xml/SchematronSelExtractTask.java @@ -47,6 +47,7 @@ import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputFile; import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.OutputDirectory; import org.gradle.api.tasks.PathSensitive; @@ -178,6 +179,26 @@ public SchematronSelExtractTask() { @Optional public abstract Property getJsonMode(); + /** + * Canonical output-mode alias for {@link #getJsonMode()}. + * + * @return JSON routing mode property + */ + @Internal + public Property getOutputMode() { + return getJsonMode(); + } + + /** + * Backward-compatible alias for {@link #getOutputMode()}. + * + * @return JSON routing mode property + */ + @Internal + public Property getMode() { + return getOutputMode(); + } + /** * Optional XIR output format used when emitting {@code .xir} group outputs. * @@ -189,6 +210,26 @@ public SchematronSelExtractTask() { @Optional public abstract Property getXirFormat(); + /** + * Canonical output-format alias for {@link #getXirFormat()}. + * + * @return XIR serializer output format property + */ + @Internal + public Property getOutputFormat() { + return getXirFormat(); + } + + /** + * Backward-compatible alias for {@link #getOutputFormat()}. + * + * @return XIR serializer output format property + */ + @Internal + public Property getXformat() { + return getOutputFormat(); + } + /** * Active Schematron phase used for on-the-fly SEL compilation. * @@ -288,6 +329,42 @@ public void jsonMode(String mode) { getJsonMode().set(mode); } + /** + * Sets JSON routing mode using canonical DSL name. + * + * @param mode one of auto, native, canonical + */ + public void outputMode(String mode) { + getOutputMode().set(mode); + } + + /** + * Groovy/Kotlin assignment-style alias for {@link #outputMode(String)}. + * + * @param mode one of auto, native, canonical + */ + public void setOutputMode(String mode) { + outputMode(mode); + } + + /** + * Backward-compatible alias for {@link #outputMode(String)}. + * + * @param mode one of auto, native, canonical + */ + public void mode(String mode) { + outputMode(mode); + } + + /** + * Groovy/Kotlin assignment-style alias for {@link #mode(String)}. + * + * @param mode one of auto, native, canonical + */ + public void setMode(String mode) { + mode(mode); + } + /** * Sets S-expression output format for {@code .xir} group outputs (Gradle DSL friendly). * @@ -297,6 +374,42 @@ public void xirFormat(String format) { getXirFormat().set(format); } + /** + * Sets S-expression output format using canonical DSL name. + * + * @param format one of compact, beautified + */ + public void outputFormat(String format) { + getOutputFormat().set(format); + } + + /** + * Groovy/Kotlin assignment-style alias for {@link #outputFormat(String)}. + * + * @param format one of compact, beautified + */ + public void setOutputFormat(String format) { + outputFormat(format); + } + + /** + * Backward-compatible alias for {@link #outputFormat(String)}. + * + * @param format one of compact, beautified + */ + public void xformat(String format) { + outputFormat(format); + } + + /** + * Groovy/Kotlin assignment-style alias for {@link #xformat(String)}. + * + * @param format one of compact, beautified + */ + public void setXformat(String format) { + xformat(format); + } + /** * Sets Schematron phase used by on-the-fly SEL compilation. * diff --git a/src/main/java/name/jurgenei/gradle/xml/ShaclSelCompileTask.java b/src/main/java/name/jurgenei/gradle/xml/ShaclSelCompileTask.java index 2cf3311..d845ea7 100644 --- a/src/main/java/name/jurgenei/gradle/xml/ShaclSelCompileTask.java +++ b/src/main/java/name/jurgenei/gradle/xml/ShaclSelCompileTask.java @@ -43,29 +43,53 @@ public abstract class ShaclSelCompileTask extends DefaultTask { private static final String SCH_NS = "http://purl.oclc.org/dsdl/schematron"; private static final String SEL_NS = "http://jurgenei.name/sel"; - /** SHACL RDF/XML schema source. */ + /** + * Returns SHACL RDF/XML schema source. + * + * @return schema file property + */ @InputFile @PathSensitive(PathSensitivity.RELATIVE) public abstract RegularFileProperty getSchema(); - /** Generated SEL stylesheet output file. */ + /** + * Returns generated SEL stylesheet output target. + * + * @return stylesheet output file property + */ @OutputFile public abstract RegularFileProperty getOutputStylesheet(); - /** Generated intermediate Schematron file (for debugging and extractor compatibility). */ + /** + * Returns generated intermediate Schematron output target. + * + * @return Schematron output file property + */ @OutputFile public abstract RegularFileProperty getOutputSchematron(); - /** Group output mapping for generated stylesheet. */ + /** + * Returns logical group to output-path mapping. + * + * @return configured group outputs + */ @Input public abstract MapProperty getGroupOutputs(); - /** Optional SEL namespace URI override. */ + /** + * Returns optional override for SEL namespace URI. + * + * @return namespace URI property + */ @Input @Optional public abstract Property getOutputNamespaceUri(); - /** Optional SEL namespace prefix override. */ + /** + * Returns optional override for SEL namespace prefix. + * + * @return namespace prefix property + */ @Input @Optional public abstract Property getOutputNamespacePrefix(); diff --git a/src/main/java/name/jurgenei/gradle/xml/XsdTask.java b/src/main/java/name/jurgenei/gradle/xml/XsdTask.java index fe5f182..95650f9 100644 --- a/src/main/java/name/jurgenei/gradle/xml/XsdTask.java +++ b/src/main/java/name/jurgenei/gradle/xml/XsdTask.java @@ -1,3 +1,4 @@ +/* (C)2026 */ package name.jurgenei.gradle.xml; import java.io.File; @@ -20,15 +21,15 @@ import org.gradle.api.tasks.InputFile; import org.gradle.api.tasks.PathSensitive; import org.gradle.api.tasks.PathSensitivity; +import org.gradle.work.DisableCachingByDefault; import org.xml.sax.ErrorHandler; import org.xml.sax.SAXParseException; -import org.gradle.work.DisableCachingByDefault; - /** * Validates XML files against XSD and emits findings normalized as SVRL. */ -@DisableCachingByDefault(because = "XSD validation depends on external schema files and validator engine") +@DisableCachingByDefault( + because = "XSD validation depends on external schema files and validator engine") public abstract class XsdTask extends AbstractXmlValidationTask { /** @@ -69,11 +70,13 @@ public void schema(Object path) { } @Override - protected ValidationResult validate(File inputFile, Map params) throws Exception { + protected ValidationResult validate(File inputFile, Map params) + throws Exception { XsdEngine engine = resolveEngine(); - List issues = engine == XsdEngine.SAXON - ? validateWithSaxon(inputFile) - : validateWithJaxp(inputFile); + List issues = + engine == XsdEngine.SAXON + ? validateWithSaxon(inputFile) + : validateWithJaxp(inputFile); return new ValidationResult(issues, null); } @@ -85,13 +88,16 @@ private XsdEngine resolveEngine() { } if (requested == XsdEngine.SAXON) { if (!isSaxonSchemaAwareAvailable()) { - throw new GradleException("Saxon schema-aware validation requires Saxon PE/EE; configure engine = JAXP or AUTO"); + throw new GradleException( + "Saxon schema-aware validation requires Saxon PE/EE; configure engine =" + + " JAXP or AUTO"); } return XsdEngine.SAXON; } return isSaxonSchemaAwareAvailable() ? XsdEngine.SAXON : XsdEngine.JAXP; } + @SuppressWarnings("deprecation") private boolean isSaxonSchemaAwareAvailable() { try { Processor processor = new Processor(false); @@ -102,6 +108,7 @@ private boolean isSaxonSchemaAwareAvailable() { } } + @SuppressWarnings("deprecation") private List validateWithSaxon(File inputFile) { List issues = new ArrayList<>(); try { @@ -125,26 +132,34 @@ private List validateWithJaxp(File inputFile) throws Exception factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); - javax.xml.validation.Schema schema = factory.newSchema(sourceForValidation(getSchema().get().getAsFile())); + javax.xml.validation.Schema schema = + factory.newSchema(sourceForValidation(getSchema().get().getAsFile())); javax.xml.validation.Validator validator = schema.newValidator(); validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); validator.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); - validator.setErrorHandler(new ErrorHandler() { - @Override - public void warning(SAXParseException exception) { - issues.add(ValidationIssue.error(exception.getMessage(), location(inputFile, exception))); - } - - @Override - public void error(SAXParseException exception) { - issues.add(ValidationIssue.error(exception.getMessage(), location(inputFile, exception))); - } - - @Override - public void fatalError(SAXParseException exception) { - issues.add(ValidationIssue.error(exception.getMessage(), location(inputFile, exception))); - } - }); + validator.setErrorHandler( + new ErrorHandler() { + @Override + public void warning(SAXParseException exception) { + issues.add( + ValidationIssue.error( + exception.getMessage(), location(inputFile, exception))); + } + + @Override + public void error(SAXParseException exception) { + issues.add( + ValidationIssue.error( + exception.getMessage(), location(inputFile, exception))); + } + + @Override + public void fatalError(SAXParseException exception) { + issues.add( + ValidationIssue.error( + exception.getMessage(), location(inputFile, exception))); + } + }); try { validator.validate(sourceForValidation(inputFile)); @@ -158,4 +173,3 @@ private static String location(File input, SAXParseException exception) { return input + ":" + exception.getLineNumber() + ":" + exception.getColumnNumber(); } } - diff --git a/src/main/java/name/jurgenei/gradle/xml/json/JsonCanonicalParser.java b/src/main/java/name/jurgenei/gradle/xml/json/JsonCanonicalParser.java index feae7e0..7de18df 100644 --- a/src/main/java/name/jurgenei/gradle/xml/json/JsonCanonicalParser.java +++ b/src/main/java/name/jurgenei/gradle/xml/json/JsonCanonicalParser.java @@ -1,3 +1,4 @@ +/* (C)2026 */ package name.jurgenei.gradle.xml.json; import com.fasterxml.jackson.databind.JsonNode; @@ -6,7 +7,6 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import java.io.IOException; import java.io.Reader; -import java.util.Iterator; import java.util.Map; import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; @@ -22,8 +22,7 @@ public final class JsonCanonicalParser { /** * Creates parser for canonical JSON/XML mapping. */ - public JsonCanonicalParser() { - } + public JsonCanonicalParser() {} /** * Parses canonical JSON input and emits equivalent SAX events. @@ -44,7 +43,8 @@ public void parse(Reader reader, ContentHandler handler) throws IOException, SAX handler.endDocument(); } - private void emitElement(JsonNode node, ContentHandler handler) throws IOException, SAXException { + private void emitElement(JsonNode node, ContentHandler handler) + throws IOException, SAXException { if (!node.isObject()) { throw new IOException("Expected object node for element, got: " + node.getNodeType()); } @@ -65,9 +65,8 @@ private void emitElement(JsonNode node, ContentHandler handler) throws IOExcepti if (!attributesNode.isObject()) { throw new IOException("'attributes' must be an object for element: " + name); } - Iterator> it = attributesNode.fields(); - while (it.hasNext()) { - Map.Entry entry = it.next(); + ObjectNode attributesObject = (ObjectNode) attributesNode; + for (Map.Entry entry : attributesObject.properties()) { String attrName = entry.getKey(); String attrValue = scalarToString(entry.getValue()); attributes.addAttribute("", attrName, attrName, "CDATA", attrValue); @@ -87,7 +86,8 @@ private void emitElement(JsonNode node, ContentHandler handler) throws IOExcepti handler.endElement("", name, name); } - private void emitChildren(ArrayNode children, ContentHandler handler, String parentName) throws IOException, SAXException { + private void emitChildren(ArrayNode children, ContentHandler handler, String parentName) + throws IOException, SAXException { for (JsonNode child : children) { if (child == null || child.isNull()) { continue; @@ -99,7 +99,11 @@ private void emitChildren(ArrayNode children, ContentHandler handler, String par } if (!child.isObject()) { - throw new IOException("Unsupported child node type under element '" + parentName + "': " + child.getNodeType()); + throw new IOException( + "Unsupported child node type under element '" + + parentName + + "': " + + child.getNodeType()); } String childType = textOrNull(child.get("type")); @@ -113,7 +117,10 @@ private void emitChildren(ArrayNode children, ContentHandler handler, String par continue; } - throw new IOException("Unsupported object child under element '" + parentName + "': missing recognized 'type'"); + throw new IOException( + "Unsupported object child under element '" + + parentName + + "': missing recognized 'type'"); } } @@ -145,4 +152,3 @@ private String textOrNull(JsonNode node) { return node == null || node.isNull() ? null : node.asText(); } } - diff --git a/src/main/java/name/jurgenei/gradle/xml/saxon/SaxonXirResolvers.java b/src/main/java/name/jurgenei/gradle/xml/saxon/SaxonXirResolvers.java index a20e784..d9c9361 100644 --- a/src/main/java/name/jurgenei/gradle/xml/saxon/SaxonXirResolvers.java +++ b/src/main/java/name/jurgenei/gradle/xml/saxon/SaxonXirResolvers.java @@ -1,3 +1,4 @@ +/* (C)2026 */ package name.jurgenei.gradle.xml.saxon; import java.net.URI; @@ -10,9 +11,9 @@ import net.sf.saxon.Configuration; import net.sf.saxon.lib.ResourceRequest; import net.sf.saxon.lib.ResourceResolver; -import net.sf.saxon.s9api.SaxonApiException; -import net.sf.saxon.s9api.Processor; import net.sf.saxon.resource.SuppliedItemResource; +import net.sf.saxon.s9api.Processor; +import net.sf.saxon.s9api.SaxonApiException; import net.sf.saxon.trans.XPathException; import org.xml.sax.InputSource; @@ -24,8 +25,7 @@ public final class SaxonXirResolvers { private static final String SEXPR_MEDIA_TYPE = "application/x-xir+xml"; - private SaxonXirResolvers() { - } + private SaxonXirResolvers() {} /** * Configures resolver hooks on a Saxon processor. @@ -46,29 +46,38 @@ public static void configure(Processor processor) { public static void configure(net.sf.saxon.s9api.XsltCompiler compiler) { DualXirResolver resolver = new DualXirResolver(); compiler.setResourceResolver(resolver); - compiler.setURIResolver(resolver); } private static void registerXirCollectionFactory(Configuration configuration) { configuration.registerFileExtension("xir", SEXPR_MEDIA_TYPE); - configuration.registerMediaType(SEXPR_MEDIA_TYPE, (context, details) -> { - try { - Processor processor = new Processor(context.getConfiguration()); - net.sf.saxon.om.NodeInfo node = processor.newDocumentBuilder() - .build(new SAXSource(new XirReader(), new InputSource(details.resourceUri))) - .getUnderlyingNode(); - return new SuppliedItemResource(() -> node, details.resourceUri); - } catch (SaxonApiException ex) { - throw new XPathException("Failed to parse S-expression resource: " + details.resourceUri, ex); - } - }); + configuration.registerMediaType( + SEXPR_MEDIA_TYPE, + (context, details) -> { + try { + Processor processor = new Processor(context.getConfiguration()); + net.sf.saxon.om.NodeInfo node = + processor + .newDocumentBuilder() + .build( + new SAXSource( + new XirReader(), + new InputSource(details.resourceUri))) + .getUnderlyingNode(); + return new SuppliedItemResource(() -> node, details.resourceUri); + } catch (SaxonApiException ex) { + throw new XPathException( + "Failed to parse S-expression resource: " + details.resourceUri, + ex); + } + }); } private static final class DualXirResolver implements ResourceResolver, URIResolver { @Override public Source resolve(ResourceRequest request) throws XPathException { - String candidate = chooseCandidateUri(request.uri, request.relativeUri, request.baseUri); + String candidate = + chooseCandidateUri(request.uri, request.relativeUri, request.baseUri); if (!isXirReference(candidate)) { return null; } @@ -81,7 +90,9 @@ public Source resolve(String href, String base) throws TransformerException { try { candidate = chooseCandidateUri(null, href, base); } catch (IllegalArgumentException ex) { - throw new TransformerException("Invalid URI while resolving '" + href + "' against base '" + base + "'", ex); + throw new TransformerException( + "Invalid URI while resolving '" + href + "' against base '" + base + "'", + ex); } if (!isXirReference(candidate)) { return null; @@ -89,7 +100,8 @@ public Source resolve(String href, String base) throws TransformerException { return new SAXSource(new XirReader(), new InputSource(candidate)); } - private static String chooseCandidateUri(String absoluteUri, String relativeUri, String baseUri) { + private static String chooseCandidateUri( + String absoluteUri, String relativeUri, String baseUri) { if (notBlank(absoluteUri)) { return absoluteUri; } diff --git a/src/test/java/name/jurgenei/gradle/xml/SchematronSelExtractTaskIntegrationTest.java b/src/test/java/name/jurgenei/gradle/xml/SchematronSelExtractTaskIntegrationTest.java index 9fb141e..b104a7a 100644 --- a/src/test/java/name/jurgenei/gradle/xml/SchematronSelExtractTaskIntegrationTest.java +++ b/src/test/java/name/jurgenei/gradle/xml/SchematronSelExtractTaskIntegrationTest.java @@ -221,7 +221,7 @@ public void supportsCanonicalJsonInput() throws Exception { tasks.register('extractSel', name.jurgenei.gradle.xml.SchematronSelExtractTask) { schema 'src/main/schematron/sel.sch' source 'src/main/json/canonical.json' - jsonMode 'canonical' + outputMode 'canonical' outputDir.set(layout.buildDirectory.dir('out/sel')) groupOutput 'knowledge', 'sel/knowledge.xml' failOnError.set(true) @@ -286,7 +286,7 @@ public void supportsXirInputAndXirGroupOutputs() throws Exception { source 'src/main/xir/canonical.xir' outputDir.set(layout.buildDirectory.dir('out/sel')) groupOutput 'knowledge', 'sel/knowledge.xir' - xirFormat 'beautified' + outputFormat 'beautified' failOnError.set(true) } """); diff --git a/src/test/java/name/jurgenei/gradle/xml/XQueryTaskIntegrationTest.java b/src/test/java/name/jurgenei/gradle/xml/XQueryTaskIntegrationTest.java index 71b8e48..f136707 100644 --- a/src/test/java/name/jurgenei/gradle/xml/XQueryTaskIntegrationTest.java +++ b/src/test/java/name/jurgenei/gradle/xml/XQueryTaskIntegrationTest.java @@ -1,3 +1,4 @@ +/* (C)2026 */ package name.jurgenei.gradle.xml; import static org.junit.Assert.assertEquals; @@ -19,8 +20,7 @@ */ public class XQueryTaskIntegrationTest { - @Rule - public final TemporaryFolder testProjectDir = new TemporaryFolder(); + @Rule public final TemporaryFolder testProjectDir = new TemporaryFolder(); /** * Verifies external variable passing for a single source file transformation. @@ -30,7 +30,9 @@ public void transformsSingleFileWithParameters() throws IOException { write("settings.gradle", """ rootProject.name = 'xquery-test' """); - write("build.gradle", """ + write( + "build.gradle", + """ plugins { id 'name.jurgenei.gradle.xml' } tasks.register('runXQuery', name.jurgenei.gradle.xml.XQueryTask) { query 'src/main/xquery/main.xq' @@ -40,21 +42,26 @@ public void transformsSingleFileWithParameters() throws IOException { } """); - write("src/main/xml/input.xml", """ + write( + "src/main/xml/input.xml", + """ Gradle """); - write("src/main/xquery/main.xq", """ + write( + "src/main/xquery/main.xq", + """ declare variable $prefix external; { $prefix }{ /root/value/text() } """); - TaskOutcome outcome = newGradleRunner() - .withProjectDir(testProjectDir.getRoot()) - .withPluginClasspath() - .withArguments("runXQuery") - .build() - .task(":runXQuery") - .getOutcome(); + TaskOutcome outcome = + newGradleRunner() + .withProjectDir(testProjectDir.getRoot()) + .withPluginClasspath() + .withArguments("runXQuery") + .build() + .task(":runXQuery") + .getOutcome(); assertEquals(TaskOutcome.SUCCESS, outcome); @@ -68,10 +75,14 @@ public void transformsSingleFileWithParameters() throws IOException { */ @Test public void infersJsonOutputMethodFromExtension() throws IOException { - write("settings.gradle", """ + write( + "settings.gradle", + """ rootProject.name = 'xquery-json-method-test' """); - write("build.gradle", """ + write( + "build.gradle", + """ plugins { id 'name.jurgenei.gradle.xml' } tasks.register('runXQuery', name.jurgenei.gradle.xml.XQueryTask) { query 'src/main/xquery/main.xq' @@ -81,20 +92,25 @@ public void infersJsonOutputMethodFromExtension() throws IOException { } """); - write("src/main/xml/input.xml", """ + write( + "src/main/xml/input.xml", + """ Gradle """); - write("src/main/xquery/main.xq", """ + write( + "src/main/xquery/main.xq", + """ map { 'value': data(/root/value) } """); - TaskOutcome outcome = newGradleRunner() - .withProjectDir(testProjectDir.getRoot()) - .withPluginClasspath() - .withArguments("runXQuery") - .build() - .task(":runXQuery") - .getOutcome(); + TaskOutcome outcome = + newGradleRunner() + .withProjectDir(testProjectDir.getRoot()) + .withPluginClasspath() + .withArguments("runXQuery") + .build() + .task(":runXQuery") + .getOutcome(); assertEquals(TaskOutcome.SUCCESS, outcome); @@ -105,10 +121,14 @@ public void infersJsonOutputMethodFromExtension() throws IOException { @Test public void autoModeWritesHierarchicalJsonForXmlNodeResults() throws IOException { - write("settings.gradle", """ + write( + "settings.gradle", + """ rootProject.name = 'xquery-json-auto-hierarchical-test' """); - write("build.gradle", """ + write( + "build.gradle", + """ plugins { id 'name.jurgenei.gradle.xml' } tasks.register('runXQuery', name.jurgenei.gradle.xml.XQueryTask) { query 'src/main/xquery/main.xq' @@ -122,17 +142,20 @@ public void autoModeWritesHierarchicalJsonForXmlNodeResults() throws IOException write("src/main/xml/input.xml", "XML"); write("src/main/xquery/main.xq", "."); - TaskOutcome outcome = newGradleRunner() - .withProjectDir(testProjectDir.getRoot()) - .withPluginClasspath() - .withArguments("runXQuery") - .build() - .task(":runXQuery") - .getOutcome(); + TaskOutcome outcome = + newGradleRunner() + .withProjectDir(testProjectDir.getRoot()) + .withPluginClasspath() + .withArguments("runXQuery") + .build() + .task(":runXQuery") + .getOutcome(); assertEquals(TaskOutcome.SUCCESS, outcome); String output = read(new File(testProjectDir.getRoot(), "build/out/xquery/input.json")); - assertTrue(output.contains("\"type\":\"element\"") || output.contains("\"type\" : \"element\"")); + assertTrue( + output.contains("\"type\":\"element\"") + || output.contains("\"type\" : \"element\"")); assertTrue(output.contains("\"name\":\"book\"") || output.contains("\"name\" : \"book\"")); assertTrue(!output.contains("\"attributes\" : { }")); assertTrue(!output.contains("\"attributes\":{}")); @@ -140,10 +163,14 @@ public void autoModeWritesHierarchicalJsonForXmlNodeResults() throws IOException @Test public void nativeModeWritesHierarchicalJsonForXmlNodeResults() throws IOException { - write("settings.gradle", """ + write( + "settings.gradle", + """ rootProject.name = 'xquery-json-native-hierarchical-test' """); - write("build.gradle", """ + write( + "build.gradle", + """ plugins { id 'name.jurgenei.gradle.xml' } tasks.register('runXQuery', name.jurgenei.gradle.xml.XQueryTask) { query 'src/main/xquery/main.xq' @@ -157,17 +184,20 @@ public void nativeModeWritesHierarchicalJsonForXmlNodeResults() throws IOExcepti write("src/main/xml/input.xml", "XML"); write("src/main/xquery/main.xq", "."); - TaskOutcome outcome = newGradleRunner() - .withProjectDir(testProjectDir.getRoot()) - .withPluginClasspath() - .withArguments("runXQuery") - .build() - .task(":runXQuery") - .getOutcome(); + TaskOutcome outcome = + newGradleRunner() + .withProjectDir(testProjectDir.getRoot()) + .withPluginClasspath() + .withArguments("runXQuery") + .build() + .task(":runXQuery") + .getOutcome(); assertEquals(TaskOutcome.SUCCESS, outcome); String output = read(new File(testProjectDir.getRoot(), "build/out/xquery/input.json")); - assertTrue(output.contains("\"type\":\"element\"") || output.contains("\"type\" : \"element\"")); + assertTrue( + output.contains("\"type\":\"element\"") + || output.contains("\"type\" : \"element\"")); assertTrue(output.contains("\"name\":\"book\"") || output.contains("\"name\" : \"book\"")); assertTrue(!output.contains("\"attributes\" : { }")); assertTrue(!output.contains("\"attributes\":{}")); @@ -178,10 +208,14 @@ public void nativeModeWritesHierarchicalJsonForXmlNodeResults() throws IOExcepti */ @Test public void usesExplicitTextOutputMethod() throws IOException { - write("settings.gradle", """ + write( + "settings.gradle", + """ rootProject.name = 'xquery-text-method-test' """); - write("build.gradle", """ + write( + "build.gradle", + """ plugins { id 'name.jurgenei.gradle.xml' } tasks.register('runXQuery', name.jurgenei.gradle.xml.XQueryTask) { query 'src/main/xquery/main.xq' @@ -192,20 +226,25 @@ public void usesExplicitTextOutputMethod() throws IOException { } """); - write("src/main/xml/input.xml", """ + write( + "src/main/xml/input.xml", + """ Gradle """); - write("src/main/xquery/main.xq", """ + write( + "src/main/xquery/main.xq", + """ concat('VALUE=', data(/root/value)) """); - TaskOutcome outcome = newGradleRunner() - .withProjectDir(testProjectDir.getRoot()) - .withPluginClasspath() - .withArguments("runXQuery") - .build() - .task(":runXQuery") - .getOutcome(); + TaskOutcome outcome = + newGradleRunner() + .withProjectDir(testProjectDir.getRoot()) + .withPluginClasspath() + .withArguments("runXQuery") + .build() + .task(":runXQuery") + .getOutcome(); assertEquals(TaskOutcome.SUCCESS, outcome); @@ -219,10 +258,14 @@ public void usesExplicitTextOutputMethod() throws IOException { */ @Test public void transformsSingleFileWithExplicitInputOutput() throws IOException { - write("settings.gradle", """ + write( + "settings.gradle", + """ rootProject.name = 'xquery-explicit-io-test' """); - write("build.gradle", """ + write( + "build.gradle", + """ plugins { id 'name.jurgenei.gradle.xml' } tasks.register('runXQuery', name.jurgenei.gradle.xml.XQueryTask) { query 'src/main/xquery/main.xq' @@ -231,20 +274,25 @@ public void transformsSingleFileWithExplicitInputOutput() throws IOException { } """); - write("src/main/xml/a.xml", """ + write( + "src/main/xml/a.xml", + """ Gradle """); - write("src/main/xquery/main.xq", """ + write( + "src/main/xquery/main.xq", + """ { /root/value/text() } """); - TaskOutcome outcome = newGradleRunner() - .withProjectDir(testProjectDir.getRoot()) - .withPluginClasspath() - .withArguments("runXQuery") - .build() - .task(":runXQuery") - .getOutcome(); + TaskOutcome outcome = + newGradleRunner() + .withProjectDir(testProjectDir.getRoot()) + .withPluginClasspath() + .withArguments("runXQuery") + .build() + .task(":runXQuery") + .getOutcome(); assertEquals(TaskOutcome.SUCCESS, outcome); @@ -258,10 +306,14 @@ public void transformsSingleFileWithExplicitInputOutput() throws IOException { */ @Test public void transformsFileTreeWithPatternsAndWorkers() throws IOException { - write("settings.gradle", """ + write( + "settings.gradle", + """ rootProject.name = 'xquery-tree-test' """); - write("build.gradle", """ + write( + "build.gradle", + """ plugins { id 'name.jurgenei.gradle.xml' } tasks.register('runXQuery', name.jurgenei.gradle.xml.XQueryTask) { query 'src/main/xquery/main.xq' @@ -278,21 +330,27 @@ public void transformsFileTreeWithPatternsAndWorkers() throws IOException { write("src/main/xml/a.xml", """ A """); - write("src/main/xml/foo/b.xml", """ + write( + "src/main/xml/foo/b.xml", + """ B """); - write("src/main/xml/skip.xml", """ + write( + "src/main/xml/skip.xml", + """ SKIP """); - write("src/main/xquery/main.xq", """ + write( + "src/main/xquery/main.xq", + """ { /root/value/text() } """); newGradleRunner() - .withProjectDir(testProjectDir.getRoot()) - .withPluginClasspath() - .withArguments("runXQuery") - .build(); + .withProjectDir(testProjectDir.getRoot()) + .withPluginClasspath() + .withArguments("runXQuery") + .build(); File outputA = new File(testProjectDir.getRoot(), "build/out/xquery/a.out.xml"); File outputB = new File(testProjectDir.getRoot(), "build/out/xquery/foo/b.out.xml"); @@ -310,10 +368,14 @@ public void transformsFileTreeWithPatternsAndWorkers() throws IOException { */ @Test public void skipsTransformationWhenOutputIsNewerThanSourceAndQuery() throws IOException { - write("settings.gradle", """ + write( + "settings.gradle", + """ rootProject.name = 'xquery-skip-test' """); - write("build.gradle", """ + write( + "build.gradle", + """ plugins { id 'name.jurgenei.gradle.xml' } tasks.register('runXQuery', name.jurgenei.gradle.xml.XQueryTask) { query 'src/main/xquery/main.xq' @@ -322,18 +384,23 @@ public void skipsTransformationWhenOutputIsNewerThanSourceAndQuery() throws IOEx } """); - write("src/main/xml/input.xml", """ + write( + "src/main/xml/input.xml", + """ Gradle """); - write("src/main/xquery/main.xq", """ + write( + "src/main/xquery/main.xq", + """ { /root/value/text() } """); - BuildResult firstRun = newGradleRunner() - .withProjectDir(testProjectDir.getRoot()) - .withPluginClasspath() - .withArguments("runXQuery", "--rerun-tasks") - .build(); + BuildResult firstRun = + newGradleRunner() + .withProjectDir(testProjectDir.getRoot()) + .withPluginClasspath() + .withArguments("runXQuery", "--rerun-tasks") + .build(); File output = new File(testProjectDir.getRoot(), "build/out/xquery/input.xml"); assertTrue(output.exists()); @@ -342,11 +409,12 @@ public void skipsTransformationWhenOutputIsNewerThanSourceAndQuery() throws IOEx long futureTimestamp = System.currentTimeMillis() + 60_000; assertTrue(output.setLastModified(futureTimestamp)); - BuildResult secondRun = newGradleRunner() - .withProjectDir(testProjectDir.getRoot()) - .withPluginClasspath() - .withArguments("runXQuery", "--rerun-tasks") - .build(); + BuildResult secondRun = + newGradleRunner() + .withProjectDir(testProjectDir.getRoot()) + .withPluginClasspath() + .withArguments("runXQuery", "--rerun-tasks") + .build(); assertTrue(secondRun.getOutput().contains("[SKIP]")); } @@ -356,10 +424,14 @@ public void skipsTransformationWhenOutputIsNewerThanSourceAndQuery() throws IOEx */ @Test public void skipsTransformationWhenOutputIsNewerEvenIfParamsChange() throws IOException { - write("settings.gradle", """ + write( + "settings.gradle", + """ rootProject.name = 'xquery-param-fingerprint-test' """); - write("build.gradle", """ + write( + "build.gradle", + """ plugins { id 'name.jurgenei.gradle.xml' } tasks.register('runXQuery', name.jurgenei.gradle.xml.XQueryTask) { query 'src/main/xquery/main.xq' @@ -369,19 +441,24 @@ public void skipsTransformationWhenOutputIsNewerEvenIfParamsChange() throws IOEx } """); - write("src/main/xml/input.xml", """ + write( + "src/main/xml/input.xml", + """ Gradle """); - write("src/main/xquery/main.xq", """ + write( + "src/main/xquery/main.xq", + """ declare variable $prefix external; { $prefix }{ /root/value/text() } """); - BuildResult firstRun = newGradleRunner() - .withProjectDir(testProjectDir.getRoot()) - .withPluginClasspath() - .withArguments("runXQuery", "--rerun-tasks") - .build(); + BuildResult firstRun = + newGradleRunner() + .withProjectDir(testProjectDir.getRoot()) + .withPluginClasspath() + .withArguments("runXQuery", "--rerun-tasks") + .build(); File output = new File(testProjectDir.getRoot(), "build/out/xquery/input.xml"); assertTrue(output.exists()); @@ -391,7 +468,9 @@ public void skipsTransformationWhenOutputIsNewerEvenIfParamsChange() throws IOEx long futureTimestamp = System.currentTimeMillis() + 60_000; assertTrue(output.setLastModified(futureTimestamp)); - write("build.gradle", """ + write( + "build.gradle", + """ plugins { id 'name.jurgenei.gradle.xml' } tasks.register('runXQuery', name.jurgenei.gradle.xml.XQueryTask) { query 'src/main/xquery/main.xq' @@ -401,11 +480,12 @@ public void skipsTransformationWhenOutputIsNewerEvenIfParamsChange() throws IOEx } """); - BuildResult secondRun = newGradleRunner() - .withProjectDir(testProjectDir.getRoot()) - .withPluginClasspath() - .withArguments("runXQuery", "--rerun-tasks") - .build(); + BuildResult secondRun = + newGradleRunner() + .withProjectDir(testProjectDir.getRoot()) + .withPluginClasspath() + .withArguments("runXQuery", "--rerun-tasks") + .build(); assertTrue(secondRun.getOutput().contains("[SKIP]")); assertTrue(read(output).contains("Hello Gradle")); @@ -414,7 +494,9 @@ public void skipsTransformationWhenOutputIsNewerEvenIfParamsChange() throws IOEx @Test public void transformsXirInputWithXQuery() throws IOException { write("settings.gradle", "rootProject.name = 'xquery-xir-input-test'"); - write("build.gradle", """ + write( + "build.gradle", + """ plugins { id 'name.jurgenei.gradle.xml' } tasks.register('runXQuery', name.jurgenei.gradle.xml.XQueryTask) { query 'src/main/xquery/main.xq' @@ -425,13 +507,14 @@ public void transformsXirInputWithXQuery() throws IOException { write("src/main/xir/input.xir", "(book { id \"b1\" } (title \"XML\"))"); write("src/main/xquery/main.xq", "{/book/title/text()}"); - TaskOutcome outcome = newGradleRunner() - .withProjectDir(testProjectDir.getRoot()) - .withPluginClasspath() - .withArguments("runXQuery") - .build() - .task(":runXQuery") - .getOutcome(); + TaskOutcome outcome = + newGradleRunner() + .withProjectDir(testProjectDir.getRoot()) + .withPluginClasspath() + .withArguments("runXQuery") + .build() + .task(":runXQuery") + .getOutcome(); assertEquals(TaskOutcome.SUCCESS, outcome); String output = read(new File(testProjectDir.getRoot(), "build/out/xquery/input.xml")); @@ -441,7 +524,9 @@ public void transformsXirInputWithXQuery() throws IOException { @Test public void resolvesXirViaDocFunction() throws IOException { write("settings.gradle", "rootProject.name = 'xquery-doc-xir-test'"); - write("build.gradle", """ + write( + "build.gradle", + """ plugins { id 'name.jurgenei.gradle.xml' } tasks.register('runXQuery', name.jurgenei.gradle.xml.XQueryTask) { query 'src/main/xquery/main.xq' @@ -451,18 +536,23 @@ public void resolvesXirViaDocFunction() throws IOException { """); write("src/main/xml/input.xml", ""); write("src/main/xir/lookup.xir", "(lookup (value \"from-xir\"))"); - String lookupUri = new File(testProjectDir.getRoot(), "src/main/xir/lookup.xir").toURI().toString(); - write("src/main/xquery/main.xq", """ + String lookupUri = + new File(testProjectDir.getRoot(), "src/main/xir/lookup.xir").toURI().toString(); + write( + "src/main/xquery/main.xq", + """ { doc("%s")/lookup/value/text() } - """.formatted(lookupUri)); - - TaskOutcome outcome = newGradleRunner() - .withProjectDir(testProjectDir.getRoot()) - .withPluginClasspath() - .withArguments("runXQuery") - .build() - .task(":runXQuery") - .getOutcome(); + """ + .formatted(lookupUri)); + + TaskOutcome outcome = + newGradleRunner() + .withProjectDir(testProjectDir.getRoot()) + .withPluginClasspath() + .withArguments("runXQuery") + .build() + .task(":runXQuery") + .getOutcome(); assertEquals(TaskOutcome.SUCCESS, outcome); String output = read(new File(testProjectDir.getRoot(), "build/out/xquery/input.xml")); @@ -472,7 +562,9 @@ public void resolvesXirViaDocFunction() throws IOException { @Test public void resolvesXirViaCollectionFunction() throws IOException { write("settings.gradle", "rootProject.name = 'xquery-collection-xir-test'"); - write("build.gradle", """ + write( + "build.gradle", + """ plugins { id 'name.jurgenei.gradle.xml' } tasks.register('runXQuery', name.jurgenei.gradle.xml.XQueryTask) { query 'src/main/xquery/main.xq' @@ -483,20 +575,25 @@ public void resolvesXirViaCollectionFunction() throws IOException { write("src/main/xml/input.xml", ""); write("src/main/xir/a.xir", "(item (name \"A\"))"); write("src/main/xir/b.xir", "(item (name \"B\"))"); - String collectionUri = new File(testProjectDir.getRoot(), "src/main/xir/").toURI().toString() - + "?select=*.xir;recurse=no"; - write("src/main/xquery/main.xq", """ + String collectionUri = + new File(testProjectDir.getRoot(), "src/main/xir/").toURI().toString() + + "?select=*.xir;recurse=no"; + write( + "src/main/xquery/main.xq", + """ let $docs := collection("%s")/item return { for $d in $docs return {$d/name/text()} } - """.formatted(collectionUri)); - - TaskOutcome outcome = newGradleRunner() - .withProjectDir(testProjectDir.getRoot()) - .withPluginClasspath() - .withArguments("runXQuery") - .build() - .task(":runXQuery") - .getOutcome(); + """ + .formatted(collectionUri)); + + TaskOutcome outcome = + newGradleRunner() + .withProjectDir(testProjectDir.getRoot()) + .withPluginClasspath() + .withArguments("runXQuery") + .build() + .task(":runXQuery") + .getOutcome(); assertEquals(TaskOutcome.SUCCESS, outcome); String output = read(new File(testProjectDir.getRoot(), "build/out/xquery/input.xml")); @@ -508,7 +605,9 @@ public void resolvesXirViaCollectionFunction() throws IOException { @Test public void writesXirOutputWithXQuery() throws IOException { write("settings.gradle", "rootProject.name = 'xquery-xir-output-test'"); - write("build.gradle", """ + write( + "build.gradle", + """ plugins { id 'name.jurgenei.gradle.xml' } tasks.register('runXQuery', name.jurgenei.gradle.xml.XQueryTask) { query 'src/main/xquery/main.xq' @@ -518,15 +617,18 @@ public void writesXirOutputWithXQuery() throws IOException { } """); write("src/main/xml/input.xml", "XML"); - write("src/main/xquery/main.xq", "{/book/title/text()}"); - - TaskOutcome outcome = newGradleRunner() - .withProjectDir(testProjectDir.getRoot()) - .withPluginClasspath() - .withArguments("runXQuery") - .build() - .task(":runXQuery") - .getOutcome(); + write( + "src/main/xquery/main.xq", + "{/book/title/text()}"); + + TaskOutcome outcome = + newGradleRunner() + .withProjectDir(testProjectDir.getRoot()) + .withPluginClasspath() + .withArguments("runXQuery") + .build() + .task(":runXQuery") + .getOutcome(); assertEquals(TaskOutcome.SUCCESS, outcome); String output = read(new File(testProjectDir.getRoot(), "build/out/xquery/input.xir")); @@ -538,7 +640,9 @@ public void writesXirOutputWithXQuery() throws IOException { @Test public void writesBeautifiedXirOutputWithXQuery() throws IOException { write("settings.gradle", "rootProject.name = 'xquery-xir-output-beautified-test'"); - write("build.gradle", """ + write( + "build.gradle", + """ plugins { id 'name.jurgenei.gradle.xml' } tasks.register('runXQuery', name.jurgenei.gradle.xml.XQueryTask) { query 'src/main/xquery/main.xq' @@ -549,15 +653,18 @@ public void writesBeautifiedXirOutputWithXQuery() throws IOException { } """); write("src/main/xml/input.xml", "XML"); - write("src/main/xquery/main.xq", "{/book/title/text()}"); - - TaskOutcome outcome = newGradleRunner() - .withProjectDir(testProjectDir.getRoot()) - .withPluginClasspath() - .withArguments("runXQuery") - .build() - .task(":runXQuery") - .getOutcome(); + write( + "src/main/xquery/main.xq", + "{/book/title/text()}"); + + TaskOutcome outcome = + newGradleRunner() + .withProjectDir(testProjectDir.getRoot()) + .withPluginClasspath() + .withArguments("runXQuery") + .build() + .task(":runXQuery") + .getOutcome(); assertEquals(TaskOutcome.SUCCESS, outcome); String output = read(new File(testProjectDir.getRoot(), "build/out/xquery/input.xir")); @@ -569,7 +676,9 @@ public void writesBeautifiedXirOutputWithXQuery() throws IOException { @Test public void transformsCanonicalJsonInputWithXQuery() throws IOException { write("settings.gradle", "rootProject.name = 'xquery-json-input-canonical-test'"); - write("build.gradle", """ + write( + "build.gradle", + """ plugins { id 'name.jurgenei.gradle.xml' } tasks.register('runXQuery', name.jurgenei.gradle.xml.XQueryTask) { query 'src/main/xquery/main.xq' @@ -578,7 +687,9 @@ public void transformsCanonicalJsonInputWithXQuery() throws IOException { jsonMode.set('canonical') } """); - write("src/main/json/input.json", """ + write( + "src/main/json/input.json", + """ { "type": "element", "name": "book", @@ -597,13 +708,14 @@ public void transformsCanonicalJsonInputWithXQuery() throws IOException { """); write("src/main/xquery/main.xq", "{/book/title/text()}"); - TaskOutcome outcome = newGradleRunner() - .withProjectDir(testProjectDir.getRoot()) - .withPluginClasspath() - .withArguments("runXQuery") - .build() - .task(":runXQuery") - .getOutcome(); + TaskOutcome outcome = + newGradleRunner() + .withProjectDir(testProjectDir.getRoot()) + .withPluginClasspath() + .withArguments("runXQuery") + .build() + .task(":runXQuery") + .getOutcome(); assertEquals(TaskOutcome.SUCCESS, outcome); String output = read(new File(testProjectDir.getRoot(), "build/out/xquery/input.xml")); @@ -613,27 +725,32 @@ public void transformsCanonicalJsonInputWithXQuery() throws IOException { @Test public void writesCanonicalJsonOutputWithXQuery() throws IOException { write("settings.gradle", "rootProject.name = 'xquery-json-output-canonical-test'"); - write("build.gradle", """ + write( + "build.gradle", + """ plugins { id 'name.jurgenei.gradle.xml' } tasks.register('runXQuery', name.jurgenei.gradle.xml.XQueryTask) { query 'src/main/xquery/main.xq' source 'src/main/xml/input.xml' outputDir.set(layout.buildDirectory.dir('out/xquery')) outputExtension.set('.json') - jsonMode.set('canonical') - xirFormat.set('beautified') + outputMode('canonical') + outputFormat.set('beautified') } """); write("src/main/xml/input.xml", "XML"); - write("src/main/xquery/main.xq", "{/book/title/text()}"); - - TaskOutcome outcome = newGradleRunner() - .withProjectDir(testProjectDir.getRoot()) - .withPluginClasspath() - .withArguments("runXQuery") - .build() - .task(":runXQuery") - .getOutcome(); + write( + "src/main/xquery/main.xq", + "{/book/title/text()}"); + + TaskOutcome outcome = + newGradleRunner() + .withProjectDir(testProjectDir.getRoot()) + .withPluginClasspath() + .withArguments("runXQuery") + .build() + .task(":runXQuery") + .getOutcome(); assertEquals(TaskOutcome.SUCCESS, outcome); String output = read(new File(testProjectDir.getRoot(), "build/out/xquery/input.json")); @@ -647,7 +764,9 @@ public void writesCanonicalJsonOutputWithXQuery() throws IOException { @Test public void roundtripsXmlThroughCanonicalJsonWithXQuery() throws IOException { write("settings.gradle", "rootProject.name = 'xquery-json-roundtrip-canonical-test'"); - write("build.gradle", """ + write( + "build.gradle", + """ plugins { id 'name.jurgenei.gradle.xml' } tasks.register('xmlToJson', name.jurgenei.gradle.xml.XQueryTask) { query 'src/main/xquery/main.xq' @@ -667,13 +786,14 @@ public void roundtripsXmlThroughCanonicalJsonWithXQuery() throws IOException { write("src/main/xml/input.xml", "XML"); write("src/main/xquery/main.xq", "."); - TaskOutcome outcome = newGradleRunner() - .withProjectDir(testProjectDir.getRoot()) - .withPluginClasspath() - .withArguments("jsonToXml") - .build() - .task(":jsonToXml") - .getOutcome(); + TaskOutcome outcome = + newGradleRunner() + .withProjectDir(testProjectDir.getRoot()) + .withPluginClasspath() + .withArguments("jsonToXml") + .build() + .task(":jsonToXml") + .getOutcome(); assertEquals(TaskOutcome.SUCCESS, outcome); String output = read(new File(testProjectDir.getRoot(), "build/out/xml/result.xml")); diff --git a/src/test/java/name/jurgenei/gradle/xml/XsdTaskIntegrationTest.java b/src/test/java/name/jurgenei/gradle/xml/XsdTaskIntegrationTest.java index 0ea7db0..d14dead 100644 --- a/src/test/java/name/jurgenei/gradle/xml/XsdTaskIntegrationTest.java +++ b/src/test/java/name/jurgenei/gradle/xml/XsdTaskIntegrationTest.java @@ -35,7 +35,7 @@ public void emitsSvrlAndJunitForInvalidInput() throws IOException { schema 'src/main/xsd/schema.xsd' source 'src/main/xml/invalid.xml' outputDir.set(layout.buildDirectory.dir('out/xsd')) - reportFormat.set(name.jurgenei.gradle.xml.validation.ReportFormat.SVRL_AND_JUNIT) + format(name.jurgenei.gradle.xml.validation.ReportFormat.SVRL_AND_JUNIT) failOnError.set(false) engine.set(name.jurgenei.gradle.xml.validation.XsdEngine.AUTO) }