From 1989df960d611ae9cf824d4ce1dcebc1754bdb53 Mon Sep 17 00:00:00 2001 From: Jurgen Hildebrand Date: Mon, 5 Oct 2026 20:03:25 +0200 Subject: [PATCH 1/6] normalized to outputMode, outputFormat --- README.md | 29 ++--- .../gradle/xml/AbstractXmlTransformTask.java | 116 +++++++++++++++++- .../gradle/xml/AbstractXmlValidationTask.java | 28 +++++ .../gradle/xml/SchematronSelExtractTask.java | 113 +++++++++++++++++ ...hematronSelExtractTaskIntegrationTest.java | 4 +- .../gradle/xml/XQueryTaskIntegrationTest.java | 4 +- .../gradle/xml/XsdTaskIntegrationTest.java | 2 +- 7 files changed, 275 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index b7670cd..f857e22 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,8 @@ S-expression runtime ships inside `gradle-xml-plugin` artifact. - 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`. +- `outputFormat` controls output style: `compact` (default) or `beautified`. + (`xirFormat` and `xformat` remain supported as legacy aliases.) XIR format details: @@ -117,7 +118,7 @@ Internal bridge note: - 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. -`xirFormat` is also reused for canonical JSON output formatting. +`outputFormat` is also reused for canonical JSON output formatting. Format conventions: @@ -154,9 +155,9 @@ tasks.register('xirToXml', name.jurgenei.gradle.xml.XsltTask) { - 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`. +- `outputFormat` controls canonical JSON output style too: `compact` or `beautified`. -Set JSON routing mode with `jsonMode`: +Set JSON routing mode with `outputMode` (`jsonMode` and `mode` remain supported as legacy aliases): - `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 @@ -170,15 +171,15 @@ tasks.register('xmlToJsonCanonical', name.jurgenei.gradle.xml.XsltTask) { source 'src/main/xml/input.xml' outputDir.set(layout.buildDirectory.dir('out/json')) outputExtension.set('.json') - jsonMode.set('canonical') - xirFormat.set('beautified') + outputMode('canonical') + outputFormat.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') } ``` @@ -192,7 +193,7 @@ tasks.register("xmlToXir") { source("src/main/xml/input.xml") outputDir.set(layout.buildDirectory.dir("out/xslt")) outputExtension.set(".xir") - xirFormat.set("beautified") + outputFormat.set("beautified") } ``` @@ -204,7 +205,7 @@ tasks.register('xmlToXir', name.jurgenei.gradle.xml.XsltTask) { source 'src/main/xml/input.xml' outputDir.set(layout.buildDirectory.dir('out/xslt')) outputExtension.set('.xir') - xirFormat.set('beautified') + outputFormat.set('beautified') } ``` @@ -227,7 +228,7 @@ Validation tasks share a common contract (`ValidationTaskSpec`) and defaults: - `outputExtension = '.svrl.xml'` - `workers = 1` -- `reportFormat = SVRL` +- `format = SVRL` (`reportFormat` remains supported as legacy alias) - `failOnError = true` - `junitOutputDir = build/reports/xml-validation/junit` @@ -374,7 +375,7 @@ tasks.register('validateSchematron', name.jurgenei.gradle.xml.SchematronTask) { 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) + format(name.jurgenei.gradle.xml.validation.ReportFormat.SVRL_AND_JUNIT) // Optional SchXslt transpiler parameters. phase.set('#ALL') severityThreshold.set('warning') @@ -386,7 +387,7 @@ 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) + format(name.jurgenei.gradle.xml.validation.ReportFormat.SVRL_AND_JUNIT) engine.set(name.jurgenei.gradle.xml.validation.XsdEngine.AUTO) } ``` @@ -539,8 +540,8 @@ tasks.register('extractSel', name.jurgenei.gradle.xml.SchematronSelExtractTask) 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') + // outputFormat.set('beautified') + outputMode('auto') failOnError.set(true) } ``` diff --git a/src/main/java/name/jurgenei/gradle/xml/AbstractXmlTransformTask.java b/src/main/java/name/jurgenei/gradle/xml/AbstractXmlTransformTask.java index 80bef6f..f399bc7 100644 --- a/src/main/java/name/jurgenei/gradle/xml/AbstractXmlTransformTask.java +++ b/src/main/java/name/jurgenei/gradle/xml/AbstractXmlTransformTask.java @@ -23,9 +23,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 +127,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 +158,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 +277,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 +322,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() { @@ -584,4 +697,3 @@ private Map resolveRelativePaths() { */ 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..5230f5b 100644 --- a/src/main/java/name/jurgenei/gradle/xml/AbstractXmlValidationTask.java +++ b/src/main/java/name/jurgenei/gradle/xml/AbstractXmlValidationTask.java @@ -60,6 +60,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(); @@ -91,6 +101,24 @@ public AbstractXmlValidationTask() { 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); + } + /** * Adds a validation parameter available to concrete engines. * 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/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..e85b38c 100644 --- a/src/test/java/name/jurgenei/gradle/xml/XQueryTaskIntegrationTest.java +++ b/src/test/java/name/jurgenei/gradle/xml/XQueryTaskIntegrationTest.java @@ -620,8 +620,8 @@ public void writesCanonicalJsonOutputWithXQuery() throws IOException { 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"); 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) } From 32c7af676d7bd6f2b7052492188c94ea38045cf4 Mon Sep 17 00:00:00 2001 From: Jurgen Hildebrand Date: Mon, 5 Oct 2026 20:04:52 +0200 Subject: [PATCH 2/6] spotless fix --- .../gradle/xml/AbstractXmlTransformTask.java | 113 ++-- .../gradle/xml/AbstractXmlValidationTask.java | 103 ++-- .../gradle/xml/XQueryTaskIntegrationTest.java | 486 +++++++++++------- 3 files changed, 451 insertions(+), 251 deletions(-) diff --git a/src/main/java/name/jurgenei/gradle/xml/AbstractXmlTransformTask.java b/src/main/java/name/jurgenei/gradle/xml/AbstractXmlTransformTask.java index f399bc7..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; @@ -375,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); @@ -415,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)); } } @@ -427,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; } @@ -449,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 { @@ -473,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; } @@ -541,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; } /** @@ -558,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; @@ -644,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; } @@ -695,5 +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 5230f5b..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 @@ -98,7 +100,12 @@ 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")); } /** @@ -182,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 { @@ -210,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); @@ -227,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); } @@ -245,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); } @@ -263,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); } @@ -285,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; } @@ -305,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/test/java/name/jurgenei/gradle/xml/XQueryTaskIntegrationTest.java b/src/test/java/name/jurgenei/gradle/xml/XQueryTaskIntegrationTest.java index e85b38c..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,7 +725,9 @@ 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' @@ -625,15 +739,18 @@ public void writesCanonicalJsonOutputWithXQuery() 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.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")); From 925fd1f7e8896f1e06c4277a3c5b0619163e6e34 Mon Sep 17 00:00:00 2001 From: Jurgen Hildebrand Date: Mon, 5 Oct 2026 20:36:27 +0200 Subject: [PATCH 3/6] rewrite README.md --- README.md | 867 ++++++++++++++++++------------------------------------ 1 file changed, 281 insertions(+), 586 deletions(-) diff --git a/README.md b/README.md index f857e22..d8cf006 100644 --- a/README.md +++ b/README.md @@ -1,718 +1,451 @@ -# Gradle XML Transform Plugin +# Gradle XML Validate and Transform Plugin ![Conformance](https://img.shields.io/badge/Conformance-Check--All%20Passing-brightgreen) - [![Plugin Portal](https://img.shields.io/gradle-plugin-portal/v/name.jurgenei.gradle.xml?label=Plugin%20Portal)](https://plugins.gradle.org/plugin/name.jurgenei.gradle.xml) [![Build and Test](https://github.com/jurgenei/gradle-xml-plugin/actions/workflows/gradle-build.yml/badge.svg)](https://github.com/jurgenei/gradle-xml-plugin/actions/workflows/gradle-build.yml) -[![JUnit Report](https://img.shields.io/badge/JUnit-Report-blue?logo=githubactions)](https://github.com/jurgenei/gradle-xml-plugin/actions/workflows/gradle-build.yml) [![Coverage CI](https://github.com/jurgenei/gradle-xml-plugin/actions/workflows/coverage.yml/badge.svg)](https://github.com/jurgenei/gradle-xml-plugin/actions/workflows/coverage.yml) [![CodeQL](https://github.com/jurgenei/gradle-xml-plugin/actions/workflows/codeql.yml/badge.svg)](https://github.com/jurgenei/gradle-xml-plugin/actions/workflows/codeql.yml) [![Dependency Check](https://github.com/jurgenei/gradle-xml-plugin/actions/workflows/dependency-check.yml/badge.svg)](https://github.com/jurgenei/gradle-xml-plugin/actions/workflows/dependency-check.yml) [![SpotBugs Security](https://github.com/jurgenei/gradle-xml-plugin/actions/workflows/spotbugs-security.yml/badge.svg)](https://github.com/jurgenei/gradle-xml-plugin/actions/workflows/spotbugs-security.yml) [![Checkstyle](https://github.com/jurgenei/gradle-xml-plugin/actions/workflows/checkstyle.yml/badge.svg)](https://github.com/jurgenei/gradle-xml-plugin/actions/workflows/checkstyle.yml) -[![Dependabot](https://img.shields.io/badge/dependabot-enabled-025E8C?logo=dependabot)](https://github.com/jurgenei/gradle-xml-plugin/security/dependabot) -[![Coverage](https://codecov.io/gh/jurgenei/gradle-xml-plugin/graph/badge.svg?branch=main)](https://codecov.io/gh/jurgenei/gradle-xml-plugin) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![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. + +## Introduction and Overview -## Overview +Use this plugin when project needs repeatable XML processing in build pipeline: -Define and execute XPath/XSLT/XQuery transformations and XML validations as Gradle tasks with: +- 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 -- 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 +Main task types: -The plugin contributes task types: +- `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` -- `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 +Plugin ID: `name.jurgenei.gradle.xml` -Both share a near-orthogonal API for unified Gradle-style configuration. +```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. -- `outputFormat` controls output style: `compact` (default) or `beautified`. - (`xirFormat` and `xformat` remain supported as legacy aliases.) +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: + +- XIR parser/serializer ships inside plugin artifact. +- No extra runtime dependency needed. + +## Transformations -- `(map ...)` and `(array ...)` are XML elements named `map`/`array`. -- XDM map/array nodes use explicit heads: `xdm:map` and `xdm:array`. +Transformations use `XsltTask` and `XQueryTask`. -Serializer compatibility modes: +### Ways to work with files -- Canonical mode (default): canonical token classes and forms shown above -- Legacy mode: retained for compatibility output only +1. File-tree mode: `source(...)` + `outputDir` +2. Explicit mode: `input(...)` + `output(...)` -Internal bridge note: +### Common transformation options -- 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. +- `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)` -`outputFormat` is also reused for canonical JSON output formatting. +### XSLT transformation -Format conventions: +
+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 +### XQuery transformation -`XsltTask` and `XQueryTask` support optional canonical JSON parsing/serialization. +
+Input/output example in XML and XIR -- Canonical JSON maps XML element trees to JSON objects with `type`, `name`, `attributes`, `children`. -- Canonical JSON mode is reversible for XML -> JSON -> XML roundtrips. -- `outputFormat` controls canonical JSON output style too: `compact` or `beautified`. - -Set JSON routing mode with `outputMode` (`jsonMode` and `mode` remain supported as legacy aliases): +```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') outputMode('canonical') outputFormat.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' - outputMode('canonical') -} ``` +
-### 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") + outputDir.set(layout.buildDirectory.dir("out/xquery")) + outputExtension.set(".json") + outputMode("canonical") outputFormat.set("beautified") } ``` +
-Groovy DSL: - -```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') - outputFormat.set('beautified') -} -``` - -## Input/Output Modes +### Canonical JSON mode details -`XsltTask` and `XQueryTask` support two equivalent execution modes: +- `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. -- **File-tree mode**: set `source(...)` and `outputDir` -- **Explicit single-file mode**: set `input(...)` and `output(...)` +Legacy aliases still work: -Notes: +- `jsonMode` / `mode` +- `xirFormat` / `xformat` -- 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. +### SEL and SHACL transformation pipelines -## Validation API Contract +- `SchematronSelCompileTask`: compile `sel:*` metadata to extraction stylesheet +- `SchematronSelExtractTask`: run extraction and emit grouped outputs +- `ShaclSelCompileTask` + `ShaclSelExtractTask`: same compile/extract shape for SHACL-driven flows -Validation tasks share a common contract (`ValidationTaskSpec`) and defaults: +## Validations -- `outputExtension = '.svrl.xml'` -- `workers = 1` -- `format = SVRL` (`reportFormat` remains supported as legacy alias) -- `failOnError = true` -- `junitOutputDir = build/reports/xml-validation/junit` +Validation tasks use shared contract: -`ReportFormat` values: +- `outputExtension` (default `.svrl.xml`) +- `workers` (default `1`) +- `failOnError` (default `true`) +- `format(...)` / `reportFormat` values: + - `SVRL` + - `JUNIT` + - `SVRL_AND_JUNIT` -- `SVRL` -- `JUNIT` -- `SVRL_AND_JUNIT` +### XSD validation (`XsdTask`) -`XsdTask` supports `XsdEngine` values: +Engine options: -- `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`: +```xml +bad +``` -```kotlin -plugins { - id("name.jurgenei.gradle.xml") -} +```lisp +(root (wrong "bad")) ``` +
-Or `build.gradle`: +
+Groovy Task Def (XSD validation) ```groovy -plugins { - id 'name.jurgenei.gradle.xml' +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) } ``` +
-Legacy `buildscript` usage: +
+Kotlin Task Def (XSD validation) ```kotlin -buildscript { - repositories { - mavenCentral() - gradlePluginPortal() - } - dependencies { - classpath("name.jurgenei.gradle:gradle-xml-transform:0.1.1") - } +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) } - -apply(plugin = "name.jurgenei.gradle.xml") ``` +
-## Example (Kotlin DSL) +### Schematron validation (`SchematronTask`) -```kotlin -plugins { - id("name.jurgenei.gradle.xml") -} +Schematron options include: -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") -} +- `style(...)` for persistent compiled stylesheet cache +- optional `transpilerStylesheet(...)` +- SchXslt parameter set: `phase`, `severityThreshold`, `streamable`, `compactReport`, and others -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") -} -``` - -## 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' -} - -tasks.register('transformOne', name.jurgenei.gradle.xml.XsltTask) { - style 'src/main/xslt/main.xsl' - input 'src/main/xml/a.xml' - output 'build/custom/b.xml' -} - -tasks.register('queryOne', name.jurgenei.gradle.xml.XQueryTask) { - query 'src/main/xquery/main.xq' - input 'src/main/xml/a.xml' - output 'build/custom/b.xml' -} -``` - -## 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')) format(name.jurgenei.gradle.xml.validation.ReportFormat.SVRL_AND_JUNIT) - // Optional SchXslt transpiler parameters. phase.set('#ALL') severityThreshold.set('warning') workers.set(4) failOnError.set(false) } - -tasks.register('validateXsd', name.jurgenei.gradle.xml.XsdTask) { - schema 'src/main/xsd/schema.xsd' - source(fileTree('src/main/xml') { include '**/*.xml' }) - outputDir.set(layout.buildDirectory.dir('reports/xsd')) - format(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: - -- 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): +
+Kotlin Task Def (Schematron validation) -```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: +### Schematron bootstrap from XSD -```xml - -``` +`SchematronBootstrapTask` can generate initial Schematron from XSD and does not overwrite existing target file. -## SEL Runtime Extraction (Phase 3) +## XSpec testing -`SchematronSelExtractTask` executes SEL extraction against canonical XML, XIR (`.xir`), or canonical JSON (`.json`) inputs and emits grouped outputs. -It can either: +### Current native scope -- compile extraction style on the fly from `schema`, or -- consume a precompiled style via `style`. +- `XSpecCompileTask`: compile `.xspec` to executable runner +- `XSpecTask`: run XSpec and emit XML + JUnit reports +- Current built-in scope: XSLT XSpec -```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' - // outputFormat.set('beautified') - outputMode('auto') - failOnError.set(true) -} -``` - -Phase behavior for SEL compile/extract tasks: - -- `#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 ``. - -## 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 +
+Kotlin Task Def (XSpec compile + run) -```bash -./gradlew test -``` - -## Test Coverage - -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: +## Code Style -- 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` +- Java 21+ +- Public API/classes documented with Javadoc +- Multiline literals use text blocks where practical +- Build scripts follow standard Gradle Groovy/Kotlin DSL idioms -Set repository secret `NVD_API_KEY` for faster/more reliable Dependency-Check NVD lookups. - -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` @@ -722,47 +455,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 From 596478d2031c35ad103674400332e86e064ec5f3 Mon Sep 17 00:00:00 2001 From: Jurgen Hildebrand Date: Mon, 5 Oct 2026 20:43:14 +0200 Subject: [PATCH 4/6] fix lost badges --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index d8cf006..cda6edd 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,17 @@ # Gradle XML Validate and Transform Plugin ![Conformance](https://img.shields.io/badge/Conformance-Check--All%20Passing-brightgreen) + [![Plugin Portal](https://img.shields.io/gradle-plugin-portal/v/name.jurgenei.gradle.xml?label=Plugin%20Portal)](https://plugins.gradle.org/plugin/name.jurgenei.gradle.xml) [![Build and Test](https://github.com/jurgenei/gradle-xml-plugin/actions/workflows/gradle-build.yml/badge.svg)](https://github.com/jurgenei/gradle-xml-plugin/actions/workflows/gradle-build.yml) +[![JUnit Report](https://img.shields.io/badge/JUnit-Report-blue?logo=githubactions)](https://github.com/jurgenei/gradle-xml-plugin/actions/workflows/gradle-build.yml) [![Coverage CI](https://github.com/jurgenei/gradle-xml-plugin/actions/workflows/coverage.yml/badge.svg)](https://github.com/jurgenei/gradle-xml-plugin/actions/workflows/coverage.yml) [![CodeQL](https://github.com/jurgenei/gradle-xml-plugin/actions/workflows/codeql.yml/badge.svg)](https://github.com/jurgenei/gradle-xml-plugin/actions/workflows/codeql.yml) [![Dependency Check](https://github.com/jurgenei/gradle-xml-plugin/actions/workflows/dependency-check.yml/badge.svg)](https://github.com/jurgenei/gradle-xml-plugin/actions/workflows/dependency-check.yml) [![SpotBugs Security](https://github.com/jurgenei/gradle-xml-plugin/actions/workflows/spotbugs-security.yml/badge.svg)](https://github.com/jurgenei/gradle-xml-plugin/actions/workflows/spotbugs-security.yml) [![Checkstyle](https://github.com/jurgenei/gradle-xml-plugin/actions/workflows/checkstyle.yml/badge.svg)](https://github.com/jurgenei/gradle-xml-plugin/actions/workflows/checkstyle.yml) +[![Dependabot](https://img.shields.io/badge/dependabot-enabled-025E8C?logo=dependabot)](https://github.com/jurgenei/gradle-xml-plugin/security/dependabot) +[![Coverage](https://codecov.io/gh/jurgenei/gradle-xml-plugin/graph/badge.svg?branch=main)](https://codecov.io/gh/jurgenei/gradle-xml-plugin) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![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/) From 7513ea8533897fcf114d50c1af097063801fb665 Mon Sep 17 00:00:00 2001 From: Jurgen Hildebrand Date: Mon, 5 Oct 2026 21:49:54 +0200 Subject: [PATCH 5/6] warning fixes --- .../jurgenei/gradle/xml/SaxXmlWriter.java | 90 +++++++++++++++++++ .../gradle/xml/ShaclSelCompileTask.java | 36 ++++++-- .../name/jurgenei/gradle/xml/XsdTask.java | 3 +- .../gradle/xml/json/JsonCanonicalParser.java | 7 +- .../gradle/xml/saxon/SaxonXirResolvers.java | 1 - 5 files changed, 124 insertions(+), 13 deletions(-) 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/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..e27593f 100644 --- a/src/main/java/name/jurgenei/gradle/xml/XsdTask.java +++ b/src/main/java/name/jurgenei/gradle/xml/XsdTask.java @@ -92,6 +92,7 @@ private XsdEngine resolveEngine() { return isSaxonSchemaAwareAvailable() ? XsdEngine.SAXON : XsdEngine.JAXP; } + @SuppressWarnings("deprecation") private boolean isSaxonSchemaAwareAvailable() { try { Processor processor = new Processor(false); @@ -102,6 +103,7 @@ private boolean isSaxonSchemaAwareAvailable() { } } + @SuppressWarnings("deprecation") private List validateWithSaxon(File inputFile) { List issues = new ArrayList<>(); try { @@ -158,4 +160,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..dbbe286 100644 --- a/src/main/java/name/jurgenei/gradle/xml/json/JsonCanonicalParser.java +++ b/src/main/java/name/jurgenei/gradle/xml/json/JsonCanonicalParser.java @@ -6,7 +6,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; @@ -65,9 +64,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); @@ -145,4 +143,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..964d649 100644 --- a/src/main/java/name/jurgenei/gradle/xml/saxon/SaxonXirResolvers.java +++ b/src/main/java/name/jurgenei/gradle/xml/saxon/SaxonXirResolvers.java @@ -46,7 +46,6 @@ 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) { From a9086525d7dc799afacf77af2664dfd612fadefc Mon Sep 17 00:00:00 2001 From: Jurgen Hildebrand Date: Mon, 5 Oct 2026 21:53:35 +0200 Subject: [PATCH 6/6] fix spotless issues --- .../name/jurgenei/gradle/xml/XsdTask.java | 63 +++++++++++-------- .../gradle/xml/json/JsonCanonicalParser.java | 21 +++++-- .../gradle/xml/saxon/SaxonXirResolvers.java | 49 +++++++++------ 3 files changed, 84 insertions(+), 49 deletions(-) diff --git a/src/main/java/name/jurgenei/gradle/xml/XsdTask.java b/src/main/java/name/jurgenei/gradle/xml/XsdTask.java index e27593f..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,7 +88,9 @@ 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; } @@ -127,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)); 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 dbbe286..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; @@ -21,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. @@ -43,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()); } @@ -85,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; @@ -97,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")); @@ -111,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'"); } } 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 964d649..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. @@ -50,24 +50,34 @@ public static void configure(net.sf.saxon.s9api.XsltCompiler compiler) { 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; } @@ -80,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; @@ -88,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; }