Browse Source

Merge branch 'master-remote' into scala

Robin Maisch 4 years ago
parent
commit
43a9f4fed5
100 changed files with 1280 additions and 1119 deletions
  1. 1 1
      .github/workflows/maven.yml
  2. 1 1
      .github/workflows/report-viewer.yml
  3. 44 0
      .github/workflows/sonarcloud-branch.yml
  4. 79 0
      .github/workflows/sonarcloud-pr.yml
  5. 6 0
      .gitignore
  6. 57 4
      jplag.cli/pom.xml
  7. 18 5
      jplag.cli/src/main/java/de/jplag/CLI.java
  8. 270 0
      jplag.cli/src/main/java/de/jplag/logger/CollectedLogger.java
  9. 44 0
      jplag.cli/src/main/java/de/jplag/logger/CollectedLoggerFactory.java
  10. 4 0
      jplag.cli/src/main/java/de/jplag/logger/Triple.java
  11. 54 0
      jplag.cli/src/main/java/org/slf4j/impl/StaticLoggerBinder.java
  12. 1 1
      jplag.cli/src/test/java/de/jplag/cli/CommandLineInterfaceTest.java
  13. 0 28
      jplag.frontend-testutils/src/test/java/de/jplag/testutils/TestErrorConsumer.java
  14. 6 16
      jplag.frontend-utils/src/main/java/de/jplag/AbstractParser.java
  15. 0 20
      jplag.frontend-utils/src/main/java/de/jplag/ErrorConsumer.java
  16. 10 15
      jplag.frontend-utils/src/main/java/de/jplag/Language.java
  17. 2 13
      jplag.frontend.chars/src/main/java/de/jplag/chars/Language.java
  18. 7 14
      jplag.frontend.chars/src/main/java/de/jplag/chars/Parser.java
  19. 2 23
      jplag.frontend.cpp/src/main/java/de/jplag/cpp/Language.java
  20. 3 5
      jplag.frontend.cpp/src/main/java/de/jplag/cpp/Scanner.java
  21. 2 2
      jplag.frontend.cpp/src/main/javacc/CPP.jj
  22. 1 1
      jplag.frontend.csharp-6/src/main/java/de/jplag/csharp/CSharpListener.java
  23. 4 6
      jplag.frontend.csharp-6/src/main/java/de/jplag/csharp/CSharpParserAdapter.java
  24. 2 23
      jplag.frontend.csharp-6/src/main/java/de/jplag/csharp/Language.java
  25. 11 7
      jplag.frontend.csharp-6/src/main/java/de/jplag/csharp/grammar/CSharpPreprocessorParserBase.java
  26. 1 3
      jplag.frontend.csharp-6/src/test/java/de/jplag/csharp/MinimalCSharpFrontendTest.java
  27. 1 1
      jplag.frontend.java/src/main/java/de/jplag/java/JavaTokenConstants.java
  28. 6 9
      jplag.frontend.java/src/main/java/de/jplag/java/JavacAdapter.java
  29. 2 23
      jplag.frontend.java/src/main/java/de/jplag/java/Language.java
  30. 3 7
      jplag.frontend.java/src/main/java/de/jplag/java/Parser.java
  31. 3 5
      jplag.frontend.kotlin/src/main/java/de/jplag/kotlin/KotlinParserAdapter.java
  32. 2 23
      jplag.frontend.kotlin/src/main/java/de/jplag/kotlin/Language.java
  33. 3 5
      jplag.frontend.kotlin/src/test/java/de/jplag/kotlin/KotlinFrontendTest.java
  34. 2 23
      jplag.frontend.python-3/src/main/java/de/jplag/python3/Language.java
  35. 7 9
      jplag.frontend.python-3/src/main/java/de/jplag/python3/Parser.java
  36. 2 23
      jplag.frontend.rlang/src/main/java/de/jplag/rlang/Language.java
  37. 3 5
      jplag.frontend.rlang/src/main/java/de/jplag/rlang/RParserAdapter.java
  38. 2 4
      jplag.frontend.rlang/src/test/java/de/jplag/rlang/RFrontendTest.java
  39. 2 23
      jplag.frontend.scheme/src/main/java/de/jplag/scheme/Language.java
  40. 6 8
      jplag.frontend.scheme/src/main/java/de/jplag/scheme/Parser.java
  41. 2 2
      jplag.frontend.scheme/src/main/javacc/Scheme.jj
  42. 1 0
      jplag.frontend.text/src/main/java/de/jplag/text/InputState.java
  43. 2 19
      jplag.frontend.text/src/main/java/de/jplag/text/Language.java
  44. 6 14
      jplag.frontend.text/src/main/java/de/jplag/text/Parser.java
  45. 0 79
      jplag/src/main/java/de/jplag/ErrorCollector.java
  46. 7 6
      jplag/src/main/java/de/jplag/GreedyStringTiling.java
  47. 11 15
      jplag/src/main/java/de/jplag/JPlag.java
  48. 2 1
      jplag/src/main/java/de/jplag/JPlagComparison.java
  49. 27 9
      jplag/src/main/java/de/jplag/JPlagResult.java
  50. 7 42
      jplag/src/main/java/de/jplag/Match.java
  51. 13 10
      jplag/src/main/java/de/jplag/Submission.java
  52. 29 36
      jplag/src/main/java/de/jplag/SubmissionSet.java
  53. 20 11
      jplag/src/main/java/de/jplag/SubmissionSetBuilder.java
  54. 1 3
      jplag/src/main/java/de/jplag/clustering/ClusteringFactory.java
  55. 1 1
      jplag/src/main/java/de/jplag/clustering/ClusteringResult.java
  56. 3 2
      jplag/src/main/java/de/jplag/clustering/IntegerMapping.java
  57. 1 1
      jplag/src/main/java/de/jplag/clustering/PreprocessorHelper.java
  58. 23 51
      jplag/src/main/java/de/jplag/clustering/algorithm/AgglomerativeClustering.java
  59. 2 4
      jplag/src/main/java/de/jplag/clustering/algorithm/BayesianOptimization.java
  60. 1 1
      jplag/src/main/java/de/jplag/clustering/algorithm/GaussianProcess.java
  61. 3 3
      jplag/src/main/java/de/jplag/clustering/algorithm/InterClusterSimilarity.java
  62. 2 2
      jplag/src/main/java/de/jplag/clustering/algorithm/SpectralClustering.java
  63. 2 2
      jplag/src/main/java/de/jplag/exceptions/ExitException.java
  64. 0 21
      jplag/src/main/java/de/jplag/exceptions/ReportGenerationException.java
  65. 2 2
      jplag/src/main/java/de/jplag/options/JPlagOptions.java
  66. 6 6
      jplag/src/main/java/de/jplag/reporting/jsonfactory/JsonFactory.java
  67. 26 39
      jplag/src/main/java/de/jplag/reporting/reportobject/ReportObjectFactory.java
  68. 54 0
      jplag/src/main/java/de/jplag/reporting/reportobject/mapper/MetricMapper.java
  69. 4 44
      jplag/src/main/java/de/jplag/reporting/reportobject/model/ComparisonReport.java
  70. 2 2
      jplag/src/main/java/de/jplag/reporting/reportobject/model/FilesOfSubmission.java
  71. 1 12
      jplag/src/main/java/de/jplag/reporting/reportobject/model/JPlagReport.java
  72. 3 60
      jplag/src/main/java/de/jplag/reporting/reportobject/model/Match.java
  73. 2 36
      jplag/src/main/java/de/jplag/reporting/reportobject/model/Metric.java
  74. 2 29
      jplag/src/main/java/de/jplag/reporting/reportobject/model/TopComparison.java
  75. 3 3
      jplag/src/main/java/de/jplag/strategy/AbstractComparisonStrategy.java
  76. 2 4
      jplag/src/main/java/de/jplag/strategy/ComparisonMode.java
  77. 1 1
      jplag/src/main/java/de/jplag/strategy/NormalComparisonStrategy.java
  78. 16 17
      jplag/src/main/java/de/jplag/strategy/ParallelComparisonStrategy.java
  79. 2 15
      jplag/src/main/java/de/jplag/strategy/SubmissionTuple.java
  80. 3 1
      jplag/src/main/resources/de/jplag/messages.properties
  81. 3 1
      jplag/src/main/resources/de/jplag/messages_de.properties
  82. 4 1
      jplag/src/main/resources/de/jplag/messages_en.properties
  83. 3 2
      jplag/src/main/resources/de/jplag/messages_es.properties
  84. 3 1
      jplag/src/main/resources/de/jplag/messages_fr.properties
  85. 3 1
      jplag/src/main/resources/de/jplag/messages_pt.properties
  86. 5 5
      jplag/src/test/java/de/jplag/BaseCodeTest.java
  87. 2 2
      jplag/src/test/java/de/jplag/NewJavaFeaturesTest.java
  88. 13 15
      jplag/src/test/java/de/jplag/NormalComparisonTest.java
  89. 9 11
      jplag/src/test/java/de/jplag/ParallelComparisonTest.java
  90. 2 3
      jplag/src/test/java/de/jplag/clustering/ClusteringAdapterTest.java
  91. 3 3
      jplag/src/test/java/de/jplag/clustering/algorithm/BayesianOptimizationTest.java
  92. 1 1
      jplag/src/test/java/de/jplag/clustering/algorithm/ClusteringData.java
  93. 1 1
      jplag/src/test/java/de/jplag/clustering/preprocessors/PreprocessingTestBase.java
  94. 115 0
      jplag/src/test/java/de/jplag/reporting/reportobject/mapper/MetricMapperTest.java
  95. 2 2
      jplag/src/test/java/de/jplag/special/VolumeTest.java
  96. 1 0
      jplag/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
  97. 39 4
      pom.xml
  98. 94 94
      report-viewer/package-lock.json
  99. 4 4
      report-viewer/package.json
  100. 4 6
      report-viewer/src/components/DistributionDiagram.vue

+ 1 - 1
.github/workflows/maven.yml

@@ -1,4 +1,4 @@
-name: CI Build
+name: Build
 
 on:
   push: 

+ 1 - 1
.github/workflows/report-viewer.yml

@@ -24,7 +24,7 @@ jobs:
           npm run build
 
       - name: Deploy 🚀
-        uses: JamesIves/github-pages-deploy-action@v4.3.4
+        uses: JamesIves/github-pages-deploy-action@v4.4.0
         with:
           branch: gh-pages
           folder: report-viewer/dist

+ 44 - 0
.github/workflows/sonarcloud-branch.yml

@@ -0,0 +1,44 @@
+name: SonarCloud Scan (master branch)
+
+on:
+  push:
+    branches:
+      - master
+
+  # Allows you to run this workflow manually from the Actions tab
+  workflow_dispatch:
+
+jobs:
+  sonar:
+    name: SonarCloud Scan
+    runs-on: ubuntu-latest
+    if: ${{ github.actor != 'dependabot[bot]' }}
+
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          fetch-depth: 0  # Shallow clones should be disabled for a better relevancy of analysis
+      - name: Set up JDK
+        uses: actions/setup-java@v3
+        with:
+          distribution: 'temurin'
+          java-version: 17
+
+      - name: Cache SonarCloud packages
+        uses: actions/cache@v3
+        with:
+          path: ~/.sonar/cache
+          key: ${{ runner.os }}-sonar
+          restore-keys: ${{ runner.os }}-sonar
+      - name: Cache Maven packages
+        uses: actions/cache@v3
+        with:
+          path: ~/.m2
+          key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
+          restore-keys: ${{ runner.os }}-m2
+          
+      - name: Build and analyze
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+        run: mvn -U -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar

+ 79 - 0
.github/workflows/sonarcloud-pr.yml

@@ -0,0 +1,79 @@
+name: SonarCloud Scan (PRs triggered by maven.yml)
+
+on:
+  workflow_run:
+    workflows:
+      - Build
+    types:
+      - completed
+
+
+
+  # Allows you to run this workflow manually from the Actions tab
+  workflow_dispatch:
+
+jobs:
+  get-info:
+    name: "Get information about the source run"
+    runs-on: ubuntu-latest
+    if: ${{ github.actor != 'dependabot[bot]' && github.event.workflow_run.conclusion == 'success' }}
+    outputs:
+      sourceHeadRepo: ${{ steps.source-run-info.outputs.sourceHeadRepo }}
+      sourceHeadBranch: ${{ steps.source-run-info.outputs.sourceHeadBranch }}
+      sourceHeadSha: ${{ steps.source-run-info.outputs.sourceHeadSha }}
+      mergeCommitSha: ${{ steps.source-run-info.outputs.mergeCommitSha }}
+      targetCommitSha: ${{ steps.source-run-info.outputs.targetCommitSha }}
+      pullRequestNumber: ${{ steps.source-run-info.outputs.pullRequestNumber }}
+      pullRequestLabels: ${{ steps.source-run-info.outputs.pullRequestLabels }}
+      targetBranch: ${{ steps.source-run-info.outputs.targetBranch }}
+      sourceEvent: ${{ steps.source-run-info.outputs.sourceEvent }}
+    steps:
+      - name: "Get information about the origin 'CI' run"
+        uses: potiuk/get-workflow-origin@v1_3
+        id: source-run-info
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+          sourceRunId: ${{ github.event.workflow_run.id }}
+
+  sonar:
+    name: SonarCloud Scan
+    runs-on: ubuntu-latest
+    if: ${{ needs.get-info.outputs.sourceEvent == 'pull_request' && github.actor != 'dependabot[bot]' && github.event.workflow_run.conclusion == 'success' }}
+    needs: get-info
+
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          repository: ${{ github.event.workflow_run.head_repository.full_name }}
+          ref: ${{ github.event.workflow_run.head_branch }}
+          fetch-depth: 0  # Shallow clones should be disabled for a better relevancy of analysis
+      - name: Set up JDK
+        uses: actions/setup-java@v3
+        with:
+          distribution: 'temurin'
+          java-version: 17
+
+      - name: Cache SonarCloud packages
+        uses: actions/cache@v3
+        with:
+          path: ~/.sonar/cache
+          key: ${{ runner.os }}-sonar
+          restore-keys: ${{ runner.os }}-sonar
+      - name: Cache Maven packages
+        uses: actions/cache@v3
+        with:
+          path: ~/.m2
+          key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
+          restore-keys: ${{ runner.os }}-m2
+          
+      - name: Build and analyze (PR)
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+        run: >
+          mvn 
+          -Dsonar.scm.revision=${{ github.event.workflow_run.head_sha }} 
+          -Dsonar.pullrequest.key=${{ needs.get-info.outputs.pullRequestNumber }} 
+          -Dsonar.pullrequest.branch=${{ needs.get-info.outputs.sourceHeadBranch }} 
+          -Dsonar.pullrequest.base=${{ needs.get-info.outputs.pullRequestNumber }} 
+          -U -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar

+ 6 - 0
.gitignore

@@ -22,3 +22,9 @@ hs_err_pid*
 .idea/
 *.iml
 .flattened-pom.xml
+
+# VSCode
+.vscode/
+
+# ANTLR
+.antlr/

+ 57 - 4
jplag.cli/pom.xml

@@ -13,12 +13,52 @@
             <artifactId>jplag</artifactId>
         </dependency>
         <dependency>
-            <groupId>net.sourceforge.argparse4j</groupId>
-            <artifactId>argparse4j</artifactId>
+            <groupId>de.jplag</groupId>
+            <artifactId>jplag</artifactId>
+        </dependency>
+        <dependency>
+            <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>
         </dependency>
         <dependency>
-            <groupId>org.slf4j</groupId>
-            <artifactId>slf4j-simple</artifactId>
+            <groupId>de.jplag</groupId>
+            <artifactId>cpp</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>de.jplag</groupId>
+            <artifactId>rlang</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>de.jplag</groupId>
+            <artifactId>kotlin</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>de.jplag</groupId>
+            <artifactId>scheme</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>de.jplag</groupId>
+            <artifactId>chars</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>net.sourceforge.argparse4j</groupId>
+            <artifactId>argparse4j</artifactId>
         </dependency>
     </dependencies>
     <build>
@@ -50,6 +90,19 @@
                     <finalName>jplag-${project.version}</finalName>
                 </configuration>
             </plugin>
+            <plugin>
+                <groupId>org.jacoco</groupId>
+                <artifactId>jacoco-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>report-aggregate</id>
+                        <goals>
+                            <goal>report-aggregate</goal>
+                        </goals>
+                        <phase>verify</phase>
+                    </execution>
+                </executions>
+            </plugin>
         </plugins>
     </build>
 </project>

+ 18 - 5
jplag.cli/src/main/java/de/jplag/CLI.java

@@ -2,6 +2,7 @@ package de.jplag;
 
 import static de.jplag.CommandLineArgument.*;
 
+import java.security.SecureRandom;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
@@ -12,6 +13,7 @@ import net.sourceforge.argparse4j.inf.ArgumentParser;
 import net.sourceforge.argparse4j.inf.ArgumentParserException;
 import net.sourceforge.argparse4j.inf.Namespace;
 
+import org.slf4j.ILoggerFactory;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -20,6 +22,7 @@ import de.jplag.clustering.ClusteringOptions;
 import de.jplag.clustering.Preprocessing;
 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;
@@ -32,10 +35,12 @@ import de.jplag.strategy.ComparisonMode;
  * Command line interface class, allows using via command line.
  * @see CLI#main(String[])
  */
-public class CLI {
+public final class CLI {
 
     private static final Logger logger = LoggerFactory.getLogger(CLI.class);
 
+    private static final Random RANDOM = new SecureRandom();
+
     private static final String CREDITS = "Created by IPD Tichy, Guido Malpohl, and others. JPlag logo designed by Sandro Koch. Currently maintained by Sebastian Hahner and Timur Saglam.";
 
     private static final String[] DESCRIPTIONS = {"Detecting Software Plagiarism", "Software-Archaeological Playground", "Since 1996",
@@ -62,12 +67,21 @@ public class CLI {
             JPlagResult result = program.run();
             Report report = new JsonReport();
             report.saveReport(result, arguments.getString(RESULT_FOLDER.flagWithoutDash()));
+            finalizeLogger();
         } catch (ExitException exception) {
             logger.error(exception.getMessage(), exception);
+            finalizeLogger();
             System.exit(1);
         }
     }
 
+    private static void finalizeLogger() {
+        ILoggerFactory factory = LoggerFactory.getILoggerFactory();
+        if (!(factory instanceof CollectedLoggerFactory collectedLoggerFactory))
+            return;
+        collectedLoggerFactory.finalizeInstances();
+    }
+
     /**
      * Creates the command line interface and initializes the argument parser.
      */
@@ -164,15 +178,14 @@ public class CLI {
     }
 
     private String generateDescription() {
-        var randomDescription = DESCRIPTIONS[new Random().nextInt(DESCRIPTIONS.length)];
-        return String.format("JPlag - %s" + System.lineSeparator() + CREDITS, randomDescription);
+        var randomDescription = DESCRIPTIONS[RANDOM.nextInt(DESCRIPTIONS.length)];
+        return String.format("JPlag - %s%n%s", CREDITS, randomDescription);
     }
 
     private void addAllMultiValueArgument(List<List<String>> argumentValues, List<String> destinationRootDirectories) {
         if (argumentValues == null) {
             return;
         }
-
-        argumentValues.stream().forEach(value -> destinationRootDirectories.addAll(value));
+        argumentValues.forEach(destinationRootDirectories::addAll);
     }
 }

+ 270 - 0
jplag.cli/src/main/java/de/jplag/logger/CollectedLogger.java

@@ -0,0 +1,270 @@
+package de.jplag.logger;
+
+import java.io.PrintStream;
+import java.io.Serial;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.concurrent.ConcurrentLinkedDeque;
+
+import org.slf4j.helpers.FormattingTuple;
+import org.slf4j.helpers.MarkerIgnoringBase;
+import org.slf4j.helpers.MessageFormatter;
+import org.slf4j.spi.LocationAwareLogger;
+
+/**
+ * This logger is able to collect errors and print them at the end. Mainly adopted from org.slf4j.impl.SimpleLogger
+ * @author Dominik Fuchss
+ */
+public final class CollectedLogger extends MarkerIgnoringBase {
+
+    @Serial
+    private static final long serialVersionUID = -1278670638921140275L;
+
+    private static final int LOG_LEVEL_TRACE = LocationAwareLogger.TRACE_INT;
+    private static final int LOG_LEVEL_DEBUG = LocationAwareLogger.DEBUG_INT;
+    private static final int LOG_LEVEL_INFO = LocationAwareLogger.INFO_INT;
+    private static final int LOG_LEVEL_WARN = LocationAwareLogger.WARN_INT;
+    private static final int LOG_LEVEL_ERROR = LocationAwareLogger.ERROR_INT;
+
+    private final int currentLogLevel = LOG_LEVEL_INFO;
+
+    /**
+     * The short name of this simple log instance
+     */
+    private transient String shortLogName = null;
+
+    /**
+     * Indicator whether finalization is in progress.
+     * @see #printAllErrorsForLogger()
+     */
+    private transient boolean isFinalizing = false;
+
+    private final transient SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-hh:mm:ss_SSS");
+
+    private final ConcurrentLinkedDeque<Triple<String, Throwable, Date>> allErrors = new ConcurrentLinkedDeque<>();
+
+    CollectedLogger(String name) {
+        this.name = name;
+    }
+
+    private void log(int level, String message, Throwable throwable) {
+        log(level, message, throwable, null);
+    }
+
+    private void log(int level, String message, Throwable throwable, Date timeOfError) {
+        if (!isLevelEnabled(level)) {
+            return;
+        }
+
+        if (level == LOG_LEVEL_ERROR && !isFinalizing) {
+            // Buffer errors for the final output
+            allErrors.add(new Triple<>(message, throwable, new Date()));
+            return;
+        }
+
+        StringBuilder builder = new StringBuilder(32);
+
+        // Append date-time
+        builder.append(dateFormat.format(timeOfError == null ? new Date() : timeOfError)).append(' ');
+
+        // Append current thread name
+        builder.append('[').append(Thread.currentThread().getName()).append("] ");
+        // Append current Level
+        builder.append('[').append(renderLevel(level)).append(']').append(' ');
+
+        // Append the name of the log instance
+        if (shortLogName == null)
+            shortLogName = computeShortName();
+        builder.append(shortLogName).append(" - ");
+        // Append the message
+        builder.append(message);
+
+        write(builder, throwable);
+    }
+
+    void printAllErrorsForLogger() {
+        this.isFinalizing = true;
+        // Copy errors to prevent infinite recursion
+        var errors = new ArrayList<>(this.allErrors);
+        if (errors.isEmpty())
+            return;
+
+        this.allErrors.removeAll(errors);
+
+        info("Summary of all Errors:");
+        errors.forEach(error -> log(LOG_LEVEL_ERROR, error.first(), error.second(), error.third()));
+        isFinalizing = false;
+    }
+
+    @SuppressWarnings("java:S106")
+    void write(StringBuilder buf, Throwable throwable) {
+        PrintStream targetStream = System.out;
+
+        targetStream.println(buf.toString());
+        writeThrowable(throwable, targetStream);
+        targetStream.flush();
+    }
+
+    private void writeThrowable(Throwable throwable, PrintStream targetStream) {
+        if (throwable != null) {
+            throwable.printStackTrace(targetStream);
+        }
+    }
+
+    private String computeShortName() {
+        return name.substring(name.lastIndexOf(".") + 1);
+    }
+
+    private boolean isLevelEnabled(int logLevel) {
+        return logLevel >= currentLogLevel;
+    }
+
+    private String renderLevel(int level) {
+        return switch (level) {
+            case LOG_LEVEL_TRACE -> "TRACE";
+            case LOG_LEVEL_DEBUG -> "DEBUG";
+            case LOG_LEVEL_INFO -> "INFO";
+            case LOG_LEVEL_WARN -> "WARN";
+            case LOG_LEVEL_ERROR -> "ERROR";
+            default -> throw new IllegalStateException("Unrecognized level [" + level + "]");
+        };
+    }
+
+    public boolean isTraceEnabled() {
+        return isLevelEnabled(LOG_LEVEL_TRACE);
+    }
+
+    @Override
+    public void trace(String message) {
+        log(LOG_LEVEL_TRACE, message, null);
+    }
+
+    public void trace(String format, Object param1) {
+        formatAndLog(LOG_LEVEL_TRACE, format, param1, null);
+    }
+
+    public void trace(String format, Object param1, Object param2) {
+        formatAndLog(LOG_LEVEL_TRACE, format, param1, param2);
+    }
+
+    public void trace(String format, Object... argArray) {
+        formatAndLog(LOG_LEVEL_TRACE, format, argArray);
+    }
+
+    public void trace(String message, Throwable t) {
+        log(LOG_LEVEL_TRACE, message, t);
+    }
+
+    public boolean isDebugEnabled() {
+        return isLevelEnabled(LOG_LEVEL_DEBUG);
+    }
+
+    public void debug(String message) {
+        log(LOG_LEVEL_DEBUG, message, null);
+    }
+
+    public void debug(String format, Object param1) {
+        formatAndLog(LOG_LEVEL_DEBUG, format, param1, null);
+    }
+
+    public void debug(String format, Object param1, Object param2) {
+        formatAndLog(LOG_LEVEL_DEBUG, format, param1, param2);
+    }
+
+    public void debug(String format, Object... argArray) {
+        formatAndLog(LOG_LEVEL_DEBUG, format, argArray);
+    }
+
+    public void debug(String message, Throwable throwable) {
+        log(LOG_LEVEL_DEBUG, message, throwable);
+    }
+
+    public boolean isInfoEnabled() {
+        return isLevelEnabled(LOG_LEVEL_INFO);
+    }
+
+    public void info(String message) {
+        log(LOG_LEVEL_INFO, message, null);
+    }
+
+    public void info(String format, Object arg) {
+        formatAndLog(LOG_LEVEL_INFO, format, arg, null);
+    }
+
+    public void info(String format, Object arg1, Object arg2) {
+        formatAndLog(LOG_LEVEL_INFO, format, arg1, arg2);
+    }
+
+    public void info(String format, Object... argArray) {
+        formatAndLog(LOG_LEVEL_INFO, format, argArray);
+    }
+
+    public void info(String message, Throwable throwable) {
+        log(LOG_LEVEL_INFO, message, throwable);
+    }
+
+    public boolean isWarnEnabled() {
+        return isLevelEnabled(LOG_LEVEL_WARN);
+    }
+
+    public void warn(String message) {
+        log(LOG_LEVEL_WARN, message, null);
+    }
+
+    public void warn(String format, Object arg) {
+        formatAndLog(LOG_LEVEL_WARN, format, arg, null);
+    }
+
+    public void warn(String format, Object arg1, Object arg2) {
+        formatAndLog(LOG_LEVEL_WARN, format, arg1, arg2);
+    }
+
+    public void warn(String format, Object... argArray) {
+        formatAndLog(LOG_LEVEL_WARN, format, argArray);
+    }
+
+    public void warn(String message, Throwable throwable) {
+        log(LOG_LEVEL_WARN, message, throwable);
+    }
+
+    public boolean isErrorEnabled() {
+        return isLevelEnabled(LOG_LEVEL_ERROR);
+    }
+
+    public void error(String message) {
+        log(LOG_LEVEL_ERROR, message, null);
+    }
+
+    public void error(String format, Object arg) {
+        formatAndLog(LOG_LEVEL_ERROR, format, arg, null);
+    }
+
+    public void error(String format, Object arg1, Object arg2) {
+        formatAndLog(LOG_LEVEL_ERROR, format, arg1, arg2);
+    }
+
+    public void error(String format, Object... argArray) {
+        formatAndLog(LOG_LEVEL_ERROR, format, argArray);
+    }
+
+    public void error(String message, Throwable throwable) {
+        log(LOG_LEVEL_ERROR, message, throwable);
+    }
+
+    private void formatAndLog(int level, String format, Object arg1, Object arg2) {
+        if (!isLevelEnabled(level)) {
+            return;
+        }
+        FormattingTuple formattingTuple = MessageFormatter.format(format, arg1, arg2);
+        log(level, formattingTuple.getMessage(), formattingTuple.getThrowable());
+    }
+
+    private void formatAndLog(int level, String format, Object... arguments) {
+        if (!isLevelEnabled(level)) {
+            return;
+        }
+        FormattingTuple formattingTuple = MessageFormatter.arrayFormat(format, arguments);
+        log(level, formattingTuple.getMessage(), formattingTuple.getThrowable());
+    }
+}

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

@@ -0,0 +1,44 @@
+package de.jplag.logger;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.slf4j.ILoggerFactory;
+import org.slf4j.Logger;
+
+/**
+ * The factory for {@link CollectedLogger}.
+ * @author Dominik Fuchss
+ */
+public final class CollectedLoggerFactory implements ILoggerFactory {
+
+    private final ConcurrentMap<String, CollectedLogger> loggerMap;
+
+    public CollectedLoggerFactory() {
+        loggerMap = new ConcurrentHashMap<>();
+    }
+
+    /**
+     * Return an appropriate {@link CollectedLogger} instance by name.
+     */
+    public Logger getLogger(String name) {
+        CollectedLogger simpleLogger = loggerMap.get(name);
+        if (simpleLogger != null) {
+            return simpleLogger;
+        } else {
+            CollectedLogger newInstance = new CollectedLogger(name);
+            Logger oldInstance = loggerMap.putIfAbsent(name, newInstance);
+            return oldInstance == null ? newInstance : oldInstance;
+        }
+    }
+
+    /**
+     * Print all errors of existing instances of {@link CollectedLogger}.
+     */
+    public void finalizeInstances() {
+        List<CollectedLogger> copy = new ArrayList<>(loggerMap.values());
+        copy.forEach(CollectedLogger::printAllErrorsForLogger);
+    }
+}

+ 4 - 0
jplag.cli/src/main/java/de/jplag/logger/Triple.java

@@ -0,0 +1,4 @@
+package de.jplag.logger;
+
+public record Triple<A, B, C> (A first, B second, C third) {
+}

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

@@ -0,0 +1,54 @@
+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;
+    }
+}

