diff --git a/cli/src/main/java/de/jplag/cli/CLI.java b/cli/src/main/java/de/jplag/cli/CLI.java index e64b81118..fc5b9cd33 100644 --- a/cli/src/main/java/de/jplag/cli/CLI.java +++ b/cli/src/main/java/de/jplag/cli/CLI.java @@ -16,6 +16,7 @@ import de.jplag.exceptions.ExitException; import de.jplag.logging.ProgressBarLogger; import de.jplag.options.JPlagOptions; +import de.jplag.util.FileUtils; /** * Command line interface class, allows using via command line. @@ -25,6 +26,10 @@ public final class CLI { private static final Logger logger = LoggerFactory.getLogger(CLI.class); private static final String DEFAULT_FILE_ENDING = ".zip"; + private static final int NAME_COLLISION_ATTEMPTS = 4; + + private static final String OUTPUT_FILE_EXISTS = "The output file (also with suffixes e.g. results(1).zip) already exists. You can use --overwrite to overwrite the file."; + private static final String OUTPUT_FILE_NOT_WRITABLE = "The output file (%s) cannot be written to."; private final CliInputHandler inputHandler; @@ -85,11 +90,12 @@ public boolean executeCliAndHandleErrors() { * @throws FileNotFoundException If the file could not be written */ public File runJPlag() throws ExitException, FileNotFoundException { + File target = new File(getWritableFileName()); + JPlagOptionsBuilder optionsBuilder = new JPlagOptionsBuilder(this.inputHandler); JPlagOptions options = optionsBuilder.buildOptions(); JPlagResult result = JPlagRunner.runJPlag(options); - File target = new File(getResultFilePath()); OutputFileGenerator.generateJPlagResultZip(result, target); OutputFileGenerator.generateCsvOutput(result, new File(getResultFileBaseName()), this.inputHandler.getCliOptions()); @@ -126,6 +132,34 @@ private String getResultFileBaseName() { return defaultOutputFile.substring(0, defaultOutputFile.length() - DEFAULT_FILE_ENDING.length()); } + private String getOffsetFileName(int offset) { + if (offset <= 0) { + return getResultFilePath(); + } else { + return getResultFileBaseName() + "(" + offset + ")" + DEFAULT_FILE_ENDING; + } + } + + private String getWritableFileName() throws CliException { + int retryAttempt = 0; + while (!this.inputHandler.getCliOptions().advanced.overwrite && new File(getOffsetFileName(retryAttempt)).exists() + && retryAttempt < NAME_COLLISION_ATTEMPTS) { + retryAttempt++; + } + + String targetFileName = this.getOffsetFileName(retryAttempt); + File targetFile = new File(targetFileName); + if (!this.inputHandler.getCliOptions().advanced.overwrite && targetFile.exists()) { + throw new CliException(OUTPUT_FILE_EXISTS); + } + + if (!FileUtils.checkWritable(targetFile)) { + throw new CliException(String.format(OUTPUT_FILE_NOT_WRITABLE, targetFileName)); + } + + return targetFileName; + } + public static void main(String[] args) { CLI cli = new CLI(args); if (cli.executeCliAndHandleErrors()) { diff --git a/cli/src/main/java/de/jplag/cli/options/CliOptions.java b/cli/src/main/java/de/jplag/cli/options/CliOptions.java index 999cd5635..4ef2200f8 100644 --- a/cli/src/main/java/de/jplag/cli/options/CliOptions.java +++ b/cli/src/main/java/de/jplag/cli/options/CliOptions.java @@ -99,6 +99,9 @@ public static class Advanced { @Option(names = "--csv-export", description = "Export pairwise similarity values as a CSV file.") public boolean csvExport = false; + + @Option(names = "--overwrite", description = "Existing result files will be overwritten.") + public boolean overwrite = false; } public static class Clustering { diff --git a/cli/src/test/java/de/jplag/cli/ArgumentBuilder.java b/cli/src/test/java/de/jplag/cli/ArgumentBuilder.java index 49848a96b..305331b1c 100644 --- a/cli/src/test/java/de/jplag/cli/ArgumentBuilder.java +++ b/cli/src/test/java/de/jplag/cli/ArgumentBuilder.java @@ -168,6 +168,26 @@ public ArgumentBuilder shownComparisons(String value) { return this; } + /** + * Sets the result file + * @param path The path to the result file + * @return self reference + */ + public ArgumentBuilder resultFile(String path) { + this.arguments.add("-r"); + this.arguments.add(path); + return this; + } + + /** + * Adds the overwrite argument + * @return self reference + */ + public ArgumentBuilder overwrite() { + this.arguments.add("--overwrite"); + return this; + } + /** * Sets the shown comparisons option * @param value The option value diff --git a/cli/src/test/java/de/jplag/cli/CheckResultFileWritableTest.java b/cli/src/test/java/de/jplag/cli/CheckResultFileWritableTest.java new file mode 100644 index 000000000..4baab95e4 --- /dev/null +++ b/cli/src/test/java/de/jplag/cli/CheckResultFileWritableTest.java @@ -0,0 +1,96 @@ +package de.jplag.cli; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.file.Files; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import de.jplag.cli.picocli.CliInputHandler; + +public class CheckResultFileWritableTest extends CommandLineInterfaceTest { + private static Field inputHandlerField; + private static Method getWritableFileMethod; + + @BeforeAll + public static void setup() throws NoSuchFieldException, NoSuchMethodException { + Class cliClass = CLI.class; + inputHandlerField = cliClass.getDeclaredField("inputHandler"); + getWritableFileMethod = cliClass.getDeclaredMethod("getWritableFileName"); + + inputHandlerField.setAccessible(true); + getWritableFileMethod.setAccessible(true); + } + + @Test + void testNonExistingWritableFile() throws Throwable { + File directory = Files.createTempDirectory("JPlagTest").toFile(); + File targetFile = new File(directory, "results.zip"); + + String path = runResultFileCheck(defaultArguments().resultFile(targetFile.getAbsolutePath())); + Assertions.assertEquals(targetFile.getAbsolutePath(), path); + } + + @Test + void testNonExistingNotWritableFile() throws IOException { + File directory = Files.createTempDirectory("JPlagTest").toFile(); + Assumptions.assumeTrue(directory.setWritable(false)); + File targetFile = new File(directory, "results.zip"); + + Assertions.assertThrows(CliException.class, () -> { + runResultFileCheck(defaultArguments().resultFile(targetFile.getAbsolutePath())); + }); + } + + @Test + void testExistingFile() throws Throwable { + File directory = Files.createTempDirectory("JPlagTest").toFile(); + File targetFile = new File(directory, "results.zip"); + Assumptions.assumeTrue(targetFile.createNewFile()); + + String path = runResultFileCheck(defaultArguments().resultFile(targetFile.getAbsolutePath())); + Assertions.assertEquals(new File(directory, "results(1).zip").getAbsolutePath(), path); + } + + @Test + void testExistingFileOverwrite() throws Throwable { + File directory = Files.createTempDirectory("JPlagTest").toFile(); + File targetFile = new File(directory, "results.zip"); + Assumptions.assumeTrue(targetFile.createNewFile()); + + String path = runResultFileCheck(defaultArguments().resultFile(targetFile.getAbsolutePath()).overwrite()); + Assertions.assertEquals(targetFile.getAbsolutePath(), path); + } + + @Test + void testExistingNotWritableFile() throws IOException { + File directory = Files.createTempDirectory("JPlagTest").toFile(); + File targetFile = new File(directory, "results.zip"); + Assumptions.assumeTrue(targetFile.createNewFile()); + Assumptions.assumeTrue(targetFile.setWritable(false)); + + Assertions.assertThrows(CliException.class, () -> { + runResultFileCheck(defaultArguments().resultFile(targetFile.getAbsolutePath()).overwrite()); + }); + } + + private String runResultFileCheck(ArgumentBuilder builder) throws Throwable { + String[] args = builder.getArgumentsAsArray(); + CLI cli = new CLI(args); + + CliInputHandler inputHandler = (CliInputHandler) inputHandlerField.get(cli); + inputHandler.parse(); + + try { + return (String) getWritableFileMethod.invoke(cli); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } +} diff --git a/language-api/src/main/java/de/jplag/util/FileUtils.java b/language-api/src/main/java/de/jplag/util/FileUtils.java index 5ce3c62b6..1f645df75 100644 --- a/language-api/src/main/java/de/jplag/util/FileUtils.java +++ b/language-api/src/main/java/de/jplag/util/FileUtils.java @@ -178,4 +178,26 @@ public static void write(File file, String content) throws IOException { writer.write(content); writer.close(); } + + /** + * Checks if the given file can be written to. If the file does not exist checks if it can be created. + * @param file The file to check + * @return true, if the file can be written to + */ + public static boolean checkWritable(File file) { + if (file.exists()) { + return file.canWrite(); + } else { + return checkParentWritable(file); + } + } + + /** + * Checks if the parent file can be written to. + * @param file The file to check + * @return true, if the parent can be written to + */ + public static boolean checkParentWritable(File file) { + return file.getAbsoluteFile().getParentFile().canWrite(); + } }