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 ef86fc01c..b4ea875e3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -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 47a19c627..7ca077d13 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,8 @@ 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! diff --git a/cli/pom.xml b/cli/pom.xml index f87b354d1..459daf3eb 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -7,6 +7,7 @@ ${revision} cli + @@ -167,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 457d99ade..ec72e3e1d 100644 --- a/cli/src/main/java/de/jplag/cli/CLI.java +++ b/cli/src/main/java/de/jplag/cli/CLI.java @@ -4,8 +4,11 @@ import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_OPTION_LIST; import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_SYNOPSIS; +import java.awt.*; 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; @@ -23,6 +26,7 @@ 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; @@ -81,15 +85,14 @@ 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); ProgressBarLogger.setProgressBarProvider(new TongfeiProgressBarProvider()); - JPlagResult result = JPlag.run(options); - ReportObjectFactory reportObjectFactory = new ReportObjectFactory(new File(cli.getResultFilePath())); - reportObjectFactory.createAndSaveReport(result); - - OutputFileGenerator.generateCsvOutput(result, new File(cli.getResultFileBaseName()), cli.options); + 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 | FileNotFoundException exception) { // do not pass exceptions here to keep log clean + } 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 { @@ -124,6 +127,27 @@ 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); + 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()); diff --git a/cli/src/main/java/de/jplag/cli/CliOptions.java b/cli/src/main/java/de/jplag/cli/CliOptions.java index 1e5d7623c..f66c18967 100644 --- a/cli/src/main/java/de/jplag/cli/CliOptions.java +++ b/cli/src/main/java/de/jplag/cli/CliOptions.java @@ -20,26 +20,26 @@ 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") + "-new"}, split = ",", description = "Root-directory with submissions to check for plagiarism (same as the root directory)") 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-directory 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 to parse the submissions (default: ${DEFAULT-VALUE}). The language names are the same as the subcommands.") 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") + "--base-code"}, description = "Path of the directory containing the base code (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") + + "as a matching section. A smaller 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") @@ -47,20 +47,24 @@ public class CliOptions implements Runnable { @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") + + "to -1 all comparisons will be shown (default: ${DEFAULT-VALUE})") public int shownComparisons = JPlagOptions.DEFAULT_SHOWN_COMPARISONS; @Option(names = {"-r", - "--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.%n") + "--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 = { + "--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; + + @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 = "%nMerging of neighboring matches to increase the similarity of concealed plagiarism:%n") public Merging merging = new Merging(); @Option(names = {"--normalize"}, description = "Activate the normalization of tokens. Supported for languages: Java, C++.") @@ -75,30 +79,33 @@ 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 = "Debug parser. Non-parsable files will be stored (default: ${DEFAULT-VALUE})") 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") + + "be saved (default: ${DEFAULT-VALUE})") public double similarityThreshold = JPlagOptions.DEFAULT_SIMILARITY_THRESHOLD; + @Option(names = {"-P", "--port"}, description = "The port used for the internal report viewer.") + public int port = 1996; + @Option(names = "--csv-export", description = "If present, a csv export will be generated in addition to the zip 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 clustering (default: ${DEFAULT-VALUE})") public boolean disable; @ArgGroup @@ -109,25 +116,25 @@ public static class ClusteringEnabled { "--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") + + "automatically. (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") + + "attempts of obfuscation. (default: ${DEFAULT-VALUE})") public SimilarityMetric metric = new ClusteringOptions().similarityMetric(); } } public static class Merging { - @Option(names = {"--match-merging"}, description = "Enables match merging (default: ${DEFAULT-VALUE})%n") + @Option(names = {"--match-merging"}, description = "Enables match merging (default: ${DEFAULT-VALUE})") public boolean enabled = MergingOptions.DEFAULT_ENABLED; - @Option(names = {"--neighbor-length"}, description = "Defines how short a match can be, to be considered (default: ${DEFAULT-VALUE})%n") + @Option(names = {"--neighbor-length"}, description = "Defines how short a match can be, to be considered (default: ${DEFAULT-VALUE})") public int minimumNeighborLength = MergingOptions.DEFAULT_NEIGHBOR_LENGTH; @Option(names = { - "--gap-size"}, description = "Defines how many token there can be between two neighboring matches (default: ${DEFAULT-VALUE})%n") + "--gap-size"}, description = "Defines how many token there can be between two neighboring matches (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/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..c43cf4c1b --- /dev/null +++ b/cli/src/main/java/de/jplag/cli/server/ReportViewer.java @@ -0,0 +1,132 @@ +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 + */ + 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..297faf3a7 --- /dev/null +++ b/cli/src/main/java/de/jplag/cli/server/ResponseData.java @@ -0,0 +1,47 @@ +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)); + } else { + 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..4b9a660bc --- /dev/null +++ b/cli/src/main/java/de/jplag/cli/server/RoutingStaticFile.java @@ -0,0 +1,43 @@ +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); + } else { + 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..c61647a22 --- /dev/null +++ b/cli/src/main/java/de/jplag/cli/server/RoutingTree.java @@ -0,0 +1,90 @@ +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); + } else { + 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/ReportViewerTest.java b/cli/src/test/java/de/jplag/cli/ReportViewerTest.java new file mode 100644 index 000000000..8abc87cd0 --- /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.*; +import java.net.URI; +import java.util.concurrent.TimeUnit; + +import javax.swing.*; + +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..35a0cfaaf --- /dev/null +++ b/cli/src/test/java/de/jplag/cli/server/RoutingFallbackTest.java @@ -0,0 +1,43 @@ +package de.jplag.cli.server; + +import static org.junit.jupiter.api.Assertions.*; + +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..b9730d933 --- /dev/null +++ b/cli/src/test/java/de/jplag/cli/server/RoutingPathTest.java @@ -0,0 +1,43 @@ +package de.jplag.cli.server; + +import static org.junit.jupiter.api.Assertions.*; + +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..3e4b4f501 --- /dev/null +++ b/cli/src/test/java/de/jplag/cli/server/RoutingResourcesTest.java @@ -0,0 +1,19 @@ +package de.jplag.cli.server; + +import static org.junit.jupiter.api.Assertions.*; + +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..4626e7a48 --- /dev/null +++ b/cli/src/test/java/de/jplag/cli/server/RoutingStaticFileTest.java @@ -0,0 +1,42 @@ +package de.jplag.cli.server; + +import static org.junit.jupiter.api.Assertions.*; + +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/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/report-viewer/src/components/ComparisonsTable.vue b/report-viewer/src/components/ComparisonsTable.vue index 5fd1492d0..f5607b316 100644 --- a/report-viewer/src/components/ComparisonsTable.vue +++ b/report-viewer/src/components/ComparisonsTable.vue @@ -76,13 +76,16 @@ '!bg-accent !bg-opacity-30 ': isHighlightedRow(item) }" > -
@@ -105,7 +108,7 @@ {{ (item.similarities[MetricType.MAXIMUM] * 100).toFixed(2) }}%
- +
@@ -170,7 +173,6 @@ import { generateColors } from '@/utils/ColorUtils' import ToolTipComponent from './ToolTipComponent.vue' import { MetricType, metricToolTips } from '@/model/MetricType' import NameElement from './NameElement.vue' -import { router } from '@/router' import ComparisonTableFilter from './ComparisonTableFilter.vue' library.add(faUserGroup) diff --git a/report-viewer/src/components/NameElement.vue b/report-viewer/src/components/NameElement.vue index a3445bf49..52a89dcfc 100644 --- a/report-viewer/src/components/NameElement.vue +++ b/report-viewer/src/components/NameElement.vue @@ -33,6 +33,7 @@ const props = defineProps({ function changeAnonymous(event: Event) { event.stopPropagation() + event.preventDefault() if (store().isAnonymous(props.id)) { store().removeAnonymous([props.id]) } else { diff --git a/report-viewer/src/model/factories/BaseFactory.ts b/report-viewer/src/model/factories/BaseFactory.ts index 7950745af..4a01d60c9 100644 --- a/report-viewer/src/model/factories/BaseFactory.ts +++ b/report-viewer/src/model/factories/BaseFactory.ts @@ -5,6 +5,8 @@ import { ZipFileHandler } from '@/model/fileHandling/ZipFileHandler' * This class provides some basic functionality for the factories. */ export class BaseFactory { + public static zipFileName = 'results.zip' + /** * Returns the content of a file through the stored loading type. * @param path - Path to the file @@ -17,16 +19,15 @@ export class BaseFactory { return this.getFileFromStore(path) } if (store().state.localModeUsed) { - if (store().state.zipModeUsed) { - await new ZipFileHandler().handleFile(await this.getLocalFile('results.zip')) - return this.getFileFromStore(path) - } else { - return await (await this.getLocalFile(`/files/${path}`)).text() - } + return await (await this.getLocalFile(`/files/${path}`)).text() } else if (store().state.zipModeUsed) { return this.getFileFromStore(path) } else if (store().state.singleModeUsed) { return store().state.singleFillRawContent + } else if (await this.useLocalZipMode()) { + await new ZipFileHandler().handleFile(await this.getLocalFile(this.zipFileName)) + store().setLoadingType('zip') + return this.getFileFromStore(path) } throw new Error('No loading type specified') } @@ -49,12 +50,26 @@ export class BaseFactory { * @return Content of the file * @throws Error if the file could not be found */ - protected static async getLocalFile(path: string): Promise { + public static async getLocalFile(path: string): Promise { const request = await fetch(`${window.location.origin}${import.meta.env.BASE_URL}${path}`) if (request.status == 200) { - return request.blob() + const blob = await request.blob() + // Check that file is not the index.html + if (blob.type == 'text/html') { + throw new Error(`Could not find ${path} in local files.`) + } + return blob } else { throw new Error(`Could not find ${path} in local files.`) } } + + public static async useLocalZipMode() { + try { + await this.getLocalFile(this.zipFileName) + return true + } catch (e) { + return false + } + } } diff --git a/report-viewer/src/model/factories/ComparisonFactory.ts b/report-viewer/src/model/factories/ComparisonFactory.ts index 1ba0eb20c..d6257e93e 100644 --- a/report-viewer/src/model/factories/ComparisonFactory.ts +++ b/report-viewer/src/model/factories/ComparisonFactory.ts @@ -10,13 +10,8 @@ import { MetricType } from '../MetricType' * Factory class for creating Comparison objects */ export class ComparisonFactory extends BaseFactory { - public static async getComparison(id1: string, id2: string): Promise { - const filePath = store().getComparisonFileName(id1, id2) - if (!filePath) { - throw new Error('Comparison file not specified') - } - - return await this.extractComparison(JSON.parse(await this.getFile(filePath))) + public static async getComparison(fileName: string): Promise { + return await this.extractComparison(JSON.parse(await this.getFile(fileName))) } /** diff --git a/report-viewer/src/router/index.ts b/report-viewer/src/router/index.ts index 48afa3ffe..16bcd5868 100644 --- a/report-viewer/src/router/index.ts +++ b/report-viewer/src/router/index.ts @@ -23,7 +23,7 @@ const router = createRouter({ component: OverviewViewWrapper }, { - path: '/comparison/:firstId/:secondId', + path: '/comparison/:comparisonFileName', name: 'ComparisonView', component: ComparisonViewWrapper, props: true diff --git a/report-viewer/src/stores/store.ts b/report-viewer/src/stores/store.ts index a15e3082c..9829d33d4 100644 --- a/report-viewer/src/stores/store.ts +++ b/report-viewer/src/stores/store.ts @@ -1,5 +1,5 @@ import { defineStore } from 'pinia' -import type { LoadConfiguration, State, UIState } from './state' +import type { State, UIState } from './state' import { MetricType } from '@/model/MetricType' import type { SubmissionFile, File } from '@/model/File' @@ -198,10 +198,10 @@ const store = defineStore('store', { * Sets the loading type * @param payload Type used to input JPlag results */ - setLoadingType(payload: LoadConfiguration) { - this.state.localModeUsed = payload.local - this.state.zipModeUsed = payload.zip - this.state.singleModeUsed = payload.single + setLoadingType(loadingType: 'zip' | 'local' | 'single') { + this.state.localModeUsed = loadingType == 'local' + this.state.zipModeUsed = loadingType == 'zip' + this.state.singleModeUsed = loadingType == 'single' }, /** * Sets the raw content of the single file mode diff --git a/report-viewer/src/viewWrapper/ComparisonViewWrapper.vue b/report-viewer/src/viewWrapper/ComparisonViewWrapper.vue index bd169c14c..59be445e1 100644 --- a/report-viewer/src/viewWrapper/ComparisonViewWrapper.vue +++ b/report-viewer/src/viewWrapper/ComparisonViewWrapper.vue @@ -1,11 +1,5 @@