+ 1 - 1
jplag.cli/src/test/java/de/jplag/cli/CommandLineInterfaceTest.java

@@ -17,7 +17,7 @@ import de.jplag.options.JPlagOptions;
  * line arguments, thus these tests do not test the functionality of the options during the comparison.
  * @author Timur Saglam
  */
-public class CommandLineInterfaceTest {
+public abstract class CommandLineInterfaceTest {
     protected static final String CURRENT_DIRECTORY = ".";
     protected static final float DELTA = 0.0001f;
 

+ 0 - 28
jplag.frontend-testutils/src/test/java/de/jplag/testutils/TestErrorConsumer.java

@@ -1,28 +0,0 @@
-package de.jplag.testutils;
-
-import static org.junit.jupiter.api.Assertions.fail;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import de.jplag.ErrorConsumer;
-
-/**
- * Mock error consumer that fails the test case on error occurrence.
- * @author Timur Saglam
- */
-public class TestErrorConsumer implements ErrorConsumer {
-    private final Logger logger = LoggerFactory.getLogger("JPlag-Test");
-
-    @Override
-    public void addError(String errorMessage) {
-        logger.error(errorMessage);
-        fail(errorMessage);
-    }
-
-    @Override
-    public void print(String message, String longMessage) {
-        logger.info(message);
-    }
-
-}

+ 6 - 16
jplag.frontend-utils/src/main/java/de/jplag/AbstractParser.java

@@ -1,15 +1,18 @@
 package de.jplag;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 /**
  * Abstract parser class. Counts errors and manages an error consumer.
  * @author Emeric Kwemou
  */
 public abstract class AbstractParser {
-    protected ErrorConsumer errorConsumer;
     protected int errors = 0;
+    public final Logger logger;
 
-    public AbstractParser(ErrorConsumer errorConsumer) {
-        this.errorConsumer = errorConsumer;
+    protected AbstractParser() {
+        this.logger = LoggerFactory.getLogger(this.getClass());
     }
 
     /**
@@ -18,17 +21,4 @@ public abstract class AbstractParser {
     public boolean hasErrors() {
         return errors != 0;
     }
-
-    /**
-     * @return the error consumer that collects and prints errors.
-     */
-    public ErrorConsumer getErrorConsumer() {
-        return errorConsumer;
-    }
-
-    /**
-     * Setter for the error consumer that collects and prints errors.
-     * @param errorConsumer is the consumer to set.
-     */
-
 }

+ 0 - 20
jplag.frontend-utils/src/main/java/de/jplag/ErrorConsumer.java

@@ -1,20 +0,0 @@
-package de.jplag;
-
-/**
- * Consumes errors from a language parser.
- */
-public interface ErrorConsumer {
-
-    /**
-     * Adds an error to the error stack. The error is only printed directly when using the corresponding verbosity option.
-     * @param errorMessage is the error message.
-     */
-    void addError(String errorMessage);
-
-    /**
-     * Prints a message depending on the verbosity.
-     * @param message is the message for the normal mode.
-     * @param longMessage is the message for verbose mode.
-     */
-    void print(String message, String longMessage);
-}

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

@@ -8,7 +8,7 @@ import java.io.File;
 public interface Language {
 
     /**
-     * Suffixes for the files containing code of the language.
+     * Suffixes for the files containing code of the language. An empty array means all suffixes are valid.
      */
     String[] suffixes();
 
@@ -41,22 +41,17 @@ public interface Language {
     boolean hasErrors();
 
     /**
-     * Determines whether the parser provide column information.
+     * Determines whether the parser provide column information. If that is the case, line and column indices are used
+     * instead of a single token index.
      */
-    boolean supportsColumns();
+    default boolean supportsColumns() {
+        return true;
+    }
 
     /**
-     * Determines whether JPlag should use a fixed-width font in its reports.
+     * Determines whether a fixed-width font should be used to display that language.
      */
-    boolean isPreformatted();
-
-    /**
-     * Determines whether tokens from the scanner are indexed.
-     */
-    boolean usesIndex();
-
-    /**
-     * Number of defined tokens in the scanner of the language.
-     */
-    int numberOfTokens();
+    default boolean isPreformatted() {
+        return true;
+    }
 }

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

@@ -2,7 +2,6 @@ package de.jplag.chars;
 
 import java.io.File;
 
-import de.jplag.ErrorConsumer;
 import de.jplag.TokenList;
 
 /*
@@ -12,8 +11,8 @@ public class Language implements de.jplag.Language {
 
     private final Parser parser;
 
-    public Language(ErrorConsumer program) {
-        parser = new Parser(program);
+    public Language() {
+        parser = new Parser();
     }
 
     @Override
@@ -55,14 +54,4 @@ public class Language implements de.jplag.Language {
     public boolean isPreformatted() {
         return false;
     }
-
-    @Override
-    public boolean usesIndex() {
-        return true;
-    }
-
-    @Override
-    public int numberOfTokens() {
-        return 36;
-    }
 }

+ 7 - 14
jplag.frontend.chars/src/main/java/de/jplag/chars/Parser.java

@@ -5,7 +5,6 @@ import java.io.FileReader;
 import java.nio.charset.StandardCharsets;
 
 import de.jplag.AbstractParser;
-import de.jplag.ErrorConsumer;
 import de.jplag.TokenConstants;
 import de.jplag.TokenList;
 
@@ -15,25 +14,24 @@ public class Parser extends AbstractParser {
 
     /**
      * Creates the parser.
-     * @param errorConsumer is the consumer for any occurring errors.
      */
-    public Parser(ErrorConsumer errorConsumer) {
-        super(errorConsumer);
+    public Parser() {
+        super();
     }
 
     public TokenList parse(File directory, String[] files) {
         tokens = new TokenList();
         errors = 0;
         for (String file : files) {
-            getErrorConsumer().print(null, "Parsing file " + file);
+            logger.trace("Parsing file {}", file);
             if (!parseFile(directory, file))
                 errors++;
             tokens.addToken(new CharToken(TokenConstants.FILE_END, file, this));
         }
         if (errors == 0)
-            errorConsumer.print(null, "OK");
+            logger.trace("OK");
         else
-            errorConsumer.print(null, errors + " ERROR" + (errors > 1 ? "S" : ""));
+            logger.error(errors + " ERROR" + (errors > 1 ? "S" : ""));
 
         return tokens;
     }
@@ -44,9 +42,7 @@ public class Parser extends AbstractParser {
         int length;
         int offset = 0;
 
-        try {
-            FileReader reader = new FileReader(new File(dir, file), StandardCharsets.UTF_8);
-
+        try (FileReader reader = new FileReader(new File(dir, file), StandardCharsets.UTF_8)) {
             do {
                 length = reader.read(buffer);
 
@@ -57,11 +53,8 @@ public class Parser extends AbstractParser {
                 }
                 offset += length;
             } while (length != -1);
-
-            // close file
-            reader.close();
         } catch (Exception e) {
-            e.printStackTrace();
+            logger.error(e.getMessage(), e);
             return false;
         }
         return true;

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

@@ -2,14 +2,13 @@ package de.jplag.cpp;
 
 import java.io.File;
 
-import de.jplag.ErrorConsumer;
 import de.jplag.TokenList;
 
 public class Language implements de.jplag.Language {
     private final Scanner scanner; // cpp code is scanned not parsed
 
-    public Language(ErrorConsumer errorConsumer) {
-        scanner = new Scanner(errorConsumer);
+    public Language() {
+        scanner = new Scanner();
     }
 
     @Override
@@ -32,21 +31,6 @@ public class Language implements de.jplag.Language {
         return 12;
     }
 
-    @Override
-    public boolean supportsColumns() {
-        return true;
-    }
-
-    @Override
-    public boolean isPreformatted() {
-        return true;
-    }
-
-    @Override
-    public boolean usesIndex() {
-        return false;
-    }
-
     @Override
     public TokenList parse(File dir, String[] files) {
         return this.scanner.scan(dir, files);
@@ -56,9 +40,4 @@ public class Language implements de.jplag.Language {
     public boolean hasErrors() {
         return this.scanner.hasErrors();
     }
-
-    @Override
-    public int numberOfTokens() {
-        return CPPTokenConstants.NUM_DIFF_TOKENS;
-    }
 }

+ 3 - 5
jplag.frontend.cpp/src/main/java/de/jplag/cpp/Scanner.java

@@ -3,7 +3,6 @@ package de.jplag.cpp;
 import java.io.File;
 
 import de.jplag.AbstractParser;
-import de.jplag.ErrorConsumer;
 import de.jplag.TokenList;
 
 public class Scanner extends AbstractParser {
@@ -13,10 +12,9 @@ public class Scanner extends AbstractParser {
 
     /**
      * Creates the parser.
-     * @param errorConsumer is the consumer for any occurring errors.
      */
-    public Scanner(ErrorConsumer errorConsumer) {
-        super(errorConsumer);
+    public Scanner() {
+        super();
     }
 
     public TokenList scan(File directory, String[] files) {
@@ -24,7 +22,7 @@ public class Scanner extends AbstractParser {
         errors = 0;
         for (String currentFile : files) {
             this.currentFile = currentFile;
-            getErrorConsumer().print(null, "Scanning file " + currentFile);
+            logger.trace("Scanning file {}", currentFile);
             if (!CPPScanner.scanFile(directory, currentFile, this)) {
                 errors++;
             }

+ 2 - 2
jplag.frontend.cpp/src/main/javacc/CPP.jj

@@ -37,10 +37,10 @@ public class CPPScanner implements CPPTokenConstants {
         try {
             scanner.scan();
         } catch (ParseException e) {
-            delegatingScanner.getErrorConsumer().addError("  Parsing Error in '" + fileName + "':\n  " + e.getMessage());
+            delegatingScanner.logger.error("Parsing Error in '" + fileName + "': " + e.getMessage());
             return false;
         } catch (TokenMgrException e) {
-            delegatingScanner.getErrorConsumer().addError("  Scanning Error in '" + fileName + "':\n  " + e.getMessage());
+            delegatingScanner.logger.error("Scanning Error in '" + fileName + "': " + e.getMessage());
             return false;
         }
         return true;

+ 1 - 1
jplag.frontend.csharp-6/src/main/java/de/jplag/csharp/CSharpListener.java

@@ -215,7 +215,7 @@ public class CSharpListener extends CSharpParserBaseListener implements CSharpTo
 
     @Override
     public void enterUsingStatement(UsingStatementContext context) {
-        transformToken(USING, null);
+        transformToken(USING, context.getStart());
         super.enterUsingStatement(context);
     }
 

+ 4 - 6
jplag.frontend.csharp-6/src/main/java/de/jplag/csharp/CSharpParserAdapter.java

@@ -12,7 +12,6 @@ import org.antlr.v4.runtime.tree.ParseTree;
 import org.antlr.v4.runtime.tree.ParseTreeWalker;
 
 import de.jplag.AbstractParser;
-import de.jplag.ErrorConsumer;
 import de.jplag.TokenList;
 import de.jplag.csharp.grammar.CSharpLexer;
 import de.jplag.csharp.grammar.CSharpParser;
@@ -28,10 +27,9 @@ public class CSharpParserAdapter extends AbstractParser {
 
     /**
      * Creates the parser adapter.
-     * @param errorConsumer is the consumer for any occurring errors.
      */
-    public CSharpParserAdapter(ErrorConsumer errorConsumer) {
-        super(errorConsumer);
+    public CSharpParserAdapter() {
+        super();
     }
 
     /**
@@ -54,7 +52,7 @@ public class CSharpParserAdapter extends AbstractParser {
 
     private boolean parseFile(File directory, String fileName) {
         File file = new File(directory, fileName);
-        try (FileInputStream inputStream = new FileInputStream(file);) {
+        try (FileInputStream inputStream = new FileInputStream(file)) {
             currentFile = fileName;
 
             // create a lexer, a parser and a buffer between them.
@@ -72,7 +70,7 @@ public class CSharpParserAdapter extends AbstractParser {
                 treeWalker.walk(new CSharpListener(this), parseTree);
             }
         } catch (IOException exception) {
-            getErrorConsumer().addError("Parsing Error in '" + fileName + "':" + File.separator + exception.toString());
+            logger.error("Parsing Error in '" + fileName + "':" + File.separator + exception, exception);
             return false;
         }
         return true;

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

@@ -3,7 +3,6 @@ package de.jplag.csharp;
 import java.io.File;
 import java.util.Arrays;
 
-import de.jplag.ErrorConsumer;
 import de.jplag.TokenList;
 
 /**
@@ -18,8 +17,8 @@ public class Language implements de.jplag.Language {
 
     private final CSharpParserAdapter parser;
 
-    public Language(ErrorConsumer program) {
-        parser = new CSharpParserAdapter(program);
+    public Language() {
+        parser = new CSharpParserAdapter();
     }
 
     @Override
@@ -51,24 +50,4 @@ public class Language implements de.jplag.Language {
     public boolean hasErrors() {
         return parser.hasErrors();
     }
-
-    @Override
-    public boolean supportsColumns() {
-        return true;
-    }
-
-    @Override
-    public boolean isPreformatted() {
-        return true;
-    }
-
-    @Override
-    public boolean usesIndex() {
-        return false;
-    }
-
-    @Override
-    public int numberOfTokens() {
-        return CSharpTokenConstants.NUM_DIFF_TOKENS;
-    }
 }

+ 11 - 7
jplag.frontend.csharp-6/src/main/java/de/jplag/csharp/grammar/CSharpPreprocessorParserBase.java

@@ -1,14 +1,18 @@
 package de.jplag.csharp.grammar;
 
 import java.util.HashSet;
+import java.util.Objects;
 import java.util.Stack;
 
-import org.antlr.v4.runtime.*;
+import org.antlr.v4.runtime.Parser;
+import org.antlr.v4.runtime.ParserRuleContext;
+import org.antlr.v4.runtime.TokenStream;
 
 /**
  * This class was taken from <a href="https://github.com/antlr/grammars-v4/tree/master/csharp">antlr/grammars-v4</a>. It
  * was originally written by Ken Domino. Note that this class is licensed under Eclipse Public License - v 1.0.
  */
+@SuppressWarnings("java:S100")
 abstract class CSharpPreprocessorParserBase extends Parser {
     protected CSharpPreprocessorParserBase(TokenStream input) {
         super(input);
@@ -16,7 +20,7 @@ abstract class CSharpPreprocessorParserBase extends Parser {
         ConditionalSymbols.add("DEBUG");
     }
 
-    Stack<Boolean> conditions = new Stack<Boolean>();
+    Stack<Boolean> conditions = new Stack<>();
     public HashSet<String> ConditionalSymbols = new HashSet<String>();
 
     protected Boolean AllConditions() {
@@ -51,7 +55,7 @@ abstract class CSharpPreprocessorParserBase extends Parser {
     protected void OnPreprocessorDirectiveElif() {
         ParserRuleContext c = this._ctx;
         CSharpPreprocessorParser.PreprocessorConditionalContext d = (CSharpPreprocessorParser.PreprocessorConditionalContext) c;
-        if (!conditions.peek()) {
+        if (Boolean.FALSE.equals(conditions.peek())) {
             conditions.pop();
             d.value = d.expr.value.equals("true") && AllConditions();
             conditions.push(d.expr.value.equals("true"));
@@ -63,9 +67,9 @@ abstract class CSharpPreprocessorParserBase extends Parser {
     protected void OnPreprocessorDirectiveElse() {
         ParserRuleContext c = this._ctx;
         CSharpPreprocessorParser.PreprocessorConditionalContext d = (CSharpPreprocessorParser.PreprocessorConditionalContext) c;
-        if (!conditions.peek()) {
+        if (Boolean.FALSE.equals(conditions.peek())) {
             conditions.pop();
-            d.value = true && AllConditions();
+            d.value = AllConditions();
             conditions.push(true);
         } else {
             d.value = false;
@@ -154,13 +158,13 @@ abstract class CSharpPreprocessorParserBase extends Parser {
     protected void OnPreprocessorExpressionConditionalEq() {
         ParserRuleContext c = this._ctx;
         CSharpPreprocessorParser.Preprocessor_expressionContext d = (CSharpPreprocessorParser.Preprocessor_expressionContext) c;
-        d.value = (d.expr1.value == d.expr2.value ? "true" : "false");
+        d.value = (Objects.equals(d.expr1.value, d.expr2.value) ? "true" : "false");
     }
 
     protected void OnPreprocessorExpressionConditionalNe() {
         ParserRuleContext c = this._ctx;
         CSharpPreprocessorParser.Preprocessor_expressionContext d = (CSharpPreprocessorParser.Preprocessor_expressionContext) c;
-        d.value = (d.expr1.value != d.expr2.value ? "true" : "false");
+        d.value = (!Objects.equals(d.expr1.value, d.expr2.value) ? "true" : "false");
     }
 
     protected void OnPreprocessorExpressionConditionalAnd() {

+ 1 - 3
jplag.frontend.csharp-6/src/test/java/de/jplag/csharp/MinimalCSharpFrontendTest.java

@@ -20,7 +20,6 @@ import de.jplag.Token;
 import de.jplag.TokenConstants;
 import de.jplag.TokenList;
 import de.jplag.TokenPrinter;
-import de.jplag.testutils.TestErrorConsumer;
 
 class MinimalCSharpFrontendTest {
     private final Logger logger = LoggerFactory.getLogger("JPlag-Test");
@@ -33,8 +32,7 @@ class MinimalCSharpFrontendTest {
 
     @BeforeEach
     public void setUp() {
-        TestErrorConsumer consumer = new TestErrorConsumer();
-        frontend = new Language(consumer);
+        frontend = new Language();
         baseDirectory = BASE_PATH.toFile();
         assertTrue(baseDirectory.exists(), "Could not find base directory!");
     }

+ 1 - 1
jplag.frontend.java/src/main/java/de/jplag/java/JavaTokenConstants.java

@@ -84,5 +84,5 @@ public interface JavaTokenConstants extends TokenConstants {
     int J_RECORD_BEGIN = 68;
     int J_RECORD_END = 69;
 
-    final static int NUM_DIFF_TOKENS = 70;	    // @formatter:on
+    int NUM_DIFF_TOKENS = 70;	    // @formatter:on
 }

+ 6 - 9
jplag.frontend.java/src/main/java/de/jplag/java/JavacAdapter.java

@@ -6,12 +6,10 @@ import java.nio.charset.StandardCharsets;
 import java.nio.file.Paths;
 import java.util.Collections;
 
-import javax.tools.Diagnostic;
-import javax.tools.DiagnosticCollector;
-import javax.tools.JavaCompiler;
+import javax.tools.*;
 import javax.tools.JavaCompiler.CompilationTask;
-import javax.tools.StandardJavaFileManager;
-import javax.tools.ToolProvider;
+
+import org.slf4j.Logger;
 
 import com.sun.source.tree.CompilationUnitTree;
 import com.sun.source.tree.LineMap;
@@ -19,7 +17,6 @@ import com.sun.source.util.JavacTask;
 import com.sun.source.util.SourcePositions;
 import com.sun.source.util.Trees;
 
-import de.jplag.ErrorConsumer;
 import de.jplag.TokenConstants;
 
 public class JavacAdapter {
@@ -39,7 +36,7 @@ public class JavacAdapter {
             ast.accept(new TokenGeneratingTreeScanner(filename, parser, map, positions, ast), null);
             parser.add(TokenConstants.FILE_END, filename, 1, -1, -1);
         }
-        return processErrors(parser.getErrorConsumer(), listener);
+        return processErrors(parser.logger, listener);
     }
 
     private Iterable<? extends CompilationUnitTree> executeCompilationTask(final CompilationTask task) {
@@ -60,11 +57,11 @@ public class JavacAdapter {
         }
     }
 
-    private int processErrors(final ErrorConsumer consumer, DiagnosticCollector<Object> listener) {
+    private int processErrors(Logger logger, DiagnosticCollector<Object> listener) {
         int errors = 0;
         for (Diagnostic<?> diagnosticItem : listener.getDiagnostics()) {
             if (diagnosticItem.getKind() == javax.tools.Diagnostic.Kind.ERROR) {
-                consumer.addError(diagnosticItem.toString());
+                logger.error("{}", diagnosticItem);
                 errors++;
             }
         }

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

@@ -2,7 +2,6 @@ package de.jplag.java;
 
 import java.io.File;
 
-import de.jplag.ErrorConsumer;
 import de.jplag.TokenList;
 
 /**
@@ -11,8 +10,8 @@ import de.jplag.TokenList;
 public class Language implements de.jplag.Language {
     private final Parser parser;
 
-    public Language(ErrorConsumer errorConsumer) {
-        parser = new Parser(errorConsumer);
+    public Language() {
+        parser = new Parser();
     }
 
     @Override
@@ -35,26 +34,6 @@ public class Language implements de.jplag.Language {
         return 9;
     }
 
-    @Override
-    public boolean supportsColumns() {
-        return true;
-    }
-
-    @Override
-    public boolean isPreformatted() {
-        return true;
-    }
-
-    @Override
-    public boolean usesIndex() {
-        return false;
-    }
-
-    @Override
-    public int numberOfTokens() {
-        return JavaTokenConstants.NUM_DIFF_TOKENS;
-    }
-
     @Override
     public TokenList parse(File directory, String[] files) {
         return this.parser.parse(directory, files);

+ 3 - 7
jplag.frontend.java/src/main/java/de/jplag/java/Parser.java

@@ -1,12 +1,9 @@
 package de.jplag.java;
 
-import static java.util.stream.Collectors.toList;
-
 import java.io.File;
 import java.util.Arrays;
 
 import de.jplag.AbstractParser;
-import de.jplag.ErrorConsumer;
 import de.jplag.TokenList;
 
 public class Parser extends AbstractParser {
@@ -14,16 +11,15 @@ public class Parser extends AbstractParser {
 
     /**
      * Creates the parser.
-     * @param errorConsumer is the consumer for any occurring errors.
      */
-    public Parser(ErrorConsumer errorConsumer) {
-        super(errorConsumer);
+    public Parser() {
+        super();
     }
 
     public TokenList parse(File directory, String[] files) {
         tokens = new TokenList();
         errors = 0;
-        var pathedFiles = Arrays.stream(files).map(it -> new File(directory, it)).collect(toList());
+        var pathedFiles = Arrays.stream(files).map(it -> new File(directory, it)).toList();
         errors += new JavacAdapter().parseFiles(directory, pathedFiles, this);
         return tokens;
     }

+ 3 - 5
jplag.frontend.kotlin/src/main/java/de/jplag/kotlin/KotlinParserAdapter.java

@@ -11,7 +11,6 @@ import org.antlr.v4.runtime.tree.ParseTree;
 import org.antlr.v4.runtime.tree.ParseTreeWalker;
 
 import de.jplag.AbstractParser;
-import de.jplag.ErrorConsumer;
 import de.jplag.TokenConstants;
 import de.jplag.TokenList;
 import de.jplag.kotlin.grammar.KotlinLexer;
@@ -25,10 +24,9 @@ public class KotlinParserAdapter extends AbstractParser {
 
     /**
      * Creates the KotlinParserAdapter
-     * @param consumer the ErrorConsumer that parser errors are passed on to.
      */
-    public KotlinParserAdapter(ErrorConsumer consumer) {
-        super(consumer);
+    public KotlinParserAdapter() {
+        super();
     }
 
     /**
@@ -66,7 +64,7 @@ public class KotlinParserAdapter extends AbstractParser {
                 treeWalker.walk(listener, parseTree);
             }
         } catch (IOException exception) {
-            getErrorConsumer().addError("Parsing Error in '%s': %s%s".formatted(fileName, File.separator, exception));
+            logger.error("Parsing Error in '{}': {}{}", fileName, File.separator, exception);
             return false;
         }
         return true;

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

@@ -2,7 +2,6 @@ package de.jplag.kotlin;
 
 import java.io.File;
 
-import de.jplag.ErrorConsumer;
 import de.jplag.TokenList;
 
 /**
@@ -16,8 +15,8 @@ public class Language implements de.jplag.Language {
     private static final String[] FILE_EXTENSIONS = {".kt"};
     private final KotlinParserAdapter parserAdapter;
 
-    public Language(ErrorConsumer consumer) {
-        this.parserAdapter = new KotlinParserAdapter(consumer);
+    public Language() {
+        this.parserAdapter = new KotlinParserAdapter();
     }
 
     @Override
@@ -49,24 +48,4 @@ public class Language implements de.jplag.Language {
     public boolean hasErrors() {
         return parserAdapter.hasErrors();
     }
-
-    @Override
-    public boolean supportsColumns() {
-        return true;
-    }
-
-    @Override
-    public boolean isPreformatted() {
-        return true;
-    }
-
-    @Override
-    public boolean usesIndex() {
-        return false;
-    }
-
-    @Override
-    public int numberOfTokens() {
-        return KotlinTokenConstants.NUMBER_DIFF_TOKENS;
-    }
 }

+ 3 - 5
jplag.frontend.kotlin/src/test/java/de/jplag/kotlin/KotlinFrontendTest.java

@@ -21,7 +21,6 @@ import de.jplag.Token;
 import de.jplag.TokenConstants;
 import de.jplag.TokenList;
 import de.jplag.TokenPrinter;
-import de.jplag.testutils.TestErrorConsumer;
 
 class KotlinFrontendTest {
 
@@ -54,8 +53,7 @@ class KotlinFrontendTest {
 
     @BeforeEach
     void setup() {
-        TestErrorConsumer consumer = new TestErrorConsumer();
-        language = new Language(consumer);
+        language = new Language();
     }
 
     @Test
@@ -78,7 +76,7 @@ class KotlinFrontendTest {
      */
     @Test
     void testTokenToString() {
-        var missingTokens = IntStream.range(0, language.numberOfTokens())
+        var missingTokens = IntStream.range(0, KotlinTokenConstants.NUMBER_DIFF_TOKENS)
                 .mapToObj(type -> new KotlinToken(type, NOT_SET_STRING, NOT_SET, NOT_SET, NOT_SET))
                 .filter(token -> token.type2string().contains("UNKNOWN")).toList();
 
@@ -158,7 +156,7 @@ class KotlinFrontendTest {
     private void testTokenCoverage(TokenList tokens, String fileName) {
         var foundTokens = StreamSupport.stream(tokens.allTokens().spliterator(), true).mapToInt(Token::getType).sorted().distinct().toArray();
         // Exclude SEPARATOR_TOKEN, as it does not occur
-        var allTokens = IntStream.range(0, language.numberOfTokens()).filter(i -> i != TokenConstants.SEPARATOR_TOKEN).toArray();
+        var allTokens = IntStream.range(0, KotlinTokenConstants.NUMBER_DIFF_TOKENS).filter(i -> i != TokenConstants.SEPARATOR_TOKEN).toArray();
 
         if (allTokens.length > foundTokens.length) {
             var diffLine = IntStream.range(0, allTokens.length)

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

@@ -2,15 +2,14 @@ package de.jplag.python3;
 
 import java.io.File;
 
-import de.jplag.ErrorConsumer;
 import de.jplag.TokenList;
 
 public class Language implements de.jplag.Language {
 
     private final Parser parser;
 
-    public Language(ErrorConsumer errorConsumer) {
-        parser = new Parser(errorConsumer);
+    public Language() {
+        parser = new Parser();
     }
 
     @Override
@@ -42,24 +41,4 @@ public class Language implements de.jplag.Language {
     public boolean hasErrors() {
         return this.parser.hasErrors();
     }
-
-    @Override
-    public boolean supportsColumns() {
-        return true;
-    }
-
-    @Override
-    public boolean isPreformatted() {
-        return true;
-    }
-
-    @Override
-    public boolean usesIndex() {
-        return false;
-    }
-
-    @Override
-    public int numberOfTokens() {
-        return Python3TokenConstants.NUM_DIFF_TOKENS;
-    }
 }

+ 7 - 9
jplag.frontend.python-3/src/main/java/de/jplag/python3/Parser.java

@@ -13,7 +13,6 @@ import org.antlr.v4.runtime.tree.ParseTree;
 import org.antlr.v4.runtime.tree.ParseTreeWalker;
 
 import de.jplag.AbstractParser;
-import de.jplag.ErrorConsumer;
 import de.jplag.TokenList;
 import de.jplag.python3.grammar.Python3Lexer;
 import de.jplag.python3.grammar.Python3Parser;
@@ -26,21 +25,20 @@ public class Parser extends AbstractParser {
 
     /**
      * Creates the parser.
-     * @param errorConsumer is the consumer for any occurring errors.
      */
-    public Parser(ErrorConsumer errorConsumer) {
-        super(errorConsumer);
+    public Parser() {
+        super();
     }
 
     public TokenList parse(File directory, String[] files) {
         tokens = new TokenList();
         errors = 0;
-        for (int i = 0; i < files.length; i++) {
-            getErrorConsumer().print(null, "Parsing file " + files[i]);
-            if (!parseFile(directory, files[i])) {
+        for (String file : files) {
+            logger.trace("Parsing file {}", file);
+            if (!parseFile(directory, file)) {
                 errors++;
             }
-            tokens.addToken(new Python3Token(Python3TokenConstants.FILE_END, files[i], -1, -1, -1));
+            tokens.addToken(new Python3Token(Python3TokenConstants.FILE_END, file, -1, -1, -1));
         }
         return tokens;
     }
@@ -71,7 +69,7 @@ public class Parser extends AbstractParser {
             }
 
         } catch (IOException e) {
-            getErrorConsumer().addError("Parsing Error in '" + file + "':\n" + e.getMessage());
+            logger.error("Parsing Error in '" + file + "': " + e.getMessage(), e);
             return false;
         }
 

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

@@ -2,7 +2,6 @@ package de.jplag.rlang;
 
 import java.io.File;
 
-import de.jplag.ErrorConsumer;
 import de.jplag.TokenList;
 
 /**
@@ -16,8 +15,8 @@ public class Language implements de.jplag.Language {
     private static final String[] FILE_EXTENSION = {".R", ".r"};
     private final RParserAdapter parserAdapter;
 
-    public Language(ErrorConsumer consumer) {
-        this.parserAdapter = new RParserAdapter(consumer);
+    public Language() {
+        this.parserAdapter = new RParserAdapter();
     }
 
     @Override
@@ -49,24 +48,4 @@ public class Language implements de.jplag.Language {
     public boolean hasErrors() {
         return parserAdapter.hasErrors();
     }
-
-    @Override
-    public boolean supportsColumns() {
-        return true;
-    }
-
-    @Override
-    public boolean isPreformatted() {
-        return true;
-    }
-
-    @Override
-    public boolean usesIndex() {
-        return false;
-    }
-
-    @Override
-    public int numberOfTokens() {
-        return RTokenConstants.NUM_DIFF_TOKENS;
-    }
 }

+ 3 - 5
jplag.frontend.rlang/src/main/java/de/jplag/rlang/RParserAdapter.java

@@ -11,7 +11,6 @@ import org.antlr.v4.runtime.tree.ParseTree;
 import org.antlr.v4.runtime.tree.ParseTreeWalker;
 
 import de.jplag.AbstractParser;
-import de.jplag.ErrorConsumer;
 import de.jplag.TokenList;
 import de.jplag.rlang.grammar.RFilter;
 import de.jplag.rlang.grammar.RLexer;
@@ -28,10 +27,9 @@ public class RParserAdapter extends AbstractParser implements RTokenConstants {
 
     /**
      * Creates the RParserAdapter
-     * @param errorConsumer the ErrorConsumer that parser errors are passed on to.
      */
-    public RParserAdapter(ErrorConsumer errorConsumer) {
-        super(errorConsumer);
+    public RParserAdapter() {
+        super();
     }
 
     /**
@@ -77,7 +75,7 @@ public class RParserAdapter extends AbstractParser implements RTokenConstants {
                 treeWalker.walk(new JplagRListener(this), parseTree);
             }
         } catch (IOException exception) {
-            getErrorConsumer().addError("Parsing Error in '" + fileName + "':" + File.separator + exception);
+            logger.error("Parsing Error in '" + fileName + "': " + File.separator + exception.getMessage(), exception);
             return false;
         }
         return true;

+ 2 - 4
jplag.frontend.rlang/src/test/java/de/jplag/rlang/RFrontendTest.java

@@ -19,9 +19,8 @@ import de.jplag.Token;
 import de.jplag.TokenConstants;
 import de.jplag.TokenList;
 import de.jplag.TokenPrinter;
-import de.jplag.testutils.TestErrorConsumer;
 
-public class RFrontendTest {
+class RFrontendTest {
 
     /**
      * Regular expression for lines that contain no code.
@@ -41,8 +40,7 @@ public class RFrontendTest {
 
     @BeforeEach
     void setup() {
-        TestErrorConsumer consumer = new TestErrorConsumer();
-        language = new Language(consumer);
+        language = new Language();
     }
 
     @Test

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

@@ -2,14 +2,13 @@ package de.jplag.scheme;
 
 import java.io.File;
 
-import de.jplag.ErrorConsumer;
 import de.jplag.TokenList;
 
 public class Language implements de.jplag.Language {
     private final de.jplag.scheme.Parser parser;
 
-    public Language(ErrorConsumer program) {
-        parser = new Parser(program);
+    public Language() {
+        parser = new Parser();
     }
 
     @Override
@@ -32,21 +31,6 @@ public class Language implements de.jplag.Language {
         return 13;
     }
 
-    @Override
-    public boolean supportsColumns() {
-        return true;
-    }
-
-    @Override
-    public boolean isPreformatted() {
-        return true;
-    }
-
-    @Override
-    public boolean usesIndex() {
-        return false;
-    }
-
     @Override
     public TokenList parse(File dir, String[] files) {
         return this.parser.parse(dir, files);
@@ -56,9 +40,4 @@ public class Language implements de.jplag.Language {
     public boolean hasErrors() {
         return this.parser.hasErrors();
     }
-
-    @Override
-    public int numberOfTokens() {
-        return SchemeTokenConstants.NUM_DIFF_TOKENS;
-    }
 }

+ 6 - 8
jplag.frontend.scheme/src/main/java/de/jplag/scheme/Parser.java

@@ -3,7 +3,6 @@ package de.jplag.scheme;
 import java.io.File;
 
 import de.jplag.AbstractParser;
-import de.jplag.ErrorConsumer;
 import de.jplag.TokenList;
 
 public class Parser extends AbstractParser {
@@ -13,19 +12,18 @@ public class Parser extends AbstractParser {
 
     /**
      * Creates the parser.
-     * @param errorConsumer is the consumer for any occurring errors.
      */
-    public Parser(ErrorConsumer errorConsumer) {
-        super(errorConsumer);
+    public Parser() {
+        super();
     }
 
     public TokenList parse(File directory, String[] files) {
         tokens = new TokenList();
         errors = 0;
-        for (int i = 0; i < files.length; i++) {
-            currentFile = files[i];
-            getErrorConsumer().print(null, "Parsing file " + files[i]);
-            if (!SchemeParser.parseFile(directory, files[i], null, this))
+        for (String file : files) {
+            currentFile = file;
+            logger.trace("Parsing file {}", file);
+            if (!SchemeParser.parseFile(directory, file, null, this))
                 errors++;
             tokens.addToken(new SchemeToken(SchemeTokenConstants.FILE_END, currentFile));
         }

+ 2 - 2
jplag.frontend.scheme/src/main/javacc/Scheme.jj

@@ -78,10 +78,10 @@ public class SchemeParser implements SchemeTokenConstants {
         try {
             parser.Program();
         } catch (ParseException e) {
-            parserX.getErrorConsumer().addError("  Parsing Error in '" + fileName + "':\n  " + e.getMessage() + "\n");
+            parserX.logger.error("Parsing Error in '" + fileName + "': " + e.getMessage());
             return false;
         } catch (TokenMgrException e) {
-            parserX.getErrorConsumer().addError("  Scanning Error in '" + fileName + "':\n  " + e.getMessage() + "\n");
+            parserX.logger.error("Scanning Error in '" + fileName + "': " + e.getMessage());
             return false;
         }
         return true;

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

@@ -10,6 +10,7 @@ import antlr.LexerSharedInputState;
  * This object contains the data associated with an input stream of characters. Multiple lexers share a single
  * LexerSharedInputState to lex the same input stream.
  */
+@SuppressWarnings({"java:S1104", "java:S2387"})
 public class InputState extends LexerSharedInputState {
     public int column = 0;
     public int tokenColumn = 0;

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

@@ -1,17 +1,15 @@
-
 package de.jplag.text;
 
 import java.io.File;
 
-import de.jplag.ErrorConsumer;
 import de.jplag.TokenList;
 
 public class Language implements de.jplag.Language {
 
     private final Parser parser;
 
-    public Language(ErrorConsumer errorConsumer) {
-        parser = new Parser(errorConsumer);
+    public Language() {
+        parser = new Parser();
     }
 
     @Override
@@ -44,23 +42,8 @@ public class Language implements de.jplag.Language {
         return this.parser.hasErrors();
     }
 
-    @Override
-    public boolean supportsColumns() {
-        return true;
-    }
-
     @Override
     public boolean isPreformatted() {
         return false;
     }
-
-    @Override
-    public boolean usesIndex() {
-        return false;
-    }
-
-    @Override
-    public int numberOfTokens() {
-        return parser.serial;
-    }
 }

+ 6 - 14
jplag.frontend.text/src/main/java/de/jplag/text/Parser.java

@@ -4,20 +4,14 @@ import java.io.File;
 import java.io.FileInputStream;
 import java.util.Hashtable;
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 import antlr.Token;
 
 import de.jplag.AbstractParser;
-import de.jplag.ErrorConsumer;
 import de.jplag.TokenConstants;
 import de.jplag.TokenList;
 
 public class Parser extends AbstractParser {
 
-    private static final Logger logger = LoggerFactory.getLogger(Parser.class);
-
     protected Hashtable<String, Integer> table = new Hashtable<>();
     protected int serial = 1; // 0 is FILE_END token
 
@@ -29,17 +23,16 @@ public class Parser extends AbstractParser {
 
     /**
      * Creates the parser.
-     * @param errorConsumer is the consumer for any occurring errors.
      */
-    public Parser(ErrorConsumer errorConsumer) {
-        super(errorConsumer);
+    public Parser() {
+        super();
     }
 
     public TokenList parse(File directory, String[] files) {
         tokens = new TokenList();
         errors = 0;
         for (String file : files) {
-            getErrorConsumer().print("", "Parsing file " + file);
+            logger.trace("Parsing file {}", file);
             if (!parseFile(directory, file))
                 errors++;
             tokens.addToken(new TextToken(TokenConstants.FILE_END, file, this));
@@ -60,8 +53,7 @@ public class Parser extends AbstractParser {
             return;
         runOut = true;
         errors++;
-        errorConsumer.print("ERROR: Out of serials!", null);
-        logger.error("de.jplag.text.Parser: ERROR: Out of serials!");
+        logger.error("Out of serials!");
     }
 
     private boolean parseFile(File directory, String file) {
@@ -86,8 +78,8 @@ public class Parser extends AbstractParser {
             // close file
             inputStream.close();
         } catch (Exception e) {
-            getErrorConsumer().addError(
-                    "  Parsing Error in '" + file + "' (line " + (inputState != null ? "" + inputState.getLine() : "") + "):\n  " + e.getMessage());
+            logger.error("Parsing Error in '" + file + "' (line " + (inputState != null ? "" + inputState.getLine() : "") + "):\n  " + e.getMessage(),
+                    e);
             return false;
         }
         return true;

+ 0 - 79
jplag/src/main/java/de/jplag/ErrorCollector.java

@@ -1,79 +0,0 @@
-package de.jplag;
-
-import static de.jplag.options.Verbosity.LONG;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import de.jplag.options.JPlagOptions;
-import de.jplag.options.Verbosity;
-
-/**
- * Error collector class that collects errors but also allows printing the collected errors.
- * @author Timur Saglam
- */
-public class ErrorCollector implements ErrorConsumer {
-    // TODO DF We should replace *all* usages of the ErrorConsumer by a suitable logger
-    private final Logger logger = LoggerFactory.getLogger("JPlag");
-    private final List<String> collectedErrors; // List of errors that occurred during the execution of the errorConsumer.
-    private final JPlagOptions options;
-    private String currentSubmissionName;
-
-    public ErrorCollector(JPlagOptions options) {
-        this.options = options;
-        collectedErrors = new ArrayList<>();
-        currentSubmissionName = "<Unknown submission>";
-    }
-
-    @Override
-    public void addError(String errorMessage) {
-        collectedErrors.add("[" + currentSubmissionName + "] " + errorMessage);
-        print(null, "\t" + errorMessage);
-    }
-
-    @Override
-    public void print(String message, String longMessage) {
-        if (message == null && longMessage == null) {
-            throw new IllegalArgumentException("At least one message parameter needs to be non-null!");
-        }
-        Verbosity verbosity = options.getVerbosity();
-        if (message != null) {
-            logger.info(message);
-        }
-        if (longMessage != null && verbosity == LONG) {
-            logger.info(longMessage);
-        }
-    }
-
-    /**
-     * Print all collected errors messages in a list-like fashion.
-     */
-    public void printCollectedErrors() {
-        StringBuilder errorReport = new StringBuilder();
-        logger.error("The following errors occured: ");
-        for (String message : collectedErrors) {
-            errorReport.append(message);
-            errorReport.append('\n');
-        }
-
-        logger.error(errorReport.toString());
-    }
-
-    /**
-     * Updates the name of the currently processed submission.
-     * @param currentSubmissionName is the name.
-     */
-    public void setCurrentSubmissionName(String currentSubmissionName) {
-        this.currentSubmissionName = currentSubmissionName;
-    }
-
-    /**
-     * @return true if there is at least one error.
-     */
-    public boolean hasErrors() {
-        return !collectedErrors.isEmpty();
-    }
-}

+ 7 - 6
jplag/src/main/java/de/jplag/GreedyStringTiling.java

@@ -98,7 +98,8 @@ public class GreedyStringTiling {
     }
 
     private JPlagComparison swapAndCompare(Submission firstSubmission, Submission secondSubmission, boolean isBaseCodeComparison) {
-        Submission smallerSubmission, largerSubmission;
+        Submission smallerSubmission;
+        Submission largerSubmission;
         if (firstSubmission.getTokenList().size() > secondSubmission.getTokenList().size()) {
             smallerSubmission = secondSubmission;
             largerSubmission = firstSubmission;
@@ -187,11 +188,11 @@ public class GreedyStringTiling {
                 }
             }
             for (int i = matches.size() - 1; i >= 0; i--) {
-                int x = matches.get(i).getStartOfFirst();  // Beginning of/in sequence A
-                int y = matches.get(i).getStartOfSecond();  // Beginning of/in sequence B
-                comparison.addMatch(x, y, matches.get(i).getLength());
+                int x = matches.get(i).startOfFirst();  // Beginning of/in sequence A
+                int y = matches.get(i).startOfSecond();  // Beginning of/in sequence B
+                comparison.addMatch(x, y, matches.get(i).length());
                 // in order that "Match" will be newly build (because reusing)
-                for (int j = matches.get(i).getLength(); j > 0; j--) {
+                for (int j = matches.get(i).length(); j > 0; j--) {
                     first.getToken(x).setMarked(true); // mark all Tokens!
                     second.getToken(y).setMarked(true);
                     if (isBaseCodeComparison) {
@@ -220,7 +221,7 @@ public class GreedyStringTiling {
     /**
      * Disable finding a match at separator tokens and basecode matches for non-basecode comparisons.
      * @param tokenList Tokens to mark.
-     * @param isBaseCodeComparison Whether the {@link Token#basecode} matches should be enabled for matching.
+     * @param isBaseCodeComparison Whether the {@link Token#isBasecode()} matches should be enabled for matching.
      */
     private void markTokens(TokenList tokenList, boolean isBaseCodeComparison) {
         for (Token token : tokenList.allTokens()) {

+ 11 - 15
jplag/src/main/java/de/jplag/JPlag.java

@@ -29,14 +29,13 @@ import de.jplag.strategy.ParallelComparisonStrategy;
  * This class coordinates the whole errorConsumer flow.
  */
 public class JPlag {
-    private static final Logger logger = LoggerFactory.getLogger("JPlag");
+    private static final Logger logger = LoggerFactory.getLogger(JPlag.class);
 
     private final JPlagOptions options;
 
     private final Language language;
     private final ComparisonStrategy comparisonStrategy;
     private final GreedyStringTiling coreAlgorithm; // Contains the comparison logic.
-    private final ErrorCollector errorCollector;
     private final Set<String> excludedFileNames;
 
     /**
@@ -45,7 +44,6 @@ public class JPlag {
      */
     public JPlag(JPlagOptions options) {
         this.options = options;
-        errorCollector = new ErrorCollector(options);
         coreAlgorithm = new GreedyStringTiling(options);
         language = initializeLanguage();
         comparisonStrategy = initializeComparisonStrategy(options.getComparisonMode());
@@ -61,9 +59,9 @@ public class JPlag {
         try (BufferedReader reader = new BufferedReader(new FileReader(exclusionFileName, JPlagOptions.CHARSET))) {
             final var excludedFileNames = reader.lines().collect(Collectors.toSet());
             if (options.getVerbosity() == LONG) {
-                errorCollector.print(null, "Excluded files:");
+                logger.info("Excluded files:");
                 for (var excludedFilename : excludedFileNames) {
-                    errorCollector.print(null, " " + excludedFilename);
+                    logger.info(excludedFilename);
                 }
             }
             return excludedFileNames;
@@ -80,7 +78,7 @@ public class JPlag {
      */
     public JPlagResult run() throws ExitException {
         // Parse and validate submissions.
-        SubmissionSetBuilder builder = new SubmissionSetBuilder(language, options, errorCollector, excludedFileNames);
+        SubmissionSetBuilder builder = new SubmissionSetBuilder(language, options, excludedFileNames);
         SubmissionSet submissionSet = builder.buildSubmissionSet();
 
         if (submissionSet.hasBaseCode()) {
@@ -94,9 +92,9 @@ public class JPlag {
 
         // Compare valid submissions.
         JPlagResult result = comparisonStrategy.compareSubmissions(submissionSet);
-        errorCollector.print("\nTotal time for comparing submissions: " + TimeUtil.formatDuration(result.getDuration()), null);
+        logger.info("\nTotal time for comparing submissions: " + TimeUtil.formatDuration(result.getDuration()));
 
-        result.setClusteringResult(ClusteringFactory.getClusterings(result.getComparisons(), options.getClusteringOptions()));
+        result.setClusteringResult(ClusteringFactory.getClusterings(result.getAllComparisons(), options.getClusteringOptions()));
 
         return result;
     }
@@ -112,19 +110,17 @@ public class JPlag {
         LanguageOption languageOption = this.options.getLanguageOption();
 
         try {
-            Constructor<?> constructor = Class.forName(languageOption.getClassPath()).getConstructor(ErrorConsumer.class);
-            Object[] constructorParams = {errorCollector};
-
-            Language language = (Language) constructor.newInstance(constructorParams);
+            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());
+            logger.info("Initialized language {}", language.getName());
             return language;
         } catch (NoSuchMethodException | SecurityException | ClassNotFoundException | InstantiationException | IllegalAccessException
                 | IllegalArgumentException | InvocationTargetException e) {
-            e.printStackTrace();
-            throw new IllegalStateException("Language instantiation failed:" + e.getMessage());
+            logger.error(e.getMessage(), e);
+            throw new IllegalStateException("Language instantiation failed:" + e.getMessage(), e);
         }
 
     }

+ 2 - 1
jplag/src/main/java/de/jplag/JPlagComparison.java

@@ -40,6 +40,7 @@ public class JPlagComparison implements Comparator<JPlagComparison> { // FIXME T
         return Float.compare(comparison2.similarity(), comparison1.similarity()); // comparison2 first!
     }
 
+    // TODO DF: hashCode is not implemented!!
     @Override
     public boolean equals(Object other) {
         if (!(other instanceof JPlagComparison)) {
@@ -76,7 +77,7 @@ public class JPlagComparison implements Comparator<JPlagComparison> { // FIXME T
         int numberOfMatchedTokens = 0;
 
         for (Match match : matches) {
-            numberOfMatchedTokens += match.getLength();
+            numberOfMatchedTokens += match.length();
         }
 
         return numberOfMatchedTokens;

+ 27 - 9
jplag/src/main/java/de/jplag/JPlagResult.java

@@ -1,9 +1,11 @@
 package de.jplag;
 
 import java.util.List;
+import java.util.function.Function;
 
 import de.jplag.clustering.ClusteringResult;
 import de.jplag.options.JPlagOptions;
+import de.jplag.options.SimilarityMetric;
 
 /**
  * Encapsulates the results of a comparison of a set of source code submissions.
@@ -21,6 +23,7 @@ public class JPlagResult {
     private final int[] similarityDistribution; // 10-element array representing the similarity distribution of the detected matches.
 
     private List<ClusteringResult<Submission>> clusteringResult;
+    private final int SIMILARITY_DISTRIBUTION_SIZE = 10;
 
     public JPlagResult(List<JPlagComparison> comparisons, SubmissionSet submissions, long durationInMillis, JPlagOptions options) {
         this.comparisons = comparisons;
@@ -47,7 +50,7 @@ public class JPlagResult {
     /**
      * @return a list of all comparisons sorted by percentage (descending)
      */
-    public List<JPlagComparison> getComparisons() {
+    public List<JPlagComparison> getAllComparisons() {
         return comparisons;
     }
 
@@ -93,22 +96,34 @@ public class JPlagResult {
     }
 
     /**
-     * Returns the similarity distribution of detected matches in a 10-element array. Each entry represents the absolute
-     * frequency of matches whose similarity lies within the respective interval. Intervals: 0: [0% - 10%), 1: [10% - 20%),
-     * 2: [20% - 30%), ..., 9: [90% - 100%]
+     * For the {@link SimilarityMetric} JPlag was run with, this returns the similarity distribution of detected matches in
+     * a 10-element array. Each entry represents the absolute frequency of matches whose similarity lies within the
+     * respective interval. Intervals: 0: [0% - 10%), 1: [10% - 20%), 2: [20% - 30%), ..., 9: [90% - 100%]
      * @return the similarity distribution array.
      */
     public int[] getSimilarityDistribution() {
         return similarityDistribution;
     }
 
+    /**
+     * For the {@link SimilarityMetric#MAX} that is built in to every {@link JPlagComparison}, this returns the similarity
+     * distribution of detected matches in a 10-element array. Each entry represents the absolute frequency of matches whose
+     * similarity lies within the respective interval. Intervals: 0: [0% - 10%), 1: [10% - 20%), 2: [20% - 30%), ..., 9:
+     * [90% - 100%]
+     * @return the similarity distribution array. When JPlag was run with the {@link SimilarityMetric#MAX}, this will return
+     * the same distribution as {@link JPlagResult#getSimilarityDistribution()}
+     */
+    public int[] getMaxSimilarityDistribution() {
+        return calculateDistributionFor(comparisons, (JPlagComparison::maximalSimilarity));
+    }
+
     public List<ClusteringResult<Submission>> getClusteringResult() {
         return this.clusteringResult;
     }
 
     @Override
     public String toString() {
-        return String.format("JPlagResult { comparisons: %d, duration: %d ms, language: %s, submissions: %d }", getComparisons().size(),
+        return String.format("JPlagResult { comparisons: %d, duration: %d ms, language: %s, submissions: %d }", getAllComparisons().size(),
                 getDuration(), getOptions().getLanguageOption(), submissions.numberOfSubmissions());
     }
 
@@ -116,11 +131,14 @@ public class JPlagResult {
      * Note: Before, comparisons with a similarity below the given threshold were also included in the similarity matrix.
      */
     private int[] calculateSimilarityDistribution(List<JPlagComparison> comparisons) {
-        int[] similarityDistribution = new int[10];
-
-        comparisons.stream().map(JPlagComparison::similarity).map(percent -> percent / 10).map(Float::intValue).map(index -> index == 10 ? 9 : index)
-                .forEach(index -> similarityDistribution[index]++);
+        return calculateDistributionFor(comparisons, JPlagComparison::similarity);
+    }
 
+    private int[] calculateDistributionFor(List<JPlagComparison> comparisons, Function<JPlagComparison, Float> similarityExtractor) {
+        int[] similarityDistribution = new int[SIMILARITY_DISTRIBUTION_SIZE];
+        comparisons.stream().map(similarityExtractor).map(percent -> percent / SIMILARITY_DISTRIBUTION_SIZE).map(Float::intValue)
+                .map(index -> index == SIMILARITY_DISTRIBUTION_SIZE ? SIMILARITY_DISTRIBUTION_SIZE - 1 : index)
+                .forEach(index -> similarityDistribution[SIMILARITY_DISTRIBUTION_SIZE - 1 - index]++);
         return similarityDistribution;
     }
 }

+ 7 - 42
jplag/src/main/java/de/jplag/Match.java

@@ -2,57 +2,22 @@ package de.jplag;
 
 /**
  * Represents two sections of two submissions that are similar.
+ * @param startOfFirst is the starting token index in the first submission.
+ * @param startOfSecond is the starting token index in the second submission.
+ * @param length is the length of these similar sections (number of tokens).
  */
-public class Match {
-
-    private final int startOfFirst;
-    private final int startOfSecond;
-    private final int length;
-
-    /**
-     * Creates a match.
-     * @param startOfFirst is the starting token index in the first submission.
-     * @param startOfSecond is the starting token index in the second submission.
-     * @param length is the length of these similar sections (number of tokens).
-     */
-    public Match(int startOfFirst, int startOfSecond, int length) {
-        this.startOfFirst = startOfFirst;
-        this.startOfSecond = startOfSecond;
-        this.length = length;
-    }
-
-    /**
-     * @return the starting index in the first submission.
-     */
-    public int getStartOfFirst() {
-        return startOfFirst;
-    }
-
-    /**
-     * @return the starting index in the second submission.
-     */
-    public int getStartOfSecond() {
-        return startOfSecond;
-    }
-
-    /**
-     * @return the length of the similar sections, meaning the number of tokens.
-     */
-    public int getLength() {
-        return length;
-    }
-
+public record Match(int startOfFirst, int startOfSecond, int length) {
     /**
      * Checks if two matches overlap.
      * @return true if they do.
      */
-    public final boolean overlap(int otherStartOfFirst, int otherStartOfSecond, int oLength) {
+    public boolean overlap(int otherStartOfFirst, int otherStartOfSecond, int otherLength) {
         if (startOfFirst < otherStartOfFirst) {
             if ((otherStartOfFirst - startOfFirst) < length) {
                 return true;
             }
         } else {
-            if ((startOfFirst - otherStartOfFirst) < oLength) {
+            if ((startOfFirst - otherStartOfFirst) < otherLength) {
                 return true;
             }
         }
@@ -60,7 +25,7 @@ public class Match {
         if (startOfSecond < otherStartOfSecond) {
             return (otherStartOfSecond - startOfSecond) < length;
         } else {
-            return (startOfSecond - otherStartOfSecond) < oLength;
+            return (startOfSecond - otherStartOfSecond) < otherLength;
         }
     }
 }

+ 13 - 10
jplag/src/main/java/de/jplag/Submission.java

@@ -6,10 +6,15 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.Collection;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 /**
  * Represents a single submission. A submission can contain multiple files.
  */
 public class Submission implements Comparable<Submission> {
+    private static final Logger logger = LoggerFactory.getLogger(Submission.class);
+
     /**
      * Directory name for storing submission files with parse errors if so requested.
      */
@@ -51,7 +56,6 @@ public class Submission implements Comparable<Submission> {
     private JPlagComparison baseCodeComparison;
 
     private final Language language;
-    private final ErrorCollector errorCollector;
 
     /**
      * Creates a submission.
@@ -60,19 +64,18 @@ public class Submission implements Comparable<Submission> {
      * @param isNew states whether the submission must be checked for plagiarism.
      * @param files are the files of the submissions, if the root is a single file it should just contain one file.
      * @param language is the language of the submission.
-     * @param errorCollector is the interface for error reporting.
      */
-    public Submission(String name, File submissionRootFile, boolean isNew, Collection<File> files, Language language, ErrorCollector errorCollector) {
+    public Submission(String name, File submissionRootFile, boolean isNew, Collection<File> files, Language language) {
         this.name = name;
         this.submissionRootFile = submissionRootFile;
         this.isNew = isNew;
         this.files = files;
         this.language = language;
-        this.errorCollector = errorCollector;
     }
 
     @Override
     public int compareTo(Submission other) {
+        // TODO DF: "equals(Object obj)" should be overridden along with the "compareTo(T obj)" method
         return name.compareTo(other.name);
     }
 
@@ -184,7 +187,7 @@ public class Submission implements Comparable<Submission> {
      * String representation of the code files contained in this submission, annotated with all tokens.
      * @return the annotated code as string.
      */
-    public String getTokenAnnotatedSourcCode() {
+    public String getTokenAnnotatedSourceCode() {
         return TokenPrinter.printTokens(tokenList, files, submissionRootFile);
     }
 
@@ -204,12 +207,12 @@ public class Submission implements Comparable<Submission> {
             try {
                 Files.copy(file.toPath(), new File(submissionDirectory, file.getName()).toPath());
             } catch (IOException exception) {
-                errorCollector.print("Error copying file: " + exception + "\n", null);
+                logger.error("Error copying file: " + exception.getMessage(), exception);
             }
         }
     }
 
-    private File createSubdirectory(File parent, String... subdirectoryNames) {
+    private static File createSubdirectory(File parent, String... subdirectoryNames) {
         File subdirectory = parent;
         for (String name : subdirectoryNames) {
             subdirectory = new File(subdirectory, name);
@@ -246,8 +249,8 @@ public class Submission implements Comparable<Submission> {
      * @return Whether parsing was successful.
      */
     /* package-private */ boolean parse(boolean debugParser) {
-        if (files == null || files.size() == 0) {
-            errorCollector.print("ERROR: nothing to parse for submission \"" + name + "\"", null);
+        if (files == null || files.isEmpty()) {
+            logger.error("ERROR: nothing to parse for submission \"{}\"", name);
             tokenList = null;
             hasErrors = true; // invalidate submission
             return false;
@@ -258,7 +261,7 @@ public class Submission implements Comparable<Submission> {
         tokenList = language.parse(submissionRootFile, relativeFilePaths);
         if (!language.hasErrors()) {
             if (tokenList.size() < 3) {
-                errorCollector.print("Submission \"" + name + "\" is too short!", null);
+                logger.error("Submission \"{}\" is too short!", name);
                 tokenList = null;
                 hasErrors = true; // invalidate submission
                 return false;

+ 29 - 36
jplag/src/main/java/de/jplag/SubmissionSet.java

@@ -1,12 +1,12 @@
 package de.jplag;
 
-import static java.util.stream.Collectors.toList;
-
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Optional;
 import java.util.stream.Collectors;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import de.jplag.exceptions.BasecodeException;
 import de.jplag.exceptions.ExitException;
 import de.jplag.exceptions.SubmissionException;
@@ -16,6 +16,8 @@ import de.jplag.options.JPlagOptions;
  * Collection of all submissions and their basecode if it exists. Parses all submissions upon creation.
  */
 public class SubmissionSet {
+    private static final Logger logger = LoggerFactory.getLogger(SubmissionSet.class);
+
     /**
      * Submissions to check for plagiarism.
      */
@@ -26,22 +28,19 @@ public class SubmissionSet {
     /**
      * Base code submission if it exists.
      */
-    private final Optional<Submission> baseCodeSubmission;
+    private final Submission baseCodeSubmission;
 
-    private final ErrorCollector errorCollector;
     private final JPlagOptions options;
     private int errors = 0;
     private String currentSubmissionName;
 
     /**
      * @param submissions Submissions to check for plagiarism.
-     * @param baseCode Base code submission if it exists.
+     * @param baseCode Base code submission if it exists or {@code null}.
      */
-    public SubmissionSet(List<Submission> submissions, Optional<Submission> baseCode, ErrorCollector errorCollector, JPlagOptions options)
-            throws ExitException {
+    public SubmissionSet(List<Submission> submissions, Submission baseCode, JPlagOptions options) throws ExitException {
         this.allSubmissions = submissions;
         this.baseCodeSubmission = baseCode;
-        this.errorCollector = errorCollector;
         this.options = options;
         parseAllSubmissions();
         this.submissions = filterValidSubmissions();
@@ -52,7 +51,7 @@ public class SubmissionSet {
      * @return Whether a basecode is available for this collection.
      */
     public boolean hasBaseCode() {
-        return baseCodeSubmission.isPresent();
+        return baseCodeSubmission != null;
     }
 
     /**
@@ -62,10 +61,10 @@ public class SubmissionSet {
      * @see #hasBaseCode
      */
     public Submission getBaseCode() {
-        if (baseCodeSubmission.isEmpty()) {
+        if (baseCodeSubmission == null) {
             throw new AssertionError("Querying a non-existing basecode submission.");
         }
-        return baseCodeSubmission.get();
+        return baseCodeSubmission;
     }
 
     /**
@@ -96,21 +95,18 @@ public class SubmissionSet {
     }
 
     private List<Submission> filterInvalidSubmissions() {
-        return allSubmissions.stream().filter(Submission::hasErrors).collect(toList());
+        return allSubmissions.stream().filter(Submission::hasErrors).toList();
     }
 
     private void parseAllSubmissions() throws ExitException {
         try {
             parseSubmissions(allSubmissions);
-            if (baseCodeSubmission.isPresent()) {
-                parseBaseCodeSubmission(baseCodeSubmission.get()); // cannot use ifPresent because of throws declaration
+            if (baseCodeSubmission != null) {
+                parseBaseCodeSubmission(baseCodeSubmission);
             }
         } catch (OutOfMemoryError exception) {
             throw new SubmissionException("Out of memory during parsing of submission \"" + currentSubmissionName + "\"", exception);
         }
-        if (errorCollector.hasErrors()) {
-            errorCollector.printCollectedErrors();
-        }
     }
 
     /**
@@ -118,16 +114,15 @@ public class SubmissionSet {
      */
     private void parseBaseCodeSubmission(Submission baseCode) throws BasecodeException {
         long startTime = System.currentTimeMillis();
-        errorCollector.print("----- Parsing basecode submission: " + baseCode.getName(), null);
+        logger.info("----- Parsing basecode submission: " + baseCode.getName());
         if (!baseCode.parse(options.isDebugParser())) {
-            errorCollector.printCollectedErrors();
             throw new BasecodeException("Could not successfully parse basecode submission!");
         } else if (baseCode.getNumberOfTokens() < options.getMinimumTokenMatch()) {
             throw new BasecodeException("Basecode submission contains fewer tokens than minimum match length allows!");
         }
-        errorCollector.print("Basecode submission parsed!", null);
+        logger.info("Basecode submission parsed!");
         long duration = System.currentTimeMillis() - startTime;
-        errorCollector.print(null, "Time for parsing Basecode: " + TimeUtil.formatDuration(duration));
+        logger.trace("Time for parsing Basecode: " + TimeUtil.formatDuration(duration));
 
     }
 
@@ -136,7 +131,7 @@ public class SubmissionSet {
      */
     private void parseSubmissions(List<Submission> submissions) {
         if (submissions.isEmpty()) {
-            errorCollector.print("No submissions to parse!", null);
+            logger.warn("No submissions to parse!");
             return;
         }
 
@@ -146,16 +141,15 @@ public class SubmissionSet {
         for (Submission submission : submissions) {
             boolean ok;
 
-            errorCollector.print(null, "------ Parsing submission: " + submission.getName());
+            logger.trace("------ Parsing submission: " + submission.getName());
             currentSubmissionName = submission.getName();
-            errorCollector.setCurrentSubmissionName(currentSubmissionName);
 
             if (!(ok = submission.parse(options.isDebugParser()))) {
                 errors++;
             }
 
             if (submission.getTokenList() != null && submission.getNumberOfTokens() < options.getMinimumTokenMatch()) {
-                errorCollector.addError("Submission contains fewer tokens than minimum match length allows!");
+                logger.error("Submission {} contains fewer tokens than minimum match length allows!", currentSubmissionName);
                 submission.setTokenList(null);
                 tooShort++;
                 ok = false;
@@ -163,31 +157,30 @@ public class SubmissionSet {
             }
 
             if (ok) {
-                errorCollector.print(null, "OK");
+                logger.trace("OK");
             } else {
-                errorCollector.print(null, "ERROR -> Submission removed");
+                logger.error("ERROR -> Submission {} removed", currentSubmissionName);
             }
         }
 
         int validSubmissions = submissions.size() - errors - tooShort;
-        errorCollector.print(validSubmissions + " submissions parsed successfully!", null);
-        errorCollector.print(errors + " parser error" + (errors != 1 ? "s!" : "!") + "", null);
-        errorCollector.print(tooShort + " too short submission" + (tooShort != 1 ? "s!" : "!") + "", null);
+        logger.trace(validSubmissions + " submissions parsed successfully!");
+        logger.trace(errors + " parser error" + (errors != 1 ? "s!" : "!"));
+        logger.trace(tooShort + " too short submission" + (tooShort != 1 ? "s!" : "!"));
         printDetails(submissions, startTime, tooShort);
-        errorCollector.print("", null); // new line
     }
 
     private void printDetails(List<Submission> submissions, long startTime, int tooShort) {
         if (tooShort == 1) {
-            errorCollector.print(null, tooShort + " submission is not valid because it contains fewer tokens than minimum match length allows.");
+            logger.trace(tooShort + " submission is not valid because it contains fewer tokens than minimum match length allows.");
         } else if (tooShort > 1) {
-            errorCollector.print(null, tooShort + " submissions are not valid because they contain fewer tokens than minimum match length allows.");
+            logger.trace(tooShort + " submissions are not valid because they contain fewer tokens than minimum match length allows.");
         }
 
         long duration = System.currentTimeMillis() - startTime;
         String timePerSubmission = submissions.size() > 0 ? Long.toString(duration / submissions.size()) : "n/a";
-        errorCollector.print(null, "Total time for parsing: " + TimeUtil.formatDuration(duration));
-        errorCollector.print(null, "Time per parsed submission: " + timePerSubmission + " msec");
+        logger.trace("Total time for parsing: " + TimeUtil.formatDuration(duration));
+        logger.trace("Time per parsed submission: " + timePerSubmission + " msec");
     }
 
 }

+ 20 - 11
jplag/src/main/java/de/jplag/SubmissionSetBuilder.java

@@ -2,7 +2,16 @@ package de.jplag;
 
 import java.io.File;
 import java.io.IOException;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
 import java.util.function.Function;
 
 import org.slf4j.Logger;
@@ -24,20 +33,17 @@ public class SubmissionSetBuilder {
 
     private final Language language;
     private final JPlagOptions options;
-    private final ErrorCollector errorCollector;
     private final Set<String> excludedFileNames; // Set of file names to be excluded in comparison.
 
     /**
      * Creates a builder for submission sets.
      * @param language is the language of the submissions.
      * @param options are the configured options.
-     * @param errorCollector is the interface for error reporting.
      * @param excludedFileNames a list of file names to be excluded
      */
-    public SubmissionSetBuilder(Language language, JPlagOptions options, ErrorCollector errorCollector, Set<String> excludedFileNames) {
+    public SubmissionSetBuilder(Language language, JPlagOptions options, Set<String> excludedFileNames) {
         this.language = language;
         this.options = options;
-        this.errorCollector = errorCollector;
         this.excludedFileNames = excludedFileNames;
     }
 
@@ -69,7 +75,7 @@ public class SubmissionSetBuilder {
 
         // Merge everything in a submission set.
         List<Submission> submissions = new ArrayList<>(foundSubmissions.values());
-        return new SubmissionSet(submissions, baseCodeSubmission, errorCollector, options);
+        return new SubmissionSet(submissions, baseCodeSubmission.orElse(null), options);
     }
 
     /**
@@ -115,8 +121,9 @@ public class SubmissionSetBuilder {
         // former use can be removed without affecting the result of the checks.
         oldSubmissionDirectories.removeAll(commonRootdirectories);
         for (File rootDirectory : commonRootdirectories) {
-            logger.warn("Root directory \"" + rootDirectory.toString()
-                    + "\" is specified both for plagiarism checking and for prior submissions, will perform plagiarism checking only.");
+            logger.warn(
+                    "Root directory \"{}\" is specified both for plagiarism checking and for prior submissions, will perform plagiarism checking only.",
+                    rootDirectory);
         }
     }
 
@@ -125,7 +132,7 @@ public class SubmissionSetBuilder {
         // Extract the basecode submission if necessary.
         Optional<Submission> baseCodeSubmission = Optional.empty();
         if (options.hasBaseCode()) {
-            String baseCodeName = options.getBaseCodeSubmissionName().get();
+            String baseCodeName = options.getBaseCodeSubmissionName().orElseThrow();
             Submission baseCode = loadBaseCodeAsPath(baseCodeName);
             if (baseCode == null) {
                 int numberOfRootDirectories = submissionDirectories.size() + oldSubmissionDirectories.size();
@@ -134,13 +141,15 @@ public class SubmissionSetBuilder {
                 }
 
                 // There is one root directory, and the submissionDirectories variable has been checked to be non-empty.
-                // That set thus contains the the one and only root directory.
+                // That set thus contains the one and only root directory.
                 File rootDirectory = submissionDirectories.iterator().next();
 
                 // Single root-directory, try the legacy way of specifying basecode.
                 baseCode = loadBaseCodeViaName(baseCodeName, rootDirectory, foundSubmissions);
             }
+            // TODO DF: Here the method assumes that baseCode can be null. later, this is not assumed anymore
             baseCodeSubmission = Optional.ofNullable(baseCode);
+
             logger.info("Basecode directory \"{}\" will be used.", baseCode.getName());
 
             // Basecode may also be registered as a user submission. If so, remove the latter.
@@ -280,7 +289,7 @@ public class SubmissionSetBuilder {
         }
 
         submissionFile = makeCanonical(submissionFile, it -> new SubmissionException("Cannot create submission: " + submissionName, it));
-        return new Submission(submissionName, submissionFile, isNew, parseFilesRecursively(submissionFile), language, errorCollector);
+        return new Submission(submissionName, submissionFile, isNew, parseFilesRecursively(submissionFile), language);
     }
 
     /**

+ 1 - 3
jplag/src/main/java/de/jplag/clustering/ClusteringFactory.java

@@ -4,7 +4,6 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
-import java.util.stream.Collectors;
 
 import de.jplag.JPlagComparison;
 import de.jplag.Submission;
@@ -42,8 +41,7 @@ public class ClusteringFactory {
     }
 
     private static ClusteringResult<Submission> removeBadClusters(final ClusteringResult<Submission> clustering) {
-        List<Cluster<Submission>> filtered = clustering.getClusters().stream().filter(cluster -> !cluster.isBadCluster())
-                .collect(Collectors.toList());
+        List<Cluster<Submission>> filtered = clustering.getClusters().stream().filter(cluster -> !cluster.isBadCluster()).toList();
         return new ClusteringResult<>(filtered, clustering.getCommunityStrength());
     }
 }

+ 1 - 1
jplag/src/main/java/de/jplag/clustering/ClusteringResult.java

@@ -66,7 +66,7 @@ public class ClusteringResult<T> {
         }
         List<Cluster<Integer>> clusters = new ArrayList<>(clustering.size());
         float communityStrength = 0;
-        if (clustering.size() > 0) {
+        if (!clustering.isEmpty()) {
             RealMatrix percentagesOfSimilaritySums = new Array2DRowRealMatrix(clustering.size(), clustering.size());
             percentagesOfSimilaritySums = percentagesOfSimilaritySums.scalarMultiply(0);
             for (int i = 0; i < numberOfSubmissions; i++) {

+ 3 - 2
jplag/src/main/java/de/jplag/clustering/IntegerMapping.java

@@ -3,14 +3,15 @@ package de.jplag.clustering;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Creates a mapping of any set to integers
  */
 public class IntegerMapping<T> {
 
-    private HashMap<T, Integer> mapping;
-    private List<T> backMapping;
+    private final Map<T, Integer> mapping;
+    private final List<T> backMapping;
     private int size = 0;
 
     public IntegerMapping(int initialCapacity) {

+ 1 - 1
jplag/src/main/java/de/jplag/clustering/PreprocessorHelper.java

@@ -24,7 +24,7 @@ public class PreprocessorHelper {
         mapping = new IntegerMapping<>(connections.length);
         RealMatrix similarity = new Array2DRowRealMatrix(connections, true);
         for (int i = 0; i < similarity.getRowDimension(); i++) {
-            if (DoubleStream.of(similarity.getRow(i)).filter(x -> x > 0).findAny().isPresent()) {
+            if (DoubleStream.of(similarity.getRow(i)).anyMatch(x -> x > 0)) {
                 rowList.add(i);
                 mapping.map(i);
             }

+ 23 - 51
jplag/src/main/java/de/jplag/clustering/algorithm/AgglomerativeClustering.java

@@ -1,11 +1,6 @@
 package de.jplag.clustering.algorithm;
 
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.List;
-import java.util.PriorityQueue;
-import java.util.Set;
+import java.util.*;
 import java.util.stream.Collectors;
 
 import org.apache.commons.math3.linear.RealMatrix;
@@ -32,32 +27,29 @@ public class AgglomerativeClustering implements GenericClusteringAlgorithm {
         // clusters, those need to be ignored.
         PriorityQueue<ClusterConnection> similarities;
 
-        {
-            // initialization
+        // initialization
 
-            List<Cluster> initialClusters = new ArrayList<>(size);
-            for (int i = 0; i < size; i++) {
-                List<Integer> members = new ArrayList<>();
-                members.add(i);
-                Cluster cluster = new Cluster(members);
-                initialClusters.add(cluster);
-                clusters.add(cluster);
-            }
+        List<Cluster> initialClusters = new ArrayList<>(size);
+        for (int i = 0; i < size; i++) {
+            List<Integer> members = new ArrayList<>();
+            members.add(i);
+            Cluster cluster = new Cluster(members);
+            initialClusters.add(cluster);
+            clusters.add(cluster);
+        }
 
-            List<ClusterConnection> initialSimilarities = new ArrayList<>(size * (size - 1) / 2);
+        List<ClusterConnection> initialSimilarities = new ArrayList<>(size * (size - 1) / 2);
 
-            for (int leftIndex = 0; leftIndex < initialClusters.size(); leftIndex++) {
-                Cluster leftCluster = initialClusters.get(leftIndex);
-                for (int rightIndex = leftIndex + 1; rightIndex < initialClusters.size(); rightIndex++) {
-                    Cluster rightCluster = initialClusters.get(rightIndex);
-                    initialSimilarities
-                            .add(new ClusterConnection(leftCluster, rightCluster, (float) similarityMatrix.getEntry(leftIndex, rightIndex)));
-                }
+        for (int leftIndex = 0; leftIndex < initialClusters.size(); leftIndex++) {
+            Cluster leftCluster = initialClusters.get(leftIndex);
+            for (int rightIndex = leftIndex + 1; rightIndex < initialClusters.size(); rightIndex++) {
+                Cluster rightCluster = initialClusters.get(rightIndex);
+                initialSimilarities.add(new ClusterConnection(leftCluster, rightCluster, (float) similarityMatrix.getEntry(leftIndex, rightIndex)));
             }
-
-            similarities = new PriorityQueue<>(initialSimilarities);
         }
 
+        similarities = new PriorityQueue<>(initialSimilarities);
+
         while (clusters.size() > 1) {
             ClusterConnection nearest = similarities.poll();
             // TODO Check that nearest is never null
@@ -70,8 +62,8 @@ public class AgglomerativeClustering implements GenericClusteringAlgorithm {
             }
             clusters.remove(nearest.left);
             clusters.remove(nearest.right);
-            nearest.left.getSubmissions().addAll(nearest.right.getSubmissions());
-            Cluster combined = new Cluster(nearest.left.getSubmissions());
+            nearest.left.submissions().addAll(nearest.right.submissions());
+            Cluster combined = new Cluster(nearest.left.submissions());
             for (Cluster otherCluster : clusters) {
                 float similarity = options.getAgglomerativeInterClusterSimilarity().clusterSimilarity(combined.submissions, otherCluster.submissions,
                         similarityMatrix);
@@ -80,21 +72,10 @@ public class AgglomerativeClustering implements GenericClusteringAlgorithm {
             clusters.add(combined);
         }
 
-        return clusters.stream().map(Cluster::getSubmissions).collect(Collectors.toList());
+        return clusters.stream().map(Cluster::submissions).collect(Collectors.toList());
     }
 
-    private static class ClusterConnection implements Comparable<ClusterConnection> {
-
-        private final Cluster left;
-        private final Cluster right;
-        private final float similarity;
-
-        public ClusterConnection(Cluster left, Cluster right, float similarity) {
-            this.left = left;
-            this.right = right;
-            this.similarity = similarity;
-        }
-
+    private record ClusterConnection(Cluster left, Cluster right, float similarity) implements Comparable<ClusterConnection> {
         @Override
         public int compareTo(ClusterConnection other) {
             return (int) Math.signum(other.similarity - similarity);
@@ -106,16 +87,7 @@ public class AgglomerativeClustering implements GenericClusteringAlgorithm {
      * Encapsulate a list in a class because we do not want hashing based on the members but on identity only. Also a
      * cluster need a different identity than the list because the lists are reused.
      */
-    private static class Cluster {
-        private final List<Integer> submissions;
-
-        public Cluster(List<Integer> submissions) {
-            this.submissions = submissions;
-        }
-
-        public List<Integer> getSubmissions() {
-            return submissions;
-        }
+    private record Cluster(List<Integer> submissions) {
     }
 
 }

+ 2 - 4
jplag/src/main/java/de/jplag/clustering/algorithm/BayesianOptimization.java

@@ -159,7 +159,7 @@ public class BayesianOptimization {
             } else {
                 // GPR
                 GaussianProcess gpr = fit(testedCoordinates, observations);
-                if (debug) {
+                if (debug && logger.isDebugEnabled()) {
                     logger.debug(gpr.toString(minima, maxima, 100, 25, 0));
                 }
                 // TODO Check that best is not null here
@@ -169,9 +169,7 @@ public class BayesianOptimization {
             OptimizationResult<T> result = objectiveFunction.apply(coordinates);
             result.params = coordinates;
             observations.add(result.getScore());
-            if (best == null) {
-                best = result;
-            } else if (result.score > best.score) {
+            if (best == null || result.score > best.score) {
                 best = result;
             }
         }

+ 1 - 1
jplag/src/main/java/de/jplag/clustering/algorithm/GaussianProcess.java

@@ -74,7 +74,7 @@ public class GaussianProcess {
      */
     public static GaussianProcess fit(List<RealVector> observedCoordinates, double[] observations, double noise, boolean normalize,
             double[] lengthScale) {
-        if (observedCoordinates.size() < 1)
+        if (observedCoordinates.isEmpty())
             throw new IllegalArgumentException("Observed coordinates are empty");
         if (observedCoordinates.size() != observations.length) {
             throw new IllegalArgumentException(MessageFormat.format("Observed coordinates and observations are of different dimensions {0} and {1}",

+ 3 - 3
jplag/src/main/java/de/jplag/clustering/algorithm/InterClusterSimilarity.java

@@ -1,7 +1,7 @@
 package de.jplag.clustering.algorithm;
 
 import java.util.List;
-import java.util.function.BiFunction;
+import java.util.function.BinaryOperator;
 
 import org.apache.commons.math3.linear.RealMatrix;
 
@@ -11,9 +11,9 @@ public enum InterClusterSimilarity {
     AVERAGE(0, Float::sum);
 
     private final float neutralElement;
-    private final BiFunction<Float, Float, Float> accumulator;
+    private final BinaryOperator<Float> accumulator;
 
-    InterClusterSimilarity(float neutralElement, BiFunction<Float, Float, Float> accumulator) {
+    InterClusterSimilarity(float neutralElement, BinaryOperator<Float> accumulator) {
         this.neutralElement = neutralElement;
         this.accumulator = accumulator;
     }

+ 2 - 2
jplag/src/main/java/de/jplag/clustering/algorithm/SpectralClustering.java

@@ -105,6 +105,7 @@ public class SpectralClustering implements GenericClusteringAlgorithm {
     private Collection<Collection<Integer>> cluster(int numberOfClusters, int dimension, List<Integer> eigenValueIds, EigenDecomposition ed) {
         RealMatrix concatenatedEigenVectors = new Array2DRowRealMatrix(dimension, numberOfClusters);
         concatenatedEigenVectors.walkInOptimizedOrder(new DefaultRealMatrixChangingVisitor() {
+            @Override
             public double visit(int row, int column, double value) {
                 int eigenVectorId = eigenValueIds.get(column);
                 RealVector eigenVector = ed.getEigenvector(eigenVectorId);
@@ -113,8 +114,7 @@ public class SpectralClustering implements GenericClusteringAlgorithm {
         });
 
         List<ClusterableEigenVector> normRows = IntStream.range(0, dimension).filter(i -> concatenatedEigenVectors.getRowVector(i).getNorm() > 0)
-                .mapToObj(row -> new ClusterableEigenVector(row, concatenatedEigenVectors.getRowVector(row).unitVector()))
-                .collect(Collectors.toList());
+                .mapToObj(row -> new ClusterableEigenVector(row, concatenatedEigenVectors.getRowVector(row).unitVector())).toList();
 
         Clusterer<ClusterableEigenVector> clusterer = new KMeansPlusPlusClusterer<>(numberOfClusters, options.getSpectralMaxKMeansIterationPerRun());
         List<? extends Cluster<ClusterableEigenVector>> clusters = clusterer.cluster(normRows);

+ 2 - 2
jplag/src/main/java/de/jplag/exceptions/ExitException.java

@@ -10,11 +10,11 @@ public abstract class ExitException extends Exception {
     @Serial
     private static final long serialVersionUID = 7091658804288889231L; // generated
 
-    public ExitException(String message) {
+    protected ExitException(String message) {
         super(message);
     }
 
-    public ExitException(String message, Throwable cause) {
+    protected ExitException(String message, Throwable cause) {
         super(message, cause);
     }
 }

+ 0 - 21
jplag/src/main/java/de/jplag/exceptions/ReportGenerationException.java

@@ -1,21 +0,0 @@
-package de.jplag.exceptions;
-
-import java.io.Serial;
-
-/**
- * Exceptions for problems with the report generation that lead to an preemptive exit.
- */
-public class ReportGenerationException extends ExitException {
-
-    @Serial
-    private static final long serialVersionUID = 628097693006117102L; // generated
-
-    public ReportGenerationException(String message) {
-        super(message);
-    }
-
-    public ReportGenerationException(String message, Throwable cause) {
-        super(message, cause);
-    }
-
-}

+ 2 - 2
jplag/src/main/java/de/jplag/options/JPlagOptions.java

@@ -289,10 +289,10 @@ public class JPlagOptions {
 
     public void setSimilarityThreshold(float similarityThreshold) {
         if (similarityThreshold > 100) {
-            logger.warn("Maximum threshold of 100 used instead of " + similarityThreshold);
+            logger.warn("Maximum threshold of 100 used instead of {}", similarityThreshold);
             this.similarityThreshold = 100;
         } else if (similarityThreshold < 0) {
-            logger.warn("Minimum threshold of 0 used instead of " + similarityThreshold);
+            logger.warn("Minimum threshold of 0 used instead of {}", similarityThreshold);
             this.similarityThreshold = 0;
         } else {
             this.similarityThreshold = similarityThreshold;

+ 6 - 6
jplag/src/main/java/de/jplag/reporting/jsonfactory/JsonFactory.java

@@ -31,12 +31,12 @@ public class JsonFactory {
     public static List<String> getJsonStrings(JPlagReport jPlagReport) {
         List<String> jsonReports = new ArrayList<>();
         try {
-            jsonReports.add(mapper.writeValueAsString(jPlagReport.getOverviewReport()));
-            for (ComparisonReport comparisonReport : jPlagReport.getComparisons()) {
+            jsonReports.add(mapper.writeValueAsString(jPlagReport.overviewReport()));
+            for (ComparisonReport comparisonReport : jPlagReport.comparisons()) {
                 jsonReports.add(mapper.writeValueAsString(comparisonReport));
             }
         } catch (JsonProcessingException e) {
-            logger.error("Error converting object to json " + e.getMessage());
+            logger.error("Error converting object to json " + e.getMessage(), e);
         }
         return jsonReports;
     }
@@ -48,9 +48,9 @@ public class JsonFactory {
     public static boolean saveJsonFiles(JPlagReport jPlagReport, String folderPath) {
         try {
             ObjectMapper mapper = new ObjectMapper();
-            mapper.writeValue(Path.of(folderPath, "overview.json").toFile(), jPlagReport.getOverviewReport());
-            for (ComparisonReport report : jPlagReport.getComparisons()) {
-                String name = report.getFirstSubmissionId().concat("-").concat(report.getSecondSubmissionId()).concat(".json");
+            mapper.writeValue(Path.of(folderPath, "overview.json").toFile(), jPlagReport.overviewReport());
+            for (ComparisonReport report : jPlagReport.comparisons()) {
+                String name = report.firstSubmissionId().concat("-").concat(report.secondSubmissionId()).concat(".json");
                 mapper.writeValue(Path.of(folderPath, name).toFile(), report);
             }
         } catch (IOException e) {

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

@@ -5,14 +5,17 @@ import java.io.File;
 import java.io.FileReader;
 import java.io.IOException;
 import java.text.SimpleDateFormat;
-import java.util.*;
-import java.util.stream.Collectors;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import de.jplag.*;
 import de.jplag.reporting.reportobject.mapper.ClusteringResultMapper;
+import de.jplag.reporting.reportobject.mapper.MetricMapper;
 import de.jplag.reporting.reportobject.model.*;
 import de.jplag.reporting.reportobject.model.Match;
 
@@ -23,6 +26,7 @@ public class ReportObjectFactory {
 
     private static final Logger logger = LoggerFactory.getLogger(ReportObjectFactory.class);
     private static final ClusteringResultMapper clusteringResultMapper = new ClusteringResultMapper();
+    private static final MetricMapper metricMapper = new MetricMapper();
 
     /**
      * Converts a JPlagResult to a JPlagReport.
@@ -38,8 +42,7 @@ public class ReportObjectFactory {
      * Generates an Overview DTO of a JPlagResult.
      */
     private static OverviewReport generateOverviewReport(JPlagResult result) {
-        int numberOfComparisons = result.getOptions().getMaximumNumberOfComparisons();
-        List<JPlagComparison> comparisons = result.getComparisons(numberOfComparisons);
+        List<JPlagComparison> comparisons = getComparisons(result);
         OverviewReport overviewReport = new OverviewReport();
 
         // TODO: Consider to treat entries that were checked differently from old entries with prior work.
@@ -72,8 +75,7 @@ public class ReportObjectFactory {
      */
     private static List<ComparisonReport> generateComparisonReports(JPlagResult result) {
         List<ComparisonReport> comparisons = new ArrayList<>();
-        int numberOfComparisons = result.getOptions().getMaximumNumberOfComparisons();
-        result.getComparisons(numberOfComparisons).forEach(comparison -> comparisons.add( //
+        getComparisons(result).forEach(comparison -> comparisons.add( //
                 new ComparisonReport(comparison.getFirstSubmission().getName(), //
                         comparison.getSecondSubmission().getName(), //
                         comparison.similarity(), //
@@ -84,9 +86,14 @@ public class ReportObjectFactory {
         return comparisons;
     }
 
+    private static List<JPlagComparison> getComparisons(JPlagResult result) {
+        int numberOfComparisons = result.getOptions().getMaximumNumberOfComparisons();
+        return result.getComparisons(numberOfComparisons);
+    }
+
     private static List<Match> convertMatchesToReportMatches(JPlagResult result, JPlagComparison comparison, List<de.jplag.Match> matches) {
-        return matches.stream().map(match -> convertMatchToReportMatch(comparison, match, result.getOptions().getLanguage().usesIndex()))
-                .collect(Collectors.toList());
+        return matches.stream().map(match -> convertMatchToReportMatch(comparison, match, !result.getOptions().getLanguage().supportsColumns()))
+                .toList();
     }
 
     /**
@@ -113,29 +120,13 @@ public class ReportObjectFactory {
         return names;
     }
 
-    // Currently, only one metric can be obtained.
-
     /**
-     * Gets the used metric in a JPlag comparison.
+     * Gets the used metrics in a JPlag comparison. As Max Metric is included in every JPlag run, this always include Max
+     * Metric.
      * @return A list contains Metric DTOs.
      */
     private static List<Metric> getMetrics(JPlagResult result) {
-        List<Metric> metrics = new ArrayList<>();
-        metrics.add(new Metric(result.getOptions().getSimilarityMetric().name(), result.getOptions().getSimilarityThreshold(),
-                intArrayToList(result.getSimilarityDistribution()), getTopComparisons(result.getComparisons())));
-        return metrics;
-    }
-
-    /**
-     * Converts JPlagComparison to a DTO for displaying only comparisons. See
-     * {@link #generateComparisonReports(JPlagResult)} for a more detailed representation of a comparison.
-     * @return List containing TopComparison DTOs.
-     */
-    private static List<TopComparison> getTopComparisons(List<JPlagComparison> comparisons) {
-        List<TopComparison> topComparisons = new ArrayList<>();
-        comparisons.forEach(comparison -> topComparisons.add(
-                new TopComparison(comparison.getFirstSubmission().getName(), comparison.getSecondSubmission().getName(), comparison.similarity())));
-        return topComparisons;
+        return List.of(metricMapper.getAverageMetric(result), metricMapper.getMaxMetric(result));
     }
 
     /**
@@ -143,7 +134,7 @@ public class ReportObjectFactory {
      * @return A list containing FilesOfSubmission DTOs.
      */
     private static List<FilesOfSubmission> getFilesForSubmission(Submission submission) {
-        return submission.getFiles().stream().map(file -> new FilesOfSubmission(file.getName(), readFileLines(file))).collect(Collectors.toList());
+        return submission.getFiles().stream().map(file -> new FilesOfSubmission(file.getName(), readFileLines(file))).toList();
     }
 
     /**
@@ -153,19 +144,19 @@ public class ReportObjectFactory {
      * @param usesIndex Indicates whether the language uses indexes.
      * @return A Match DTO.
      */
-    private static Match convertMatchToReportMatch(JPlagComparison comparison, de.jplag.Match match, Boolean usesIndex) {
+    private static Match convertMatchToReportMatch(JPlagComparison comparison, de.jplag.Match match, boolean usesIndex) {
         TokenList tokensFirst = comparison.getFirstSubmission().getTokenList();
         TokenList tokensSecond = comparison.getSecondSubmission().getTokenList();
-        Token startTokenFirst = tokensFirst.getToken(match.getStartOfFirst());
-        Token endTokenFirst = tokensFirst.getToken(match.getStartOfFirst() + match.getLength() - 1);
-        Token startTokenSecond = tokensSecond.getToken(match.getStartOfSecond());
-        Token endTokenSecond = tokensSecond.getToken(match.getStartOfSecond() + match.getLength() - 1);
+        Token startTokenFirst = tokensFirst.getToken(match.startOfFirst());
+        Token endTokenFirst = tokensFirst.getToken(match.startOfFirst() + match.length() - 1);
+        Token startTokenSecond = tokensSecond.getToken(match.startOfSecond());
+        Token endTokenSecond = tokensSecond.getToken(match.startOfSecond() + match.length() - 1);
 
         int startFirst = usesIndex ? startTokenFirst.getIndex() : startTokenFirst.getLine();
         int endFirst = usesIndex ? endTokenFirst.getIndex() : endTokenFirst.getLine();
         int startSecond = usesIndex ? startTokenSecond.getIndex() : startTokenSecond.getLine();
         int endSecond = usesIndex ? endTokenSecond.getIndex() : endTokenSecond.getLine();
-        int tokens = match.getLength();
+        int tokens = match.length();
 
         return new Match(startTokenFirst.getFile(), startTokenSecond.getFile(), startFirst, endFirst, startSecond, endSecond, tokens);
     }
@@ -178,7 +169,7 @@ public class ReportObjectFactory {
                 lines.add(line);
             }
         } catch (IOException exception) {
-            logger.error("Could not read file: " + exception.getMessage());
+            logger.error("Could not read file: " + exception.getMessage(), exception);
         }
         return lines;
     }
@@ -188,8 +179,4 @@ public class ReportObjectFactory {
         Date date = new Date();
         return dateFormat.format(date);
     }
-
-    private static List<Integer> intArrayToList(int[] array) {
-        return Arrays.stream(array).boxed().collect(Collectors.toList());
-    }
 }

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

@@ -0,0 +1,54 @@
+package de.jplag.reporting.reportobject.mapper;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import de.jplag.JPlagComparison;
+import de.jplag.JPlagResult;
+import de.jplag.Messages;
+import de.jplag.options.SimilarityMetric;
+import de.jplag.reporting.reportobject.model.Metric;
+import de.jplag.reporting.reportobject.model.TopComparison;
+
+/**
+ * Extracts and maps metrics from the JPlagResult to the corresponding JSON DTO
+ */
+public class MetricMapper {
+    public Metric getAverageMetric(JPlagResult result) {
+        return new Metric(SimilarityMetric.AVG.name(), intArrayToList(result.getSimilarityDistribution()), getTopComparisons(getComparisons(result)),
+                Messages.getString("SimilarityMetric.Avg.Description"));
+    }
+
+    public Metric getMaxMetric(JPlagResult result) {
+        return new Metric(SimilarityMetric.MAX.name(), intArrayToList(result.getMaxSimilarityDistribution()),
+                getMaxSimilarityTopComparisons(getComparisons(result)), Messages.getString("SimilarityMetric.Max.Description"));
+    }
+
+    private List<JPlagComparison> getComparisons(JPlagResult result) {
+        int maxNumberOfComparisons = result.getOptions().getMaximumNumberOfComparisons();
+        return result.getComparisons(maxNumberOfComparisons);
+    }
+
+    private static List<Integer> intArrayToList(int[] array) {
+        return Arrays.stream(array).boxed().collect(Collectors.toList());
+    }
+
+    private static List<TopComparison> getTopComparisons(List<JPlagComparison> comparisons, Function<JPlagComparison, Float> similarityExtractor) {
+        List<TopComparison> topComparisons = new ArrayList<>();
+        comparisons.forEach(comparison -> topComparisons.add(new TopComparison(comparison.getFirstSubmission().getName(),
+                comparison.getSecondSubmission().getName(), similarityExtractor.apply(comparison))));
+        return topComparisons;
+    }
+
+    private static List<TopComparison> getTopComparisons(List<JPlagComparison> comparisons) {
+        return getTopComparisons(comparisons, JPlagComparison::similarity);
+    }
+
+    private static List<TopComparison> getMaxSimilarityTopComparisons(List<JPlagComparison> comparisons) {
+        return getTopComparisons(comparisons, JPlagComparison::maximalSimilarity);
+    }
+
+}

+ 4 - 44
jplag/src/main/java/de/jplag/reporting/reportobject/model/ComparisonReport.java

@@ -4,26 +4,10 @@ import java.util.List;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
 
-public class ComparisonReport {
-
-    @JsonProperty("first_submission_id")
-    private final String firstSubmissionId;
-
-    @JsonProperty("second_submission_id")
-    private final String secondSubmissionId;
-
-    @JsonProperty("match_percentage")
-    private final float matchPercentage;
-
-    @JsonProperty("files_of_first_submission")
-    private final List<FilesOfSubmission> filesOfFirstSubmission;
-
-    @JsonProperty("files_of_second_submission")
-    private final List<FilesOfSubmission> filesOfSecondSubmission;
-
-    @JsonProperty("matches")
-    private final List<Match> matches;
-
+public record ComparisonReport(@JsonProperty("first_submission_id") String firstSubmissionId,
+        @JsonProperty("second_submission_id") String secondSubmissionId, @JsonProperty("match_percentage") float matchPercentage,
+        @JsonProperty("files_of_first_submission") List<FilesOfSubmission> filesOfFirstSubmission,
+        @JsonProperty("files_of_second_submission") List<FilesOfSubmission> filesOfSecondSubmission, @JsonProperty("matches") List<Match> matches) {
     public ComparisonReport(String firstSubmissionId, String secondSubmissionId, float matchPercentage,
             List<FilesOfSubmission> filesOfFirstSubmission, List<FilesOfSubmission> filesOfSecondSubmission, List<Match> matches) {
 
@@ -34,28 +18,4 @@ public class ComparisonReport {
         this.filesOfSecondSubmission = List.copyOf(filesOfSecondSubmission);
         this.matches = matches;
     }
-
-    public String getFirstSubmissionId() {
-        return firstSubmissionId;
-    }
-
-    public String getSecondSubmissionId() {
-        return secondSubmissionId;
-    }
-
-    public float getMatchPercentage() {
-        return matchPercentage;
-    }
-
-    public List<FilesOfSubmission> getFilesOfFirstSubmission() {
-        return filesOfFirstSubmission;
-    }
-
-    public List<FilesOfSubmission> getFilesOfSecondSubmission() {
-        return filesOfSecondSubmission;
-    }
-
-    public List<Match> getMatches() {
-        return matches;
-    }
 }

+ 2 - 2
jplag/src/main/java/de/jplag/reporting/reportobject/model/FilesOfSubmission.java

@@ -12,9 +12,9 @@ public class FilesOfSubmission {
     @JsonProperty("lines")
     private final List<String> lines;
 
-    public FilesOfSubmission(String fileName, List<String> file_code) {
+    public FilesOfSubmission(String fileName, List<String> fileCode) {
         this.fileName = fileName;
-        this.lines = List.copyOf(file_code);
+        this.lines = List.copyOf(fileCode);
     }
 
     public String getFileName() {

+ 1 - 12
jplag/src/main/java/de/jplag/reporting/reportobject/model/JPlagReport.java

@@ -2,20 +2,9 @@ package de.jplag.reporting.reportobject.model;
 
 import java.util.List;
 
-public class JPlagReport {
-    private final OverviewReport overviewReport;
-    private final List<ComparisonReport> comparisons;
-
+public record JPlagReport(OverviewReport overviewReport, List<ComparisonReport> comparisons) {
     public JPlagReport(OverviewReport overviewReport, List<ComparisonReport> comparisons) {
         this.overviewReport = overviewReport;
         this.comparisons = List.copyOf(comparisons);
     }
-
-    public OverviewReport getOverviewReport() {
-        return overviewReport;
-    }
-
-    public List<ComparisonReport> getComparisons() {
-        return comparisons;
-    }
 }

+ 3 - 60
jplag/src/main/java/de/jplag/reporting/reportobject/model/Match.java

@@ -2,64 +2,7 @@ package de.jplag.reporting.reportobject.model;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
 
-public class Match {
-
-    @JsonProperty("first_file_name")
-    private final String firstFileName;
-
-    @JsonProperty("second_file_name")
-    private final String secondFileName;
-
-    @JsonProperty("start_in_first")
-    private final int startInFirst;
-
-    @JsonProperty("end_in_first")
-    private final int endInFirst;
-
-    @JsonProperty("start_in_second")
-    private final int startInSecond;
-
-    @JsonProperty("end_in_second")
-    private final int endInSecond;
-
-    @JsonProperty("tokens")
-    private final int tokens;
-
-    public Match(String firstFileName, String secondFileName, int startInFirst, int endInFirst, int startInSecond, int endInSecond, int tokens) {
-        this.firstFileName = firstFileName;
-        this.secondFileName = secondFileName;
-        this.startInFirst = startInFirst;
-        this.endInFirst = endInFirst;
-        this.startInSecond = startInSecond;
-        this.endInSecond = endInSecond;
-        this.tokens = tokens;
-    }
-
-    public String getFirstFileName() {
-        return firstFileName;
-    }
-
-    public String getSecondFileName() {
-        return secondFileName;
-    }
-
-    public int getStartInFirst() {
-        return startInFirst;
-    }
-
-    public int getEndInFirst() {
-        return endInFirst;
-    }
-
-    public int getStartInSecond() {
-        return startInSecond;
-    }
-
-    public int getEndInSecond() {
-        return endInSecond;
-    }
-
-    public int getTokens() {
-        return tokens;
-    }
+public record Match(@JsonProperty("first_file_name") String firstFileName, @JsonProperty("second_file_name") String secondFileName,
+        @JsonProperty("start_in_first") int startInFirst, @JsonProperty("end_in_first") int endInFirst,
+        @JsonProperty("start_in_second") int startInSecond, @JsonProperty("end_in_second") int endInSecond, @JsonProperty("tokens") int tokens) {
 }

+ 2 - 36
jplag/src/main/java/de/jplag/reporting/reportobject/model/Metric.java

@@ -4,40 +4,6 @@ import java.util.List;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
 
-public class Metric {
-
-    @JsonProperty("name")
-    private final String name;
-
-    @JsonProperty("threshold")
-    private final float threshold;
-
-    @JsonProperty("distribution")
-    private final List<Integer> distribution;
-
-    @JsonProperty("topComparisons")
-    private final List<TopComparison> topComparisons;
-
-    public Metric(String name, float threshold, List<Integer> distribution, List<TopComparison> topComparisons) {
-        this.name = name;
-        this.threshold = threshold;
-        this.distribution = List.copyOf(distribution);
-        this.topComparisons = List.copyOf(topComparisons);
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public float getThreshold() {
-        return threshold;
-    }
-
-    public List<Integer> getDistribution() {
-        return distribution;
-    }
-
-    public List<TopComparison> getTopComparisons() {
-        return topComparisons;
-    }
+public record Metric(@JsonProperty("name") String name, @JsonProperty("distribution") List<Integer> distribution,
+        @JsonProperty("topComparisons") List<TopComparison> topComparisons, @JsonProperty String description) {
 }

+ 2 - 29
jplag/src/main/java/de/jplag/reporting/reportobject/model/TopComparison.java

@@ -2,33 +2,6 @@ package de.jplag.reporting.reportobject.model;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
 
-public class TopComparison {
-
-    @JsonProperty("first_submission")
-    private final String firstSubmission;
-
-    @JsonProperty("second_submission")
-    private final String secondSubmission;
-
-    @JsonProperty("match_percentage")
-    private final float matchPercentage;
-
-    public TopComparison(String firstSubmission, String secondSubmission, float matchPercentage) {
-        this.firstSubmission = firstSubmission;
-        this.secondSubmission = secondSubmission;
-        this.matchPercentage = matchPercentage;
-    }
-
-    public String getFirstSubmission() {
-        return firstSubmission;
-    }
-
-    public String getSecondSubmission() {
-        return secondSubmission;
-    }
-
-    public float getMatchPercentage() {
-        return matchPercentage;
-    }
-
+public record TopComparison(@JsonProperty("first_submission") String firstSubmission, @JsonProperty("second_submission") String secondSubmission,
+        @JsonProperty("match_percentage") float matchPercentage) {
 }

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

@@ -19,9 +19,9 @@ public abstract class AbstractComparisonStrategy implements ComparisonStrategy {
 
     private final GreedyStringTiling greedyStringTiling;
 
-    protected JPlagOptions options;
+    protected final JPlagOptions options;
 
-    public AbstractComparisonStrategy(JPlagOptions options, GreedyStringTiling greedyStringTiling) {
+    protected AbstractComparisonStrategy(JPlagOptions options, GreedyStringTiling greedyStringTiling) {
         this.greedyStringTiling = greedyStringTiling;
         this.options = options;
     }
@@ -47,7 +47,7 @@ public abstract class AbstractComparisonStrategy implements ComparisonStrategy {
      */
     protected Optional<JPlagComparison> compareSubmissions(Submission first, Submission second, boolean withBaseCode) {
         JPlagComparison comparison = greedyStringTiling.compare(first, second);
-        logger.info("Comparing " + first.getName() + "-" + second.getName() + ": " + comparison.similarity());
+        logger.info("Comparing {}-{}: {}", first.getName(), second.getName(), comparison.similarity());
 
         if (options.getSimilarityMetric().isAboveThreshold(comparison, options.getSimilarityThreshold())) {
             return Optional.of(comparison);

+ 2 - 4
jplag/src/main/java/de/jplag/strategy/ComparisonMode.java

@@ -1,7 +1,5 @@
 package de.jplag.strategy;
 
-import static java.util.stream.Collectors.toList;
-
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Optional;
@@ -16,7 +14,7 @@ public enum ComparisonMode {
     private final String name;
     private final String description;
 
-    private ComparisonMode(String description) {
+    ComparisonMode(String description) {
         this.description = description;
         name = toString().toLowerCase();
     }
@@ -40,7 +38,7 @@ public enum ComparisonMode {
      * @return a collections of all mode names.
      */
     public static Collection<String> allNames() {
-        return Arrays.stream(values()).map(ComparisonMode::getName).collect(toList());
+        return Arrays.stream(values()).map(ComparisonMode::getName).toList();
     }
 
     /**

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

@@ -28,7 +28,7 @@ public class NormalComparisonStrategy extends AbstractComparisonStrategy {
         List<JPlagComparison> comparisons = new ArrayList<>();
 
         for (SubmissionTuple tuple : buildComparisonTuples(submissions)) {
-            compareSubmissions(tuple.getLeft(), tuple.getRight(), withBaseCode).ifPresent(comparisons::add);
+            compareSubmissions(tuple.left(), tuple.right(), withBaseCode).ifPresent(comparisons::add);
         }
 
         long durationInMillis = System.currentTimeMillis() - timeBeforeStartInMillis;

+ 16 - 17
jplag/src/main/java/de/jplag/strategy/ParallelComparisonStrategy.java

@@ -3,19 +3,14 @@ package de.jplag.strategy;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
+import java.util.concurrent.*;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 
-import de.jplag.GreedyStringTiling;
-import de.jplag.JPlagComparison;
-import de.jplag.JPlagResult;
-import de.jplag.Submission;
-import de.jplag.SubmissionSet;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import de.jplag.*;
 import de.jplag.options.JPlagOptions;
 
 /**
@@ -23,6 +18,8 @@ import de.jplag.options.JPlagOptions;
  * @author Timur Saglam
  */
 public class ParallelComparisonStrategy extends AbstractComparisonStrategy {
+    private static final Logger logger = LoggerFactory.getLogger("JPlag");
+
     private static final int TIMEOUT_IN_SECONDS = 5;
     private final ConcurrentMap<String, Lock> submissionLocks;
     private ExecutorService threadPool;
@@ -60,8 +57,9 @@ public class ParallelComparisonStrategy extends AbstractComparisonStrategy {
         while (successfulComparisons < tuples.size()) {
             try {
                 Thread.sleep(5);
-            } catch (InterruptedException exception) {
-                exception.printStackTrace();
+            } catch (InterruptedException e) {
+                logger.error(e.getMessage(), e);
+                Thread.currentThread().interrupt();
             }
         }
 
@@ -81,13 +79,13 @@ public class ParallelComparisonStrategy extends AbstractComparisonStrategy {
         return new Runnable() {
             @Override
             public void run() {
-                Lock leftLock = getOrCreateLock(tuple.getLeft().getName());
-                Lock rightLock = getOrCreateLock(tuple.getRight().getName());
+                Lock leftLock = getOrCreateLock(tuple.left().getName());
+                Lock rightLock = getOrCreateLock(tuple.right().getName());
                 boolean hasLeft = leftLock.tryLock();
                 boolean hasRight = hasLeft && rightLock.tryLock();
                 try {
                     if (hasLeft && hasRight) { // both locks acquired!
-                        compareSubmissions(tuple.getLeft(), tuple.getRight(), withBaseCode).ifPresent(comparisons::add);
+                        compareSubmissions(tuple.left(), tuple.right(), withBaseCode).ifPresent(comparisons::add);
                         synchronized (this) {
                             successfulComparisons++;
                         }
@@ -123,8 +121,9 @@ public class ParallelComparisonStrategy extends AbstractComparisonStrategy {
             if (!threadPool.awaitTermination(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS)) {
                 throw new IllegalStateException("Parallel comparison calculation timed out!");
             }
-        } catch (InterruptedException exception) {
-            throw new IllegalStateException("Thread pool interrupted during comparison: " + exception.getMessage());
+        } catch (InterruptedException e) {
+            logger.error(e.getMessage(), e);
+            Thread.currentThread().interrupt();
         }
     }
 

+ 2 - 15
jplag/src/main/java/de/jplag/strategy/SubmissionTuple.java

@@ -5,27 +5,14 @@ import de.jplag.Submission;
 /**
  * Tupel of source code submissions.
  */
-public class SubmissionTuple {
-    private final Submission left;
-    private final Submission right;
-
+public record SubmissionTuple(Submission left, Submission right) {
     /**
      * Creates a tupel from two submissions.
      */
-    public SubmissionTuple(Submission left, Submission right) {
+    public SubmissionTuple {
         if (left == null || right == null) {
             throw new IllegalArgumentException("Submissions cannot be null");
         }
-        this.left = left;
-        this.right = right;
-    }
-
-    public Submission getLeft() {
-        return left;
-    }
-
-    public Submission getRight() {
-        return right;
     }
 
     @Override

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

@@ -26,4 +26,6 @@ CommandLineArgument.ClusterAgglomerativeInterClusterSimilarity=How to measure th
 CommandLineArgument.ClusterPreprocessingNone=Do not use any preprocessing before clustering. Not recommended for spectral clustering.
 CommandLineArgument.ClusterPreprocessingCdf=Before clustering, the value of the cumulative distribution function of all similarities is estimated. The similarities are multiplied with these estimates. This has the effect of supressing similarities that are low compared to other similarities.
 CommandLineArgument.ClusterPreprocessingPercentile=Any similarity smaller than the given percentile will be suppressed during clustering.
-CommandLineArgument.ClusterPreprocessingThreshold=Any similarity smaller than the given threshold value will be suppressed during clustering.
+CommandLineArgument.ClusterPreprocessingThreshold=Any similarity smaller than the given threshold value will be suppressed during clustering.
+SimilarityMetric.Avg.Description = Average of both program coverages. This is the default similarity which works in most cases: Matches with a high average similarity indicate that the programs work in a very similar way.
+SimilarityMetric.Max.Description = Maximum of both program coverages. This ranking is especially useful if the programs are very different in size. This can happen when dead code was inserted to disguise the origin of the plagiarized program.

+ 3 - 1
jplag/src/main/resources/de/jplag/messages_de.properties

@@ -32,7 +32,6 @@ Report.Matches_for_X1_AND_X2=&Uuml;bereinstimmungen f&uuml;r {1_NAME} & {2_NAME}
 Report.Links=Links
 Report.INDEX=INDEX
 Report.HELP=HILFE
-Report.Distribution=Verteilung
 Report.Token_Distribution=Verteilung der Token
 
 Clusters.MIN_single_link=MIN, einfache Verkn&uuml;pfung
@@ -57,3 +56,6 @@ AllMatches.Distribution=Verteilung
 AllMatches.Length=L&auml;nge
 AllMatches.Number_of_matches=Anzahl der &Uuml;bereinstimmungen
 AllMatches.Basecode=Referenz&uuml;bereinstimmung
+
+SimilarityMetric.Avg.Description = Average of both program coverages. This is the default similarity which works in most cases: Matches with a high average similarity indicate that the programs work in a very similar way.
+SimilarityMetric.Max.Description = Maximum of both program coverages. This ranking is especially useful if the programs are very different in size. This can happen when dead code was inserted to disguise the origin of the plagiarized program.

+ 4 - 1
jplag/src/main/resources/de/jplag/messages_en.properties

@@ -32,9 +32,9 @@ Report.Matches_for_X1_AND_X2=Matches for {1_NAME} & {2_NAME}
 Report.Links=Links
 Report.INDEX=INDEX
 Report.HELP=HELP
-Report.Distribution=Distribution
 Report.Token_Distribution=Token Distribution
 
+
 Clusters.MIN_single_link=MIN, single link
 Clusters.AVR_group_average=AVR, group average
 Clusters.MAX_complete_link=MAX, complete link
@@ -57,3 +57,6 @@ AllMatches.Distribution=Distribution
 AllMatches.Length=Length
 AllMatches.Number_of_matches=No of matches
 AllMatches.Basecode=Basecode
+
+SimilarityMetric.Avg.Description = Average of both program coverages. This is the default similarity which works in most cases: Matches with a high average similarity indicate that the programs work in a very similar way.
+SimilarityMetric.Max.Description = Maximum of both program coverages. This ranking is especially useful if the programs are very different in size. This can happen when dead code was inserted to disguise the origin of the plagiarized program.

+ 3 - 2
jplag/src/main/resources/de/jplag/messages_es.properties

@@ -3,7 +3,6 @@ Report.MatchesAvg=Coincidencias clasificadas por la semejanza media
 Report.MatchesMax=Coincidencias clasificadas por semejanza máxima
 Report.MatchesMin=Coincidencias clasificadas por semejanza mínima
 Report.average_similarity=semejanza media
-Report.maximum_similarity=semejanza máxima
 Report.maximum_similarity=semejanza mínima
 Report.WhatIsThis=¿Qué es esto?
 Report.Search_Results=Resultados de la Búsqueda
@@ -32,7 +31,6 @@ Report.Matches_for_X1_AND_X2=Coinicdencias para {1_NAME} & {2_NAME}
 Report.Links=Links
 Report.INDEX=INDICE
 Report.HELP=AYUDA
-Report.Distribution=Distribución
 Report.Token_Distribution=Distribución de símbolos
 
 Clusters.MIN_single_link=MIN, link único
@@ -57,3 +55,6 @@ AllMatches.Distribution=Distribuci
 AllMatches.Length=Longitud
 AllMatches.Number_of_matches=No de coincidencias
 AllMatches.Basecode=Código Base
+
+SimilarityMetric.Avg.Description = Average of both program coverages. This is the default similarity which works in most cases: Matches with a high average similarity indicate that the programs work in a very similar way.
+SimilarityMetric.Max.Description = Maximum of both program coverages. This ranking is especially useful if the programs are very different in size. This can happen when dead code was inserted to disguise the origin of the plagiarized program.

+ 3 - 1
jplag/src/main/resources/de/jplag/messages_fr.properties

@@ -32,7 +32,6 @@ Report.Matches_for_X1_AND_X2=Ressemblance pour {1_NAME} & {2_NAME}
 Report.Links=Liens
 Report.INDEX=INDEX
 Report.HELP=AIDE
-Report.Distribution=Repartition
 Report.Token_Distribution=Repartition des tokens
 
 Clusters.MIN_single_link=MIN, single link
@@ -57,3 +56,6 @@ AllMatches.Distribution=Distribution
 AllMatches.Length=Length
 AllMatches.Number_of_matches=No of matches
 AllMatches.Basecode=Code source de reference
+
+SimilarityMetric.Avg.Description = Average of both program coverages. This is the default similarity which works in most cases: Matches with a high average similarity indicate that the programs work in a very similar way.
+SimilarityMetric.Max.Description = Maximum of both program coverages. This ranking is especially useful if the programs are very different in size. This can happen when dead code was inserted to disguise the origin of the plagiarized program.

+ 3 - 1
jplag/src/main/resources/de/jplag/messages_pt.properties

@@ -32,7 +32,6 @@ Report.Matches_for_X1_AND_X2=Matches for {1_NAME} & {2_NAME}
 Report.Links=Links
 Report.INDEX=INDEX
 Report.HELP=HELP
-Report.Distribution=Distribution
 Report.Token_Distribution=Token Distribution
 
 Clusters.MIN_single_link=MIN, single link
@@ -57,3 +56,6 @@ AllMatches.Distribution=Distribution
 AllMatches.Length=Length
 AllMatches.Number_of_matches=No of matches
 AllMatches.Basecode=Basecode
+
+SimilarityMetric.Avg.Description = Average of both program coverages. This is the default similarity which works in most cases: Matches with a high average similarity indicate that the programs work in a very similar way.
+SimilarityMetric.Max.Description = Maximum of both program coverages. This ranking is especially useful if the programs are very different in size. This can happen when dead code was inserted to disguise the origin of the plagiarized program.

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

@@ -39,10 +39,10 @@ public class BaseCodeTest extends TestBase {
 
     private void verifyResults(JPlagResult result) {
         assertEquals(2, result.getNumberOfSubmissions());
-        assertEquals(1, result.getComparisons().size());
-        assertEquals(1, result.getComparisons().get(0).getMatches().size());
-        assertEquals(1, result.getSimilarityDistribution()[8]);
-        assertEquals(85f, result.getComparisons().get(0).similarity(), DELTA);
+        assertEquals(1, result.getAllComparisons().size());
+        assertEquals(1, result.getAllComparisons().get(0).getMatches().size());
+        assertEquals(1, result.getSimilarityDistribution()[1]);
+        assertEquals(85f, result.getAllComparisons().get(0).similarity(), DELTA);
     }
 
     @Test
@@ -52,7 +52,7 @@ public class BaseCodeTest extends TestBase {
     }
 
     @Test
-    void testInvalidRoot() throws ExitException {
+    void testInvalidRoot() {
         assertThrows(RootDirectoryException.class, () -> runJPlag("basecode", it -> it.setSubmissionDirectories(List.of("WrongRoot"))));
     }
 

+ 2 - 2
jplag/src/test/java/de/jplag/NewJavaFeaturesTest.java

@@ -23,10 +23,10 @@ public class NewJavaFeaturesTest extends TestBase {
         for (Submission submission : result.getSubmissions().getSubmissions()) {
             assertEquals(6, submission.getFiles().size(), String.format(CHANGE_MESSAGE, "Files"));
         }
-        assertEquals(1, result.getComparisons().size(), String.format(CHANGE_MESSAGE, "Comparisons"));
+        assertEquals(1, result.getAllComparisons().size(), String.format(CHANGE_MESSAGE, "Comparisons"));
 
         // Check similarity and number of matches:
-        var comparison = result.getComparisons().get(0);
+        var comparison = result.getAllComparisons().get(0);
         assertEquals(EXPECTED_SIMILARITY, comparison.similarity(), DELTA);
         assertEquals(EXPECTED_MATCHES, comparison.getMatches().size());
     }

+ 13 - 15
jplag/src/test/java/de/jplag/NormalComparisonTest.java

@@ -21,10 +21,10 @@ class NormalComparisonTest extends TestBase {
         JPlagResult result = runJPlagWithDefaultOptions("SimpleDuplicate");
 
         assertEquals(2, result.getNumberOfSubmissions());
-        assertEquals(1, result.getComparisons().size());
-        assertEquals(1, result.getComparisons().get(0).getMatches().size());
-        assertEquals(1, result.getSimilarityDistribution()[6]);
-        assertEquals(62.07f, result.getComparisons().get(0).similarity(), 0.1f);
+        assertEquals(1, result.getAllComparisons().size());
+        assertEquals(1, result.getAllComparisons().get(0).getMatches().size());
+        assertEquals(1, result.getSimilarityDistribution()[3]);
+        assertEquals(62.07f, result.getAllComparisons().get(0).similarity(), 0.1f);
     }
 
     /**
@@ -35,11 +35,9 @@ class NormalComparisonTest extends TestBase {
         JPlagResult result = runJPlagWithDefaultOptions("NoDuplicate");
 
         assertEquals(3, result.getNumberOfSubmissions());
-        assertEquals(3, result.getComparisons().size());
+        assertEquals(3, result.getAllComparisons().size());
 
-        result.getComparisons().forEach(comparison -> {
-            assertEquals(0f, comparison.similarity(), 0.1f);
-        });
+        result.getAllComparisons().forEach(comparison -> assertEquals(0f, comparison.similarity(), 0.1f));
     }
 
     /**
@@ -52,10 +50,10 @@ class NormalComparisonTest extends TestBase {
         JPlagResult result = runJPlagWithDefaultOptions("PartialPlagiarism");
 
         assertEquals(5, result.getNumberOfSubmissions());
-        assertEquals(10, result.getComparisons().size());
+        assertEquals(10, result.getAllComparisons().size());
 
         // All comparisons with E shall have no matches
-        result.getComparisons().stream()
+        result.getAllComparisons().stream()
                 .filter(comparison -> comparison.getSecondSubmission().getName().equals("E") || comparison.getFirstSubmission().getName().equals("E"))
                 .forEach(comparison -> assertEquals(0f, comparison.similarity(), DELTA));
 
@@ -81,7 +79,7 @@ class NormalComparisonTest extends TestBase {
     }
 
     private Optional<JPlagComparison> getSelectedComparison(JPlagResult result, String nameA, String nameB) {
-        return result.getComparisons().stream().filter(
+        return result.getAllComparisons().stream().filter(
                 comparison -> comparison.getFirstSubmission().getName().equals(nameA) && comparison.getSecondSubmission().getName().equals(nameB)
                         || comparison.getFirstSubmission().getName().equals(nameB) && comparison.getSecondSubmission().getName().equals(nameA))
                 .findFirst();
@@ -112,7 +110,7 @@ class NormalComparisonTest extends TestBase {
     }
 
     @Test
-    public void testMultiRootDirBasecodeName() throws ExitException {
+    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)));
@@ -125,7 +123,7 @@ class NormalComparisonTest extends TestBase {
         JPlagResult result = runJPlag(newDirectories, oldDirectories, it -> {
         });
         int numberOfExpectedComparison = 1 + 3 * 2;
-        assertEquals(numberOfExpectedComparison, result.getComparisons().size());
+        assertEquals(numberOfExpectedComparison, result.getAllComparisons().size());
     }
 
     @Test
@@ -135,7 +133,7 @@ class NormalComparisonTest extends TestBase {
         JPlagResult result = runJPlag(newDirectories, oldDirectories, it -> {
         });
         int numberOfExpectedComparison = 1;
-        assertEquals(numberOfExpectedComparison, result.getComparisons().size());
+        assertEquals(numberOfExpectedComparison, result.getAllComparisons().size());
     }
 
     @Test
@@ -145,6 +143,6 @@ class NormalComparisonTest extends TestBase {
         List<String> oldDirectories = List.of(getBasePath("basecode")); // 3 - 1 submissions
         JPlagResult result = runJPlag(newDirectories, oldDirectories, it -> it.setBaseCodeSubmissionName(basecodePath));
         int numberOfExpectedComparison = 1 + 2 * 2;
-        assertEquals(numberOfExpectedComparison, result.getComparisons().size());
+        assertEquals(numberOfExpectedComparison, result.getAllComparisons().size());
     }
 }

+ 9 - 11
jplag/src/test/java/de/jplag/ParallelComparisonTest.java

@@ -25,10 +25,10 @@ public class ParallelComparisonTest extends TestBase {
         JPlagResult result = runJPlag("SimpleDuplicate", it -> it.setComparisonMode(PARALLEL));
 
         assertEquals(2, result.getNumberOfSubmissions());
-        assertEquals(1, result.getComparisons().size());
-        assertEquals(1, result.getComparisons().get(0).getMatches().size());
-        assertEquals(1, result.getSimilarityDistribution()[6]);
-        assertEquals(62.07f, result.getComparisons().get(0).similarity(), DELTA);
+        assertEquals(1, result.getAllComparisons().size());
+        assertEquals(1, result.getAllComparisons().get(0).getMatches().size());
+        assertEquals(1, result.getSimilarityDistribution()[3]);
+        assertEquals(62.07f, result.getAllComparisons().get(0).similarity(), DELTA);
     }
 
     /**
@@ -39,11 +39,9 @@ public class ParallelComparisonTest extends TestBase {
         JPlagResult result = runJPlag("NoDuplicate", it -> it.setComparisonMode(PARALLEL));
 
         assertEquals(3, result.getNumberOfSubmissions());
-        assertEquals(3, result.getComparisons().size());
+        assertEquals(3, result.getAllComparisons().size());
 
-        result.getComparisons().forEach(comparison -> {
-            assertEquals(0f, comparison.similarity(), DELTA);
-        });
+        result.getAllComparisons().forEach(comparison -> assertEquals(0f, comparison.similarity(), DELTA));
     }
 
     /**
@@ -56,10 +54,10 @@ public class ParallelComparisonTest extends TestBase {
         JPlagResult result = runJPlag("PartialPlagiarism", it -> it.setComparisonMode(PARALLEL));
 
         assertEquals(5, result.getNumberOfSubmissions());
-        assertEquals(10, result.getComparisons().size());
+        assertEquals(10, result.getAllComparisons().size());
 
         // All comparisons with E shall have no matches
-        result.getComparisons().stream()
+        result.getAllComparisons().stream()
                 .filter(comparison -> comparison.getSecondSubmission().getName().equals("E") || comparison.getFirstSubmission().getName().equals("E"))
                 .forEach(comparison -> assertEquals(0f, comparison.similarity(), DELTA));
 
@@ -85,7 +83,7 @@ public class ParallelComparisonTest extends TestBase {
     }
 
     private Optional<JPlagComparison> getSelectedComparison(JPlagResult result, String nameA, String nameB) {
-        return result.getComparisons().stream().filter(
+        return result.getAllComparisons().stream().filter(
                 comparison -> comparison.getFirstSubmission().getName().equals(nameA) && comparison.getSecondSubmission().getName().equals(nameB)
                         || comparison.getFirstSubmission().getName().equals(nameB) && comparison.getSecondSubmission().getName().equals(nameA))
                 .findFirst();

+ 2 - 3
jplag/src/test/java/de/jplag/clustering/ClusteringAdapterTest.java

@@ -23,7 +23,7 @@ public class ClusteringAdapterTest {
 
     @Test
     public void testClustering() {
-        List<Submission> submissions = IntStream.range(0, 4).mapToObj(x -> mock(Submission.class)).collect(Collectors.toList());
+        List<Submission> submissions = IntStream.range(0, 4).mapToObj(x -> mock(Submission.class)).toList();
         List<JPlagComparison> comparisons = new ArrayList<>(6);
         for (int i = 0; i < submissions.size(); i++) {
             for (int j = i + 1; j < submissions.size(); j++) {
@@ -38,8 +38,7 @@ public class ClusteringAdapterTest {
         GenericClusteringAlgorithm algorithm = mock(GenericClusteringAlgorithm.class);
         when(algorithm.cluster(any(RealMatrix.class))).then((InvocationOnMock invocation) -> {
             RealMatrix arg = invocation.getArgument(0);
-            Collection<Collection<Integer>> result = List.of(IntStream.range(0, arg.getRowDimension()).boxed().collect(Collectors.toList()));
-            return result;
+            return List.of(IntStream.range(0, arg.getRowDimension()).boxed().collect(Collectors.toList()));
         });
 
         ClusteringAdapter clustering = new ClusteringAdapter(comparisons, x -> 0.f);

+ 3 - 3
jplag/src/test/java/de/jplag/clustering/algorithm/BayesianOptimizationTest.java

@@ -6,10 +6,10 @@ import org.apache.commons.math3.linear.ArrayRealVector;
 import org.apache.commons.math3.linear.RealVector;
 import org.junit.jupiter.api.Test;
 
-public class BayesianOptimizationTest {
+class BayesianOptimizationTest {
 
     @Test
-    public void findParabolaMax() {
+    void findParabolaMax() {
         RealVector minima = new ArrayRealVector(new double[] {-20});
         RealVector maxima = new ArrayRealVector(new double[] {10});
         RealVector lengthScale = new ArrayRealVector(new double[] {5});
@@ -19,7 +19,7 @@ public class BayesianOptimizationTest {
             double val = v.getEntry(0);
             double result = -(val - maximumAt) * (val - maximumAt);
             result += 0.2 * (Math.random() - 0.5);
-            return new BayesianOptimization.OptimizationResult<Double>(result, result);
+            return new BayesianOptimization.OptimizationResult<>(result, result);
         }).getParams().getEntry(0);
         assertEquals(maximumAt, maximumPoint, 1);
     }

+ 1 - 1
jplag/src/test/java/de/jplag/clustering/algorithm/ClusteringData.java

@@ -38,7 +38,7 @@ public enum ClusteringData {
     private final Set<Set<Integer>> expected;
     private final ClusteringOptions options;
 
-    private ClusteringData(Supplier<RealMatrix> similarity, int[][] expected, ClusteringOptions.Builder options) {
+    ClusteringData(Supplier<RealMatrix> similarity, int[][] expected, ClusteringOptions.Builder options) {
         this.similarity = similarity.get();
         this.expected = makeSets(
                 Arrays.stream(expected).map(intArray -> Arrays.stream(intArray).boxed().collect(Collectors.toList())).collect(Collectors.toList()));

+ 1 - 1
jplag/src/test/java/de/jplag/clustering/preprocessors/PreprocessingTestBase.java

@@ -50,7 +50,7 @@ public class PreprocessingTestBase {
 
         assertTrue(usedOriginalIndices.stream().allMatch(index -> index >= 0 && index < original.getColumnDimension()), "original indices valid");
 
-        double[] columnNorms = IntStream.range(0, result.getColumnDimension()).mapToObj(index -> result.getColumn(index))
+        double[] columnNorms = IntStream.range(0, result.getColumnDimension()).mapToObj(result::getColumn)
                 .mapToDouble(array -> new ArrayRealVector(array, false).getNorm()).toArray();
 
         for (int i = 0; i < columnNorms.length; i++) {

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

@@ -0,0 +1,115 @@
+package de.jplag.reporting.reportobject.mapper;
+
+import static org.mockito.Mockito.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import de.jplag.JPlagComparison;
+import de.jplag.JPlagResult;
+import de.jplag.Submission;
+import de.jplag.options.JPlagOptions;
+import de.jplag.reporting.reportobject.model.TopComparison;
+
+public class MetricMapperTest {
+    private final MetricMapper metricMapper = new MetricMapper();
+
+    @Test
+    public void test_getAverageMetric() {
+        // given
+        JPlagResult jPlagResult = createJPlagResult(MockMetric.AVG, distribution(2, 3, 5, 7, 11, 13, 17, 19, 23, 29),
+                comparison(submission("1"), submission("2"), .7f), comparison(submission("3"), submission("4"), .3f));
+        // when
+        var result = metricMapper.getAverageMetric(jPlagResult);
+
+        // then
+        Assertions.assertEquals("AVG", result.name());
+        Assertions.assertIterableEquals(List.of(2, 3, 5, 7, 11, 13, 17, 19, 23, 29), result.distribution());
+        Assertions.assertEquals(List.of(new TopComparison("1", "2", .7f), new TopComparison("3", "4", .3f)), result.topComparisons());
+        Assertions.assertEquals(
+                "Average of both program coverages. This is the default similarity which"
+                        + " works in most cases: Matches with a high average similarity indicate that the programs work " + "in a very similar way.",
+                result.description());
+    }
+
+    @Test
+    public void test_getMaxMetric() {
+        // given
+        JPlagResult jPlagResult = createJPlagResult(MockMetric.MAX, distribution(2, 3, 5, 7, 11, 13, 17, 19, 23, 29),
+                comparison(submission("00"), submission("01"), .7f), comparison(submission("10"), submission("11"), .3f));
+        // when
+        var result = metricMapper.getMaxMetric(jPlagResult);
+
+        // then
+        Assertions.assertEquals("MAX", result.name());
+        Assertions.assertIterableEquals(List.of(2, 3, 5, 7, 11, 13, 17, 19, 23, 29), result.distribution());
+        Assertions.assertEquals(List.of(new TopComparison("00", "01", .7f), new TopComparison("10", "11", .3f)), result.topComparisons());
+        Assertions.assertEquals(
+                "Maximum of both program coverages. This ranking is especially useful if the programs are very "
+                        + "different in size. This can happen when dead code was inserted to disguise the origin of the plagiarized program.",
+                result.description());
+    }
+
+    private int[] distribution(int... numbers) {
+        return numbers;
+    }
+
+    private CreateSubmission submission(String name) {
+        return new CreateSubmission(name);
+    }
+
+    private Comparison comparison(CreateSubmission submission1, CreateSubmission submission2, float similarity) {
+        return new Comparison(submission1, submission2, similarity);
+    }
+
+    private JPlagResult createJPlagResult(MockMetric metricToMock, int[] distribution, Comparison... createComparisonsDto) {
+        JPlagResult jPlagResult = mock(JPlagResult.class);
+
+        if (metricToMock.equals(MockMetric.AVG)) {
+            doReturn(distribution).when(jPlagResult).getSimilarityDistribution();
+        } else if (metricToMock.equals(MockMetric.MAX)) {
+            doReturn(distribution).when(jPlagResult).getMaxSimilarityDistribution();
+
+        }
+
+        JPlagOptions options = mock(JPlagOptions.class);
+        doReturn(createComparisonsDto.length).when(options).getMaximumNumberOfComparisons();
+        doReturn(options).when(jPlagResult).getOptions();
+
+        List<JPlagComparison> comparisonList = new ArrayList<>();
+        for (Comparison comparisonDto : createComparisonsDto) {
+            Submission submission1 = mock(Submission.class);
+            doReturn(comparisonDto.submission1.name).when(submission1).getName();
+            Submission submission2 = mock(Submission.class);
+            doReturn(comparisonDto.submission2.name).when(submission2).getName();
+
+            JPlagComparison mockedComparison = mock(JPlagComparison.class);
+            doReturn(submission1).when(mockedComparison).getFirstSubmission();
+            doReturn(submission2).when(mockedComparison).getSecondSubmission();
+            if (metricToMock.equals(MockMetric.AVG)) {
+                doReturn(comparisonDto.similarity).when(mockedComparison).similarity();
+            } else if (metricToMock.equals(MockMetric.MAX)) {
+                doReturn(comparisonDto.similarity).when(mockedComparison).maximalSimilarity();
+            }
+            comparisonList.add(mockedComparison);
+        }
+
+        doReturn(comparisonList).when(jPlagResult).getComparisons(anyInt());
+        return jPlagResult;
+    }
+
+    private enum MockMetric {
+        MAX,
+        AVG
+    }
+
+    private record Comparison(CreateSubmission submission1, CreateSubmission submission2, float similarity) {
+    }
+
+    private record CreateSubmission(String name) {
+    }
+
+}

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

@@ -51,10 +51,10 @@ public class VolumeTest extends TestBase {
 
         var csv = readCSVResults(String.format("%s/%s", this.getBasePath(), "matches_avg.csv"));
 
-        assertEquals(csv.size(), results.getComparisons().size());
+        assertEquals(csv.size(), results.getAllComparisons().size());
         System.out.println("Volume test size: " + csv.size());
 
-        results.getComparisons().forEach(result -> {
+        results.getAllComparisons().forEach(result -> {
             var key = result.getFirstSubmission().getName() + result.getSecondSubmission().getName();
 
             assertTrue(csv.containsKey(key));

+ 1 - 0
jplag/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker

@@ -0,0 +1 @@
+mock-maker-inline

+ 39 - 4
pom.xml

@@ -65,6 +65,13 @@
 
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <sonar.projectKey>jplag_JPlag</sonar.projectKey>
+        <sonar.moduleKey>${project.groupId}:${project.artifactId}</sonar.moduleKey>
+        <sonar.organization>jplag</sonar.organization>
+        <sonar.host.url>https://sonarcloud.io</sonar.host.url>
+        <!--suppress UnresolvedMavenProperty -->
+        <sonar.coverage.jacoco.xmlReportPaths>${maven.multiModuleProjectDirectory}/jplag.cli/target/site/jacoco-aggregate/jacoco.xml</sonar.coverage.jacoco.xmlReportPaths>
+
         <maven.compiler.source>17</maven.compiler.source>
         <maven.compiler.target>17</maven.compiler.target>
         <spotless.version>2.23.0</spotless.version>
@@ -197,7 +204,7 @@
         <dependency>
             <groupId>org.junit.jupiter</groupId>
             <artifactId>junit-jupiter-engine</artifactId>
-            <version>5.8.2</version>
+            <version>5.9.0</version>
             <scope>test</scope>
         </dependency>
         <dependency>
@@ -265,7 +272,7 @@
                 <plugin>
                     <groupId>org.apache.maven.plugins</groupId>
                     <artifactId>maven-assembly-plugin</artifactId>
-                    <version>3.4.1</version>
+                    <version>3.4.2</version>
                     <configuration>
                         <descriptorRefs>
                             <descriptorRef>jar-with-dependencies</descriptorRef>
@@ -283,6 +290,30 @@
                     <artifactId>maven-surefire-plugin</artifactId>
                     <version>3.0.0-M7</version>
                 </plugin>
+                <plugin>
+                    <groupId>org.jacoco</groupId>
+                    <artifactId>jacoco-maven-plugin</artifactId>
+                    <version>0.8.8</version>
+                    <executions>
+                        <execution>
+                            <id>prepare-agent</id>
+                            <goals>
+                                <goal>prepare-agent</goal>
+                            </goals>
+                        </execution>
+                        <execution>
+                            <id>report</id>
+                            <goals>
+                                <goal>report</goal>
+                            </goals>
+                            <configuration>
+                                <formats>
+                                    <format>XML</format>
+                                </formats>
+                            </configuration>
+                        </execution>
+                    </executions>
+                </plugin>
                 <plugin>
                     <groupId>org.apache.maven.plugins</groupId>
                     <artifactId>maven-gpg-plugin</artifactId>
@@ -291,7 +322,7 @@
                 <plugin>
                     <groupId>org.apache.maven.plugins</groupId>
                     <artifactId>maven-deploy-plugin</artifactId>
-                    <version>3.0.0-M2</version>
+                    <version>3.0.0</version>
                 </plugin>
             </plugins>
         </pluginManagement>
@@ -357,7 +388,7 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-source-plugin</artifactId>
-                <version>3.2.0</version>
+                <version>3.2.1</version>
                 <executions>
                     <execution>
                         <id>attach-sources</id>
@@ -404,6 +435,10 @@
                     </pom>
                 </configuration>
             </plugin>
+            <plugin>
+                <groupId>org.jacoco</groupId>
+                <artifactId>jacoco-maven-plugin</artifactId>
+            </plugin>
         </plugins>
     </build>
     <profiles>

+ 94 - 94
report-viewer/package-lock.json

@@ -9,9 +9,9 @@
       "version": "0.1.0",
       "dependencies": {
         "@highlightjs/vue-plugin": "^2.1.0",
-        "chart.js": "^3.8.0",
+        "chart.js": "^3.8.2",
         "chartjs-plugin-datalabels": "^2.0.0",
-        "core-js": "^3.23.5",
+        "core-js": "^3.24.0",
         "gitart-vue-dialog": "^2.4.0",
         "highlight.js": "^11.6.0",
         "jszip": "^3.10.0",
@@ -23,8 +23,8 @@
         "vuex": "^4.0.2"
       },
       "devDependencies": {
-        "@typescript-eslint/eslint-plugin": "^5.30.7",
-        "@typescript-eslint/parser": "^5.30.7",
+        "@typescript-eslint/eslint-plugin": "^5.31.0",
+        "@typescript-eslint/parser": "^5.31.0",
         "@vue/cli-plugin-babel": "~5.0.8",
         "@vue/cli-plugin-eslint": "~5.0.8",
         "@vue/cli-plugin-router": "~5.0.8",
@@ -2221,14 +2221,14 @@
       }
     },
     "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "5.30.7",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.7.tgz",
-      "integrity": "sha512-l4L6Do+tfeM2OK0GJsU7TUcM/1oN/N25xHm3Jb4z3OiDU4Lj8dIuxX9LpVMS9riSXQs42D1ieX7b85/r16H9Fw==",
+      "version": "5.31.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.31.0.tgz",
+      "integrity": "sha512-VKW4JPHzG5yhYQrQ1AzXgVgX8ZAJEvCz0QI6mLRX4tf7rnFfh5D8SKm0Pq6w5PyNfAWJk6sv313+nEt3ohWMBQ==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/scope-manager": "5.30.7",
-        "@typescript-eslint/type-utils": "5.30.7",
-        "@typescript-eslint/utils": "5.30.7",
+        "@typescript-eslint/scope-manager": "5.31.0",
+        "@typescript-eslint/type-utils": "5.31.0",
+        "@typescript-eslint/utils": "5.31.0",
         "debug": "^4.3.4",
         "functional-red-black-tree": "^1.0.1",
         "ignore": "^5.2.0",
@@ -2269,14 +2269,14 @@
       }
     },
     "node_modules/@typescript-eslint/parser": {
-      "version": "5.30.7",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.30.7.tgz",
-      "integrity": "sha512-Rg5xwznHWWSy7v2o0cdho6n+xLhK2gntImp0rJroVVFkcYFYQ8C8UJTSuTw/3CnExBmPjycjmUJkxVmjXsld6A==",
+      "version": "5.31.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.31.0.tgz",
+      "integrity": "sha512-UStjQiZ9OFTFReTrN+iGrC6O/ko9LVDhreEK5S3edmXgR396JGq7CoX2TWIptqt/ESzU2iRKXAHfSF2WJFcWHw==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/scope-manager": "5.30.7",
-        "@typescript-eslint/types": "5.30.7",
-        "@typescript-eslint/typescript-estree": "5.30.7",
+        "@typescript-eslint/scope-manager": "5.31.0",
+        "@typescript-eslint/types": "5.31.0",
+        "@typescript-eslint/typescript-estree": "5.31.0",
         "debug": "^4.3.4"
       },
       "engines": {
@@ -2296,13 +2296,13 @@
       }
     },
     "node_modules/@typescript-eslint/scope-manager": {
-      "version": "5.30.7",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.30.7.tgz",
-      "integrity": "sha512-7BM1bwvdF1UUvt+b9smhqdc/eniOnCKxQT/kj3oXtj3LqnTWCAM0qHRHfyzCzhEfWX0zrW7KqXXeE4DlchZBKw==",
+      "version": "5.31.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.31.0.tgz",
+      "integrity": "sha512-8jfEzBYDBG88rcXFxajdVavGxb5/XKXyvWgvD8Qix3EEJLCFIdVloJw+r9ww0wbyNLOTYyBsR+4ALNGdlalLLg==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "5.30.7",
-        "@typescript-eslint/visitor-keys": "5.30.7"
+        "@typescript-eslint/types": "5.31.0",
+        "@typescript-eslint/visitor-keys": "5.31.0"
       },
       "engines": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -2313,12 +2313,12 @@
       }
     },
     "node_modules/@typescript-eslint/type-utils": {
-      "version": "5.30.7",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.30.7.tgz",
-      "integrity": "sha512-nD5qAE2aJX/YLyKMvOU5jvJyku4QN5XBVsoTynFrjQZaDgDV6i7QHFiYCx10wvn7hFvfuqIRNBtsgaLe0DbWhw==",
+      "version": "5.31.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.31.0.tgz",
+      "integrity": "sha512-7ZYqFbvEvYXFn9ax02GsPcEOmuWNg+14HIf4q+oUuLnMbpJ6eHAivCg7tZMVwzrIuzX3QCeAOqKoyMZCv5xe+w==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/utils": "5.30.7",
+        "@typescript-eslint/utils": "5.31.0",
         "debug": "^4.3.4",
         "tsutils": "^3.21.0"
       },
@@ -2339,9 +2339,9 @@
       }
     },
     "node_modules/@typescript-eslint/types": {
-      "version": "5.30.7",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.30.7.tgz",
-      "integrity": "sha512-ocVkETUs82+U+HowkovV6uxf1AnVRKCmDRNUBUUo46/5SQv1owC/EBFkiu4MOHeZqhKz2ktZ3kvJJ1uFqQ8QPg==",
+      "version": "5.31.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.31.0.tgz",
+      "integrity": "sha512-/f/rMaEseux+I4wmR6mfpM2wvtNZb1p9hAV77hWfuKc3pmaANp5dLAZSiE3/8oXTYTt3uV9KW5yZKJsMievp6g==",
       "dev": true,
       "engines": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -2352,13 +2352,13 @@
       }
     },
     "node_modules/@typescript-eslint/typescript-estree": {
-      "version": "5.30.7",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.30.7.tgz",
-      "integrity": "sha512-tNslqXI1ZdmXXrHER83TJ8OTYl4epUzJC0aj2i4DMDT4iU+UqLT3EJeGQvJ17BMbm31x5scSwo3hPM0nqQ1AEA==",
+      "version": "5.31.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.31.0.tgz",
+      "integrity": "sha512-3S625TMcARX71wBc2qubHaoUwMEn+l9TCsaIzYI/ET31Xm2c9YQ+zhGgpydjorwQO9pLfR/6peTzS/0G3J/hDw==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "5.30.7",
-        "@typescript-eslint/visitor-keys": "5.30.7",
+        "@typescript-eslint/types": "5.31.0",
+        "@typescript-eslint/visitor-keys": "5.31.0",
         "debug": "^4.3.4",
         "globby": "^11.1.0",
         "is-glob": "^4.0.3",
@@ -2394,15 +2394,15 @@
       }
     },
     "node_modules/@typescript-eslint/utils": {
-      "version": "5.30.7",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.30.7.tgz",
-      "integrity": "sha512-Z3pHdbFw+ftZiGUnm1GZhkJgVqsDL5CYW2yj+TB2mfXDFOMqtbzQi2dNJIyPqPbx9mv2kUxS1gU+r2gKlKi1rQ==",
+      "version": "5.31.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.31.0.tgz",
+      "integrity": "sha512-kcVPdQS6VIpVTQ7QnGNKMFtdJdvnStkqS5LeALr4rcwx11G6OWb2HB17NMPnlRHvaZP38hL9iK8DdE9Fne7NYg==",
       "dev": true,
       "dependencies": {
         "@types/json-schema": "^7.0.9",
-        "@typescript-eslint/scope-manager": "5.30.7",
-        "@typescript-eslint/types": "5.30.7",
-        "@typescript-eslint/typescript-estree": "5.30.7",
+        "@typescript-eslint/scope-manager": "5.31.0",
+        "@typescript-eslint/types": "5.31.0",
+        "@typescript-eslint/typescript-estree": "5.31.0",
         "eslint-scope": "^5.1.1",
         "eslint-utils": "^3.0.0"
       },
@@ -2418,12 +2418,12 @@
       }
     },
     "node_modules/@typescript-eslint/visitor-keys": {
-      "version": "5.30.7",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.30.7.tgz",
-      "integrity": "sha512-KrRXf8nnjvcpxDFOKej4xkD7657+PClJs5cJVSG7NNoCNnjEdc46juNAQt7AyuWctuCgs6mVRc1xGctEqrjxWw==",
+      "version": "5.31.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.31.0.tgz",
+      "integrity": "sha512-ZK0jVxSjS4gnPirpVjXHz7mgdOsZUHzNYSfTw2yPa3agfbt9YfqaBiBZFSSxeBWnpWkzCxTfUpnzA3Vily/CSg==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "5.30.7",
+        "@typescript-eslint/types": "5.31.0",
         "eslint-visitor-keys": "^3.3.0"
       },
       "engines": {
@@ -4118,9 +4118,9 @@
       }
     },
     "node_modules/chart.js": {
-      "version": "3.8.0",
-      "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.8.0.tgz",
-      "integrity": "sha512-cr8xhrXjLIXVLOBZPkBZVF6NDeiVIrPLHcMhnON7UufudL+CNeRrD+wpYanswlm8NpudMdrt3CHoLMQMxJhHRg=="
+      "version": "3.8.2",
+      "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.8.2.tgz",
+      "integrity": "sha512-7rqSlHWMUKFyBDOJvmFGW2lxULtcwaPLegDjX/Nu5j6QybY+GCiQkEY+6cqHw62S5tcwXMD8Y+H5OBGoR7d+ZQ=="
     },
     "node_modules/chartjs-plugin-datalabels": {
       "version": "2.0.0",
@@ -4662,9 +4662,9 @@
       }
     },
     "node_modules/core-js": {
-      "version": "3.23.5",
-      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.23.5.tgz",
-      "integrity": "sha512-7Vh11tujtAZy82da4duVreQysIoO2EvVrur7y6IzZkH1IHPSekuDi8Vuw1+YKjkbfWLRD7Nc9ICQ/sIUDutcyg==",
+      "version": "3.24.0",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.24.0.tgz",
+      "integrity": "sha512-IeOyT8A6iK37Ep4kZDD423mpi6JfPRoPUdQwEWYiGolvn4o6j2diaRzNfDfpTdu3a5qMbrGUzKUpYpRY8jXCkQ==",
       "hasInstallScript": true,
       "funding": {
         "type": "opencollective",
@@ -14296,14 +14296,14 @@
       }
     },
     "@typescript-eslint/eslint-plugin": {
-      "version": "5.30.7",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.7.tgz",
-      "integrity": "sha512-l4L6Do+tfeM2OK0GJsU7TUcM/1oN/N25xHm3Jb4z3OiDU4Lj8dIuxX9LpVMS9riSXQs42D1ieX7b85/r16H9Fw==",
+      "version": "5.31.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.31.0.tgz",
+      "integrity": "sha512-VKW4JPHzG5yhYQrQ1AzXgVgX8ZAJEvCz0QI6mLRX4tf7rnFfh5D8SKm0Pq6w5PyNfAWJk6sv313+nEt3ohWMBQ==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/scope-manager": "5.30.7",
-        "@typescript-eslint/type-utils": "5.30.7",
-        "@typescript-eslint/utils": "5.30.7",
+        "@typescript-eslint/scope-manager": "5.31.0",
+        "@typescript-eslint/type-utils": "5.31.0",
+        "@typescript-eslint/utils": "5.31.0",
         "debug": "^4.3.4",
         "functional-red-black-tree": "^1.0.1",
         "ignore": "^5.2.0",
@@ -14324,52 +14324,52 @@
       }
     },
     "@typescript-eslint/parser": {
-      "version": "5.30.7",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.30.7.tgz",
-      "integrity": "sha512-Rg5xwznHWWSy7v2o0cdho6n+xLhK2gntImp0rJroVVFkcYFYQ8C8UJTSuTw/3CnExBmPjycjmUJkxVmjXsld6A==",
+      "version": "5.31.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.31.0.tgz",
+      "integrity": "sha512-UStjQiZ9OFTFReTrN+iGrC6O/ko9LVDhreEK5S3edmXgR396JGq7CoX2TWIptqt/ESzU2iRKXAHfSF2WJFcWHw==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/scope-manager": "5.30.7",
-        "@typescript-eslint/types": "5.30.7",
-        "@typescript-eslint/typescript-estree": "5.30.7",
+        "@typescript-eslint/scope-manager": "5.31.0",
+        "@typescript-eslint/types": "5.31.0",
+        "@typescript-eslint/typescript-estree": "5.31.0",
         "debug": "^4.3.4"
       }
     },
     "@typescript-eslint/scope-manager": {
-      "version": "5.30.7",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.30.7.tgz",
-      "integrity": "sha512-7BM1bwvdF1UUvt+b9smhqdc/eniOnCKxQT/kj3oXtj3LqnTWCAM0qHRHfyzCzhEfWX0zrW7KqXXeE4DlchZBKw==",
+      "version": "5.31.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.31.0.tgz",
+      "integrity": "sha512-8jfEzBYDBG88rcXFxajdVavGxb5/XKXyvWgvD8Qix3EEJLCFIdVloJw+r9ww0wbyNLOTYyBsR+4ALNGdlalLLg==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/types": "5.30.7",
-        "@typescript-eslint/visitor-keys": "5.30.7"
+        "@typescript-eslint/types": "5.31.0",
+        "@typescript-eslint/visitor-keys": "5.31.0"
       }
     },
     "@typescript-eslint/type-utils": {
-      "version": "5.30.7",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.30.7.tgz",
-      "integrity": "sha512-nD5qAE2aJX/YLyKMvOU5jvJyku4QN5XBVsoTynFrjQZaDgDV6i7QHFiYCx10wvn7hFvfuqIRNBtsgaLe0DbWhw==",
+      "version": "5.31.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.31.0.tgz",
+      "integrity": "sha512-7ZYqFbvEvYXFn9ax02GsPcEOmuWNg+14HIf4q+oUuLnMbpJ6eHAivCg7tZMVwzrIuzX3QCeAOqKoyMZCv5xe+w==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/utils": "5.30.7",
+        "@typescript-eslint/utils": "5.31.0",
         "debug": "^4.3.4",
         "tsutils": "^3.21.0"
       }
     },
     "@typescript-eslint/types": {
-      "version": "5.30.7",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.30.7.tgz",
-      "integrity": "sha512-ocVkETUs82+U+HowkovV6uxf1AnVRKCmDRNUBUUo46/5SQv1owC/EBFkiu4MOHeZqhKz2ktZ3kvJJ1uFqQ8QPg==",
+      "version": "5.31.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.31.0.tgz",
+      "integrity": "sha512-/f/rMaEseux+I4wmR6mfpM2wvtNZb1p9hAV77hWfuKc3pmaANp5dLAZSiE3/8oXTYTt3uV9KW5yZKJsMievp6g==",
       "dev": true
     },
     "@typescript-eslint/typescript-estree": {
-      "version": "5.30.7",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.30.7.tgz",
-      "integrity": "sha512-tNslqXI1ZdmXXrHER83TJ8OTYl4epUzJC0aj2i4DMDT4iU+UqLT3EJeGQvJ17BMbm31x5scSwo3hPM0nqQ1AEA==",
+      "version": "5.31.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.31.0.tgz",
+      "integrity": "sha512-3S625TMcARX71wBc2qubHaoUwMEn+l9TCsaIzYI/ET31Xm2c9YQ+zhGgpydjorwQO9pLfR/6peTzS/0G3J/hDw==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/types": "5.30.7",
-        "@typescript-eslint/visitor-keys": "5.30.7",
+        "@typescript-eslint/types": "5.31.0",
+        "@typescript-eslint/visitor-keys": "5.31.0",
         "debug": "^4.3.4",
         "globby": "^11.1.0",
         "is-glob": "^4.0.3",
@@ -14389,26 +14389,26 @@
       }
     },
     "@typescript-eslint/utils": {
-      "version": "5.30.7",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.30.7.tgz",
-      "integrity": "sha512-Z3pHdbFw+ftZiGUnm1GZhkJgVqsDL5CYW2yj+TB2mfXDFOMqtbzQi2dNJIyPqPbx9mv2kUxS1gU+r2gKlKi1rQ==",
+      "version": "5.31.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.31.0.tgz",
+      "integrity": "sha512-kcVPdQS6VIpVTQ7QnGNKMFtdJdvnStkqS5LeALr4rcwx11G6OWb2HB17NMPnlRHvaZP38hL9iK8DdE9Fne7NYg==",
       "dev": true,
       "requires": {
         "@types/json-schema": "^7.0.9",
-        "@typescript-eslint/scope-manager": "5.30.7",
-        "@typescript-eslint/types": "5.30.7",
-        "@typescript-eslint/typescript-estree": "5.30.7",
+        "@typescript-eslint/scope-manager": "5.31.0",
+        "@typescript-eslint/types": "5.31.0",
+        "@typescript-eslint/typescript-estree": "5.31.0",
         "eslint-scope": "^5.1.1",
         "eslint-utils": "^3.0.0"
       }
     },
     "@typescript-eslint/visitor-keys": {
-      "version": "5.30.7",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.30.7.tgz",
-      "integrity": "sha512-KrRXf8nnjvcpxDFOKej4xkD7657+PClJs5cJVSG7NNoCNnjEdc46juNAQt7AyuWctuCgs6mVRc1xGctEqrjxWw==",
+      "version": "5.31.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.31.0.tgz",
+      "integrity": "sha512-ZK0jVxSjS4gnPirpVjXHz7mgdOsZUHzNYSfTw2yPa3agfbt9YfqaBiBZFSSxeBWnpWkzCxTfUpnzA3Vily/CSg==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/types": "5.30.7",
+        "@typescript-eslint/types": "5.31.0",
         "eslint-visitor-keys": "^3.3.0"
       }
     },
@@ -15716,9 +15716,9 @@
       }
     },
     "chart.js": {
-      "version": "3.8.0",
-      "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.8.0.tgz",
-      "integrity": "sha512-cr8xhrXjLIXVLOBZPkBZVF6NDeiVIrPLHcMhnON7UufudL+CNeRrD+wpYanswlm8NpudMdrt3CHoLMQMxJhHRg=="
+      "version": "3.8.2",
+      "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.8.2.tgz",
+      "integrity": "sha512-7rqSlHWMUKFyBDOJvmFGW2lxULtcwaPLegDjX/Nu5j6QybY+GCiQkEY+6cqHw62S5tcwXMD8Y+H5OBGoR7d+ZQ=="
     },
     "chartjs-plugin-datalabels": {
       "version": "2.0.0",
@@ -16124,9 +16124,9 @@
       }
     },
     "core-js": {
-      "version": "3.23.5",
-      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.23.5.tgz",
-      "integrity": "sha512-7Vh11tujtAZy82da4duVreQysIoO2EvVrur7y6IzZkH1IHPSekuDi8Vuw1+YKjkbfWLRD7Nc9ICQ/sIUDutcyg=="
+      "version": "3.24.0",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.24.0.tgz",
+      "integrity": "sha512-IeOyT8A6iK37Ep4kZDD423mpi6JfPRoPUdQwEWYiGolvn4o6j2diaRzNfDfpTdu3a5qMbrGUzKUpYpRY8jXCkQ=="
     },
     "core-js-compat": {
       "version": "3.23.4",

+ 4 - 4
report-viewer/package.json

@@ -9,9 +9,9 @@
   },
   "dependencies": {
     "@highlightjs/vue-plugin": "^2.1.0",
-    "chart.js": "^3.8.0",
+    "chart.js": "^3.8.2",
     "chartjs-plugin-datalabels": "^2.0.0",
-    "core-js": "^3.23.5",
+    "core-js": "^3.24.0",
     "gitart-vue-dialog": "^2.4.0",
     "highlight.js": "^11.6.0",
     "jszip": "^3.10.0",
@@ -23,8 +23,8 @@
     "vuex": "^4.0.2"
   },
   "devDependencies": {
-    "@typescript-eslint/eslint-plugin": "^5.30.7",
-    "@typescript-eslint/parser": "^5.30.7",
+    "@typescript-eslint/eslint-plugin": "^5.31.0",
+    "@typescript-eslint/parser": "^5.31.0",
     "@vue/cli-plugin-babel": "~5.0.8",
     "@vue/cli-plugin-eslint": "~5.0.8",
     "@vue/cli-plugin-router": "~5.0.8",

+ 4 - 6
report-viewer/src/components/DistributionDiagram.vue

@@ -3,11 +3,11 @@
 -->
 <template>
   <div class="wrapper">
-    <BarChart :chartData="chartData" :options="options" class="chart"/>
+    <BarChart :chartData="chartData" :options="options" class="chart" />
   </div>
 </template>
 
-<script>
+<script lang="ts">
 import {defineComponent, ref, watch} from "vue";
 import {BarChart} from "vue-chart-3"
 import {Chart, registerables} from 'chart.js';
@@ -21,7 +21,7 @@ export default defineComponent({
   components: {BarChart},
   props: {
     distribution: {
-      type: Array,
+      type: Array<number>,
       required: true
     }
   },
@@ -111,11 +111,9 @@ export default defineComponent({
   box-shadow: #777777 2px 3px 3px;
   display: flex;
   height: 100%;
-
 }
 
 .chart {
   width: 100%;
-
 }
-</style>
+</style>

Some files were not shown because too many files changed in this diff