From 23d282d2fc1b7fe698974255f157be992459a041 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Wed, 12 Nov 2025 08:03:19 +0100 Subject: [PATCH 1/7] File output / FS --- .../dev/vml/es/acm/core/code/FileOutput.java | 70 +++++++++++++------ .../manual/example/ACME-203_output_csv.groovy | 57 +++++++++++++++ ...puts.groovy => ACME-203_output_xls.groovy} | 0 3 files changed, 107 insertions(+), 20 deletions(-) create mode 100644 ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output_csv.groovy rename ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/{ACME-203_outputs.groovy => ACME-203_output_xls.groovy} (100%) diff --git a/core/src/main/java/dev/vml/es/acm/core/code/FileOutput.java b/core/src/main/java/dev/vml/es/acm/core/code/FileOutput.java index bc0532fb..e3c2778a 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/FileOutput.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/FileOutput.java @@ -1,22 +1,27 @@ package dev.vml.es.acm.core.code; import com.fasterxml.jackson.annotation.JsonIgnore; -import dev.vml.es.acm.core.gui.SpaSettings; -import dev.vml.es.acm.core.osgi.OsgiContext; -import dev.vml.es.acm.core.repo.RepoChunks; import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.Flushable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintStream; +import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; -import org.apache.sling.api.resource.ResourceResolverFactory; public class FileOutput extends Output implements Flushable, Closeable { + private static final String TEMP_DIR = "acm/execution/file"; + + @JsonIgnore + private transient File tempFile; + @JsonIgnore - private transient RepoChunks repoChunks; + private transient FileOutputStream fileOutputStream; @JsonIgnore private transient PrintStream printStream; @@ -59,28 +64,43 @@ public void setMimeType(String mimeType) { } @JsonIgnore - private RepoChunks getRepoChunks() { - if (repoChunks == null) { - OsgiContext osgi = executionContext.getCodeContext().getOsgiContext(); - SpaSettings spaSettings = osgi.getService(SpaSettings.class); - ResourceResolverFactory resolverFactory = osgi.getService(ResourceResolverFactory.class); - String chunkFolderPath = String.format( - "%s/outputs/%s", - ExecutionContext.varPath(executionContext.getId()), StringUtils.replace(getName(), "/", "-")); - repoChunks = - new RepoChunks(resolverFactory, chunkFolderPath, spaSettings.getExecutionFileOutputChunkSize()); + private File getTempFile() { + if (tempFile == null) { + File tmpDir = FileUtils.getTempDirectory(); + String sanitizedName = StringUtils.replace(getName(), "/", "-"); + String filePath = String.format("%s/%s/%s", TEMP_DIR, executionContext.getId(), sanitizedName); + tempFile = new File(tmpDir, filePath); + try { + FileUtils.forceMkdirParent(tempFile); + } catch (IOException e) { + throw new RuntimeException( + String.format("Cannot create temp directory for output '%s'", getName()), e); + } } - return repoChunks; + return tempFile; } @JsonIgnore public OutputStream getOutputStream() { - return getRepoChunks().getOutputStream(); + if (fileOutputStream == null) { + try { + fileOutputStream = new FileOutputStream(getTempFile()); + } catch (IOException e) { + throw new RuntimeException( + String.format("Cannot create output stream for file output '%s'", getName()), e); + } + } + return fileOutputStream; } @JsonIgnore public InputStream getInputStream() { - return getRepoChunks().getInputStream(); + try { + return new FileInputStream(getTempFile()); + } catch (IOException e) { + throw new RuntimeException( + String.format("Cannot read file output '%s'", getName()), e); + } } @JsonIgnore @@ -96,11 +116,21 @@ public void flush() throws IOException { if (printStream != null) { printStream.flush(); } - getRepoChunks().flush(); + if (fileOutputStream != null) { + fileOutputStream.flush(); + } } @Override public void close() throws IOException { - getRepoChunks().close(); + if (printStream != null) { + printStream.close(); + } + if (fileOutputStream != null) { + fileOutputStream.close(); + } + if (tempFile != null && tempFile.exists()) { + FileUtils.forceDelete(tempFile); + } } } diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output_csv.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output_csv.groovy new file mode 100644 index 00000000..6db15ad1 --- /dev/null +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output_csv.groovy @@ -0,0 +1,57 @@ +import java.time.LocalDate +import java.util.Random + +boolean canRun() { + return conditions.always() +} + +void describeRun() { + inputs.integerNumber("count") { label = "Users to generate"; min = 1; value = 1000 } + inputs.text("firstNames") { label = "First names"; description = "One first name per line"; value = "John\nJane\nJack\nAlice\nBob"} + inputs.text("lastNames") { label = "Last names"; description = "One last name per line"; value = "Doe\nSmith\nBrown\nJohnson\nWhite" } +} + +void doRun() { + log.info "Users CSV report generation started" + + int count = inputs.value("count") + def firstNames = (inputs.value("firstNames")).readLines().findAll { it.trim() } + def lastNames = (inputs.value("lastNames")).readLines().findAll { it.trim() } + + def random = new Random() + def now = LocalDate.now() + def hundredYearsAgo = now.minusYears(100) + + def report = outputs.file("report") { + label = "Report" + description = "Users report generated as CSV file" + downloadName = "report.csv" + } + + // CSV header + report.out.println("Name,Surname,Birth Date") + + // Generate users + (1..count).each { i -> + def name = firstNames[random.nextInt(firstNames.size())] + def surname = lastNames[random.nextInt(lastNames.size())] + def birthDate = randomDateBetween(hundredYearsAgo, now) + + report.out.println("${name},${surname},${birthDate}") + + if (i % 100 == 0) log.info("Generated ${i} users...") + } + + outputs.text("summary") { + value = "Processed ${count} user(s)" + } + + log.info "Users CSV report generation ended successfully" +} + +LocalDate randomDateBetween(LocalDate start, LocalDate end) { + long startDay = start.toEpochDay() + long endDay = end.toEpochDay() + long randomDay = startDay + new Random().nextInt((int)(endDay - startDay + 1)) + return LocalDate.ofEpochDay(randomDay) +} diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_outputs.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output_xls.groovy similarity index 100% rename from ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_outputs.groovy rename to ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output_xls.groovy From 763b85247390b48bae985c89ec28f1523393f3f9 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Wed, 12 Nov 2025 08:34:18 +0100 Subject: [PATCH 2/7] Abortable output scripts --- .../dev/vml/es/acm/core/code/FileOutput.java | 25 +++++++++++-------- ..._csv.groovy => ACME-203_output-csv.groovy} | 4 ++- ..._xls.groovy => ACME-203_output-xls.groovy} | 4 ++- 3 files changed, 20 insertions(+), 13 deletions(-) rename ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/{ACME-203_output_csv.groovy => ACME-203_output-csv.groovy} (96%) rename ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/{ACME-203_output_xls.groovy => ACME-203_output-xls.groovy} (98%) diff --git a/core/src/main/java/dev/vml/es/acm/core/code/FileOutput.java b/core/src/main/java/dev/vml/es/acm/core/code/FileOutput.java index e3c2778a..a0fe6d7b 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/FileOutput.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/FileOutput.java @@ -1,6 +1,7 @@ package dev.vml.es.acm.core.code; import com.fasterxml.jackson.annotation.JsonIgnore; +import dev.vml.es.acm.core.AcmException; import java.io.Closeable; import java.io.File; import java.io.FileInputStream; @@ -67,15 +68,19 @@ public void setMimeType(String mimeType) { private File getTempFile() { if (tempFile == null) { File tmpDir = FileUtils.getTempDirectory(); + String contextPrefix = StringUtils.replace(executionContext.getId(), "/", "-"); String sanitizedName = StringUtils.replace(getName(), "/", "-"); - String filePath = String.format("%s/%s/%s", TEMP_DIR, executionContext.getId(), sanitizedName); - tempFile = new File(tmpDir, filePath); - try { - FileUtils.forceMkdirParent(tempFile); - } catch (IOException e) { - throw new RuntimeException( - String.format("Cannot create temp directory for output '%s'", getName()), e); + String fileName = String.format("%s_%s.out", contextPrefix, sanitizedName); + File tempFile = new File(new File(tmpDir, TEMP_DIR), fileName); + File parentDir = tempFile.getParentFile(); + if (parentDir != null && !parentDir.exists()) { + try { + FileUtils.forceMkdir(parentDir); + } catch (IOException e) { + throw new AcmException(String.format("Cannot create temp directory for output '%s'", getName()), e); + } } + this.tempFile = tempFile; } return tempFile; } @@ -86,8 +91,7 @@ public OutputStream getOutputStream() { try { fileOutputStream = new FileOutputStream(getTempFile()); } catch (IOException e) { - throw new RuntimeException( - String.format("Cannot create output stream for file output '%s'", getName()), e); + throw new AcmException(String.format("Cannot create output stream for file output '%s'", getName()), e); } } return fileOutputStream; @@ -98,8 +102,7 @@ public InputStream getInputStream() { try { return new FileInputStream(getTempFile()); } catch (IOException e) { - throw new RuntimeException( - String.format("Cannot read file output '%s'", getName()), e); + throw new AcmException(String.format("Cannot read file output '%s'", getName()), e); } } diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output_csv.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-csv.groovy similarity index 96% rename from ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output_csv.groovy rename to ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-csv.groovy index 6db15ad1..30f34c7d 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output_csv.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-csv.groovy @@ -6,7 +6,7 @@ boolean canRun() { } void describeRun() { - inputs.integerNumber("count") { label = "Users to generate"; min = 1; value = 1000 } + inputs.integerNumber("count") { label = "Users to generate"; min = 1; value = 5000000 } inputs.text("firstNames") { label = "First names"; description = "One first name per line"; value = "John\nJane\nJack\nAlice\nBob"} inputs.text("lastNames") { label = "Last names"; description = "One last name per line"; value = "Doe\nSmith\nBrown\nJohnson\nWhite" } } @@ -33,6 +33,8 @@ void doRun() { // Generate users (1..count).each { i -> + context.checkAborted() + def name = firstNames[random.nextInt(firstNames.size())] def surname = lastNames[random.nextInt(lastNames.size())] def birthDate = randomDateBetween(hundredYearsAgo, now) diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output_xls.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-xls.groovy similarity index 98% rename from ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output_xls.groovy rename to ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-xls.groovy index 06cd1de2..bcaf7921 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output_xls.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-xls.groovy @@ -10,7 +10,7 @@ boolean canRun() { } void describeRun() { - inputs.integerNumber("count") { label = "Users to generate"; min = 1; value = 1000 } + inputs.integerNumber("count") { label = "Users to generate"; min = 1; value = 100000 } inputs.text("firstNames") { label = "First names"; description = "One first name per line"; value = "John\nJane\nJack\nAlice\nBob"} inputs.text("lastNames") { label = "Last names"; description = "One last name per line"; value = "Doe\nSmith\nBrown\nJohnson\nWhite" } } @@ -38,6 +38,8 @@ void doRun() { dateStyle.setDataFormat(helper.createDataFormat().getFormat("yyyy-mm-dd")) (1..count).each { i -> + context.checkAborted() + def name = firstNames[random.nextInt(firstNames.size())] def surname = lastNames[random.nextInt(lastNames.size())] def birthDate = randomDateBetween(hundredYearsAgo, now) From 35134259e3763b71b775adf58fcae8e393d6d290 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Wed, 12 Nov 2025 08:35:37 +0100 Subject: [PATCH 3/7] Minor --- core/src/main/java/dev/vml/es/acm/core/code/FileOutput.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/dev/vml/es/acm/core/code/FileOutput.java b/core/src/main/java/dev/vml/es/acm/core/code/FileOutput.java index a0fe6d7b..31125dad 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/FileOutput.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/FileOutput.java @@ -77,7 +77,7 @@ private File getTempFile() { try { FileUtils.forceMkdir(parentDir); } catch (IOException e) { - throw new AcmException(String.format("Cannot create temp directory for output '%s'", getName()), e); + throw new AcmException(String.format("Cannot create temp directory for output '%s'!", getName()), e); } } this.tempFile = tempFile; @@ -91,7 +91,7 @@ public OutputStream getOutputStream() { try { fileOutputStream = new FileOutputStream(getTempFile()); } catch (IOException e) { - throw new AcmException(String.format("Cannot create output stream for file output '%s'", getName()), e); + throw new AcmException(String.format("Cannot create output stream for file output '%s'!", getName()), e); } } return fileOutputStream; @@ -102,7 +102,7 @@ public InputStream getInputStream() { try { return new FileInputStream(getTempFile()); } catch (IOException e) { - throw new AcmException(String.format("Cannot read file output '%s'", getName()), e); + throw new AcmException(String.format("Cannot read file output '%s'!", getName()), e); } } From 05ffbde30a57efea8ef98bb04d5e66e4c4f319e5 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Wed, 12 Nov 2025 08:39:31 +0100 Subject: [PATCH 4/7] Shadowing fix --- core/src/main/java/dev/vml/es/acm/core/code/FileOutput.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/dev/vml/es/acm/core/code/FileOutput.java b/core/src/main/java/dev/vml/es/acm/core/code/FileOutput.java index 31125dad..10b20ef8 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/FileOutput.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/FileOutput.java @@ -71,8 +71,8 @@ private File getTempFile() { String contextPrefix = StringUtils.replace(executionContext.getId(), "/", "-"); String sanitizedName = StringUtils.replace(getName(), "/", "-"); String fileName = String.format("%s_%s.out", contextPrefix, sanitizedName); - File tempFile = new File(new File(tmpDir, TEMP_DIR), fileName); - File parentDir = tempFile.getParentFile(); + File result = new File(new File(tmpDir, TEMP_DIR), fileName); + File parentDir = result.getParentFile(); if (parentDir != null && !parentDir.exists()) { try { FileUtils.forceMkdir(parentDir); @@ -80,7 +80,7 @@ private File getTempFile() { throw new AcmException(String.format("Cannot create temp directory for output '%s'!", getName()), e); } } - this.tempFile = tempFile; + this.tempFile = result; } return tempFile; } From cdf8b51a803b0b4251c41027f4742d865d6f30ee Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Wed, 12 Nov 2025 08:42:13 +0100 Subject: [PATCH 5/7] Output closing --- .../java/dev/vml/es/acm/core/code/ExecutionHistory.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/dev/vml/es/acm/core/code/ExecutionHistory.java b/core/src/main/java/dev/vml/es/acm/core/code/ExecutionHistory.java index a7a7c556..ed66f90f 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/ExecutionHistory.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/ExecutionHistory.java @@ -91,7 +91,12 @@ private void saveFileOutput(FileOutput output, Resource entry) { "Output '%s' cannot be flushed before saving to the execution history!", output.getName()), e); } - file.saveFile(output.getMimeType(), output.getInputStream()); + try (InputStream inputStream = output.getInputStream()) { + file.saveFile(output.getMimeType(), inputStream); + } catch (IOException e) { + throw new AcmException( + String.format("Output '%s' cannot be saved to the execution history!", output.getName()), e); + } } public InputStream readOutputByName(Execution execution, String name) { From 353ef661db7937f929d31be56d216f068106ea5e Mon Sep 17 00:00:00 2001 From: Krystian Panek <81212505+krystian-panek-vmltech@users.noreply.github.com> Date: Wed, 12 Nov 2025 08:44:31 +0100 Subject: [PATCH 6/7] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../dev/vml/es/acm/core/code/FileOutput.java | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/dev/vml/es/acm/core/code/FileOutput.java b/core/src/main/java/dev/vml/es/acm/core/code/FileOutput.java index 10b20ef8..fc43198c 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/FileOutput.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/FileOutput.java @@ -126,14 +126,31 @@ public void flush() throws IOException { @Override public void close() throws IOException { - if (printStream != null) { - printStream.close(); - } - if (fileOutputStream != null) { - fileOutputStream.close(); + IOException exception = null; + try { + if (printStream != null) { + printStream.close(); + } + if (fileOutputStream != null) { + fileOutputStream.close(); + } + } catch (IOException e) { + exception = e; + } finally { + try { + if (tempFile != null && tempFile.exists()) { + FileUtils.forceDelete(tempFile); + } + } catch (IOException e) { + if (exception != null) { + exception.addSuppressed(e); + } else { + exception = e; + } + } } - if (tempFile != null && tempFile.exists()) { - FileUtils.forceDelete(tempFile); + if (exception != null) { + throw exception; } } } From 398735ff1422c03b92ec3f6009354d903c48a9d8 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Wed, 12 Nov 2025 08:44:52 +0100 Subject: [PATCH 7/7] Fmt --- core/src/main/java/dev/vml/es/acm/core/code/FileOutput.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/dev/vml/es/acm/core/code/FileOutput.java b/core/src/main/java/dev/vml/es/acm/core/code/FileOutput.java index 10b20ef8..abb50bbd 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/FileOutput.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/FileOutput.java @@ -77,7 +77,8 @@ private File getTempFile() { try { FileUtils.forceMkdir(parentDir); } catch (IOException e) { - throw new AcmException(String.format("Cannot create temp directory for output '%s'!", getName()), e); + throw new AcmException( + String.format("Cannot create temp directory for output '%s'!", getName()), e); } } this.tempFile = result; @@ -91,7 +92,8 @@ public OutputStream getOutputStream() { try { fileOutputStream = new FileOutputStream(getTempFile()); } catch (IOException e) { - throw new AcmException(String.format("Cannot create output stream for file output '%s'!", getName()), e); + throw new AcmException( + String.format("Cannot create output stream for file output '%s'!", getName()), e); } } return fileOutputStream;