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

Merge pull request #572 from jplag/feature/jplagoptions-ro

Convert JPlagOption to Record (→ Read-Only)
Dominik Fuchß 4 лет назад
Родитель
Сommit
8ec60b6a30
29 измененных файлов с 362 добавлено и 471 удалено
  1. 41 16
      jplag.cli/src/main/java/de/jplag/CLI.java
  2. 3 4
      jplag.cli/src/test/java/de/jplag/cli/BaseCodeOptionTest.java
  3. 6 6
      jplag.cli/src/test/java/de/jplag/cli/ClusteringTest.java
  4. 3 3
      jplag.cli/src/test/java/de/jplag/cli/ComparisonModeTest.java
  5. 2 2
      jplag.cli/src/test/java/de/jplag/cli/LanguageTest.java
  6. 10 18
      jplag.cli/src/test/java/de/jplag/cli/MinTokenMatchTest.java
  7. 19 19
      jplag.cli/src/test/java/de/jplag/cli/OldNewRootDirectoriesArgumentTest.java
  8. 10 10
      jplag.cli/src/test/java/de/jplag/cli/SimiliarityThresholdTest.java
  9. 11 11
      jplag.cli/src/test/java/de/jplag/cli/StoredMatchesTest.java
  10. 5 5
      jplag.endtoend-testing/src/test/java/de/jplag/endtoend/EndToEndSuiteTest.java
  11. 1 1
      jplag/src/main/java/de/jplag/GreedyStringTiling.java
  12. 5 55
      jplag/src/main/java/de/jplag/JPlag.java
  13. 2 2
      jplag/src/main/java/de/jplag/JPlagResult.java
  14. 4 4
      jplag/src/main/java/de/jplag/SubmissionSet.java
  15. 12 13
      jplag/src/main/java/de/jplag/SubmissionSetBuilder.java
  16. 169 238
      jplag/src/main/java/de/jplag/options/JPlagOptions.java
  17. 1 1
      jplag/src/main/java/de/jplag/reporting/jsonfactory/ComparisonReportWriter.java
  18. 9 9
      jplag/src/main/java/de/jplag/reporting/reportobject/ReportObjectFactory.java
  19. 1 1
      jplag/src/main/java/de/jplag/reporting/reportobject/mapper/MetricMapper.java
  20. 1 1
      jplag/src/main/java/de/jplag/reporting/reportobject/mapper/SubmissionNameToIdMapper.java
  21. 1 1
      jplag/src/main/java/de/jplag/strategy/AbstractComparisonStrategy.java
  22. 8 8
      jplag/src/test/java/de/jplag/BaseCodeTest.java
  23. 1 1
      jplag/src/test/java/de/jplag/InvalidSubmissionTest.java
  24. 8 11
      jplag/src/test/java/de/jplag/NormalComparisonTest.java
  25. 3 3
      jplag/src/test/java/de/jplag/ParallelComparisonTest.java
  26. 10 10
      jplag/src/test/java/de/jplag/TestBase.java
  27. 1 1
      jplag/src/test/java/de/jplag/reporting/reportobject/mapper/MetricMapperTest.java
  28. 14 16
      jplag/src/test/java/de/jplag/special/TokenPrinterTest.java
  29. 1 1
      jplag/src/test/java/de/jplag/special/VolumeTest.java

+ 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();

+ 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;

+ 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"));