Просмотр исходного кода

Merge remote-tracking branch 'origin/master' into feature/endToEndDokumentation

Patrick Thiel 4 лет назад
Родитель
Сommit
74e1b78025
46 измененных файлов с 559 добавлено и 803 удалено
  1. 1 1
      README.md
  2. 41 16
      jplag.cli/src/main/java/de/jplag/CLI.java
  3. 3 4
      jplag.cli/src/test/java/de/jplag/cli/BaseCodeOptionTest.java
  4. 6 6
      jplag.cli/src/test/java/de/jplag/cli/ClusteringTest.java
  5. 3 3
      jplag.cli/src/test/java/de/jplag/cli/ComparisonModeTest.java
  6. 2 2
      jplag.cli/src/test/java/de/jplag/cli/LanguageTest.java
  7. 10 18
      jplag.cli/src/test/java/de/jplag/cli/MinTokenMatchTest.java
  8. 19 19
      jplag.cli/src/test/java/de/jplag/cli/OldNewRootDirectoriesArgumentTest.java
  9. 10 10
      jplag.cli/src/test/java/de/jplag/cli/SimiliarityThresholdTest.java
  10. 11 11
      jplag.cli/src/test/java/de/jplag/cli/StoredMatchesTest.java
  11. 5 5
      jplag.endtoend-testing/src/test/java/de/jplag/endtoend/EndToEndSuiteTest.java
  12. 6 7
      jplag.frontend.rust/src/test/java/de/jplag/rust/RustFrontendTest.java
  13. 4 25
      jplag.frontend.text/pom.xml
  14. 0 102
      jplag.frontend.text/src/main/antlr/text.g
  15. 0 79
      jplag.frontend.text/src/main/java/de/jplag/text/AntlrParserToken.java
  16. 0 49
      jplag.frontend.text/src/main/java/de/jplag/text/InputState.java
  17. 80 33
      jplag.frontend.text/src/main/java/de/jplag/text/ParserAdapter.java
  18. 2 2
      jplag.frontend.text/src/main/java/de/jplag/text/TextToken.java
  19. 25 2
      jplag.frontend.text/src/test/java/jplag/text/TextFrontendTest.java
  20. 1 1
      jplag/src/main/java/de/jplag/GreedyStringTiling.java
  21. 5 55
      jplag/src/main/java/de/jplag/JPlag.java
  22. 2 2
      jplag/src/main/java/de/jplag/JPlagResult.java
  23. 4 4
      jplag/src/main/java/de/jplag/SubmissionSet.java
  24. 12 13
      jplag/src/main/java/de/jplag/SubmissionSetBuilder.java
  25. 169 238
      jplag/src/main/java/de/jplag/options/JPlagOptions.java
  26. 1 1
      jplag/src/main/java/de/jplag/reporting/jsonfactory/ComparisonReportWriter.java
  27. 1 1
      jplag/src/main/java/de/jplag/reporting/jsonfactory/DirectoryManager.java
  28. 9 9
      jplag/src/main/java/de/jplag/reporting/reportobject/ReportObjectFactory.java
  29. 1 1
      jplag/src/main/java/de/jplag/reporting/reportobject/mapper/MetricMapper.java
  30. 1 1
      jplag/src/main/java/de/jplag/reporting/reportobject/mapper/SubmissionNameToIdMapper.java
  31. 1 1
      jplag/src/main/java/de/jplag/strategy/AbstractComparisonStrategy.java
  32. 8 8
      jplag/src/test/java/de/jplag/BaseCodeTest.java
  33. 1 1
      jplag/src/test/java/de/jplag/InvalidSubmissionTest.java
  34. 8 11
      jplag/src/test/java/de/jplag/NormalComparisonTest.java
  35. 3 3
      jplag/src/test/java/de/jplag/ParallelComparisonTest.java
  36. 10 10
      jplag/src/test/java/de/jplag/TestBase.java
  37. 1 1
      jplag/src/test/java/de/jplag/reporting/reportobject/mapper/MetricMapperTest.java
  38. 14 16
      jplag/src/test/java/de/jplag/special/TokenPrinterTest.java
  39. 1 1
      jplag/src/test/java/de/jplag/special/VolumeTest.java
  40. 7 0
      pom.xml
  41. 45 9
      report-viewer/package-lock.json
  42. 1 0
      report-viewer/package.json
  43. 1 1
      report-viewer/src/router/index.ts
  44. 13 12
      report-viewer/src/store/store.ts
  45. 1 2
      report-viewer/src/views/ComparisonView.vue
  46. 10 7
      report-viewer/src/views/FileUploadView.vue

+ 1 - 1
README.md

