diff --git a/.github/workflows/files/progpedia.zip b/.github/workflows/files/progpedia.zip
index 310d196fa..7af75298c 100644
Binary files a/.github/workflows/files/progpedia.zip and b/.github/workflows/files/progpedia.zip differ
diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index 443f3c50f..e83f58f23 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -1,7 +1,7 @@
name: Build
on:
- push:
+ push:
paths:
- ".github/workflows/maven.yml"
- "**/pom.xml"
@@ -14,7 +14,7 @@ on:
- "**/pom.xml"
- "**.java"
- "**.g4"
-
+
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
@@ -43,17 +43,21 @@ jobs:
with:
java-version: 21
distribution: 'temurin'
-
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: "18"
+
- name: Run Tests
run: mvn verify -B -U
-
+
- name: Build Assembly
- run: mvn clean package assembly:single
-
+ run: mvn -Pwith-report-viewer clean package assembly:single
+
- name: Upload Assembly
uses: actions/upload-artifact@v4
with:
name: "JPlag"
path: "jplag.cli/target/jplag-*-jar-with-dependencies.jar"
-
+
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 5a31d8afe..b4ea875e3 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -13,7 +13,7 @@ jobs:
java-version: '21'
distribution: 'temurin'
- name: Set maven settings.xml
- uses: whelk-io/maven-settings-xml-action@v21
+ uses: whelk-io/maven-settings-xml-action@v22
with:
servers: '[{ "id": "ossrh", "username": "jplag", "password": "${{ secrets.OSSRH_TOKEN }}" }]'
- name: Import GPG key
@@ -34,8 +34,12 @@ jobs:
with:
java-version: '21'
distribution: 'temurin'
+ - uses: actions/setup-node@v4
+ with:
+ node-version: "18"
+
- name: Build JPlag
- run: mvn -U -B clean package assembly:single
+ run: mvn -Pwith-report-viewer -U -B clean package assembly:single
- name: Attach CLI to Release on GitHub
uses: softprops/action-gh-release@v1
diff --git a/.github/workflows/sonarcloud-branch.yml b/.github/workflows/sonarcloud-branch.yml
index d47c15001..486fb0777 100644
--- a/.github/workflows/sonarcloud-branch.yml
+++ b/.github/workflows/sonarcloud-branch.yml
@@ -42,7 +42,7 @@ jobs:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2
-
+
- name: Build and analyze
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/sonarcloud-pr.yml b/.github/workflows/sonarcloud-pr.yml
index ca89871fc..5c3cfc300 100644
--- a/.github/workflows/sonarcloud-pr.yml
+++ b/.github/workflows/sonarcloud-pr.yml
@@ -65,7 +65,7 @@ jobs:
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 }}
diff --git a/.github/workflows/spotless.yml b/.github/workflows/spotless.yml
index 0357fd108..51058bb69 100644
--- a/.github/workflows/spotless.yml
+++ b/.github/workflows/spotless.yml
@@ -14,7 +14,7 @@ on:
- "**/pom.xml"
- "**.java"
- "**.g4"
-
+
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
@@ -43,9 +43,9 @@ jobs:
with:
java-version: 21
distribution: 'temurin'
-
+
- name: Check with Spotless
run: mvn clean spotless:check
-
-
+
+
diff --git a/README.md b/README.md
index aaa5ed368..303ce1a57 100644
--- a/README.md
+++ b/README.md
@@ -10,34 +10,42 @@
[![GitHub commit activity](https://img.shields.io/github/commit-activity/y/jplag/JPlag)](https://github.com/jplag/JPlag/pulse)
[![SonarCloud Coverage](https://sonarcloud.io/api/project_badges/measure?project=jplag_JPlag&metric=coverage)](https://sonarcloud.io/component_measures/metric/coverage/list?id=jplag_JPlag)
[![Report Viewer](https://img.shields.io/badge/report%20viewer-online-b80025)](https://jplag.github.io/JPlag/)
-[![Java Version](https://img.shields.io/badge/java-SE%2017-yellowgreen)](#download-and-installation)
+[![Java Version](https://img.shields.io/badge/java-SE%2021-yellowgreen)](#download-and-installation)
-JPlag is a system that finds similarities among multiple sets of source code files. This way it can detect software plagiarism and collusion in software development. JPlag currently supports various programming languages, EMF metamodels, and natural language text.
+JPlag finds pairwise similarities among a set of multiple programs. It can reliably detect software plagiarism and collusion in software development, even when obfuscated. All similarities are calculated locally, and no source code or plagiarism results are ever uploaded to the internet. JPlag supports a large number of programming and modeling languages.
+
+* 📈 [JPlag Demo](https://jplag.github.io/Demo/)
+
+* 🏛️ [JPlag on Helmholtz RSD](https://helmholtz.software/software/jplag)
+
+* 🤩 [Give us Feedback in a **short (<5 min) survey**](https://docs.google.com/forms/d/e/1FAIpQLSckqUlXhIlJ-H2jtu2VmGf_mJt4hcnHXaDlwhpUL3XG1I8UYw/viewform?usp=sf_link)
+
## Supported Languages
-In the following, a list of all supported languages with their supported language version is provided. A language can be selected from the command line using subcommands (jplag [jplag options] [language options]). Alternatively you can use the legacy "-l" argument.
+All supported languages and their supported versions are listed below.
| Language | Version | CLI Argument Name | [state](https://github.com/jplag/JPlag/wiki/2.-Supported-Languages) | parser |
|--------------------------------------------------------|---------------------------------------------------------------------------------------:|-------------------|:-------------------------------------------------------------------:|:---------:|
| [Java](https://www.java.com) | 21 | java | mature | JavaC |
-| [C/C++](https://isocpp.org) | 11 | cpp | legacy | JavaCC |
-| [C/C++](https://isocpp.org) | 14 | cpp2 | beta | ANTLR 4 |
-| [C#](https://docs.microsoft.com/en-us/dotnet/csharp/) | 6 | csharp | beta | ANTLR 4 |
+| [C](https://isocpp.org) | 11 | c | legacy | JavaCC |
+| [C++](https://isocpp.org) | 14 | cpp | beta | ANTLR 4 |
+| [C#](https://docs.microsoft.com/en-us/dotnet/csharp/) | 6 | csharp | mature | ANTLR 4 |
+| [Python](https://www.python.org) | 3.6 | python3 | legacy | ANTLR 4 |
+| [JavaScript](https://www.javascript.com/) | ES6 | javascript | beta | ANTLR 4 |
+| [TypeScript](https://www.typescriptlang.org/) | [~5](https://github.com/antlr/grammars-v4/tree/master/javascript/typescript/README.md) | typescript | beta | ANTLR 4 |
| [Go](https://go.dev) | 1.17 | golang | beta | ANTLR 4 |
| [Kotlin](https://kotlinlang.org) | 1.3 | kotlin | beta | ANTLR 4 |
-| [Python](https://www.python.org) | 3.6 | python3 | legacy | ANTLR 4 |
| [R](https://www.r-project.org/) | 3.5.0 | rlang | beta | ANTLR 4 |
| [Rust](https://www.rust-lang.org/) | 1.60.0 | rust | beta | ANTLR 4 |
-| [Scala](https://www.scala-lang.org) | 2.13.8 | scala | beta | Scalameta |
-| [Scheme](http://www.scheme-reports.org) | ? | scheme | unknown | JavaCC |
| [Swift](https://www.swift.org) | 5.4 | swift | beta | ANTLR 4 |
+| [Scala](https://www.scala-lang.org) | 2.13.8 | scala | beta | Scalameta |
+| [LLVM IR](https://llvm.org) | 15 | llvmir | beta | ANTLR 4 |
+| [Scheme](http://www.scheme-reports.org) | ? | scheme | legacy | JavaCC |
| [EMF Metamodel](https://www.eclipse.org/modeling/emf/) | 2.25.0 | emf | beta | EMF |
| [EMF Model](https://www.eclipse.org/modeling/emf/) | 2.25.0 | emf-model | alpha | EMF |
-| [LLVM IR](https://llvm.org) | 15 | llvmir | beta | ANTLR 4 |
-| [TypeScript](https://www.typescriptlang.org/) | [~5](https://github.com/antlr/grammars-v4/tree/master/javascript/typescript/README.md) | typescript | beta | ANTLR 4 |
-| JavaScript | ES6 | javascript | beta | ANTLR 4 |
+| [SCXML](https://www.w3.org/TR/scxml/) | 1.0 | scxml | alpha | XML |
| Text (naive) | - | text | legacy | CoreNLP |
## Download and Installation
@@ -53,6 +61,7 @@ JPlag is released on [Maven Central](https://search.maven.org/search?q=de.jplag)
de.jplagjplag
+
```
@@ -60,94 +69,70 @@ JPlag is released on [Maven Central](https://search.maven.org/search?q=de.jplag)
1. Download or clone the code from this repository.
2. Run `mvn clean package` from the root of the repository to compile and build all submodules.
Run `mvn clean package assembly:single` instead if you need the full jar which includes all dependencies.
-5. You will find the generated JARs in the subdirectory `cli/target`.
+ Run `mvn -P with-report-viewer clean package assembly:single` to build the full jar with the report viewer. In this case, you'll need [Node.js](https://nodejs.org/en/download) installed.
+3. You will find the generated JARs in the subdirectory `cli/target`.
## Usage
JPlag can either be used via the CLI or directly via its Java API. For more information, see the [usage information in the wiki](https://github.com/jplag/JPlag/wiki/1.-How-to-Use-JPlag). If you are using the CLI, you can display your results via [jplag.github.io](https://jplag.github.io/JPlag/). No data will leave your computer!
### CLI
*Note that the [legacy CLI](https://github.com/jplag/jplag/blob/legacy/README.md) is varying slightly.*
-
-The language can either be set with the -l parameter or as a subcommand. If both a subcommand and the -l option are specified, the subcommand will take priority.
-When using the subcommand language specific arguments can be set.
-A list of language specific options can be obtained by requesting the help page of a subcommand (e.g. "jplag java -h").
+The language can either be set with the -l parameter or as a subcommand (`jplag [jplag options] [language options]`). A subcommand takes priority over the -l option.
+When using the subcommand, language-specific arguments can be set. A list of language-specific options can be obtained by requesting the help page of a subcommand (e.g. `jplag java -h`).
```
-Usage: jplag [OPTIONS] [root-dirs[,root-dirs...]...] [COMMAND]
-
+Parameter descriptions:
[root-dirs[,root-dirs...]...]
- Root-directory with submissions to check for plagiarism
-
+ Root-directory with submissions to check for plagiarism.
-bc, --bc, --base-code=
- Path of the directory containing the base code
- (common framework used in all submissions)
-
- -h, --help display this help and exit
- -l, --language=
- Select the language to parse the submissions (default:
- java). The language names are the same as the
- subcommands.
-
- -n, --shown-comparisons=
- The maximum number of comparisons that will be shown
- in the generated report, if set to -1 all comparisons
- will be shown (default: 100)
-
+ Path to the base code directory (common framework used in all submissions).
+ -l, --language=
+ Select the language of the submissions (default: java). See subcommands below.
+ -M, --mode=<{RUN, VIEW, RUN_AND_VIEW}>
+ The mode of JPlag: either only run analysis, only open the viewer, or do both (default: null)
+ -n, --shown-comparisons=
+ The maximum number of comparisons that will be shown in the generated report, if set to -1 all comparisons will be shown (default: 500)
-new, --new=[,...]
- Root-directory with submissions to check for plagiarism
- (same as the root directory)
-
+ Root-directories with submissions to check for plagiarism (same as root).
+ --normalize Activate the normalization of tokens. Supported for languages: Java, C++.
-old, --old=[,...]
- Root-directory with prior submissions to compare against
-
- -r, --result-directory=
- Name of the directory in which the comparison results
- will be stored (default: result)
-
- -t, --min-tokens=
- Tunes the comparison sensitivity by adjusting the
- minimum token required to be counted as a matching
- section. A smaller increases the sensitivity but
- might lead to more false-positives
+ Root-directories with prior submissions to compare against.
+ -r, --result-file=
+ Name of the file in which the comparison results will be stored (default: results). Missing .zip endings will be automatically added.
+ -t, --min-tokens=
+ Tunes the comparison sensitivity by adjusting the minimum token required to be counted as a matching section. A smaller value increases the sensitivity but might lead to more
+ false-positives.
Advanced
- -d, --debug Debug parser. Non-parsable files will be stored
- (default: false)
-
- -m, --similarity-threshold=
- Comparison similarity threshold [0.0-1.0]: All
- comparisons above this threshold will be saved
- (default: 0.0)
-
- -p, --suffixes=[,...]
- comma-separated list of all filename suffixes that are
- included
-
- -s, --subdirectory=
- Look in directories /*/ for programs
-
- -x, --exclusion-file=
- All files named in this file will be ignored in the
- comparison (line-separated list)
+ --csv-export Export pairwise similarity values as a CSV file.
+ -d, --debug Store on-parsable files in error folder.
+ -m, --similarity-threshold=
+ Comparison similarity threshold [0.0-1.0]: All comparisons above this threshold will be saved (default: 0.0).
+ -p, --suffixes=[,...]
+ comma-separated list of all filename suffixes that are included.
+ -P, --port= The port used for the internal report viewer (default: 1996).
+ -s, --subdirectory=
+ Look in directories /*/ for programs.
+ -x, --exclusion-file=
+ All files named in this file will be ignored in the comparison (line-separated list).
Clustering
- --cluster-alg, --cluster-algorithm=
- Which clustering algorithm to use. Agglomerative merges
- similar submissions bottom up. Spectral clustering is
- combined with Bayesian Optimization to execute
- the k-Means clustering algorithm multiple times,
- hopefully finding a "good" clustering
- automatically. (default: spectral)
-
- --cluster-metric=
- The metric used for clustering. AVG is intersection
- over union, MAX can expose some attempts of
- obfuscation. (default: MAX)
-
- --cluster-skip Skips the clustering (default: false)
-Commands:
+ --cluster-alg, --cluster-algorithm=<{AGGLOMERATIVE, SPECTRAL}>
+ Specifies the clustering algorithm (default: spectral).
+ --cluster-metric=<{AVG, MIN, MAX, INTERSECTION}>
+ The similarity metric used for clustering (default: average similarity).
+ --cluster-skip Skips the cluster calculation.
+
+Subsequence Match Merging
+ --gap-size=
+ Maximal gap between neighboring matches to be merged (between 1 and minTokenMatch, default: 6).
+ --match-merging Enables merging of neighboring matches to counteract obfuscation attempts.
+ --neighbor-length=
+ Minimal length of neighboring matches to be merged (between 1 and minTokenMatch, default: 2).
+
+Subcommands (supported languages):
+ c
cpp
- cpp2
csharp
emf
emf-model
@@ -174,20 +159,21 @@ The new API makes it easy to integrate JPlag's plagiarism detection into externa
```java
-JavaLanguage language = new JavaLanguage();
-language.getOptions(); //Use the object returned by this to set language options(same as language specific arguments above).
+Language language = new JavaLanguage();
Set submissionDirectories = Set.of(new File("/path/to/rootDir"));
File baseCode = new File("/path/to/baseCode");
JPlagOptions options = new JPlagOptions(language, submissionDirectories, Set.of()).withBaseCodeSubmissionDirectory(baseCode);
try {
JPlagResult result = JPlag.run(options);
-
+
// Optional
- ReportObjectFactory reportObjectFactory = new ReportObjectFactory();
- reportObjectFactory.createAndSaveReport(result, "/path/to/output");
+ ReportObjectFactory reportObjectFactory = new ReportObjectFactory(new File("/path/to/output"));
+ reportObjectFactory.createAndSaveReport(result);
} catch (ExitException e) {
// error handling here
+} catch (FileNotFoundException e) {
+ // handle IO exception here
}
```
diff --git a/cli/pom.xml b/cli/pom.xml
index b8851012e..459daf3eb 100644
--- a/cli/pom.xml
+++ b/cli/pom.xml
@@ -7,6 +7,7 @@
${revision}cli
+
@@ -44,12 +45,12 @@
de.jplag
- cpp
+ c${revision}de.jplag
- cpp2
+ cpp${revision}
@@ -128,6 +129,12 @@
picocli4.7.5
+
+
+ me.tongfei
+ progressbar
+ 0.10.0
+
@@ -161,4 +168,56 @@
+
+
+
+ with-report-viewer
+
+
+
+ report-viewer
+ ../report-viewer/dist
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 1.3.2
+
+
+ npm install
+
+ exec
+
+ generate-resources
+
+ npm
+ ../report-viewer
+
+ install
+
+
+
+
+ npm build
+
+ exec
+
+ generate-resources
+
+ npm
+ ../report-viewer
+
+ run
+ build
+
+
+
+
+
+
+
+
+
diff --git a/cli/src/main/java/de/jplag/cli/CLI.java b/cli/src/main/java/de/jplag/cli/CLI.java
index 029e6455d..ac79e68c0 100644
--- a/cli/src/main/java/de/jplag/cli/CLI.java
+++ b/cli/src/main/java/de/jplag/cli/CLI.java
@@ -4,7 +4,11 @@
import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_OPTION_LIST;
import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_SYNOPSIS;
+import java.awt.Desktop;
import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.URI;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.HashSet;
@@ -21,9 +25,12 @@
import de.jplag.JPlagResult;
import de.jplag.Language;
import de.jplag.cli.logger.CollectedLoggerFactory;
+import de.jplag.cli.logger.TongfeiProgressBarProvider;
+import de.jplag.cli.server.ReportViewer;
import de.jplag.clustering.ClusteringOptions;
import de.jplag.clustering.Preprocessing;
import de.jplag.exceptions.ExitException;
+import de.jplag.logging.ProgressBarLogger;
import de.jplag.merging.MergingOptions;
import de.jplag.options.JPlagOptions;
import de.jplag.options.LanguageOption;
@@ -45,12 +52,12 @@ public final class CLI {
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 CREDITS = "Created by IPD Tichy, Guido Malpohl, and others. Maintained by Timur Saglam and Sebastian Hahner. Logo by Sandro Koch.";
private static final String[] DESCRIPTIONS = {"Detecting Software Plagiarism", "Software-Archaeological Playground", "Since 1996",
"Scientifically Published", "Maintained by SDQ", "RIP Structure and Table", "What else?", "You have been warned!", "Since Java 1.0",
- "More Abstract than Tree", "Students Nightmare", "No, changing variable names does not work", "The tech is out there!",
- "Developed by plagiarism experts."};
+ "More Abstract than Tree", "Students Nightmare", "No, changing variable names does not work...", "The tech is out there!",
+ "Developed by plagiarism experts.", "State of the Art Obfuscation Resilience", "www.helmholtz.software/software/jplag"};
private static final String OPTION_LIST_HEADING = "Parameter descriptions: ";
@@ -63,6 +70,8 @@ public final class CLI {
private static final String DESCRIPTION_PATTERN = "%nJPlag - %s%n%s%n%n";
+ private static final String DEFAULT_FILE_ENDING = ".zip";
+
/**
* Main class for using JPlag via the CLI.
* @param args are the CLI arguments that will be passed to JPlag.
@@ -76,15 +85,20 @@ public static void main(String[] args) {
ParseResult parseResult = cli.parseOptions(args);
if (!parseResult.isUsageHelpRequested() && !(parseResult.subcommand() != null && parseResult.subcommand().isUsageHelpRequested())) {
- JPlagOptions options = cli.buildOptionsFromArguments(parseResult);
- JPlagResult result = JPlag.run(options);
- ReportObjectFactory reportObjectFactory = new ReportObjectFactory();
- reportObjectFactory.createAndSaveReport(result, cli.getResultFolder());
-
- OutputFileGenerator.generateCsvOutput(result, new File(cli.getResultFolder()), cli.options);
+ ProgressBarLogger.setProgressBarProvider(new TongfeiProgressBarProvider());
+ switch (cli.options.mode) {
+ case RUN -> cli.runJPlag(parseResult);
+ case VIEW -> cli.runViewer(null);
+ case RUN_AND_VIEW -> cli.runViewer(cli.runJPlag(parseResult));
+ }
+ }
+ } catch (ExitException | IOException exception) { // do not pass exceptions here to keep log clean
+ if (exception.getCause() != null) {
+ logger.error("{} - {}", exception.getMessage(), exception.getCause().getMessage());
+ } else {
+ logger.error(exception.getMessage());
}
- } catch (ExitException exception) {
- logger.error(exception.getMessage()); // do not pass exception here to keep log clean
+
finalizeLogger();
System.exit(1);
}
@@ -102,9 +116,8 @@ public CLI() {
this.commandLine.getHelpSectionMap().put(SECTION_KEY_OPTION_LIST, help -> help.optionList().lines().map(it -> {
if (it.startsWith(" -")) {
return " " + it;
- } else {
- return it;
}
+ return it;
}).collect(Collectors.joining(System.lineSeparator())));
buildSubcommands().forEach(commandLine::addSubcommand);
@@ -114,6 +127,29 @@ public CLI() {
this.commandLine.setAllowSubcommandsAsOptionParameters(true);
}
+ public File runJPlag(ParseResult parseResult) throws ExitException, FileNotFoundException {
+ JPlagOptions jplagOptions = buildOptionsFromArguments(parseResult);
+ JPlagResult result = JPlag.run(jplagOptions);
+ File target = new File(getResultFilePath());
+ ReportObjectFactory reportObjectFactory = new ReportObjectFactory(target);
+ reportObjectFactory.createAndSaveReport(result);
+ logger.info("Successfully written the result: {}", target.getPath());
+ logger.info("View the result using --mode or at: https://jplag.github.io/JPlag/");
+ OutputFileGenerator.generateCsvOutput(result, new File(getResultFileBaseName()), this.options);
+ return target;
+ }
+
+ public void runViewer(File zipFile) throws IOException {
+ ReportViewer reportViewer = new ReportViewer(zipFile, this.options.advanced.port);
+ int port = reportViewer.start();
+ logger.info("ReportViewer started on port http://localhost:{}", port);
+ Desktop.getDesktop().browse(URI.create("http://localhost:" + port + "/"));
+
+ System.out.println("Press Enter key to exit...");
+ System.in.read();
+ reportViewer.stop();
+ }
+
private List buildSubcommands() {
return LanguageLoader.getAllAvailableLanguages().values().stream().map(language -> {
CommandSpec command = CommandSpec.create().name(language.getIdentifier());
@@ -143,7 +179,7 @@ public ParseResult parseOptions(String... args) throws CliException {
}
return result;
} catch (CommandLine.ParameterException e) {
- if (e.getArgSpec().isOption() && Arrays.asList(((OptionSpec) e.getArgSpec()).names()).contains("-l")) {
+ if (e.getArgSpec() != null && e.getArgSpec().isOption() && Arrays.asList(((OptionSpec) e.getArgSpec()).names()).contains("-l")) {
throw new CliException(String.format(UNKOWN_LANGAUGE_EXCEPTION, e.getValue(),
String.join(", ", LanguageLoader.getAllAvailableLanguageIdentifiers())));
}
@@ -181,33 +217,31 @@ public JPlagOptions buildOptionsFromArguments(ParseResult parseResult) throws Cl
JPlagOptions jPlagOptions = new JPlagOptions(loadLanguage(parseResult), this.options.minTokenMatch, submissionDirectories,
oldSubmissionDirectories, null, this.options.advanced.subdirectory, suffixes, this.options.advanced.exclusionFileName,
JPlagOptions.DEFAULT_SIMILARITY_METRIC, this.options.advanced.similarityThreshold, this.options.shownComparisons, clusteringOptions,
- this.options.advanced.debug, mergingOptions);
+ this.options.advanced.debug, mergingOptions, this.options.normalize);
String baseCodePath = this.options.baseCode;
File baseCodeDirectory = baseCodePath == null ? null : new File(baseCodePath);
if (baseCodeDirectory == null || baseCodeDirectory.exists()) {
return jPlagOptions.withBaseCodeSubmissionDirectory(baseCodeDirectory);
- } else {
- logger.warn("Using legacy partial base code API. Please migrate to new full path base code API.");
- return jPlagOptions.withBaseCodeSubmissionName(baseCodePath);
}
+ logger.warn("Using legacy partial base code API. Please migrate to new full path base code API.");
+ return jPlagOptions.withBaseCodeSubmissionName(baseCodePath);
}
private Language loadLanguage(ParseResult result) throws CliException {
- if (result.subcommand() != null) {
- ParseResult subcommandResult = result.subcommand();
- Language language = LanguageLoader.getLanguage(subcommandResult.commandSpec().name())
- .orElseThrow(() -> new CliException(IMPOSSIBLE_EXCEPTION));
- LanguageOptions languageOptions = language.getOptions();
- languageOptions.getOptionsAsList().forEach(option -> {
- if (subcommandResult.hasMatchedOption(option.getNameAsUnixParameter())) {
- option.setValue(subcommandResult.matchedOptionValue(option.getNameAsUnixParameter(), null));
- }
- });
- return language;
- } else {
+ if (result.subcommand() == null) {
return this.options.language;
}
+ ParseResult subcommandResult = result.subcommand();
+ Language language = LanguageLoader.getLanguage(subcommandResult.commandSpec().name())
+ .orElseThrow(() -> new CliException(IMPOSSIBLE_EXCEPTION));
+ LanguageOptions languageOptions = language.getOptions();
+ languageOptions.getOptionsAsList().forEach(option -> {
+ if (subcommandResult.hasMatchedOption(option.getNameAsUnixParameter())) {
+ option.setValue(subcommandResult.matchedOptionValue(option.getNameAsUnixParameter(), null));
+ }
+ });
+ return language;
}
private static ClusteringOptions getClusteringOptions(CliOptions options) {
@@ -249,7 +283,16 @@ private String generateDescription() {
return String.format(DESCRIPTION_PATTERN, randomDescription, CREDITS);
}
- public String getResultFolder() {
- return this.options.resultFolder;
+ private String getResultFilePath() {
+ String optionValue = this.options.resultFile;
+ if (optionValue.endsWith(DEFAULT_FILE_ENDING)) {
+ return optionValue;
+ }
+ return optionValue + DEFAULT_FILE_ENDING;
+ }
+
+ private String getResultFileBaseName() {
+ String defaultOutputFile = getResultFilePath();
+ return defaultOutputFile.substring(0, defaultOutputFile.length() - DEFAULT_FILE_ENDING.length());
}
}
diff --git a/cli/src/main/java/de/jplag/cli/CliOptions.java b/cli/src/main/java/de/jplag/cli/CliOptions.java
index da249342c..384a2d41c 100644
--- a/cli/src/main/java/de/jplag/cli/CliOptions.java
+++ b/cli/src/main/java/de/jplag/cli/CliOptions.java
@@ -7,6 +7,7 @@
import de.jplag.clustering.ClusteringOptions;
import de.jplag.clustering.algorithm.InterClusterSimilarity;
import de.jplag.java.JavaLanguage;
+import de.jplag.merging.MergingOptions;
import de.jplag.options.JPlagOptions;
import de.jplag.options.SimilarityMetric;
@@ -19,47 +20,51 @@
public class CliOptions implements Runnable {
public static final Language defaultLanguage = new JavaLanguage();
- @Parameters(paramLabel = "root-dirs", description = "Root-directory with submissions to check for plagiarism%n", split = ",")
+ @Parameters(paramLabel = "root-dirs", description = "Root-directory with submissions to check for plagiarism.", split = ",")
public File[] rootDirectory = new File[0];
- @Option(names = {"--new",
- "-new"}, split = ",", description = "Root-directory with submissions to check for plagiarism (same as the root directory)%n")
+ @Option(names = {"--new", "-new"}, split = ",", description = "Root-directories with submissions to check for plagiarism (same as root).")
public File[] newDirectories = new File[0];
- @Option(names = {"--old", "-old"}, split = ",", description = "Root-directory with prior submissions to compare against%n")
+ @Option(names = {"--old", "-old"}, split = ",", description = "Root-directories with prior submissions to compare against.")
public File[] oldDirectories = new File[0];
@Option(names = {"--language",
- "-l"}, arity = "1", converter = LanguageConverter.class, completionCandidates = LanguageCandidates.class, description = "Select the language to parse the submissions (default: ${DEFAULT-VALUE}). The language names are the same as the subcommands.%n")
+ "-l"}, arity = "1", converter = LanguageConverter.class, completionCandidates = LanguageCandidates.class, description = "Select the language of the submissions (default: ${DEFAULT-VALUE}). See subcommands below.")
public Language language = defaultLanguage;
- @Option(names = {"-bc", "--bc",
- "--base-code"}, description = "Path of the directory containing the base code (common framework used in all submissions)%n")
+ @Option(names = {"-bc", "--bc", "--base-code"}, description = "Path to the base code directory (common framework used in all submissions).")
public String baseCode;
- @Option(names = {"-t", "--min-tokens"}, description = "Tunes the comparison sensitivity by adjusting the minimum token required to be counted "
- + "as a matching section. A smaller increases the sensitivity but might lead to more " + "false-positives%n")
+ @Option(names = {"-t",
+ "--min-tokens"}, description = "Tunes the comparison sensitivity by adjusting the minimum token required to be counted as a matching section. A smaller value increases the sensitivity but might lead to more false-positives.")
public Integer minTokenMatch = null;
- @Option(names = {"-h", "--help"}, usageHelp = true, description = "display this help and exit")
+ @Option(names = {"-h", "--help"}, usageHelp = true, description = "Display this help text", hidden = true)
public boolean help;
@Option(names = {"-n",
- "--shown-comparisons"}, description = "The maximum number of comparisons that will be shown in the generated report, if set "
- + "to -1 all comparisons will be shown (default: ${DEFAULT-VALUE})%n")
+ "--shown-comparisons"}, description = "The maximum number of comparisons that will be shown in the generated report, if set to -1 all comparisons will be shown (default: ${DEFAULT-VALUE})")
public int shownComparisons = JPlagOptions.DEFAULT_SHOWN_COMPARISONS;
@Option(names = {"-r",
- "--result-directory"}, description = "Name of the directory in which the comparison results will be stored (default: ${DEFAULT-VALUE})%n")
- public String resultFolder = "results";
+ "--result-file"}, description = "Name of the file in which the comparison results will be stored (default: ${DEFAULT-VALUE}). Missing .zip endings will be automatically added.")
+ public String resultFile = "results";
- @ArgGroup(heading = "Advanced%n", exclusive = false)
+ @Option(names = {"-M",
+ "--mode"}, description = "The mode of JPlag: either only run analysis, only open the viewer, or do both (default: ${DEFAULT_VALUE})")
+ public JPlagMode mode = JPlagMode.RUN;
+
+ @Option(names = {"--normalize"}, description = "Activate the normalization of tokens. Supported for languages: Java, C++.")
+ public boolean normalize = false;
+
+ @ArgGroup(heading = "%nAdvanced%n", exclusive = false)
public Advanced advanced = new Advanced();
- @ArgGroup(validate = false, heading = "Clustering%n")
+ @ArgGroup(validate = false, heading = "%nClustering%n")
public Clustering clustering = new Clustering();
- @ArgGroup(validate = false, heading = "Merging of neighboring matches to increase the similarity of concealed plagiarism:%n")
+ @ArgGroup(validate = false, heading = "%nSubsequence Match Merging%n")
public Merging merging = new Merging();
/**
@@ -71,59 +76,58 @@ public void run() {
}
public static class Advanced {
- @Option(names = {"-d", "--debug"}, description = "Debug parser. Non-parsable files will be stored (default: ${DEFAULT-VALUE})%n")
+ @Option(names = {"-d", "--debug"}, description = "Store on-parsable files in error folder.")
public boolean debug;
- @Option(names = {"-s", "--subdirectory"}, description = "Look in directories /*/ for programs%n")
+ @Option(names = {"-s", "--subdirectory"}, description = "Look in directories /*/ for programs.")
public String subdirectory;
- @Option(names = {"-p", "--suffixes"}, split = ",", description = "comma-separated list of all filename suffixes that are included%n")
+ @Option(names = {"-p", "--suffixes"}, split = ",", description = "comma-separated list of all filename suffixes that are included.")
public String[] suffixes = new String[0];
@Option(names = {"-x",
- "--exclusion-file"}, description = "All files named in this file will be ignored in the comparison (line-separated list)%n")
+ "--exclusion-file"}, description = "All files named in this file will be ignored in the comparison (line-separated list).")
public String exclusionFileName;
@Option(names = {"-m",
- "--similarity-threshold"}, description = "Comparison similarity threshold [0.0-1.0]: All comparisons above this threshold will "
- + "be saved (default: ${DEFAULT-VALUE})%n")
+ "--similarity-threshold"}, description = "Comparison similarity threshold [0.0-1.0]: All comparisons above this threshold will "
+ + "be saved (default: ${DEFAULT-VALUE}).")
public double similarityThreshold = JPlagOptions.DEFAULT_SIMILARITY_THRESHOLD;
- @Option(names = "--csv-export", description = "If present, a csv export will be generated in addition to the zip file.")
+ @Option(names = {"-P", "--port"}, description = "The port used for the internal report viewer (default: ${DEFAULT-VALUE}).")
+ public int port = 1996;
+
+ @Option(names = "--csv-export", description = "Export pairwise similarity values as a CSV file.")
public boolean csvExport = false;
}
public static class Clustering {
- @Option(names = {"--cluster-skip"}, description = "Skips the clustering (default: ${DEFAULT-VALUE})%n")
+ @Option(names = {"--cluster-skip"}, description = "Skips the cluster calculation.")
public boolean disable;
@ArgGroup
public ClusteringEnabled enabled = new ClusteringEnabled();
public static class ClusteringEnabled {
- @Option(names = {"--cluster-alg",
- "--cluster-algorithm"}, description = "Which clustering algorithm to use. Agglomerative merges similar submissions bottom up. "
- + "Spectral clustering is combined with Bayesian Optimization to execute the k-Means "
- + "clustering algorithm multiple times, hopefully finding a \"good\" clustering "
- + "automatically. (default: ${DEFAULT-VALUE})%n")
+ @Option(names = {"--cluster-alg", "--cluster-algorithm"}, description = "Specifies the clustering algorithm (default: ${DEFAULT-VALUE}).")
public ClusteringAlgorithm algorithm = new ClusteringOptions().algorithm();
- @Option(names = {
- "--cluster-metric"}, description = "The metric used for clustering. AVG is intersection over union, MAX can expose some "
- + "attempts of obfuscation. (default: ${DEFAULT-VALUE})%n")
+ @Option(names = {"--cluster-metric"}, description = "The similarity metric used for clustering (default: ${DEFAULT-VALUE}).")
public SimilarityMetric metric = new ClusteringOptions().similarityMetric();
}
}
public static class Merging {
- @Option(names = {"--match-merging"}, description = "Enables match merging (default: false)%n")
- public boolean enabled;
+ @Option(names = {"--match-merging"}, description = "Enables merging of neighboring matches to counteract obfuscation attempts.")
+ public boolean enabled = MergingOptions.DEFAULT_ENABLED;
- @Option(names = {"--neighbor-length"}, description = "Defines how short a match can be, to be considered (default: 2)%n")
- public int minimumNeighborLength;
+ @Option(names = {
+ "--neighbor-length"}, description = "Minimal length of neighboring matches to be merged (between 1 and minTokenMatch, default: ${DEFAULT-VALUE}).%n")
+ public int minimumNeighborLength = MergingOptions.DEFAULT_NEIGHBOR_LENGTH;
- @Option(names = {"--gap-size"}, description = "Defines how many token there can be between two neighboring matches (default: 6)%n")
- public int maximumGapSize;
+ @Option(names = {
+ "--gap-size"}, description = "Maximal gap between neighboring matches to be merged (between 1 and minTokenMatch, default: ${DEFAULT-VALUE}).")
+ public int maximumGapSize = MergingOptions.DEFAULT_GAP_SIZE;
}
diff --git a/cli/src/main/java/de/jplag/cli/JPlagMode.java b/cli/src/main/java/de/jplag/cli/JPlagMode.java
new file mode 100644
index 000000000..402a18b58
--- /dev/null
+++ b/cli/src/main/java/de/jplag/cli/JPlagMode.java
@@ -0,0 +1,19 @@
+package de.jplag.cli;
+
+/**
+ * The mode JPlag runs in. This influences which steps JPlag will execute.
+ */
+public enum JPlagMode {
+ /**
+ * Only run JPlag and create a results.zip
+ */
+ RUN,
+ /**
+ * Only start the report viewer
+ */
+ VIEW,
+ /**
+ * Run JPlag and open the result in report viewer
+ */
+ RUN_AND_VIEW
+}
diff --git a/cli/src/main/java/de/jplag/cli/LanguageLoader.java b/cli/src/main/java/de/jplag/cli/LanguageLoader.java
index 77c2de673..2ee1c815b 100644
--- a/cli/src/main/java/de/jplag/cli/LanguageLoader.java
+++ b/cli/src/main/java/de/jplag/cli/LanguageLoader.java
@@ -32,8 +32,9 @@ private LanguageLoader() {
* @return the languages as unmodifiable map from identifier to language instance.
*/
public static synchronized Map getAllAvailableLanguages() {
- if (cachedLanguageInstances != null)
+ if (cachedLanguageInstances != null) {
return cachedLanguageInstances;
+ }
Map languages = new TreeMap<>();
@@ -44,10 +45,10 @@ public static synchronized Map getAllAvailableLanguages() {
languages.remove(languageIdentifier);
continue;
}
- logger.debug("Loading Language Module '{}'", language.getName());
+ logger.trace("Loading Language Module '{}'", language.getName());
languages.put(languageIdentifier, language);
}
- logger.info("Available languages: '{}'", languages.values().stream().map(Language::getName).toList());
+ logger.debug("Available languages: '{}'", languages.values().stream().map(Language::getName).toList());
cachedLanguageInstances = Collections.unmodifiableMap(languages);
return cachedLanguageInstances;
@@ -61,8 +62,9 @@ public static synchronized Map getAllAvailableLanguages() {
*/
public static Optional getLanguage(String identifier) {
var language = getAllAvailableLanguages().get(identifier);
- if (language == null)
+ if (language == null) {
logger.warn("Attempt to load Language {} was not successful", identifier);
+ }
return Optional.ofNullable(language);
}
diff --git a/cli/src/main/java/de/jplag/cli/logger/CollectedLogger.java b/cli/src/main/java/de/jplag/cli/logger/CollectedLogger.java
index 80d94a74a..3be42c8cd 100644
--- a/cli/src/main/java/de/jplag/cli/logger/CollectedLogger.java
+++ b/cli/src/main/java/de/jplag/cli/logger/CollectedLogger.java
@@ -77,8 +77,9 @@ private void log(int level, String message, Throwable throwable, Date timeOfErro
builder.append('[').append(renderLevel(level)).append(']').append(' ');
// Append the name of the log instance
- if (shortLogName == null)
+ if (shortLogName == null) {
shortLogName = computeShortName();
+ }
builder.append(shortLogName).append(" - ");
// Append the message
builder.append(message);
@@ -90,8 +91,9 @@ void printAllErrorsForLogger() {
this.isFinalizing = true;
// Copy errors to prevent infinite recursion
var errors = new ArrayList<>(this.allErrors);
- if (errors.isEmpty())
+ if (errors.isEmpty()) {
return;
+ }
this.allErrors.removeAll(errors);
diff --git a/cli/src/main/java/de/jplag/cli/logger/CollectedLoggerFactory.java b/cli/src/main/java/de/jplag/cli/logger/CollectedLoggerFactory.java
index ffba004b9..e356763e3 100644
--- a/cli/src/main/java/de/jplag/cli/logger/CollectedLoggerFactory.java
+++ b/cli/src/main/java/de/jplag/cli/logger/CollectedLoggerFactory.java
@@ -28,11 +28,10 @@ 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;
}
+ CollectedLogger newInstance = new CollectedLogger(name);
+ Logger oldInstance = loggerMap.putIfAbsent(name, newInstance);
+ return oldInstance == null ? newInstance : oldInstance;
}
/**
diff --git a/cli/src/main/java/de/jplag/cli/logger/TongfeiProgressBar.java b/cli/src/main/java/de/jplag/cli/logger/TongfeiProgressBar.java
new file mode 100644
index 000000000..4305a497e
--- /dev/null
+++ b/cli/src/main/java/de/jplag/cli/logger/TongfeiProgressBar.java
@@ -0,0 +1,24 @@
+package de.jplag.cli.logger;
+
+import de.jplag.logging.ProgressBar;
+
+/**
+ * A ProgressBar, that used the tongfei progress bar library underneath, to show progress bars on the cli.
+ */
+public class TongfeiProgressBar implements ProgressBar {
+ private final me.tongfei.progressbar.ProgressBar progressBar;
+
+ public TongfeiProgressBar(me.tongfei.progressbar.ProgressBar progressBar) {
+ this.progressBar = progressBar;
+ }
+
+ @Override
+ public void step(int number) {
+ this.progressBar.stepBy(number);
+ }
+
+ @Override
+ public void dispose() {
+ this.progressBar.close();
+ }
+}
diff --git a/cli/src/main/java/de/jplag/cli/logger/TongfeiProgressBarProvider.java b/cli/src/main/java/de/jplag/cli/logger/TongfeiProgressBarProvider.java
new file mode 100644
index 000000000..da09fff33
--- /dev/null
+++ b/cli/src/main/java/de/jplag/cli/logger/TongfeiProgressBarProvider.java
@@ -0,0 +1,20 @@
+package de.jplag.cli.logger;
+
+import de.jplag.logging.ProgressBar;
+import de.jplag.logging.ProgressBarProvider;
+import de.jplag.logging.ProgressBarType;
+
+import me.tongfei.progressbar.ProgressBarBuilder;
+import me.tongfei.progressbar.ProgressBarStyle;
+
+/**
+ * A ProgressBar provider, that used the tongfei progress bar library underneath, to show progress bars on the cli.
+ */
+public class TongfeiProgressBarProvider implements ProgressBarProvider {
+ @Override
+ public ProgressBar initProgressBar(ProgressBarType type, int totalSteps) {
+ me.tongfei.progressbar.ProgressBar progressBar = new ProgressBarBuilder().setTaskName(type.getDefaultText()).setInitialMax(totalSteps)
+ .setStyle(ProgressBarStyle.ASCII).build();
+ return new TongfeiProgressBar(progressBar);
+ }
+}
diff --git a/cli/src/main/java/de/jplag/cli/server/ContentType.java b/cli/src/main/java/de/jplag/cli/server/ContentType.java
new file mode 100644
index 000000000..cf90673b3
--- /dev/null
+++ b/cli/src/main/java/de/jplag/cli/server/ContentType.java
@@ -0,0 +1,41 @@
+package de.jplag.cli.server;
+
+/**
+ * Data types used by JPlag in the context of http. Contains the according mime type.
+ */
+public enum ContentType {
+ HTML("text/html; charset=utf-8", ".html"),
+ JS("application/javascript; charset=utf-8", ".js"),
+ CSS("text/css; charset=utf-8", ".css"),
+ PNG("image/png", ".png"),
+ PLAIN("text/plain; charset=utf-8", null),
+ ZIP("application/zip", ".zip");
+
+ private final String value;
+
+ private final String nameSuffix;
+
+ ContentType(String value, String nameSuffix) {
+ this.value = value;
+ this.nameSuffix = nameSuffix;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ /**
+ * Guesses the type from the given path using the suffix after the last '.'.
+ * @param path The path to guess from
+ * @return The guessed type
+ */
+ public static ContentType fromPath(String path) {
+ String suffix = path.substring(path.lastIndexOf('.'));
+ for (ContentType value : ContentType.values()) {
+ if (suffix.equals(value.nameSuffix)) {
+ return value;
+ }
+ }
+ return ContentType.PLAIN;
+ }
+}
diff --git a/cli/src/main/java/de/jplag/cli/server/HttpRequestMethod.java b/cli/src/main/java/de/jplag/cli/server/HttpRequestMethod.java
new file mode 100644
index 000000000..acbf4a5b9
--- /dev/null
+++ b/cli/src/main/java/de/jplag/cli/server/HttpRequestMethod.java
@@ -0,0 +1,32 @@
+package de.jplag.cli.server;
+
+/**
+ * Wraps the http request methods used by JPlag. Request methods determine the capabilities of a http request.
+ */
+public enum HttpRequestMethod {
+ GET("GET"),
+ POST("POST");
+
+ private final String name;
+
+ /**
+ * @param name The name of the request method
+ */
+ HttpRequestMethod(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public static HttpRequestMethod fromName(String name) {
+ for (HttpRequestMethod value : HttpRequestMethod.values()) {
+ if (value.name.equals(name)) {
+ return value;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/cli/src/main/java/de/jplag/cli/server/ReportViewer.java b/cli/src/main/java/de/jplag/cli/server/ReportViewer.java
new file mode 100644
index 000000000..6e861c926
--- /dev/null
+++ b/cli/src/main/java/de/jplag/cli/server/ReportViewer.java
@@ -0,0 +1,133 @@
+package de.jplag.cli.server;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.BindException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.tuple.Pair;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+
+/**
+ * Manages the internal report viewer. Serves the static files for the report viewer and the results.zip.
+ */
+public class ReportViewer implements HttpHandler {
+ private static final String REPORT_VIEWER_RESOURCE_PREFIX = "report-viewer";
+ private static final String INDEX_PATH = "index.html";
+ private static final String RESULT_PATH = "results.zip";
+
+ private static final Logger logger = LoggerFactory.getLogger(ReportViewer.class);
+ private static final int SUCCESS_RESPONSE = 200;
+ private static final int NOT_FOUND_RESPONSE = 404;
+ private static final int MAX_PORT_LOOKUPS = 4;
+
+ private final RoutingTree routingTree;
+ private final int port;
+
+ private HttpServer server;
+
+ /**
+ * @param zipFile The zip file to use for the report viewer
+ * @param port The port to use for the server. You can use 0 to use any free port.
+ * @throws IOException If the zip file cannot be read
+ */
+ public ReportViewer(File zipFile, int port) throws IOException {
+ this.routingTree = new RoutingTree();
+
+ this.routingTree.insertRouting("", new RoutingResources(REPORT_VIEWER_RESOURCE_PREFIX).or(new RoutingAlias(INDEX_PATH)));
+ this.routingTree.insertRouting(RESULT_PATH, new RoutingStaticFile(zipFile, ContentType.ZIP));
+ this.port = port;
+ }
+
+ /**
+ * Starts the server and serves the internal report viewer. If available, the result.zip is also exposed. If the given
+ * port is already in use, the next free port will be used.
+ * @return The port the server runs at
+ * @throws IOException If the server cannot be started
+ */
+ public int start() throws IOException {
+ if (server != null) {
+ throw new IllegalStateException("Server already started");
+ }
+
+ int currentPort = this.port;
+ int remainingLookups = MAX_PORT_LOOKUPS;
+ BindException lastException = new BindException("Could not create server. Probably due to no free port found.");
+ while (server == null && remainingLookups-- > 0) {
+ try {
+ server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), currentPort), 0);
+ } catch (BindException e) {
+ logger.info("Port {} is not available. Trying to find a different one.", currentPort);
+ lastException = e;
+ currentPort++;
+ }
+ }
+ if (server == null) {
+ throw lastException;
+ }
+ server.createContext("/", this);
+ server.setExecutor(null);
+ server.start();
+
+ return server.getAddress().getPort();
+ }
+
+ /**
+ * Stops the server
+ */
+ public void stop() {
+ server.stop(0);
+ }
+
+ /**
+ * Do not call manually. Called by the running web server.
+ * @param exchange The http reqest
+ * @throws IOException If the IO handling goes wrong
+ */
+ @Override
+ public void handle(HttpExchange exchange) throws IOException {
+ RoutingPath path = new RoutingPath(exchange.getRequestURI().getPath());
+ Pair resolved = this.routingTree.resolveRouting(path);
+ HttpRequestMethod method = HttpRequestMethod.fromName(exchange.getRequestMethod());
+
+ if (resolved == null || !ArrayUtils.contains(resolved.getRight().allowedMethods(), method)) {
+ exchange.sendResponseHeaders(NOT_FOUND_RESPONSE, 0);
+ exchange.close();
+ return;
+ }
+
+ logger.debug("Serving {}", path);
+
+ ResponseData responseData = resolved.getRight().fetchData(resolved.getLeft(), exchange, this);
+ if (responseData == null) {
+ logger.warn("No response data found for path: {}", path.asPath());
+ exchange.sendResponseHeaders(NOT_FOUND_RESPONSE, 0);
+ exchange.close();
+ return;
+ }
+
+ InputStream inputStream = responseData.stream();
+
+ if (responseData.contentType() != null) {
+ exchange.getResponseHeaders().set("Content-Type", responseData.contentType().getValue());
+ }
+ exchange.sendResponseHeaders(SUCCESS_RESPONSE, responseData.size());
+
+ inputStream.transferTo(exchange.getResponseBody());
+ exchange.getResponseBody().flush();
+ exchange.getResponseBody().close();
+ inputStream.close();
+ }
+
+ RoutingTree getRoutingTree() {
+ return routingTree;
+ }
+}
diff --git a/cli/src/main/java/de/jplag/cli/server/ResponseData.java b/cli/src/main/java/de/jplag/cli/server/ResponseData.java
new file mode 100644
index 000000000..26e9d037d
--- /dev/null
+++ b/cli/src/main/java/de/jplag/cli/server/ResponseData.java
@@ -0,0 +1,46 @@
+package de.jplag.cli.server;
+
+import java.io.InputStream;
+
+/**
+ * Data for a http response
+ * @param stream The stream containing the binary data
+ * @param contentType The type of data
+ * @param size The total size of the data
+ */
+public record ResponseData(InputStream stream, ContentType contentType, int size) {
+ /**
+ * Constructor with unknown type and size. Type will be set to PLAIN.
+ * @param data The binary data to respond with
+ */
+ public ResponseData(InputStream data) {
+ this(data, ContentType.PLAIN, 0);
+ }
+
+ /**
+ * Constructor with unknown size
+ * @param data The binary data
+ * @param contentType The type of content
+ */
+ public ResponseData(InputStream data, ContentType contentType) {
+ this(data, contentType, 0);
+ }
+
+ /**
+ * Creates a new instance for a given resource url.
+ * @param url The resource url
+ * @return The new response data
+ */
+ public static ResponseData fromResourceUrl(String url) {
+ if (url.endsWith("/")) {
+ return null;
+ }
+
+ InputStream inputStream = ResponseData.class.getResourceAsStream(url);
+
+ if (inputStream != null) {
+ return new ResponseData(inputStream, ContentType.fromPath(url));
+ }
+ return null;
+ }
+}
diff --git a/cli/src/main/java/de/jplag/cli/server/Routing.java b/cli/src/main/java/de/jplag/cli/server/Routing.java
new file mode 100644
index 000000000..a6152a031
--- /dev/null
+++ b/cli/src/main/java/de/jplag/cli/server/Routing.java
@@ -0,0 +1,33 @@
+package de.jplag.cli.server;
+
+import com.sun.net.httpserver.HttpExchange;
+
+/**
+ * Handles the data for a url prefix.
+ */
+public interface Routing {
+ /**
+ * @return The methods, that this routing can be used for.
+ */
+ default HttpRequestMethod[] allowedMethods() {
+ return new HttpRequestMethod[] {HttpRequestMethod.GET};
+ }
+
+ /**
+ * Gets the data for the given url
+ * @param subPath The remaining suffix of the url, that is not jet interpreted
+ * @param request The original http request
+ * @param viewer The current report viewer
+ * @return The data to respond with
+ */
+ ResponseData fetchData(RoutingPath subPath, HttpExchange request, ReportViewer viewer);
+
+ /**
+ * Use the other routing if this routing does not find any data.
+ * @param other The other routing
+ * @return The combined routing
+ */
+ default Routing or(Routing other) {
+ return new RoutingFallback(this, other);
+ }
+}
diff --git a/cli/src/main/java/de/jplag/cli/server/RoutingAlias.java b/cli/src/main/java/de/jplag/cli/server/RoutingAlias.java
new file mode 100644
index 000000000..70c4aefe1
--- /dev/null
+++ b/cli/src/main/java/de/jplag/cli/server/RoutingAlias.java
@@ -0,0 +1,36 @@
+package de.jplag.cli.server;
+
+import org.apache.commons.lang3.tuple.Pair;
+
+import com.sun.net.httpserver.HttpExchange;
+
+/**
+ * An alias routing, that will respond with the response for a different path
+ */
+public class RoutingAlias implements Routing {
+ private final RoutingPath path;
+
+ /**
+ * @param path The path to actually use
+ */
+ public RoutingAlias(RoutingPath path) {
+ this.path = path;
+ }
+
+ /**
+ * @param path The path to actually use
+ */
+ public RoutingAlias(String path) {
+ this(new RoutingPath(path));
+ }
+
+ @Override
+ public ResponseData fetchData(RoutingPath subPath, HttpExchange request, ReportViewer viewer) {
+ Pair redirect = viewer.getRoutingTree().resolveRouting(path);
+ if (redirect == null) {
+ return null;
+ }
+
+ return redirect.getValue().fetchData(redirect.getLeft(), request, viewer);
+ }
+}
diff --git a/cli/src/main/java/de/jplag/cli/server/RoutingFallback.java b/cli/src/main/java/de/jplag/cli/server/RoutingFallback.java
new file mode 100644
index 000000000..13de6d6d2
--- /dev/null
+++ b/cli/src/main/java/de/jplag/cli/server/RoutingFallback.java
@@ -0,0 +1,30 @@
+package de.jplag.cli.server;
+
+import com.sun.net.httpserver.HttpExchange;
+
+/**
+ * Responds with the first given routing, unless that would respond with null, in that case the second one is used.
+ */
+public class RoutingFallback implements Routing {
+ private final Routing first;
+ private final Routing second;
+
+ /**
+ * @param first The first routing
+ * @param second The second routing
+ */
+ public RoutingFallback(Routing first, Routing second) {
+ this.first = first;
+ this.second = second;
+ }
+
+ @Override
+ public ResponseData fetchData(RoutingPath subPath, HttpExchange request, ReportViewer viewer) {
+ ResponseData attempt = this.first.fetchData(subPath, request, viewer);
+ if (attempt != null) {
+ return attempt;
+ }
+
+ return this.second.fetchData(subPath, request, viewer);
+ }
+}
diff --git a/cli/src/main/java/de/jplag/cli/server/RoutingPath.java b/cli/src/main/java/de/jplag/cli/server/RoutingPath.java
new file mode 100644
index 000000000..e9263c8e8
--- /dev/null
+++ b/cli/src/main/java/de/jplag/cli/server/RoutingPath.java
@@ -0,0 +1,72 @@
+package de.jplag.cli.server;
+
+import java.util.Arrays;
+
+/**
+ * A path used for routing. Can be used like a linked list.
+ */
+public class RoutingPath {
+ private final String[] components;
+ private final int offset;
+
+ /**
+ * @param path The full path
+ */
+ public RoutingPath(String path) {
+ this.components = Arrays.stream(path.split("/", 0)).filter(it -> !it.isBlank()).toArray(String[]::new);
+ this.offset = 0;
+ }
+
+ private RoutingPath(String[] components, int offset) {
+ this.components = components;
+ this.offset = offset;
+ }
+
+ /**
+ * @return The first path segment
+ */
+ public String head() {
+ return components[offset];
+ }
+
+ /**
+ * @return All path segments except the first
+ */
+ public RoutingPath tail() {
+ if (!hasTail()) {
+ throw new IllegalStateException("Routing path is done.");
+ }
+
+ return new RoutingPath(this.components, this.offset + 1);
+ }
+
+ /**
+ * @return True, if the tail has at least 0 elements
+ */
+ public boolean hasTail() {
+ return this.components.length > this.offset;
+ }
+
+ /**
+ * @return True, if there are no segments in this path
+ */
+ public boolean isEmpty() {
+ return this.offset == this.components.length;
+ }
+
+ /**
+ * @return The remaining path as a string
+ */
+ public String asPath() {
+ StringBuilder builder = new StringBuilder();
+
+ for (int i = this.offset; i < this.components.length; i++) {
+ if (i > this.offset) {
+ builder.append("/");
+ }
+ builder.append(this.components[i]);
+ }
+
+ return builder.toString();
+ }
+}
diff --git a/cli/src/main/java/de/jplag/cli/server/RoutingResources.java b/cli/src/main/java/de/jplag/cli/server/RoutingResources.java
new file mode 100644
index 000000000..d46a8fe43
--- /dev/null
+++ b/cli/src/main/java/de/jplag/cli/server/RoutingResources.java
@@ -0,0 +1,31 @@
+package de.jplag.cli.server;
+
+import com.sun.net.httpserver.HttpExchange;
+
+/**
+ * Responds with data from the resources
+ */
+public class RoutingResources implements Routing {
+ private String prefix;
+
+ /**
+ * @param prefix The prefix to use within the resources
+ */
+ public RoutingResources(String prefix) {
+ this.prefix = prefix;
+
+ if (!this.prefix.startsWith("/")) {
+ this.prefix = "/" + this.prefix;
+ }
+
+ if (!this.prefix.endsWith("/")) {
+ this.prefix = this.prefix + "/";
+ }
+ }
+
+ @Override
+ public ResponseData fetchData(RoutingPath subPath, HttpExchange request, ReportViewer viewer) {
+ String fullPath = this.prefix + subPath.asPath();
+ return ResponseData.fromResourceUrl(fullPath);
+ }
+}
diff --git a/cli/src/main/java/de/jplag/cli/server/RoutingStaticFile.java b/cli/src/main/java/de/jplag/cli/server/RoutingStaticFile.java
new file mode 100644
index 000000000..3ce5bed1a
--- /dev/null
+++ b/cli/src/main/java/de/jplag/cli/server/RoutingStaticFile.java
@@ -0,0 +1,42 @@
+package de.jplag.cli.server;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+
+import com.sun.net.httpserver.HttpExchange;
+
+/**
+ * Responds with a given file
+ */
+public class RoutingStaticFile implements Routing {
+ private final byte[] data;
+ private final ContentType contentType;
+
+ /**
+ * @param file The file to use
+ * @param contentType The type of content in the file
+ * @throws IOException If the file cannot be read
+ */
+ public RoutingStaticFile(File file, ContentType contentType) throws IOException {
+ if (file != null) {
+ try (FileInputStream inputStream = new FileInputStream(file)) {
+ this.data = inputStream.readAllBytes();
+
+ this.contentType = contentType;
+ }
+ } else {
+ this.data = null;
+ this.contentType = contentType;
+ }
+ }
+
+ @Override
+ public ResponseData fetchData(RoutingPath subPath, HttpExchange request, ReportViewer viewer) {
+ if (this.data != null) {
+ return new ResponseData(new ByteArrayInputStream(this.data), contentType, this.data.length);
+ }
+ return null;
+ }
+}
diff --git a/cli/src/main/java/de/jplag/cli/server/RoutingTree.java b/cli/src/main/java/de/jplag/cli/server/RoutingTree.java
new file mode 100644
index 000000000..83bc4dff3
--- /dev/null
+++ b/cli/src/main/java/de/jplag/cli/server/RoutingTree.java
@@ -0,0 +1,87 @@
+package de.jplag.cli.server;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.lang3.tuple.Pair;
+
+/**
+ * Manages the tree of paths handled by the web server
+ */
+public class RoutingTree {
+ private final RoutingTreeNode root;
+
+ /**
+ * Creates an empty tree
+ */
+ public RoutingTree() {
+ this.root = new RoutingTreeNode();
+ }
+
+ /**
+ * Adds a new routing to the tree
+ * @param path The path to use the routing for
+ * @param routing The routing
+ */
+ public void insertRouting(RoutingPath path, Routing routing) {
+ this.root.buildRouting(path, routing);
+ }
+
+ /**
+ * Adds a new routing to the tree
+ * @param path The path to use the routing for
+ * @param routing The routing
+ */
+ public void insertRouting(String path, Routing routing) {
+ this.insertRouting(new RoutingPath(path), routing);
+ }
+
+ /**
+ * Gets the routing for a given path
+ * @param path The path to look up
+ * @return The remaining path to be handled by the routing and the found routing
+ */
+ public Pair resolveRouting(RoutingPath path) {
+ return this.root.resolve(path);
+ }
+
+ private static class RoutingTreeNode {
+ private final Map children;
+ private Routing routing;
+
+ public RoutingTreeNode(RoutingPath building, Routing routing) {
+ this();
+ this.buildRouting(building, routing);
+ }
+
+ public RoutingTreeNode() {
+ this.children = new HashMap<>();
+ }
+
+ public void buildRouting(RoutingPath building, Routing routing) {
+ if (building.isEmpty()) {
+ this.routing = routing;
+ } else if (this.children.containsKey(building.head())) {
+ this.children.get(building.head()).buildRouting(building.tail(), routing);
+ } else {
+ this.children.put(building.head(), new RoutingTreeNode(building.tail(), routing));
+ }
+ }
+
+ public Pair resolve(RoutingPath path) {
+ if ((path.isEmpty() || !this.children.containsKey(path.head())) && this.routing != null) {
+ return Pair.of(path, this.routing);
+ }
+
+ if (this.children.containsKey(path.head()) && !path.isEmpty()) {
+ Pair childResolved = this.children.get(path.head()).resolve(path.tail());
+ if (childResolved == null && this.routing != null) {
+ return Pair.of(path, this.routing);
+ }
+ return childResolved;
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/cli/src/main/resources/README.md b/cli/src/main/resources/README.md
new file mode 100644
index 000000000..5355d847c
--- /dev/null
+++ b/cli/src/main/resources/README.md
@@ -0,0 +1 @@
+Copy ReportViewer to a directory called `JPlag`
\ No newline at end of file
diff --git a/cli/src/test/java/de/jplag/cli/ClusteringTest.java b/cli/src/test/java/de/jplag/cli/ClusteringTest.java
index bf591726e..75ee12124 100644
--- a/cli/src/test/java/de/jplag/cli/ClusteringTest.java
+++ b/cli/src/test/java/de/jplag/cli/ClusteringTest.java
@@ -1,6 +1,8 @@
package de.jplag.cli;
-import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
diff --git a/cli/src/test/java/de/jplag/cli/MergingOptionsTest.java b/cli/src/test/java/de/jplag/cli/MergingOptionsTest.java
new file mode 100644
index 000000000..bf2b642c7
--- /dev/null
+++ b/cli/src/test/java/de/jplag/cli/MergingOptionsTest.java
@@ -0,0 +1,25 @@
+package de.jplag.cli;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import de.jplag.merging.MergingOptions;
+
+/**
+ * Test cases for the options of the match merging mechanism.
+ */
+class MergingOptionsTest extends CommandLineInterfaceTest {
+
+ @Test
+ @DisplayName("Test if default values are used when creating merging options from CLI")
+ void testMergingDefault() throws CliException {
+ buildOptionsFromCLI(defaultArguments());
+ assertNotNull(options.mergingOptions());
+ assertEquals(MergingOptions.DEFAULT_ENABLED, options.mergingOptions().enabled());
+ assertEquals(MergingOptions.DEFAULT_NEIGHBOR_LENGTH, options.mergingOptions().minimumNeighborLength());
+ assertEquals(MergingOptions.DEFAULT_GAP_SIZE, options.mergingOptions().maximumGapSize());
+ }
+}
diff --git a/cli/src/test/java/de/jplag/cli/ReportViewerTest.java b/cli/src/test/java/de/jplag/cli/ReportViewerTest.java
new file mode 100644
index 000000000..cc5f592ce
--- /dev/null
+++ b/cli/src/test/java/de/jplag/cli/ReportViewerTest.java
@@ -0,0 +1,33 @@
+package de.jplag.cli;
+
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import java.awt.Desktop;
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+import javax.swing.JOptionPane;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+import de.jplag.cli.server.ReportViewer;
+
+@Timeout(value = 5, unit = TimeUnit.MINUTES)
+class ReportViewerTest {
+ @Test
+ @Disabled("Starts the internal server for manual testing. Does not terminal automatically.")
+ void testStartViewer() throws Exception {
+ assumeTrue(Desktop.isDesktopSupported());
+ ReportViewer viewer = new ReportViewer(null, 0);
+
+ int port = viewer.start();
+ Desktop.getDesktop().browse(URI.create("http://localhost:" + port));
+
+ // Open Dialog to keep the test running
+ JOptionPane.showMessageDialog(null, "Press OK to stop the server");
+ viewer.stop();
+ }
+
+}
\ No newline at end of file
diff --git a/cli/src/test/java/de/jplag/cli/server/RoutingFallbackTest.java b/cli/src/test/java/de/jplag/cli/server/RoutingFallbackTest.java
new file mode 100644
index 000000000..6dc08d057
--- /dev/null
+++ b/cli/src/test/java/de/jplag/cli/server/RoutingFallbackTest.java
@@ -0,0 +1,44 @@
+package de.jplag.cli.server;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.junit.jupiter.api.Test;
+
+class RoutingFallbackTest {
+ private final Routing nullRouting;
+ private final Routing contentRouting;
+
+ RoutingFallbackTest() throws IOException {
+ File testFile = File.createTempFile("content", ".any");
+ this.nullRouting = new RoutingStaticFile(null, ContentType.PLAIN);
+ this.contentRouting = new RoutingStaticFile(testFile, ContentType.PLAIN);
+ }
+
+ @Test
+ void testSecondNull() {
+ Routing routing = this.nullRouting.or(this.contentRouting);
+ assertNotNull(routing.fetchData(null, null, null));
+ }
+
+ @Test
+ void testFirstNull() {
+ Routing routing = this.contentRouting.or(this.nullRouting);
+ assertNotNull(routing.fetchData(null, null, null));
+ }
+
+ @Test
+ void testNeitherNull() {
+ Routing routing = this.contentRouting.or(this.contentRouting);
+ assertNotNull(routing.fetchData(null, null, null));
+ }
+
+ @Test
+ void testBothNull() {
+ Routing routing = this.nullRouting.or(this.nullRouting);
+ assertNull(routing.fetchData(null, null, null));
+ }
+}
\ No newline at end of file
diff --git a/cli/src/test/java/de/jplag/cli/server/RoutingPathTest.java b/cli/src/test/java/de/jplag/cli/server/RoutingPathTest.java
new file mode 100644
index 000000000..e4c2ebff0
--- /dev/null
+++ b/cli/src/test/java/de/jplag/cli/server/RoutingPathTest.java
@@ -0,0 +1,46 @@
+package de.jplag.cli.server;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class RoutingPathTest {
+ private static final String TEST_PATH = "some/path/to/index.html";
+ private static final String TEST_PATH_WITH_BEGINNING_SLASH = "/some/path/to/index.html";
+ private static final String TEST_PATH_WITH_ADDITIONAL_SLASHES = "///some/path////to/index.html";
+
+ private static final String[] TEST_PATH_PARTS = new String[] {"some", "path", "to", "index.html"};
+
+ @ParameterizedTest
+ @ValueSource(strings = {TEST_PATH_WITH_BEGINNING_SLASH, TEST_PATH, TEST_PATH_WITH_ADDITIONAL_SLASHES})
+ void testAsPath(String path) {
+ RoutingPath routingPath = new RoutingPath(path);
+ assertEquals(TEST_PATH, routingPath.asPath());
+ }
+
+ @Test
+ void testIterating() {
+ RoutingPath routingPath = new RoutingPath(TEST_PATH);
+ for (String expectedPart : TEST_PATH_PARTS) {
+ String currentPart = routingPath.head();
+ routingPath = routingPath.tail();
+ assertEquals(expectedPart, currentPart);
+ }
+
+ assertFalse(routingPath.hasTail());
+ assertTrue(routingPath.isEmpty());
+ }
+
+ @Test
+ void testErrorWithEmptyTail() {
+ assertThrowsExactly(IllegalStateException.class, () -> {
+ RoutingPath routingPath = new RoutingPath("");
+ routingPath.tail();
+ });
+ }
+}
\ No newline at end of file
diff --git a/cli/src/test/java/de/jplag/cli/server/RoutingResourcesTest.java b/cli/src/test/java/de/jplag/cli/server/RoutingResourcesTest.java
new file mode 100644
index 000000000..56df92140
--- /dev/null
+++ b/cli/src/test/java/de/jplag/cli/server/RoutingResourcesTest.java
@@ -0,0 +1,20 @@
+package de.jplag.cli.server;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.Test;
+
+class RoutingResourcesTest {
+ private static final RoutingResources routing = new RoutingResources("/");
+
+ @Test
+ void testExistingFile() {
+ assertNotNull(routing.fetchData(new RoutingPath("testResource.txt"), null, null));
+ }
+
+ @Test
+ void testNotExistingFile() {
+ assertNull(routing.fetchData(new RoutingPath("otherFile.txt"), null, null));
+ }
+}
\ No newline at end of file
diff --git a/cli/src/test/java/de/jplag/cli/server/RoutingStaticFileTest.java b/cli/src/test/java/de/jplag/cli/server/RoutingStaticFileTest.java
new file mode 100644
index 000000000..8e9549d00
--- /dev/null
+++ b/cli/src/test/java/de/jplag/cli/server/RoutingStaticFileTest.java
@@ -0,0 +1,43 @@
+package de.jplag.cli.server;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+class RoutingStaticFileTest {
+ private static final String TEST_FILE_CONTENT = "some test content.";
+ private static final ContentType TEST_CONTENT_TYPE = ContentType.PLAIN;
+ private static RoutingStaticFile routing;
+
+ @BeforeAll
+ static void setUp() throws IOException {
+ File testFile = File.createTempFile("testFile", ".txt");
+ try (FileWriter writer = new FileWriter(testFile)) {
+ writer.write(TEST_FILE_CONTENT);
+ }
+ routing = new RoutingStaticFile(testFile, TEST_CONTENT_TYPE);
+ }
+
+ @Test
+ void testRespondsWithFileContent() throws IOException {
+ ResponseData responseData = routing.fetchData(null, null, null);
+ assertEquals(TEST_CONTENT_TYPE, responseData.contentType());
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseData.stream()))) {
+ assertEquals(TEST_FILE_CONTENT, reader.readLine());
+ }
+ }
+
+ @Test
+ void testWithNullFile() throws IOException {
+ RoutingStaticFile nullRouting = new RoutingStaticFile(null, TEST_CONTENT_TYPE);
+ assertNull(nullRouting.fetchData(null, null, null));
+ }
+}
\ No newline at end of file
diff --git a/cli/src/test/java/de/jplag/cli/server/RoutingTreeTest.java b/cli/src/test/java/de/jplag/cli/server/RoutingTreeTest.java
new file mode 100644
index 000000000..318c00db2
--- /dev/null
+++ b/cli/src/test/java/de/jplag/cli/server/RoutingTreeTest.java
@@ -0,0 +1,80 @@
+package de.jplag.cli.server;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import com.sun.net.httpserver.HttpExchange;
+
+class RoutingTreeTest {
+ private static final String firstRoutingPath = "/content/image.png";
+ private static final String secondRoutingPath = "/index.html";
+ private RoutingTree routingTree;
+
+ @BeforeEach
+ void setUp() {
+ this.routingTree = new RoutingTree();
+ this.routingTree.insertRouting(firstRoutingPath, new TestRouting(firstRoutingPath));
+ this.routingTree.insertRouting(secondRoutingPath, new TestRouting(secondRoutingPath));
+ }
+
+ @Test
+ void testAccessRoutingTree() {
+ Pair firstRouting = this.routingTree.resolveRouting(new RoutingPath(firstRoutingPath));
+ Pair secondRouting = this.routingTree.resolveRouting(new RoutingPath(secondRoutingPath + "/suffix"));
+
+ assertTrue(firstRouting.getLeft().isEmpty());
+ assertFalse(secondRouting.getLeft().isEmpty());
+ assertEquals("suffix", secondRouting.getLeft().asPath());
+
+ assertInstanceOf(TestRouting.class, firstRouting.getRight());
+ assertInstanceOf(TestRouting.class, secondRouting.getRight());
+
+ assertEquals(firstRoutingPath, ((TestRouting) firstRouting.getRight()).path);
+ assertEquals(secondRoutingPath, ((TestRouting) secondRouting.getRight()).path);
+ }
+
+ @Test
+ void testUnknownPath() {
+ assertNull(this.routingTree.resolveRouting(new RoutingPath("/unknown.html")));
+ }
+
+ @Test
+ void testPartialPathRoute() {
+ RoutingTree routingTree = new RoutingTree();
+ routingTree.insertRouting("/path/", new TestRouting(""));
+ assertNotNull(routingTree.resolveRouting(new RoutingPath("/path/index.html")));
+ }
+
+ @Test
+ void testPartialPathRouteWithSubpath() {
+ RoutingTree routingTree = new RoutingTree();
+ routingTree.insertRouting("/path/", new TestRouting("/path/"));
+ routingTree.insertRouting("/path/subPath/a.html", new TestRouting(""));
+
+ Pair result = routingTree.resolveRouting(new RoutingPath("/path/subPath/b.html"));
+ assertNotNull(result);
+ assertInstanceOf(TestRouting.class, result.getRight());
+ assertEquals("/path/", ((TestRouting) result.getRight()).path);
+ }
+
+ private static class TestRouting implements Routing {
+ private final String path;
+
+ public TestRouting(String path) {
+ this.path = path;
+ }
+
+ @Override
+ public ResponseData fetchData(RoutingPath subPath, HttpExchange request, ReportViewer viewer) {
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/cli/src/test/resources/testResource.txt b/cli/src/test/resources/testResource.txt
new file mode 100644
index 000000000..c29d01b9b
--- /dev/null
+++ b/cli/src/test/resources/testResource.txt
@@ -0,0 +1 @@
+Test resource file for RoutingResourcesTest
\ No newline at end of file
diff --git a/core/src/main/java/de/jplag/JPlag.java b/core/src/main/java/de/jplag/JPlag.java
index 822dc959c..41f1c08c8 100644
--- a/core/src/main/java/de/jplag/JPlag.java
+++ b/core/src/main/java/de/jplag/JPlag.java
@@ -71,9 +71,13 @@ public static JPlagResult run(JPlagOptions options) throws ExitException {
// Parse and validate submissions.
SubmissionSetBuilder builder = new SubmissionSetBuilder(options);
SubmissionSet submissionSet = builder.buildSubmissionSet();
+ if (options.normalize() && options.language().supportsNormalization() && options.language().requiresCoreNormalization()) {
+ submissionSet.normalizeSubmissions();
+ }
int submissionCount = submissionSet.numberOfSubmissions();
- if (submissionCount < 2)
+ if (submissionCount < 2) {
throw new SubmissionException("Not enough valid submissions! (found " + submissionCount + " valid submissions)");
+ }
// Compare valid submissions.
JPlagResult result = comparisonStrategy.compareSubmissions(submissionSet);
@@ -83,8 +87,9 @@ public static JPlagResult run(JPlagOptions options) throws ExitException {
result = new MatchMerging(options).mergeMatchesOf(result);
}
- if (logger.isInfoEnabled())
+ if (logger.isInfoEnabled()) {
logger.info("Total time for comparing submissions: {}", TimeUtil.formatDuration(result.getDuration()));
+ }
result.setClusteringResult(ClusteringFactory.getClusterings(result.getAllComparisons(), options.clusteringOptions()));
logSkippedSubmissions(submissionSet, options);
@@ -103,6 +108,10 @@ private static void logSkippedSubmissions(SubmissionSet submissionSet, JPlagOpti
}
private static void checkForConfigurationConsistency(JPlagOptions options) throws RootDirectoryException {
+ if (options.normalize() && !options.language().supportsNormalization()) {
+ logger.error(String.format("The language %s cannot be used with normalization.", options.language().getName()));
+ }
+
List duplicateNames = getDuplicateSubmissionFolderNames(options);
if (duplicateNames.size() > 0) {
throw new RootDirectoryException(String.format("Duplicate root directory names found: %s", String.join(", ", duplicateNames)));
diff --git a/core/src/main/java/de/jplag/JPlagResult.java b/core/src/main/java/de/jplag/JPlagResult.java
index 2b1aabd32..581079b87 100644
--- a/core/src/main/java/de/jplag/JPlagResult.java
+++ b/core/src/main/java/de/jplag/JPlagResult.java
@@ -1,5 +1,6 @@
package de.jplag;
+import java.util.Comparator;
import java.util.List;
import java.util.function.ToDoubleFunction;
@@ -27,7 +28,7 @@ public class JPlagResult {
public JPlagResult(List comparisons, SubmissionSet submissions, long durationInMillis, JPlagOptions options) {
// sort by similarity (descending)
- this.comparisons = comparisons.stream().sorted((first, second) -> Double.compare(second.similarity(), first.similarity())).toList();
+ this.comparisons = comparisons.stream().sorted(Comparator.comparing(JPlagComparison::similarity).reversed()).toList();
this.submissions = submissions;
this.durationInMillis = durationInMillis;
this.options = options;
diff --git a/core/src/main/java/de/jplag/Match.java b/core/src/main/java/de/jplag/Match.java
index 0350dff37..1e94315a7 100644
--- a/core/src/main/java/de/jplag/Match.java
+++ b/core/src/main/java/de/jplag/Match.java
@@ -22,9 +22,8 @@ public boolean overlaps(Match other) {
if (startOfSecond < other.startOfSecond) {
return (other.startOfSecond - startOfSecond) < length;
- } else {
- return (startOfSecond - other.startOfSecond) < other.length;
}
+ return (startOfSecond - other.startOfSecond) < other.length;
}
/**
diff --git a/core/src/main/java/de/jplag/NumberOfArgumentValues.java b/core/src/main/java/de/jplag/NumberOfArgumentValues.java
deleted file mode 100644
index 8a4987c2a..000000000
--- a/core/src/main/java/de/jplag/NumberOfArgumentValues.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package de.jplag;
-
-/**
- * Allowed number of values of a command-line argument in the CLI.
- */
-public enum NumberOfArgumentValues {
- SINGLE_VALUE(""),
- ONE_OR_MORE_VALUES("+"),
- ZERO_OR_MORE_VALUES("*");
-
- private final String representation;
-
- NumberOfArgumentValues(String representation) {
- this.representation = representation;
- }
-
- @Override
- public String toString() {
- return representation;
- }
-}
\ No newline at end of file
diff --git a/core/src/main/java/de/jplag/Submission.java b/core/src/main/java/de/jplag/Submission.java
index 0fec4e0b3..0fe8db33f 100644
--- a/core/src/main/java/de/jplag/Submission.java
+++ b/core/src/main/java/de/jplag/Submission.java
@@ -7,8 +7,10 @@
import java.util.ArrayList;
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.Objects;
import java.util.Set;
@@ -61,6 +63,8 @@ public class Submission implements Comparable {
private final Language language;
+ private Map fileTokenCount;
+
/**
* Creates a submission.
* @param name Identification of the submission (directory or filename).
@@ -235,9 +239,11 @@ private static File createErrorDirectory(String... subdirectoryNames) {
/**
* Parse files of the submission.
+ * @param debugParser specifies if the submission should be copied upon parsing errors.
+ * @param normalize specifies if the tokens sequences should be normalized.
* @return Whether parsing was successful.
*/
- /* package-private */ boolean parse(boolean debugParser) {
+ /* package-private */ boolean parse(boolean debugParser, boolean normalize) {
if (files == null || files.isEmpty()) {
logger.error("ERROR: nothing to parse for submission \"{}\"", name);
tokenList = null;
@@ -246,14 +252,15 @@ private static File createErrorDirectory(String... subdirectoryNames) {
}
try {
- tokenList = language.parse(new HashSet<>(files));
+ tokenList = language.parse(new HashSet<>(files), normalize);
if (logger.isDebugEnabled()) {
for (Token token : tokenList) {
logger.debug(String.join(" | ", token.getType().toString(), Integer.toString(token.getLine()), token.getSemantics().toString()));
}
}
} catch (ParsingException e) {
- logger.warn("Failed to parse submission {} with error {}", this, e.getMessage(), e);
+ String shortenedMessage = e.getMessage().replace(submissionRootFile.toString(), name);
+ logger.warn("Failed to parse submission {}:{}{}", name, System.lineSeparator(), shortenedMessage);
tokenList = null;
hasErrors = true;
if (debugParser) {
@@ -272,7 +279,7 @@ private static File createErrorDirectory(String... subdirectoryNames) {
}
/**
- * Perform token string normalization, which makes the token string invariant to dead code insertion and independent
+ * Perform token sequence normalization, which makes the token sequence invariant to dead code insertion and independent
* statement reordering.
*/
void normalize() {
@@ -309,4 +316,24 @@ public Submission copy() {
copy.setBaseCodeComparison(baseCodeComparison);
return copy;
}
+
+ /**
+ * @return A mapping of each file in the submission to the number of tokens in the file
+ */
+ public Map getTokenCountPerFile() {
+ if (this.tokenList == null) {
+ return Collections.emptyMap();
+ }
+
+ if (fileTokenCount == null) {
+ fileTokenCount = new HashMap<>();
+ for (File file : this.files) {
+ fileTokenCount.put(file, 0);
+ }
+ for (Token token : this.tokenList) {
+ fileTokenCount.put(token.getFile(), fileTokenCount.get(token.getFile()) + 1);
+ }
+ }
+ return fileTokenCount;
+ }
}
diff --git a/core/src/main/java/de/jplag/SubmissionFileData.java b/core/src/main/java/de/jplag/SubmissionFileData.java
new file mode 100644
index 000000000..91eb3edda
--- /dev/null
+++ b/core/src/main/java/de/jplag/SubmissionFileData.java
@@ -0,0 +1,13 @@
+package de.jplag;
+
+import java.io.File;
+
+/**
+ * Contains the information about a single file in a submission. For single file submissions the submission file is the
+ * same as the root.
+ * @param submissionFile The file, that is part of a submission
+ * @param root The root of the submission
+ * @param isNew Indicates weather this follows the new or the old syntax
+ */
+public record SubmissionFileData(File submissionFile, File root, boolean isNew) {
+}
diff --git a/core/src/main/java/de/jplag/SubmissionSet.java b/core/src/main/java/de/jplag/SubmissionSet.java
index 166a151fb..907687974 100644
--- a/core/src/main/java/de/jplag/SubmissionSet.java
+++ b/core/src/main/java/de/jplag/SubmissionSet.java
@@ -10,6 +10,9 @@
import de.jplag.exceptions.BasecodeException;
import de.jplag.exceptions.ExitException;
import de.jplag.exceptions.SubmissionException;
+import de.jplag.logging.ProgressBar;
+import de.jplag.logging.ProgressBarLogger;
+import de.jplag.logging.ProgressBarType;
import de.jplag.options.JPlagOptions;
/**
@@ -37,6 +40,7 @@ public class SubmissionSet {
/**
* @param submissions Submissions to check for plagiarism.
* @param baseCode Base code submission if it exists or {@code null}.
+ * @param options The JPlag options
*/
public SubmissionSet(List submissions, Submission baseCode, JPlagOptions options) throws ExitException {
this.allSubmissions = submissions;
@@ -119,9 +123,10 @@ private void parseAllSubmissions() throws ExitException {
private void parseBaseCodeSubmission(Submission baseCode) throws BasecodeException {
long startTime = System.currentTimeMillis();
logger.trace("----- Parsing basecode submission: " + baseCode.getName());
- if (!baseCode.parse(options.debugParser())) {
+ if (!baseCode.parse(options.debugParser(), options.normalize())) {
throw new BasecodeException("Could not successfully parse basecode submission!");
- } else if (baseCode.getNumberOfTokens() < options.minimumTokenMatch()) {
+ }
+ if (baseCode.getNumberOfTokens() < options.minimumTokenMatch()) {
throw new BasecodeException(String.format("Basecode submission contains %d token(s), which is less than the minimum match length (%d)!",
baseCode.getNumberOfTokens(), options.minimumTokenMatch()));
}
@@ -133,6 +138,7 @@ private void parseBaseCodeSubmission(Submission baseCode) throws BasecodeExcepti
/**
* Parse all given submissions.
+ * @param submissions The list of submissions
*/
private void parseSubmissions(List submissions) {
if (submissions.isEmpty()) {
@@ -143,14 +149,14 @@ private void parseSubmissions(List submissions) {
long startTime = System.currentTimeMillis();
int tooShort = 0;
+ ProgressBar progressBar = ProgressBarLogger.createProgressBar(ProgressBarType.PARSING, submissions.size());
for (Submission submission : submissions) {
- logger.info("Parsing submission {}", submission.getName());
boolean ok;
logger.trace("------ Parsing submission: " + submission.getName());
currentSubmissionName = submission.getName();
- if (!(ok = submission.parse(options.debugParser()))) {
+ if (!(ok = submission.parse(options.debugParser(), options.normalize()))) {
errors++;
}
@@ -168,7 +174,9 @@ private void parseSubmissions(List submissions) {
} else {
logger.error("ERROR -> Submission {} removed", currentSubmissionName);
}
+ progressBar.step();
}
+ progressBar.dispose();
int validSubmissions = submissions.size() - errors - tooShort;
logger.trace(validSubmissions + " submissions parsed successfully!");
diff --git a/core/src/main/java/de/jplag/SubmissionSetBuilder.java b/core/src/main/java/de/jplag/SubmissionSetBuilder.java
index 4d93c0d44..e0cf584e4 100644
--- a/core/src/main/java/de/jplag/SubmissionSetBuilder.java
+++ b/core/src/main/java/de/jplag/SubmissionSetBuilder.java
@@ -6,6 +6,7 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
+import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -21,6 +22,9 @@
import de.jplag.exceptions.ExitException;
import de.jplag.exceptions.RootDirectoryException;
import de.jplag.exceptions.SubmissionException;
+import de.jplag.logging.ProgressBar;
+import de.jplag.logging.ProgressBarLogger;
+import de.jplag.logging.ProgressBarType;
import de.jplag.options.JPlagOptions;
/**
@@ -35,9 +39,9 @@ public class SubmissionSetBuilder {
/**
* Creates a builder for submission sets.
- * @deprecated in favor of {@link #SubmissionSetBuilder(JPlagOptions)}.
* @param language is the language of the submissions.
* @param options are the configured options.
+ * @deprecated in favor of {@link #SubmissionSetBuilder(JPlagOptions)}.
*/
@Deprecated(since = "4.3.0")
public SubmissionSetBuilder(Language language, JPlagOptions options) {
@@ -67,14 +71,21 @@ public SubmissionSet buildSubmissionSet() throws ExitException {
int numberOfRootDirectories = submissionDirectories.size() + oldSubmissionDirectories.size();
boolean multipleRoots = (numberOfRootDirectories > 1);
- // Collect valid looking entries from the root directories.
- Map foundSubmissions = new HashMap<>();
- for (File directory : submissionDirectories) {
- processRootDirectoryEntries(directory, multipleRoots, foundSubmissions, true);
+ List submissionFiles = new ArrayList<>();
+ for (File submissionDirectory : submissionDirectories) {
+ submissionFiles.addAll(listSubmissionFiles(submissionDirectory, true));
}
- for (File oldDirectory : oldSubmissionDirectories) {
- processRootDirectoryEntries(oldDirectory, multipleRoots, foundSubmissions, false);
+ for (File submissionDirectory : oldSubmissionDirectories) {
+ submissionFiles.addAll(listSubmissionFiles(submissionDirectory, false));
+ }
+
+ ProgressBar progressBar = ProgressBarLogger.createProgressBar(ProgressBarType.LOADING, submissionFiles.size());
+ Map foundSubmissions = new HashMap<>();
+ for (SubmissionFileData submissionFile : submissionFiles) {
+ processSubmissionFile(submissionFile, multipleRoots, foundSubmissions);
+ progressBar.step();
}
+ progressBar.dispose();
Optional baseCodeSubmission = loadBaseCode();
baseCodeSubmission.ifPresent(baseSubmission -> foundSubmissions.remove(baseSubmission.getRoot()));
@@ -84,7 +95,7 @@ public SubmissionSet buildSubmissionSet() throws ExitException {
// Some languages expect a certain order, which is ensured here:
if (options.language().expectsSubmissionOrder()) {
- List rootFiles = foundSubmissions.values().stream().map(it -> it.getRoot()).toList();
+ List rootFiles = foundSubmissions.values().stream().map(Submission::getRoot).toList();
rootFiles = options.language().customizeSubmissionOrder(rootFiles);
submissions = new ArrayList<>(rootFiles.stream().map(foundSubmissions::get).toList());
}
@@ -155,31 +166,25 @@ private Optional loadBaseCode() throws ExitException {
Submission baseCodeSubmission = processSubmission(baseCodeSubmissionDirectory.getName(), baseCodeSubmissionDirectory, false);
logger.info("Basecode directory \"{}\" will be used.", baseCodeSubmission.getName());
- return Optional.ofNullable(baseCodeSubmission);
+ return Optional.of(baseCodeSubmission);
}
- /**
- * Read entries in the given root directory.
- */
- private String[] listSubmissionFiles(File rootDirectory) throws ExitException {
+ private List listSubmissionFiles(File rootDirectory, boolean isNew) throws RootDirectoryException {
if (!rootDirectory.isDirectory()) {
throw new AssertionError("Given root is not a directory.");
}
- String[] fileNames;
-
try {
- fileNames = rootDirectory.list();
+ File[] files = rootDirectory.listFiles();
+ if (files == null) {
+ throw new RootDirectoryException("Cannot list files of the root directory!");
+ }
+
+ return Arrays.stream(files).sorted(Comparator.comparing(File::getName)).map(it -> new SubmissionFileData(it, rootDirectory, isNew))
+ .toList();
} catch (SecurityException exception) {
throw new RootDirectoryException("Cannot list files of the root directory! " + exception.getMessage(), exception);
}
-
- if (fileNames == null) {
- throw new RootDirectoryException("Cannot list files of the root directory!");
- }
-
- Arrays.sort(fileNames);
- return fileNames;
}
/**
@@ -200,6 +205,7 @@ private String isExcludedEntry(File submissionEntry) {
/**
* Process the given directory entry as a submission, the path MUST not be excluded.
+ * @param submissionName The name of the submission
* @param submissionFile the file for the submission.
* @param isNew states whether submissions found in the root directory must be checked for plagiarism.
* @return The entry converted to a submission.
@@ -225,27 +231,16 @@ private Submission processSubmission(String submissionName, File submissionFile,
return new Submission(submissionName, submissionFile, isNew, parseFilesRecursively(submissionFile), options.language());
}
- /**
- * Process entries in the root directory to check whether they qualify as submissions.
- * @param rootDirectory is the root directory being examined.
- * @param foundSubmissions Submissions found so far, is updated in-place.
- * @param isNew states whether submissions found in the root directory must be checked for plagiarism.
- */
- private void processRootDirectoryEntries(File rootDirectory, boolean multipleRoots, Map foundSubmissions, boolean isNew)
- throws ExitException {
- for (String fileName : listSubmissionFiles(rootDirectory)) {
- File submissionFile = new File(rootDirectory, fileName);
-
- String errorMessage = isExcludedEntry(submissionFile);
- if (errorMessage == null) {
- String rootDirectoryPrefix = multipleRoots ? (rootDirectory.getName() + File.separator) : "";
- String submissionName = rootDirectoryPrefix + fileName;
- Submission submission = processSubmission(submissionName, submissionFile, isNew);
- foundSubmissions.put(submission.getRoot(), submission);
- } else {
- logger.error(errorMessage);
- }
+ private void processSubmissionFile(SubmissionFileData file, boolean multipleRoots, Map foundSubmissions) throws ExitException {
+ String errorMessage = isExcludedEntry(file.submissionFile());
+ if (errorMessage != null) {
+ logger.error(errorMessage);
}
+
+ String rootDirectoryPrefix = multipleRoots ? (file.root().getName() + File.separator) : "";
+ String submissionName = rootDirectoryPrefix + file.submissionFile().getName();
+ Submission submission = processSubmission(submissionName, file.submissionFile(), file.isNew());
+ foundSubmissions.put(submission.getRoot(), submission);
}
/**
@@ -311,4 +306,5 @@ private File makeCanonical(File file, Function excepti
throw exceptionWrapper.apply(exception);
}
}
+
}
diff --git a/core/src/main/java/de/jplag/SubsequenceHashLookupTable.java b/core/src/main/java/de/jplag/SubsequenceHashLookupTable.java
index aa44c783b..e19a41643 100644
--- a/core/src/main/java/de/jplag/SubsequenceHashLookupTable.java
+++ b/core/src/main/java/de/jplag/SubsequenceHashLookupTable.java
@@ -46,16 +46,6 @@ class SubsequenceHashLookupTable {
computeSubsequenceHashes(marked);
}
- /** Returns the size of the subsequences used for hashing */
- int getWindowSize() {
- return windowSize;
- }
-
- /** Returns the list of values for which the hashes were computed */
- int[] getValues() {
- return values;
- }
-
/**
* Returns the hash over the subsequence from startIndex to startIndex+windowSize.
* @param startIndex the start index.
diff --git a/core/src/main/java/de/jplag/logging/ProgressBar.java b/core/src/main/java/de/jplag/logging/ProgressBar.java
new file mode 100644
index 000000000..04450434a
--- /dev/null
+++ b/core/src/main/java/de/jplag/logging/ProgressBar.java
@@ -0,0 +1,24 @@
+package de.jplag.logging;
+
+/**
+ * Exposed interactions for a running progress bar.
+ */
+public interface ProgressBar {
+ /**
+ * Advances the progress bar by a single step
+ */
+ default void step() {
+ step(1);
+ }
+
+ /**
+ * Advances the progress bar by amount steps
+ * @param number The number of steps
+ */
+ void step(int number);
+
+ /**
+ * Closes the progress bar. After this method has been called the behaviour of the other methods is undefined.
+ */
+ void dispose();
+}
diff --git a/core/src/main/java/de/jplag/logging/ProgressBarLogger.java b/core/src/main/java/de/jplag/logging/ProgressBarLogger.java
new file mode 100644
index 000000000..889d391bb
--- /dev/null
+++ b/core/src/main/java/de/jplag/logging/ProgressBarLogger.java
@@ -0,0 +1,68 @@
+package de.jplag.logging;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Provides static access to the creation of progress bars.
+ */
+public class ProgressBarLogger {
+ private static ProgressBarProvider progressBarProvider = new DummyProvider();
+
+ private ProgressBarLogger() {
+ // Hides default constructor
+ }
+
+ /**
+ * Creates a new {@link ProgressBar}
+ * @param type The type of the progress bar
+ * @param totalSteps The total number of steps
+ * @return The newly created progress bar
+ */
+ public static ProgressBar createProgressBar(ProgressBarType type, int totalSteps) {
+ return progressBarProvider.initProgressBar(type, totalSteps);
+ }
+
+ /**
+ * Sets the {@link ProgressBarProvider}. Should be used by the ui before calling JPlag, if progress bars should be
+ * shown.
+ * @param progressBarProvider The provider
+ */
+ public static void setProgressBarProvider(ProgressBarProvider progressBarProvider) {
+ ProgressBarLogger.progressBarProvider = progressBarProvider;
+ }
+
+ private static class DummyProvider implements ProgressBarProvider {
+ @Override
+ public ProgressBar initProgressBar(ProgressBarType type, int totalSteps) {
+ return new DummyBar(type, totalSteps);
+ }
+ }
+
+ private static class DummyBar implements ProgressBar {
+ private static final Logger logger = LoggerFactory.getLogger(DummyBar.class);
+ private int currentStep;
+
+ public DummyBar(ProgressBarType type, int totalSteps) {
+ this.currentStep = 0;
+ logger.info("{} ({})", type.getDefaultText(), totalSteps);
+ }
+
+ @Override
+ public void step() {
+ logger.info("Now at step {}", this.currentStep++);
+ }
+
+ @Override
+ public void step(int number) {
+ for (int i = 0; i < number; i++) {
+ step();
+ }
+ }
+
+ @Override
+ public void dispose() {
+ logger.info("Progress bar done.");
+ }
+ }
+}
diff --git a/core/src/main/java/de/jplag/logging/ProgressBarProvider.java b/core/src/main/java/de/jplag/logging/ProgressBarProvider.java
new file mode 100644
index 000000000..13268325b
--- /dev/null
+++ b/core/src/main/java/de/jplag/logging/ProgressBarProvider.java
@@ -0,0 +1,14 @@
+package de.jplag.logging;
+
+/**
+ * Provides the capability to create new progress bars, to allow JPlag to access the ui.
+ */
+public interface ProgressBarProvider {
+ /**
+ * Creates a new progress bar
+ * @param type The type of progress bar. Should mostly determine the name
+ * @param totalSteps The total number of steps the progress bar should have
+ * @return The newly created bar
+ */
+ ProgressBar initProgressBar(ProgressBarType type, int totalSteps);
+}
diff --git a/core/src/main/java/de/jplag/logging/ProgressBarType.java b/core/src/main/java/de/jplag/logging/ProgressBarType.java
new file mode 100644
index 000000000..88e520fcc
--- /dev/null
+++ b/core/src/main/java/de/jplag/logging/ProgressBarType.java
@@ -0,0 +1,23 @@
+package de.jplag.logging;
+
+/**
+ * The available processes. Used as a hint for the ui, which step JPlag is currently performing.
+ */
+public enum ProgressBarType {
+ LOADING("Loading Submissions "),
+ PARSING("Parsing Submissions "),
+ COMPARING("Comparing Submissions");
+
+ private final String defaultText;
+
+ ProgressBarType(String defaultText) {
+ this.defaultText = defaultText;
+ }
+
+ /**
+ * @return The default display text for the type
+ */
+ public String getDefaultText() {
+ return defaultText;
+ }
+}
diff --git a/core/src/main/java/de/jplag/merging/MatchMerging.java b/core/src/main/java/de/jplag/merging/MatchMerging.java
index cdcb426dd..3067e32a6 100644
--- a/core/src/main/java/de/jplag/merging/MatchMerging.java
+++ b/core/src/main/java/de/jplag/merging/MatchMerging.java
@@ -1,10 +1,8 @@
package de.jplag.merging;
import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
+import java.util.Comparator;
import java.util.List;
-import java.util.Map;
import de.jplag.JPlagComparison;
import de.jplag.JPlagResult;
@@ -24,7 +22,7 @@
* {@link JPlagOptions} as {@link MergingOptions} and default to (2,6).
*/
public class MatchMerging {
- private JPlagOptions options;
+ private final JPlagOptions options;
/**
* Instantiates the match merging algorithm for a comparison result and a set of specific options.
@@ -68,25 +66,15 @@ public JPlagResult mergeMatchesOf(JPlagResult result) {
*/
private List computeNeighbors(List globalMatches) {
List neighbors = new ArrayList<>();
+ List sortedByLeft = new ArrayList<>(globalMatches);
+ List sortedByRight = new ArrayList<>(globalMatches);
- Map> matchesByLeft = new HashMap<>();
- Map> matchesByRight = new HashMap<>();
+ sortedByLeft.sort(Comparator.comparingInt(Match::startOfFirst));
+ sortedByRight.sort(Comparator.comparingInt(Match::startOfSecond));
- // Group matches by their left and right positions
- for (Match match : globalMatches) {
- matchesByLeft.computeIfAbsent(match.startOfFirst(), key -> new ArrayList<>()).add(match);
- matchesByRight.computeIfAbsent(match.startOfSecond(), key -> new ArrayList<>()).add(match);
- }
-
- // Iterate through the matches and find neighbors
- for (List matches : matchesByLeft.values()) {
- for (Match match : matches) {
- List rightMatches = matchesByRight.getOrDefault(match.startOfSecond(), Collections.emptyList());
- for (Match rightMatch : rightMatches) {
- if (rightMatch != match) {
- neighbors.add(new Neighbor(match, rightMatch));
- }
- }
+ for (int i = 0; i < sortedByLeft.size() - 1; i++) {
+ if (sortedByRight.indexOf(sortedByLeft.get(i)) == (sortedByRight.indexOf(sortedByLeft.get(i + 1)) - 1)) {
+ neighbors.add(new Neighbor(sortedByLeft.get(i), sortedByLeft.get(i + 1)));
}
}
diff --git a/core/src/main/java/de/jplag/merging/MergingOptions.java b/core/src/main/java/de/jplag/merging/MergingOptions.java
index 4c2b49e08..7d77f7f31 100644
--- a/core/src/main/java/de/jplag/merging/MergingOptions.java
+++ b/core/src/main/java/de/jplag/merging/MergingOptions.java
@@ -10,12 +10,16 @@
public record MergingOptions(@JsonProperty("enabled") boolean enabled, @JsonProperty("min_neighbour_length") int minimumNeighborLength,
@JsonProperty("max_gap_size") int maximumGapSize) {
+ public static final boolean DEFAULT_ENABLED = false;
+ public static final int DEFAULT_NEIGHBOR_LENGTH = 2;
+ public static final int DEFAULT_GAP_SIZE = 6;
+
/**
* The default values of MergingOptions are false for the enable-switch, which deactivate MatchMerging, while
* minimumNeighborLength and maximumGapSize default to (2,6), which in testing yielded the best results.
*/
public MergingOptions() {
- this(false, 2, 6);
+ this(DEFAULT_ENABLED, DEFAULT_NEIGHBOR_LENGTH, DEFAULT_GAP_SIZE);
}
/**
diff --git a/core/src/main/java/de/jplag/normalization/MultipleEdge.java b/core/src/main/java/de/jplag/normalization/MultipleEdge.java
index 6265c5371..b10fda2ea 100644
--- a/core/src/main/java/de/jplag/normalization/MultipleEdge.java
+++ b/core/src/main/java/de/jplag/normalization/MultipleEdge.java
@@ -9,7 +9,7 @@
* Models a multiple edge in the normalization graph. Contains multiple edges.
*/
class MultipleEdge {
- private Set edges;
+ private final Set edges;
private boolean isVariableFlow;
private boolean isVariableReverseFlow;
@@ -27,10 +27,12 @@ boolean isVariableReverseFlow() {
}
void addEdge(EdgeType type, Variable cause) {
- if (type == EdgeType.VARIABLE_FLOW)
+ if (type == EdgeType.VARIABLE_FLOW) {
isVariableFlow = true;
- if (type == EdgeType.VARIABLE_REVERSE_FLOW)
+ }
+ if (type == EdgeType.VARIABLE_REVERSE_FLOW) {
isVariableReverseFlow = true;
+ }
edges.add(new Edge(type, cause));
}
}
diff --git a/core/src/main/java/de/jplag/normalization/NormalizationGraphConstructor.java b/core/src/main/java/de/jplag/normalization/NormalizationGraphConstructor.java
index a5f4e496e..ace0d1ba3 100644
--- a/core/src/main/java/de/jplag/normalization/NormalizationGraphConstructor.java
+++ b/core/src/main/java/de/jplag/normalization/NormalizationGraphConstructor.java
@@ -17,14 +17,14 @@
* Constructs the normalization graph.
*/
class NormalizationGraphConstructor {
- private SimpleDirectedGraph graph;
+ private final SimpleDirectedGraph graph;
private int bidirectionalBlockDepth;
- private Collection fullPositionSignificanceIncoming;
+ private final Collection fullPositionSignificanceIncoming;
private Statement lastFullPositionSignificance;
private Statement lastPartialPositionSignificance;
- private Map> variableReads;
- private Map> variableWrites;
- private Set inCurrentBidirectionalBlock;
+ private final Map> variableReads;
+ private final Map> variableWrites;
+ private final Set inCurrentBidirectionalBlock;
private Statement current;
NormalizationGraphConstructor(List tokens) {
@@ -57,24 +57,28 @@ private void addStatement(Statement statement) {
processPartialPositionSignificance();
processReads();
processWrites();
- for (Variable variable : current.semantics().reads())
+ for (Variable variable : current.semantics().reads()) {
addVariableToMap(variableReads, variable);
- for (Variable variable : current.semantics().writes())
+ }
+ for (Variable variable : current.semantics().writes()) {
addVariableToMap(variableWrites, variable);
+ }
}
private void processBidirectionalBlock() {
bidirectionalBlockDepth += current.semantics().bidirectionalBlockDepthChange();
- if (bidirectionalBlockDepth > 0)
+ if (bidirectionalBlockDepth > 0) {
inCurrentBidirectionalBlock.add(current);
- else
+ } else {
inCurrentBidirectionalBlock.clear();
+ }
}
private void processFullPositionSignificance() {
if (current.semantics().hasFullPositionSignificance()) {
- for (Statement node : fullPositionSignificanceIncoming)
+ for (Statement node : fullPositionSignificanceIncoming) {
addIncomingEdgeToCurrent(node, EdgeType.POSITION_SIGNIFICANCE_FULL, null);
+ }
fullPositionSignificanceIncoming.clear();
lastFullPositionSignificance = current;
} else if (lastFullPositionSignificance != null) {
@@ -94,15 +98,17 @@ private void processPartialPositionSignificance() {
private void processReads() {
for (Variable variable : current.semantics().reads()) {
- for (Statement node : variableWrites.getOrDefault(variable, Set.of()))
+ for (Statement node : variableWrites.getOrDefault(variable, Set.of())) {
addIncomingEdgeToCurrent(node, EdgeType.VARIABLE_FLOW, variable);
+ }
}
}
private void processWrites() {
for (Variable variable : current.semantics().writes()) {
- for (Statement node : variableWrites.getOrDefault(variable, Set.of()))
+ for (Statement node : variableWrites.getOrDefault(variable, Set.of())) {
addIncomingEdgeToCurrent(node, EdgeType.VARIABLE_ORDER, variable);
+ }
for (Statement node : variableReads.getOrDefault(variable, Set.of())) {
EdgeType edgeType = inCurrentBidirectionalBlock.contains(node) ? //
EdgeType.VARIABLE_REVERSE_FLOW : EdgeType.VARIABLE_ORDER;
diff --git a/core/src/main/java/de/jplag/normalization/Statement.java b/core/src/main/java/de/jplag/normalization/Statement.java
index c7086bb74..a749a5774 100644
--- a/core/src/main/java/de/jplag/normalization/Statement.java
+++ b/core/src/main/java/de/jplag/normalization/Statement.java
@@ -41,24 +41,28 @@ private int tokenOrdinal(Token token) {
@Override
public int compareTo(Statement other) {
int sizeComp = Integer.compare(this.tokens.size(), other.tokens.size());
- if (sizeComp != 0)
+ if (sizeComp != 0) {
return -sizeComp; // bigger size should come first
+ }
Iterator myTokens = this.tokens.iterator();
Iterator otherTokens = other.tokens.iterator();
for (int i = 0; i < this.tokens.size(); i++) {
int tokenComp = Integer.compare(tokenOrdinal(myTokens.next()), tokenOrdinal(otherTokens.next()));
- if (tokenComp != 0)
+ if (tokenComp != 0) {
return tokenComp;
+ }
}
return 0;
}
@Override
public boolean equals(Object obj) {
- if (this == obj)
+ if (this == obj) {
return true;
- if (obj == null || getClass() != obj.getClass())
+ }
+ if (obj == null || getClass() != obj.getClass()) {
return false;
+ }
return tokens.equals(((Statement) obj).tokens);
}
diff --git a/core/src/main/java/de/jplag/normalization/StatementBuilder.java b/core/src/main/java/de/jplag/normalization/StatementBuilder.java
index 1afaa3eb3..eef5d0c82 100644
--- a/core/src/main/java/de/jplag/normalization/StatementBuilder.java
+++ b/core/src/main/java/de/jplag/normalization/StatementBuilder.java
@@ -10,7 +10,7 @@
*/
class StatementBuilder {
- private List tokens;
+ private final List tokens;
private final int lineNumber;
StatementBuilder(int lineNumber) {
diff --git a/core/src/main/java/de/jplag/normalization/TokenStringNormalizer.java b/core/src/main/java/de/jplag/normalization/TokenStringNormalizer.java
index c58a9188e..5ece0ff1f 100644
--- a/core/src/main/java/de/jplag/normalization/TokenStringNormalizer.java
+++ b/core/src/main/java/de/jplag/normalization/TokenStringNormalizer.java
@@ -13,7 +13,7 @@
import de.jplag.Token;
/**
- * Performs token string normalization.
+ * Performs token sequence normalization.
*/
public class TokenStringNormalizer {
@@ -21,11 +21,11 @@ private TokenStringNormalizer() {
}
/**
- * Performs token string normalization. Tokens representing dead code have been eliminated and tokens representing
+ * Performs token sequence normalization. Tokens representing dead code have been eliminated and tokens representing
* subsequent independent statements have been put in a fixed order. Works by first constructing a Normalization Graph
- * and then turning it back into a token string.
- * @param tokens The original token string, remains unaltered.
- * @return The normalized token string.
+ * and then turning it back into a token sequence.
+ * @param tokens The original token sequence, remains unaltered.
+ * @return The normalized token sequence.
*/
public static List normalize(List tokens) {
SimpleDirectedGraph normalizationGraph = new NormalizationGraphConstructor(tokens).get();
diff --git a/core/src/main/java/de/jplag/options/JPlagOptions.java b/core/src/main/java/de/jplag/options/JPlagOptions.java
index f2e876f43..6d7067748 100644
--- a/core/src/main/java/de/jplag/options/JPlagOptions.java
+++ b/core/src/main/java/de/jplag/options/JPlagOptions.java
@@ -55,7 +55,8 @@ public record JPlagOptions(@JsonSerialize(using = LanguageSerializer.class) Lang
@JsonProperty("subdirectory_name") String subdirectoryName, @JsonProperty("file_suffixes") List fileSuffixes,
@JsonProperty("exclusion_file_name") String exclusionFileName, @JsonProperty("similarity_metric") SimilarityMetric similarityMetric,
@JsonProperty("similarity_threshold") double similarityThreshold, @JsonProperty("max_comparisons") int maximumNumberOfComparisons,
- @JsonProperty("cluster") ClusteringOptions clusteringOptions, boolean debugParser, @JsonProperty("merging") MergingOptions mergingOptions) {
+ @JsonProperty("cluster") ClusteringOptions clusteringOptions, boolean debugParser, @JsonProperty("merging") MergingOptions mergingOptions,
+ @JsonProperty("normalize") boolean normalize) {
public static final double DEFAULT_SIMILARITY_THRESHOLD = 0;
public static final int DEFAULT_SHOWN_COMPARISONS = 500;
@@ -68,13 +69,13 @@ public record JPlagOptions(@JsonSerialize(using = LanguageSerializer.class) Lang
public JPlagOptions(Language language, Set submissionDirectories, Set oldSubmissionDirectories) {
this(language, null, submissionDirectories, oldSubmissionDirectories, null, null, null, null, DEFAULT_SIMILARITY_METRIC,
- DEFAULT_SIMILARITY_THRESHOLD, DEFAULT_SHOWN_COMPARISONS, new ClusteringOptions(), false, new MergingOptions());
+ DEFAULT_SIMILARITY_THRESHOLD, DEFAULT_SHOWN_COMPARISONS, new ClusteringOptions(), false, new MergingOptions(), false);
}
public JPlagOptions(Language language, Integer minimumTokenMatch, Set submissionDirectories, Set oldSubmissionDirectories,
File baseCodeSubmissionDirectory, String subdirectoryName, List fileSuffixes, String exclusionFileName,
SimilarityMetric similarityMetric, double similarityThreshold, int maximumNumberOfComparisons, ClusteringOptions clusteringOptions,
- boolean debugParser, MergingOptions mergingOptions) {
+ boolean debugParser, MergingOptions mergingOptions, boolean normalize) {
this.language = language;
this.debugParser = debugParser;
this.fileSuffixes = fileSuffixes == null || fileSuffixes.isEmpty() ? null : Collections.unmodifiableList(fileSuffixes);
@@ -89,90 +90,97 @@ public JPlagOptions(Language language, Integer minimumTokenMatch, Set subm
this.subdirectoryName = subdirectoryName;
this.clusteringOptions = clusteringOptions;
this.mergingOptions = mergingOptions;
+ this.normalize = normalize;
}
public JPlagOptions withLanguageOption(Language language) {
return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionDirectory,
subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
- clusteringOptions, debugParser, mergingOptions);
+ clusteringOptions, debugParser, mergingOptions, normalize);
}
public JPlagOptions withDebugParser(boolean debugParser) {
return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionDirectory,
subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
- clusteringOptions, debugParser, mergingOptions);
+ clusteringOptions, debugParser, mergingOptions, normalize);
}
public JPlagOptions withFileSuffixes(List fileSuffixes) {
return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionDirectory,
subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
- clusteringOptions, debugParser, mergingOptions);
+ clusteringOptions, debugParser, mergingOptions, normalize);
}
public JPlagOptions withSimilarityThreshold(double similarityThreshold) {
return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionDirectory,
subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
- clusteringOptions, debugParser, mergingOptions);
+ clusteringOptions, debugParser, mergingOptions, normalize);
}
public JPlagOptions withMaximumNumberOfComparisons(int maximumNumberOfComparisons) {
return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionDirectory,
subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
- clusteringOptions, debugParser, mergingOptions);
+ clusteringOptions, debugParser, mergingOptions, normalize);
}
public JPlagOptions withSimilarityMetric(SimilarityMetric similarityMetric) {
return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionDirectory,
subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
- clusteringOptions, debugParser, mergingOptions);
+ clusteringOptions, debugParser, mergingOptions, normalize);
}
public JPlagOptions withMinimumTokenMatch(Integer minimumTokenMatch) {
return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionDirectory,
subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
- clusteringOptions, debugParser, mergingOptions);
+ clusteringOptions, debugParser, mergingOptions, normalize);
}
public JPlagOptions withExclusionFileName(String exclusionFileName) {
return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionDirectory,
subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
- clusteringOptions, debugParser, mergingOptions);
+ clusteringOptions, debugParser, mergingOptions, normalize);
}
public JPlagOptions withSubmissionDirectories(Set submissionDirectories) {
return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionDirectory,
subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
- clusteringOptions, debugParser, mergingOptions);
+ clusteringOptions, debugParser, mergingOptions, normalize);
}
public JPlagOptions withOldSubmissionDirectories(Set oldSubmissionDirectories) {
return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionDirectory,
subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
- clusteringOptions, debugParser, mergingOptions);
+ clusteringOptions, debugParser, mergingOptions, normalize);
}
public JPlagOptions withBaseCodeSubmissionDirectory(File baseCodeSubmissionDirectory) {
return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionDirectory,
subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
- clusteringOptions, debugParser, mergingOptions);
+ clusteringOptions, debugParser, mergingOptions, normalize);
}
public JPlagOptions withSubdirectoryName(String subdirectoryName) {
return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionDirectory,
subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
- clusteringOptions, debugParser, mergingOptions);
+ clusteringOptions, debugParser, mergingOptions, normalize);
}
public JPlagOptions withClusteringOptions(ClusteringOptions clusteringOptions) {
return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionDirectory,
subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
- clusteringOptions, debugParser, mergingOptions);
+ clusteringOptions, debugParser, mergingOptions, normalize);
}
public JPlagOptions withMergingOptions(MergingOptions mergingOptions) {
return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionDirectory,
subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
- clusteringOptions, debugParser, mergingOptions);
+ clusteringOptions, debugParser, mergingOptions, normalize);
+ }
+
+ public JPlagOptions withNormalize(boolean normalize) {
+ return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionDirectory,
+ subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
+ clusteringOptions, debugParser, mergingOptions, normalize);
}
public boolean hasBaseCode() {
@@ -186,16 +194,18 @@ public Set excludedFiles() {
@Override
public List fileSuffixes() {
var language = language();
- if ((fileSuffixes == null || fileSuffixes.isEmpty()) && language != null)
+ if ((fileSuffixes == null || fileSuffixes.isEmpty()) && language != null) {
return Arrays.stream(language.suffixes()).toList();
+ }
return fileSuffixes == null ? null : Collections.unmodifiableList(fileSuffixes);
}
@Override
public Integer minimumTokenMatch() {
var language = language();
- if (minimumTokenMatch == null && language != null)
+ if (minimumTokenMatch == null && language != null) {
return language.minimumTokenMatch();
+ }
return minimumTokenMatch;
}
@@ -216,7 +226,8 @@ private static double normalizeSimilarityThreshold(double similarityThreshold) {
if (similarityThreshold > 1) {
logger.warn("Maximum threshold of 1 used instead of {}", similarityThreshold);
return 1;
- } else if (similarityThreshold < 0) {
+ }
+ if (similarityThreshold < 0) {
logger.warn("Minimum threshold of 0 used instead of {}", similarityThreshold);
return 0;
} else {
@@ -264,7 +275,7 @@ public JPlagOptions(Language language, Integer minimumTokenMatch, File submissio
boolean debugParser, MergingOptions mergingOptions) throws BasecodeException {
this(language, minimumTokenMatch, Set.of(submissionDirectory), oldSubmissionDirectories,
convertLegacyBaseCodeToFile(baseCodeSubmissionName, submissionDirectory), subdirectoryName, fileSuffixes, exclusionFileName,
- similarityMetric, similarityThreshold, maximumNumberOfComparisons, clusteringOptions, debugParser, mergingOptions);
+ similarityMetric, similarityThreshold, maximumNumberOfComparisons, clusteringOptions, debugParser, mergingOptions, false);
}
/**
@@ -305,19 +316,18 @@ private static File convertLegacyBaseCodeToFile(String baseCodeSubmissionName, F
File baseCodeAsAbsolutePath = new File(baseCodeSubmissionName);
if (baseCodeAsAbsolutePath.exists()) {
return baseCodeAsAbsolutePath;
- } else {
- String normalizedName = baseCodeSubmissionName;
- while (normalizedName.startsWith(File.separator)) {
- normalizedName = normalizedName.substring(1);
- }
- while (normalizedName.endsWith(File.separator)) {
- normalizedName = normalizedName.substring(0, normalizedName.length() - 1);
- }
- if (normalizedName.isEmpty() || normalizedName.contains(File.separator) || normalizedName.contains(".")) {
- throw new BasecodeException(
- "The basecode directory name \"" + normalizedName + "\" cannot contain dots! Please migrate to the path-based API.");
- }
- return new File(submissionDirectory, baseCodeSubmissionName);
}
+ String normalizedName = baseCodeSubmissionName;
+ while (normalizedName.startsWith(File.separator)) {
+ normalizedName = normalizedName.substring(1);
+ }
+ while (normalizedName.endsWith(File.separator)) {
+ normalizedName = normalizedName.substring(0, normalizedName.length() - 1);
+ }
+ if (normalizedName.isEmpty() || normalizedName.contains(File.separator) || normalizedName.contains(".")) {
+ throw new BasecodeException(
+ "The basecode directory name \"" + normalizedName + "\" cannot contain dots! Please migrate to the path-based API.");
+ }
+ return new File(submissionDirectory, baseCodeSubmissionName);
}
}
diff --git a/core/src/main/java/de/jplag/reporting/FilePathUtil.java b/core/src/main/java/de/jplag/reporting/FilePathUtil.java
index d2db74ad7..51aefdd36 100644
--- a/core/src/main/java/de/jplag/reporting/FilePathUtil.java
+++ b/core/src/main/java/de/jplag/reporting/FilePathUtil.java
@@ -7,6 +7,7 @@
import de.jplag.Submission;
public final class FilePathUtil {
+ private static final String ZIP_PATH_SEPARATOR = "/"; // Paths in zip files are always separated by a slash
private FilePathUtil() {
// private constructor to prevent instantiation
@@ -26,4 +27,23 @@ public static String getRelativeSubmissionPath(File file, Submission submission,
return Path.of(submissionToIdFunction.apply(submission), submission.getRoot().toPath().relativize(file.toPath()).toString()).toString();
}
+ /**
+ * Joins logical paths using a slash. This method ensures, that no duplicate slashes are created in between.
+ * @param left The left path segment
+ * @param right The right path segment
+ * @return The joined paths
+ */
+ public static String joinZipPathSegments(String left, String right) {
+ String rightStripped = right;
+ while (rightStripped.startsWith(ZIP_PATH_SEPARATOR)) {
+ rightStripped = rightStripped.substring(1);
+ }
+
+ String leftStripped = left;
+ while (leftStripped.endsWith(ZIP_PATH_SEPARATOR)) {
+ leftStripped = leftStripped.substring(0, leftStripped.length() - 1);
+ }
+
+ return leftStripped + ZIP_PATH_SEPARATOR + rightStripped;
+ }
}
diff --git a/core/src/main/java/de/jplag/reporting/jsonfactory/ComparisonReportWriter.java b/core/src/main/java/de/jplag/reporting/jsonfactory/ComparisonReportWriter.java
index 8ebb4c555..29f93744c 100644
--- a/core/src/main/java/de/jplag/reporting/jsonfactory/ComparisonReportWriter.java
+++ b/core/src/main/java/de/jplag/reporting/jsonfactory/ComparisonReportWriter.java
@@ -3,7 +3,6 @@
import java.util.Comparator;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
@@ -16,7 +15,7 @@
import de.jplag.reporting.FilePathUtil;
import de.jplag.reporting.reportobject.model.ComparisonReport;
import de.jplag.reporting.reportobject.model.Match;
-import de.jplag.reporting.reportobject.writer.JsonWriter;
+import de.jplag.reporting.reportobject.writer.JPlagResultWriter;
/**
* Writes {@link ComparisonReport}s of given {@link JPlagResult} to the disk under the specified path. Instantiated with
@@ -24,44 +23,43 @@
*/
public class ComparisonReportWriter {
- private final JsonWriter fileWriter;
+ private final JPlagResultWriter resultWriter;
private final Function submissionToIdFunction;
private final Map> submissionIdToComparisonFileName = new ConcurrentHashMap<>();
private final Map fileNameCollisions = new ConcurrentHashMap<>();
- public ComparisonReportWriter(Function submissionToIdFunction, JsonWriter fileWriter) {
+ public ComparisonReportWriter(Function submissionToIdFunction, JPlagResultWriter resultWriter) {
this.submissionToIdFunction = submissionToIdFunction;
- this.fileWriter = fileWriter;
+ this.resultWriter = resultWriter;
}
/**
* Generates detailed ComparisonReport DTO for each comparison in a JPlagResult and writes them to the disk as json
* files.
* @param jPlagResult The JPlagResult to generate the comparison reports from. contains information about a comparison
- * @param path The path to write the comparison files to
* @return Nested map that associates each pair of submissions (by their ids) to their comparison file name. The
* comparison file name for submission with id id1 and id2 can be fetched by executing get two times:
* map.get(id1).get(id2). The nested map is symmetrical therefore, both map.get(id1).get(id2) and map.get(id2).get(id1)
* yield the same result.
*/
- public Map> writeComparisonReports(JPlagResult jPlagResult, String path) {
+ public Map> writeComparisonReports(JPlagResult jPlagResult) {
int numberOfComparisons = jPlagResult.getOptions().maximumNumberOfComparisons();
List comparisons = jPlagResult.getComparisons(numberOfComparisons);
- writeComparisons(path, comparisons);
+ writeComparisons(comparisons);
return submissionIdToComparisonFileName;
}
- private void writeComparisons(String path, List comparisons) {
- comparisons.parallelStream().forEach(comparison -> {
+ private void writeComparisons(List comparisons) {
+ for (JPlagComparison comparison : comparisons) {
String firstSubmissionId = submissionToIdFunction.apply(comparison.firstSubmission());
String secondSubmissionId = submissionToIdFunction.apply(comparison.secondSubmission());
String fileName = generateComparisonName(firstSubmissionId, secondSubmissionId);
addToLookUp(firstSubmissionId, secondSubmissionId, fileName);
var comparisonReport = new ComparisonReport(firstSubmissionId, secondSubmissionId,
Map.of(SimilarityMetric.AVG.name(), comparison.similarity(), SimilarityMetric.MAX.name(), comparison.maximalSimilarity()),
- convertMatchesToReportMatches(comparison));
- fileWriter.writeFile(comparisonReport, path, fileName);
- });
+ convertMatchesToReportMatches(comparison), comparison.similarityOfFirst(), comparison.similarityOfSecond());
+ resultWriter.addJsonEntry(comparisonReport, fileName);
+ }
}
private void addToLookUp(String firstSubmissionId, String secondSubmissionId, String fileName) {
@@ -99,20 +97,16 @@ private Match convertMatchToReportMatch(JPlagComparison comparison, de.jplag.Mat
List tokensFirst = comparison.firstSubmission().getTokenList().subList(match.startOfFirst(), match.endOfFirst() + 1);
List tokensSecond = comparison.secondSubmission().getTokenList().subList(match.startOfSecond(), match.endOfSecond() + 1);
- Comparator super Token> lineComparator = (first, second) -> first.getLine() - second.getLine();
+ Comparator super Token> lineComparator = Comparator.comparingInt(Token::getLine);
Token startOfFirst = tokensFirst.stream().min(lineComparator).orElseThrow();
Token endOfFirst = tokensFirst.stream().max(lineComparator).orElseThrow();
Token startOfSecond = tokensSecond.stream().min(lineComparator).orElseThrow();
Token endOfSecond = tokensSecond.stream().max(lineComparator).orElseThrow();
- List firstTotalTokens = tokensFirst.stream().filter(x -> Objects.equals(x.getFile(), startOfFirst.getFile())).toList();
- List secondTotalTokens = tokensSecond.stream().filter(x -> Objects.equals(x.getFile(), startOfSecond.getFile())).toList();
-
return new Match(FilePathUtil.getRelativeSubmissionPath(startOfFirst.getFile(), comparison.firstSubmission(), submissionToIdFunction),
FilePathUtil.getRelativeSubmissionPath(startOfSecond.getFile(), comparison.secondSubmission(), submissionToIdFunction),
- startOfFirst.getLine(), endOfFirst.getLine(), startOfSecond.getLine(), endOfSecond.getLine(), match.length(), firstTotalTokens.size(),
- secondTotalTokens.size());
+ startOfFirst.getLine(), endOfFirst.getLine(), startOfSecond.getLine(), endOfSecond.getLine(), match.length());
}
}
diff --git a/core/src/main/java/de/jplag/reporting/jsonfactory/DirectoryManager.java b/core/src/main/java/de/jplag/reporting/jsonfactory/DirectoryManager.java
deleted file mode 100644
index 8bbe60593..000000000
--- a/core/src/main/java/de/jplag/reporting/jsonfactory/DirectoryManager.java
+++ /dev/null
@@ -1,121 +0,0 @@
-package de.jplag.reporting.jsonfactory;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.nio.file.FileVisitResult;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.SimpleFileVisitor;
-import java.nio.file.attribute.BasicFileAttributes;
-import java.util.Comparator;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipOutputStream;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Provides Methods for creating directories.
- */
-public class DirectoryManager {
- private static final Logger logger = LoggerFactory.getLogger(DirectoryManager.class);
-
- /**
- * Creates a full path directory.
- * @param path The path under which the new directory or file ought to be created
- * @param name The name of the new directory. According to this name we can get sub-folder's structure after this
- * directory.
- * @param file The file, which has the path of sub-folders
- * @param submissionRoot The file, which has the root path of submission
- * @return The created directory which has the whole structure as file
- */
- public static File createDirectory(String path, String name, File file, File submissionRoot) throws IOException {
- File directory;
- String fileFullPath = file.getPath();
- String submissionRootPath = submissionRoot.getPath();
- String filePathWithoutRootName = fileFullPath.substring(submissionRootPath.length());
- String outputRootDirectory = Path.of(path, name).toString();
- if ("".equals(filePathWithoutRootName)) {
- directory = new File(Path.of(outputRootDirectory, name).toString());
- } else {
- directory = new File(outputRootDirectory + filePathWithoutRootName);
- }
- if (!directory.exists() && !directory.mkdirs()) {
- throw new IOException("Failed to create dir.");
- }
- return directory;
- }
-
- /**
- * Creates a directory.
- * @param path The path under which the new directory ought to be created
- * @param name The name of the new directory
- * @return The created directory
- */
- public static File createDirectory(String path, String name) throws IOException {
- File directory = new File(path.concat(File.separator).concat(name));
- if (!directory.exists() && !directory.mkdirs()) {
- throw new IOException("Failed to create dir.");
- }
- return directory;
- }
-
- /**
- * Create a directory with the given path
- * @param path The path of the new directory
- */
- public static void createDirectory(String path) throws IOException {
- createDirectory(path, "");
- }
-
- /**
- * Delete the directory and all of its contents, identified by the given path
- * @param path The path that identifies the directory to delete
- */
- public static void deleteDirectory(String path) {
- try (var f = Files.walk(Path.of(path))) {
- f.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
- } catch (IOException e) {
- logger.error("Could not delete folder " + path, e);
- }
- }
-
- /**
- * Zip the directory identified by the given path
- * @param path The path that identifies the directory to zip
- * @return True if zip was successful, false otherwise
- */
- public static boolean zipDirectory(String path) {
- Path p = Path.of(path);
- String zipName = path + ".zip";
-
- try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipName))) {
-
- Files.walkFileTree(p, new SimpleFileVisitor<>() {
- @Override
- public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
- Path targetFile = p.relativize(file);
- zos.putNextEntry(new ZipEntry(targetFile.toString()));
-
- byte[] bytes = Files.readAllBytes(file);
- zos.write(bytes, 0, bytes.length);
- return FileVisitResult.CONTINUE;
- }
-
- @Override
- public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
- logger.error("Unable to zip " + file, exc);
- throw exc;
- }
- });
- } catch (IOException e) {
- logger.error(e.getMessage(), e);
- deleteDirectory(zipName);
- return false;
- }
- logger.info("Successfully zipped report files: {}", zipName);
- logger.info("Display the results with the report viewer at https://jplag.github.io/JPlag/");
- return true;
- }
-}
diff --git a/core/src/main/java/de/jplag/reporting/jsonfactory/serializer/LanguageSerializer.java b/core/src/main/java/de/jplag/reporting/jsonfactory/serializer/LanguageSerializer.java
index 22e961105..c521507c7 100644
--- a/core/src/main/java/de/jplag/reporting/jsonfactory/serializer/LanguageSerializer.java
+++ b/core/src/main/java/de/jplag/reporting/jsonfactory/serializer/LanguageSerializer.java
@@ -1,6 +1,7 @@
package de.jplag.reporting.jsonfactory.serializer;
import java.io.IOException;
+import java.io.Serial;
import de.jplag.Language;
@@ -10,6 +11,9 @@
public class LanguageSerializer extends StdSerializer {
+ @Serial
+ private static final long serialVersionUID = 5944655736767387268L; // generated
+
/**
* Constructor used by the fasterxml.jackson
*/
diff --git a/core/src/main/java/de/jplag/reporting/reportobject/ReportObjectFactory.java b/core/src/main/java/de/jplag/reporting/reportobject/ReportObjectFactory.java
index f8bc7c4dd..d569a5252 100644
--- a/core/src/main/java/de/jplag/reporting/reportobject/ReportObjectFactory.java
+++ b/core/src/main/java/de/jplag/reporting/reportobject/ReportObjectFactory.java
@@ -1,19 +1,13 @@
package de.jplag.reporting.reportobject;
-import static de.jplag.reporting.jsonfactory.DirectoryManager.createDirectory;
-import static de.jplag.reporting.jsonfactory.DirectoryManager.deleteDirectory;
-import static de.jplag.reporting.jsonfactory.DirectoryManager.zipDirectory;
import static de.jplag.reporting.reportobject.mapper.SubmissionNameToIdMapper.buildSubmissionNameToIdMap;
import java.io.File;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.StandardCopyOption;
+import java.io.FileNotFoundException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
-import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -34,21 +28,19 @@
import de.jplag.reporting.reportobject.mapper.ClusteringResultMapper;
import de.jplag.reporting.reportobject.mapper.MetricMapper;
import de.jplag.reporting.reportobject.model.OverviewReport;
+import de.jplag.reporting.reportobject.model.SubmissionFile;
import de.jplag.reporting.reportobject.model.SubmissionFileIndex;
import de.jplag.reporting.reportobject.model.Version;
-import de.jplag.reporting.reportobject.writer.JsonWriter;
-import de.jplag.reporting.reportobject.writer.TextWriter;
+import de.jplag.reporting.reportobject.writer.JPlagResultWriter;
+import de.jplag.reporting.reportobject.writer.ZipWriter;
/**
* Factory class, responsible for converting a JPlagResult object to Overview and Comparison DTO classes and writing it
* to the disk.
*/
public class ReportObjectFactory {
- private static final String DIRECTORY_ERROR = "Could not create directory {} for report viewer generation";
-
private static final Logger logger = LoggerFactory.getLogger(ReportObjectFactory.class);
- private static final JsonWriter jsonFileWriter = new JsonWriter();
public static final String OVERVIEW_FILE_NAME = "overview.json";
public static final String README_FILE_NAME = "README.txt";
@@ -56,49 +48,51 @@ public class ReportObjectFactory {
private static final String[] README_CONTENT = new String[] {"This is a software plagiarism report generated by JPlag.",
"To view the report go to https://jplag.github.io/JPlag/ and drag the generated zip file onto the page."};
- public static final String SUBMISSIONS_FOLDER = "files";
public static final String SUBMISSION_FILE_INDEX_FILE_NAME = "submissionFileIndex.json";
public static final Version REPORT_VIEWER_VERSION = JPlag.JPLAG_VERSION;
+ private static final String SUBMISSIONS_ROOT_PATH = "files/";
+
private Map submissionNameToIdMap;
private Function submissionToIdFunction;
private Map> submissionNameToNameToComparisonFileName;
+ private final JPlagResultWriter resultWriter;
+
/**
- * Creates all necessary report viewer files, writes them to the disk as zip.
- * @param result The JPlagResult to be converted into a report.
- * @param path The Path to save the report to
+ * Creates a new report object factory, that can be used to write a report.
+ * @param resultWriter The writer to use for writing report content
*/
- public void createAndSaveReport(JPlagResult result, String path) {
-
- try {
- logger.info("Start writing report files...");
- createDirectory(path);
- buildSubmissionToIdMap(result);
+ public ReportObjectFactory(JPlagResultWriter resultWriter) {
+ this.resultWriter = resultWriter;
+ }
- copySubmissionFilesToReport(path, result);
+ /**
+ * Creates a new report object factory, that can be used to write a zip report.
+ * @param zipFile The zip file to write the report to
+ * @throws FileNotFoundException If the file cannot be opened for writing
+ */
+ public ReportObjectFactory(File zipFile) throws FileNotFoundException {
+ this(new ZipWriter(zipFile));
+ }
- writeComparisons(result, path);
- writeOverview(result, path);
- writeSubmissionIndexFile(result, path);
- writeReadMeFile(path);
- writeOptionsFiles(result.getOptions(), path);
+ /**
+ * Creates all necessary report viewer files, writes them to the disk as zip.
+ * @param result The JPlagResult to be converted into a report.
+ */
+ public void createAndSaveReport(JPlagResult result) {
+ logger.info("Start writing report...");
+ buildSubmissionToIdMap(result);
- logger.info("Zipping report files...");
- zipAndDelete(path);
- } catch (IOException e) {
- logger.error(DIRECTORY_ERROR, e, path);
- }
+ copySubmissionFilesToReport(result);
- }
+ writeComparisons(result);
+ writeOverview(result);
+ writeSubmissionIndexFile(result);
+ writeReadMeFile();
+ writeOptionsFiles(result.getOptions());
- private void zipAndDelete(String path) {
- boolean zipWasSuccessful = zipDirectory(path);
- if (zipWasSuccessful) {
- deleteDirectory(path);
- } else {
- logger.error("Could not zip results. The results are still available uncompressed at " + path);
- }
+ this.resultWriter.close();
}
private void buildSubmissionToIdMap(JPlagResult result) {
@@ -106,61 +100,23 @@ private void buildSubmissionToIdMap(JPlagResult result) {
submissionToIdFunction = (Submission submission) -> submissionNameToIdMap.get(submission.getName());
}
- private void copySubmissionFilesToReport(String path, JPlagResult result) {
- logger.info("Start copying submission files to the output directory...");
+ private void copySubmissionFilesToReport(JPlagResult result) {
+ logger.info("Start to export results...");
List comparisons = result.getComparisons(result.getOptions().maximumNumberOfComparisons());
Set submissions = getSubmissions(comparisons);
- File submissionsPath = createSubmissionsDirectory(path);
- if (submissionsPath == null) {
- return;
- }
Language language = result.getOptions().language();
for (Submission submission : submissions) {
- File directory = createSubmissionDirectory(path, submissionsPath, submission);
- File submissionRoot = submission.getRoot();
- if (directory == null) {
- continue;
- }
+ String submissionRootPath = SUBMISSIONS_ROOT_PATH + submissionToIdFunction.apply(submission);
for (File file : submission.getFiles()) {
- File fullPath = createSubmissionDirectory(path, submissionsPath, submission, file, submissionRoot);
- File fileToCopy = getFileToCopy(language, file);
- try {
- if (fullPath != null) {
- Files.copy(fileToCopy.toPath(), fullPath.toPath(), StandardCopyOption.REPLACE_EXISTING);
- } else {
- throw new NullPointerException("Could not create file with full path");
- }
- } catch (IOException e) {
- logger.error("Could not save submission file " + fileToCopy, e);
+ String relativeFilePath = file.getAbsolutePath().substring(submission.getRoot().getAbsolutePath().length());
+ if (relativeFilePath.isEmpty()) {
+ relativeFilePath = file.getName();
}
- }
- }
- }
-
- private File createSubmissionDirectory(String path, File submissionsPath, Submission submission, File file, File submissionRoot) {
- try {
- return createDirectory(submissionsPath.getPath(), submissionToIdFunction.apply(submission), file, submissionRoot);
- } catch (IOException e) {
- logger.error(DIRECTORY_ERROR, e, path);
- return null;
- }
- }
-
- private File createSubmissionDirectory(String path, File submissionsPath, Submission submission) {
- try {
- return createDirectory(submissionsPath.getPath(), submissionToIdFunction.apply(submission));
- } catch (IOException e) {
- logger.error(DIRECTORY_ERROR, e, path);
- return null;
- }
- }
+ String zipPath = FilePathUtil.joinZipPathSegments(submissionRootPath, relativeFilePath);
- private File createSubmissionsDirectory(String path) {
- try {
- return createDirectory(path, SUBMISSIONS_FOLDER);
- } catch (IOException e) {
- logger.error(DIRECTORY_ERROR, e, path);
- return null;
+ File fileToCopy = getFileToCopy(language, file);
+ this.resultWriter.addFileContentEntry(zipPath, fileToCopy);
+ }
}
}
@@ -168,13 +124,12 @@ private File getFileToCopy(Language language, File file) {
return language.useViewFiles() ? new File(file.getPath() + language.viewFileSuffix()) : file;
}
- private void writeComparisons(JPlagResult result, String path) {
- ComparisonReportWriter comparisonReportWriter = new ComparisonReportWriter(submissionToIdFunction, jsonFileWriter);
- submissionNameToNameToComparisonFileName = comparisonReportWriter.writeComparisonReports(result, path);
+ private void writeComparisons(JPlagResult result) {
+ ComparisonReportWriter comparisonReportWriter = new ComparisonReportWriter(submissionToIdFunction, this.resultWriter);
+ submissionNameToNameToComparisonFileName = comparisonReportWriter.writeComparisonReports(result);
}
- private void writeOverview(JPlagResult result, String path) {
-
+ private void writeOverview(JPlagResult result) {
List folders = new ArrayList<>();
folders.addAll(result.getOptions().submissionDirectories());
folders.addAll(result.getOptions().oldSubmissionDirectories());
@@ -204,30 +159,34 @@ private void writeOverview(JPlagResult result, String path) {
clusteringResultMapper.map(result), // clusters
totalComparisons); // totalComparisons
- jsonFileWriter.writeFile(overviewReport, path, OVERVIEW_FILE_NAME);
+ this.resultWriter.addJsonEntry(overviewReport, OVERVIEW_FILE_NAME);
+
}
- private void writeReadMeFile(String path) {
- new TextWriter().writeFile(String.join(System.lineSeparator(), README_CONTENT), path, README_FILE_NAME);
+ private void writeReadMeFile() {
+ this.resultWriter.writeStringEntry(String.join(System.lineSeparator(), README_CONTENT), README_FILE_NAME);
}
- private void writeSubmissionIndexFile(JPlagResult result, String path) {
+ private void writeSubmissionIndexFile(JPlagResult result) {
List comparisons = result.getComparisons(result.getOptions().maximumNumberOfComparisons());
Set submissions = getSubmissions(comparisons);
SubmissionFileIndex fileIndex = new SubmissionFileIndex(new HashMap<>());
- for (Submission submission : submissions) {
- List filePaths = new LinkedList<>();
- for (File file : submission.getFiles()) {
- filePaths.add(FilePathUtil.getRelativeSubmissionPath(file, submission, submissionToIdFunction));
+ List
## What is JPlag
-JPlag is a system that finds similarities among multiple sets of source code files. This way it can detect software plagiarism and collusion in software development. JPlag does not merely compare bytes of text but is aware of programming language syntax and program structure and hence is robust against many kinds of attempts to disguise similarities between plagiarized files. JPlag currently supports Java, C#, C/C++, Python 3, Go, Rust, Kotlin, Swift, Scala, Scheme, EMF, and natural language text.
+JPlag finds pairwise similarities among a set of multiple programs. It can reliably detect software plagiarism and collusion in software development. All similarities are calculated locally, and no source code or plagiarism results are ever uploaded to the internet. JPlag supports a large number of programming and modeling languages. JPlag does not merely compare bytes of text but is aware of programming language syntax and program structure and hence is robust against many kinds of attempts to disguise similarities (_obfusction_) between plagiarized files.
JPlag is typically used to detect and thus discourage the unallowed copying of student exercise programs in programming education. But in principle, it can also be used to detect stolen software parts among large amounts of source text or modules that have been duplicated (and only slightly modified). JPlag has already played a part in several intellectual property cases where it has been successfully used by expert witnesses.
-[TODO]: <> (Link or visualize example report)
-
**Just to make it clear**: JPlag does not compare to the internet! It is designed to find similarities among the student solutions, which is usually sufficient for computer programs.
+* 📈 [JPlag Demo](https://jplag.github.io/Demo/)
+
+* 🏛️ [JPlag on Helmholtz RSD](https://helmholtz.software/software/jplag)
+
+* 🤩 [Give us Feedback in a **short (<5 min) survey**](https://docs.google.com/forms/d/e/1FAIpQLSckqUlXhIlJ-H2jtu2VmGf_mJt4hcnHXaDlwhpUL3XG1I8UYw/viewform?usp=sf_link)
+
## History
Originally, JPlag was developed in 1996 by Guido Mahlpohl and others at the chair of Prof. Walter Tichy at Karlsruhe Institute of Technology (KIT). It was first documented in a [Tech Report](https://publikationen.bibliothek.kit.edu/542000) in 2000 and later more formally in the [Journal of Universal Computer Science](http://www.ipd.kit.edu/tichy/uploads/publikationen/16/finding_plagiarisms_among_a_set_of_progr_638847.pdf). Since 2015 JPlag is hosted here on GitHub. After 25 years of its creation, JPlag is still used frequently in many universities in different countries around the world.
@@ -26,15 +30,9 @@ JPlag is released on [Maven Central](https://search.maven.org/search?q=de.jplag)
de.jplagjplag
+
```
## JPlag legacy version
-In case you depend on the legacy version of JPlag we refer to the [legacy release v2.12.1](https://github.com/jplag/jplag/releases/tag/v2.12.1-SNAPSHOT) and the [legacy branch](https://github.com/jplag/jplag/tree/legacy). Note that the legacy CLI usage is slightly different.
-
-The following features are only available in version v4.0.0 and onwards:
-* a modern web-based UI
-* a simplified command-line interface
-* support for Kotlin, Scala, Go, Rust, and R
-* support for Java 17 language features
-* a Java API for third-party integration
+In case you depend on the legacy version of JPlag, we refer to the [legacy release v2.12.1](https://github.com/jplag/jplag/releases/tag/v2.12.1-SNAPSHOT) and the [legacy branch](https://github.com/jplag/jplag/tree/legacy). Note that the legacy CLI and report UI are different and provide fewer features.
diff --git a/endtoend-testing/src/main/java/de/jplag/endtoend/helper/FileHelper.java b/endtoend-testing/src/main/java/de/jplag/endtoend/helper/FileHelper.java
index ffcff5b2e..abc9991f3 100644
--- a/endtoend-testing/src/main/java/de/jplag/endtoend/helper/FileHelper.java
+++ b/endtoend-testing/src/main/java/de/jplag/endtoend/helper/FileHelper.java
@@ -72,6 +72,7 @@ private static String createNewIOExceptionStringForFileOrFOlderCreation(File fil
public static void unzip(File zip, File targetDirectory) throws IOException {
try (ZipFile zipFile = new ZipFile(zip)) {
Enumeration extends ZipEntry> entries = zipFile.entries();
+ File canonicalTarget = targetDirectory.getCanonicalFile();
long totalSizeArchive = 0;
long totalEntriesArchive = 0;
@@ -80,9 +81,9 @@ public static void unzip(File zip, File targetDirectory) throws IOException {
totalEntriesArchive++;
ZipEntry entry = entries.nextElement();
- File unzippedFile = new File(targetDirectory, entry.getName()).getCanonicalFile();
+ File unzippedFile = new File(canonicalTarget, entry.getName()).getCanonicalFile();
- if (unzippedFile.getAbsolutePath().startsWith(targetDirectory.getAbsolutePath())) {
+ if (unzippedFile.getAbsolutePath().startsWith(canonicalTarget.getAbsolutePath())) {
if (entry.isDirectory()) {
unzippedFile.mkdirs();
} else {
@@ -91,10 +92,7 @@ public static void unzip(File zip, File targetDirectory) throws IOException {
}
}
- if (totalSizeArchive > ZIP_THRESHOLD_SIZE) {
- throw new IllegalStateException(String.format(ZIP_BOMB_ERROR_MESSAGE, zip.getAbsolutePath()));
- }
- if (totalEntriesArchive > ZIP_THRESHOLD_ENTRIES) {
+ if ((totalSizeArchive > ZIP_THRESHOLD_SIZE) || (totalEntriesArchive > ZIP_THRESHOLD_ENTRIES)) {
throw new IllegalStateException(String.format(ZIP_BOMB_ERROR_MESSAGE, zip.getAbsolutePath()));
}
}
diff --git a/endtoend-testing/src/main/java/de/jplag/endtoend/model/DataSet.java b/endtoend-testing/src/main/java/de/jplag/endtoend/model/DataSet.java
index 4be001365..229018daa 100644
--- a/endtoend-testing/src/main/java/de/jplag/endtoend/model/DataSet.java
+++ b/endtoend-testing/src/main/java/de/jplag/endtoend/model/DataSet.java
@@ -57,7 +57,8 @@ File actualSourceDirectory() throws IOException {
location = String.format(DEFAULT_SOURCE_DIRECTORY, this.name);
}
return new File(TestDirectoryConstants.BASE_PATH_TO_RESOURCES.toFile(), location);
- } else if (actualStorageFormat == StorageFormat.ZIP) {
+ }
+ if (actualStorageFormat == StorageFormat.ZIP) {
String location = sourceLocation;
if (location == null) {
location = String.format(DEFAULT_SOURCE_ZIP, this.name);
@@ -75,9 +76,8 @@ File actualSourceDirectory() throws IOException {
public File getResultFile() {
if (resultFile == null) {
return new File(TestDirectoryConstants.BASE_PATH_TO_RESULT_JSON.toFile(), String.format(DEFAULT_RESULT_FILE_NAME, this.name));
- } else {
- return new File(TestDirectoryConstants.BASE_PATH_TO_RESULT_JSON.toFile(), resultFile);
}
+ return new File(TestDirectoryConstants.BASE_PATH_TO_RESULT_JSON.toFile(), resultFile);
}
/**
diff --git a/endtoend-testing/src/main/java/de/jplag/endtoend/model/Options.java b/endtoend-testing/src/main/java/de/jplag/endtoend/model/Options.java
index 651f7d5bb..c84041cf5 100644
--- a/endtoend-testing/src/main/java/de/jplag/endtoend/model/Options.java
+++ b/endtoend-testing/src/main/java/de/jplag/endtoend/model/Options.java
@@ -41,10 +41,12 @@ public int[] getMinimumTokenMatches() {
@Override
public boolean equals(Object o) {
- if (this == o)
+ if (this == o) {
return true;
- if (o == null || getClass() != o.getClass())
+ }
+ if (o == null || getClass() != o.getClass()) {
return false;
+ }
Options options = (Options) o;
return Arrays.equals(minimumTokenMatches, options.minimumTokenMatches) && Objects.equals(baseCodeDirectory, options.baseCodeDirectory);
}
diff --git a/endtoend-testing/src/main/java/de/jplag/endtoend/model/ResultDescription.java b/endtoend-testing/src/main/java/de/jplag/endtoend/model/ResultDescription.java
index 81ebc6627..c46cc6f6e 100644
--- a/endtoend-testing/src/main/java/de/jplag/endtoend/model/ResultDescription.java
+++ b/endtoend-testing/src/main/java/de/jplag/endtoend/model/ResultDescription.java
@@ -6,8 +6,8 @@
/**
* Object that maps the results of the end top end tests using the identifierToResultMap. this creates a map of test
- * data and its results for each possible option specified. this is important both for serializing the data into json
- * format and for deserialization.
+ * stream and its results for each possible option specified. this is important both for serializing the stream into
+ * json format and for deserialization.
*/
public record ResultDescription(@JsonProperty String identifier, @JsonProperty("tests") Map identifierToResultMap,
@JsonProperty GoldStandard goldStandard) {
diff --git a/language-antlr-utils/pom.xml b/language-antlr-utils/pom.xml
index c5542fbfc..6697788a5 100644
--- a/language-antlr-utils/pom.xml
+++ b/language-antlr-utils/pom.xml
@@ -14,7 +14,6 @@
org.antlrantlr4-runtime
- 4.13.1de.jplag
diff --git a/language-antlr-utils/src/main/java/de/jplag/antlr/AbstractAntlrLanguage.java b/language-antlr-utils/src/main/java/de/jplag/antlr/AbstractAntlrLanguage.java
index 93f6ace30..1d75847bd 100644
--- a/language-antlr-utils/src/main/java/de/jplag/antlr/AbstractAntlrLanguage.java
+++ b/language-antlr-utils/src/main/java/de/jplag/antlr/AbstractAntlrLanguage.java
@@ -34,9 +34,9 @@ protected AbstractAntlrLanguage() {
}
@Override
- public List parse(Set files) throws ParsingException {
+ public List parse(Set files, boolean normalize) throws ParsingException {
if (this.parser == null) {
- this.parser = this.initializeParser();
+ this.parser = this.initializeParser(normalize);
}
return this.parser.parse(files);
@@ -46,7 +46,7 @@ public List parse(Set files) throws ParsingException {
* Lazily creates the parser. Has to be implemented, if no parser is passed in the constructor.
* @return The newly initialized parser
*/
- protected AbstractAntlrParserAdapter> initializeParser() {
+ protected AbstractAntlrParserAdapter> initializeParser(boolean normalize) {
throw new UnsupportedOperationException(
String.format("The initializeParser method needs to be implemented for %s", this.getClass().getName()));
}
diff --git a/language-antlr-utils/src/main/java/de/jplag/antlr/AbstractVisitor.java b/language-antlr-utils/src/main/java/de/jplag/antlr/AbstractVisitor.java
index 7946df1d9..0ec4c19af 100644
--- a/language-antlr-utils/src/main/java/de/jplag/antlr/AbstractVisitor.java
+++ b/language-antlr-utils/src/main/java/de/jplag/antlr/AbstractVisitor.java
@@ -2,7 +2,11 @@
import java.util.ArrayList;
import java.util.List;
-import java.util.function.*;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
import org.antlr.v4.runtime.Token;
import org.slf4j.Logger;
diff --git a/language-antlr-utils/src/main/java/de/jplag/antlr/ContextVisitor.java b/language-antlr-utils/src/main/java/de/jplag/antlr/ContextVisitor.java
index 0d3aabe3a..96436248d 100644
--- a/language-antlr-utils/src/main/java/de/jplag/antlr/ContextVisitor.java
+++ b/language-antlr-utils/src/main/java/de/jplag/antlr/ContextVisitor.java
@@ -2,7 +2,11 @@
import java.util.ArrayList;
import java.util.List;
-import java.util.function.*;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.Token;
@@ -132,6 +136,7 @@ void exit(HandlerData data) {
exitHandlers.forEach(handler -> handler.accept(data));
}
+ @Override
Token extractEnterToken(T entity) {
return entity.getStart();
}
diff --git a/language-antlr-utils/src/main/java/de/jplag/antlr/InternalListener.java b/language-antlr-utils/src/main/java/de/jplag/antlr/InternalListener.java
index 39178e0fb..5d2a8e51a 100644
--- a/language-antlr-utils/src/main/java/de/jplag/antlr/InternalListener.java
+++ b/language-antlr-utils/src/main/java/de/jplag/antlr/InternalListener.java
@@ -14,7 +14,7 @@
class InternalListener implements ParseTreeListener {
private final AbstractAntlrListener listener;
private final TokenCollector collector;
- protected final VariableRegistry variableRegistry;
+ private final VariableRegistry variableRegistry;
InternalListener(AbstractAntlrListener listener, TokenCollector collector) {
this.listener = listener;
diff --git a/language-antlr-utils/src/main/java/de/jplag/antlr/TerminalVisitor.java b/language-antlr-utils/src/main/java/de/jplag/antlr/TerminalVisitor.java
index 170f5d627..80fa407f9 100644
--- a/language-antlr-utils/src/main/java/de/jplag/antlr/TerminalVisitor.java
+++ b/language-antlr-utils/src/main/java/de/jplag/antlr/TerminalVisitor.java
@@ -13,6 +13,7 @@ public class TerminalVisitor extends AbstractVisitor {
super(condition);
}
+ @Override
Token extractEnterToken(Token token) {
return token;
}
diff --git a/language-antlr-utils/src/test/java/de/jplag/antlr/LanguageTest.java b/language-antlr-utils/src/test/java/de/jplag/antlr/LanguageTest.java
index 81941ac8d..b8fe50f51 100644
--- a/language-antlr-utils/src/test/java/de/jplag/antlr/LanguageTest.java
+++ b/language-antlr-utils/src/test/java/de/jplag/antlr/LanguageTest.java
@@ -20,19 +20,19 @@ class LanguageTest {
void testExceptionForNoDefinedParser() {
LanguageWithoutParser lang = new LanguageWithoutParser();
Set emptySet = Set.of();
- assertThrows(UnsupportedOperationException.class, () -> lang.parse(emptySet));
+ assertThrows(UnsupportedOperationException.class, () -> lang.parse(emptySet, false));
}
@Test
void testLanguageWithStaticParser() throws ParsingException {
TestLanguage lang = new TestLanguage();
- Assertions.assertEquals(0, lang.parse(Set.of()).size());
+ Assertions.assertEquals(0, lang.parse(Set.of(), false).size());
}
@Test
void testLanguageWithLazyParser() throws ParsingException {
LanguageWithLazyParser lang = new LanguageWithLazyParser();
- Assertions.assertEquals(0, lang.parse(Set.of()).size());
+ Assertions.assertEquals(0, lang.parse(Set.of(), false).size());
}
private static class LanguageWithoutParser extends AbstractAntlrLanguage {
@@ -59,7 +59,7 @@ public int minimumTokenMatch() {
private static class LanguageWithLazyParser extends LanguageWithoutParser {
@Override
- protected AbstractAntlrParserAdapter> initializeParser() {
+ protected AbstractAntlrParserAdapter> initializeParser(boolean normalize) {
return new TestParserAdapter();
}
}
diff --git a/language-antlr-utils/src/test/java/de/jplag/antlr/ParserTest.java b/language-antlr-utils/src/test/java/de/jplag/antlr/ParserTest.java
index e33277201..6538dc40c 100644
--- a/language-antlr-utils/src/test/java/de/jplag/antlr/ParserTest.java
+++ b/language-antlr-utils/src/test/java/de/jplag/antlr/ParserTest.java
@@ -1,6 +1,10 @@
package de.jplag.antlr;
-import static de.jplag.antlr.testLanguage.TestTokenType.*;
+import static de.jplag.antlr.testLanguage.TestTokenType.ADDITION;
+import static de.jplag.antlr.testLanguage.TestTokenType.NUMBER;
+import static de.jplag.antlr.testLanguage.TestTokenType.SUBTRACTION;
+import static de.jplag.antlr.testLanguage.TestTokenType.SUB_EXPRESSION_BEGIN;
+import static de.jplag.antlr.testLanguage.TestTokenType.SUB_EXPRESSION_END;
import de.jplag.antlr.testLanguage.TestLanguage;
import de.jplag.antlr.testLanguage.TestTokenType;
diff --git a/language-antlr-utils/src/test/java/de/jplag/antlr/testLanguage/TestListener.java b/language-antlr-utils/src/test/java/de/jplag/antlr/testLanguage/TestListener.java
index 9073ad954..c61ec4c3c 100644
--- a/language-antlr-utils/src/test/java/de/jplag/antlr/testLanguage/TestListener.java
+++ b/language-antlr-utils/src/test/java/de/jplag/antlr/testLanguage/TestListener.java
@@ -1,9 +1,18 @@
package de.jplag.antlr.testLanguage;
-import static de.jplag.antlr.testLanguage.TestTokenType.*;
+import static de.jplag.antlr.testLanguage.TestTokenType.ADDITION;
+import static de.jplag.antlr.testLanguage.TestTokenType.NUMBER;
+import static de.jplag.antlr.testLanguage.TestTokenType.SUBTRACTION;
+import static de.jplag.antlr.testLanguage.TestTokenType.SUB_EXPRESSION_BEGIN;
+import static de.jplag.antlr.testLanguage.TestTokenType.SUB_EXPRESSION_END;
+import static de.jplag.antlr.testLanguage.TestTokenType.VARDEF;
-import de.jplag.antlr.*;
-import de.jplag.antlr.TestParser.*;
+import de.jplag.antlr.AbstractAntlrListener;
+import de.jplag.antlr.TestParser;
+import de.jplag.antlr.TestParser.CalcExpressionContext;
+import de.jplag.antlr.TestParser.OperatorContext;
+import de.jplag.antlr.TestParser.SubExpressionContext;
+import de.jplag.antlr.TestParser.VarDefContext;
import de.jplag.semantics.CodeSemantics;
import de.jplag.semantics.VariableScope;
diff --git a/language-antlr-utils/src/test/java/de/jplag/antlr/testLanguage/TestParserAdapter.java b/language-antlr-utils/src/test/java/de/jplag/antlr/testLanguage/TestParserAdapter.java
index c2873a167..985477250 100644
--- a/language-antlr-utils/src/test/java/de/jplag/antlr/testLanguage/TestParserAdapter.java
+++ b/language-antlr-utils/src/test/java/de/jplag/antlr/testLanguage/TestParserAdapter.java
@@ -5,7 +5,10 @@
import org.antlr.v4.runtime.Lexer;
import org.antlr.v4.runtime.ParserRuleContext;
-import de.jplag.antlr.*;
+import de.jplag.antlr.AbstractAntlrListener;
+import de.jplag.antlr.AbstractAntlrParserAdapter;
+import de.jplag.antlr.TestLexer;
+import de.jplag.antlr.TestParser;
public class TestParserAdapter extends AbstractAntlrParserAdapter {
private static final TestListener listener = new TestListener();
diff --git a/language-api/src/main/java/de/jplag/Language.java b/language-api/src/main/java/de/jplag/Language.java
index c7e199667..ffe6aa9a6 100644
--- a/language-api/src/main/java/de/jplag/Language.java
+++ b/language-api/src/main/java/de/jplag/Language.java
@@ -32,12 +32,25 @@ public interface Language {
int minimumTokenMatch();
/**
- * Parses a set of files.
+ * Parses a set of files. Override this method, if you don't require normalization.
* @param files are the files to parse.
* @return the list of parsed JPlag tokens.
* @throws ParsingException if an error during parsing the files occurred.
+ * @deprecated Replaced by {@link #parse(Set, boolean)}
*/
- List parse(Set files) throws ParsingException;
+ @Deprecated(forRemoval = true)
+ default List parse(Set files) throws ParsingException {
+ return parse(files, false);
+ }
+
+ /**
+ * Parses a set of files. Override this method, if you require normalization within the language module.
+ * @param files are the files to parse.
+ * @param normalize True, if the tokens should be normalized
+ * @return the list of parsed JPlag tokens.
+ * @throws ParsingException if an error during parsing the files occurred.
+ */
+ List parse(Set files, boolean normalize) throws ParsingException;
/**
* Indicates whether the tokens returned by parse have semantic information added to them, i.e. whether the token
@@ -93,4 +106,20 @@ default boolean expectsSubmissionOrder() {
default List customizeSubmissionOrder(List submissions) {
return submissions;
}
+
+ /**
+ * @return True, if this language supports token sequence normalization. This does not include other normalization
+ * mechanisms that might be part of the language modules.
+ */
+ default boolean supportsNormalization() {
+ return false;
+ }
+
+ /**
+ * Override this method, if you need normalization within the language module, but not in the core module.
+ * @return True, If the core normalization should be used.
+ */
+ default boolean requiresCoreNormalization() {
+ return true;
+ }
}
diff --git a/language-api/src/main/java/de/jplag/ParsingException.java b/language-api/src/main/java/de/jplag/ParsingException.java
index 4f83721b4..c6e1ad627 100644
--- a/language-api/src/main/java/de/jplag/ParsingException.java
+++ b/language-api/src/main/java/de/jplag/ParsingException.java
@@ -60,16 +60,14 @@ public ParsingException(File file, String reason, Throwable cause) {
* the provided exception if only one was provided.
*/
public static ParsingException wrappingExceptions(Collection exceptions) {
- switch (exceptions.size()) {
- case 0:
- return null;
- case 1:
- return exceptions.iterator().next();
- default: {
+ return switch (exceptions.size()) {
+ case 0 -> null;
+ case 1 -> exceptions.iterator().next();
+ default -> {
String message = exceptions.stream().map(ParsingException::getMessage).collect(Collectors.joining("\n"));
- return new ParsingException(message);
+ yield new ParsingException(message);
}
- }
+ };
}
private ParsingException(String message) {
@@ -78,9 +76,11 @@ private ParsingException(String message) {
private static String constructMessage(File file, String reason) {
StringBuilder messageBuilder = new StringBuilder();
- messageBuilder.append("failed to parse '%s'".formatted(file));
+ if (reason == null || !reason.contains(file.toString())) {
+ messageBuilder.append("failed to parse '%s'".formatted(file));
+ }
if (reason != null && !reason.isBlank()) {
- messageBuilder.append(" with reason: %s".formatted(reason));
+ messageBuilder.append(reason);
}
return messageBuilder.toString();
}
diff --git a/language-api/src/main/java/de/jplag/Token.java b/language-api/src/main/java/de/jplag/Token.java
index b279b886d..d4ac238b6 100644
--- a/language-api/src/main/java/de/jplag/Token.java
+++ b/language-api/src/main/java/de/jplag/Token.java
@@ -13,15 +13,15 @@
* The language parsers decide what is a token and what is not.
*/
public class Token {
- private Logger logger = LoggerFactory.getLogger(this.getClass());
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
/** Indicates that the requested field has no value. */
public static final int NO_VALUE = -1;
- private int line;
- private int column;
- private int length;
- private File file;
- private TokenType type;
+ private final int line;
+ private final int column;
+ private final int length;
+ private final File file;
+ private final TokenType type;
private CodeSemantics semantics; // value null if no semantics
/**
diff --git a/language-api/src/main/java/de/jplag/TokenPrinter.java b/language-api/src/main/java/de/jplag/TokenPrinter.java
index fb2198c4b..1581beb02 100644
--- a/language-api/src/main/java/de/jplag/TokenPrinter.java
+++ b/language-api/src/main/java/de/jplag/TokenPrinter.java
@@ -224,8 +224,9 @@ private void resetLinePosition() {
public PrinterOutputBuilder append(String str) {
// Avoid too many blank lines
trailingLineSeparators = str.equals(LINE_SEPARATOR) ? trailingLineSeparators + 1 : 0;
- if (trailingLineSeparators >= 3)
+ if (trailingLineSeparators >= 3) {
return this;
+ }
builder.append(str);
columnIndex += str.length();
diff --git a/language-api/src/main/java/de/jplag/semantics/CodeSemantics.java b/language-api/src/main/java/de/jplag/semantics/CodeSemantics.java
index 2ef9f0883..2eb99262d 100644
--- a/language-api/src/main/java/de/jplag/semantics/CodeSemantics.java
+++ b/language-api/src/main/java/de/jplag/semantics/CodeSemantics.java
@@ -14,8 +14,8 @@ public class CodeSemantics {
private boolean keep;
private PositionSignificance positionSignificance;
private final int bidirectionalBlockDepthChange;
- private Set reads;
- private Set writes;
+ private final Set reads;
+ private final Set writes;
/**
* Creates new semantics. reads and writes, which each contain the variables which were (potentially) read from/written
@@ -196,16 +196,21 @@ public static CodeSemantics join(List semanticsList) {
@Override
public String toString() {
List properties = new LinkedList<>();
- if (keep)
+ if (keep) {
properties.add("keep");
- if (positionSignificance != PositionSignificance.NONE)
+ }
+ if (positionSignificance != PositionSignificance.NONE) {
properties.add(positionSignificance.name().toLowerCase() + " position significance");
- if (bidirectionalBlockDepthChange != 0)
+ }
+ if (bidirectionalBlockDepthChange != 0) {
properties.add("change bidirectional block depth by " + bidirectionalBlockDepthChange);
- if (!reads.isEmpty())
+ }
+ if (!reads.isEmpty()) {
properties.add("read " + String.join(" ", reads.stream().map(Variable::toString).toList()));
- if (!writes.isEmpty())
+ }
+ if (!writes.isEmpty()) {
properties.add("write " + String.join(" ", writes.stream().map(Variable::toString).toList()));
+ }
return String.join(", ", properties);
}
}
\ No newline at end of file
diff --git a/language-api/src/main/java/de/jplag/semantics/VariableRegistry.java b/language-api/src/main/java/de/jplag/semantics/VariableRegistry.java
index 82b48f325..b187db34c 100644
--- a/language-api/src/main/java/de/jplag/semantics/VariableRegistry.java
+++ b/language-api/src/main/java/de/jplag/semantics/VariableRegistry.java
@@ -17,10 +17,10 @@ public class VariableRegistry {
private static final Logger logger = LoggerFactory.getLogger(VariableRegistry.class);
private CodeSemantics semantics;
- private Map fileVariables;
- private Deque