From 75202f02c19bbea45e01507c6fafa5105fdca510 Mon Sep 17 00:00:00 2001 From: Milan Kuchtiak Date: Thu, 5 Mar 2026 11:06:33 +0100 Subject: [PATCH] issue 1324: curation task - implement 3 types of reporters to allow proper report writing to selected destination (ufal/clarin-dspace#1326) * issue 1324: implement 3 types of reporters to allow proper writing to file or to console * resolve Copilot comments * don't throw exception when not needed * no AbstractUnitTest required - AbstractDSpaceTest is sufficient * comment added * improve append(str) method in reporters, by using StringUtils.chomp() method (cherry picked from commit 6f19d1f54db5f729e53787d5d14ffa5f850a7a86) --- .../main/java/org/dspace/curate/Curation.java | 37 ++++--- .../curate/reporters/DoNothingReporter.java | 37 +++++++ .../curate/reporters/FilePrinterReporter.java | 66 +++++++++++++ .../curate/reporters/SystemOutReporter.java | 57 +++++++++++ .../dspace/curate/CuratorReporterTest.java | 97 +++++++++++++++++++ 5 files changed, 279 insertions(+), 15 deletions(-) create mode 100644 dspace-api/src/main/java/org/dspace/curate/reporters/DoNothingReporter.java create mode 100644 dspace-api/src/main/java/org/dspace/curate/reporters/FilePrinterReporter.java create mode 100644 dspace-api/src/main/java/org/dspace/curate/reporters/SystemOutReporter.java create mode 100644 dspace-api/src/test/java/org/dspace/curate/CuratorReporterTest.java diff --git a/dspace-api/src/main/java/org/dspace/curate/Curation.java b/dspace-api/src/main/java/org/dspace/curate/Curation.java index 3c014fec911d..d828e3e714d8 100644 --- a/dspace-api/src/main/java/org/dspace/curate/Curation.java +++ b/dspace-api/src/main/java/org/dspace/curate/Curation.java @@ -12,10 +12,6 @@ import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.PrintStream; -import java.io.Writer; import java.sql.SQLException; import java.util.HashMap; import java.util.Iterator; @@ -23,7 +19,6 @@ import java.util.UUID; import org.apache.commons.cli.ParseException; -import org.apache.commons.io.output.NullOutputStream; import org.dspace.app.util.DSpaceObjectUtilsImpl; import org.dspace.app.util.service.DSpaceObjectUtils; import org.dspace.authorize.AuthorizeException; @@ -31,6 +26,9 @@ import org.dspace.content.factory.ContentServiceFactory; import org.dspace.core.Context; import org.dspace.core.factory.CoreServiceFactory; +import org.dspace.curate.reporters.DoNothingReporter; +import org.dspace.curate.reporters.FilePrinterReporter; +import org.dspace.curate.reporters.SystemOutReporter; import org.dspace.eperson.EPerson; import org.dspace.eperson.factory.EPersonServiceFactory; import org.dspace.eperson.service.EPersonService; @@ -53,6 +51,7 @@ public class Curation extends DSpaceRunnable { HandleService handleService = HandleServiceFactory.getInstance().getHandleService(); protected Context context; private CurationClientOptions curationClientOptions; + private Reporter outputReporter; private String task; private String taskFile; @@ -173,10 +172,20 @@ private long runQueue(TaskQueue queue, Curator curator) throws SQLException, Aut * @throws SQLException If DSpace context can't complete */ private void endScript(long timeRun) throws SQLException { - context.complete(); - if (verbose) { - long elapsed = System.currentTimeMillis() - timeRun; - this.handler.logInfo("Ending curation. Elapsed time: " + elapsed); + try { + context.complete(); + if (verbose) { + long elapsed = System.currentTimeMillis() - timeRun; + this.handler.logInfo("Ending curation. Elapsed time: " + elapsed); + } + } finally { + if (outputReporter != null) { + try { + outputReporter.close(); + } catch (Exception e) { + handler.handleException("Something went wrong trying to close the reporter", e); + } + } } } @@ -188,16 +197,14 @@ private void endScript(long timeRun) throws SQLException { */ private Curator initCurator() throws FileNotFoundException { Curator curator = new Curator(handler); - OutputStream reporterStream; if (null == this.reporter) { - reporterStream = NullOutputStream.NULL_OUTPUT_STREAM; + outputReporter = new DoNothingReporter(); } else if ("-".equals(this.reporter)) { - reporterStream = System.out; + outputReporter = new SystemOutReporter(); } else { - reporterStream = new PrintStream(this.reporter); + outputReporter = new FilePrinterReporter(this.reporter); } - Writer reportWriter = new OutputStreamWriter(reporterStream); - curator.setReporter(reportWriter); + curator.setReporter(outputReporter); if (this.scope != null) { Curator.TxScope txScope = Curator.TxScope.valueOf(this.scope.toUpperCase()); diff --git a/dspace-api/src/main/java/org/dspace/curate/reporters/DoNothingReporter.java b/dspace-api/src/main/java/org/dspace/curate/reporters/DoNothingReporter.java new file mode 100644 index 000000000000..c0dd1bf6b6e4 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/curate/reporters/DoNothingReporter.java @@ -0,0 +1,37 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.curate.reporters; + +import org.dspace.curate.Reporter; + +/** + * Reporter that ignores all input. + * + * @author Milan Kuchtiak + */ +public class DoNothingReporter implements Reporter { + + @Override + public Appendable append(CharSequence csq) { + return this; + } + + @Override + public Appendable append(CharSequence csq, int start, int end) { + return this; + } + + @Override + public Appendable append(char c) { + return this; + } + + @Override + public void close() { + } +} diff --git a/dspace-api/src/main/java/org/dspace/curate/reporters/FilePrinterReporter.java b/dspace-api/src/main/java/org/dspace/curate/reporters/FilePrinterReporter.java new file mode 100644 index 000000000000..f7f016ff6fd9 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/curate/reporters/FilePrinterReporter.java @@ -0,0 +1,66 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.curate.reporters; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.curate.Reporter; + +/** + * Reporter that writes to a specified file. + * + * @author Milan Kuchtiak + */ +public class FilePrinterReporter implements Reporter { + private final PrintWriter writer; + + public FilePrinterReporter(String fileName) throws FileNotFoundException { + File file = new File(fileName); + try { + writer = new PrintWriter(file, StandardCharsets.UTF_8); + } catch (FileNotFoundException e) { + throw e; + } catch (IOException e) { + throw new RuntimeException("Error initializing FilePrinterReporter for file: " + fileName, e); + } + } + + @Override + public Appendable append(CharSequence csq) { + // strip newline from the end of the string to avoid double newlines when using println + // do not print empty lines + if (!StringUtils.isEmpty(csq)) { + writer.println(StringUtils.chomp(csq.toString())); + } + return this; + } + + @Override + public Appendable append(CharSequence csq, int start, int end) { + writer.append(csq, start, end); + return this; + } + + @Override + public Appendable append(char c) { + writer.append(c); + return this; + } + + @Override + public void close() { + // flush and close the writer to ensure all data is written to the file + writer.flush(); + writer.close(); + } +} diff --git a/dspace-api/src/main/java/org/dspace/curate/reporters/SystemOutReporter.java b/dspace-api/src/main/java/org/dspace/curate/reporters/SystemOutReporter.java new file mode 100644 index 000000000000..412b8ddcb557 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/curate/reporters/SystemOutReporter.java @@ -0,0 +1,57 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.curate.reporters; + +import java.io.PrintWriter; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.curate.Reporter; + +/** + * Reporter that writes to console (System.out). + * + * @author Milan Kuchtiak + */ +public class SystemOutReporter implements Reporter { + + private final PrintWriter writer; + + public SystemOutReporter() { + // we use PrintWriter to avoid auto-flush after every println, + // which is the default behavior of System.out.println + writer = new PrintWriter(System.out, false); + } + + @Override + public Appendable append(CharSequence csq) { + // strip newline from the end of the string to avoid double newlines when using println + // do not print empty lines + if (!StringUtils.isEmpty(csq)) { + writer.println(StringUtils.chomp(csq.toString())); + } + return this; + } + + @Override + public Appendable append(CharSequence csq, int start, int end) { + writer.append(csq, start, end); + return this; + } + + @Override + public Appendable append(char c) { + writer.append(c); + return this; + } + + @Override + public void close() { + // Note: We don't close the PrintWriter to avoid closing System.out + writer.flush(); + } +} diff --git a/dspace-api/src/test/java/org/dspace/curate/CuratorReporterTest.java b/dspace-api/src/test/java/org/dspace/curate/CuratorReporterTest.java new file mode 100644 index 000000000000..1382d4c618bd --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/curate/CuratorReporterTest.java @@ -0,0 +1,97 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.curate; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.nio.file.Files; + +import org.dspace.AbstractDSpaceTest; +import org.dspace.content.Item; +import org.dspace.core.factory.CoreServiceFactory; +import org.dspace.ctask.general.NoOpCurationTask; +import org.dspace.curate.reporters.DoNothingReporter; +import org.dspace.curate.reporters.FilePrinterReporter; +import org.dspace.curate.reporters.SystemOutReporter; +import org.dspace.services.ConfigurationService; +import org.junit.Before; +import org.junit.Test; + +/** + * Test different Reporter implementations with Curator. + * + * @author Milan Kuchtiak + */ +public class CuratorReporterTest extends AbstractDSpaceTest { + private static final String TASK_NAME = "noop"; + private static final String TEST_HANDLE = "testHandle"; + private static final String NO_OP = "No operation performed on " + TEST_HANDLE; + + private Curator curator; + + @Before + public void setup() { + CoreServiceFactory.getInstance().getPluginService().clearNamedPluginClasses(); + + // Configure the noop task to be run. + ConfigurationService cfg = kernelImpl.getConfigurationService(); + cfg.setProperty("plugin.named.org.dspace.curate.CurationTask", + NoOpCurationTask.class.getName() + " = " + TASK_NAME); + + // Get and configure a Curator. + curator = new Curator(); + } + + @Test + public void testCurateWithDoNothingReporter() throws Exception { + try (Reporter reporter = new DoNothingReporter()) { + runCuratorWithReporter(reporter); + } + } + + @Test + public void testCurateWithSystemOutReporter() throws Exception { + try (Reporter reporter = new SystemOutReporter()) { + runCuratorWithReporter(reporter); + } + } + + @Test + public void testCurateWithFilePrinterReporter() throws Exception { + File tempFile = File.createTempFile("curator-test-report", "txt"); + try (Reporter reporter = new FilePrinterReporter(tempFile.getAbsolutePath())) { + runCuratorWithReporter(reporter); + } + // check if the file contains expected line with one line separator + String fileOutput = Files.readString(tempFile.toPath()); + assertEquals(NO_OP + System.lineSeparator(), fileOutput); + } + + @Test + public void testCurateWithStringBuilder() throws Exception { + StringBuilder stringBuilder = new StringBuilder(); + runCuratorWithReporter(stringBuilder); + assertEquals(NO_OP, stringBuilder.toString()); + } + + private void runCuratorWithReporter(Appendable reporter) throws Exception { + curator.setReporter(reporter); + curator.addTask(TASK_NAME); + Item item = mock(Item.class); + when(item.getType()).thenReturn(2); + when(item.getHandle()).thenReturn(TEST_HANDLE); + curator.curate(item); + + assertEquals(Curator.CURATE_SUCCESS, curator.getStatus(TASK_NAME)); + assertEquals(NO_OP, curator.getResult(TASK_NAME)); + } + +}