Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
870 changes: 285 additions & 585 deletions README.md

Large diffs are not rendered by default.

229 changes: 192 additions & 37 deletions src/main/java/name/jurgenei/gradle/xml/AbstractXmlTransformTask.java
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* (C)2026 */
package name.jurgenei.gradle.xml;

import java.io.File;
Expand All @@ -23,9 +24,10 @@
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.PathSensitive;
import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.SourceTask;
Expand Down Expand Up @@ -126,6 +128,26 @@ public enum JsonMode {
@Optional
public abstract Property<String> getXirFormat();

/**
* Canonical output-format alias for {@link #getXirFormat()}.
*
* @return S-expression output format property
*/
@Internal
public Property<String> getOutputFormat() {
return getXirFormat();
}

/**
* Backward-compatible alias for {@link #getOutputFormat()}.
*
* @return S-expression output format property
*/
@Internal
public Property<String> getXformat() {
return getOutputFormat();
}

/**
* Optional JSON mode controlling how {@code .json} input/output is routed.
*
Expand All @@ -137,6 +159,26 @@ public enum JsonMode {
@Optional
public abstract Property<String> getJsonMode();

/**
* Canonical output-mode alias for {@link #getJsonMode()}.
*
* @return JSON routing mode property
*/
@Internal
public Property<String> getOutputMode() {
return getJsonMode();
}

/**
* Backward-compatible alias for {@link #getOutputMode()}.
*
* @return JSON routing mode property
*/
@Internal
public Property<String> getMode() {
return getOutputMode();
}

/**
* Transform parameters exposed to the execution engine.
*
Expand Down Expand Up @@ -236,6 +278,42 @@ public void xirFormat(String format) {
getXirFormat().set(format);
}

/**
* Sets explicit S-expression output format using canonical DSL name.
*
* @param format one of {@code compact} or {@code beautified}
*/
public void outputFormat(String format) {
getOutputFormat().set(format);
}

/**
* Groovy/Kotlin assignment-style alias for {@link #outputFormat(String)}.
*
* @param format one of {@code compact} or {@code beautified}
*/
public void setOutputFormat(String format) {
outputFormat(format);
}

/**
* Backward-compatible alias for {@link #outputFormat(String)}.
*
* @param format one of {@code compact} or {@code beautified}
*/
public void xformat(String format) {
outputFormat(format);
}

/**
* Groovy/Kotlin assignment-style alias for {@link #xformat(String)}.
*
* @param format one of {@code compact} or {@code beautified}
*/
public void setXformat(String format) {
xformat(format);
}

/**
* Sets JSON routing mode (Gradle DSL friendly).
*
Expand All @@ -245,6 +323,42 @@ public void jsonMode(String mode) {
getJsonMode().set(mode);
}

/**
* Sets JSON routing mode using canonical DSL name.
*
* @param mode one of {@code auto}, {@code native}, or {@code canonical}
*/
public void outputMode(String mode) {
getOutputMode().set(mode);
}

/**
* Groovy/Kotlin assignment-style alias for {@link #outputMode(String)}.
*
* @param mode one of {@code auto}, {@code native}, or {@code canonical}
*/
public void setOutputMode(String mode) {
outputMode(mode);
}

/**
* Backward-compatible alias for {@link #outputMode(String)}.
*
* @param mode one of {@code auto}, {@code native}, or {@code canonical}
*/
public void mode(String mode) {
outputMode(mode);
}

/**
* Groovy/Kotlin assignment-style alias for {@link #mode(String)}.
*
* @param mode one of {@code auto}, {@code native}, or {@code canonical}
*/
public void setMode(String mode) {
mode(mode);
}

@Override
@PathSensitive(PathSensitivity.RELATIVE)
public org.gradle.api.file.FileTree getSource() {
Expand All @@ -262,10 +376,12 @@ public void transformAll() {
boolean hasExplicitInput = getInputFile().isPresent();
boolean hasExplicitOutput = getOutputFile().isPresent();
if (hasExplicitInput != hasExplicitOutput) {
throw new GradleException("Both inputFile and outputFile must be set together for single-file mode");
throw new GradleException(
"Both inputFile and outputFile must be set together for single-file mode");
}

Map<String, String> params = Collections.unmodifiableMap(new HashMap<>(getParams().getOrElse(Map.of())));
Map<String, String> params =
Collections.unmodifiableMap(new HashMap<>(getParams().getOrElse(Map.of())));

if (hasExplicitInput) {
transformExplicit(params);
Expand Down Expand Up @@ -302,7 +418,9 @@ public void transformAll() {
}

if (!failures.isEmpty()) {
throw new GradleException("Transformation failed for " + failures.size() + " input file(s)", failures.get(0));
throw new GradleException(
"Transformation failed for " + failures.size() + " input file(s)",
failures.get(0));
}
}

Expand All @@ -314,8 +432,7 @@ private void transformExplicit(Map<String, String> params) {
}

long newestDependencyTimestamp = latestDependencyTimestamp(inputFile);
if (outputFile.exists()
&& outputFile.lastModified() >= newestDependencyTimestamp) {
if (outputFile.exists() && outputFile.lastModified() >= newestDependencyTimestamp) {
getLogger().lifecycle("[SKIP] {}", inputFile);
return;
}
Expand All @@ -336,11 +453,27 @@ private void transformExplicit(Map<String, String> params) {
}
}

private void runParallel(List<File> inputFiles, File outputRoot, Map<Path, String> relativePaths, Map<String, String> params, int workers, List<Exception> failures) {
try (ExecutorService executor = Executors.newFixedThreadPool(workers, Thread.ofVirtual().name(getName() + "-vt-", 0).factory())) {
private void runParallel(
List<File> inputFiles,
File outputRoot,
Map<Path, String> relativePaths,
Map<String, String> params,
int workers,
List<Exception> failures) {
try (ExecutorService executor =
Executors.newFixedThreadPool(
workers, Thread.ofVirtual().name(getName() + "-vt-", 0).factory())) {
List<java.util.concurrent.Future<?>> 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 {
Expand All @@ -360,12 +493,16 @@ private void runParallel(List<File> inputFiles, File outputRoot, Map<Path, Strin
}
}

private void transformOne(File inputFile, File outputRoot, Map<Path, String> relativePaths, Map<String, String> params, List<Exception> failures) {
private void transformOne(
File inputFile,
File outputRoot,
Map<Path, String> relativePaths,
Map<String, String> params,
List<Exception> 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;
}
Expand Down Expand Up @@ -428,12 +565,14 @@ protected XirSerializer.OutputFormat resolveXirOutputFormat() {
normalized = "beautified";
}
if (!SUPPORTED_SEXPR_FORMATS.contains(normalized)) {
throw new GradleException("Unsupported xirFormat '" + configured
+ "'. Supported values: compact, beautified");
throw new GradleException(
"Unsupported xirFormat '"
+ configured
+ "'. Supported values: compact, beautified");
}
return "beautified".equals(normalized)
? XirSerializer.OutputFormat.BEAUTIFIED
: XirSerializer.OutputFormat.COMPACT;
? XirSerializer.OutputFormat.BEAUTIFIED
: XirSerializer.OutputFormat.COMPACT;
}

/**
Expand All @@ -445,8 +584,10 @@ protected JsonMode resolveJsonMode() {
String configured = getJsonMode().getOrElse("auto");
String normalized = configured.trim().toLowerCase(Locale.ROOT);
if (!SUPPORTED_JSON_MODES.contains(normalized)) {
throw new GradleException("Unsupported jsonMode '" + configured
+ "'. Supported values: auto, native, canonical");
throw new GradleException(
"Unsupported jsonMode '"
+ configured
+ "'. Supported values: auto, native, canonical");
}
return switch (normalized) {
case "native" -> JsonMode.NATIVE;
Expand Down Expand Up @@ -531,46 +672,60 @@ private String inferSerializerMethod(File outputFile) {
}

private String normalizeAndValidateMethod(String configuredMethod) {
String normalized = configuredMethod == null ? "" : configuredMethod.trim().toLowerCase(Locale.ROOT);
String normalized =
configuredMethod == null ? "" : configuredMethod.trim().toLowerCase(Locale.ROOT);
if ("txt".equals(normalized)) {
normalized = "text";
}
if (!SUPPORTED_OUTPUT_METHODS.contains(normalized)) {
throw new GradleException("Unsupported outputMethod '" + configuredMethod
+ "'. Supported values: xml, json, text");
throw new GradleException(
"Unsupported outputMethod '"
+ configuredMethod
+ "'. Supported values: xml, json, text");
}
return normalized;
}


private File outputFileFor(File inputFile, File outputRoot, Map<Path, String> 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<Path, String> resolveRelativePaths() {
Map<Path, String> 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;
}

Expand All @@ -582,6 +737,6 @@ private Map<Path, String> 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<String, String> params) throws Exception;
protected abstract void transform(File inputFile, File outputFile, Map<String, String> params)
throws Exception;
}

Loading
Loading