@@ -29,7 +29,7 @@ In the following, a list of all supported languages with their supported languag
 | [Scheme](http://www.scheme-reports.org)                          |       ? | scheme                | unknown | JavaCC |
 | [EMF Metamodel](https://www.eclipse.org/modeling/emf/)           |  2.25.0 | emf-metamodel         | alpha | EMF |
 | [EMF Metamodel](https://www.eclipse.org/modeling/emf/) (dynamic) |  2.25.0 | emf-metamodel-dynamic | alpha | EMF |
-| Text (naive)                                                     |       - | text                  | legacy | ANTLR |
+| Text (naive)                                                     |       - | text                  | legacy | CoreNLP |
 
 ## Download and Installation
 

+ 41 - 16
jplag.cli/src/main/java/de/jplag/CLI.java

@@ -1,9 +1,38 @@
 package de.jplag;
 
-import static de.jplag.CommandLineArgument.*;
+import static de.jplag.CommandLineArgument.BASE_CODE;
+import static de.jplag.CommandLineArgument.CLUSTER_AGGLOMERATIVE_INTER_CLUSTER_SIMILARITY;
+import static de.jplag.CommandLineArgument.CLUSTER_AGGLOMERATIVE_THRESHOLD;
+import static de.jplag.CommandLineArgument.CLUSTER_ALGORITHM;
+import static de.jplag.CommandLineArgument.CLUSTER_DISABLE;
+import static de.jplag.CommandLineArgument.CLUSTER_METRIC;
+import static de.jplag.CommandLineArgument.CLUSTER_PREPROCESSING_CDF;
+import static de.jplag.CommandLineArgument.CLUSTER_PREPROCESSING_NONE;
+import static de.jplag.CommandLineArgument.CLUSTER_PREPROCESSING_PERCENTILE;
+import static de.jplag.CommandLineArgument.CLUSTER_PREPROCESSING_THRESHOLD;
+import static de.jplag.CommandLineArgument.CLUSTER_SPECTRAL_BANDWIDTH;
+import static de.jplag.CommandLineArgument.CLUSTER_SPECTRAL_KMEANS_ITERATIONS;
+import static de.jplag.CommandLineArgument.CLUSTER_SPECTRAL_MAX_RUNS;
+import static de.jplag.CommandLineArgument.CLUSTER_SPECTRAL_MIN_RUNS;
+import static de.jplag.CommandLineArgument.CLUSTER_SPECTRAL_NOISE;
+import static de.jplag.CommandLineArgument.COMPARISON_MODE;
+import static de.jplag.CommandLineArgument.DEBUG;
+import static de.jplag.CommandLineArgument.EXCLUDE_FILE;
+import static de.jplag.CommandLineArgument.LANGUAGE;
+import static de.jplag.CommandLineArgument.MIN_TOKEN_MATCH;
+import static de.jplag.CommandLineArgument.NEW_DIRECTORY;
+import static de.jplag.CommandLineArgument.OLD_DIRECTORY;
+import static de.jplag.CommandLineArgument.RESULT_FOLDER;
+import static de.jplag.CommandLineArgument.ROOT_DIRECTORY;
+import static de.jplag.CommandLineArgument.SHOWN_COMPARISONS;
+import static de.jplag.CommandLineArgument.SIMILARITY_THRESHOLD;
+import static de.jplag.CommandLineArgument.SUBDIRECTORY;
+import static de.jplag.CommandLineArgument.SUFFIXES;
+import static de.jplag.CommandLineArgument.VERBOSITY;
 
 import java.security.SecureRandom;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Optional;
 import java.util.Random;
@@ -125,21 +154,15 @@ public final class CLI {
         addAllMultiValueArgument(NEW_DIRECTORY.getListFrom(namespace), submissionDirectories);
         addAllMultiValueArgument(OLD_DIRECTORY.getListFrom(namespace), oldSubmissionDirectories);
 
-        JPlagOptions options = new JPlagOptions(submissionDirectories, oldSubmissionDirectories, LANGUAGE.getFrom(namespace));
-        options.setBaseCodeSubmissionName(BASE_CODE.getFrom(namespace));
-        options.setVerbosity(Verbosity.fromOption(VERBOSITY.getFrom(namespace)));
-        options.setDebugParser(DEBUG.getFrom(namespace));
-        options.setSubdirectoryName(SUBDIRECTORY.getFrom(namespace));
-        options.setFileSuffixes(fileSuffixes);
-        options.setExclusionFileName(EXCLUDE_FILE.getFrom(namespace));
-        options.setMinimumTokenMatch(MIN_TOKEN_MATCH.getFrom(namespace));
-        options.setSimilarityThreshold(SIMILARITY_THRESHOLD.getFrom(namespace));
-        options.setMaximumNumberOfComparisons(SHOWN_COMPARISONS.getFrom(namespace));
-        ComparisonMode.fromName(COMPARISON_MODE.getFrom(namespace)).ifPresentOrElse(options::setComparisonMode,
-                () -> logger.warn("Unknown comparison mode, using default mode!"));
+        var language = LanguageLoader.getLanguage(LANGUAGE.getFrom(namespace)).orElseThrow();
+        var comparisonModeOptional = ComparisonMode.fromName(COMPARISON_MODE.getFrom(namespace));
+        if (comparisonModeOptional.isEmpty()) {
+            logger.warn("Unknown comparison mode, using default mode!");
+        }
+        var comparisonMode = comparisonModeOptional.orElse(JPlagOptions.DEFAULT_COMPARISON_MODE);
 
         ClusteringOptions.Builder clusteringBuilder = new ClusteringOptions.Builder();
-        Optional.ofNullable(!(Boolean) CLUSTER_DISABLE.getFrom(namespace)).ifPresent(clusteringBuilder::enabled);
+        Optional.ofNullable((Boolean) CLUSTER_DISABLE.getFrom(namespace)).ifPresent(disabled -> clusteringBuilder.enabled(!disabled));
         Optional.ofNullable((ClusteringAlgorithm) CLUSTER_ALGORITHM.getFrom(namespace)).ifPresent(clusteringBuilder::algorithm);
         Optional.ofNullable((SimilarityMetric) CLUSTER_METRIC.getFrom(namespace)).ifPresent(clusteringBuilder::similarityMetric);
         Optional.ofNullable((Float) CLUSTER_SPECTRAL_BANDWIDTH.getFrom(namespace)).ifPresent(clusteringBuilder::spectralKernelBandwidth);
@@ -169,9 +192,11 @@ public final class CLI {
             clusteringBuilder.preprocessor(Preprocessing.THRESHOLD);
             clusteringBuilder.preprocessorPercentile(threshold);
         });
-        options.setClusteringOptions(clusteringBuilder.build());
 
-        return options;
+        return new JPlagOptions(language, MIN_TOKEN_MATCH.getFrom(namespace), submissionDirectories, oldSubmissionDirectories,
+                BASE_CODE.getFrom(namespace), SUBDIRECTORY.getFrom(namespace), Arrays.stream(fileSuffixes).toList(), EXCLUDE_FILE.getFrom(namespace),
+                JPlagOptions.DEFAULT_SIMILARITY_METRIC, SIMILARITY_THRESHOLD.getFrom(namespace), SHOWN_COMPARISONS.getFrom(namespace),
+                clusteringBuilder.build(), comparisonMode, Verbosity.fromOption(VERBOSITY.getFrom(namespace)), DEBUG.getFrom(namespace));
     }
 
     private String generateDescription() {

+ 3 - 4
jplag.cli/src/test/java/de/jplag/cli/BaseCodeOptionTest.java

@@ -1,8 +1,7 @@
 package de.jplag.cli;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import java.util.Optional;
+import static org.junit.jupiter.api.Assertions.assertNull;
 
 import org.junit.jupiter.api.Test;
 
@@ -15,13 +14,13 @@ class BaseCodeOptionTest extends CommandLineInterfaceTest {
     @Test
     void testDefaultValue() {
         buildOptionsFromCLI(CURRENT_DIRECTORY);
-        assertEquals(Optional.empty(), options.getBaseCodeSubmissionName());
+        assertNull(options.baseCodeSubmissionName());
     }
 
     @Test
     void testCustomName() {
         String argument = buildArgument(CommandLineArgument.BASE_CODE, NAME);
         buildOptionsFromCLI(argument, CURRENT_DIRECTORY);
-        assertEquals(NAME, options.getBaseCodeSubmissionName().get());
+        assertEquals(NAME, options.baseCodeSubmissionName());
     }
 }

+ 6 - 6
jplag.cli/src/test/java/de/jplag/cli/ClusteringTest.java

@@ -15,35 +15,35 @@ class ClusteringTest extends CommandLineInterfaceTest {
     void parseSkipClustering() {
         String argument = CommandLineArgument.CLUSTER_DISABLE.flag();
         buildOptionsFromCLI(argument, CURRENT_DIRECTORY);
-        assertEquals(false, options.getClusteringOptions().isEnabled());
+        assertEquals(false, options.clusteringOptions().isEnabled());
     }
 
     @Test
     void parseDefaultClustering() {
         buildOptionsFromCLI(CURRENT_DIRECTORY);
-        assertEquals(true, options.getClusteringOptions().isEnabled());
+        assertEquals(true, options.clusteringOptions().isEnabled());
     }
 
     @Test
     void parsePercentilePreProcessor() {
         String argument = buildArgument(CommandLineArgument.CLUSTER_PREPROCESSING_PERCENTILE, Float.toString(0.5f));
         buildOptionsFromCLI(argument, CURRENT_DIRECTORY);
-        assertEquals(Preprocessing.PERCENTILE, options.getClusteringOptions().getPreprocessor());
-        assertEquals(0.5, options.getClusteringOptions().getPreprocessorPercentile(), EPSILON);
+        assertEquals(Preprocessing.PERCENTILE, options.clusteringOptions().getPreprocessor());
+        assertEquals(0.5, options.clusteringOptions().getPreprocessorPercentile(), EPSILON);
     }
 
     @Test
     void parseCdfPreProcessor() {
         String argument = CommandLineArgument.CLUSTER_PREPROCESSING_CDF.flag();
         buildOptionsFromCLI(argument, CURRENT_DIRECTORY);
-        assertEquals(Preprocessing.CUMULATIVE_DISTRIBUTION_FUNCTION, options.getClusteringOptions().getPreprocessor());
+        assertEquals(Preprocessing.CUMULATIVE_DISTRIBUTION_FUNCTION, options.clusteringOptions().getPreprocessor());
     }
 
     @Test
     void parseNoPreProcessor() {
         String argument = CommandLineArgument.CLUSTER_PREPROCESSING_NONE.flag();
         buildOptionsFromCLI(argument, CURRENT_DIRECTORY);
-        assertEquals(Preprocessing.NONE, options.getClusteringOptions().getPreprocessor());
+        assertEquals(Preprocessing.NONE, options.clusteringOptions().getPreprocessor());
     }
 
 }

+ 3 - 3
jplag.cli/src/test/java/de/jplag/cli/ComparisonModeTest.java

@@ -14,7 +14,7 @@ class ComparisonModeTest extends CommandLineInterfaceTest {
     @Test
     void testDefaultMode() {
         buildOptionsFromCLI(CURRENT_DIRECTORY);
-        assertEquals(JPlagOptions.DEFAULT_COMPARISON_MODE, options.getComparisonMode());
+        assertEquals(JPlagOptions.DEFAULT_COMPARISON_MODE, options.comparisonMode());
     }
 
     @Test
@@ -29,7 +29,7 @@ class ComparisonModeTest extends CommandLineInterfaceTest {
         ComparisonMode mode = ComparisonMode.NORMAL;
         String argument = buildArgument(CommandLineArgument.COMPARISON_MODE, mode.getName());
         buildOptionsFromCLI(argument, CURRENT_DIRECTORY);
-        assertEquals(mode, options.getComparisonMode());
+        assertEquals(mode, options.comparisonMode());
     }
 
     @Test
@@ -37,7 +37,7 @@ class ComparisonModeTest extends CommandLineInterfaceTest {
         ComparisonMode mode = ComparisonMode.PARALLEL;
         String argument = buildArgument(CommandLineArgument.COMPARISON_MODE, mode.getName());
         buildOptionsFromCLI(argument, CURRENT_DIRECTORY);
-        assertEquals(mode, options.getComparisonMode());
+        assertEquals(mode, options.comparisonMode());
     }
 
 }

+ 2 - 2
jplag.cli/src/test/java/de/jplag/cli/LanguageTest.java

@@ -15,7 +15,7 @@ class LanguageTest extends CommandLineInterfaceTest {
     @Test
     void testDefaultLanguage() {
         buildOptionsFromCLI(CURRENT_DIRECTORY);
-        assertEquals(CommandLineArgument.DEFAULT_LANGUAGE_IDENTIFIER, options.getLanguageIdentifier());
+        assertEquals(CommandLineArgument.DEFAULT_LANGUAGE_IDENTIFIER, options.language().getIdentifier());
     }
 
     @Test
@@ -36,7 +36,7 @@ class LanguageTest extends CommandLineInterfaceTest {
         for (Language language : LanguageLoader.getAllAvailableLanguages().values()) {
             String argument = buildArgument(CommandLineArgument.LANGUAGE, language.getIdentifier());
             buildOptionsFromCLI(argument, CURRENT_DIRECTORY);
-            assertEquals(language.getIdentifier(), options.getLanguageIdentifier());
+            assertEquals(language.getIdentifier(), options.language().getIdentifier());
         }
     }
 

+ 10 - 18
jplag.cli/src/test/java/de/jplag/cli/MinTokenMatchTest.java

@@ -6,51 +6,43 @@ import static org.junit.jupiter.api.Assertions.*;
 import org.junit.jupiter.api.Test;
 
 import de.jplag.CommandLineArgument;
-import de.jplag.JPlag;
 
-public class MinTokenMatchTest extends CommandLineInterfaceTest {
+class MinTokenMatchTest extends CommandLineInterfaceTest {
 
     @Test
-    public void testLanguageDefault() {
+    void testLanguageDefault() {
         // Language defaults not set yet:
         buildOptionsFromCLI(CURRENT_DIRECTORY);
-        assertNull(options.getMinimumTokenMatch());
-        assertNull(options.getLanguage());
-
-        // Init JPlag:
-        new JPlag(options);
-
-        // Now the language is set:
-        assertNotNull(options.getLanguage());
-        assertEquals(options.getLanguage().minimumTokenMatch(), options.getMinimumTokenMatch().intValue());
+        assertNotNull(options.language());
+        assertEquals(options.language().minimumTokenMatch(), options.minimumTokenMatch().intValue());
     }
 
     @Test
-    public void testInvalidInput() throws Exception {
+    void testInvalidInput() throws Exception {
         String argument = buildArgument(CommandLineArgument.MIN_TOKEN_MATCH, "Not an integer...");
         int statusCode = catchSystemExit(() -> buildOptionsFromCLI(argument, CURRENT_DIRECTORY));
         assertEquals(1, statusCode);
     }
 
     @Test
-    public void testUpperBound() throws Exception {
+    void testUpperBound() throws Exception {
         String argument = buildArgument(CommandLineArgument.MIN_TOKEN_MATCH, "2147483648"); // max value plus one
         int statusCode = catchSystemExit(() -> buildOptionsFromCLI(argument, CURRENT_DIRECTORY));
         assertEquals(1, statusCode);
     }
 
     @Test
-    public void testLowerBound() {
+    void testLowerBound() {
         String argument = buildArgument(CommandLineArgument.MIN_TOKEN_MATCH, Integer.toString(-1));
         buildOptionsFromCLI(argument, CURRENT_DIRECTORY);
-        assertEquals(1, options.getMinimumTokenMatch().intValue());
+        assertEquals(1, options.minimumTokenMatch().intValue());
     }
 
     @Test
-    public void testValidThreshold() {
+    void testValidThreshold() {
         int expectedValue = 50;
         String argument = buildArgument(CommandLineArgument.MIN_TOKEN_MATCH, Integer.toString(expectedValue));
         buildOptionsFromCLI(argument, CURRENT_DIRECTORY);
-        assertEquals(expectedValue, options.getMinimumTokenMatch().intValue());
+        assertEquals(expectedValue, options.minimumTokenMatch().intValue());
     }
 }

+ 19 - 19
jplag.cli/src/test/java/de/jplag/cli/OldNewRootDirectoriesArgumentTest.java

@@ -4,52 +4,52 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import org.junit.jupiter.api.Test;
 
-public class OldNewRootDirectoriesArgumentTest extends CommandLineInterfaceTest {
+class OldNewRootDirectoriesArgumentTest extends CommandLineInterfaceTest {
     @Test
-    public void testNoRootDirectories() {
+    void testNoRootDirectories() {
         buildOptionsFromCLI();
 
-        assertEquals(0, options.getSubmissionDirectories().size());
-        assertEquals(0, options.getOldSubmissionDirectories().size());
+        assertEquals(0, options.submissionDirectories().size());
+        assertEquals(0, options.oldSubmissionDirectories().size());
     }
 
     @Test
-    public void testTwoRootDirectoryArguments() {
+    void testTwoRootDirectoryArguments() {
         buildOptionsFromCLI("root1", "root2");
 
-        assertEquals(2, options.getSubmissionDirectories().size());
-        assertEquals(0, options.getOldSubmissionDirectories().size());
+        assertEquals(2, options.submissionDirectories().size());
+        assertEquals(0, options.oldSubmissionDirectories().size());
     }
 
     @Test
-    public void testNewOption() {
+    void testNewOption() {
         buildOptionsFromCLI("-new", "root1", "root2");
 
-        assertEquals(2, options.getSubmissionDirectories().size());
-        assertEquals(0, options.getOldSubmissionDirectories().size());
+        assertEquals(2, options.submissionDirectories().size());
+        assertEquals(0, options.oldSubmissionDirectories().size());
     }
 
     @Test
-    public void testDoubleNewOption() {
+    void testDoubleNewOption() {
         buildOptionsFromCLI("-new", "root1", "-new", "root2");
 
-        assertEquals(2, options.getSubmissionDirectories().size());
-        assertEquals(0, options.getOldSubmissionDirectories().size());
+        assertEquals(2, options.submissionDirectories().size());
+        assertEquals(0, options.oldSubmissionDirectories().size());
     }
 
     @Test
-    public void testOldOption() {
+    void testOldOption() {
         buildOptionsFromCLI("-old", "root1");
 
-        assertEquals(0, options.getSubmissionDirectories().size());
-        assertEquals(1, options.getOldSubmissionDirectories().size());
+        assertEquals(0, options.submissionDirectories().size());
+        assertEquals(1, options.oldSubmissionDirectories().size());
     }
 
     @Test
-    public void testNewAndOldOption() {
+    void testNewAndOldOption() {
         buildOptionsFromCLI("-new", "root1", "-old", "root2");
 
-        assertEquals(1, options.getSubmissionDirectories().size());
-        assertEquals(1, options.getOldSubmissionDirectories().size());
+        assertEquals(1, options.submissionDirectories().size());
+        assertEquals(1, options.oldSubmissionDirectories().size());
     }
 }

+ 10 - 10
jplag.cli/src/test/java/de/jplag/cli/SimiliarityThresholdTest.java

@@ -8,40 +8,40 @@ import org.junit.jupiter.api.Test;
 import de.jplag.CommandLineArgument;
 import de.jplag.options.JPlagOptions;
 
-public class SimiliarityThresholdTest extends CommandLineInterfaceTest {
+class SimiliarityThresholdTest extends CommandLineInterfaceTest {
 
     @Test
-    public void testDefaultThreshold() {
+    void testDefaultThreshold() {
         buildOptionsFromCLI(CURRENT_DIRECTORY);
-        assertEquals(JPlagOptions.DEFAULT_SIMILARITY_THRESHOLD, options.getSimilarityThreshold(), DELTA);
+        assertEquals(JPlagOptions.DEFAULT_SIMILARITY_THRESHOLD, options.similarityThreshold(), DELTA);
     }
 
     @Test
-    public void testInvalidThreshold() throws Exception {
+    void testInvalidThreshold() throws Exception {
         String argument = buildArgument(CommandLineArgument.SIMILARITY_THRESHOLD, "Not a float...");
         int statusCode = catchSystemExit(() -> buildOptionsFromCLI(argument, CURRENT_DIRECTORY));
         assertEquals(1, statusCode);
     }
 
     @Test
-    public void testLowerBound() {
+    void testLowerBound() {
         String argument = buildArgument(CommandLineArgument.SIMILARITY_THRESHOLD, Float.toString(-1f));
         buildOptionsFromCLI(argument, CURRENT_DIRECTORY);
-        assertEquals(0f, options.getSimilarityThreshold(), DELTA);
+        assertEquals(0f, options.similarityThreshold(), DELTA);
     }
 
     @Test
-    public void testUpperBound() {
+    void testUpperBound() {
         String argument = buildArgument(CommandLineArgument.SIMILARITY_THRESHOLD, Float.toString(101f));
         buildOptionsFromCLI(argument, CURRENT_DIRECTORY);
-        assertEquals(100f, options.getSimilarityThreshold(), DELTA);
+        assertEquals(100f, options.similarityThreshold(), DELTA);
     }
 
     @Test
-    public void testValidThreshold() {
+    void testValidThreshold() {
         float expectedValue = 50f;
         String argument = buildArgument(CommandLineArgument.SIMILARITY_THRESHOLD, Float.toString(expectedValue));
         buildOptionsFromCLI(argument, CURRENT_DIRECTORY);
-        assertEquals(expectedValue, options.getSimilarityThreshold(), DELTA);
+        assertEquals(expectedValue, options.similarityThreshold(), DELTA);
     }
 }

+ 11 - 11
jplag.cli/src/test/java/de/jplag/cli/StoredMatchesTest.java

@@ -8,39 +8,39 @@ import org.junit.jupiter.api.Test;
 import de.jplag.CommandLineArgument;
 import de.jplag.options.JPlagOptions;
 
-public class StoredMatchesTest extends CommandLineInterfaceTest {
+class StoredMatchesTest extends CommandLineInterfaceTest {
 
     @Test
-    public void testDefault() {
+    void testDefault() {
         buildOptionsFromCLI(CURRENT_DIRECTORY);
-        assertEquals(JPlagOptions.DEFAULT_SHOWN_COMPARISONS, options.getMaximumNumberOfComparisons());
+        assertEquals(JPlagOptions.DEFAULT_SHOWN_COMPARISONS, options.maximumNumberOfComparisons());
     }
 
     @Test
-    public void testValidThreshold() {
+    void testValidThreshold() {
         int expectedValue = 999;
         String argument = buildArgument(CommandLineArgument.SHOWN_COMPARISONS, Integer.toString(expectedValue));
         buildOptionsFromCLI(argument, CURRENT_DIRECTORY);
-        assertEquals(expectedValue, options.getMaximumNumberOfComparisons());
+        assertEquals(expectedValue, options.maximumNumberOfComparisons());
     }
 
     @Test
-    public void testAll() {
-        int expectedValue = -1;
+    void testAll() {
+        int expectedValue = JPlagOptions.SHOW_ALL_COMPARISONS;
         String argument = buildArgument(CommandLineArgument.SHOWN_COMPARISONS, Integer.toString(expectedValue));
         buildOptionsFromCLI(argument, CURRENT_DIRECTORY);
-        assertEquals(expectedValue, options.getMaximumNumberOfComparisons());
+        assertEquals(expectedValue, options.maximumNumberOfComparisons());
     }
 
     @Test
-    public void testLowerBound() {
+    void testLowerBound() {
         String argument = buildArgument(CommandLineArgument.SHOWN_COMPARISONS, Integer.toString(-2));
         buildOptionsFromCLI(argument, CURRENT_DIRECTORY);
-        assertEquals(-1, options.getMaximumNumberOfComparisons());
+        assertEquals(JPlagOptions.SHOW_ALL_COMPARISONS, options.maximumNumberOfComparisons());
     }
 
     @Test
-    public void testInvalidThreshold() throws Exception {
+    void testInvalidThreshold() throws Exception {
         String argument = buildArgument(CommandLineArgument.SHOWN_COMPARISONS, "Not an integer...");
         int statusCode = catchSystemExit(() -> buildOptionsFromCLI(argument, CURRENT_DIRECTORY));
         assertEquals(1, statusCode);

+ 5 - 5
jplag.endtoend-testing/src/test/java/de/jplag/endtoend/EndToEndSuiteTest.java

@@ -22,6 +22,7 @@ import org.junit.jupiter.api.TestFactory;
 import de.jplag.JPlag;
 import de.jplag.JPlagComparison;
 import de.jplag.JPlagResult;
+import de.jplag.LanguageLoader;
 import de.jplag.endtoend.helper.FileHelper;
 import de.jplag.endtoend.helper.JsonHelper;
 import de.jplag.endtoend.helper.TestSuiteHelper;
@@ -108,7 +109,7 @@ public class EndToEndSuiteTest {
     /**
      * Superordinate test function to be able to continue to check all data to be tested in case of failed tests
      * @param directoryName name of the current tested directory
-     * @param options for the current test run
+     * @param option for the current test run
      * @param currentLanguageIdentifier current JPlag language option
      * @param testFiles files to be tested
      * @param currentResultDescription results stored for the test data
@@ -142,10 +143,9 @@ public class EndToEndSuiteTest {
             ResultDescription currentResultDescription) throws IOException, ExitException {
         String[] submissionPath = FileHelper.createNewTestCaseDirectory(testFiles);
 
-        JPlagOptions jplagOptions = new JPlagOptions(Arrays.asList(submissionPath), new ArrayList<>(), languageIdentifier);
-
-        jplagOptions.setMinimumTokenMatch(options.minimumTokenMatch());
-
+        var language = LanguageLoader.getLanguage(languageIdentifier).orElseThrow();
+        JPlagOptions jplagOptions = new JPlagOptions(language, Arrays.asList(submissionPath), new ArrayList<>())
+                .withMinimumTokenMatch(options.minimumTokenMatch());
         JPlagResult jplagResult = new JPlag(jplagOptions).run();
 
         List<JPlagComparison> currentJPlagComparison = jplagResult.getAllComparisons();

+ 6 - 7
jplag.frontend.rust/src/test/java/de/jplag/rust/RustFrontendTest.java

@@ -1,6 +1,6 @@
 package de.jplag.rust;
 
-import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.File;
 import java.io.IOException;
@@ -37,6 +37,7 @@ class RustFrontendTest {
     private static final String EMPTY_STRING = "";
     private static final String RUST_SHEBANG = "#!.*$";
     private static final double EPSILON = 1E-6;
+    public static final double BASELINE_COVERAGE = 0.75;
 
     private final Logger logger = LoggerFactory.getLogger("Rust frontend test");
     private final String[] testFiles = new String[] {"deno_core_runtime.rs", COMPLETE_TEST_FILE};
@@ -86,15 +87,12 @@ class RustFrontendTest {
             } else {
                 logger.info("Coverage: %.1f%%.".formatted(coverage * 100));
                 logger.info("Missing lines {}", codeLines);
-                if (coverage - 0.9 <= EPSILON) {
-                    // TODO use fail() instead when frontend is ready
-                    logger.error("Source coverage is unsatisfactory");
-                }
+                assertTrue(coverage - BASELINE_COVERAGE >= EPSILON, "Source coverage is unsatisfactory");
             }
 
         } catch (IOException exception) {
             logger.info("Error while reading test file %s".formatted(fileName), exception);
-            fail();
+            assertTrue(false);
         }
     }
 
@@ -139,8 +137,9 @@ class RustFrontendTest {
 
         if (!allTokens.isEmpty()) {
             var notFoundTypes = allTokens.stream().map(type -> new RustToken(type, EMPTY_STRING, NOT_SET, NOT_SET, NOT_SET).type2string()).toList();
-            fail("Some %d token types were not found in the complete code example '%s':\n%s".formatted(notFoundTypes.size(), fileName,
+            logger.error("Some %d token types were not found in the complete code example '%s':\n%s".formatted(notFoundTypes.size(), fileName,
                     notFoundTypes));
+            assertTrue(false);
         }
     }
 

+ 4 - 25
jplag.frontend.text/pom.xml

@@ -9,34 +9,13 @@
     <artifactId>text</artifactId>
 
     <dependencies>
-        <dependency>
-            <groupId>antlr</groupId>
-            <artifactId>antlr</artifactId>
-        </dependency>
         <dependency>
             <groupId>de.jplag</groupId>
             <artifactId>frontend-utils</artifactId>
         </dependency>
+        <dependency>
+            <groupId>edu.stanford.nlp</groupId>
+            <artifactId>stanford-corenlp</artifactId>
+        </dependency>
     </dependencies>
-    <build>
-        <plugins>
-            <plugin>
-                <groupId>org.codehaus.mojo</groupId>
-                <artifactId>antlr-maven-plugin</artifactId>
-                <configuration>
-                    <!-- Comma separated list of grammar files or pattern grammar files 
-						By default, grammar file(s) is in ${basedir}/src/main/antlr -->
-                    <!-- <grammars>*.g</grammars> -->
-                    <grammars>text.g</grammars>
-                </configuration>
-                <executions>
-                    <execution>
-                        <goals>
-                            <goal>generate</goal>
-                        </goals>
-                    </execution>
-                </executions>
-            </plugin>
-        </plugins>
-    </build>
 </project>

+ 0 - 102
jplag.frontend.text/src/main/antlr/text.g

@@ -1,102 +0,0 @@
-header {
-package de.jplag.text;
-}
-
-// tell ANTLR that we want to generate Java source code
-options {
-  language="Java";
-}
-
-class TextParser extends Parser;
-options {
-  k = 2;			  // two token lookahead
-  //  exportVocab=Text;	          // Call its vocabulary "Text"
-  codeGenMakeSwitchThreshold = 2; // Some optimizations
-  codeGenBitsetTestThreshold = 3;
-  defaultErrorHandler = true;
-  buildAST = false;
-  ASTLabelType = "AntlrParserToken";
-}
-
-{
-    private de.jplag.text.ParserAdapter parser;
-
-    public void setParserAdapter(de.jplag.text.ParserAdapter adapter) {
-        this.parser = adapter;
-    }
-}
-
-file : ( w:WORD { parser.add(w); } | PUNCTUATION | SPECIALS )* EOF ;
-
-//----------------------------------------------------------------------------
-// The Text scanner
-//----------------------------------------------------------------------------
-
-{
-import de.jplag.text.InputState;
-import de.jplag.text.AntlrParserToken;
-}
-
-class TextLexer extends Lexer;
-options {
-    //  exportVocab=Text;    // call the vocabulary "Text"
-    testLiterals = false;    // don't automatically test for literals
-    k = 2;                   // two characters of lookahead
-    charVocabulary = '\u0000'..'\u00FF';
-}
-
-{
-    public void newline() {
-        super.newline();
-        ((InputState) inputState).setColumnIndex(1);
-    }
-
-    public void consume() throws antlr.CharStreamException {
-        if (inputState.guessing == 0) {
-            InputState state = (InputState) inputState;
-            if (text.length() == 0) {
-                // remember token start column
-                state.setTokenColumnIndex(state.getColumnIndex());
-            }
-            state.setColumnIndex(state.getColumnIndex() + 1);
-        }
-        super.consume();
-    }
-
-    protected Token makeToken(int t) {
-        AntlrParserToken token = (AntlrParserToken) super.makeToken(t);
-        token.setColumn(((InputState) inputState).getTokenColumnIndex());
-        return token;
-    }
-}
-
-WORD
-options { paraphrase = "an identifier"; } :
-  (( '0'..'9') | ('A'..'Z') | ('a'..'z') |
-   ('\300' .. '\326') | ('\330' .. '\366') | ('\370' .. '\377'))+ ;
-
-PUNCTUATION : (	'!' | '"' | '\'' | '(' | ')' | ',' | '-' | '.' |
-		':' | ';' | '?'  | '[' | ']' | '`' | '{' | '}' |
-		'\253' | '\264' | '\273' | '\277' | '\0') ;
-
-SPECIALS : ('#' | '$' | '%' | '&' | '+' | '<' | '=' | '*' |
-	    '/' | '>' | '@' | '\\' | '^' | '_' | '|' | '~' |
-	    ('\241' .. '\252') | ('\254' .. '\263') | ('\265' .. '\272') |
-	    ('\274' .. '\276') | '\327' | '\367' | ('\200' .. '\237') ) ;
-
-// Whitespace -- ignored
-SPACE : ( ' '
-	  |	'\t' //{ ((InputState)inputState).setColumnIndex((InputState)inputState).getColumnIndex() + 7); }
-	  |	'\f'
-	  |     '\240'
-	  |     ('\001' .. '\010')
-	  |     ('\016' .. '\037')
-	  |     '\013'
-	  |     '\177'
-	) { _ttype = Token.SKIP; } ;
-
-NEWLINE	: // handle newlines
-  ( "\r\n" | // Evil DOS
-    '\r'   | // Macintosh
-    '\n'   ) // Unix (the right way)
-  { newline(); _ttype = Token.SKIP; } ;

+ 0 - 79
jplag.frontend.text/src/main/java/de/jplag/text/AntlrParserToken.java

@@ -1,79 +0,0 @@
-package de.jplag.text;
-
-import antlr.Token;
-
-/**
- * Token of the ANTLR grammar, needs to be converted into a JPlag token by the parser adapter.
- */
-public class AntlrParserToken extends Token {
-    /**
-     * This variable holds the line number of the current token.
-     */
-    private int line = -1;
-
-    /**
-     * This variable holds the column of the current token in its line.
-     */
-    private int column = -1;
-
-    /**
-     * This variable holds the label of the current token.
-     */
-    private String text = null;
-
-    /**
-     * This variable holds the identifier of the current token.
-     */
-    private int id = -1;
-
-    public AntlrParserToken() {
-        super();
-    }
-
-    @Override
-    public void setLine(int line) {
-        this.line = line;
-    }
-
-    @Override
-    public void setColumn(int column) {
-        this.column = column;
-    }
-
-    public void setID(int id) {
-        this.id = id;
-    }
-
-    @Override
-    public void setText(String text) {
-        this.text = (text != null ? text.intern() : null);
-    }
-
-    @Override
-    public int getColumn() {
-        return column;
-    }
-
-    @Override
-    public int getLine() {
-        return line;
-    }
-
-    @Override
-    public String getText() {
-        return text;
-    }
-
-    public int getID() {
-        return id;
-    }
-
-    public int getLength() {
-        return text.length();
-    }
-
-    @Override
-    public String toString() {
-        return "{\"" + getText() + "\", <" + getType() + ">, " + getLine() + " " + getColumn() + "}";
-    }
-}

+ 0 - 49
jplag.frontend.text/src/main/java/de/jplag/text/InputState.java

@@ -1,49 +0,0 @@
-package de.jplag.text;
-
-import java.io.InputStream;
-import java.io.Reader;
-
-import antlr.InputBuffer;
-import antlr.LexerSharedInputState;
-
-/**
- * This object contains the data associated with an input stream of characters. Multiple lexers share a single
- * LexerSharedInputState to lex the same input stream.
- */
-public class InputState extends LexerSharedInputState {
-    private int columnIndex = 1;
-    private int tokenColumnIndex = 1;
-
-    public InputState(InputBuffer inputBuffer) {
-        super(inputBuffer);
-    }
-
-    public InputState(InputStream inputStream) {
-        super(inputStream);
-    }
-
-    public InputState(Reader inputReader) {
-        super(inputReader);
-    }
-
-    @Override
-    public int getLine() {
-        return line;
-    }
-
-    public int getColumnIndex() {
-        return columnIndex;
-    }
-
-    public void setColumnIndex(int columnIndex) {
-        this.columnIndex = columnIndex;
-    }
-
-    public int getTokenColumnIndex() {
-        return tokenColumnIndex;
-    }
-
-    public void setTokenColumnIndex(int tokenColumnIndex) {
-        this.tokenColumnIndex = tokenColumnIndex;
-    }
-}

+ 80 - 33
jplag.frontend.text/src/main/java/de/jplag/text/ParserAdapter.java

@@ -1,23 +1,44 @@
 package de.jplag.text;
 
 import java.io.File;
-import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.HashMap;
 import java.util.Map;
-
-import antlr.Token;
+import java.util.Properties;
 
 import de.jplag.AbstractParser;
 import de.jplag.TokenConstants;
 import de.jplag.TokenList;
 
+import edu.stanford.nlp.ling.CoreLabel;
+import edu.stanford.nlp.pipeline.CoreDocument;
+import edu.stanford.nlp.pipeline.StanfordCoreNLP;
+
 public class ParserAdapter extends AbstractParser {
 
+    private static final char LF = '\n';
+    private static final char CR = '\r';
+    private static final String ANNOTATORS_KEY = "annotators";
+    private static final String ANNOTATORS_VALUE = "tokenize";
     private final Map<String, Integer> tokenTypes = new HashMap<>();
-    private int tokenTypeIndex = 1; // 0 is FILE_END token, SEPARATOR is not used as there are no methods.
+    private final StanfordCoreNLP pipeline;
+    private int tokenTypeIndex = 2; // 0 is FILE_END token, 1 is SEPARATOR_TOKEN, so start at 2.
 
     private TokenList tokens;
     private String currentFile;
+    private int currentLine;
+    /**
+     * The position of the current line break in the content string
+     */
+    private int currentLineBreakIndex;
+
+    public ParserAdapter() {
+        Properties properties = new Properties();
+        properties.put(ANNOTATORS_KEY, ANNOTATORS_VALUE);
+        this.pipeline = new StanfordCoreNLP(properties);
+    }
 
     public TokenList parse(File directory, String[] files) {
         tokens = new TokenList();
@@ -32,42 +53,69 @@ public class ParserAdapter extends AbstractParser {
         return tokens;
     }
 
+    private boolean parseFile(File directory, String file) {
+        this.currentFile = file;
+        this.currentLine = 1; // lines start at 1
+        this.currentLineBreakIndex = 0;
+        Path filePath = directory.toPath().resolve(file);
+        String content = readFile(filePath);
+        if (content == null) {
+            return false;
+        }
+        int lastTokenEnd = 0;
+        CoreDocument coreDocument = pipeline.processToCoreDocument(content);
+        for (CoreLabel token : coreDocument.tokens()) {
+            advanceLineBreaks(content, lastTokenEnd, token.beginPosition());
+            lastTokenEnd = token.endPosition();
+            if (isWord(token)) {
+                addToken(token);
+            }
+        }
+        return true;
+    }
+
     /**
-     * Converts a ANTLR token into a JPlag token and adds it to the token list.
-     * @param token is the ANTLR token to convert.
+     * Scan for line breaks and increase {@link #currentLine} and {@link #currentLineBreakIndex} accordingly.
+     * @param content the file content
+     * @param lastTokenEnd the end position of the last token
+     * @param nextTokenBegin the begin position of the next token
      */
-    public void add(Token token) {
-        if (token instanceof AntlrParserToken parserToken) {
-            String text = token.getText();
-            int type = getTokenType(text);
-            tokens.addToken(new TextToken(text, type, currentFile, parserToken));
-        } else {
-            throw new IllegalArgumentException("Illegal token implementation: " + token);
+    private void advanceLineBreaks(String content, int lastTokenEnd, int nextTokenBegin) {
+        for (int i = lastTokenEnd; i < nextTokenBegin; i++) {
+            if (content.charAt(i) == LF) {
+                currentLine++;
+                currentLineBreakIndex = i;
+            } else if (content.charAt(i) == CR) {
+                if (i + 1 < content.length() && content.charAt(i + 1) == LF) { // CRLF
+                    i++; // skip following LF
+                }
+                currentLine++;
+                currentLineBreakIndex = i;
+            }
         }
     }
 
-    private boolean parseFile(File directory, String file) {
-        InputState inputState = null;
-        try (FileInputStream inputStream = new FileInputStream(new File(directory, file))) {
-            currentFile = file;
-            // Create a scanner that reads from the input stream passed to us
-            inputState = new InputState(inputStream);
-            TextLexer lexer = new TextLexer(inputState);
-            lexer.setFilename(file);
-            lexer.setTokenObjectClass("de.jplag.text.AntlrParserToken");
+    private boolean isWord(CoreLabel token) {
+        // consider a token as a word if it contains any alphanumeric character
+        String text = token.originalText();
+        return text.chars().anyMatch(it -> Character.isAlphabetic(it) || Character.isDigit(it));
+    }
 
-            // Create a parser that reads from the scanner
-            TextParser parser = new TextParser(lexer);
-            parser.setFilename(file);
-            parser.setParserAdapter(this);
+    private void addToken(CoreLabel label) {
+        String text = label.originalText();
+        int type = getTokenType(text);
+        int column = label.beginPosition() - currentLineBreakIndex;
+        int length = label.endPosition() - label.beginPosition();
+        tokens.addToken(new TextToken(text, type, currentFile, currentLine, column, length));
+    }
 
-            // start parsing at the compilationUnit rule
-            parser.file();
-        } catch (Exception e) {
-            logger.error("Parsing Error in " + file + " (line " + (inputState != null ? "" + inputState.getLine() : "") + "):" + e.getMessage(), e);
-            return false;
+    private String readFile(Path filePath) {
+        try {
+            return Files.readString(filePath);
+        } catch (IOException e) {
+            logger.error("Error reading from file {}", filePath, e);
+            return null;
         }
-        return true;
     }
 
     private int getTokenType(String text) {
@@ -79,6 +127,5 @@ public class ParserAdapter extends AbstractParser {
             return ++tokenTypeIndex;
         });
         return tokenTypes.get(text);
-
     }
 }

+ 2 - 2
jplag.frontend.text/src/main/java/de/jplag/text/TextToken.java

@@ -12,8 +12,8 @@ public class TextToken extends Token {
         this.text = NO_TEXT;
     }
 
-    public TextToken(String text, int type, String file, AntlrParserToken parserToken) {
-        super(type, file, parserToken.getLine(), parserToken.getColumn(), parserToken.getLength());
+    public TextToken(String text, int type, String file, int line, int column, int length) {
+        super(type, file, line, column, length);
         this.text = text.toLowerCase();
     }
 

+ 25 - 2
jplag.frontend.text/src/test/java/jplag/text/TextFrontendTest.java

@@ -4,12 +4,17 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.HashMap;
 import java.util.Map;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -45,8 +50,26 @@ class TextFrontendTest {
         Map<Integer, Token> tokenTypes = new HashMap<>();
         result.allTokens().forEach(it -> tokenTypes.put(it.getType(), it));
 
-        assertEquals(293, result.size());
-        assertEquals(156, tokenTypes.values().size());
+        assertEquals(283, result.size());
+        assertEquals(158, tokenTypes.values().size());
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"\n", "\r", "\r\n",})
+    void testLineBreakInputs(String input, @TempDir Path tempDir) throws IOException {
+        Path file = tempDir.resolve("input.txt");
+        Files.writeString(file, input);
+        TokenList result = frontend.parse(tempDir.toFile(), new String[] {"input.txt"});
+        assertEquals(1, result.size());
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"\ntoken", "\rtoken", "\r\ntoken",})
+    void testTokenAfterLineBreak(String input, @TempDir Path tempDir) throws IOException {
+        Path file = tempDir.resolve("input.txt");
+        Files.writeString(file, input);
+        TokenList result = frontend.parse(tempDir.toFile(), new String[] {"input.txt"});
+        assertEquals(2, result.getToken(0).getLine());
     }
 
 }

+ 1 - 1
jplag/src/main/java/de/jplag/GreedyStringTiling.java

@@ -130,7 +130,7 @@ public class GreedyStringTiling {
 
         // Initialize:
         JPlagComparison comparison = new JPlagComparison(firstSubmission, secondSubmission);
-        int minimumTokenMatch = options.getMinimumTokenMatch(); // minimal required token match
+        int minimumTokenMatch = options.minimumTokenMatch(); // minimal required token match
 
         if (first.size() <= minimumTokenMatch || second.size() <= minimumTokenMatch) { // <= because of pivots!
             return comparison;

+ 5 - 55
jplag/src/main/java/de/jplag/JPlag.java

@@ -1,15 +1,5 @@
 package de.jplag;
 
-import static de.jplag.options.Verbosity.LONG;
-
-import java.io.BufferedReader;
-import java.io.FileReader;
-import java.io.IOException;
-import java.util.Collections;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -33,7 +23,6 @@ public class JPlag {
     private final Language language;
     private final ComparisonStrategy comparisonStrategy;
     private final GreedyStringTiling coreAlgorithm; // Contains the comparison logic.
-    private final Set<String> excludedFileNames;
 
     /**
      * Creates and initializes a JPlag instance, parameterized by a set of options.
@@ -42,30 +31,8 @@ public class JPlag {
     public JPlag(JPlagOptions options) {
         this.options = options;
         coreAlgorithm = new GreedyStringTiling(options);
-        language = initializeLanguage(this.options);
-        comparisonStrategy = initializeComparisonStrategy(options.getComparisonMode());
-        excludedFileNames = Optional.ofNullable(this.options.getExclusionFileName()).map(this::readExclusionFile).orElse(Collections.emptySet());
-        options.setExcludedFiles(excludedFileNames); // store for report
-    }
-
-    /**
-     * If an exclusion file is given, it is read in and all strings are saved in the set "excluded".
-     * @param exclusionFileName the file name or path
-     */
-    private Set<String> readExclusionFile(final String exclusionFileName) {
-        try (BufferedReader reader = new BufferedReader(new FileReader(exclusionFileName, JPlagOptions.CHARSET))) {
-            final var excludedFileNames = reader.lines().collect(Collectors.toSet());
-            if (options.getVerbosity() == LONG) {
-                logger.info("Excluded files:");
-                for (var excludedFilename : excludedFileNames) {
-                    logger.info(excludedFilename);
-                }
-            }
-            return excludedFileNames;
-        } catch (IOException e) {
-            logger.error("Could not read exclusion file: " + e.getMessage(), e);
-            return Collections.emptySet();
-        }
+        language = this.options.language();
+        comparisonStrategy = initializeComparisonStrategy(options.comparisonMode());
     }
 
     /**
@@ -75,11 +42,11 @@ public class JPlag {
      */
     public JPlagResult run() throws ExitException {
         // Parse and validate submissions.
-        SubmissionSetBuilder builder = new SubmissionSetBuilder(language, options, excludedFileNames);
+        SubmissionSetBuilder builder = new SubmissionSetBuilder(language, options);
         SubmissionSet submissionSet = builder.buildSubmissionSet();
 
         if (submissionSet.hasBaseCode()) {
-            coreAlgorithm.createHashes(submissionSet.getBaseCode().getTokenList(), options.getMinimumTokenMatch(), true);
+            coreAlgorithm.createHashes(submissionSet.getBaseCode().getTokenList(), options.minimumTokenMatch(), true);
         }
 
         int submissionCount = submissionSet.numberOfSubmissions();
@@ -92,7 +59,7 @@ public class JPlag {
         if (logger.isInfoEnabled())
             logger.info("Total time for comparing submissions: {}", TimeUtil.formatDuration(result.getDuration()));
 
-        result.setClusteringResult(ClusteringFactory.getClusterings(result.getAllComparisons(), options.getClusteringOptions()));
+        result.setClusteringResult(ClusteringFactory.getClusterings(result.getAllComparisons(), options.clusteringOptions()));
 
         return result;
     }
@@ -103,21 +70,4 @@ public class JPlag {
             case PARALLEL -> new ParallelComparisonStrategy(options, coreAlgorithm);
         };
     }
-
-    private static Language initializeLanguage(JPlagOptions options) {
-        String languageIdentifier = options.getLanguageIdentifier();
-        Language currentLanguage = options.getLanguage();
-
-        if (currentLanguage != null && (languageIdentifier == null || languageIdentifier.equals(currentLanguage.getIdentifier()))) {
-            // Ensure that we do not rely on the ServiceLoader API. We can also load an arbitrary language via Options
-            options.setLanguageDefaults(currentLanguage);
-            return currentLanguage;
-        }
-
-        Language language = LanguageLoader.getLanguage(languageIdentifier).orElseThrow();
-        options.setLanguage(language);
-        options.setLanguageDefaults(language);
-        logger.info("Loaded language {}", language.getName());
-        return language;
-    }
 }

+ 2 - 2
jplag/src/main/java/de/jplag/JPlagResult.java

@@ -61,7 +61,7 @@ public class JPlagResult {
      * @return a list of comparisons sorted descending by percentage.
      */
     public List<JPlagComparison> getComparisons(int numberOfComparisons) {
-        if (numberOfComparisons == -1) {
+        if (numberOfComparisons == JPlagOptions.SHOW_ALL_COMPARISONS) {
             return comparisons;
         }
         return comparisons.subList(0, Math.min(numberOfComparisons, comparisons.size()));
@@ -124,7 +124,7 @@ public class JPlagResult {
     @Override
     public String toString() {
         return String.format("JPlagResult { comparisons: %d, duration: %d ms, language: %s, submissions: %d }", getAllComparisons().size(),
-                getDuration(), getOptions().getLanguage().getName(), submissions.numberOfSubmissions());
+                getDuration(), getOptions().language().getName(), submissions.numberOfSubmissions());
     }
 
     /**

+ 4 - 4
jplag/src/main/java/de/jplag/SubmissionSet.java

@@ -115,9 +115,9 @@ public class SubmissionSet {
     private void parseBaseCodeSubmission(Submission baseCode) throws BasecodeException {
         long startTime = System.currentTimeMillis();
         logger.info("----- Parsing basecode submission: " + baseCode.getName());
-        if (!baseCode.parse(options.isDebugParser())) {
+        if (!baseCode.parse(options.debugParser())) {
             throw new BasecodeException("Could not successfully parse basecode submission!");
-        } else if (baseCode.getNumberOfTokens() < options.getMinimumTokenMatch()) {
+        } else if (baseCode.getNumberOfTokens() < options.minimumTokenMatch()) {
             throw new BasecodeException("Basecode submission contains fewer tokens than minimum match length allows!");
         }
         logger.info("Basecode submission parsed!");
@@ -144,11 +144,11 @@ public class SubmissionSet {
             logger.trace("------ Parsing submission: " + submission.getName());
             currentSubmissionName = submission.getName();
 
-            if (!(ok = submission.parse(options.isDebugParser()))) {
+            if (!(ok = submission.parse(options.debugParser()))) {
                 errors++;
             }
 
-            if (submission.getTokenList() != null && submission.getNumberOfTokens() < options.getMinimumTokenMatch()) {
+            if (submission.getTokenList() != null && submission.getNumberOfTokens() < options.minimumTokenMatch()) {
                 logger.error("Submission {} contains fewer tokens than minimum match length allows!", currentSubmissionName);
                 submission.setTokenList(null);
                 tooShort++;

+ 12 - 13
jplag/src/main/java/de/jplag/SubmissionSetBuilder.java

@@ -39,12 +39,11 @@ public class SubmissionSetBuilder {
      * Creates a builder for submission sets.
      * @param language is the language of the submissions.
      * @param options are the configured options.
-     * @param excludedFileNames a list of file names to be excluded
      */
-    public SubmissionSetBuilder(Language language, JPlagOptions options, Set<String> excludedFileNames) {
+    public SubmissionSetBuilder(Language language, JPlagOptions options) {
         this.language = language;
         this.options = options;
-        this.excludedFileNames = excludedFileNames;
+        this.excludedFileNames = options.excludedFiles();
     }
 
     /**
@@ -53,8 +52,8 @@ public class SubmissionSetBuilder {
      * @throws ExitException if the directory cannot be read.
      */
     public SubmissionSet buildSubmissionSet() throws ExitException {
-        Set<File> submissionDirectories = verifyRootDirectories(options.getSubmissionDirectories(), true);
-        Set<File> oldSubmissionDirectories = verifyRootDirectories(options.getOldSubmissionDirectories(), false);
+        Set<File> submissionDirectories = verifyRootDirectories(options.submissionDirectories(), true);
+        Set<File> oldSubmissionDirectories = verifyRootDirectories(options.oldSubmissionDirectories(), false);
         checkForNonOverlappingRootDirectories(submissionDirectories, oldSubmissionDirectories);
 
         // For backward compatibility, don't prefix submission names with their root directory
@@ -134,7 +133,7 @@ public class SubmissionSetBuilder {
             return Optional.empty();
         }
 
-        String baseCodeName = options.getBaseCodeSubmissionName().orElseThrow();
+        String baseCodeName = Optional.ofNullable(options.baseCodeSubmissionName()).orElseThrow();
         Submission baseCode = loadBaseCodeAsPath(baseCodeName);
         if (baseCode == null) {
             int numberOfRootDirectories = submissionDirectories.size() + oldSubmissionDirectories.size();
@@ -275,17 +274,17 @@ public class SubmissionSetBuilder {
      */
     private Submission processSubmission(String submissionName, File submissionFile, boolean isNew) throws ExitException {
 
-        if (submissionFile.isDirectory() && options.getSubdirectoryName() != null) {
+        if (submissionFile.isDirectory() && options.subdirectoryName() != null) {
             // Use subdirectory instead
-            submissionFile = new File(submissionFile, options.getSubdirectoryName());
+            submissionFile = new File(submissionFile, options.subdirectoryName());
 
             if (!submissionFile.exists()) {
                 throw new SubmissionException(
-                        String.format("Submission %s does not contain the given subdirectory '%s'", submissionName, options.getSubdirectoryName()));
+                        String.format("Submission %s does not contain the given subdirectory '%s'", submissionName, options.subdirectoryName()));
             }
 
             if (!submissionFile.isDirectory()) {
-                throw new SubmissionException(String.format("The given subdirectory '%s' is not a directory!", options.getSubdirectoryName()));
+                throw new SubmissionException(String.format("The given subdirectory '%s' is not a directory!", options.subdirectoryName()));
             }
         }
 
@@ -322,13 +321,13 @@ public class SubmissionSetBuilder {
      * @return true if the file suffix matches the language.
      */
     private boolean hasValidSuffix(File file) {
-        String[] validSuffixes = options.getFileSuffixes();
+        List<String> validSuffixes = options.fileSuffixes();
 
         // This is the case if either the language frontends or the CLI did not set the valid suffixes array in options
-        if (validSuffixes == null || validSuffixes.length == 0) {
+        if (validSuffixes == null || validSuffixes.isEmpty()) {
             return true;
         }
-        return Arrays.stream(validSuffixes).anyMatch(suffix -> file.getName().endsWith(suffix));
+        return validSuffixes.stream().anyMatch(suffix -> file.getName().endsWith(suffix));
     }
 
     /**

+ 169 - 238
jplag/src/main/java/de/jplag/options/JPlagOptions.java

@@ -1,320 +1,251 @@
 package de.jplag.options;
 
+import static de.jplag.options.Verbosity.LONG;
 import static de.jplag.strategy.ComparisonMode.NORMAL;
 
-import java.io.File;
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
-import java.util.Collections;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
+import java.util.*;
+import java.util.stream.Collectors;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import de.jplag.JPlag;
 import de.jplag.Language;
 import de.jplag.clustering.ClusteringOptions;
 import de.jplag.strategy.ComparisonMode;
 
-public class JPlagOptions {
+/**
+ * This record defines the options to configure {@link JPlag}.
+ * @param language Language to use when parsing the submissions.
+ * @param minimumTokenMatch Tunes the comparison sensitivity by adjusting the minimum token required to be counted as
+ * matching section. A smaller {@code <n>} increases the sensitivity but might lead to more false-positives.
+ * @param submissionDirectories Directories with new submissions. These must be checked for plagiarism.
+ * @param oldSubmissionDirectories Directories with old submissions to check against.
+ * @param baseCodeSubmissionName Path name of the directory containing the base code.
+ * @param subdirectoryName Example: If the subdirectoryName is 'src', only the code inside submissionDir/src of each
+ * submission will be used for comparison.
+ * @param fileSuffixes List of file suffixes that should be included.
+ * @param exclusionFileName Name of the file that contains the names of files to exclude from comparison.
+ * @param similarityMetric The similarity metric determines how the minimum similarity threshold required for a
+ * comparison (of two submissions) is calculated. This affects which comparisons are stored and thus make it into the
+ * result object.
+ * @param similarityThreshold Percentage value (must be between 0 and 100). Comparisons (of submissions pairs) with a
+ * similarity below this threshold will be ignored. The default value of 0 allows all matches to be stored. This affects
+ * which comparisons are stored and thus make it into the result object. See also {@link #similarityMetric()}.
+ * @param maximumNumberOfComparisons The maximum number of comparisons that will be shown in the generated report. If
+ * set to {@link #SHOW_ALL_COMPARISONS} all comparisons will be shown.
+ * @param clusteringOptions Clustering options
+ * @param comparisonMode Determines which strategy to use for the comparison of submissions.
+ * @param verbosity Level of output verbosity.
+ * @param debugParser If true, submissions that cannot be parsed will be stored in a separate directory.
+ */
+public record JPlagOptions(Language language, Integer minimumTokenMatch, List<String> submissionDirectories, List<String> oldSubmissionDirectories,
+        String baseCodeSubmissionName, String subdirectoryName, List<String> fileSuffixes, String exclusionFileName,
+        SimilarityMetric similarityMetric, float similarityThreshold, int maximumNumberOfComparisons, ClusteringOptions clusteringOptions,
+        ComparisonMode comparisonMode, Verbosity verbosity, boolean debugParser) {
 
-    private static final Logger logger = LoggerFactory.getLogger("JPlag");
     public static final ComparisonMode DEFAULT_COMPARISON_MODE = NORMAL;
     public static final float DEFAULT_SIMILARITY_THRESHOLD = 0;
     public static final int DEFAULT_SHOWN_COMPARISONS = 30;
-
+    public static final int SHOW_ALL_COMPARISONS = 0;
+    public static final SimilarityMetric DEFAULT_SIMILARITY_METRIC = SimilarityMetric.AVG;
     public static final Charset CHARSET = StandardCharsets.UTF_8;
 
-    /**
-     * The identifier of the language used to parse the submissions.
-     */
-    private final String languageIdentifier;
-
-    /**
-     * Language used to parse the submissions.
-     */
-    private Language language;
-
-    /**
-     * Determines which strategy to use for the comparison of submissions.
-     */
-    private ComparisonMode comparisonMode = DEFAULT_COMPARISON_MODE;
-
-    /**
-     * If true, submissions that cannot be parsed will be stored in a separate directory.
-     */
-    private boolean debugParser = false;
-
-    /**
-     * Array of file suffixes that should be included.
-     */
-    private String[] fileSuffixes;
-
-    /**
-     * Percentage value (must be between 0 and 100). Comparisons (of submissions pairs) with a similarity below this
-     * threshold will be ignored. The default value of 0 allows all matches to be stored. This affects which comparisons are
-     * stored and thus make it into the result object.
-     * @see JPlagOptions#similarityMetric
-     */
-    private float similarityThreshold = DEFAULT_SIMILARITY_THRESHOLD;
-
-    /**
-     * The maximum number of comparisons that will be shown in the generated report. If set to -1 all comparisons will be
-     * shown.
-     */
-    private int maximumNumberOfComparisons = DEFAULT_SHOWN_COMPARISONS;
-
-    /**
-     * The similarity metric determines how the minimum similarity threshold required for a comparison (of two submissions)
-     * is calculated. This affects which comparisons are stored and thus make it into the result object.
-     * @see JPlagOptions#similarityThreshold
-     */
-    private SimilarityMetric similarityMetric = SimilarityMetric.AVG;
-
-    /**
-     * Tunes the comparison sensitivity by adjusting the minimum token required to be counted as matching section. A smaller
-     * <n> increases the sensitivity but might lead to more false-positives.
-     */
-    private Integer minimumTokenMatch;
-
-    /**
-     * Name of the file that contains the names of files to exclude from comparison.
-     */
-    private String exclusionFileName;
-
-    /**
-     * Names of the excluded files.
-     */
-    private Set<String> excludedFiles = Collections.emptySet();
-
-    /**
-     * Directories with new submissions. These must be checked for plagiarism.
-     */
-    private List<String> submissionDirectories;
-
-    /**
-     * Directories with old submissions to check against.
-     */
-    private List<String> oldSubmissionDirectories;
-
-    /**
-     * Path name of the directory containing the base code.
-     * <p>
-     * For backwards compatibility it may also be a directory name inside the root directory. Condition for the latter is
-     * <ul>
-     * <li>Specified path does not exist.</li>
-     * <li>Name has not have a separator character after trimming them from both ends (leaving at least a one-character
-     * name).</li>
-     * <li>A submission with the specified name exists in the root directory.</li>
-     * </ul>
-     * It's an error if a string has been provided but it is neither an existing path nor does it fulfill all the conditions
-     * of the compatibility fallback listed above.
-     * </p>
-     */
-    private String baseCodeSubmissionName = null;
-
-    /**
-     * Example: If the subdirectoryName is 'src', only the code inside submissionDir/src of each submission will be used for
-     * comparison.
-     */
-    private String subdirectoryName;
-
-    /**
-     * Level of output verbosity.
-     */
-    private Verbosity verbosity;
-
-    /**
-     * Clustering options
-     */
-    private ClusteringOptions clusteringOptions = new ClusteringOptions.Builder().build();
-
-    /**
-     * Constructor with required attributes.
-     * @param languageIdentifier the identifier of the language to use. If set to {@code null} you have to use
-     * {@link #setLanguage(Language)} to set the language programmatically.
-     */
-    public JPlagOptions(List<String> submissionDirectories, List<String> oldSubmissionDirectories, String languageIdentifier) {
-        this.submissionDirectories = submissionDirectories;
-        this.oldSubmissionDirectories = oldSubmissionDirectories;
-        this.languageIdentifier = languageIdentifier;
-    }
-
-    public Optional<String> getBaseCodeSubmissionName() {
-        return Optional.ofNullable(baseCodeSubmissionName);
-    }
-
-    public ComparisonMode getComparisonMode() {
-        return comparisonMode;
-    }
+    private static final Logger logger = LoggerFactory.getLogger(JPlag.class);
 
-    public Set<String> getExcludedFiles() {
-        return excludedFiles;
+    public JPlagOptions(Language language, List<String> submissionDirectories, List<String> oldSubmissionDirectories) {
+        this(language, null, submissionDirectories, oldSubmissionDirectories, null, null, null, null, DEFAULT_SIMILARITY_METRIC,
+                DEFAULT_SIMILARITY_THRESHOLD, DEFAULT_SHOWN_COMPARISONS, new ClusteringOptions.Builder().build(), DEFAULT_COMPARISON_MODE, null,
+                false);
     }
 
-    public String getExclusionFileName() {
-        return exclusionFileName;
-    }
-
-    public String[] getFileSuffixes() {
-        return fileSuffixes;
-    }
-
-    public String getLanguageIdentifier() {
-        return languageIdentifier;
-    }
-
-    public Language getLanguage() {
-        return language;
+    public JPlagOptions(Language language, Integer minimumTokenMatch, List<String> submissionDirectories, List<String> oldSubmissionDirectories,
+            String baseCodeSubmissionName, String subdirectoryName, List<String> fileSuffixes, String exclusionFileName,
+            SimilarityMetric similarityMetric, float similarityThreshold, int maximumNumberOfComparisons, ClusteringOptions clusteringOptions,
+            ComparisonMode comparisonMode, Verbosity verbosity, boolean debugParser) {
+        this.language = language;
+        this.comparisonMode = comparisonMode;
+        this.debugParser = debugParser;
+        this.fileSuffixes = fileSuffixes == null ? null : Collections.unmodifiableList(fileSuffixes);
+        this.similarityThreshold = normalizeSimilarityThreshold(similarityThreshold);
+        this.maximumNumberOfComparisons = normalizeMaximumNumberOfComparisons(maximumNumberOfComparisons);
+        this.similarityMetric = similarityMetric;
+        this.minimumTokenMatch = normalizeMinimumTokenMatch(minimumTokenMatch);
+        this.exclusionFileName = exclusionFileName;
+        this.submissionDirectories = submissionDirectories == null ? null : Collections.unmodifiableList(submissionDirectories);
+        this.oldSubmissionDirectories = oldSubmissionDirectories == null ? null : Collections.unmodifiableList(oldSubmissionDirectories);
+        this.baseCodeSubmissionName = (baseCodeSubmissionName == null || baseCodeSubmissionName.isBlank()) ? null : baseCodeSubmissionName;
+        this.subdirectoryName = subdirectoryName;
+        this.verbosity = verbosity;
+        this.clusteringOptions = clusteringOptions;
     }
 
-    public int getMaximumNumberOfComparisons() {
-        return this.maximumNumberOfComparisons;
+    public JPlagOptions withLanguageOption(Language language) {
+        return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionName,
+                subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
+                clusteringOptions, comparisonMode, verbosity, debugParser);
     }
 
-    public Integer getMinimumTokenMatch() {
-        return minimumTokenMatch;
+    public JPlagOptions withComparisonMode(ComparisonMode comparisonMode) {
+        return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionName,
+                subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
+                clusteringOptions, comparisonMode, verbosity, debugParser);
     }
 
-    public List<String> getSubmissionDirectories() {
-        return submissionDirectories;
+    public JPlagOptions withDebugParser(boolean debugParser) {
+        return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionName,
+                subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
+                clusteringOptions, comparisonMode, verbosity, debugParser);
     }
 
-    public List<String> getOldSubmissionDirectories() {
-        return oldSubmissionDirectories;
+    public JPlagOptions withFileSuffixes(List<String> fileSuffixes) {
+        return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionName,
+                subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
+                clusteringOptions, comparisonMode, verbosity, debugParser);
     }
 
-    public SimilarityMetric getSimilarityMetric() {
-        return similarityMetric;
+    public JPlagOptions withSimilarityThreshold(float similarityThreshold) {
+        return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionName,
+                subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
+                clusteringOptions, comparisonMode, verbosity, debugParser);
     }
 
-    public float getSimilarityThreshold() {
-        return similarityThreshold;
+    public JPlagOptions withMaximumNumberOfComparisons(int maximumNumberOfComparisons) {
+        return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionName,
+                subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
+                clusteringOptions, comparisonMode, verbosity, debugParser);
     }
 
-    public String getSubdirectoryName() {
-        return subdirectoryName;
+    public JPlagOptions withSimilarityMetric(SimilarityMetric similarityMetric) {
+        return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionName,
+                subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
+                clusteringOptions, comparisonMode, verbosity, debugParser);
     }
 
-    public Verbosity getVerbosity() {
-        return verbosity;
+    public JPlagOptions withMinimumTokenMatch(Integer minimumTokenMatch) {
+        return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionName,
+                subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
+                clusteringOptions, comparisonMode, verbosity, debugParser);
     }
 
-    public boolean hasBaseCode() {
-        return this.baseCodeSubmissionName != null;
+    public JPlagOptions withExclusionFileName(String exclusionFileName) {
+        return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionName,
+                subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
+                clusteringOptions, comparisonMode, verbosity, debugParser);
     }
 
-    public boolean isDebugParser() {
-        return debugParser;
+    public JPlagOptions withSubmissionDirectories(List<String> submissionDirectories) {
+        return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionName,
+                subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
+                clusteringOptions, comparisonMode, verbosity, debugParser);
     }
 
-    public ClusteringOptions getClusteringOptions() {
-        return this.clusteringOptions;
+    public JPlagOptions withOldSubmissionDirectories(List<String> oldSubmissionDirectories) {
+        return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionName,
+                subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
+                clusteringOptions, comparisonMode, verbosity, debugParser);
     }
 
-    public void setBaseCodeSubmissionName(String baseCodeSubmissionName) {
-        if (baseCodeSubmissionName == null || baseCodeSubmissionName.isEmpty()) {
-            this.baseCodeSubmissionName = null;
-        } else {
-            this.baseCodeSubmissionName = baseCodeSubmissionName;
-        }
+    public JPlagOptions withBaseCodeSubmissionName(String baseCodeSubmissionName) {
+        return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionName,
+                subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
+                clusteringOptions, comparisonMode, verbosity, debugParser);
     }
 
-    public void setComparisonMode(ComparisonMode comparisonMode) {
-        this.comparisonMode = comparisonMode;
+    public JPlagOptions withSubdirectoryName(String subdirectoryName) {
+        return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionName,
+                subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
+                clusteringOptions, comparisonMode, verbosity, debugParser);
     }
 
-    public void setDebugParser(boolean debugParser) {
-        this.debugParser = debugParser;
+    public JPlagOptions withVerbosity(Verbosity verbosity) {
+        return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionName,
+                subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
+                clusteringOptions, comparisonMode, verbosity, debugParser);
     }
 
-    public void setExcludedFiles(Set<String> excludedFiles) {
-        this.excludedFiles = excludedFiles;
+    public JPlagOptions withClusteringOptions(ClusteringOptions clusteringOptions) {
+        return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionName,
+                subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
+                clusteringOptions, comparisonMode, verbosity, debugParser);
     }
 
-    public void setExclusionFileName(String exclusionFileName) {
-        this.exclusionFileName = exclusionFileName;
+    public boolean hasBaseCode() {
+        return baseCodeSubmissionName != null;
     }
 
-    public void setFileSuffixes(String[] fileSuffixes) {
-        this.fileSuffixes = fileSuffixes;
+    public Set<String> excludedFiles() {
+        return Optional.ofNullable(exclusionFileName()).map(this::readExclusionFile).orElse(Collections.emptySet());
     }
 
-    public void setLanguage(Language language) {
-        this.language = language;
+    @Override
+    public List<String> fileSuffixes() {
+        var language = language();
+        if (fileSuffixes == null && language != null)
+            return Arrays.stream(language.suffixes()).toList();
+        return fileSuffixes == null ? null : Collections.unmodifiableList(fileSuffixes);
     }
 
     /**
-     * After the selected language has been initialized, this method is called by JPlag to set default values for options
-     * not set by the user.
-     * @param language - initialized language instance
+     * Path name of the directory containing the base code.<br>
+     * For backwards compatibility it may also be a directory name inside the root directory. Condition for the latter is
+     * <ul>
+     * <li>Specified path does not exist.</li>
+     * <li>Name has not have a separator character after trimming them from both ends (leaving at least a one-character
+     * name).</li>
+     * <li>A submission with the specified name exists in the root directory.</li>
+     * </ul>
+     * It's an error if a string has been provided, but it is neither an existing path nor does it fulfill all the
+     * conditions of the compatibility fallback listed above.
      */
-    public void setLanguageDefaults(Language language) {
-        if (!hasMinimumTokenMatch()) {
-            setMinimumTokenMatch(language.minimumTokenMatch());
-        }
-
-        if (!hasFileSuffixes()) {
-            fileSuffixes = language.suffixes();
-        }
+    @Override
+    public String baseCodeSubmissionName() {
+        return baseCodeSubmissionName;
     }
 
-    public void setMaximumNumberOfComparisons(int maximumNumberOfComparisons) {
-        this.maximumNumberOfComparisons = Math.max(maximumNumberOfComparisons, -1);
+    @Override
+    public Integer minimumTokenMatch() {
+        var language = language();
+        if (minimumTokenMatch == null && language != null)
+            return language.minimumTokenMatch();
+        return minimumTokenMatch;
     }
 
-    public void setMinimumTokenMatch(Integer minimumTokenMatch) {
-        if (minimumTokenMatch != null && minimumTokenMatch < 1) {
-            this.minimumTokenMatch = 1;
-        } else {
-            this.minimumTokenMatch = minimumTokenMatch;
+    private Set<String> readExclusionFile(final String exclusionFileName) {
+        try (BufferedReader reader = new BufferedReader(new FileReader(exclusionFileName, JPlagOptions.CHARSET))) {
+            final var excludedFileNames = reader.lines().collect(Collectors.toSet());
+            if (verbosity() == LONG && logger.isInfoEnabled()) {
+                logger.info("Excluded files:\n{}", String.join("\n", excludedFileNames));
+            }
+            return excludedFileNames;
+        } catch (IOException e) {
+            logger.error("Could not read exclusion file: " + e.getMessage(), e);
+            return Collections.emptySet();
         }
     }
 
-    public void setSubmissionDirectories(List<String> submissionDirectories) {
-        this.submissionDirectories = submissionDirectories;
-    }
-
-    public void setOldSubmissionDirectories(List<String> oldSubmissionDirectories) {
-        this.oldSubmissionDirectories = oldSubmissionDirectories;
-    }
-
-    public void setSimilarityMetric(SimilarityMetric similarityMetric) {
-        this.similarityMetric = similarityMetric;
-    }
-
-    public void setSimilarityThreshold(float similarityThreshold) {
+    private static float normalizeSimilarityThreshold(float similarityThreshold) {
         if (similarityThreshold > 100) {
             logger.warn("Maximum threshold of 100 used instead of {}", similarityThreshold);
-            this.similarityThreshold = 100;
+            return 100;
         } else if (similarityThreshold < 0) {
             logger.warn("Minimum threshold of 0 used instead of {}", similarityThreshold);
-            this.similarityThreshold = 0;
+            return 0;
         } else {
-            this.similarityThreshold = similarityThreshold;
+            return similarityThreshold;
         }
     }
 
-    public void setSubdirectoryName(String subdirectoryName) {
-        // Trim problematic file separators.
-        this.subdirectoryName = (subdirectoryName == null) ? null : subdirectoryName.replace(File.separator, "");
-    }
-
-    public void setVerbosity(Verbosity verbosity) {
-        this.verbosity = verbosity;
-    }
-
-    public void setClusteringOptions(ClusteringOptions clusteringOptions) {
-        this.clusteringOptions = clusteringOptions;
+    private Integer normalizeMaximumNumberOfComparisons(Integer maximumNumberOfComparisons) {
+        return Math.max(maximumNumberOfComparisons, SHOW_ALL_COMPARISONS);
     }
 
-    private boolean hasFileSuffixes() {
-        return fileSuffixes != null && fileSuffixes.length > 0;
+    private Integer normalizeMinimumTokenMatch(Integer minimumTokenMatch) {
+        return (minimumTokenMatch != null && minimumTokenMatch < 1) ? Integer.valueOf(1) : minimumTokenMatch;
     }
-
-    private boolean hasMinimumTokenMatch() {
-        return minimumTokenMatch != null;
-    }
-
 }

+ 1 - 1
jplag/src/main/java/de/jplag/reporting/jsonfactory/ComparisonReportWriter.java

@@ -35,7 +35,7 @@ public class ComparisonReportWriter {
      * yield the same result.
      */
     public Map<String, Map<String, String>> writeComparisonReports(JPlagResult jPlagResult, String path) {
-        int numberOfComparisons = jPlagResult.getOptions().getMaximumNumberOfComparisons();
+        int numberOfComparisons = jPlagResult.getOptions().maximumNumberOfComparisons();
         List<JPlagComparison> comparisons = jPlagResult.getComparisons(numberOfComparisons);
         writeComparisons(path, comparisons);
         return submissionIdToComparisonFileName;

+ 1 - 1
jplag/src/main/java/de/jplag/reporting/jsonfactory/DirectoryManager.java

@@ -28,7 +28,7 @@ public class DirectoryManager {
      * @return The created directory
      */
     public static File createDirectory(String path, String name) throws IOException {
-        File directory = new File(path.concat("/").concat(name));
+        File directory = new File(path.concat(File.separator).concat(name));
         if (!directory.exists() && !directory.mkdirs()) {
             throw new IOException("Failed to create dir.");
         }

+ 9 - 9
jplag/src/main/java/de/jplag/reporting/reportobject/ReportObjectFactory.java

@@ -78,13 +78,13 @@ public class ReportObjectFactory {
     }
 
     private void copySubmissionFilesToReport(String path, JPlagResult result) {
-        List<JPlagComparison> comparisons = result.getComparisons(result.getOptions().getMaximumNumberOfComparisons());
+        List<JPlagComparison> comparisons = result.getComparisons(result.getOptions().maximumNumberOfComparisons());
         Set<Submission> submissions = getSubmissions(comparisons);
         File submissionsPath = createSubmissionsDirectory(path);
         if (submissionsPath == null) {
             return;
         }
-        Language language = result.getOptions().getLanguage();
+        Language language = result.getOptions().language();
         for (Submission submission : submissions) {
             File directory = createSubmissionDirectory(path, submissionsPath, submission);
             if (directory == null) {
@@ -127,21 +127,21 @@ public class ReportObjectFactory {
     private void writeOverview(JPlagResult result, String path) {
 
         List<String> folders = new ArrayList<>();
-        folders.addAll(result.getOptions().getSubmissionDirectories());
-        folders.addAll(result.getOptions().getOldSubmissionDirectories());
+        folders.addAll(result.getOptions().submissionDirectories());
+        folders.addAll(result.getOptions().oldSubmissionDirectories());
 
-        String baseCodePath = result.getOptions().hasBaseCode() ? result.getOptions().getBaseCodeSubmissionName().orElse("") : "";
+        String baseCodePath = result.getOptions().hasBaseCode() ? result.getOptions().baseCodeSubmissionName() : "";
         ClusteringResultMapper clusteringResultMapper = new ClusteringResultMapper(submissionToIdFunction);
 
         OverviewReport overviewReport = new OverviewReport(folders, // submissionFolderPath
                 baseCodePath, // baseCodeFolderPath
-                result.getOptions().getLanguage().getName(), // language
-                List.of(result.getOptions().getFileSuffixes()), // fileExtensions
+                result.getOptions().language().getName(), // language
+                result.getOptions().fileSuffixes(), // fileExtensions
                 submissionNameToIdMap.entrySet().stream().collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey)), // submissionIds
                 submissionNameToNameToComparisonFileName, // result.getOptions().getMinimumTokenMatch(),
                 List.of(), // failedSubmissionNames
-                result.getOptions().getExcludedFiles(), // excludedFiles
-                result.getOptions().getMinimumTokenMatch(), // matchSensitivity
+                result.getOptions().excludedFiles(), // excludedFiles
+                result.getOptions().minimumTokenMatch(), // matchSensitivity
                 getDate(),// dateOfExecution
                 result.getDuration(), // executionTime
                 getMetrics(result),// metrics

+ 1 - 1
jplag/src/main/java/de/jplag/reporting/reportobject/mapper/MetricMapper.java

@@ -35,7 +35,7 @@ public class MetricMapper {
     }
 
     private List<JPlagComparison> getComparisons(JPlagResult result) {
-        int maxNumberOfComparisons = result.getOptions().getMaximumNumberOfComparisons();
+        int maxNumberOfComparisons = result.getOptions().maximumNumberOfComparisons();
         return result.getComparisons(maxNumberOfComparisons);
     }
 

+ 1 - 1
jplag/src/main/java/de/jplag/reporting/reportobject/mapper/SubmissionNameToIdMapper.java

@@ -37,7 +37,7 @@ public class SubmissionNameToIdMapper {
     }
 
     private static List<JPlagComparison> getComparisons(JPlagResult result) {
-        int numberOfComparisons = result.getOptions().getMaximumNumberOfComparisons();
+        int numberOfComparisons = result.getOptions().maximumNumberOfComparisons();
         return result.getComparisons(numberOfComparisons);
     }
 }

+ 1 - 1
jplag/src/main/java/de/jplag/strategy/AbstractComparisonStrategy.java

@@ -49,7 +49,7 @@ public abstract class AbstractComparisonStrategy implements ComparisonStrategy {
         JPlagComparison comparison = greedyStringTiling.compare(first, second);
         logger.info("Comparing {}-{}: {}", first.getName(), second.getName(), comparison.similarity());
 
-        if (options.getSimilarityMetric().isAboveThreshold(comparison, options.getSimilarityThreshold())) {
+        if (options.similarityMetric().isAboveThreshold(comparison, options.similarityThreshold())) {
             return Optional.of(comparison);
         }
         return Optional.empty();

+ 8 - 8
jplag/src/test/java/de/jplag/BaseCodeTest.java

@@ -16,24 +16,24 @@ public class BaseCodeTest extends TestBase {
 
     @Test
     void testBasecodeUserSubmissionComparison() throws ExitException {
-        JPlagResult result = runJPlag("basecode", it -> it.setBaseCodeSubmissionName("base"));
+        JPlagResult result = runJPlag("basecode", it -> it.withBaseCodeSubmissionName("base"));
         verifyResults(result);
     }
 
     @Test
     void testTinyBasecode() {
-        assertThrows(BasecodeException.class, () -> runJPlag("TinyBasecode", it -> it.setBaseCodeSubmissionName("base")));
+        assertThrows(BasecodeException.class, () -> runJPlag("TinyBasecode", it -> it.withBaseCodeSubmissionName("base")));
     }
 
     @Test
     void testEmptySubmission() throws ExitException {
-        JPlagResult result = runJPlag("emptysubmission", it -> it.setBaseCodeSubmissionName("base"));
+        JPlagResult result = runJPlag("emptysubmission", it -> it.withBaseCodeSubmissionName("base"));
         verifyResults(result);
     }
 
     @Test
     void testAutoTrimFileSeparators() throws ExitException {
-        JPlagResult result = runJPlag("basecode", it -> it.setBaseCodeSubmissionName(File.separator + "base" + File.separator));
+        JPlagResult result = runJPlag("basecode", it -> it.withBaseCodeSubmissionName(File.separator + "base" + File.separator));
         verifyResults(result);
     }
 
@@ -47,22 +47,22 @@ public class BaseCodeTest extends TestBase {
 
     @Test
     void testBasecodePathComparison() throws ExitException {
-        JPlagResult result = runJPlag("basecode", it -> it.setBaseCodeSubmissionName(getBasePath("basecode-base")));
+        JPlagResult result = runJPlag("basecode", it -> it.withBaseCodeSubmissionName(getBasePath("basecode-base")));
         assertEquals(3, result.getNumberOfSubmissions()); // "basecode/base" is now a user submission.
     }
 
     @Test
     void testInvalidRoot() {
-        assertThrows(RootDirectoryException.class, () -> runJPlag("basecode", it -> it.setSubmissionDirectories(List.of("WrongRoot"))));
+        assertThrows(RootDirectoryException.class, () -> runJPlag("basecode", it -> it.withSubmissionDirectories(List.of("WrongRoot"))));
     }
 
     @Test
     void testInvalidBasecode() {
-        assertThrows(BasecodeException.class, () -> runJPlag("basecode", it -> it.setBaseCodeSubmissionName("WrongBasecode")));
+        assertThrows(BasecodeException.class, () -> runJPlag("basecode", it -> it.withBaseCodeSubmissionName("WrongBasecode")));
     }
 
     @Test
     void testBasecodeUserSubmissionWithDots() {
-        assertThrows(BasecodeException.class, () -> runJPlag("basecode", it -> it.setBaseCodeSubmissionName("base.ext")));
+        assertThrows(BasecodeException.class, () -> runJPlag("basecode", it -> it.withBaseCodeSubmissionName("base.ext")));
     }
 }

+ 1 - 1
jplag/src/test/java/de/jplag/InvalidSubmissionTest.java

@@ -22,7 +22,7 @@ class InvalidSubmissionTest extends TestBase {
     @Test
     void testInvalidSubmissionsWithDebug() throws ExitException {
         try {
-            runJPlag(SAMPLE_NAME, it -> it.setDebugParser(true));
+            runJPlag(SAMPLE_NAME, it -> it.withDebugParser(true));
             fail("No submission exception was thrown!");
         } catch (SubmissionException e) {
             System.out.println(e.getMessage());

+ 8 - 11
jplag/src/test/java/de/jplag/NormalComparisonTest.java

@@ -34,7 +34,7 @@ class NormalComparisonTest extends TestBase {
     @Test
     void testWithMinTokenMatch() throws ExitException {
         var expectedDistribution = new int[] {1, 0, 0, 0, 0, 0, 0, 0, 0, 0};
-        JPlagResult result = runJPlag("SimpleDuplicate", it -> it.setMinimumTokenMatch(5));
+        JPlagResult result = runJPlag("SimpleDuplicate", it -> it.withMinimumTokenMatch(5));
 
         assertEquals(2, result.getNumberOfSubmissions());
         assertEquals(1, result.getAllComparisons().size());
@@ -104,8 +104,7 @@ class NormalComparisonTest extends TestBase {
     @Test
     void testMultiRootDirNoBasecode() throws ExitException {
         List<String> paths = List.of(getBasePath("basecode"), getBasePath("SimpleDuplicate")); // 3 + 2 submissions.
-        JPlagResult result = runJPlag(paths, options -> {
-        });
+        JPlagResult result = runJPlag(paths, it -> it);
         assertEquals(5, result.getNumberOfSubmissions());
     }
 
@@ -113,7 +112,7 @@ class NormalComparisonTest extends TestBase {
     void testMultiRootDirSeparateBasecode() throws ExitException {
         String basecodePath = getBasePath("basecode-base");
         List<String> paths = List.of(getBasePath("basecode"), getBasePath("SimpleDuplicate")); // 3 + 2 submissions.
-        JPlagResult result = runJPlag(paths, it -> it.setBaseCodeSubmissionName(basecodePath));
+        JPlagResult result = runJPlag(paths, it -> it.withBaseCodeSubmissionName(basecodePath));
         assertEquals(5, result.getNumberOfSubmissions());
     }
 
@@ -121,7 +120,7 @@ class NormalComparisonTest extends TestBase {
     public void testMultiRootDirBasecodeInSubmissionDir() throws ExitException {
         String basecodePath = getBasePath("basecode", "base");
         List<String> paths = List.of(getBasePath("basecode"), getBasePath("SimpleDuplicate")); // 2 + 2 submissions.
-        JPlagResult result = runJPlag(paths, it -> it.setBaseCodeSubmissionName(basecodePath));
+        JPlagResult result = runJPlag(paths, it -> it.withBaseCodeSubmissionName(basecodePath));
         assertEquals(4, result.getNumberOfSubmissions());
     }
 
@@ -129,15 +128,14 @@ class NormalComparisonTest extends TestBase {
     public void testMultiRootDirBasecodeName() {
         List<String> paths = List.of(getBasePath("basecode"), getBasePath("SimpleDuplicate"));
         String basecodePath = "base"; // Should *not* find basecode/base
-        assertThrows(BasecodeException.class, () -> runJPlag(paths, it -> it.setBaseCodeSubmissionName(basecodePath)));
+        assertThrows(BasecodeException.class, () -> runJPlag(paths, it -> it.withBaseCodeSubmissionName(basecodePath)));
     }
 
     @Test
     public void testDisjunctNewAndOldRootDirectories() throws ExitException {
         List<String> newDirectories = List.of(getBasePath("SimpleDuplicate")); // 2 submissions
         List<String> oldDirectories = List.of(getBasePath("basecode")); // 3 submissions
-        JPlagResult result = runJPlag(newDirectories, oldDirectories, it -> {
-        });
+        JPlagResult result = runJPlag(newDirectories, oldDirectories, it -> it);
         int numberOfExpectedComparison = 1 + 3 * 2;
         assertEquals(numberOfExpectedComparison, result.getAllComparisons().size());
     }
@@ -146,8 +144,7 @@ class NormalComparisonTest extends TestBase {
     void testOverlappingNewAndOldDirectoriesOverlap() throws ExitException {
         List<String> newDirectories = List.of(getBasePath("SimpleDuplicate")); // 2 submissions
         List<String> oldDirectories = List.of(getBasePath("SimpleDuplicate"));
-        JPlagResult result = runJPlag(newDirectories, oldDirectories, it -> {
-        });
+        JPlagResult result = runJPlag(newDirectories, oldDirectories, it -> it);
         int numberOfExpectedComparison = 1;
         assertEquals(numberOfExpectedComparison, result.getAllComparisons().size());
     }
@@ -157,7 +154,7 @@ class NormalComparisonTest extends TestBase {
         String basecodePath = getBasePath("basecode", "base");
         List<String> newDirectories = List.of(getBasePath("SimpleDuplicate")); // 2 submissions
         List<String> oldDirectories = List.of(getBasePath("basecode")); // 3 - 1 submissions
-        JPlagResult result = runJPlag(newDirectories, oldDirectories, it -> it.setBaseCodeSubmissionName(basecodePath));
+        JPlagResult result = runJPlag(newDirectories, oldDirectories, it -> it.withBaseCodeSubmissionName(basecodePath));
         int numberOfExpectedComparison = 1 + 2 * 2;
         assertEquals(numberOfExpectedComparison, result.getAllComparisons().size());
     }

+ 3 - 3
jplag/src/test/java/de/jplag/ParallelComparisonTest.java

@@ -22,7 +22,7 @@ public class ParallelComparisonTest extends TestBase {
      */
     @Test
     public void testSimpleDuplicate() throws ExitException {
-        JPlagResult result = runJPlag("SimpleDuplicate", it -> it.setComparisonMode(PARALLEL));
+        JPlagResult result = runJPlag("SimpleDuplicate", it -> it.withComparisonMode(PARALLEL));
 
         assertEquals(2, result.getNumberOfSubmissions());
         assertEquals(1, result.getAllComparisons().size());
@@ -36,7 +36,7 @@ public class ParallelComparisonTest extends TestBase {
      */
     @Test
     public void testNoDuplicate() throws ExitException {
-        JPlagResult result = runJPlag("NoDuplicate", it -> it.setComparisonMode(PARALLEL));
+        JPlagResult result = runJPlag("NoDuplicate", it -> it.withComparisonMode(PARALLEL));
 
         assertEquals(3, result.getNumberOfSubmissions());
         assertEquals(3, result.getAllComparisons().size());
@@ -51,7 +51,7 @@ public class ParallelComparisonTest extends TestBase {
      */
     @Test
     public void testPartialPlagiarism() throws ExitException {
-        JPlagResult result = runJPlag("PartialPlagiarism", it -> it.setComparisonMode(PARALLEL));
+        JPlagResult result = runJPlag("PartialPlagiarism", it -> it.withComparisonMode(PARALLEL));
 
         assertEquals(5, result.getNumberOfSubmissions());
         assertEquals(10, result.getAllComparisons().size());

+ 10 - 10
jplag/src/test/java/de/jplag/TestBase.java

@@ -4,7 +4,7 @@ import java.io.File;
 import java.nio.file.Path;
 import java.util.List;
 import java.util.StringJoiner;
-import java.util.function.Consumer;
+import java.util.function.Function;
 
 import de.jplag.exceptions.ExitException;
 import de.jplag.java.Language;
@@ -31,26 +31,26 @@ public abstract class TestBase {
 
     protected JPlagResult runJPlagWithExclusionFile(String testSampleName, String exclusionFileName) throws ExitException {
         String blackList = Path.of(BASE_PATH, testSampleName, exclusionFileName).toString();
-        return runJPlag(testSampleName, options -> options.setExclusionFileName(blackList));
+        return runJPlag(testSampleName, options -> options.withExclusionFileName(blackList));
     }
 
     protected JPlagResult runJPlagWithDefaultOptions(String testSampleName) throws ExitException {
-        return runJPlag(testSampleName, options -> {
-        });
+        return runJPlag(testSampleName, options -> options);
     }
 
-    protected JPlagResult runJPlag(String testSampleName, Consumer<JPlagOptions> customization) throws ExitException {
+    protected JPlagResult runJPlag(String testSampleName, Function<JPlagOptions, JPlagOptions> customization) throws ExitException {
         return runJPlag(List.of(getBasePath(testSampleName)), List.of(), customization);
     }
 
-    protected JPlagResult runJPlag(List<String> newPaths, Consumer<JPlagOptions> customization) throws ExitException {
+    protected JPlagResult runJPlag(List<String> newPaths, Function<JPlagOptions, JPlagOptions> customization) throws ExitException {
         return runJPlag(newPaths, List.of(), customization);
     }
 
-    protected JPlagResult runJPlag(List<String> newPaths, List<String> oldPaths, Consumer<JPlagOptions> customization) throws ExitException {
-        JPlagOptions options = new JPlagOptions(newPaths, oldPaths, Language.IDENTIFIER);
-        options.setVerbosity(Verbosity.LONG);
-        customization.accept(options);
+    protected JPlagResult runJPlag(List<String> newPaths, List<String> oldPaths, Function<JPlagOptions, JPlagOptions> customization)
+            throws ExitException {
+        JPlagOptions options = new JPlagOptions(LanguageLoader.getLanguage(Language.IDENTIFIER).orElseThrow(), newPaths, oldPaths);
+        options = customization.apply(options);
+        options = options.withVerbosity(Verbosity.LONG);
         JPlag jplag = new JPlag(options);
         return jplag.run();
     }

+ 1 - 1
jplag/src/test/java/de/jplag/reporting/reportobject/mapper/MetricMapperTest.java

@@ -76,7 +76,7 @@ public class MetricMapperTest {
         }
 
         JPlagOptions options = mock(JPlagOptions.class);
-        doReturn(createComparisonsDto.length).when(options).getMaximumNumberOfComparisons();
+        doReturn(createComparisonsDto.length).when(options).maximumNumberOfComparisons();
         doReturn(options).when(jPlagResult).getOptions();
 
         List<JPlagComparison> comparisonList = new ArrayList<>();

+ 14 - 16
jplag/src/test/java/de/jplag/special/TokenPrinterTest.java

@@ -2,12 +2,16 @@ package de.jplag.special;
 
 import static org.junit.jupiter.api.Assertions.fail;
 
-import java.util.function.Consumer;
+import java.util.function.Function;
 
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 
-import de.jplag.*;
+import de.jplag.JPlagResult;
+import de.jplag.LanguageLoader;
+import de.jplag.Submission;
+import de.jplag.TestBase;
+import de.jplag.TokenPrinter;
 import de.jplag.exceptions.ExitException;
 import de.jplag.options.JPlagOptions;
 
@@ -31,42 +35,36 @@ class TokenPrinterTest extends TestBase {
     @Disabled("Not a meaningful test, used for designing the token set")
     @Test
     void printCPPFiles() {
-        printSubmissions(options -> {
-            options.setLanguage(LanguageLoader.getLanguage(LANGUAGE_CPP).orElseThrow());
-            options.setMinimumTokenMatch(MIN_TOKEN_MATCH); // for printing also allow small files
-        });
+        printSubmissions(
+                options -> options.withLanguageOption(LanguageLoader.getLanguage(LANGUAGE_CPP).orElseThrow()).withMinimumTokenMatch(MIN_TOKEN_MATCH));
     }
 
     @Disabled("Not a meaningful test, used for designing the token set")
     @Test
     void printJavaFiles() {
-        printSubmissions(options -> {
-            options.setMinimumTokenMatch(MIN_TOKEN_MATCH); // for printing also allow small files
-        });
+        printSubmissions(options -> options.withMinimumTokenMatch(MIN_TOKEN_MATCH));
     }
 
     @Disabled("Not a meaningful test, used for designing the token set")
     @Test
     void printRLangFiles() {
-        printSubmissions(options -> {
-            options.setLanguage(LanguageLoader.getLanguage(LANGUAGE_R).orElseThrow());
-            options.setMinimumTokenMatch(MIN_TOKEN_MATCH); // for printing also allow small files
-        });
+        printSubmissions(
+                options -> options.withLanguageOption(LanguageLoader.getLanguage(LANGUAGE_R).orElseThrow()).withMinimumTokenMatch(MIN_TOKEN_MATCH));
     }
 
     @Disabled("Not a meaningful test, used for designing the token set")
     @Test
     void printGoFiles() {
-        printSubmissions(options -> options.setLanguage(LanguageLoader.getLanguage(LANGUAGE_GO).orElseThrow()));
+        printSubmissions(options -> options.withLanguageOption(LanguageLoader.getLanguage(LANGUAGE_GO).orElseThrow()));
     }
 
     @Disabled("Not a meaningful test, used for designing the token set")
     @Test
     void printKotlinFiles() {
-        printSubmissions(options -> options.setLanguage(LanguageLoader.getLanguage(LANGUAGE_KOTLIN).orElseThrow()));
+        printSubmissions(options -> options.withLanguageOption(LanguageLoader.getLanguage(LANGUAGE_KOTLIN).orElseThrow()));
     }
 
-    private void printSubmissions(Consumer<JPlagOptions> optionsCustomization) {
+    private void printSubmissions(Function<JPlagOptions, JPlagOptions> optionsCustomization) {
         try {
             JPlagResult result = runJPlag(PRINTER_FOLDER, optionsCustomization);
             for (Submission submission : result.getSubmissions().getSubmissions()) {

+ 1 - 1
jplag/src/test/java/de/jplag/special/VolumeTest.java

@@ -47,7 +47,7 @@ public class VolumeTest extends TestBase {
             return;
         }
 
-        var results = runJPlag("data", jPlagOptions -> jPlagOptions.setMaximumNumberOfComparisons(-1));
+        var results = runJPlag("data", jPlagOptions -> jPlagOptions.withMaximumNumberOfComparisons(JPlagOptions.SHOW_ALL_COMPARISONS));
 
         var csv = readCSVResults(String.format("%s/%s", this.getBasePath(), "matches_avg.csv"));
 

+ 7 - 0
pom.xml

@@ -112,6 +112,13 @@
                 <version>0.9.0</version>
             </dependency>
 
+            <!-- CoreNLP -->
+            <dependency>
+                <groupId>edu.stanford.nlp</groupId>
+                <artifactId>stanford-corenlp</artifactId>
+                <version>4.5.0</version>
+            </dependency>
+
             <!-- LOGGER -->
             <dependency>
                 <groupId>org.slf4j</groupId>

+ 45 - 9
report-viewer/package-lock.json

@@ -16,6 +16,7 @@
         "highlight.js": "^11.6.0",
         "jszip": "^3.10.0",
         "node-polyfill-webpack-plugin": "^2.0.0",
+        "slash": "^4.0.0",
         "vue": "^3.2.37",
         "vue-chart-3": "^3.1.8",
         "vue-draggable-next": "^2.1.1",
@@ -5505,6 +5506,15 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/del/node_modules/slash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+      "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/depd": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
@@ -7221,6 +7231,15 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/globby/node_modules/slash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+      "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/graceful-fs": {
       "version": "4.2.9",
       "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz",
@@ -11712,12 +11731,14 @@
       }
     },
     "node_modules/slash": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
-      "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
-      "dev": true,
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz",
+      "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==",
       "engines": {
-        "node": ">=8"
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
     "node_modules/slice-ansi": {
@@ -17892,6 +17913,14 @@
         "p-map": "^4.0.0",
         "rimraf": "^3.0.2",
         "slash": "^3.0.0"
+      },
+      "dependencies": {
+        "slash": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+          "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+          "dev": true
+        }
       }
     },
     "depd": {
@@ -19175,6 +19204,14 @@
         "ignore": "^5.2.0",
         "merge2": "^1.4.1",
         "slash": "^3.0.0"
+      },
+      "dependencies": {
+        "slash": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+          "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+          "dev": true
+        }
       }
     },
     "graceful-fs": {
@@ -22465,10 +22502,9 @@
       }
     },
     "slash": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
-      "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
-      "dev": true
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz",
+      "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew=="
     },
     "slice-ansi": {
       "version": "5.0.0",

+ 1 - 0
report-viewer/package.json

@@ -16,6 +16,7 @@
     "highlight.js": "^11.6.0",
     "jszip": "^3.10.0",
     "node-polyfill-webpack-plugin": "^2.0.0",
+    "slash": "^4.0.0",
     "vue": "^3.2.37",
     "vue-chart-3": "^3.1.8",
     "vue-draggable-next": "^2.1.1",

+ 1 - 1
report-viewer/src/router/index.ts

@@ -18,7 +18,7 @@ const routes: Array<RouteRecordRaw> = [
     component: OverviewView,
   },
   {
-    path: "/comparison",
+    path: "/comparison/:firstId/:secondId",
     name: "ComparisonView",
     component: ComparisonView,
     props: true,

+ 13 - 12
report-viewer/src/store/store.ts

@@ -95,21 +95,22 @@ const store = createStore<State>({
       return Array.from(state.fileIdToDisplayName.keys());
     },
     getComparisonFileName:
-      (state) => (submissionId: string, fileId: string) => {
+      (state) => (submissionId1: string, submissionId2: string) => {
         return state.submissionIdsToComparisonFileName
-          .get(submissionId)
-          ?.get(fileId);
+          .get(submissionId1)
+          ?.get(submissionId2);
       },
-      getComparisonFileForSubmissions: (state, getters) => (submissionId1: string, submissionId2: string) => {
-        const expectedFileName = getters.getComparisonFileName(submissionId1, submissionId2);
-        const index = Object.keys(store.state.files).find(
-          (name) =>
-             name.endsWith(expectedFileName)
+    getComparisonFileForSubmissions:
+      (state, getters) => (submissionId1: string, submissionId2: string) => {
+        const expectedFileName = getters.getComparisonFileName(
+          submissionId1,
+          submissionId2
         );
-        return index != undefined
-          ? store.state.files[index]
-          : undefined
-      }
+        const index = Object.keys(store.state.files).find((name) =>
+          name.endsWith(expectedFileName)
+        );
+        return index != undefined ? store.state.files[index] : undefined;
+      },
   },
   mutations: {
     addAnonymous(state: State, id) {

+ 1 - 2
report-viewer/src/views/ComparisonView.vue

@@ -118,8 +118,7 @@ export default defineComponent({
       );
     }
     if (!comparison) {
-      console.warn("Could not build comparison file");
-      return;
+      throw "Could not build comparison file";
     }
     const filesOfFirst = ref(comparison.filesOfFirstSubmission);
     const filesOfSecond = ref(comparison.filesOfSecondSubmission);

+ 10 - 7
report-viewer/src/views/FileUploadView.vue

@@ -25,6 +25,7 @@ import router from "@/router";
 import store from "@/store/store";
 import { getFileExtension } from "@/utils/Utils";
 import path from "path";
+import slash from "slash";
 
 export default defineComponent({
   name: "FileUploadView",
@@ -66,22 +67,24 @@ export default defineComponent({
      */
     const handleZipFile = (file: File) => {
       jszip.loadAsync(file).then(async (zip) => {
-        for (const fileName of Object.keys(zip.files)) {
+        for (const originalFileName of Object.keys(zip.files)) {
+          const unixFileName = slash(originalFileName);
           if (
-            /((.+\/)*)submissions\/(.+)\/(.+)/.test(fileName) &&
-            !/^__MACOSX\//.test(fileName)
+            /((.+\/)*)submissions\/(.+)\/(.+)/.test(unixFileName) &&
+            !/^__MACOSX\//.test(unixFileName)
           ) {
-            const filePath = path.parse(fileName);
+            const filePath = path.parse(unixFileName);
+
             const submissionFileName = extractSubmissionFileName(filePath);
-            await zip.files[fileName].async("string").then((data) => {
+            await zip.files[originalFileName].async("string").then((data) => {
               store.commit("saveSubmissionFile", {
                 name: submissionFileName,
                 file: { fileName: filePath.base, data: data },
               });
             });
           } else {
-            await zip.files[fileName].async("string").then((data) => {
-              store.commit("saveFile", { fileName: fileName, data: data });
+            await zip.files[originalFileName].async("string").then((data) => {
+              store.commit("saveFile", { fileName: unixFileName, data: data });
             });
           }
         }