Bläddra i källkod

Merge remote-tracking branch 'origin/rust' into rust

robin 4 år sedan
förälder
incheckning
fa2a1e3e40
100 ändrade filer med 3351 tillägg och 1342 borttagningar
  1. 11 7
      jplag.cli/pom.xml
  2. 23 0
      jplag.cli/src/assembly/src.xml
  3. 42 19
      jplag.cli/src/main/java/de/jplag/CLI.java
  4. 58 40
      jplag.cli/src/main/java/de/jplag/CommandLineArgument.java
  5. 44 0
      jplag.cli/src/main/java/de/jplag/logger/CollectedLoggerServiceProvider.java
  6. 0 54
      jplag.cli/src/main/java/org/slf4j/impl/StaticLoggerBinder.java
  7. 3 4
      jplag.cli/src/test/java/de/jplag/cli/BaseCodeOptionTest.java
  8. 6 6
      jplag.cli/src/test/java/de/jplag/cli/ClusteringTest.java
  9. 3 3
      jplag.cli/src/test/java/de/jplag/cli/ComparisonModeTest.java
  10. 0 35
      jplag.cli/src/test/java/de/jplag/cli/LanguageOptionTest.java
  11. 43 0
      jplag.cli/src/test/java/de/jplag/cli/LanguageTest.java
  12. 10 18
      jplag.cli/src/test/java/de/jplag/cli/MinTokenMatchTest.java
  13. 19 19
      jplag.cli/src/test/java/de/jplag/cli/OldNewRootDirectoriesArgumentTest.java
  14. 10 10
      jplag.cli/src/test/java/de/jplag/cli/SimiliarityThresholdTest.java
  15. 11 11
      jplag.cli/src/test/java/de/jplag/cli/StoredMatchesTest.java
  16. 0 105
      jplag.endToEndTesting/README.md
  17. 0 32
      jplag.endToEndTesting/src/main/java/de/jplag/end_to_end_testing/constants/TestDirectoryConstants.java
  18. 0 120
      jplag.endToEndTesting/src/main/java/de/jplag/end_to_end_testing/helper/JPlagTestSuiteHelper.java
  19. 0 33
      jplag.endToEndTesting/src/main/java/de/jplag/end_to_end_testing/helper/JsonHelper.java
  20. 0 52
      jplag.endToEndTesting/src/main/java/de/jplag/end_to_end_testing/model/JsonModel.java
  21. 0 48
      jplag.endToEndTesting/src/main/java/de/jplag/end_to_end_testing/model/ResultModel.java
  22. 0 46
      jplag.endToEndTesting/src/main/java/de/jplag/end_to_end_testing/model/TestCaseModel.java
  23. 0 114
      jplag.endToEndTesting/src/test/java/de/jplag/end_to_end_testing/JavaEndToEndTest.java
  24. 0 36
      jplag.endToEndTesting/src/test/resources/results/JavaResult.json
  25. 174 0
      jplag.endtoend-testing/README.md
  26. 6 1
      jplag.endtoend-testing/pom.xml
  27. 35 0
      jplag.endtoend-testing/src/main/java/de/jplag/endtoend/constants/TestDirectoryConstants.java
  28. 143 0
      jplag.endtoend-testing/src/main/java/de/jplag/endtoend/helper/FileHelper.java
  29. 71 0
      jplag.endtoend-testing/src/main/java/de/jplag/endtoend/helper/JsonHelper.java
  30. 89 0
      jplag.endtoend-testing/src/main/java/de/jplag/endtoend/helper/TestSuiteHelper.java
  31. 12 0
      jplag.endtoend-testing/src/main/java/de/jplag/endtoend/model/ExpectedResult.java
  32. 10 0
      jplag.endtoend-testing/src/main/java/de/jplag/endtoend/model/Options.java
  33. 34 0
      jplag.endtoend-testing/src/main/java/de/jplag/endtoend/model/ResultDescription.java
  34. 258 0
      jplag.endtoend-testing/src/test/java/de/jplag/endtoend/EndToEndSuiteTest.java
  35. 0 0
      jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo.java
  36. 0 0
      jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo1.java
  37. 0 0
      jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo1_2.java
  38. 0 0
      jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo1_3.java
  39. 0 0
      jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo1_4.java
  40. 0 0
      jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo1_5.java
  41. 0 0
      jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo1_6.java
  42. 0 0
      jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo2.java
  43. 0 0
      jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo2_5.java
  44. 0 0
      jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo3.java
  45. 0 0
      jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo3_5.java
  46. 0 0
      jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo3_6.java
  47. 0 0
      jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo4.java
  48. 0 0
      jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo4d1.java
  49. 0 0
      jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo4d2.java
  50. 0 0
      jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo4d3.java
  51. 0 0
      jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo5.java
  52. 0 0
      jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo6.java
  53. 0 0
      jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo7.java
  54. 1723 0
      jplag.endtoend-testing/src/test/resources/results/java/sortAlgo.json
  55. 6 0
      jplag.frontend-utils/pom.xml
  56. 2 2
      jplag.frontend-utils/src/main/java/de/jplag/Language.java
  57. 75 0
      jplag.frontend-utils/src/main/java/de/jplag/LanguageLoader.java
  58. 7 2
      jplag.frontend.cpp/src/main/java/de/jplag/cpp/Language.java
  59. 6 3
      jplag.frontend.csharp-6/src/main/java/de/jplag/csharp/Language.java
  60. 6 3
      jplag.frontend.emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/Language.java
  61. 5 3
      jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/Language.java
  62. 6 3
      jplag.frontend.golang/src/main/java/de/jplag/golang/Language.java
  63. 19 10
      jplag.frontend.java/src/main/java/de/jplag/java/JavacAdapter.java
  64. 7 2
      jplag.frontend.java/src/main/java/de/jplag/java/Language.java
  65. 6 3
      jplag.frontend.kotlin/src/main/java/de/jplag/kotlin/Language.java
  66. 7 2
      jplag.frontend.python-3/src/main/java/de/jplag/python3/Language.java
  67. 6 3
      jplag.frontend.rlang/src/main/java/de/jplag/rlang/Language.java
  68. 3 3
      jplag.frontend.rust/src/main/java/de/jplag/rust/Language.java
  69. 1 0
      jplag.frontend.scala/src/main/resources/META-INF/services/de.jplag.Language
  70. 3 1
      jplag.frontend.scala/src/main/scala/de/jplag/scala/Language.scala
  71. 7 2
      jplag.frontend.scheme/src/main/java/de/jplag/scheme/Language.java
  72. 6 7
      jplag.frontend.text/src/main/java/de/jplag/text/Language.java
  73. 11 19
      jplag/pom.xml
  74. 1 1
      jplag/src/main/java/de/jplag/GreedyStringTiling.java
  75. 7 61
      jplag/src/main/java/de/jplag/JPlag.java
  76. 2 2
      jplag/src/main/java/de/jplag/JPlagResult.java
  77. 1 1
      jplag/src/main/java/de/jplag/Submission.java
  78. 4 4
      jplag/src/main/java/de/jplag/SubmissionSet.java
  79. 12 13
      jplag/src/main/java/de/jplag/SubmissionSetBuilder.java
  80. 169 241
      jplag/src/main/java/de/jplag/options/JPlagOptions.java
  81. 0 53
      jplag/src/main/java/de/jplag/options/LanguageOption.java
  82. 1 1
      jplag/src/main/java/de/jplag/reporting/jsonfactory/ComparisonReportWriter.java
  83. 1 1
      jplag/src/main/java/de/jplag/reporting/jsonfactory/DirectoryManager.java
  84. 9 9
      jplag/src/main/java/de/jplag/reporting/reportobject/ReportObjectFactory.java
  85. 1 1
      jplag/src/main/java/de/jplag/reporting/reportobject/mapper/MetricMapper.java
  86. 1 1
      jplag/src/main/java/de/jplag/reporting/reportobject/mapper/SubmissionNameToIdMapper.java
  87. 1 1
      jplag/src/main/java/de/jplag/strategy/AbstractComparisonStrategy.java
  88. 1 1
      jplag/src/main/resources/de/jplag/messages.properties
  89. 8 8
      jplag/src/test/java/de/jplag/BaseCodeTest.java
  90. 3 2
      jplag/src/test/java/de/jplag/InvalidSubmissionTest.java
  91. 8 11
      jplag/src/test/java/de/jplag/NormalComparisonTest.java
  92. 3 3
      jplag/src/test/java/de/jplag/ParallelComparisonTest.java
  93. 11 11
      jplag/src/test/java/de/jplag/TestBase.java
  94. 1 1
      jplag/src/test/java/de/jplag/reporting/reportobject/mapper/MetricMapperTest.java
  95. 23 20
      jplag/src/test/java/de/jplag/special/TokenPrinterTest.java
  96. 1 1
      jplag/src/test/java/de/jplag/special/VolumeTest.java
  97. 8 3
      pom.xml
  98. 45 9
      report-viewer/package-lock.json
  99. 1 0
      report-viewer/package.json
  100. 1 1
      report-viewer/src/router/index.ts

+ 11 - 7
jplag.cli/pom.xml

@@ -12,6 +12,8 @@
             <groupId>de.jplag</groupId>
             <artifactId>jplag</artifactId>
         </dependency>
+
+        <!-- Languages -->
         <dependency>
             <groupId>de.jplag</groupId>
             <artifactId>frontend-utils</artifactId>
@@ -68,9 +70,10 @@
             <groupId>de.jplag</groupId>
             <artifactId>emf-metamodel-dynamic</artifactId>
         </dependency>
+        <!-- CLI -->
         <dependency>
-            <groupId>de.jplag</groupId>
-            <artifactId>endToEndTesting</artifactId>
+            <groupId>org.kohsuke.metainf-services</groupId>
+            <artifactId>metainf-services</artifactId>
         </dependency>
 
         <dependency>
@@ -91,17 +94,18 @@
                     </archive>
                 </configuration>
             </plugin>
-
             <plugin>
                 <artifactId>maven-assembly-plugin</artifactId>
-                <configuration>
-                    <descriptorRefs>
-                        <descriptorRef>jar-with-dependencies</descriptorRef>
-                    </descriptorRefs>
+                <configuration combine.self="override">
+                    <descriptors>
+                        <descriptor>src/assembly/src.xml</descriptor>
+                    </descriptors>
                     <archive>
                         <manifest>
                             <mainClass>de.jplag.CLI</mainClass>
                             <packageName>jplag</packageName>
+                            <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
+                            <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
                         </manifest>
                     </archive>
                     <finalName>jplag-${project.version}</finalName>

+ 23 - 0
jplag.cli/src/assembly/src.xml

@@ -0,0 +1,23 @@
+<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.1.1"
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.1.1 https://maven.apache.org/xsd/assembly-2.1.1.xsd">
+    <id>jar-with-dependencies</id>
+    <formats>
+        <format>jar</format>
+    </formats>
+    <includeBaseDirectory>false</includeBaseDirectory>
+    <dependencySets>
+        <dependencySet>
+            <outputDirectory>/</outputDirectory>
+            <useProjectArtifact>true</useProjectArtifact>
+            <unpack>true</unpack>
+            <scope>runtime</scope>
+        </dependencySet>
+    </dependencySets>
+    <!-- Merge MetaInf-Services in final JAR -->
+    <containerDescriptorHandlers>
+        <containerDescriptorHandler>
+            <handlerName>metaInf-services</handlerName>
+        </containerDescriptorHandler>
+    </containerDescriptorHandlers>
+</assembly>

+ 42 - 19
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;
@@ -24,7 +53,6 @@ import de.jplag.clustering.algorithm.InterClusterSimilarity;
 import de.jplag.exceptions.ExitException;
 import de.jplag.logger.CollectedLoggerFactory;
 import de.jplag.options.JPlagOptions;
-import de.jplag.options.LanguageOption;
 import de.jplag.options.SimilarityMetric;
 import de.jplag.options.Verbosity;
 import de.jplag.reporting.reportobject.ReportObjectFactory;
@@ -48,7 +76,7 @@ public final class CLI {
 
     private static final String PROGRAM_NAME = "jplag";
     static final String CLUSTERING_GROUP_NAME = "Clustering";
-    static final String CLUSTERING_PREPROCESSING_GROUP_NAME = "Clustering - Preprocessing";
+    static final String ADVANCED_GROUP = "Advanced";
 
     private final ArgumentParser parser;
 
@@ -126,22 +154,15 @@ public final class CLI {
         addAllMultiValueArgument(NEW_DIRECTORY.getListFrom(namespace), submissionDirectories);
         addAllMultiValueArgument(OLD_DIRECTORY.getListFrom(namespace), oldSubmissionDirectories);
 
-        LanguageOption language = LanguageOption.fromDisplayName(LANGUAGE.getFrom(namespace));
-        JPlagOptions options = new JPlagOptions(submissionDirectories, oldSubmissionDirectories, language);
-        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);
@@ -171,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() {

+ 58 - 40
jplag.cli/src/main/java/de/jplag/CommandLineArgument.java

@@ -1,13 +1,14 @@
 package de.jplag;
 
+import static de.jplag.CLI.ADVANCED_GROUP;
 import static de.jplag.CLI.CLUSTERING_GROUP_NAME;
-import static de.jplag.CLI.CLUSTERING_PREPROCESSING_GROUP_NAME;
 import static de.jplag.options.JPlagOptions.DEFAULT_COMPARISON_MODE;
 import static de.jplag.options.JPlagOptions.DEFAULT_SHOWN_COMPARISONS;
 import static de.jplag.options.JPlagOptions.DEFAULT_SIMILARITY_THRESHOLD;
 import static net.sourceforge.argparse4j.impl.Arguments.append;
 import static net.sourceforge.argparse4j.impl.Arguments.storeTrue;
 
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
 import java.util.Optional;
@@ -17,12 +18,12 @@ import net.sourceforge.argparse4j.inf.Argument;
 import net.sourceforge.argparse4j.inf.ArgumentAction;
 import net.sourceforge.argparse4j.inf.ArgumentContainer;
 import net.sourceforge.argparse4j.inf.ArgumentParser;
+import net.sourceforge.argparse4j.inf.FeatureControl;
 import net.sourceforge.argparse4j.inf.Namespace;
 
 import de.jplag.clustering.ClusteringAlgorithm;
 import de.jplag.clustering.ClusteringOptions;
 import de.jplag.clustering.algorithm.InterClusterSimilarity;
-import de.jplag.options.LanguageOption;
 import de.jplag.options.SimilarityMetric;
 import de.jplag.strategy.ComparisonMode;
 
@@ -34,16 +35,20 @@ public enum CommandLineArgument {
     ROOT_DIRECTORY(new Builder("rootDir", String.class).nargs(NumberOfArgumentValues.ZERO_OR_MORE_VALUES)),
     NEW_DIRECTORY(new Builder("-new", String.class).nargs(NumberOfArgumentValues.ONE_OR_MORE_VALUES)),
     OLD_DIRECTORY(new Builder("-old", String.class).nargs(NumberOfArgumentValues.ONE_OR_MORE_VALUES)),
-    LANGUAGE(new Builder("-l", String.class).defaultsTo(LanguageOption.getDefault().getDisplayName()).choices(LanguageOption.getAllDisplayNames())),
+    LANGUAGE(
+            new Builder("-l", String.class).defaultsTo(de.jplag.java.Language.IDENTIFIER)
+                    .choices(LanguageLoader.getAllAvailableLanguageIdentifiers())),
     BASE_CODE("-bc", String.class),
-    VERBOSITY(new Builder("-v", String.class).defaultsTo("quiet").choices(List.of("quiet", "long"))), // TODO SH: Replace verbosity when integrating a
-                                                                                                      // real logging library
-    DEBUG("-d", Boolean.class),
-    SUBDIRECTORY("-S", String.class),
-    SUFFIXES("-p", String.class),
-    EXCLUDE_FILE("-x", String.class),
+    VERBOSITY(new Builder("-v", String.class).defaultsTo("quiet").choices("quiet", "long").argumentGroup(ADVANCED_GROUP)), // TODO SH: Replace
+                                                                                                                           // verbosity when
+                                                                                                                           // integrating a
+    // real logging library
+    DEBUG(new Builder("-d", Boolean.class).argumentGroup(ADVANCED_GROUP)),
+    SUBDIRECTORY(new Builder("-S", String.class).argumentGroup(ADVANCED_GROUP)),
+    SUFFIXES(new Builder("-p", String.class).argumentGroup(ADVANCED_GROUP)),
+    EXCLUDE_FILE(new Builder("-x", String.class).argumentGroup(ADVANCED_GROUP)),
     MIN_TOKEN_MATCH("-t", Integer.class),
-    SIMILARITY_THRESHOLD(new Builder("-m", Float.class).defaultsTo(DEFAULT_SIMILARITY_THRESHOLD)),
+    SIMILARITY_THRESHOLD(new Builder("-m", Float.class).defaultsTo(DEFAULT_SIMILARITY_THRESHOLD).argumentGroup(ADVANCED_GROUP)),
     SHOWN_COMPARISONS(new Builder("-n", Integer.class).defaultsTo(DEFAULT_SHOWN_COMPARISONS)),
     RESULT_FOLDER(new Builder("-r", String.class).defaultsTo("result")),
     COMPARISON_MODE(new Builder("-c", String.class).defaultsTo(DEFAULT_COMPARISON_MODE.getName()).choices(ComparisonMode.allNames())),
@@ -55,35 +60,36 @@ public enum CommandLineArgument {
             new Builder("--cluster-metric", SimilarityMetric.class).argumentGroup(CLUSTERING_GROUP_NAME)
                     .defaultsTo(ClusteringOptions.DEFAULTS.getSimilarityMetric())),
     CLUSTER_SPECTRAL_BANDWIDTH(
-            new Builder("--cluster-spectral-bandwidth", Float.class).argumentGroup(CLUSTERING_GROUP_NAME).metaVar("bandwidth")
-                    .defaultsTo(ClusteringOptions.DEFAULTS.getSpectralKernelBandwidth())),
+            new Builder("--cluster-spectral-bandwidth", Float.class).metaVar("bandwidth")
+                    .defaultsTo(ClusteringOptions.DEFAULTS.getSpectralKernelBandwidth()).hidden()),
     CLUSTER_SPECTRAL_NOISE(
-            new Builder("--cluster-spectral-noise", Float.class).argumentGroup(CLUSTERING_GROUP_NAME).metaVar("noise")
-                    .defaultsTo(ClusteringOptions.DEFAULTS.getSpectralGaussianProcessVariance())),
+            new Builder("--cluster-spectral-noise", Float.class).metaVar("noise")
+                    .defaultsTo(ClusteringOptions.DEFAULTS.getSpectralGaussianProcessVariance()).hidden()),
     CLUSTER_SPECTRAL_MIN_RUNS(
-            new Builder("--cluster-spectral-min-runs", Integer.class).argumentGroup(CLUSTERING_GROUP_NAME).metaVar("min")
-                    .defaultsTo(ClusteringOptions.DEFAULTS.getSpectralMinRuns())),
+            new Builder("--cluster-spectral-min-runs", Integer.class).metaVar("min").defaultsTo(ClusteringOptions.DEFAULTS.getSpectralMinRuns())
+                    .hidden()),
     CLUSTER_SPECTRAL_MAX_RUNS(
-            new Builder("--cluster-spectral-max-runs", Integer.class).argumentGroup(CLUSTERING_GROUP_NAME).metaVar("max")
-                    .defaultsTo(ClusteringOptions.DEFAULTS.getSpectralMaxRuns())),
+            new Builder("--cluster-spectral-max-runs", Integer.class).metaVar("max").defaultsTo(ClusteringOptions.DEFAULTS.getSpectralMaxRuns())
+                    .hidden()),
     CLUSTER_SPECTRAL_KMEANS_ITERATIONS(
-            new Builder("--cluster-spectral-kmeans-interations", Integer.class).argumentGroup(CLUSTERING_GROUP_NAME).metaVar("iterations")
-                    .defaultsTo(ClusteringOptions.DEFAULTS.getSpectralMaxKMeansIterationPerRun())),
+            new Builder("--cluster-spectral-kmeans-interations", Integer.class).metaVar("iterations")
+                    .defaultsTo(ClusteringOptions.DEFAULTS.getSpectralMaxKMeansIterationPerRun()).hidden()),
     CLUSTER_AGGLOMERATIVE_THRESHOLD(
-            new Builder("--cluster-agglomerative-threshold", Float.class).argumentGroup(CLUSTERING_GROUP_NAME).metaVar("threshold")
-                    .defaultsTo(ClusteringOptions.DEFAULTS.getAgglomerativeThreshold())),
+            new Builder("--cluster-agglomerative-threshold", Float.class).metaVar("threshold")
+                    .defaultsTo(ClusteringOptions.DEFAULTS.getAgglomerativeThreshold()).hidden()),
     CLUSTER_AGGLOMERATIVE_INTER_CLUSTER_SIMILARITY(
-            new Builder("--cluster-agglomerative-inter-cluster-similarity", InterClusterSimilarity.class).argumentGroup(CLUSTERING_GROUP_NAME)
-                    .defaultsTo(ClusteringOptions.DEFAULTS.getAgglomerativeInterClusterSimilarity())),
-    CLUSTER_PREPROCESSING_NONE(
-            new Builder("--cluster-pp-none", Boolean.class).mutuallyExclusiveGroup(CLUSTERING_PREPROCESSING_GROUP_NAME)
-                    .action(Arguments.storeTrue())),
-    CLUSTER_PREPROCESSING_CDF(
-            new Builder("--cluster-pp-cdf", Boolean.class).mutuallyExclusiveGroup(CLUSTERING_PREPROCESSING_GROUP_NAME).action(Arguments.storeTrue())),
-    CLUSTER_PREPROCESSING_PERCENTILE(
-            new Builder("--cluster-pp-percentile", Float.class).mutuallyExclusiveGroup(CLUSTERING_PREPROCESSING_GROUP_NAME).metaVar("percentile")),
-    CLUSTER_PREPROCESSING_THRESHOLD(
-            new Builder("--cluster-pp-threshold", Float.class).mutuallyExclusiveGroup(CLUSTERING_PREPROCESSING_GROUP_NAME).metaVar("threshold"));
+            new Builder("--cluster-agglomerative-inter-cluster-similarity", InterClusterSimilarity.class)
+                    .defaultsTo(ClusteringOptions.DEFAULTS.getAgglomerativeInterClusterSimilarity()).hidden()),
+    CLUSTER_PREPROCESSING_NONE(new Builder("--cluster-pp-none", Boolean.class).action(Arguments.storeTrue()).hidden()),
+    CLUSTER_PREPROCESSING_CDF(new Builder("--cluster-pp-cdf", Boolean.class).action(Arguments.storeTrue()).hidden()),
+    CLUSTER_PREPROCESSING_PERCENTILE(new Builder("--cluster-pp-percentile", Float.class).metaVar("percentile").hidden()),
+    CLUSTER_PREPROCESSING_THRESHOLD(new Builder("--cluster-pp-threshold", Float.class).metaVar("threshold").hidden());
+
+    /**
+     * The identifier of the default {@link Language}.
+     * @see Language#getIdentifier()
+     */
+    public static final String DEFAULT_LANGUAGE_IDENTIFIER = de.jplag.java.Language.IDENTIFIER;
 
     private final String flag;
     private final NumberOfArgumentValues numberOfValues;
@@ -95,12 +101,13 @@ public enum CommandLineArgument {
     private final Optional<ArgumentAction> action;
     private final Optional<String> metaVar;
     private final Class<?> type;
+    private final boolean hidden;
 
-    private CommandLineArgument(String flag, Class<?> type) {
+    CommandLineArgument(String flag, Class<?> type) {
         this(new Builder(flag, type));
     }
 
-    private CommandLineArgument(Builder builder) {
+    CommandLineArgument(Builder builder) {
         this.flag = builder.flag;
         this.type = builder.type;
         this.defaultValue = builder.defaultValue;
@@ -110,7 +117,9 @@ public enum CommandLineArgument {
         this.action = builder.action;
         this.metaVar = builder.metaVar;
         this.numberOfValues = builder.nargs.orElse(NumberOfArgumentValues.SINGLE_VALUE);
+        this.hidden = builder.hidden;
         this.description = retrieveDescriptionFromMessages();
+
     }
 
     /**
@@ -169,6 +178,9 @@ public enum CommandLineArgument {
         if (type == Boolean.class) {
             argument.action(storeTrue());
         }
+        if (hidden) {
+            argument.help(FeatureControl.SUPPRESS);
+        }
         if (!numberOfValues.toString().isEmpty()) {
             // For multi-value arguments keep all invocations.
             // This causes the argument value to change its type to 'List<List<String>>'.
@@ -201,6 +213,7 @@ public enum CommandLineArgument {
         private Optional<ArgumentAction> action = Optional.empty();
         private Optional<String> metaVar = Optional.empty();
         private Optional<NumberOfArgumentValues> nargs = Optional.empty();
+        private boolean hidden;
 
         public Builder(String flag, Class<?> type) {
             this.flag = flag;
@@ -212,6 +225,11 @@ public enum CommandLineArgument {
             return this;
         }
 
+        public Builder choices(String... choices) {
+            this.choices = Optional.of(Arrays.asList(choices));
+            return this;
+        }
+
         public Builder choices(Collection<String> choices) {
             this.choices = Optional.of(choices);
             return this;
@@ -222,11 +240,6 @@ public enum CommandLineArgument {
             return this;
         }
 
-        public Builder mutuallyExclusiveGroup(String mutuallyExclusiveGroup) {
-            this.mutuallyExclusiveGroup = Optional.of(mutuallyExclusiveGroup);
-            return this;
-        }
-
         public Builder action(ArgumentAction action) {
             this.action = Optional.of(action);
             return this;
@@ -241,5 +254,10 @@ public enum CommandLineArgument {
             this.nargs = Optional.of(nargs);
             return this;
         }
+
+        public Builder hidden() {
+            this.hidden = true;
+            return this;
+        }
     }
 }

+ 44 - 0
jplag.cli/src/main/java/de/jplag/logger/CollectedLoggerServiceProvider.java

@@ -0,0 +1,44 @@
+package de.jplag.logger;
+
+import org.kohsuke.MetaInfServices;
+import org.slf4j.ILoggerFactory;
+import org.slf4j.IMarkerFactory;
+import org.slf4j.helpers.BasicMarkerFactory;
+import org.slf4j.helpers.NOPMDCAdapter;
+import org.slf4j.spi.MDCAdapter;
+import org.slf4j.spi.SLF4JServiceProvider;
+
+@MetaInfServices(SLF4JServiceProvider.class)
+public class CollectedLoggerServiceProvider implements SLF4JServiceProvider {
+    public static final String REQUESTED_API_VERSION = "2.0.99";
+    private ILoggerFactory loggerFactory;
+    private IMarkerFactory markerFactory;
+    private MDCAdapter mdcAdapter;
+
+    @Override
+    public ILoggerFactory getLoggerFactory() {
+        return loggerFactory;
+    }
+
+    @Override
+    public IMarkerFactory getMarkerFactory() {
+        return markerFactory;
+    }
+
+    @Override
+    public MDCAdapter getMDCAdapter() {
+        return mdcAdapter;
+    }
+
+    @Override
+    public String getRequestedApiVersion() {
+        return REQUESTED_API_VERSION;
+    }
+
+    @Override
+    public void initialize() {
+        this.loggerFactory = new CollectedLoggerFactory();
+        this.markerFactory = new BasicMarkerFactory();
+        this.mdcAdapter = new NOPMDCAdapter();
+    }
+}

+ 0 - 54
jplag.cli/src/main/java/org/slf4j/impl/StaticLoggerBinder.java

@@ -1,54 +0,0 @@
-package org.slf4j.impl;
-
-import org.slf4j.ILoggerFactory;
-import org.slf4j.LoggerFactory;
-import org.slf4j.spi.LoggerFactoryBinder;
-
-import de.jplag.logger.CollectedLoggerFactory;
-
-/**
- * The binding of {@link LoggerFactory} class with an actual instance of {@link ILoggerFactory} is performed using
- * information returned by this class. The class has been adopted from SimpleLogger.
- * @author Dominik Fuchss
- */
-public class StaticLoggerBinder implements LoggerFactoryBinder {
-
-    /**
-     * The unique instance of this class.
-     */
-    private static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder();
-
-    /**
-     * Return the singleton of this class.
-     * @return the StaticLoggerBinder singleton
-     */
-    public static StaticLoggerBinder getSingleton() {
-        return SINGLETON;
-    }
-
-    /**
-     * Declare the version of the SLF4J API this implementation is compiled against. The value of this field is modified
-     * with each major release.
-     */
-    // to avoid constant folding by the compiler, this field must *not* be final
-    public static String REQUESTED_API_VERSION = "1.6.99"; // !final
-
-    private static final String loggerFactoryClassStr = CollectedLoggerFactory.class.getName();
-
-    /**
-     * The ILoggerFactory instance returned by the {@link #getLoggerFactory} method should always be the same object
-     */
-    private final ILoggerFactory loggerFactory;
-
-    private StaticLoggerBinder() {
-        loggerFactory = new CollectedLoggerFactory();
-    }
-
-    public ILoggerFactory getLoggerFactory() {
-        return loggerFactory;
-    }
-
-    public String getLoggerFactoryClassStr() {
-        return loggerFactoryClassStr;
-    }
-}

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

+ 0 - 35
jplag.cli/src/test/java/de/jplag/cli/LanguageOptionTest.java

@@ -1,35 +0,0 @@
-package de.jplag.cli;
-
-import static com.github.stefanbirkner.systemlambda.SystemLambda.catchSystemExit;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import org.junit.jupiter.api.Test;
-
-import de.jplag.CommandLineArgument;
-import de.jplag.options.LanguageOption;
-
-public class LanguageOptionTest extends CommandLineInterfaceTest {
-
-    @Test
-    public void testDefaultLanguage() {
-        buildOptionsFromCLI(CURRENT_DIRECTORY);
-        assertEquals(LanguageOption.getDefault(), options.getLanguageOption());
-    }
-
-    @Test
-    public void testInvalidLanguage() throws Exception {
-        String argument = buildArgument(CommandLineArgument.LANGUAGE, "Piet");
-        int statusCode = catchSystemExit(() -> buildOptionsFromCLI(argument, CURRENT_DIRECTORY));
-        assertEquals(1, statusCode);
-    }
-
-    @Test
-    public void testValidLanguages() {
-        for (LanguageOption language : LanguageOption.values()) {
-            String argument = buildArgument(CommandLineArgument.LANGUAGE, language.getDisplayName());
-            buildOptionsFromCLI(argument, CURRENT_DIRECTORY);
-            assertEquals(language, options.getLanguageOption());
-        }
-    }
-
-}

+ 43 - 0
jplag.cli/src/test/java/de/jplag/cli/LanguageTest.java

@@ -0,0 +1,43 @@
+package de.jplag.cli;
+
+import static com.github.stefanbirkner.systemlambda.SystemLambda.catchSystemExit;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import de.jplag.CommandLineArgument;
+import de.jplag.Language;
+import de.jplag.LanguageLoader;
+
+class LanguageTest extends CommandLineInterfaceTest {
+
+    @Test
+    void testDefaultLanguage() {
+        buildOptionsFromCLI(CURRENT_DIRECTORY);
+        assertEquals(CommandLineArgument.DEFAULT_LANGUAGE_IDENTIFIER, options.language().getIdentifier());
+    }
+
+    @Test
+    void testInvalidLanguage() throws Exception {
+        String argument = buildArgument(CommandLineArgument.LANGUAGE, "Piet");
+        int statusCode = catchSystemExit(() -> buildOptionsFromCLI(argument, CURRENT_DIRECTORY));
+        assertEquals(1, statusCode);
+    }
+
+    @Test
+    void testLoading() {
+        var languages = LanguageLoader.getAllAvailableLanguages();
+        Assertions.assertEquals(12, languages.size(), "Loaded Languages: " + languages.keySet());
+    }
+
+    @Test
+    void testValidLanguages() {
+        for (Language language : LanguageLoader.getAllAvailableLanguages().values()) {
+            String argument = buildArgument(CommandLineArgument.LANGUAGE, language.getIdentifier());
+            buildOptionsFromCLI(argument, CURRENT_DIRECTORY);
+            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);

+ 0 - 105
jplag.endToEndTesting/README.md

@@ -1,105 +0,0 @@
-# JPlag - End To End Testing
-With the help of the end-to-end module, changes to the detection of JPlag are to be tested.
-With the help of elaborated plagiarisms, which have been worked out from suggestions in the literature on the topic of "plagiarism detection and avoidance", a wide range of detectable change can be covered. The selected plagiarisms are the decisive factor here as to whether a change in recognition can be perceived. 
-
-### References
-These elaborations provide basic ideas on how a modification of the plagiarized source code can look like or be adapted.
-These code adaptations refer to a wide range of changes starting from
-adding/removing comments to architectural changes in the deliverables.
-
-The following elaborations were used to be able to create the plagiarisms with the largest coverage:
-- [Mossad: defeating software plagiarism detection](https://dl.acm.org/doi/abs/10.1145/3428206 "Mossad: defeating software plagiarism detection")
-- [Detecting Source Code Plagiarism on Introductory Programming Course Assignments Using a Bytecode Approach - Oscar Karnalim](https://ieeexplore.ieee.org/abstract/document/7910274 "Detecting Source Code Plagiarism on Introductory Programming Course Assignments Using a Bytecode Approach - Oscar Karnalim")
-- [Detecting Disguised Plagiarism - Hatem A. Mahmoud](https://arxiv.org/abs/1711.02149 "Detecting Disguised Plagiarism - Hatem A. Mahmoud")
-- [Instructor-centric source code plagiarism detection and plagiarism corpus](https://dl.acm.org/doi/abs/10.1145/2325296.2325328 "Instructor-centric source code plagiarism detection and plagiarism corpus")
-
-### Steps Towards Plagiarism
-The following changes were applied to sample tasks to create test cases:
-<ul type="1">
-	<li>Inserting comments or empty lines (normalization level)</li>
-	<li>Changing variable names or function names (normalization level)</li>
-	<li>Insertion of unnecessary or changed code lines (token generation)</li>
-	<li>Changing the program flow (token generation) (statments and functions must be independent from each other)</li>
-		<ul>
-			<li>Variable decleration at the beginning of the program</li>
-			<li>Combining declerations of variables</li>
-			<li>Reuse of the same variable for other functions</li>
-		</ul>
-	<li>Changing control structures</li>
-		<ul>
-			<li>for(...) to while(...)</li>
-			<li>if(...) to switch-case</li>
-		</ul>
-	<li>Modification of expressions</li>
-		<ul>
-			<li>(X < Y) to !(X >= Y) and ++x to x = x + 1</li>
-		</ul>
-	<li>Splitting and merging statements</li>
-		<ul>
-			<li>x = getSomeValue(); y = x- z; to y = (getSomeValue() - Z;</li>
-		</ul>
-	<li>Inserting unnecessary casts</li>
-</ul>
-
-More detailed information about the create as well as about the subject to the issue can be found in the issue [Develop an end-to-end testing strategy](https://github.com/jplag/JPlag/issues/193 "Develop an end-to-end testing strategy").
-
-**The changes listed above have been developed and evaluated for purely scientific purposes and are not intended to be used for plagiarism in the public or private domain.**
-
-Software is according to [§ 2 of the copyright law](https://www.gesetze-im-internet.de/urhg/__2.html "§ 2 of the copyright law") a protected work which may not be plagiarized. 
-
-### JPlag - End To End TestSuite Structure
-The construction of an end to end test is done with the help of the JPlag api. 
-
-``` java 
-[...]
-    /**
-     * This method creates the necessary results as well as models for a test run and summarizes them for a comparison.
-     * @param testClassNames Plagiarized classes names in the resource directorie which are needed for the test
-     * @param testIdentifier name of the testId to load and identify the stored results
-     * @throws IOException is thrown in case of problems with copying the plagiarism classes
-     * @throws ExitException in case the plagiarism detection with JPlag is preemptively terminated would be of the test.
-     */
-    private void runJPlagTestSuite(String[] testClassNames, int testIdentifier) throws IOException, ExitException {
-        String functionName = StackWalker.getInstance().walk(stream -> stream.skip(1).findFirst().get()).getMethodName();
-        TestCaseModel testCaseModel = jplagTestSuiteHelper.createNewTestCase(testClassNames, functionName);
-        JPlagResult jplagResult = new JPlag(testCaseModel.getJPlagOptionsFromCurrentModel()).run();
-
-        for (JPlagComparison jPlagComparison : jplagResult.getAllComparisons()) {
-            assertEquals(testCaseModel.getCurrentJsonModel().getResultModelById(testIdentifier).getResultSimilarity(), jPlagComparison.similarity(),
-                    "The JPlag results [similarity] do not match the stored values!");
-        }
-    }
-[...]
-```
-The created plagiarisms are copied with the JPlagTestSuiteHelper into temporary directories, which can then be checked with JPlag. 
-
-In order to be able to distinguish in which domain of the recognition changes have occurred, fine granular test cases are used. These are composed of the changes already mentioned above. The plagiarism is compared with the original delivery and thus it is possible to detect and test small sections of the recognition. 
-
-The values compared so far to detect a match are limited to the similarity of the JPlag scan. It is already planned to expand the comparative values in order to have more indications of a change. 
-
-The Current Discussion is already taking place and can be found at [End to end testing - "comparative values"](https://github.com/jplag/JPlag/issues/548 "End to end testing - \"comparative values\""). 
-
-The current JPlag scans will be compared with the stored ones.
-This was done by storing the data in a *.json file which is read at the beginning of each test run. 
-
-``` json  
-{
-[...]
-    "function_name": "normalizationLevelTest",
-    "test_results": [
-      {
-        "result_similarity": 100,
-        "test_identifier": 0
-      },
-      {
-        "result_similarity": 100,
-        "test_identifier": 1
-      },
-      {
-        "result_similarity": 100,
-        "test_identifier": 2
-      }
-    ]
-  }
-[...]
-```

+ 0 - 32
jplag.endToEndTesting/src/main/java/de/jplag/end_to_end_testing/constants/TestDirectoryConstants.java

@@ -1,32 +0,0 @@
-package de.jplag.end_to_end_testing.constants;
-
-import java.nio.file.Path;
-
-/**
- * All constant values that are needed in the test cases or helper classes.
- */
-public final class TestDirectoryConstants {
-
-    private TestDirectoryConstants() {
-        // private constructor to prevent instantiation
-    }
-
-    /**
-     * Name for the folder to copy the submissions to in order to test them with JPlag
-     */
-    public static final String TEMPORARY_DIRECTORY_NAME = "testing-directory-submission";
-    /**
-     * Create the complete path to the submission files. Here the temporary system path is extended with the
-     * "TEMPORARY_DIRECTORY_NAME", which is predefined in this class.
-     */
-    public static final String TEMPORARY_SUBMISSION_DIRECTORY_NAME = Path.of("target", TEMPORARY_DIRECTORY_NAME).toString();
-
-    /**
-     * Base path to the created plagiarism and the main file located in the project resources.
-     */
-    public static final Path BASE_PATH_TO_JAVA_RESOURCES_SORTALGO = Path.of("src", "test", "resources", "java", "sortAlgo");
-    /**
-     * Base path to the saved results of the previous tests in a *.json file
-     */
-    public static final Path BASE_PATH_TO_JAVA_RESULT_JSON = Path.of("src", "test", "resources", "results", "JavaResult.json");
-}

+ 0 - 120
jplag.endToEndTesting/src/main/java/de/jplag/end_to_end_testing/helper/JPlagTestSuiteHelper.java

@@ -1,120 +0,0 @@
-package de.jplag.end_to_end_testing.helper;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.StandardCopyOption;
-import java.util.Arrays;
-import java.util.List;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import de.jplag.end_to_end_testing.constants.TestDirectoryConstants;
-import de.jplag.end_to_end_testing.model.JsonModel;
-import de.jplag.end_to_end_testing.model.TestCaseModel;
-import de.jplag.options.LanguageOption;
-
-/**
- * This helper class deals with creating the test cases as well as copying and deleting for the end-to-end tests. The
- * required plagiarisms are copied from the resource folder to a temporary location, thus creating a folder structure
- * that can be tested by JPlag. Models are instantiated here and required information for the tests is loaded.
- */
-public class JPlagTestSuiteHelper {
-
-    private static final Logger logger = LoggerFactory.getLogger(JPlagTestSuiteHelper.class);
-
-    private String[] resourceNames;
-    private List<JsonModel> resultModel;
-    private LanguageOption languageOption;
-
-    /**
-     * Helper class for the endToEnd tests. In this class the necessary resources are loaded, prepared and copied for the
-     * tests based on the passed parameters. An instance of this class loads all necessary paths and properties for a test
-     * run with the specified language
-     * @param languageOption for loading language-specific resources
-     * @throws IOException is thrown for all problems that may occur while parsing the json file. This includes both reading
-     * and parsing problems.
-     */
-    public JPlagTestSuiteHelper(LanguageOption languageOption) throws IOException {
-        this.languageOption = languageOption;
-        this.resourceNames = new File(TestDirectoryConstants.BASE_PATH_TO_JAVA_RESOURCES_SORTALGO.toString()).list();
-
-        this.resultModel = JsonHelper.getResultModelFromPath();
-        logger.debug("temp path at [{}]", TestDirectoryConstants.TEMPORARY_SUBMISSION_DIRECTORY_NAME);
-    }
-
-    /**
-     * creates all necessary folder paths and objects for a test run. Also searches for the stored previous results of the
-     * test in order to compare them with the current results.
-     * @param classNames Array of class names with language specific extension to be prepared for a test.
-     * @return comparison results saved for the test
-     * @throws IOException Exception can be thrown in cases that involve reading, copying or locating files.
-     */
-    public TestCaseModel createNewTestCase(String[] classNames, String functionName) throws IOException {
-        createNewTestCaseDirectory(classNames);
-        JsonModel resultJsonModel = resultModel.stream().filter(jsonModel -> functionName.equals(jsonModel.getFunctionName())).findAny().orElse(null);
-        return new TestCaseModel(TestDirectoryConstants.TEMPORARY_SUBMISSION_DIRECTORY_NAME, resultJsonModel, languageOption);
-    }
-
-    /**
-     * The copied data should be deleted after instance closure
-     * @throws IOException if an I/O error occurs
-     */
-    public void clear() throws IOException {
-        logger.debug("Class instance was cleaned!");
-        deleteCopiedFiles(new File(TestDirectoryConstants.TEMPORARY_SUBMISSION_DIRECTORY_NAME));
-    }
-
-    /**
-     * Copies the passed filenames to a temporary path to use them in the tests
-     * @throws IOException Exception can be thrown in cases that involve reading, copying or locating files.
-     */
-    private void createNewTestCaseDirectory(String[] classNames) throws IOException {
-        // before copying files to the test path, check if all files are in the resource
-        // directory
-        for (String className : classNames) {
-            if (!Arrays.asList(resourceNames).contains(className)) {
-                throw new FileNotFoundException(String.format("The specified class could not be found! [%s]", className));
-            }
-        }
-        // Copy the resources data to the temporary path
-        for (int counter = 0; counter < classNames.length; counter++) {
-            Path originalPath = Path.of(TestDirectoryConstants.BASE_PATH_TO_JAVA_RESOURCES_SORTALGO.toString(), classNames[counter]);
-            Path copiePath = Path.of(TestDirectoryConstants.TEMPORARY_SUBMISSION_DIRECTORY_NAME,
-                    TestDirectoryConstants.TEMPORARY_DIRECTORY_NAME + (counter + 1), classNames[counter]);
-
-            File directory = new File(copiePath.toString());
-            if (!directory.exists()) {
-                directory.mkdirs();
-            }
-
-            Files.copy(originalPath, copiePath, StandardCopyOption.REPLACE_EXISTING);
-            logger.debug("Copy file from [{}] to [{}]", originalPath, copiePath);
-        }
-    }
-
-    /**
-     * Delete directory with including files
-     * @param file Path to a folder or file to be deleted. This happens recursively to the path
-     * @throws IOException if an I/O error occurs
-     */
-    private void deleteCopiedFiles(File folder) throws IOException {
-        File[] files = folder.listFiles();
-        if (files != null) { // some JVMs return null for empty dirs
-            for (File file : files) {
-                if (file.isDirectory()) {
-                    deleteCopiedFiles(file);
-                } else {
-                    Files.delete(file.toPath());
-                    logger.debug("Delete file in folder: [{}]", file);
-                }
-            }
-        }
-        Files.delete(folder.toPath());
-        logger.debug("Delete folder: [{}]", folder);
-    }
-
-}

+ 0 - 33
jplag.endToEndTesting/src/main/java/de/jplag/end_to_end_testing/helper/JsonHelper.java

@@ -1,33 +0,0 @@
-package de.jplag.end_to_end_testing.helper;
-
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.List;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import de.jplag.end_to_end_testing.constants.TestDirectoryConstants;
-import de.jplag.end_to_end_testing.model.JsonModel;
-
-/**
- * Helper class for serializing and creating all json dependent events.
- */
-public final class JsonHelper {
-
-    /**
-     * private constructor to prevent instantiation
-     */
-    private JsonHelper() {
-        // For Serialization
-    }
-
-    /**
-     * Parsing the old results in the json file as a list from ResultJsonModel.
-     * @return list of saved results for the test cases
-     * @throws IOException is thrown for all problems that may occur while parsing the json file. This includes both reading
-     * and parsing problems.
-     */
-    public static List<JsonModel> getResultModelFromPath() throws IOException {
-        return Arrays.asList(new ObjectMapper().readValue(TestDirectoryConstants.BASE_PATH_TO_JAVA_RESULT_JSON.toFile(), JsonModel[].class));
-    }
-}

+ 0 - 52
jplag.endToEndTesting/src/main/java/de/jplag/end_to_end_testing/model/JsonModel.java

@@ -1,52 +0,0 @@
-package de.jplag.end_to_end_testing.model;
-
-import java.util.Arrays;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-/**
- * The ResultJsonModel is the java object for the JavaResult.json file. The object contains all the necessary
- * information for the comparisons in the test cases between old and new results, which have been stored in the
- * JavaResult.json file.
- */
-public class JsonModel {
-    @JsonProperty("function_name")
-    private String functionName;
-    @JsonProperty("test_results")
-    private ResultModel[] results;
-
-    /**
-     * Constructor for the JsonModel. The model is the serialization of the Json file in the form of a Java object.
-     * @param functionName the function name for the associated test results. Used as identifier to search results for the
-     * test cases.
-     * @param results Collection of the results that are in the current json file for certain tests
-     */
-    public JsonModel(String functionName, ResultModel[] results) {
-        this.functionName = functionName;
-        this.results = results;
-    }
-
-    /**
-     * empty constructor in case the serialization contains an empty object to prevent throwing exceptions. this constructor
-     * was necessary for serialization with the Jackson parse extension
-     */
-    public JsonModel() {
-        // For Serialization
-    }
-
-    /**
-     * @return the name of the currently used function stored in json
-     */
-    public String getFunctionName() {
-        return functionName;
-    }
-
-    /**
-     * returns the results for the function that have been stored for the given id.
-     * @param identifier for the comparative values
-     * @return associated comparison values that have been assigned to the identifier
-     */
-    public ResultModel getResultModelById(Integer identifier) {
-        return Arrays.asList(results).stream().filter(resultModel -> identifier.equals(resultModel.getTestId())).findAny().orElse(null);
-    }
-}

+ 0 - 48
jplag.endToEndTesting/src/main/java/de/jplag/end_to_end_testing/model/ResultModel.java

@@ -1,48 +0,0 @@
-package de.jplag.end_to_end_testing.model;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-/**
- * The ResultModel contains all information and comparison values that are important for a test and that could be parsed
- * from the json file.
- */
-public class ResultModel {
-
-    @JsonProperty("result_similarity")
-    private float resultSimilarity;
-    @JsonProperty("test_identifier")
-    private int testIdentifier;
-
-    /**
-     * Constructor for the ResultModel. The model is the serialization of the Json file in the form of a Java object.
-     * @param resultSimilarity stored comparative value of the similarity
-     * @param testIdentifier specifies which associated test results are needed for a test case and are therefore associated
-     * with this Id.
-     */
-    public ResultModel(float resultSimilarity, int testIdentifier) {
-        this.resultSimilarity = resultSimilarity;
-        this.testIdentifier = testIdentifier;
-    }
-
-    /**
-     * empty constructor in case the serialization contains an empty object to prevent throwing exceptions. this constructor
-     * was necessary for serialization with the Jackson parse extension
-     */
-    public ResultModel() {
-        // For Serialization
-    }
-
-    /**
-     * @return Identifier assigned to the result
-     */
-    public int getTestId() {
-        return testIdentifier;
-    }
-
-    /**
-     * @return of the comparative similarity
-     */
-    public float getResultSimilarity() {
-        return resultSimilarity;
-    }
-}

+ 0 - 46
jplag.endToEndTesting/src/main/java/de/jplag/end_to_end_testing/model/TestCaseModel.java

@@ -1,46 +0,0 @@
-package de.jplag.end_to_end_testing.model;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-
-import de.jplag.options.JPlagOptions;
-import de.jplag.options.LanguageOption;
-
-/**
- * Median class for trading the results and the paths used in the tests. Also the JPlagOptions used for a test run are
- * created here.
- */
-public class TestCaseModel {
-
-    private JsonModel jsonModel;
-    private LanguageOption languageOption;
-    private String submissionFolderPath;
-
-    /**
-     * Constructor for creating the instance
-     * @param submissionFolderPath path to the plagiarism folder structure to be tested using Jplag
-     * @param jsonModel the parsed result model needed for the test to be able to match it later in the tests.
-     * @param languageOption the language selection in which the tests take place
-     */
-    public TestCaseModel(String submissionFolderPath, JsonModel jsonModel, LanguageOption languageOption) {
-        this.submissionFolderPath = submissionFolderPath;
-        this.jsonModel = jsonModel;
-        this.languageOption = languageOption;
-    }
-
-    /**
-     * returns the, for the test, current result model that has been persisted in the saved list.
-     * @return ResultJsonModel loaded for the test
-     */
-    public JsonModel getCurrentJsonModel() {
-        return jsonModel;
-    }
-
-    /**
-     * creates the JPlag options for the test run from the values passed in the constructor.
-     * @return current JPlag options for the created object
-     */
-    public JPlagOptions getJPlagOptionsFromCurrentModel() {
-        return new JPlagOptions(new ArrayList<>(Arrays.asList(submissionFolderPath)), new ArrayList<>(), languageOption);
-    }
-}

+ 0 - 114
jplag.endToEndTesting/src/test/java/de/jplag/end_to_end_testing/JavaEndToEndTest.java

@@ -1,114 +0,0 @@
-package de.jplag.end_to_end_testing;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import java.io.IOException;
-import java.util.stream.Stream;
-
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.TestInstance;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
-
-import de.jplag.JPlag;
-import de.jplag.JPlagComparison;
-import de.jplag.JPlagResult;
-import de.jplag.end_to_end_testing.constants.TestDirectoryConstants;
-import de.jplag.end_to_end_testing.helper.JPlagTestSuiteHelper;
-import de.jplag.end_to_end_testing.model.TestCaseModel;
-import de.jplag.exceptions.ExitException;
-import de.jplag.options.LanguageOption;
-
-/**
- * Main test class for end-to-end testing in the Java language. The test cases aim to detect changes in the detection of
- * plagiarism in the Java language and to be able to roughly categorize them. The plagiarism is compared with the
- * original class. The results are compared with the results from previous tests and changes are detected.
- */
-@TestInstance(TestInstance.Lifecycle.PER_CLASS)
-class JavaEndToEndTest {
-
-    private JPlagTestSuiteHelper jplagTestSuiteHelper;
-
-    @BeforeAll
-    public void setUp() throws IOException {
-        jplagTestSuiteHelper = new JPlagTestSuiteHelper(LanguageOption.JAVA);
-        assertTrue(TestDirectoryConstants.BASE_PATH_TO_JAVA_RESOURCES_SORTALGO.toFile().exists(), "Could not find base directory!");
-        assertTrue(TestDirectoryConstants.BASE_PATH_TO_JAVA_RESULT_JSON.toFile().isFile(), "Could not find java result json!");
-    }
-
-    @AfterEach
-    public void teardown() throws IOException {
-        // after close the created directories are deleted
-        jplagTestSuiteHelper.clear();
-    }
-
-    /**
-     * Test cases created for the normalization level
-     * @return the classes to be tested with the corresponding identifier
-     */
-    private static Stream<Arguments> normalizationLevelTestArguments() {
-        return Stream.of(Arguments.of((Object) new String[] {"SortAlgo.java", "SortAlgo1.java"}, 0),
-                Arguments.of((Object) new String[] {"SortAlgo.java", "SortAlgo2.java"}, 1),
-                Arguments.of((Object) new String[] {"SortAlgo1.java", "SortAlgo2.java"}, 2));
-    }
-
-    /**
-     * Test cases for the token generation level
-     * @return the classes to be tested with the corresponding identifier
-     */
-    private static Stream<Arguments> tokenGenerationLevelTestArguments() {
-        return Stream.of(Arguments.of((Object) new String[] {"SortAlgo.java", "SortAlgo3.java"}, 0),
-                Arguments.of((Object) new String[] {"SortAlgo.java", "SortAlgo4.java"}, 1),
-                Arguments.of((Object) new String[] {"SortAlgo.java", "SortAlgo4d1.java"}, 2));
-    }
-
-    /**
-     * Inserting comments or empty lines (normalization level) -> id = 0 Changing variable names or function names
-     * (normalization level) -> id = 1 Inserting comments or empty lines (normalization level) -> id = 2
-     * @param testClassNames Plagiarized classes names in the resource directorie which are needed for the test
-     * @param testIdentifier name of the testId to load and identify the stored results
-     * @throws IOException is thrown in case of problems with copying the plagiarism classes
-     * @throws ExitException in case the plagiarism detection with JPlag is preemptively terminated would be of the test.
-     */
-    @ParameterizedTest
-    @MethodSource("normalizationLevelTestArguments")
-    void normalizationLevelTest(String[] testClassNames, int testIdentifier) throws IOException, ExitException {
-        runJPlagTestSuite(testClassNames, testIdentifier);
-    }
-
-    /**
-     * Insertion of unnecessary or changed code lines (token generation) -> id = 0 Changing the program flow (token
-     * generation) (statements and functions must be independent from each other) -> id = 1 Variable declaration at the
-     * beginning of the program (Detecting Source Code Plagiarism [...]) -> id = 2
-     * @param testClassNames Plagiarized classes names in the resource directorie which are needed for the test
-     * @param testIdentifier name of the testId to load and identify the stored results
-     * @throws IOException is thrown in case of problems with copying the plagiarism classes
-     * @throws ExitException in case the plagiarism detection with JPlag is preemptively terminated would be of the test.
-     */
-    @ParameterizedTest
-    @MethodSource("tokenGenerationLevelTestArguments")
-    void tokenGenerationLevelTest(String[] testClassNames, int testIdentifier) throws IOException, ExitException {
-        runJPlagTestSuite(testClassNames, testIdentifier);
-    }
-
-    /**
-     * This method creates the necessary results as well as models for a test run and summarizes them for a comparison.
-     * @param testClassNames Plagiarized classes names in the resource directorie which are needed for the test
-     * @param testIdentifier name of the testId to load and identify the stored results
-     * @throws IOException is thrown in case of problems with copying the plagiarism classes
-     * @throws ExitException in case the plagiarism detection with JPlag is preemptively terminated would be of the test.
-     */
-    private void runJPlagTestSuite(String[] testClassNames, int testIdentifier) throws IOException, ExitException {
-        String functionName = StackWalker.getInstance().walk(stream -> stream.skip(1).findFirst().get()).getMethodName();
-        TestCaseModel testCaseModel = jplagTestSuiteHelper.createNewTestCase(testClassNames, functionName);
-        JPlagResult jplagResult = new JPlag(testCaseModel.getJPlagOptionsFromCurrentModel()).run();
-
-        for (JPlagComparison jPlagComparison : jplagResult.getAllComparisons()) {
-            assertEquals(testCaseModel.getCurrentJsonModel().getResultModelById(testIdentifier).getResultSimilarity(), jPlagComparison.similarity(),
-                    "The JPlag results [similarity] do not match the stored values!");
-        }
-    }
-}

+ 0 - 36
jplag.endToEndTesting/src/test/resources/results/JavaResult.json

@@ -1,36 +0,0 @@
-[
-  {
-    "function_name": "normalizationLevelTest",
-    "test_results": [
-      {
-        "result_similarity": 100,
-        "test_identifier": 0
-      },
-      {
-        "result_similarity": 100,
-        "test_identifier": 1
-      },
-      {
-        "result_similarity": 100,
-        "test_identifier": 2
-      }
-    ]
-  },
-  {
-    "function_name": "tokenGenerationLevelTest",
-    "test_results": [
-      {
-        "result_similarity": 64.0,
-        "test_identifier": 0
-      },
-      {
-        "result_similarity": 73.04348,
-        "test_identifier": 1
-      },
-      {
-        "result_similarity": 73.94958,
-        "test_identifier": 2
-      }
-    ]
-  }
-]

+ 174 - 0
jplag.endtoend-testing/README.md

@@ -0,0 +1,174 @@
+# JPlag - End To End Testing
+With the help of the end-to-end module, changes to the detection of JPlag are to be tested.
+With the help of elaborated plagiarisms, which have been worked out from suggestions in the literature on the topic of "plagiarism detection and avoidance", a wide range of detectable change can be covered. The selected plagiarisms are the decisive factor here as to whether a change in recognition can be perceived. 
+
+## References
+These elaborations provide basic ideas on how a modification of the plagiarized source code can look like or be adapted.
+These code adaptations refer to a wide range of changes starting from
+adding/removing comments to architectural changes in the deliverables.
+
+The following elaborations were used to be able to create the plagiarisms with the largest coverage:
+- [Detecting Source Code Plagiarism on Introductory Programming Course Assignments Using a Bytecode Approach - Oscar Karnalim](https://ieeexplore.ieee.org/abstract/document/7910274 "Detecting Source Code Plagiarism on Introductory Programming Course Assignments Using a Bytecode Approach - Oscar Karnalim")
+- [Detecting Disguised Plagiarism - Hatem A. Mahmoud](https://arxiv.org/abs/1711.02149 "Detecting Disguised Plagiarism - Hatem A. Mahmoud")
+- [Instructor-centric source code plagiarism detection and plagiarism corpus](https://dl.acm.org/doi/abs/10.1145/2325296.2325328 "Instructor-centric source code plagiarism detection and plagiarism corpus")
+
+## Steps Towards Plagiarism
+The following changes were applied to sample tasks to create test cases:
+<ul type="1">
+	<li>Inserting comments or empty lines (normalization level)</li>
+	<li>Changing variable names or function names (normalization level)</li>
+	<li>Insertion of unnecessary or changed code lines (token generation)</li>
+	<li>Changing the program flow (token generation) (statments and functions must be independent from each other)</li>
+		<ul>
+			<li>Variable decleration at the beginning of the program</li>
+			<li>Combining declerations of variables</li>
+			<li>Reuse of the same variable for other functions</li>
+		</ul>
+	<li>Changing control structures</li>
+		<ul>
+			<li>for(...) to while(...)</li>
+			<li>if(...) to switch-case</li>
+		</ul>
+	<li>Modification of expressions</li>
+		<ul>
+			<li>(X < Y) to !(X >= Y) and ++x to x = x + 1</li>
+		</ul>
+	<li>Splitting and merging statements</li>
+		<ul>
+			<li>x = getSomeValue(); y = x- z; to y = (getSomeValue() - Z;</li>
+		</ul>
+	<li>Inserting unnecessary casts</li>
+</ul>
+
+More detailed information about the create as well as about the subject to the issue can be found in the issue [Develop an end-to-end testing strategy](https://github.com/jplag/JPlag/issues/193 "Develop an end-to-end testing strategy").
+
+**The changes listed above have been developed and evaluated for purely scientific purposes and are not intended to be used for plagiarism in the public or private domain.**
+
+Software is according to [§ 2 of the copyright law](https://www.gesetze-im-internet.de/urhg/__2.html "§ 2 of the copyright law") a protected work which may not be plagiarized. 
+
+## JPlag - End To End TestSuite Structure
+The construction of an end to end test is done with the help of the JPlag api. 
+The tests are generated dynamically according to the existing test data and allow the creation of endToEnd tests for all supported languages of JPlag without having to make any changes to the code.
+The helper loads the existing test data from the designated directory and creates dynamic tests for the individual directories. It is therefore possible to create different test classes for the different languages.
+- JPlagTestSuiteHelper:
+
+``` java 
+public static Map<LanguageOption, Map<String, Path>> getAllLanguageResources()
+```
+
+The list of languages created in this way and their associated data are dynamically generated into test cases in the Test Suite.
+
+``` java
+ Collection<DynamicTest> dynamicOverAllTest()
+```
+
+In order to be able to distinguish in which domain of the recognition changes have occurred, fine granular test cases are used. These are composed of the changes already mentioned above. The plagiarism is compared with the original delivery and thus it is possible to detect and test small sections of the recognition. 
+
+The comparative values were discussed and tested. The following results of the JPlag scan are used for the comparison:
+1. minimal similarity as `float`
+2. maximum similarity as `float`
+3. matched token numbe as `int`
+
+The comparative values were disscussed and elaborated in the issue [End to end testing - "comparative values"](https://github.com/jplag/JPlag/issues/548 "End to end testing - \"comparative values\""). 
+
+Additionally it is possible to create several options for the test data. More information about the test options can be found at [JPlag - option variants for the endToEnd tests #590](https://github.com/jplag/JPlag/issues/590 "JPlag - option variants for the endToEnd tests #590"). Currently, various settings are supported by the `minimumTokenMatch`.  This can be extended as desired in the record class `Options`.
+
+The current JPlag scans will be compared with the stored ones.
+This was done by storing the data in a *.json file which is read at the beginning of each test run.
+
+``` json  
+[...]
+{
+  "options" : {
+    "minimum_token_match" : 1
+  },
+  "tests" : {
+    "SortAlgo-SortAlgo5" : {
+      "minimal_similarity" : 82.14286,
+      "maximum_similarity" : 82.14286,
+      "matched_token_number" : 46
+    },
+    "SortAlgo-SortAlgo6" : {
+      "minimal_similarity" : 83.58209,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo-SortAlgo7" : {
+      "minimal_similarity" : 96.42857,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 54
+    },
+[...]
+```
+--- 
+
+## Create New Language End To End Tests
+
+This section explains how to create new end to end tests in the existing test suite. 
+### Creating The Plagiarism
+Before you add a new language to the end to end tests i would like to point out that the quality of the tests depends dreadfully on the plagiarism techniques you choose wicht were explaint in sechtion [Steps Towards Plagiarism](#steps-towards-plagiarism).
+If you need more information about the creation of plans for this purpose, you can also read the elaborations that can be found under [References](#references).
+The more and varied changes you apply, the more accurate the end-to-end tests for the language will be.
+
+In the following an example is shown which is in the JavaEndToEnd tests and is used.
+
+**Changing control structures for(…) to while(…):**
+
+``` java
+//base class
+public class SortAlgo {
+//[...]
+public void BubbleSortWithoutRecursion(Integer arr[]) {
+		for(int i = arr.length; i > 1 ; i--) {
+			for(int innerCounter = 0; innerCounter < arr.length-1; innerCounter++)
+			{
+				if (arr[innerCounter] > arr[innerCounter + 1]) {
+					swap(arr, innerCounter, (innerCounter + 1));
+				}
+			}
+		}
+	}
+//[...]
+}
+```
+
+``` java
+//created plagiarism
+public class SortAlgo5{
+//[...]
+public void BubbleSortWithoutRecursion(Integer arr[]) {
+		int i = arr.length;
+		while(i > 1)
+		{
+			int innerCounter = 0;
+			while(innerCounter < arr.length -1)
+			{
+				if (arr[innerCounter] > arr[innerCounter + 1]) {
+					swap(arr, innerCounter, (innerCounter + 1));
+				}
+				innerCounter++;
+			}
+			i--;
+		}
+	}
+//[...]
+}
+```
+### Copying Plagiarism To The Resources
+
+The plagiarisms created in [Creating The Plagiarism](#creating-the-plagiarism) must now be copied to the corresponding resources folder. It is important not to mix the languages of the plagiarisms or to copy the data into bottle resource paths.
+
+- At the path `JPlag\jplag.endToEndTesting\src\test\resources\languageTestFiles` a new folder for the language should be created if it does not already exist. For example `[...]\resources\languageTestFiles\JAVA`. If you have plagiarized several different code samples, you can also create additional subfolders under the newly created folder for example `[...]\resources\languageTestFiles\JAVA\sortAlgo`.
+
+It is important to note that the resource folder name must be exactly the same as the language identifier name in JPlag/Language. Otherwise the language option cannot be parsed correctly to the enum-type.
+ - c++ with "cpp"
+ - c# with "csharp"
+ - GO with "go"
+ - Java with "java"
+ - Kotlin with "kotlin"
+ - Python3 with "python3"
+ - R with "rlang"
+ - Rust with "rust"
+ - Scala with "scala"
+
+Once the tests have been run for the first time, the information for the tests is stored in the folder `..\target\testing-directory-submission\LANGUAGE`.  This data can be copied to the path `[...]\resources\results\LANGUAGE`. Each subdirectory gets its own result json file as `[...]\resources\results\JAVA\sortAlgo.json`. Once the test data has been copied, the endToEnd tests can be successfully tested. As soon as a change in the detection takes place, the results will differ from the stored results and the tests will fail if the results have changed.

+ 6 - 1
jplag.endToEndTesting/pom.xml → jplag.endtoend-testing/pom.xml

@@ -6,13 +6,18 @@
         <artifactId>aggregator</artifactId>
         <version>${revision}</version>
     </parent>
-    <artifactId>endToEndTesting</artifactId>
+    <artifactId>endtoend-testing</artifactId>
 
     <dependencies>
         <dependency>
             <groupId>de.jplag</groupId>
             <artifactId>jplag</artifactId>
         </dependency>
+        <dependency>
+            <groupId>de.jplag</groupId>
+            <artifactId>java</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>de.jplag</groupId>
             <artifactId>frontend-testutils</artifactId>

+ 35 - 0
jplag.endtoend-testing/src/main/java/de/jplag/endtoend/constants/TestDirectoryConstants.java

@@ -0,0 +1,35 @@
+package de.jplag.endtoend.constants;
+
+import java.nio.file.Path;
+
+/**
+ * All constant values that are needed in the test cases or helper classes.
+ */
+public final class TestDirectoryConstants {
+
+    private TestDirectoryConstants() {
+        // private constructor to prevent instantiation
+    }
+
+    /**
+     * Create the complete path to the temporary result files. Here the temporary system path is extended with the
+     * "RESULT_DIRECTORY_NAME", which is predefined in this class.
+     */
+    public static final Path TEMPORARY_RESULT_DIRECTORY_NAME = Path.of("target", "testing-directory-temporary-result");
+
+    /**
+     * Base path to the saved results
+     */
+    public static final Path BASE_PATH_TO_RESULT_JSON = Path.of("src", "test", "resources", "results");
+
+    /**
+     * Base path to the endToEnd testing resources
+     */
+    public static final Path BASE_PATH_TO_LANGUAGE_RESOURCES = Path.of("src", "test", "resources", "languageTestFiles");
+
+    /**
+     * Create the complete path to the submission files. Here the temporary system path is extended with the
+     * "SUBMISSION_DIRECTORY_NAME", which is predefined in this class.
+     */
+    public static final Path TEMPORARY_SUBMISSION_DIRECTORY_NAME = Path.of("target", "testing-directory-submission");
+}

+ 143 - 0
jplag.endtoend-testing/src/main/java/de/jplag/endtoend/helper/FileHelper.java

@@ -0,0 +1,143 @@
+package de.jplag.endtoend.helper;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import de.jplag.endtoend.constants.TestDirectoryConstants;
+
+/**
+ * Helper class to perform all necessary operations or functions on files or folders.
+ */
+public class FileHelper {
+
+    private FileHelper() {
+        // private constructor to prevent instantiation
+    }
+
+    /**
+     * Merges all contained filenames together without extension
+     * @param files whose names are to be merged
+     * @return merged filenames
+     */
+    public static String getEnclosedFileNamesFromCollection(Collection<File> files) {
+
+        return files.stream().map(File::getName).map(fileName -> fileName.substring(0, fileName.lastIndexOf('.'))).collect(Collectors.joining());
+    }
+
+    /**
+     * Load all possible languages in resource path
+     * @param directoryNames folder names for which the language options should be listed.
+     * @return list of all LanguageOptions included in the resource path
+     */
+    public static List<String> getLanguageOptionsFromPath(String[] directoryNames) {
+        return Arrays.stream(directoryNames).map(language -> language).filter(Objects::nonNull).toList();
+    }
+
+    /**
+     * @param directorieRoot path from which all folders should be loaded
+     * @return all folders found in the specified path
+     */
+    public static String[] getAllDirectoriesInPath(Path directorieRoot) {
+        return directorieRoot.toFile().list((dir, name) -> new File(dir, name).isDirectory());
+    }
+
+    /**
+     * Copies the passed filenames to a temporary path to use them in the tests
+     * @param classNames for which the test case is to be created
+     * @return paths created to the test submissions
+     * @throws IOException Exception can be thrown in cases that involve reading, copying or locating files.
+     */
+    public static String[] createNewTestCaseDirectory(String[] classNames) throws IOException {
+        // Copy the resources data to the temporary path
+        String[] returnSubmissionPath = new String[classNames.length];
+        for (int counter = 0; counter < classNames.length; counter++) {
+            Path originalPath = Path.of(classNames[counter]);
+            returnSubmissionPath[counter] = Path
+                    .of(TestDirectoryConstants.TEMPORARY_SUBMISSION_DIRECTORY_NAME.toString(), "submission" + (counter + 1)).toAbsolutePath()
+                    .toString();
+            Path copyPath = Path.of(TestDirectoryConstants.TEMPORARY_SUBMISSION_DIRECTORY_NAME.toString(), "submission" + (counter + 1),
+                    originalPath.getFileName().toString());
+
+            File directory = new File(copyPath.toString());
+            if (!directory.exists()) {
+                directory.mkdirs();
+            }
+            Files.copy(originalPath, copyPath, StandardCopyOption.REPLACE_EXISTING);
+        }
+        return returnSubmissionPath;
+    }
+
+    /**
+     * Delete directory with including files
+     * @param folder Path to a folder or file to be deleted. This happens recursively to the path
+     * @throws IOException if an I/O error occurs
+     */
+    public static void deleteCopiedFiles(File folder) throws IOException {
+        if (!folder.exists()) {
+            return;
+        }
+        File[] files = folder.listFiles();
+        if (files == null) { // some JVMs return null for empty dirs
+            return;
+        }
+        for (File file : files) {
+            if (file.isDirectory()) {
+                deleteCopiedFiles(file);
+            } else {
+                Files.delete(file.toPath());
+            }
+        }
+        Files.delete(folder.toPath());
+    }
+
+    /**
+     * Creates directory if it dose not exist
+     * @param directory to be created
+     * @throws IOException if the directory could not be created
+     */
+    public static void createDirectoryIfItDoesNotExist(File directory) throws IOException {
+        if (!directory.exists() && !directory.mkdirs()) {
+            throw new IOException(createNewIOExceptionStringForFileOrFOlderCreation(directory));
+        }
+    }
+
+    /**
+     * Creates file if it dose not exist
+     * @param file to be created
+     * @throws IOException if the file could not be created
+     */
+    public static void createFileIfItDoesNotExist(File file) throws IOException {
+        if (!file.exists() && !file.createNewFile()) {
+            throw new IOException(createNewIOExceptionStringForFileOrFOlderCreation(file));
+        }
+    }
+
+    /**
+     * @param resourcenPaths list of paths that lead to test resources
+     * @return all filenames contained in the paths
+     */
+    public static String[] loadAllTestFileNames(Path resourcenPaths) {
+        var files = resourcenPaths.toFile().listFiles();
+        String[] fileNames = new String[files.length];
+        for (int i = 0; i < files.length; i++) {
+            fileNames[i] = files[i].getName();
+        }
+        return fileNames;
+    }
+
+    /**
+     * @param file for which the exception text is to be created
+     * @return exception text for the specified file
+     */
+    private static String createNewIOExceptionStringForFileOrFOlderCreation(File file) {
+        return "The file/folder at the location [" + file.toString() + "] could not be created!";
+    }
+}

+ 71 - 0
jplag.endtoend-testing/src/main/java/de/jplag/endtoend/helper/JsonHelper.java

@@ -0,0 +1,71 @@
+package de.jplag.endtoend.helper;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectWriter;
+
+import de.jplag.endtoend.constants.TestDirectoryConstants;
+import de.jplag.endtoend.model.ResultDescription;
+
+/**
+ * Helper class for serialization and deserialization of the used json format into the correct record classes. The
+ * serialization/deserialization is enabled using the Jackson library. The record classes used can be found at
+ * {@link de.jplag.endtoend.model}.
+ */
+public class JsonHelper {
+    /**
+     * private constructor to prevent instantiation
+     */
+    private JsonHelper() {
+        // For Serialization
+    }
+
+    /**
+     * @param directoryName name to the result path
+     * @param languageIdentifier for which the results are to be loaded
+     * @return ResultDescription as serialized object
+     * @throws IOException is thrown for all problems that may occur while parsing the json file. This includes both reading
+     */
+    public static List<ResultDescription> getJsonModelListFromPath(String directoryName, String languageIdentifier) throws IOException {
+
+        Path jsonPath = Path.of(TestDirectoryConstants.BASE_PATH_TO_RESULT_JSON.toString(), languageIdentifier, directoryName + ".json");
+
+        if (jsonPath.toFile().exists() && jsonPath.toFile().length() > 0) {
+
+            return Arrays.asList(new ObjectMapper().readValue(jsonPath.toFile(), ResultDescription[].class));
+        } else {
+            return Collections.<ResultDescription>emptyList();
+        }
+    }
+
+    /**
+     * Saves the passed object as a json file to the given path
+     * @param resultDescriptionist list of elements to be saved
+     * @param directoryName path to the temporary storage location
+     * @param languageIdentifier for which the results should be stored
+     * @throws IOException Signals that an I/O exception of some sort has occurred. Thisclass is the general class of
+     * exceptions produced by failed orinterrupted I/O operations.
+     */
+    public static void writeJsonModelsToJsonFile(List<ResultDescription> resultDescriptionist, String directoryName, String languageIdentifier)
+            throws IOException {
+        // create an instance of DefaultPrettyPrinter
+        // new DefaultPrettyPrinter()
+        ObjectWriter writer = new ObjectMapper().writer().withDefaultPrettyPrinter();
+
+        Path temporaryDirectory = Path.of(TestDirectoryConstants.TEMPORARY_SUBMISSION_DIRECTORY_NAME.toString(), languageIdentifier,
+                directoryName + ".json");
+
+        FileHelper.createDirectoryIfItDoesNotExist(temporaryDirectory.getParent().toFile());
+        FileHelper.createFileIfItDoesNotExist(temporaryDirectory.toFile());
+
+        // convert book object to JSON file
+
+        writer.writeValue(temporaryDirectory.toFile(), resultDescriptionist.toArray());
+
+    }
+}

+ 89 - 0
jplag.endtoend-testing/src/main/java/de/jplag/endtoend/helper/TestSuiteHelper.java

@@ -0,0 +1,89 @@
+package de.jplag.endtoend.helper;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import de.jplag.JPlagComparison;
+import de.jplag.Submission;
+import de.jplag.endtoend.constants.TestDirectoryConstants;
+
+/**
+ * Helper class to perform all necessary additional functions for the endToEnd tests.
+ */
+public class TestSuiteHelper {
+
+    /**
+     * private constructor to prevent instantiation
+     */
+    private TestSuiteHelper() {
+        // For Serialization
+    }
+
+    /**
+     * Loads all existing test data into a test structure. the complex structure consists of the LanguageOption and their
+     * data to be tested. These are divided into folders. for more information please read the README file
+     * @return mapped LanguageOption to the data under test
+     */
+    public static Map<String, Map<String, Path>> getAllLanguageResources() {
+        String[] languageDirectoryNames = FileHelper.getAllDirectoriesInPath(TestDirectoryConstants.BASE_PATH_TO_LANGUAGE_RESOURCES);
+        List<String> languageInPathList = FileHelper.getLanguageOptionsFromPath(languageDirectoryNames);
+
+        Map<String, Map<String, Path>> returnMap = new HashMap<>();
+
+        for (String languageIdentifier : languageInPathList) {
+            var tempMap = new HashMap<String, Path>();
+            var allDirectoriesInPath = FileHelper
+                    .getAllDirectoriesInPath(Path.of(TestDirectoryConstants.BASE_PATH_TO_LANGUAGE_RESOURCES.toString(), languageIdentifier));
+            Arrays.asList(allDirectoriesInPath).forEach(directory -> tempMap.put(Path.of(directory).toFile().getName(),
+                    Path.of(TestDirectoryConstants.BASE_PATH_TO_LANGUAGE_RESOURCES.toString(), languageIdentifier, directory)));
+            returnMap.put(languageIdentifier, tempMap);
+        }
+        return returnMap;
+    }
+
+    /**
+     * Creates a unique identifier from the submissions in the JPlagComparison object which is used to find the results in
+     * the json files.
+     * @param jPlagComparison object from which the hash should be generated
+     * @return unique identifier for test case recognition
+     */
+    public static String getTestIdentifier(JPlagComparison jPlagComparison) {
+
+        return List.of(jPlagComparison.getFirstSubmission(), jPlagComparison.getSecondSubmission()).stream().map(Submission::getFiles)
+                .map(FileHelper::getEnclosedFileNamesFromCollection).sorted().collect(Collectors.joining("-"));
+
+    }
+
+    /**
+     * Creates the permutation of all data contained in the passed parameters and adds it to the given path.
+     * @param fileNames for which the permutations are needed
+     * @param path to which the permutations are to be copied
+     * @return all permutations of the specified files to the path specified
+     */
+    public static List<String[]> getTestCases(String[] fileNames, Path path) {
+        ArrayList<String[]> testCases = new ArrayList<>();
+
+        for (int outerCounter = 0; outerCounter < fileNames.length; outerCounter++) {
+            for (int innerCounter = outerCounter + 1; innerCounter < fileNames.length; innerCounter++) {
+                testCases.add(new String[] {Path.of(path.toAbsolutePath().toString(), fileNames[outerCounter]).toString(),
+                        Path.of(path.toAbsolutePath().toString(), fileNames[innerCounter]).toString()});
+            }
+        }
+        return testCases;
+    }
+
+    /**
+     * The copied data should be deleted after instance closure
+     * @throws IOException if an I/O error occurs
+     */
+    public static void clear() throws IOException {
+        FileHelper.deleteCopiedFiles(new File(TestDirectoryConstants.TEMPORARY_SUBMISSION_DIRECTORY_NAME.toString()));
+    }
+}

+ 12 - 0
jplag.endtoend-testing/src/main/java/de/jplag/endtoend/model/ExpectedResult.java

@@ -0,0 +1,12 @@
+package de.jplag.endtoend.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * contains the current comparative values for the endToEnd tests. The comparative values were determined by discussion
+ * which can be found at https://github.com/jplag/JPlag/issues/548 Here this object is used for serialization and
+ * deserialization of the information from json to object or object to json.
+ */
+public record ExpectedResult(@JsonProperty("minimal_similarity") float resultSimilarityMinimum,
+        @JsonProperty("maximum_similarity") float resultSimilarityMaximum, @JsonProperty("matched_token_number") int resultMatchedTokenNumber) {
+}

+ 10 - 0
jplag.endtoend-testing/src/main/java/de/jplag/endtoend/model/Options.java

@@ -0,0 +1,10 @@
+package de.jplag.endtoend.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The object contains required options for the endToEndt tests which are important for the test suite. The options were
+ * determined by discussion which can be found at https://github.com/jplag/JPlag/issues/590.
+ */
+public record Options(@JsonProperty("minimum_token_match") Integer minimumTokenMatch) {
+}

+ 34 - 0
jplag.endtoend-testing/src/main/java/de/jplag/endtoend/model/ResultDescription.java

@@ -0,0 +1,34 @@
+package de.jplag.endtoend.model;
+
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Object that maps the results of the end top end tests using the identifierToResultMap. this creates a map of test
+ * data and its results for each possible option specified. this is important both for serializing the data into json
+ * format and for deserialization.
+ */
+public record ResultDescription(@JsonIgnore String languageIdentifier, @JsonProperty("options") Options options,
+        @JsonProperty("tests") Map<String, ExpectedResult> identifierToResultMap) {
+
+    /**
+     * @param identifier for which the stored results are needed
+     * @return stored results as ExpectedResult object for the passed id
+     */
+    @JsonIgnore
+    public ExpectedResult getExpectedResultByIdentifier(String identifier) {
+        return identifierToResultMap.get(identifier);
+    }
+
+    /**
+     * Adds expected results to the existing list of the object. These results are stored in a map with the given
+     * identifier.
+     * @param identifier under which the results should be stored
+     * @param expectedResult expected results belonging to the identifier
+     */
+    public void putIdentifierToResultMap(String identifier, ExpectedResult expectedResult) {
+        identifierToResultMap.put(identifier, expectedResult);
+    }
+}

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

@@ -0,0 +1,258 @@
+package de.jplag.endtoend;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
+import java.util.StringJoiner;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.DynamicTest;
+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;
+import de.jplag.endtoend.model.*;
+import de.jplag.exceptions.ExitException;
+import de.jplag.options.JPlagOptions;
+
+/**
+ * Main test class for end-to-end testing in all language. The test cases aim to detect changes in the detection of
+ * plagiarism in the Java language and to be able to roughly categorize them. The plagiarism is compared with the
+ * original class. The results are compared with the results from previous tests and changes are detected.
+ */
+public class EndToEndSuiteTest {
+    // Language -> directory names and Paths
+    private Map<String, Map<String, Path>> LanguageToTestCaseMapper;
+
+    private List<Options> options;
+
+    private static Map<String, List<ResultDescription>> temporaryResultList;
+    private static List<String> validationErrors;
+    private static String languageIdentifier;
+
+    public EndToEndSuiteTest() throws IOException {
+        // Loading the test resources
+        LanguageToTestCaseMapper = TestSuiteHelper.getAllLanguageResources();
+        // creating the temporary lists for the test run
+        validationErrors = new ArrayList<>();
+        temporaryResultList = new HashMap<>();
+        // creating options object for the testSuite
+        setRunOptions();
+    }
+
+    /**
+     * creating the required options object for the endToEnd tests
+     */
+    private void setRunOptions() {
+        options = new ArrayList<>();
+        options.add(new Options(1));
+        options.add(new Options(15));
+    }
+
+    /**
+     * Creates the result json files based on the current test results after the test run.
+     * @throws IOException is thrown for all problems that may occur while parsing the json file. This includes both reading
+     */
+    @AfterAll
+    public static void tearDown() throws IOException {
+        for (var resultDescriptionItem : temporaryResultList.entrySet()) {
+            JsonHelper.writeJsonModelsToJsonFile(resultDescriptionItem.getValue(), resultDescriptionItem.getKey(), languageIdentifier);
+        }
+    }
+
+    /**
+     * Creates the test cases over all language options for which data is available and the current test options.
+     * @return dynamic test cases across all test data and languages
+     * @throws IOException is thrown for all problems that may occur while parsing the json file. This includes both reading
+     */
+    @TestFactory
+    Collection<DynamicTest> dynamicOverAllTest() throws IOException {
+        for (Entry<String, Map<String, Path>> languageMap : LanguageToTestCaseMapper.entrySet()) {
+            String currentLanguageIdentifier = languageMap.getKey();
+            for (Entry<String, Path> languagePaths : languageMap.getValue().entrySet()) {
+                String[] fileNames = FileHelper.loadAllTestFileNames(languagePaths.getValue());
+                var testCases = TestSuiteHelper.getTestCases(fileNames, languagePaths.getValue());
+                var testCollection = new ArrayList<DynamicTest>();
+                String directoryName = languagePaths.getValue().getFileName().toString();
+                List<ResultDescription> tempResult = JsonHelper.getJsonModelListFromPath(directoryName, currentLanguageIdentifier);
+                languageIdentifier = currentLanguageIdentifier;
+                for (Options option : options) {
+                    for (var testCase : testCases) {
+                        Optional<ResultDescription> currentResultDescription = tempResult.stream().filter(x -> x.options().equals(option))
+                                .findFirst();
+                        testCollection.add(DynamicTest.dynamicTest(getTestCaseDisplayName(option, languageIdentifier, testCase), () -> {
+                            runTests(directoryName, option, currentLanguageIdentifier, testCase, currentResultDescription);
+                        }));
+                    }
+                }
+                return testCollection;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 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 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
+     * @throws IOException Signals that an I/O exception of some sort has occurred. Thisclass is the general class of
+     * exceptions produced by failed orinterrupted I/O operations
+     * @throws ExitException Exceptions for problems during the execution of JPlag that lead to an preemptive exit.
+     */
+    private void runTests(String directoryName, Options option, String currentLanguageIdentifier, String[] testFiles,
+            Optional<ResultDescription> currentResultDescription) throws IOException, ExitException {
+        try {
+            ResultDescription currentResult = currentResultDescription.orElse(null);
+            runJPlagTestSuite(directoryName, option, currentLanguageIdentifier, testFiles, currentResult);
+        } finally {
+            validationErrors.clear();
+            TestSuiteHelper.clear();
+        }
+    }
+
+    /**
+     * EndToEnd test for the passed objects
+     * @param directoryName name of the current tested directory
+     * @param options for the current test run
+     * @param languageIdentifier current JPlag language option
+     * @param testFiles files to be tested
+     * @param currentResultDescription results stored for the test data
+     * @throws IOException Signals that an I/O exception of some sort has occurred. Thisclass is the general class of
+     * exceptions produced by failed orinterrupted I/O operations
+     * @throws ExitException Exceptions for problems during the execution of JPlag that lead to an preemptive exit.
+     */
+    private void runJPlagTestSuite(String directoryName, Options options, String languageIdentifier, String[] testFiles,
+            ResultDescription currentResultDescription) throws IOException, ExitException {
+        String[] submissionPath = FileHelper.createNewTestCaseDirectory(testFiles);
+
+        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();
+
+        for (JPlagComparison jPlagComparison : currentJPlagComparison) {
+            String identifier = TestSuiteHelper.getTestIdentifier(jPlagComparison);
+            addToTemporaryResultMap(directoryName, options, jPlagComparison, languageIdentifier);
+
+            assertNotNull(currentResultDescription, "No stored result could be found for the current LanguageOption! " + options.toString());
+
+            ExpectedResult result = currentResultDescription.getExpectedResultByIdentifier(TestSuiteHelper.getTestIdentifier(jPlagComparison));
+            assertNotNull(result, "No stored result could be found for the identifier! " + identifier);
+
+            if (Float.compare(result.resultSimilarityMinimum(), jPlagComparison.minimalSimilarity()) != 0) {
+                addToValidationErrors("minimalSimilarity", String.valueOf(result.resultSimilarityMinimum()),
+                        String.valueOf(jPlagComparison.minimalSimilarity()));
+            }
+            if (Float.compare(result.resultSimilarityMaximum(), jPlagComparison.maximalSimilarity()) != 0) {
+                addToValidationErrors("maximalSimilarity", String.valueOf(result.resultSimilarityMaximum()),
+                        String.valueOf(jPlagComparison.maximalSimilarity()));
+            }
+            if (Integer.compare(result.resultMatchedTokenNumber(), jPlagComparison.getNumberOfMatchedTokens()) != 0) {
+                addToValidationErrors("numberOfMatchedTokens", String.valueOf(result.resultMatchedTokenNumber()),
+                        String.valueOf(jPlagComparison.getNumberOfMatchedTokens()));
+            }
+
+            assertTrue(validationErrors.isEmpty(), createValidationErrorOutout());
+        }
+    }
+
+    /**
+     * Creates the display message for failed tests
+     * @param valueName Name of the failed test object
+     * @param currentValue current test values
+     * @param expectedValue expected test values
+     */
+    private void addToValidationErrors(String valueName, String currentValue, String expectedValue) {
+        validationErrors.add(valueName + " was " + currentValue + " but expected " + expectedValue);
+    }
+
+    /**
+     * Creates the display info from the current failed test results
+     * @return formatted text for the failed comparative values of the current test
+     */
+    private String createValidationErrorOutout() {
+        StringJoiner joiner = new StringJoiner(System.lineSeparator());
+        joiner.add(""); // empty line at start
+        joiner.add("There were <" + validationErrors.size() + "> validation error(s):");
+
+        validationErrors.stream().forEach(errorLine -> joiner.add(errorLine));
+        joiner.add(""); // line break at the end
+
+        return joiner.toString();
+    }
+
+    /**
+     * Add or create the current test results in a temporary list to be able to save them later.
+     * @param directoryName name of the current tested directory
+     * @param options for the current test run
+     * @param jPlagComparison current test results
+     * @param languageIdentifier current JPlag language option
+     */
+    private void addToTemporaryResultMap(String directoryName, Options options, JPlagComparison jPlagComparison, String languageIdentifier) {
+        var element = temporaryResultList.get(directoryName);
+
+        if (element != null) {
+            for (var item : element) {
+                if (item.options().equals(options)) {
+                    item.putIdentifierToResultMap(TestSuiteHelper.getTestIdentifier(jPlagComparison), new ExpectedResult(
+                            jPlagComparison.minimalSimilarity(), jPlagComparison.maximalSimilarity(), jPlagComparison.getNumberOfMatchedTokens()));
+                    return;
+                }
+            }
+            Map<String, ExpectedResult> temporaryHashMap = new HashMap<>();
+            temporaryHashMap.put(TestSuiteHelper.getTestIdentifier(jPlagComparison), new ExpectedResult(jPlagComparison.minimalSimilarity(),
+                    jPlagComparison.maximalSimilarity(), jPlagComparison.getNumberOfMatchedTokens()));
+            element.add(new ResultDescription(languageIdentifier, options, temporaryHashMap));
+        } else {
+            var temporaryNewResultList = new ArrayList<ResultDescription>();
+            Map<String, ExpectedResult> temporaryHashMap = new HashMap<>();
+            temporaryHashMap.put(TestSuiteHelper.getTestIdentifier(jPlagComparison), new ExpectedResult(jPlagComparison.minimalSimilarity(),
+                    jPlagComparison.maximalSimilarity(), jPlagComparison.getNumberOfMatchedTokens()));
+
+            temporaryNewResultList.add(new ResultDescription(languageIdentifier, options, temporaryHashMap));
+
+            temporaryResultList.put(directoryName, temporaryNewResultList);
+        }
+    }
+
+    /**
+     * Creates the name of the test for better assignment and readability Pattern: Language: (option values)
+     * filename1-filename2
+     * @param option under which the current test run
+     * @param languageIdentifier current language used in the test
+     * @param testFiles test data for assigning by filename
+     * @return display name for the individual tests
+     */
+    private String getTestCaseDisplayName(Options option, String languageIdentifier, String[] testFiles) {
+        StringBuilder stringBuilder = new StringBuilder();
+        stringBuilder.append("(" + String.valueOf(option.minimumTokenMatch()) + ")");
+        for (int counter = 0; counter < testFiles.length; counter++) {
+            String fileName = Path.of(testFiles[counter]).toFile().getName();
+            stringBuilder.append(fileName.substring(0, fileName.lastIndexOf('.')));
+            if (counter + 1 < testFiles.length) {
+                stringBuilder.append("-");
+            }
+        }
+        return languageIdentifier + ": " + stringBuilder.toString();
+    }
+}

+ 0 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo.java → jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo.java


+ 0 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo1.java → jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo1.java


+ 0 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo1_2.java → jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo1_2.java


+ 0 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo1_3.java → jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo1_3.java


+ 0 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo1_4.java → jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo1_4.java


+ 0 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo1_5.java → jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo1_5.java


+ 0 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo1_6.java → jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo1_6.java


+ 0 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo2.java → jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo2.java


+ 0 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo2_5.java → jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo2_5.java


+ 0 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo3.java → jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo3.java


+ 0 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo3_5.java → jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo3_5.java


+ 0 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo3_6.java → jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo3_6.java


+ 0 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo4.java → jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo4.java


+ 0 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo4d1.java → jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo4d1.java


+ 0 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo4d2.java → jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo4d2.java


+ 0 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo4d3.java → jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo4d3.java


+ 0 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo5.java → jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo5.java


+ 0 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo6.java → jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo6.java


+ 0 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo7.java → jplag.endtoend-testing/src/test/resources/languageTestFiles/java/sortAlgo/SortAlgo7.java


+ 1723 - 0
jplag.endtoend-testing/src/test/resources/results/java/sortAlgo.json

@@ -0,0 +1,1723 @@
+[ {
+  "options" : {
+    "minimum_token_match" : 1
+  },
+  "tests" : {
+    "SortAlgo-SortAlgo5" : {
+      "minimal_similarity" : 82.14286,
+      "maximum_similarity" : 82.14286,
+      "matched_token_number" : 46
+    },
+    "SortAlgo-SortAlgo6" : {
+      "minimal_similarity" : 83.58209,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo-SortAlgo7" : {
+      "minimal_similarity" : 96.42857,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 54
+    },
+    "SortAlgo3_5-SortAlgo3_6" : {
+      "minimal_similarity" : 78.666664,
+      "maximum_similarity" : 85.50725,
+      "matched_token_number" : 59
+    },
+    "SortAlgo6-SortAlgo7" : {
+      "minimal_similarity" : 80.597015,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 54
+    },
+    "SortAlgo1_3-SortAlgo7" : {
+      "minimal_similarity" : 72.0,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 54
+    },
+    "SortAlgo1_3-SortAlgo6" : {
+      "minimal_similarity" : 85.333336,
+      "maximum_similarity" : 95.522385,
+      "matched_token_number" : 64
+    },
+    "SortAlgo-SortAlgo1" : {
+      "minimal_similarity" : 100.0,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1_3-SortAlgo3" : {
+      "minimal_similarity" : 92.0,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 69
+    },
+    "SortAlgo-SortAlgo2" : {
+      "minimal_similarity" : 100.0,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1_3-SortAlgo2" : {
+      "minimal_similarity" : 74.666664,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo-SortAlgo3" : {
+      "minimal_similarity" : 81.159424,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1_3-SortAlgo5" : {
+      "minimal_similarity" : 62.666668,
+      "maximum_similarity" : 83.92857,
+      "matched_token_number" : 47
+    },
+    "SortAlgo-SortAlgo4" : {
+      "minimal_similarity" : 94.91525,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1_3-SortAlgo4" : {
+      "minimal_similarity" : 77.333336,
+      "maximum_similarity" : 98.305084,
+      "matched_token_number" : 58
+    },
+    "SortAlgo2-SortAlgo2_5" : {
+      "minimal_similarity" : 82.14286,
+      "maximum_similarity" : 82.14286,
+      "matched_token_number" : 46
+    },
+    "SortAlgo1_2-SortAlgo2" : {
+      "minimal_similarity" : 73.68421,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1_2-SortAlgo3" : {
+      "minimal_similarity" : 82.89474,
+      "maximum_similarity" : 91.304344,
+      "matched_token_number" : 63
+    },
+    "SortAlgo5-SortAlgo6" : {
+      "minimal_similarity" : 70.149254,
+      "maximum_similarity" : 83.92857,
+      "matched_token_number" : 47
+    },
+    "SortAlgo1_2-SortAlgo4" : {
+      "minimal_similarity" : 76.31579,
+      "maximum_similarity" : 98.305084,
+      "matched_token_number" : 58
+    },
+    "SortAlgo5-SortAlgo7" : {
+      "minimal_similarity" : 82.14286,
+      "maximum_similarity" : 85.18519,
+      "matched_token_number" : 46
+    },
+    "SortAlgo1_2-SortAlgo5" : {
+      "minimal_similarity" : 61.842106,
+      "maximum_similarity" : 83.92857,
+      "matched_token_number" : 47
+    },
+    "SortAlgo1_2-SortAlgo6" : {
+      "minimal_similarity" : 84.210526,
+      "maximum_similarity" : 95.522385,
+      "matched_token_number" : 64
+    },
+    "SortAlgo1_2-SortAlgo7" : {
+      "minimal_similarity" : 71.052635,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 54
+    },
+    "SortAlgo1-SortAlgo2_5" : {
+      "minimal_similarity" : 82.14286,
+      "maximum_similarity" : 82.14286,
+      "matched_token_number" : 46
+    },
+    "SortAlgo1_3-SortAlgo3_6" : {
+      "minimal_similarity" : 93.333336,
+      "maximum_similarity" : 93.333336,
+      "matched_token_number" : 70
+    },
+    "SortAlgo4-SortAlgo5" : {
+      "minimal_similarity" : 77.9661,
+      "maximum_similarity" : 82.14286,
+      "matched_token_number" : 46
+    },
+    "SortAlgo4-SortAlgo6" : {
+      "minimal_similarity" : 88.0597,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 59
+    },
+    "SortAlgo1_4-SortAlgo3_5" : {
+      "minimal_similarity" : 72.46377,
+      "maximum_similarity" : 84.745766,
+      "matched_token_number" : 50
+    },
+    "SortAlgo4-SortAlgo7" : {
+      "minimal_similarity" : 91.52542,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 54
+    },
+    "SortAlgo1_4-SortAlgo3_6" : {
+      "minimal_similarity" : 78.666664,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 59
+    },
+    "SortAlgo1_5-SortAlgo3_6" : {
+      "minimal_similarity" : 62.666668,
+      "maximum_similarity" : 83.92857,
+      "matched_token_number" : 47
+    },
+    "SortAlgo1_5-SortAlgo3_5" : {
+      "minimal_similarity" : 81.159424,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1_6-SortAlgo3_5" : {
+      "minimal_similarity" : 73.91304,
+      "maximum_similarity" : 85.0,
+      "matched_token_number" : 51
+    },
+    "SortAlgo-SortAlgo4d3" : {
+      "minimal_similarity" : 90.32258,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1_6-SortAlgo3_6" : {
+      "minimal_similarity" : 80.0,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 60
+    },
+    "SortAlgo-SortAlgo4d2" : {
+      "minimal_similarity" : 87.5,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo-SortAlgo4d1" : {
+      "minimal_similarity" : 87.30159,
+      "maximum_similarity" : 98.21429,
+      "matched_token_number" : 55
+    },
+    "SortAlgo1_4-SortAlgo3" : {
+      "minimal_similarity" : 84.05797,
+      "maximum_similarity" : 98.305084,
+      "matched_token_number" : 58
+    },
+    "SortAlgo3-SortAlgo3_5" : {
+      "minimal_similarity" : 85.50725,
+      "maximum_similarity" : 85.50725,
+      "matched_token_number" : 59
+    },
+    "SortAlgo1_4-SortAlgo4" : {
+      "minimal_similarity" : 100.0,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 59
+    },
+    "SortAlgo1_4-SortAlgo2" : {
+      "minimal_similarity" : 94.91525,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo3-SortAlgo3_6" : {
+      "minimal_similarity" : 92.0,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 69
+    },
+    "SortAlgo1_4-SortAlgo7" : {
+      "minimal_similarity" : 91.52542,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 54
+    },
+    "SortAlgo1_4-SortAlgo5" : {
+      "minimal_similarity" : 77.9661,
+      "maximum_similarity" : 82.14286,
+      "matched_token_number" : 46
+    },
+    "SortAlgo2-SortAlgo3_5" : {
+      "minimal_similarity" : 72.46377,
+      "maximum_similarity" : 89.28571,
+      "matched_token_number" : 50
+    },
+    "SortAlgo1_4-SortAlgo6" : {
+      "minimal_similarity" : 88.0597,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 59
+    },
+    "SortAlgo2-SortAlgo3_6" : {
+      "minimal_similarity" : 74.666664,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1-SortAlgo3_6" : {
+      "minimal_similarity" : 74.666664,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1-SortAlgo3_5" : {
+      "minimal_similarity" : 72.46377,
+      "maximum_similarity" : 89.28571,
+      "matched_token_number" : 50
+    },
+    "SortAlgo-SortAlgo1_2" : {
+      "minimal_similarity" : 73.68421,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1_3-SortAlgo3_5" : {
+      "minimal_similarity" : 80.0,
+      "maximum_similarity" : 86.95652,
+      "matched_token_number" : 60
+    },
+    "SortAlgo1_2-SortAlgo3_5" : {
+      "minimal_similarity" : 72.36842,
+      "maximum_similarity" : 79.710144,
+      "matched_token_number" : 55
+    },
+    "SortAlgo1_2-SortAlgo3_6" : {
+      "minimal_similarity" : 88.1579,
+      "maximum_similarity" : 89.333336,
+      "matched_token_number" : 67
+    },
+    "SortAlgo-SortAlgo1_6" : {
+      "minimal_similarity" : 93.333336,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo-SortAlgo1_5" : {
+      "minimal_similarity" : 82.14286,
+      "maximum_similarity" : 82.14286,
+      "matched_token_number" : 46
+    },
+    "SortAlgo-SortAlgo1_4" : {
+      "minimal_similarity" : 94.91525,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo-SortAlgo1_3" : {
+      "minimal_similarity" : 74.666664,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1_6-SortAlgo4d1" : {
+      "minimal_similarity" : 88.888885,
+      "maximum_similarity" : 93.333336,
+      "matched_token_number" : 56
+    },
+    "SortAlgo4d2-SortAlgo7" : {
+      "minimal_similarity" : 84.375,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 54
+    },
+    "SortAlgo1_6-SortAlgo4d2" : {
+      "minimal_similarity" : 89.0625,
+      "maximum_similarity" : 95.0,
+      "matched_token_number" : 57
+    },
+    "SortAlgo4d2-SortAlgo5" : {
+      "minimal_similarity" : 73.4375,
+      "maximum_similarity" : 83.92857,
+      "matched_token_number" : 47
+    },
+    "SortAlgo4d2-SortAlgo6" : {
+      "minimal_similarity" : 89.55224,
+      "maximum_similarity" : 93.75,
+      "matched_token_number" : 60
+    },
+    "SortAlgo1_6-SortAlgo4d3" : {
+      "minimal_similarity" : 91.935486,
+      "maximum_similarity" : 95.0,
+      "matched_token_number" : 57
+    },
+    "SortAlgo1_5-SortAlgo2_5" : {
+      "minimal_similarity" : 100.0,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1_2-SortAlgo4d1" : {
+      "minimal_similarity" : 76.31579,
+      "maximum_similarity" : 92.06349,
+      "matched_token_number" : 58
+    },
+    "SortAlgo1_3-SortAlgo4d3" : {
+      "minimal_similarity" : 82.666664,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 62
+    },
+    "SortAlgo2-SortAlgo3" : {
+      "minimal_similarity" : 81.159424,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1_2-SortAlgo4d2" : {
+      "minimal_similarity" : 77.63158,
+      "maximum_similarity" : 92.1875,
+      "matched_token_number" : 59
+    },
+    "SortAlgo1_3-SortAlgo4d2" : {
+      "minimal_similarity" : 85.333336,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 64
+    },
+    "SortAlgo2-SortAlgo4" : {
+      "minimal_similarity" : 94.91525,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1_6-SortAlgo2_5" : {
+      "minimal_similarity" : 76.666664,
+      "maximum_similarity" : 82.14286,
+      "matched_token_number" : 46
+    },
+    "SortAlgo1_4-SortAlgo4d1" : {
+      "minimal_similarity" : 87.30159,
+      "maximum_similarity" : 93.22034,
+      "matched_token_number" : 55
+    },
+    "SortAlgo1_5-SortAlgo4d3" : {
+      "minimal_similarity" : 75.80645,
+      "maximum_similarity" : 83.92857,
+      "matched_token_number" : 47
+    },
+    "SortAlgo1_4-SortAlgo4d2" : {
+      "minimal_similarity" : 87.5,
+      "maximum_similarity" : 94.91525,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1_5-SortAlgo4d2" : {
+      "minimal_similarity" : 73.4375,
+      "maximum_similarity" : 83.92857,
+      "matched_token_number" : 47
+    },
+    "SortAlgo1_4-SortAlgo4d3" : {
+      "minimal_similarity" : 90.32258,
+      "maximum_similarity" : 94.91525,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1_5-SortAlgo4d1" : {
+      "minimal_similarity" : 73.01588,
+      "maximum_similarity" : 82.14286,
+      "matched_token_number" : 46
+    },
+    "SortAlgo4d2-SortAlgo4d3" : {
+      "minimal_similarity" : 96.875,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 62
+    },
+    "SortAlgo2-SortAlgo7" : {
+      "minimal_similarity" : 96.42857,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 54
+    },
+    "SortAlgo1_2-SortAlgo4d3" : {
+      "minimal_similarity" : 76.31579,
+      "maximum_similarity" : 93.548386,
+      "matched_token_number" : 58
+    },
+    "SortAlgo1_3-SortAlgo4d1" : {
+      "minimal_similarity" : 84.0,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 63
+    },
+    "SortAlgo2-SortAlgo5" : {
+      "minimal_similarity" : 82.14286,
+      "maximum_similarity" : 82.14286,
+      "matched_token_number" : 46
+    },
+    "SortAlgo4d1-SortAlgo4d2" : {
+      "minimal_similarity" : 98.4375,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 63
+    },
+    "SortAlgo2-SortAlgo6" : {
+      "minimal_similarity" : 83.58209,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo4d1-SortAlgo4d3" : {
+      "minimal_similarity" : 96.82539,
+      "maximum_similarity" : 98.3871,
+      "matched_token_number" : 61
+    },
+    "SortAlgo1_6-SortAlgo7" : {
+      "minimal_similarity" : 90.0,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 54
+    },
+    "SortAlgo2_5-SortAlgo3_5" : {
+      "minimal_similarity" : 81.159424,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo3_6-SortAlgo6" : {
+      "minimal_similarity" : 89.333336,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 67
+    },
+    "SortAlgo2_5-SortAlgo3_6" : {
+      "minimal_similarity" : 62.666668,
+      "maximum_similarity" : 83.92857,
+      "matched_token_number" : 47
+    },
+    "SortAlgo3_6-SortAlgo5" : {
+      "minimal_similarity" : 62.666668,
+      "maximum_similarity" : 83.92857,
+      "matched_token_number" : 47
+    },
+    "SortAlgo3_6-SortAlgo7" : {
+      "minimal_similarity" : 72.0,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 54
+    },
+    "SortAlgo4d3-SortAlgo6" : {
+      "minimal_similarity" : 89.55224,
+      "maximum_similarity" : 96.77419,
+      "matched_token_number" : 60
+    },
+    "SortAlgo4d3-SortAlgo7" : {
+      "minimal_similarity" : 87.09677,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 54
+    },
+    "SortAlgo4d3-SortAlgo5" : {
+      "minimal_similarity" : 75.80645,
+      "maximum_similarity" : 83.92857,
+      "matched_token_number" : 47
+    },
+    "SortAlgo1_4-SortAlgo2_5" : {
+      "minimal_similarity" : 77.9661,
+      "maximum_similarity" : 82.14286,
+      "matched_token_number" : 46
+    },
+    "SortAlgo1-SortAlgo4" : {
+      "minimal_similarity" : 94.91525,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1-SortAlgo5" : {
+      "minimal_similarity" : 82.14286,
+      "maximum_similarity" : 82.14286,
+      "matched_token_number" : 46
+    },
+    "SortAlgo1-SortAlgo2" : {
+      "minimal_similarity" : 100.0,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo3_6-SortAlgo4" : {
+      "minimal_similarity" : 78.666664,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 59
+    },
+    "SortAlgo1-SortAlgo3" : {
+      "minimal_similarity" : 81.159424,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1_3-SortAlgo2_5" : {
+      "minimal_similarity" : 62.666668,
+      "maximum_similarity" : 83.92857,
+      "matched_token_number" : 47
+    },
+    "SortAlgo1_6-SortAlgo2" : {
+      "minimal_similarity" : 93.333336,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1-SortAlgo6" : {
+      "minimal_similarity" : 83.58209,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1-SortAlgo7" : {
+      "minimal_similarity" : 96.42857,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 54
+    },
+    "SortAlgo1_2-SortAlgo2_5" : {
+      "minimal_similarity" : 61.842106,
+      "maximum_similarity" : 83.92857,
+      "matched_token_number" : 47
+    },
+    "SortAlgo-SortAlgo2_5" : {
+      "minimal_similarity" : 82.14286,
+      "maximum_similarity" : 82.14286,
+      "matched_token_number" : 46
+    },
+    "SortAlgo1_6-SortAlgo6" : {
+      "minimal_similarity" : 89.55224,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 60
+    },
+    "SortAlgo1_6-SortAlgo5" : {
+      "minimal_similarity" : 76.666664,
+      "maximum_similarity" : 82.14286,
+      "matched_token_number" : 46
+    },
+    "SortAlgo1_6-SortAlgo4" : {
+      "minimal_similarity" : 98.333336,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 59
+    },
+    "SortAlgo1_6-SortAlgo3" : {
+      "minimal_similarity" : 85.50725,
+      "maximum_similarity" : 98.333336,
+      "matched_token_number" : 59
+    },
+    "SortAlgo1_5-SortAlgo6" : {
+      "minimal_similarity" : 70.149254,
+      "maximum_similarity" : 83.92857,
+      "matched_token_number" : 47
+    },
+    "SortAlgo1_5-SortAlgo7" : {
+      "minimal_similarity" : 82.14286,
+      "maximum_similarity" : 85.18519,
+      "matched_token_number" : 46
+    },
+    "SortAlgo2_5-SortAlgo7" : {
+      "minimal_similarity" : 82.14286,
+      "maximum_similarity" : 85.18519,
+      "matched_token_number" : 46
+    },
+    "SortAlgo1-SortAlgo4d3" : {
+      "minimal_similarity" : 90.32258,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1_5-SortAlgo4" : {
+      "minimal_similarity" : 77.9661,
+      "maximum_similarity" : 82.14286,
+      "matched_token_number" : 46
+    },
+    "SortAlgo1-SortAlgo4d2" : {
+      "minimal_similarity" : 87.5,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1_5-SortAlgo5" : {
+      "minimal_similarity" : 100.0,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1-SortAlgo4d1" : {
+      "minimal_similarity" : 87.30159,
+      "maximum_similarity" : 98.21429,
+      "matched_token_number" : 55
+    },
+    "SortAlgo1_5-SortAlgo2" : {
+      "minimal_similarity" : 82.14286,
+      "maximum_similarity" : 82.14286,
+      "matched_token_number" : 46
+    },
+    "SortAlgo3_5-SortAlgo6" : {
+      "minimal_similarity" : 81.159424,
+      "maximum_similarity" : 83.58209,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1_5-SortAlgo3" : {
+      "minimal_similarity" : 68.115944,
+      "maximum_similarity" : 83.92857,
+      "matched_token_number" : 47
+    },
+    "SortAlgo3_5-SortAlgo7" : {
+      "minimal_similarity" : 69.565216,
+      "maximum_similarity" : 88.888885,
+      "matched_token_number" : 48
+    },
+    "SortAlgo3-SortAlgo4d1" : {
+      "minimal_similarity" : 88.4058,
+      "maximum_similarity" : 96.82539,
+      "matched_token_number" : 61
+    },
+    "SortAlgo3-SortAlgo4d3" : {
+      "minimal_similarity" : 86.95652,
+      "maximum_similarity" : 96.77419,
+      "matched_token_number" : 60
+    },
+    "SortAlgo3-SortAlgo4d2" : {
+      "minimal_similarity" : 89.85507,
+      "maximum_similarity" : 96.875,
+      "matched_token_number" : 62
+    },
+    "SortAlgo2-SortAlgo4d3" : {
+      "minimal_similarity" : 90.32258,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo2-SortAlgo4d1" : {
+      "minimal_similarity" : 87.30159,
+      "maximum_similarity" : 98.21429,
+      "matched_token_number" : 55
+    },
+    "SortAlgo1_5-SortAlgo1_6" : {
+      "minimal_similarity" : 76.666664,
+      "maximum_similarity" : 82.14286,
+      "matched_token_number" : 46
+    },
+    "SortAlgo2-SortAlgo4d2" : {
+      "minimal_similarity" : 87.5,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo3_6-SortAlgo4d2" : {
+      "minimal_similarity" : 84.0,
+      "maximum_similarity" : 98.4375,
+      "matched_token_number" : 63
+    },
+    "SortAlgo4d1-SortAlgo7" : {
+      "minimal_similarity" : 84.12698,
+      "maximum_similarity" : 98.14815,
+      "matched_token_number" : 53
+    },
+    "SortAlgo3_6-SortAlgo4d1" : {
+      "minimal_similarity" : 82.666664,
+      "maximum_similarity" : 98.4127,
+      "matched_token_number" : 62
+    },
+    "SortAlgo4d1-SortAlgo6" : {
+      "minimal_similarity" : 88.0597,
+      "maximum_similarity" : 93.650795,
+      "matched_token_number" : 59
+    },
+    "SortAlgo4-SortAlgo4d1" : {
+      "minimal_similarity" : 87.30159,
+      "maximum_similarity" : 93.22034,
+      "matched_token_number" : 55
+    },
+    "SortAlgo3_6-SortAlgo4d3" : {
+      "minimal_similarity" : 81.333336,
+      "maximum_similarity" : 98.3871,
+      "matched_token_number" : 61
+    },
+    "SortAlgo4-SortAlgo4d2" : {
+      "minimal_similarity" : 87.5,
+      "maximum_similarity" : 94.91525,
+      "matched_token_number" : 56
+    },
+    "SortAlgo4-SortAlgo4d3" : {
+      "minimal_similarity" : 90.32258,
+      "maximum_similarity" : 94.91525,
+      "matched_token_number" : 56
+    },
+    "SortAlgo3_5-SortAlgo4" : {
+      "minimal_similarity" : 72.46377,
+      "maximum_similarity" : 84.745766,
+      "matched_token_number" : 50
+    },
+    "SortAlgo3_5-SortAlgo4d2" : {
+      "minimal_similarity" : 81.159424,
+      "maximum_similarity" : 87.5,
+      "matched_token_number" : 56
+    },
+    "SortAlgo4d1-SortAlgo5" : {
+      "minimal_similarity" : 73.01588,
+      "maximum_similarity" : 82.14286,
+      "matched_token_number" : 46
+    },
+    "SortAlgo3_5-SortAlgo4d3" : {
+      "minimal_similarity" : 78.26087,
+      "maximum_similarity" : 87.09677,
+      "matched_token_number" : 54
+    },
+    "SortAlgo3_5-SortAlgo5" : {
+      "minimal_similarity" : 81.159424,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo2_5-SortAlgo4" : {
+      "minimal_similarity" : 77.9661,
+      "maximum_similarity" : 82.14286,
+      "matched_token_number" : 46
+    },
+    "SortAlgo2_5-SortAlgo3" : {
+      "minimal_similarity" : 68.115944,
+      "maximum_similarity" : 83.92857,
+      "matched_token_number" : 47
+    },
+    "SortAlgo3_5-SortAlgo4d1" : {
+      "minimal_similarity" : 79.710144,
+      "maximum_similarity" : 87.30159,
+      "matched_token_number" : 55
+    },
+    "SortAlgo2_5-SortAlgo6" : {
+      "minimal_similarity" : 70.149254,
+      "maximum_similarity" : 83.92857,
+      "matched_token_number" : 47
+    },
+    "SortAlgo2_5-SortAlgo5" : {
+      "minimal_similarity" : 100.0,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1-SortAlgo1_3" : {
+      "minimal_similarity" : 74.666664,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1-SortAlgo1_2" : {
+      "minimal_similarity" : 73.68421,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1-SortAlgo1_6" : {
+      "minimal_similarity" : 93.333336,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1-SortAlgo1_5" : {
+      "minimal_similarity" : 82.14286,
+      "maximum_similarity" : 82.14286,
+      "matched_token_number" : 46
+    },
+    "SortAlgo1-SortAlgo1_4" : {
+      "minimal_similarity" : 94.91525,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo2_5-SortAlgo4d1" : {
+      "minimal_similarity" : 73.01588,
+      "maximum_similarity" : 82.14286,
+      "matched_token_number" : 46
+    },
+    "SortAlgo3-SortAlgo7" : {
+      "minimal_similarity" : 78.26087,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 54
+    },
+    "SortAlgo2_5-SortAlgo4d2" : {
+      "minimal_similarity" : 73.4375,
+      "maximum_similarity" : 83.92857,
+      "matched_token_number" : 47
+    },
+    "SortAlgo3-SortAlgo6" : {
+      "minimal_similarity" : 92.753624,
+      "maximum_similarity" : 95.522385,
+      "matched_token_number" : 64
+    },
+    "SortAlgo1_4-SortAlgo1_5" : {
+      "minimal_similarity" : 77.9661,
+      "maximum_similarity" : 82.14286,
+      "matched_token_number" : 46
+    },
+    "SortAlgo3-SortAlgo5" : {
+      "minimal_similarity" : 68.115944,
+      "maximum_similarity" : 83.92857,
+      "matched_token_number" : 47
+    },
+    "SortAlgo1_4-SortAlgo1_6" : {
+      "minimal_similarity" : 98.333336,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 59
+    },
+    "SortAlgo3-SortAlgo4" : {
+      "minimal_similarity" : 84.05797,
+      "maximum_similarity" : 98.305084,
+      "matched_token_number" : 58
+    },
+    "SortAlgo1_3-SortAlgo1_5" : {
+      "minimal_similarity" : 62.666668,
+      "maximum_similarity" : 83.92857,
+      "matched_token_number" : 47
+    },
+    "SortAlgo1_3-SortAlgo1_4" : {
+      "minimal_similarity" : 77.333336,
+      "maximum_similarity" : 98.305084,
+      "matched_token_number" : 58
+    },
+    "SortAlgo2_5-SortAlgo4d3" : {
+      "minimal_similarity" : 75.80645,
+      "maximum_similarity" : 83.92857,
+      "matched_token_number" : 47
+    },
+    "SortAlgo1_3-SortAlgo1_6" : {
+      "minimal_similarity" : 78.666664,
+      "maximum_similarity" : 98.333336,
+      "matched_token_number" : 59
+    },
+    "SortAlgo-SortAlgo3_6" : {
+      "minimal_similarity" : 74.666664,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1_2-SortAlgo1_5" : {
+      "minimal_similarity" : 61.842106,
+      "maximum_similarity" : 83.92857,
+      "matched_token_number" : 47
+    },
+    "SortAlgo-SortAlgo3_5" : {
+      "minimal_similarity" : 72.46377,
+      "maximum_similarity" : 89.28571,
+      "matched_token_number" : 50
+    },
+    "SortAlgo1_2-SortAlgo1_6" : {
+      "minimal_similarity" : 76.31579,
+      "maximum_similarity" : 96.666664,
+      "matched_token_number" : 58
+    },
+    "SortAlgo1_2-SortAlgo1_3" : {
+      "minimal_similarity" : 82.89474,
+      "maximum_similarity" : 84.0,
+      "matched_token_number" : 63
+    },
+    "SortAlgo1_2-SortAlgo1_4" : {
+      "minimal_similarity" : 76.31579,
+      "maximum_similarity" : 98.305084,
+      "matched_token_number" : 58
+    }
+  }
+}, {
+  "options" : {
+    "minimum_token_match" : 15
+  },
+  "tests" : {
+    "SortAlgo-SortAlgo5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo-SortAlgo6" : {
+      "minimal_similarity" : 43.28358,
+      "maximum_similarity" : 51.785713,
+      "matched_token_number" : 29
+    },
+    "SortAlgo-SortAlgo7" : {
+      "minimal_similarity" : 82.14286,
+      "maximum_similarity" : 85.18519,
+      "matched_token_number" : 46
+    },
+    "SortAlgo3_5-SortAlgo3_6" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo6-SortAlgo7" : {
+      "minimal_similarity" : 43.28358,
+      "maximum_similarity" : 53.703705,
+      "matched_token_number" : 29
+    },
+    "SortAlgo1_3-SortAlgo7" : {
+      "minimal_similarity" : 53.333332,
+      "maximum_similarity" : 74.07407,
+      "matched_token_number" : 40
+    },
+    "SortAlgo1_3-SortAlgo6" : {
+      "minimal_similarity" : 32.0,
+      "maximum_similarity" : 35.820896,
+      "matched_token_number" : 24
+    },
+    "SortAlgo-SortAlgo1" : {
+      "minimal_similarity" : 100.0,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1_3-SortAlgo3" : {
+      "minimal_similarity" : 54.666668,
+      "maximum_similarity" : 59.420288,
+      "matched_token_number" : 41
+    },
+    "SortAlgo-SortAlgo2" : {
+      "minimal_similarity" : 100.0,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1_3-SortAlgo2" : {
+      "minimal_similarity" : 53.333332,
+      "maximum_similarity" : 71.42857,
+      "matched_token_number" : 40
+    },
+    "SortAlgo-SortAlgo3" : {
+      "minimal_similarity" : 57.971016,
+      "maximum_similarity" : 71.42857,
+      "matched_token_number" : 40
+    },
+    "SortAlgo1_3-SortAlgo5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo-SortAlgo4" : {
+      "minimal_similarity" : 35.59322,
+      "maximum_similarity" : 37.5,
+      "matched_token_number" : 21
+    },
+    "SortAlgo1_3-SortAlgo4" : {
+      "minimal_similarity" : 24.0,
+      "maximum_similarity" : 30.508474,
+      "matched_token_number" : 18
+    },
+    "SortAlgo2-SortAlgo2_5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo1_2-SortAlgo2" : {
+      "minimal_similarity" : 59.210526,
+      "maximum_similarity" : 80.35714,
+      "matched_token_number" : 45
+    },
+    "SortAlgo1_2-SortAlgo3" : {
+      "minimal_similarity" : 52.63158,
+      "maximum_similarity" : 57.971016,
+      "matched_token_number" : 40
+    },
+    "SortAlgo5-SortAlgo6" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo1_2-SortAlgo4" : {
+      "minimal_similarity" : 28.947369,
+      "maximum_similarity" : 37.288136,
+      "matched_token_number" : 22
+    },
+    "SortAlgo5-SortAlgo7" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo1_2-SortAlgo5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo1_2-SortAlgo6" : {
+      "minimal_similarity" : 38.157894,
+      "maximum_similarity" : 43.28358,
+      "matched_token_number" : 29
+    },
+    "SortAlgo1_2-SortAlgo7" : {
+      "minimal_similarity" : 46.05263,
+      "maximum_similarity" : 64.81481,
+      "matched_token_number" : 35
+    },
+    "SortAlgo1-SortAlgo2_5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo1_3-SortAlgo3_6" : {
+      "minimal_similarity" : 32.0,
+      "maximum_similarity" : 32.0,
+      "matched_token_number" : 24
+    },
+    "SortAlgo4-SortAlgo5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo4-SortAlgo6" : {
+      "minimal_similarity" : 31.343283,
+      "maximum_similarity" : 35.59322,
+      "matched_token_number" : 21
+    },
+    "SortAlgo1_4-SortAlgo3_5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo4-SortAlgo7" : {
+      "minimal_similarity" : 35.59322,
+      "maximum_similarity" : 38.88889,
+      "matched_token_number" : 21
+    },
+    "SortAlgo1_4-SortAlgo3_6" : {
+      "minimal_similarity" : 32.0,
+      "maximum_similarity" : 40.677967,
+      "matched_token_number" : 24
+    },
+    "SortAlgo1_5-SortAlgo3_6" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo1_5-SortAlgo3_5" : {
+      "minimal_similarity" : 59.420288,
+      "maximum_similarity" : 73.21429,
+      "matched_token_number" : 41
+    },
+    "SortAlgo1_6-SortAlgo3_5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo-SortAlgo4d3" : {
+      "minimal_similarity" : 70.96774,
+      "maximum_similarity" : 78.57143,
+      "matched_token_number" : 44
+    },
+    "SortAlgo1_6-SortAlgo3_6" : {
+      "minimal_similarity" : 57.333332,
+      "maximum_similarity" : 71.666664,
+      "matched_token_number" : 43
+    },
+    "SortAlgo-SortAlgo4d2" : {
+      "minimal_similarity" : 68.75,
+      "maximum_similarity" : 78.57143,
+      "matched_token_number" : 44
+    },
+    "SortAlgo-SortAlgo4d1" : {
+      "minimal_similarity" : 69.84127,
+      "maximum_similarity" : 78.57143,
+      "matched_token_number" : 44
+    },
+    "SortAlgo1_4-SortAlgo3" : {
+      "minimal_similarity" : 34.782608,
+      "maximum_similarity" : 40.677967,
+      "matched_token_number" : 24
+    },
+    "SortAlgo3-SortAlgo3_5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo1_4-SortAlgo4" : {
+      "minimal_similarity" : 79.66102,
+      "maximum_similarity" : 79.66102,
+      "matched_token_number" : 47
+    },
+    "SortAlgo1_4-SortAlgo2" : {
+      "minimal_similarity" : 52.542374,
+      "maximum_similarity" : 55.357143,
+      "matched_token_number" : 31
+    },
+    "SortAlgo3-SortAlgo3_6" : {
+      "minimal_similarity" : 56.0,
+      "maximum_similarity" : 60.869564,
+      "matched_token_number" : 42
+    },
+    "SortAlgo1_4-SortAlgo7" : {
+      "minimal_similarity" : 52.542374,
+      "maximum_similarity" : 57.407406,
+      "matched_token_number" : 31
+    },
+    "SortAlgo1_4-SortAlgo5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo2-SortAlgo3_5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo1_4-SortAlgo6" : {
+      "minimal_similarity" : 40.298508,
+      "maximum_similarity" : 45.76271,
+      "matched_token_number" : 27
+    },
+    "SortAlgo2-SortAlgo3_6" : {
+      "minimal_similarity" : 32.0,
+      "maximum_similarity" : 42.857143,
+      "matched_token_number" : 24
+    },
+    "SortAlgo1-SortAlgo3_6" : {
+      "minimal_similarity" : 32.0,
+      "maximum_similarity" : 42.857143,
+      "matched_token_number" : 24
+    },
+    "SortAlgo1-SortAlgo3_5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo-SortAlgo1_2" : {
+      "minimal_similarity" : 59.210526,
+      "maximum_similarity" : 80.35714,
+      "matched_token_number" : 45
+    },
+    "SortAlgo1_3-SortAlgo3_5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo1_2-SortAlgo3_5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo1_2-SortAlgo3_6" : {
+      "minimal_similarity" : 31.578947,
+      "maximum_similarity" : 32.0,
+      "matched_token_number" : 24
+    },
+    "SortAlgo-SortAlgo1_6" : {
+      "minimal_similarity" : 56.666668,
+      "maximum_similarity" : 60.714287,
+      "matched_token_number" : 34
+    },
+    "SortAlgo-SortAlgo1_5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo-SortAlgo1_4" : {
+      "minimal_similarity" : 52.542374,
+      "maximum_similarity" : 55.357143,
+      "matched_token_number" : 31
+    },
+    "SortAlgo-SortAlgo1_3" : {
+      "minimal_similarity" : 53.333332,
+      "maximum_similarity" : 71.42857,
+      "matched_token_number" : 40
+    },
+    "SortAlgo1_6-SortAlgo4d1" : {
+      "minimal_similarity" : 36.50794,
+      "maximum_similarity" : 38.333332,
+      "matched_token_number" : 23
+    },
+    "SortAlgo4d2-SortAlgo7" : {
+      "minimal_similarity" : 62.5,
+      "maximum_similarity" : 74.07407,
+      "matched_token_number" : 40
+    },
+    "SortAlgo1_6-SortAlgo4d2" : {
+      "minimal_similarity" : 35.9375,
+      "maximum_similarity" : 38.333332,
+      "matched_token_number" : 23
+    },
+    "SortAlgo4d2-SortAlgo5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo4d2-SortAlgo6" : {
+      "minimal_similarity" : 34.328358,
+      "maximum_similarity" : 35.9375,
+      "matched_token_number" : 23
+    },
+    "SortAlgo1_6-SortAlgo4d3" : {
+      "minimal_similarity" : 37.096775,
+      "maximum_similarity" : 38.333332,
+      "matched_token_number" : 23
+    },
+    "SortAlgo1_5-SortAlgo2_5" : {
+      "minimal_similarity" : 100.0,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1_2-SortAlgo4d1" : {
+      "minimal_similarity" : 57.894737,
+      "maximum_similarity" : 69.84127,
+      "matched_token_number" : 44
+    },
+    "SortAlgo1_3-SortAlgo4d3" : {
+      "minimal_similarity" : 53.333332,
+      "maximum_similarity" : 64.51613,
+      "matched_token_number" : 40
+    },
+    "SortAlgo2-SortAlgo3" : {
+      "minimal_similarity" : 57.971016,
+      "maximum_similarity" : 71.42857,
+      "matched_token_number" : 40
+    },
+    "SortAlgo1_2-SortAlgo4d2" : {
+      "minimal_similarity" : 57.894737,
+      "maximum_similarity" : 68.75,
+      "matched_token_number" : 44
+    },
+    "SortAlgo1_3-SortAlgo4d2" : {
+      "minimal_similarity" : 53.333332,
+      "maximum_similarity" : 62.5,
+      "matched_token_number" : 40
+    },
+    "SortAlgo2-SortAlgo4" : {
+      "minimal_similarity" : 35.59322,
+      "maximum_similarity" : 37.5,
+      "matched_token_number" : 21
+    },
+    "SortAlgo1_6-SortAlgo2_5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo1_4-SortAlgo4d1" : {
+      "minimal_similarity" : 36.50794,
+      "maximum_similarity" : 38.98305,
+      "matched_token_number" : 23
+    },
+    "SortAlgo1_5-SortAlgo4d3" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo1_4-SortAlgo4d2" : {
+      "minimal_similarity" : 35.9375,
+      "maximum_similarity" : 38.98305,
+      "matched_token_number" : 23
+    },
+    "SortAlgo1_5-SortAlgo4d2" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo1_4-SortAlgo4d3" : {
+      "minimal_similarity" : 37.096775,
+      "maximum_similarity" : 38.98305,
+      "matched_token_number" : 23
+    },
+    "SortAlgo1_5-SortAlgo4d1" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo4d2-SortAlgo4d3" : {
+      "minimal_similarity" : 93.75,
+      "maximum_similarity" : 96.77419,
+      "matched_token_number" : 60
+    },
+    "SortAlgo2-SortAlgo7" : {
+      "minimal_similarity" : 82.14286,
+      "maximum_similarity" : 85.18519,
+      "matched_token_number" : 46
+    },
+    "SortAlgo1_2-SortAlgo4d3" : {
+      "minimal_similarity" : 57.894737,
+      "maximum_similarity" : 70.96774,
+      "matched_token_number" : 44
+    },
+    "SortAlgo1_3-SortAlgo4d1" : {
+      "minimal_similarity" : 53.333332,
+      "maximum_similarity" : 63.49206,
+      "matched_token_number" : 40
+    },
+    "SortAlgo2-SortAlgo5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo4d1-SortAlgo4d2" : {
+      "minimal_similarity" : 98.4375,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 63
+    },
+    "SortAlgo2-SortAlgo6" : {
+      "minimal_similarity" : 43.28358,
+      "maximum_similarity" : 51.785713,
+      "matched_token_number" : 29
+    },
+    "SortAlgo4d1-SortAlgo4d3" : {
+      "minimal_similarity" : 95.2381,
+      "maximum_similarity" : 96.77419,
+      "matched_token_number" : 60
+    },
+    "SortAlgo1_6-SortAlgo7" : {
+      "minimal_similarity" : 56.666668,
+      "maximum_similarity" : 62.962963,
+      "matched_token_number" : 34
+    },
+    "SortAlgo2_5-SortAlgo3_5" : {
+      "minimal_similarity" : 59.420288,
+      "maximum_similarity" : 73.21429,
+      "matched_token_number" : 41
+    },
+    "SortAlgo3_6-SortAlgo6" : {
+      "minimal_similarity" : 73.333336,
+      "maximum_similarity" : 82.089554,
+      "matched_token_number" : 55
+    },
+    "SortAlgo2_5-SortAlgo3_6" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo3_6-SortAlgo5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo3_6-SortAlgo7" : {
+      "minimal_similarity" : 32.0,
+      "maximum_similarity" : 44.444443,
+      "matched_token_number" : 24
+    },
+    "SortAlgo4d3-SortAlgo6" : {
+      "minimal_similarity" : 34.328358,
+      "maximum_similarity" : 37.096775,
+      "matched_token_number" : 23
+    },
+    "SortAlgo4d3-SortAlgo7" : {
+      "minimal_similarity" : 64.51613,
+      "maximum_similarity" : 74.07407,
+      "matched_token_number" : 40
+    },
+    "SortAlgo4d3-SortAlgo5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo1_4-SortAlgo2_5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo1-SortAlgo4" : {
+      "minimal_similarity" : 35.59322,
+      "maximum_similarity" : 37.5,
+      "matched_token_number" : 21
+    },
+    "SortAlgo1-SortAlgo5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo1-SortAlgo2" : {
+      "minimal_similarity" : 100.0,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo3_6-SortAlgo4" : {
+      "minimal_similarity" : 24.0,
+      "maximum_similarity" : 30.508474,
+      "matched_token_number" : 18
+    },
+    "SortAlgo1-SortAlgo3" : {
+      "minimal_similarity" : 57.971016,
+      "maximum_similarity" : 71.42857,
+      "matched_token_number" : 40
+    },
+    "SortAlgo1_3-SortAlgo2_5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo1_6-SortAlgo2" : {
+      "minimal_similarity" : 56.666668,
+      "maximum_similarity" : 60.714287,
+      "matched_token_number" : 34
+    },
+    "SortAlgo1-SortAlgo6" : {
+      "minimal_similarity" : 43.28358,
+      "maximum_similarity" : 51.785713,
+      "matched_token_number" : 29
+    },
+    "SortAlgo1-SortAlgo7" : {
+      "minimal_similarity" : 82.14286,
+      "maximum_similarity" : 85.18519,
+      "matched_token_number" : 46
+    },
+    "SortAlgo1_2-SortAlgo2_5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo-SortAlgo2_5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo1_6-SortAlgo6" : {
+      "minimal_similarity" : 82.089554,
+      "maximum_similarity" : 91.666664,
+      "matched_token_number" : 55
+    },
+    "SortAlgo1_6-SortAlgo5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo1_6-SortAlgo4" : {
+      "minimal_similarity" : 35.0,
+      "maximum_similarity" : 35.59322,
+      "matched_token_number" : 21
+    },
+    "SortAlgo1_6-SortAlgo3" : {
+      "minimal_similarity" : 34.782608,
+      "maximum_similarity" : 40.0,
+      "matched_token_number" : 24
+    },
+    "SortAlgo1_5-SortAlgo6" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo1_5-SortAlgo7" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo2_5-SortAlgo7" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo1-SortAlgo4d3" : {
+      "minimal_similarity" : 70.96774,
+      "maximum_similarity" : 78.57143,
+      "matched_token_number" : 44
+    },
+    "SortAlgo1_5-SortAlgo4" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo1-SortAlgo4d2" : {
+      "minimal_similarity" : 68.75,
+      "maximum_similarity" : 78.57143,
+      "matched_token_number" : 44
+    },
+    "SortAlgo1_5-SortAlgo5" : {
+      "minimal_similarity" : 100.0,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1-SortAlgo4d1" : {
+      "minimal_similarity" : 69.84127,
+      "maximum_similarity" : 78.57143,
+      "matched_token_number" : 44
+    },
+    "SortAlgo1_5-SortAlgo2" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo3_5-SortAlgo6" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo1_5-SortAlgo3" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo3_5-SortAlgo7" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo3-SortAlgo4d1" : {
+      "minimal_similarity" : 56.52174,
+      "maximum_similarity" : 61.904762,
+      "matched_token_number" : 39
+    },
+    "SortAlgo3-SortAlgo4d3" : {
+      "minimal_similarity" : 56.52174,
+      "maximum_similarity" : 62.903225,
+      "matched_token_number" : 39
+    },
+    "SortAlgo3-SortAlgo4d2" : {
+      "minimal_similarity" : 56.52174,
+      "maximum_similarity" : 60.9375,
+      "matched_token_number" : 39
+    },
+    "SortAlgo2-SortAlgo4d3" : {
+      "minimal_similarity" : 70.96774,
+      "maximum_similarity" : 78.57143,
+      "matched_token_number" : 44
+    },
+    "SortAlgo2-SortAlgo4d1" : {
+      "minimal_similarity" : 69.84127,
+      "maximum_similarity" : 78.57143,
+      "matched_token_number" : 44
+    },
+    "SortAlgo1_5-SortAlgo1_6" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo2-SortAlgo4d2" : {
+      "minimal_similarity" : 68.75,
+      "maximum_similarity" : 78.57143,
+      "matched_token_number" : 44
+    },
+    "SortAlgo3_6-SortAlgo4d2" : {
+      "minimal_similarity" : 30.666666,
+      "maximum_similarity" : 35.9375,
+      "matched_token_number" : 23
+    },
+    "SortAlgo4d1-SortAlgo7" : {
+      "minimal_similarity" : 63.49206,
+      "maximum_similarity" : 74.07407,
+      "matched_token_number" : 40
+    },
+    "SortAlgo3_6-SortAlgo4d1" : {
+      "minimal_similarity" : 30.666666,
+      "maximum_similarity" : 36.50794,
+      "matched_token_number" : 23
+    },
+    "SortAlgo4d1-SortAlgo6" : {
+      "minimal_similarity" : 34.328358,
+      "maximum_similarity" : 36.50794,
+      "matched_token_number" : 23
+    },
+    "SortAlgo4-SortAlgo4d1" : {
+      "minimal_similarity" : 28.571428,
+      "maximum_similarity" : 30.508474,
+      "matched_token_number" : 18
+    },
+    "SortAlgo3_6-SortAlgo4d3" : {
+      "minimal_similarity" : 30.666666,
+      "maximum_similarity" : 37.096775,
+      "matched_token_number" : 23
+    },
+    "SortAlgo4-SortAlgo4d2" : {
+      "minimal_similarity" : 28.125,
+      "maximum_similarity" : 30.508474,
+      "matched_token_number" : 18
+    },
+    "SortAlgo4-SortAlgo4d3" : {
+      "minimal_similarity" : 29.032259,
+      "maximum_similarity" : 30.508474,
+      "matched_token_number" : 18
+    },
+    "SortAlgo3_5-SortAlgo4" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo3_5-SortAlgo4d2" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo4d1-SortAlgo5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo3_5-SortAlgo4d3" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo3_5-SortAlgo5" : {
+      "minimal_similarity" : 59.420288,
+      "maximum_similarity" : 73.21429,
+      "matched_token_number" : 41
+    },
+    "SortAlgo2_5-SortAlgo4" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo2_5-SortAlgo3" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo3_5-SortAlgo4d1" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo2_5-SortAlgo6" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo2_5-SortAlgo5" : {
+      "minimal_similarity" : 100.0,
+      "maximum_similarity" : 100.0,
+      "matched_token_number" : 56
+    },
+    "SortAlgo1-SortAlgo1_3" : {
+      "minimal_similarity" : 53.333332,
+      "maximum_similarity" : 71.42857,
+      "matched_token_number" : 40
+    },
+    "SortAlgo1-SortAlgo1_2" : {
+      "minimal_similarity" : 59.210526,
+      "maximum_similarity" : 80.35714,
+      "matched_token_number" : 45
+    },
+    "SortAlgo1-SortAlgo1_6" : {
+      "minimal_similarity" : 56.666668,
+      "maximum_similarity" : 60.714287,
+      "matched_token_number" : 34
+    },
+    "SortAlgo1-SortAlgo1_5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo1-SortAlgo1_4" : {
+      "minimal_similarity" : 52.542374,
+      "maximum_similarity" : 55.357143,
+      "matched_token_number" : 31
+    },
+    "SortAlgo2_5-SortAlgo4d1" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo3-SortAlgo7" : {
+      "minimal_similarity" : 57.971016,
+      "maximum_similarity" : 74.07407,
+      "matched_token_number" : 40
+    },
+    "SortAlgo2_5-SortAlgo4d2" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo3-SortAlgo6" : {
+      "minimal_similarity" : 52.173912,
+      "maximum_similarity" : 53.731342,
+      "matched_token_number" : 36
+    },
+    "SortAlgo1_4-SortAlgo1_5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo3-SortAlgo5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo1_4-SortAlgo1_6" : {
+      "minimal_similarity" : 51.666668,
+      "maximum_similarity" : 52.542374,
+      "matched_token_number" : 31
+    },
+    "SortAlgo3-SortAlgo4" : {
+      "minimal_similarity" : 26.086956,
+      "maximum_similarity" : 30.508474,
+      "matched_token_number" : 18
+    },
+    "SortAlgo1_3-SortAlgo1_5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo1_3-SortAlgo1_4" : {
+      "minimal_similarity" : 32.0,
+      "maximum_similarity" : 40.677967,
+      "matched_token_number" : 24
+    },
+    "SortAlgo2_5-SortAlgo4d3" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo1_3-SortAlgo1_6" : {
+      "minimal_similarity" : 32.0,
+      "maximum_similarity" : 40.0,
+      "matched_token_number" : 24
+    },
+    "SortAlgo-SortAlgo3_6" : {
+      "minimal_similarity" : 32.0,
+      "maximum_similarity" : 42.857143,
+      "matched_token_number" : 24
+    },
+    "SortAlgo1_2-SortAlgo1_5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo-SortAlgo3_5" : {
+      "minimal_similarity" : 0.0,
+      "maximum_similarity" : 0.0,
+      "matched_token_number" : 0
+    },
+    "SortAlgo1_2-SortAlgo1_6" : {
+      "minimal_similarity" : 44.736843,
+      "maximum_similarity" : 56.666668,
+      "matched_token_number" : 34
+    },
+    "SortAlgo1_2-SortAlgo1_3" : {
+      "minimal_similarity" : 52.63158,
+      "maximum_similarity" : 53.333332,
+      "matched_token_number" : 40
+    },
+    "SortAlgo1_2-SortAlgo1_4" : {
+      "minimal_similarity" : 40.789474,
+      "maximum_similarity" : 52.542374,
+      "matched_token_number" : 31
+    }
+  }
+} ]

+ 6 - 0
jplag.frontend-utils/pom.xml

@@ -8,4 +8,10 @@
         <version>${revision}</version>
     </parent>
     <artifactId>frontend-utils</artifactId>
+    <dependencies>
+        <dependency>
+            <groupId>org.kohsuke.metainf-services</groupId>
+            <artifactId>metainf-services</artifactId>
+        </dependency>
+    </dependencies>
 </project>

+ 2 - 2
jplag.frontend-utils/src/main/java/de/jplag/Language.java

@@ -18,9 +18,9 @@ public interface Language {
     String getName();
 
     /**
-     * Short name of the language used for CLI options.
+     * Identifier of the language used for CLI options and dynamic loading. You should use some name within {@code [a-z_-]+}
      */
-    String getShortName();
+    String getIdentifier();
 
     /**
      * Minimum number of tokens required for a match.

+ 75 - 0
jplag.frontend-utils/src/main/java/de/jplag/LanguageLoader.java

@@ -0,0 +1,75 @@
+package de.jplag;
+
+import java.util.*;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class contains methods to load {@link Language Languages}.
+ * @author Dominik Fuchss
+ */
+public final class LanguageLoader {
+    private static final Logger logger = LoggerFactory.getLogger(LanguageLoader.class);
+
+    private static Map<String, Language> cachedLanguageInstances = null;
+
+    private LanguageLoader() {
+        throw new IllegalAccessError();
+    }
+
+    /**
+     * Get all languages that are currently in the classpath. The languages will be cached. Use {@link #clearCache()} to
+     * obtain new instances.
+     * @return the languages as unmodifiable map from identifier to language instance.
+     */
+    public static synchronized Map<String, Language> getAllAvailableLanguages() {
+        if (cachedLanguageInstances != null)
+            return cachedLanguageInstances;
+
+        Map<String, Language> languages = new TreeMap<>();
+
+        for (Language language : ServiceLoader.load(Language.class)) {
+            String languageIdentifier = language.getIdentifier();
+            if (languages.containsKey(languageIdentifier)) {
+                logger.error("Multiple implementations for a language '{}' are present in the classpath! Skipping ..", languageIdentifier);
+                languages.remove(languageIdentifier);
+                continue;
+            }
+            logger.info("Loading Language Frontend '{}'", language.getName());
+            languages.put(languageIdentifier, language);
+        }
+
+        cachedLanguageInstances = Collections.unmodifiableMap(languages);
+        return cachedLanguageInstances;
+    }
+
+    /**
+     * Load a language that is currently in the classpath by its short name.
+     * @param identifier the identifier of the language
+     * @return the language or an empty optional if no language has been found.
+     * @see Language#getIdentifier()
+     */
+    public static Optional<Language> getLanguage(String identifier) {
+        var language = getAllAvailableLanguages().get(identifier);
+        if (language == null)
+            logger.warn("Attempt to load Language {} was not successful", identifier);
+        return Optional.ofNullable(language);
+    }
+
+    /**
+     * Get an unmodifiable set of all available languages with their identifiers.
+     * @return identifiers of all available languages
+     * @see Language#getIdentifier()
+     */
+    public static Set<String> getAllAvailableLanguageIdentifiers() {
+        return new TreeSet<>(getAllAvailableLanguages().keySet());
+    }
+
+    /**
+     * Resets the internal cache of all loaded languages
+     */
+    public static synchronized void clearCache() {
+        cachedLanguageInstances = null;
+    }
+}

+ 7 - 2
jplag.frontend.cpp/src/main/java/de/jplag/cpp/Language.java

@@ -2,9 +2,14 @@ package de.jplag.cpp;
 
 import java.io.File;
 
+import org.kohsuke.MetaInfServices;
+
 import de.jplag.TokenList;
 
+@MetaInfServices(de.jplag.Language.class)
 public class Language implements de.jplag.Language {
+    public static final String IDENTIFIER = "cpp";
+
     private final Scanner scanner; // cpp code is scanned not parsed
 
     public Language() {
@@ -22,8 +27,8 @@ public class Language implements de.jplag.Language {
     }
 
     @Override
-    public String getShortName() {
-        return "cpp";
+    public String getIdentifier() {
+        return IDENTIFIER;
     }
 
     @Override

+ 6 - 3
jplag.frontend.csharp-6/src/main/java/de/jplag/csharp/Language.java

@@ -3,15 +3,18 @@ package de.jplag.csharp;
 import java.io.File;
 import java.util.Arrays;
 
+import org.kohsuke.MetaInfServices;
+
 import de.jplag.TokenList;
 
 /**
  * C# language with full support of C# 6 features and below.
  * @author Timur Saglam
  */
+@MetaInfServices(de.jplag.Language.class)
 public class Language implements de.jplag.Language {
     private static final String NAME = "C# 6 Parser";
-    private static final String SHORT_NAME = "C# 6";
+    private static final String IDENTIFIER = "csharp";
     private static final String[] FILE_ENDINGS = new String[] {".cs", ".CS"};
     private static final int DEFAULT_MIN_TOKEN_MATCH = 8;
 
@@ -32,8 +35,8 @@ public class Language implements de.jplag.Language {
     }
 
     @Override
-    public String getShortName() {
-        return SHORT_NAME;
+    public String getIdentifier() {
+        return IDENTIFIER;
     }
 
     @Override

+ 6 - 3
jplag.frontend.emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/Language.java

@@ -1,5 +1,7 @@
 package de.jplag.emf.dynamic;
 
+import org.kohsuke.MetaInfServices;
+
 import de.jplag.emf.dynamic.parser.DynamicEcoreParser;
 
 /**
@@ -7,9 +9,10 @@ import de.jplag.emf.dynamic.parser.DynamicEcoreParser;
  * created token set instead of a hand-picked one.
  * @author Timur Saglam
  */
+@MetaInfServices(de.jplag.Language.class)
 public class Language extends de.jplag.emf.Language {
     private static final String NAME = "EMF metamodels (dynamically created token set)";
-    private static final String SHORT_NAME = "EMF metamodel (dynamic)";
+    public static final String IDENTIFIER = "emf-metamodel-dynamic";
 
     private static final int DEFAULT_MIN_TOKEN_MATCH = 10;
 
@@ -23,8 +26,8 @@ public class Language extends de.jplag.emf.Language {
     }
 
     @Override
-    public String getShortName() {
-        return SHORT_NAME;
+    public String getIdentifier() {
+        return IDENTIFIER;
     }
 
     @Override

+ 5 - 3
jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/Language.java

@@ -4,6 +4,7 @@ import java.io.File;
 import java.util.Arrays;
 
 import org.eclipse.emf.ecore.EcorePackage;
+import org.kohsuke.MetaInfServices;
 
 import de.jplag.TokenList;
 import de.jplag.emf.parser.EcoreParser;
@@ -12,12 +13,13 @@ import de.jplag.emf.parser.EcoreParser;
  * Language for EMF metamodels from the Eclipse Modeling Framework (EMF).
  * @author Timur Saglam
  */
+@MetaInfServices(de.jplag.Language.class)
 public class Language implements de.jplag.Language {
     public static final String VIEW_FILE_SUFFIX = ".TreeView";
     public static final String FILE_ENDING = "." + EcorePackage.eNAME;
 
     private static final String NAME = "EMF metamodel";
-    private static final String SHORT_NAME = "EMF metamodel";
+    public static final String IDENTIFIER = "emf-metamodel";
     private static final int DEFAULT_MIN_TOKEN_MATCH = 6;
 
     protected final EcoreParser parser;
@@ -41,8 +43,8 @@ public class Language implements de.jplag.Language {
     }
 
     @Override
-    public String getShortName() {
-        return SHORT_NAME;
+    public String getIdentifier() {
+        return IDENTIFIER;
     }
 
     @Override

+ 6 - 3
jplag.frontend.golang/src/main/java/de/jplag/golang/Language.java

@@ -2,12 +2,15 @@ package de.jplag.golang;
 
 import java.io.File;
 
+import org.kohsuke.MetaInfServices;
+
 import de.jplag.TokenList;
 
+@MetaInfServices(de.jplag.Language.class)
 public class Language implements de.jplag.Language {
 
     private static final String NAME = "Go Parser";
-    private static final String SHORT_NAME = "Go";
+    public static final String IDENTIFIER = "go";
     private static final int DEFAULT_MIN_TOKEN_MATCH = 8;
     private static final String[] FILE_EXTENSIONS = {".go"};
     private final GoParserAdapter parserAdapter;
@@ -27,8 +30,8 @@ public class Language implements de.jplag.Language {
     }
 
     @Override
-    public String getShortName() {
-        return SHORT_NAME;
+    public String getIdentifier() {
+        return IDENTIFIER;
     }
 
     @Override

+ 19 - 10
jplag.frontend.java/src/main/java/de/jplag/java/JavacAdapter.java

@@ -5,6 +5,7 @@ import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Paths;
 import java.util.Collections;
+import java.util.List;
 
 import javax.tools.*;
 import javax.tools.JavaCompiler.CompilationTask;
@@ -24,17 +25,25 @@ public class JavacAdapter {
     private static final JavaCompiler javac = ToolProvider.getSystemJavaCompiler();
 
     public int parseFiles(File directory, Iterable<File> pathedFiles, final Parser parser) {
-        final StandardJavaFileManager fileManager = javac.getStandardFileManager(null, null, StandardCharsets.UTF_8);
         var listener = new DiagnosticCollector<>();
-        var javaFiles = fileManager.getJavaFileObjectsFromFiles(pathedFiles);
-        final CompilationTask task = javac.getTask(null, fileManager, listener, null, null, javaFiles);
-        final Trees trees = Trees.instance(task);
-        final SourcePositions positions = trees.getSourcePositions();
-        for (final CompilationUnitTree ast : executeCompilationTask(task)) {
-            final String filename = fileNameOf(directory, ast);
-            final LineMap map = ast.getLineMap();
-            ast.accept(new TokenGeneratingTreeScanner(filename, parser, map, positions, ast), null);
-            parser.add(TokenConstants.FILE_END, filename, 1, -1, -1);
+
+        try (final StandardJavaFileManager fileManager = javac.getStandardFileManager(listener, null, StandardCharsets.UTF_8)) {
+            var javaFiles = fileManager.getJavaFileObjectsFromFiles(pathedFiles);
+
+            // We need to disable annotation processing
+            // See
+            // https://stackoverflow.com/questions/72737445/system-java-compiler-behaves-different-depending-on-dependencies-defined-in-mave
+            final CompilationTask task = javac.getTask(null, fileManager, listener, List.of("-proc:none"), null, javaFiles);
+            final Trees trees = Trees.instance(task);
+            final SourcePositions positions = trees.getSourcePositions();
+            for (final CompilationUnitTree ast : executeCompilationTask(task)) {
+                final String filename = fileNameOf(directory, ast);
+                final LineMap map = ast.getLineMap();
+                ast.accept(new TokenGeneratingTreeScanner(filename, parser, map, positions, ast), null);
+                parser.add(TokenConstants.FILE_END, filename, 1, -1, -1);
+            }
+        } catch (IOException e) {
+            parser.logger.error(e.getMessage(), e);
         }
         return processErrors(parser.logger, listener);
     }

+ 7 - 2
jplag.frontend.java/src/main/java/de/jplag/java/Language.java

@@ -2,12 +2,17 @@ package de.jplag.java;
 
 import java.io.File;
 
+import org.kohsuke.MetaInfServices;
+
 import de.jplag.TokenList;
 
 /**
  * Language for Java 9 and newer.
  */
+@MetaInfServices(de.jplag.Language.class)
 public class Language implements de.jplag.Language {
+    public static final String IDENTIFIER = "java";
+
     private final Parser parser;
 
     public Language() {
@@ -25,8 +30,8 @@ public class Language implements de.jplag.Language {
     }
 
     @Override
-    public String getShortName() {
-        return "java";
+    public String getIdentifier() {
+        return IDENTIFIER;
     }
 
     @Override

+ 6 - 3
jplag.frontend.kotlin/src/main/java/de/jplag/kotlin/Language.java

@@ -2,15 +2,18 @@ package de.jplag.kotlin;
 
 import java.io.File;
 
+import org.kohsuke.MetaInfServices;
+
 import de.jplag.TokenList;
 
 /**
  * This represents the Kotlin language as a language supported by JPlag.
  */
+@MetaInfServices(de.jplag.Language.class)
 public class Language implements de.jplag.Language {
 
     private static final String NAME = "Kotlin Parser";
-    private static final String SHORT_NAME = "Kotlin";
+    public static final String IDENTIFIER = "kotlin";
     private static final int DEFAULT_MIN_TOKEN_MATCH = 8;
     private static final String[] FILE_EXTENSIONS = {".kt"};
     private final KotlinParserAdapter parserAdapter;
@@ -30,8 +33,8 @@ public class Language implements de.jplag.Language {
     }
 
     @Override
-    public String getShortName() {
-        return SHORT_NAME;
+    public String getIdentifier() {
+        return IDENTIFIER;
     }
 
     @Override

+ 7 - 2
jplag.frontend.python-3/src/main/java/de/jplag/python3/Language.java

@@ -2,10 +2,15 @@ package de.jplag.python3;
 
 import java.io.File;
 
+import org.kohsuke.MetaInfServices;
+
 import de.jplag.TokenList;
 
+@MetaInfServices(de.jplag.Language.class)
 public class Language implements de.jplag.Language {
 
+    public static final String IDENTIFIER = "python3";
+
     private final Parser parser;
 
     public Language() {
@@ -23,8 +28,8 @@ public class Language implements de.jplag.Language {
     }
 
     @Override
-    public String getShortName() {
-        return "python3";
+    public String getIdentifier() {
+        return IDENTIFIER;
     }
 
     @Override

+ 6 - 3
jplag.frontend.rlang/src/main/java/de/jplag/rlang/Language.java

@@ -2,15 +2,18 @@ package de.jplag.rlang;
 
 import java.io.File;
 
+import org.kohsuke.MetaInfServices;
+
 import de.jplag.TokenList;
 
 /**
  * This represents the R language as a language supported by JPlag.
  */
+@MetaInfServices(de.jplag.Language.class)
 public class Language implements de.jplag.Language {
 
     private static final String NAME = "R Parser";
-    private static final String SHORT_NAME = "R";
+    public static final String IDENTIFIER = "rlang";
     private static final int DEFAULT_MIN_TOKEN_MATCH = 8;
     private static final String[] FILE_EXTENSION = {".R", ".r"};
     private final RParserAdapter parserAdapter;
@@ -30,8 +33,8 @@ public class Language implements de.jplag.Language {
     }
 
     @Override
-    public String getShortName() {
-        return SHORT_NAME;
+    public String getIdentifier() {
+        return IDENTIFIER;
     }
 
     @Override

+ 3 - 3
jplag.frontend.rust/src/main/java/de/jplag/rust/Language.java

@@ -8,7 +8,7 @@ public class Language implements de.jplag.Language {
 
     protected static final String[] FILE_EXTENSIONS = {".rs"};
     public static final String NAME = "Rust frontend";
-    public static final String SHORT_NAME = "Rust";
+    public static final String IDENTIFIER = "rust";
     public static final int MINIMUM_TOKEN_MATCH = 8;
 
     private final RustParserAdapter parserAdapter;
@@ -28,8 +28,8 @@ public class Language implements de.jplag.Language {
     }
 
     @Override
-    public String getShortName() {
-        return SHORT_NAME;
+    public String getIdentifier() {
+        return IDENTIFIER;
     }
 
     @Override

+ 1 - 0
jplag.frontend.scala/src/main/resources/META-INF/services/de.jplag.Language

@@ -0,0 +1 @@
+de.jplag.scala.Language

+ 3 - 1
jplag.frontend.scala/src/main/scala/de/jplag/scala/Language.scala

@@ -4,6 +4,8 @@ import de.jplag.TokenList
 
 import java.io.File
 
+import org.kohsuke.MetaInfServices
+
 class Language extends de.jplag.Language {
   private val parser = new Parser
   private final val fileExtensions = Array(".scala", ".sc")
@@ -12,7 +14,7 @@ class Language extends de.jplag.Language {
 
   override def getName = "Scala parser"
 
-  override def getShortName = "scala"
+  override def getIdentifier = "scala"
 
   override def minimumTokenMatch = 8
 

+ 7 - 2
jplag.frontend.scheme/src/main/java/de/jplag/scheme/Language.java

@@ -2,9 +2,14 @@ package de.jplag.scheme;
 
 import java.io.File;
 
+import org.kohsuke.MetaInfServices;
+
 import de.jplag.TokenList;
 
+@MetaInfServices(de.jplag.Language.class)
 public class Language implements de.jplag.Language {
+
+    public static final String IDENTIFIER = "scheme";
     private final de.jplag.scheme.Parser parser;
 
     public Language() {
@@ -22,8 +27,8 @@ public class Language implements de.jplag.Language {
     }
 
     @Override
-    public String getShortName() {
-        return "scheme";
+    public String getIdentifier() {
+        return IDENTIFIER;
     }
 
     @Override

+ 6 - 7
jplag.frontend.text/src/main/java/de/jplag/text/Language.java

@@ -2,6 +2,8 @@ package de.jplag.text;
 
 import java.io.File;
 
+import org.kohsuke.MetaInfServices;
+
 import de.jplag.TokenList;
 
 /**
@@ -9,8 +11,10 @@ import de.jplag.TokenList;
  * individual words are interpreted as token types. Whitespace and special characters are ignored. This approach works,
  * but there are better approaches for text plagiarism out there (based on NLP techniques).
  */
+@MetaInfServices(de.jplag.Language.class)
 public class Language implements de.jplag.Language {
 
+    public static final String IDENTIFIER = "text";
     private final ParserAdapter parserAdapter;
 
     public Language() {
@@ -28,8 +32,8 @@ public class Language implements de.jplag.Language {
     }
 
     @Override
-    public String getShortName() {
-        return "text";
+    public String getIdentifier() {
+        return IDENTIFIER;
     }
 
     @Override
@@ -46,9 +50,4 @@ public class Language implements de.jplag.Language {
     public boolean hasErrors() {
         return parserAdapter.hasErrors();
     }
-
-    @Override
-    public boolean isPreformatted() {
-        return false;
-    }
 }

+ 11 - 19
jplag/pom.xml

@@ -25,53 +25,45 @@
             <groupId>de.jplag</groupId>
             <artifactId>frontend-utils</artifactId>
         </dependency>
-        <dependency>
-            <groupId>de.jplag</groupId>
-            <artifactId>text</artifactId>
-        </dependency>
         <dependency>
             <groupId>de.jplag</groupId>
             <artifactId>java</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>de.jplag</groupId>
-            <artifactId>python-3</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>de.jplag</groupId>
-            <artifactId>csharp-6</artifactId>
+            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>de.jplag</groupId>
             <artifactId>cpp</artifactId>
+            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>de.jplag</groupId>
-            <artifactId>golang</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>de.jplag</groupId>
-            <artifactId>kotlin</artifactId>
+            <artifactId>rlang</artifactId>
+            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>de.jplag</groupId>
-            <artifactId>rlang</artifactId>
+            <artifactId>golang</artifactId>
+            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>de.jplag</groupId>
             <artifactId>scala</artifactId>
+            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>de.jplag</groupId>
-            <artifactId>scheme</artifactId>
+            <artifactId>kotlin</artifactId>
+            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>de.jplag</groupId>
             <artifactId>emf-metamodel</artifactId>
+            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>de.jplag</groupId>
             <artifactId>emf-metamodel-dynamic</artifactId>
+            <scope>test</scope>
         </dependency>
     </dependencies>
 </project>

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

+ 7 - 61
jplag/src/main/java/de/jplag/JPlag.java

@@ -1,17 +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.lang.reflect.Constructor;
-import java.lang.reflect.InvocationTargetException;
-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;
 
@@ -19,7 +7,6 @@ import de.jplag.clustering.ClusteringFactory;
 import de.jplag.exceptions.ExitException;
 import de.jplag.exceptions.SubmissionException;
 import de.jplag.options.JPlagOptions;
-import de.jplag.options.LanguageOption;
 import de.jplag.strategy.ComparisonMode;
 import de.jplag.strategy.ComparisonStrategy;
 import de.jplag.strategy.NormalComparisonStrategy;
@@ -36,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.
@@ -45,30 +31,8 @@ public class JPlag {
     public JPlag(JPlagOptions options) {
         this.options = options;
         coreAlgorithm = new GreedyStringTiling(options);
-        language = initializeLanguage();
-        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());
     }
 
     /**
@@ -78,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,9 +56,10 @@ public class JPlag {
 
         // Compare valid submissions.
         JPlagResult result = comparisonStrategy.compareSubmissions(submissionSet);
-        logger.info("\nTotal time for comparing submissions: " + TimeUtil.formatDuration(result.getDuration()));
+        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;
     }
@@ -105,23 +70,4 @@ public class JPlag {
             case PARALLEL -> new ParallelComparisonStrategy(options, coreAlgorithm);
         };
     }
-
-    private Language initializeLanguage() {
-        LanguageOption languageOption = this.options.getLanguageOption();
-
-        try {
-            Constructor<?> constructor = Class.forName(languageOption.getClassPath()).getConstructor();
-            Language language = (Language) constructor.newInstance();
-
-            this.options.setLanguage(language);
-            this.options.setLanguageDefaults(language);
-            logger.info("Initialized language {}", language.getName());
-            return language;
-        } catch (NoSuchMethodException | SecurityException | ClassNotFoundException | InstantiationException | IllegalAccessException
-                | IllegalArgumentException | InvocationTargetException e) {
-            logger.error(e.getMessage(), e);
-            throw new IllegalStateException("Language instantiation failed:" + e.getMessage(), e);
-        }
-
-    }
 }

+ 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().getLanguageOption(), submissions.numberOfSubmissions());
+                getDuration(), getOptions().language().getName(), submissions.numberOfSubmissions());
     }
 
     /**

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

@@ -218,7 +218,7 @@ public class Submission implements Comparable<Submission> {
     private void copySubmission() {
         File rootDirectory = submissionRootFile.getParentFile();
         assert rootDirectory != null;
-        File submissionDirectory = createSubdirectory(rootDirectory, ERROR_FOLDER, language.getShortName(), name);
+        File submissionDirectory = createSubdirectory(rootDirectory, ERROR_FOLDER, language.getIdentifier(), name);
         for (File file : files) {
             try {
                 Files.copy(file.toPath(), new File(submissionDirectory, file.getName()).toPath());

+ 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 - 241
jplag/src/main/java/de/jplag/options/JPlagOptions.java

@@ -1,323 +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 {
-
-    private static final Logger logger = LoggerFactory.getLogger("JPlag");
+/**
+ * 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) {
 
     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;
 
-    /**
-     * 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;
-
-    /**
-     * Language to use when parsing the submissions.
-     */
-    private LanguageOption languageOption;
+    private static final Logger logger = LoggerFactory.getLogger(JPlag.class);
 
-    /**
-     * Level of output verbosity.
-     */
-    private Verbosity verbosity;
-
-    /**
-     * Clustering options
-     */
-    private ClusteringOptions clusteringOptions = new ClusteringOptions.Builder().build();
-
-    /**
-     * Constructor with required attributes.
-     */
-    public JPlagOptions(List<String> submissionDirectories, List<String> oldSubmissionDirectories, LanguageOption languageOption) {
-        this.submissionDirectories = submissionDirectories;
-        this.oldSubmissionDirectories = oldSubmissionDirectories;
-        this.languageOption = languageOption;
-    }
-
-    public Optional<String> getBaseCodeSubmissionName() {
-        return Optional.ofNullable(baseCodeSubmissionName);
-    }
-
-    public ComparisonMode getComparisonMode() {
-        return comparisonMode;
-    }
-
-    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 Language getLanguage() {
-        return language;
-    }
-
-    public LanguageOption getLanguageOption() {
-        return languageOption;
+    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();
-        }
-    }
-
-    public void setLanguageOption(LanguageOption languageOption) {
-        this.languageOption = languageOption;
+    @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;
-    }
-
 }

+ 0 - 53
jplag/src/main/java/de/jplag/options/LanguageOption.java

@@ -1,53 +0,0 @@
-package de.jplag.options;
-
-import java.util.Arrays;
-import java.util.Collection;
-
-/**
- * The available languages.
- */
-public enum LanguageOption {
-    JAVA("java", "de.jplag.java.Language"),
-    PYTHON_3("python3", "de.jplag.python3.Language"),
-    C_CPP("cpp", "de.jplag.cpp.Language"),
-    C_SHARP("csharp", "de.jplag.csharp.Language"),
-    GO_LANG("golang", "de.jplag.golang.Language"),
-    KOTLIN("kotlin", "de.jplag.kotlin.Language"),
-    R_LANG("rlang", "de.jplag.rlang.Language"),
-    RUST("rust", "de.jplag.rust.Language"),
-    SCALA("scala", "de.jplag.scala.Language"),
-    CHAR("char", "de.jplag.chars.Language"),
-    TEXT("text", "de.jplag.text.Language"),
-    SCHEME("scheme", "de.jplag.scheme.Language"),
-    EMF("emf-metamodel", "de.jplag.emf.Language"),
-    EMF_DYNAMIC("emf-metamodel-dynamic", "de.jplag.emf.dynamic.Language");
-
-    private final String classPath;
-    private final String displayName;
-
-    LanguageOption(String displayName, String classPath) {
-        this.displayName = displayName;
-        this.classPath = classPath;
-    }
-
-    public String getClassPath() {
-        return this.classPath;
-    }
-
-    public String getDisplayName() {
-        return this.displayName;
-    }
-
-    public static LanguageOption fromDisplayName(String displayName) {
-        return Arrays.stream(LanguageOption.values()).filter(languageOption -> languageOption.displayName.equalsIgnoreCase(displayName)).findFirst()
-                .orElse(getDefault());
-    }
-
-    public static Collection<String> getAllDisplayNames() {
-        return Arrays.stream(LanguageOption.values()).map(languageOption -> languageOption.displayName).toList();
-    }
-
-    public static LanguageOption getDefault() {
-        return LanguageOption.JAVA;
-    }
-}

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

+ 1 - 1
jplag/src/main/resources/de/jplag/messages.properties

@@ -13,7 +13,7 @@ CommandLineArgument.ShownComparisons=The maximum number of comparisons that will
 CommandLineArgument.SimilarityThreshold=Comparison similarity threshold [0-100]: All comparisons above this threshold will be saved
 CommandLineArgument.Subdirectory=Look in directories <root-dir>/*/<dir> for programs
 CommandLineArgument.Verbosity=Verbosity of the logging
-CommandLineArgument.ClusterEnable=Skips the clustering
+CommandLineArgument.ClusterDisable=Skips the clustering
 CommandLineArgument.ClusterAlgorithm=Which clustering algorithm to use. Agglomerative merges similar submissions bottom up. Spectral clustering is combined with Bayesian Optimization to execute the k-Means clustering algorithm multiple times, hopefully finding a "good" clustering automatically.
 CommandLineArgument.ClusterMetric=The metric used for clustering. AVG is intersection over union, MAX can expose some attempts of obfuscation.
 CommandLineArgument.ClusterSpectralBandwidth=Bandwidth of the matern kernel in the Gaussian Process used during the search for a good number of clusters for spectral clustering. If a good clustering result is found during the search, numbers of clusters that differ by something in range of the bandwidth are also expected to good.

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

+ 3 - 2
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());
@@ -30,7 +30,8 @@ class InvalidSubmissionTest extends TestBase {
             File errorFolder = new File(Path.of(BASE_PATH, SAMPLE_NAME, "errors", "java").toString());
             assertTrue(errorFolder.exists());
             String[] errorSubmissions = errorFolder.list();
-            Arrays.sort(errorSubmissions); // File systems don't promise alphabetical order.
+            if (errorSubmissions != null)
+                Arrays.sort(errorSubmissions); // File systems don't promise alphabetical order.
             deleteDirectory(errorFolder.getParentFile());
             assertArrayEquals(new String[] {"A", "B"}, errorSubmissions);
         }

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

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

@@ -4,11 +4,11 @@ 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;
 import de.jplag.options.JPlagOptions;
-import de.jplag.options.LanguageOption;
 import de.jplag.options.Verbosity;
 
 public abstract class TestBase {
@@ -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, LanguageOption.JAVA);
-        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<>();

+ 23 - 20
jplag/src/test/java/de/jplag/special/TokenPrinterTest.java

@@ -2,66 +2,69 @@ 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;
-import de.jplag.options.LanguageOption;
 
 /**
  * Special test case the does not really test anything but prints the tokens and the corresponding line of code.
  * @author Timur Saglam
  */
-public class TokenPrinterTest extends TestBase {
+class TokenPrinterTest extends TestBase {
     private static final String LARGE_SPACE = "   ";
     private static final String LINE = "------------------";
 
     private static final int MIN_TOKEN_MATCH = 5;
     private static final String PRINTER_FOLDER = "PRINTER"; // in the folder 'jplag/src/test/resources/samples'
 
-    @Disabled
+    private static final String LANGUAGE_CPP = de.jplag.cpp.Language.IDENTIFIER;
+    private static final String LANGUAGE_R = de.jplag.rlang.Language.IDENTIFIER;
+    private static final String LANGUAGE_KOTLIN = de.jplag.kotlin.Language.IDENTIFIER;
+
+    private static final String LANGUAGE_GO = de.jplag.golang.Language.IDENTIFIER;
+
+    @Disabled("Not a meaningful test, used for designing the token set")
     @Test
     void printCPPFiles() {
-        printSubmissions(options -> {
-            options.setLanguageOption(LanguageOption.C_CPP);
-            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
+    @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.setLanguageOption(LanguageOption.R_LANG);
-            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.setLanguageOption(LanguageOption.GO_LANG));
+        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.setLanguageOption(LanguageOption.KOTLIN));
+        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"));
 

+ 8 - 3
pom.xml

@@ -75,7 +75,7 @@
         <maven.compiler.source>17</maven.compiler.source>
         <maven.compiler.target>17</maven.compiler.target>
         <spotless.version>2.25.0</spotless.version>
-        <slf4j.version>1.7.36</slf4j.version>
+        <slf4j.version>2.0.0</slf4j.version>
         <junit.version>5.9.0</junit.version>
 
         <antlr2.version>2.7.7</antlr2.version>
@@ -123,6 +123,11 @@
                 <artifactId>slf4j-simple</artifactId>
                 <version>${slf4j.version}</version>
             </dependency>
+            <dependency>
+                <groupId>org.kohsuke.metainf-services</groupId>
+                <artifactId>metainf-services</artifactId>
+                <version>1.9</version>
+            </dependency>
 
             <!-- JPLAG -->
             <dependency>
@@ -202,7 +207,7 @@
             </dependency>
             <dependency>
                 <groupId>de.jplag</groupId>
-                <artifactId>endToEndTesting</artifactId>
+                <artifactId>endtoend-testing</artifactId>
                 <version>${revision}</version>
             </dependency>
             <dependency>
@@ -482,7 +487,7 @@
                 <module>jplag.frontend.text</module>
                 <module>jplag.frontend.emf-metamodel</module>
                 <module>jplag.frontend.emf-metamodel-dynamic</module>
-                <module>jplag.endToEndTesting</module>
+                <module>jplag.endtoend-testing</module>
                 <module>jplag</module>
                 <module>jplag.cli</module>
             </modules>

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

Vissa filer visades inte eftersom för många filer har ändrats