Kaynağa Gözat

Merge pull request #535 from nestabentum/optimize_reportViewer_dto_size_new_ready

Optimize report viewer dto size new
Timur Sağlam 4 yıl önce
ebeveyn
işleme
8d28e953df
23 değiştirilmiş dosya ile 382 ekleme ve 408 silme
  1. 3 5
      jplag.cli/src/main/java/de/jplag/CLI.java
  2. 9 3
      jplag/src/main/java/de/jplag/JPlagResult.java
  3. 0 36
      jplag/src/main/java/de/jplag/reporting/JsonReport.java
  4. 0 34
      jplag/src/main/java/de/jplag/reporting/Report.java
  5. 29 0
      jplag/src/main/java/de/jplag/reporting/jsonfactory/FileWriter.java
  6. 0 62
      jplag/src/main/java/de/jplag/reporting/jsonfactory/JsonFactory.java
  7. 59 86
      jplag/src/main/java/de/jplag/reporting/reportobject/ReportObjectFactory.java
  8. 56 0
      jplag/src/main/java/de/jplag/reporting/reportobject/mapper/ComparisonReportMapper.java
  9. 5 5
      jplag/src/main/java/de/jplag/reporting/reportobject/mapper/MetricMapper.java
  10. 9 13
      jplag/src/main/java/de/jplag/reporting/reportobject/model/ComparisonReport.java
  11. 0 28
      jplag/src/main/java/de/jplag/reporting/reportobject/model/FilesOfSubmission.java
  12. 0 10
      jplag/src/main/java/de/jplag/reporting/reportobject/model/JPlagReport.java
  13. 3 3
      jplag/src/main/java/de/jplag/reporting/reportobject/model/Match.java
  14. 2 1
      report-viewer/package.json
  15. 1 1
      report-viewer/src/components/ClusterRadarChart.vue
  16. 40 21
      report-viewer/src/components/ComparisonsTable.vue
  17. 63 47
      report-viewer/src/components/DistributionDiagram.vue
  18. 28 20
      report-viewer/src/model/factories/ComparisonFactory.ts
  19. 1 1
      report-viewer/src/model/factories/OverviewFactory.ts
  20. 23 0
      report-viewer/src/store/store.ts
  21. 18 8
      report-viewer/src/views/FileUploadView.vue
  22. 19 21
      report-viewer/src/views/OverviewView.vue
  23. 14 3
      report-viewer/vue.config.js

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

@@ -27,8 +27,7 @@ import de.jplag.options.JPlagOptions;
 import de.jplag.options.LanguageOption;
 import de.jplag.options.SimilarityMetric;
 import de.jplag.options.Verbosity;
-import de.jplag.reporting.JsonReport;
-import de.jplag.reporting.Report;
+import de.jplag.reporting.reportobject.ReportObjectFactory;
 import de.jplag.strategy.ComparisonMode;
 
 /**
@@ -65,9 +64,8 @@ public final class CLI {
             JPlag program = new JPlag(options);
             logger.info("JPlag initialized");
             JPlagResult result = program.run();
-            Report report = new JsonReport();
-            report.saveReport(result, arguments.getString(RESULT_FOLDER.flagWithoutDash()));
-            finalizeLogger();
+            ReportObjectFactory.createAndSaveReport(result, arguments.getString(RESULT_FOLDER.flagWithoutDash()));
+
         } catch (ExitException exception) {
             logger.error(exception.getMessage(), exception);
             finalizeLogger();

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

@@ -136,9 +136,15 @@ public class JPlagResult {
 
     private int[] calculateDistributionFor(List<JPlagComparison> comparisons, Function<JPlagComparison, Float> similarityExtractor) {
         int[] similarityDistribution = new int[SIMILARITY_DISTRIBUTION_SIZE];
-        comparisons.stream().map(similarityExtractor).map(percent -> percent / SIMILARITY_DISTRIBUTION_SIZE).map(Float::intValue)
-                .map(index -> index == SIMILARITY_DISTRIBUTION_SIZE ? SIMILARITY_DISTRIBUTION_SIZE - 1 : index)
-                .forEach(index -> similarityDistribution[SIMILARITY_DISTRIBUTION_SIZE - 1 - index]++);
+        int similarityDistributionBucketSize = 100 / SIMILARITY_DISTRIBUTION_SIZE;
+        for (JPlagComparison comparison : comparisons) {
+            float similarity = similarityExtractor.apply(comparison); // extract similarity in percent: 0f <= similarity <= 100f
+            int index = (int) (similarity / similarityDistributionBucketSize); // divide similarity by bucket size to find index of correct bucket.
+            index = Math.min(index, SIMILARITY_DISTRIBUTION_SIZE - 1);// index is out of bounds when similarity is 100%. decrease by one to count
+                                                                      // towards the highest value bucket
+            similarityDistribution[SIMILARITY_DISTRIBUTION_SIZE - 1 - index]++; // count comparison towards its determined bucket. bucket order is
+            // reversed, so that the highest value bucket has the lowest index
+        }
         return similarityDistribution;
     }
 }

+ 0 - 36
jplag/src/main/java/de/jplag/reporting/JsonReport.java

@@ -1,36 +0,0 @@
-package de.jplag.reporting;
-
-import java.io.File;
-import java.util.List;
-
-import de.jplag.JPlagResult;
-import de.jplag.reporting.jsonfactory.JsonFactory;
-import de.jplag.reporting.reportobject.ReportObjectFactory;
-import de.jplag.reporting.reportobject.model.JPlagReport;
-
-// ReportImplementation -> JsonReport
-
-/**
- * A report generator which reports the JPlagResult in Json format.
- */
-public class JsonReport implements Report {
-
-    @Override
-    public List<String> getReportStrings(JPlagResult result) {
-        JPlagReport report = ReportObjectFactory.getReportObject(result);
-        return JsonFactory.getJsonStrings(report);
-    }
-
-    @Override
-    public boolean saveReport(JPlagResult result, String path) {
-        JPlagReport report = ReportObjectFactory.getReportObject(result);
-        File directory = new File(path);
-        if (!directory.exists()) {
-            if (!directory.mkdirs()) {
-                logger.error("Failed to create dir.");
-            }
-        }
-        return JsonFactory.saveJsonFiles(report, path);
-    }
-
-}

+ 0 - 34
jplag/src/main/java/de/jplag/reporting/Report.java

@@ -1,34 +0,0 @@
-package de.jplag.reporting;
-
-import java.util.List;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import de.jplag.JPlagResult;
-
-// ReportStrategy -> Report
-
-/**
- * Strategy interface for reporting. A report generator should implement this interface.
- */
-public interface Report {
-
-    Logger logger = LoggerFactory.getLogger(Report.class);
-
-    /**
-     * This function returns a list containing the report objects as simple strings. The first element is the string of the
-     * overview object. Each following element is a string of a comparison report object.
-     * @param result The result of a JPlag comparison
-     * @return A list containing report objects to string. First element is Overview. Other elements are comparisons.
-     */
-    List<String> getReportStrings(JPlagResult result);
-
-    /**
-     * Creates and saves JPlag report files to the disk.
-     * @param result The result of a JPlag comparison.
-     * @param path Path to the directory where the report should be saved.
-     * @return True if the process is successful, otherwise false.
-     */
-    boolean saveReport(JPlagResult result, String path);
-}

+ 29 - 0
jplag/src/main/java/de/jplag/reporting/jsonfactory/FileWriter.java

@@ -0,0 +1,29 @@
+package de.jplag.reporting.jsonfactory;
+
+import java.io.IOException;
+import java.nio.file.Path;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class FileWriter {
+    private static final Logger logger = LoggerFactory.getLogger(FileWriter.class);
+
+    private static final ObjectMapper objectMapper = new ObjectMapper();
+
+    /**
+     * Saves the provided object to the provided path under the provided name
+     * @param fileToSave The object to save
+     * @param folderPath The path to save the object to
+     * @param fileName The name to save the object under
+     */
+    public void saveAsJSON(Object fileToSave, String folderPath, String fileName) {
+        try {
+            objectMapper.writeValue(Path.of(folderPath, fileName).toFile(), fileToSave);
+        } catch (IOException e) {
+            logger.error("Failed to save json file " + fileName + ": " + e.getMessage(), e);
+        }
+    }
+}

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

@@ -1,62 +0,0 @@
-package de.jplag.reporting.jsonfactory;
-
-import java.io.IOException;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.List;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import de.jplag.reporting.reportobject.model.ComparisonReport;
-import de.jplag.reporting.reportobject.model.JPlagReport;
-
-/**
- * Factory class, responsible for creating Json strings and writing them to files.
- */
-public class JsonFactory {
-
-    private static final Logger logger = LoggerFactory.getLogger(JsonFactory.class);
-
-    private static final ObjectMapper mapper = new ObjectMapper();
-
-    /**
-     * Uses Jackson to create Json Strings from JPlagReport object.
-     * @return A list, first element is Json String of Overview object. The rest elements are Json Strings of Comparison
-     * objects.
-     */
-    public static List<String> getJsonStrings(JPlagReport jPlagReport) {
-        List<String> jsonReports = new ArrayList<>();
-        try {
-            jsonReports.add(mapper.writeValueAsString(jPlagReport.overviewReport()));
-            for (ComparisonReport comparisonReport : jPlagReport.comparisons()) {
-                jsonReports.add(mapper.writeValueAsString(comparisonReport));
-            }
-        } catch (JsonProcessingException e) {
-            logger.error("Error converting object to json " + e.getMessage(), e);
-        }
-        return jsonReports;
-    }
-
-    /**
-     * Creates Json Files for the given JPlagReport and saves them in the given folder.
-     * @return A boolean, representing whether the process was successful.
-     */
-    public static boolean saveJsonFiles(JPlagReport jPlagReport, String folderPath) {
-        try {
-            ObjectMapper mapper = new ObjectMapper();
-            mapper.writeValue(Path.of(folderPath, "overview.json").toFile(), jPlagReport.overviewReport());
-            for (ComparisonReport report : jPlagReport.comparisons()) {
-                String name = report.firstSubmissionId().concat("-").concat(report.secondSubmissionId()).concat(".json");
-                mapper.writeValue(Path.of(folderPath, name).toFile(), report);
-            }
-        } catch (IOException e) {
-            logger.error("Failed to save json files: " + e.getMessage(), e);
-            return false;
-        }
-        return true;
-    }
-}

+ 59 - 86
jplag/src/main/java/de/jplag/reporting/reportobject/ReportObjectFactory.java

@@ -1,47 +1,63 @@
 package de.jplag.reporting.reportobject;
 
-import java.io.BufferedReader;
 import java.io.File;
-import java.io.FileReader;
 import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
 import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.HashSet;
-import java.util.List;
+import java.util.*;
+import java.util.stream.Collectors;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import de.jplag.*;
+import de.jplag.JPlagComparison;
+import de.jplag.JPlagResult;
+import de.jplag.Submission;
+import de.jplag.reporting.jsonfactory.FileWriter;
 import de.jplag.reporting.reportobject.mapper.ClusteringResultMapper;
+import de.jplag.reporting.reportobject.mapper.ComparisonReportMapper;
 import de.jplag.reporting.reportobject.mapper.MetricMapper;
-import de.jplag.reporting.reportobject.model.*;
-import de.jplag.reporting.reportobject.model.Match;
+import de.jplag.reporting.reportobject.model.Metric;
+import de.jplag.reporting.reportobject.model.OverviewReport;
 
 /**
  * Factory class, responsible for converting a JPlagResult object to Overview and Comparison DTO classes.
  */
 public class ReportObjectFactory {
-
     private static final Logger logger = LoggerFactory.getLogger(ReportObjectFactory.class);
+
     private static final ClusteringResultMapper clusteringResultMapper = new ClusteringResultMapper();
     private static final MetricMapper metricMapper = new MetricMapper();
+    private static final ComparisonReportMapper comparisonReportMapper = new ComparisonReportMapper();
+    private static final FileWriter fileWriter = new FileWriter();
+    public static final String OVERVIEW_FILE_NAME = "overview.json";
+    public static final String SUBMISSIONS_FOLDER = "submissions";
 
     /**
-     * Converts a JPlagResult to a JPlagReport.
-     * @return JPlagReport for the given JPlagResult.
+     * @param result The JPlagResult to be converted into a report.
+     * @param path The Path to save the report to
      */
-    public static JPlagReport getReportObject(JPlagResult result) {
-        OverviewReport overviewReport = generateOverviewReport(result);
-        List<ComparisonReport> comparisons = generateComparisonReports(result);
-        return new JPlagReport(overviewReport, comparisons);
+    public static void createAndSaveReport(JPlagResult result, String path) {
+        createDirectory(path);
+        writeOverview(result, path);
+        copySubmissionFilesToReport(path, result);
+        comparisonReportMapper.writeComparisonReports(result, path);
     }
 
-    /**
-     * Generates an Overview DTO of a JPlagResult.
-     */
-    private static OverviewReport generateOverviewReport(JPlagResult result) {
+    private static File createDirectory(String path, String name) {
+        File directory = new File(path.concat("/").concat(name));
+        if (!directory.exists() && !directory.mkdirs()) {
+            logger.error("Failed to create dir.");
+        }
+        return directory;
+    }
+
+    private static void createDirectory(String path) {
+        createDirectory(path, "");
+    }
+
+    private static void writeOverview(JPlagResult result, String path) {
         List<JPlagComparison> comparisons = getComparisons(result);
         OverviewReport overviewReport = new OverviewReport();
 
@@ -66,24 +82,31 @@ public class ReportObjectFactory {
         overviewReport.setMetrics(getMetrics(result));
         overviewReport.setClusters(clusteringResultMapper.map(result));
 
-        return overviewReport;
+        fileWriter.saveAsJSON(overviewReport, path, OVERVIEW_FILE_NAME);
+
     }
 
-    /**
-     * Generates detailed ComparisonReport DTO for each comparison in a JPlagResult.
-     * @return A list with ComparisonReport DTOs.
-     */
-    private static List<ComparisonReport> generateComparisonReports(JPlagResult result) {
-        List<ComparisonReport> comparisons = new ArrayList<>();
-        getComparisons(result).forEach(comparison -> comparisons.add( //
-                new ComparisonReport(comparison.getFirstSubmission().getName(), //
-                        comparison.getSecondSubmission().getName(), //
-                        comparison.similarity(), //
-                        getFilesForSubmission(comparison.getFirstSubmission()), //
-                        getFilesForSubmission(comparison.getSecondSubmission()), //
-                        convertMatchesToReportMatches(result, comparison, comparison.getMatches()) //
-                )));
-        return comparisons;
+    private static void copySubmissionFilesToReport(String path, JPlagResult result) {
+        List<JPlagComparison> comparisons = result.getComparisons(result.getOptions().getMaximumNumberOfComparisons());
+        var submissions = getSubmissions(comparisons);
+        var submissionsPath = createDirectory(path, SUBMISSIONS_FOLDER);
+        for (var submission : submissions) {
+            File directory = createDirectory(submissionsPath.getPath(), submission.getName());
+            for (var file : submission.getFiles()) {
+                try {
+                    Files.copy(file.toPath(), (new File(directory, file.getName())).toPath(), StandardCopyOption.REPLACE_EXISTING);
+                } catch (IOException e) {
+                    logger.error("Could not save submission file " + file, e);
+                }
+            }
+        }
+    }
+
+    private static Set<Submission> getSubmissions(List<JPlagComparison> comparisons) {
+        var submissions = comparisons.stream().map(JPlagComparison::getFirstSubmission).collect(Collectors.toSet());
+        Set<Submission> secondSubmissions = comparisons.stream().map(JPlagComparison::getSecondSubmission).collect(Collectors.toSet());
+        submissions.addAll(secondSubmissions);
+        return submissions;
     }
 
     private static List<JPlagComparison> getComparisons(JPlagResult result) {
@@ -91,11 +114,6 @@ public class ReportObjectFactory {
         return result.getComparisons(numberOfComparisons);
     }
 
-    private static List<Match> convertMatchesToReportMatches(JPlagResult result, JPlagComparison comparison, List<de.jplag.Match> matches) {
-        return matches.stream().map(match -> convertMatchToReportMatch(comparison, match, !result.getOptions().getLanguage().supportsColumns()))
-                .toList();
-    }
-
     /**
      * Gets the names of all submissions.
      * @return A list containing all submission names.
@@ -129,51 +147,6 @@ public class ReportObjectFactory {
         return List.of(metricMapper.getAverageMetric(result), metricMapper.getMaxMetric(result));
     }
 
-    /**
-     * Converts files of a submission to FilesOFSubmission DTO.
-     * @return A list containing FilesOfSubmission DTOs.
-     */
-    private static List<FilesOfSubmission> getFilesForSubmission(Submission submission) {
-        return submission.getFiles().stream().map(file -> new FilesOfSubmission(file.getName(), readFileLines(file))).toList();
-    }
-
-    /**
-     * Converts a JPlag Match object to a Match DTO.
-     * @param comparison The comparison from which the match originates.
-     * @param match The match to be converted.
-     * @param usesIndex Indicates whether the language uses indexes.
-     * @return A Match DTO.
-     */
-    private static Match convertMatchToReportMatch(JPlagComparison comparison, de.jplag.Match match, boolean usesIndex) {
-        TokenList tokensFirst = comparison.getFirstSubmission().getTokenList();
-        TokenList tokensSecond = comparison.getSecondSubmission().getTokenList();
-        Token startTokenFirst = tokensFirst.getToken(match.startOfFirst());
-        Token endTokenFirst = tokensFirst.getToken(match.startOfFirst() + match.length() - 1);
-        Token startTokenSecond = tokensSecond.getToken(match.startOfSecond());
-        Token endTokenSecond = tokensSecond.getToken(match.startOfSecond() + match.length() - 1);
-
-        int startFirst = usesIndex ? startTokenFirst.getIndex() : startTokenFirst.getLine();
-        int endFirst = usesIndex ? endTokenFirst.getIndex() : endTokenFirst.getLine();
-        int startSecond = usesIndex ? startTokenSecond.getIndex() : startTokenSecond.getLine();
-        int endSecond = usesIndex ? endTokenSecond.getIndex() : endTokenSecond.getLine();
-        int tokens = match.length();
-
-        return new Match(startTokenFirst.getFile(), startTokenSecond.getFile(), startFirst, endFirst, startSecond, endSecond, tokens);
-    }
-
-    private static List<String> readFileLines(File file) {
-        List<String> lines = new ArrayList<>();
-        try (BufferedReader bufferedReader = new BufferedReader(new FileReader(file))) {
-            String line;
-            while ((line = bufferedReader.readLine()) != null) {
-                lines.add(line);
-            }
-        } catch (IOException exception) {
-            logger.error("Could not read file: " + exception.getMessage(), exception);
-        }
-        return lines;
-    }
-
     private static String getDate() {
         SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yy");
         Date date = new Date();

+ 56 - 0
jplag/src/main/java/de/jplag/reporting/reportobject/mapper/ComparisonReportMapper.java

@@ -0,0 +1,56 @@
+package de.jplag.reporting.reportobject.mapper;
+
+import java.util.List;
+
+import de.jplag.*;
+import de.jplag.reporting.jsonfactory.FileWriter;
+import de.jplag.reporting.reportobject.model.ComparisonReport;
+import de.jplag.reporting.reportobject.model.Match;
+
+public class ComparisonReportMapper {
+
+    private static final FileWriter FILE_WRITER = new FileWriter();
+
+    /**
+     * Generates detailed ComparisonReport DTO for each comparison in a JPlagResult.
+     * @param jPlagResult The JPlagResult to generate the comparison reports from. contains information about a comparison
+     * between two submission. The JPlagResult is used to extract the information on matches between two submissions.
+     */
+    public void writeComparisonReports(JPlagResult jPlagResult, String path) {
+        int numberOfComparisons = jPlagResult.getOptions().getMaximumNumberOfComparisons();
+        List<JPlagComparison> comparisons = jPlagResult.getComparisons(numberOfComparisons);
+        writeComparisons(jPlagResult, path, comparisons);
+    }
+
+    private void writeComparisons(JPlagResult jPlagResult, String path, List<JPlagComparison> comparisons) {
+        for (JPlagComparison comparison : comparisons) {
+            var comparisonReport = new ComparisonReport(comparison.getFirstSubmission().getName(), comparison.getSecondSubmission().getName(),
+                    comparison.similarity(), convertMatchesToReportMatches(jPlagResult, comparison));
+            String fileName = comparisonReport.firstSubmissionId().concat("-").concat(comparisonReport.secondSubmissionId()).concat(".json");
+            FILE_WRITER.saveAsJSON(comparisonReport, path, fileName);
+        }
+    }
+
+    private List<Match> convertMatchesToReportMatches(JPlagResult result, JPlagComparison comparison) {
+        return comparison.getMatches().stream()
+                .map(match -> convertMatchToReportMatch(comparison, match, result.getOptions().getLanguage().supportsColumns())).toList();
+    }
+
+    private Match convertMatchToReportMatch(JPlagComparison comparison, de.jplag.Match match, boolean usesIndex) {
+        TokenList tokensFirst = comparison.getFirstSubmission().getTokenList();
+        TokenList tokensSecond = comparison.getSecondSubmission().getTokenList();
+        Token startTokenFirst = tokensFirst.getToken(match.startOfFirst());
+        Token endTokenFirst = tokensFirst.getToken(match.startOfFirst() + match.length() - 1);
+        Token startTokenSecond = tokensSecond.getToken(match.startOfSecond());
+        Token endTokenSecond = tokensSecond.getToken(match.startOfSecond() + match.length() - 1);
+
+        int startFirst = usesIndex ? startTokenFirst.getIndex() : startTokenFirst.getLine();
+        int endFirst = usesIndex ? endTokenFirst.getIndex() : endTokenFirst.getLine();
+        int startSecond = usesIndex ? startTokenSecond.getIndex() : startTokenSecond.getLine();
+        int endSecond = usesIndex ? endTokenSecond.getIndex() : endTokenSecond.getLine();
+        int tokens = match.length();
+
+        return new Match(startTokenFirst.getFile(), startTokenSecond.getFile(), startFirst, endFirst, startSecond, endSecond, tokens);
+    }
+
+}

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

@@ -1,7 +1,7 @@
 package de.jplag.reporting.reportobject.mapper;
 
-import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Comparator;
 import java.util.List;
 import java.util.function.Function;
 import java.util.stream.Collectors;
@@ -37,10 +37,10 @@ public class MetricMapper {
     }
 
     private static List<TopComparison> getTopComparisons(List<JPlagComparison> comparisons, Function<JPlagComparison, Float> similarityExtractor) {
-        List<TopComparison> topComparisons = new ArrayList<>();
-        comparisons.forEach(comparison -> topComparisons.add(new TopComparison(comparison.getFirstSubmission().getName(),
-                comparison.getSecondSubmission().getName(), similarityExtractor.apply(comparison))));
-        return topComparisons;
+        return comparisons.stream().sorted(Comparator.comparing(similarityExtractor).reversed())
+                .map(comparison -> new TopComparison(comparison.getFirstSubmission().getName(), comparison.getSecondSubmission().getName(),
+                        similarityExtractor.apply(comparison)))
+                .toList();
     }
 
     private static List<TopComparison> getTopComparisons(List<JPlagComparison> comparisons) {

+ 9 - 13
jplag/src/main/java/de/jplag/reporting/reportobject/model/ComparisonReport.java

@@ -4,18 +4,14 @@ import java.util.List;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
 
-public record ComparisonReport(@JsonProperty("first_submission_id") String firstSubmissionId,
-        @JsonProperty("second_submission_id") String secondSubmissionId, @JsonProperty("match_percentage") float matchPercentage,
-        @JsonProperty("files_of_first_submission") List<FilesOfSubmission> filesOfFirstSubmission,
-        @JsonProperty("files_of_second_submission") List<FilesOfSubmission> filesOfSecondSubmission, @JsonProperty("matches") List<Match> matches) {
-    public ComparisonReport(String firstSubmissionId, String secondSubmissionId, float matchPercentage,
-            List<FilesOfSubmission> filesOfFirstSubmission, List<FilesOfSubmission> filesOfSecondSubmission, List<Match> matches) {
+/**
+ * ReportViewer DTO for the comparison of two submissions.
+ * @param firstSubmissionId id of the first submission
+ * @param secondSubmissionId id of the second submission
+ * @param matchPercentage similarity in percent. between 0f and 100f.
+ * @param matches the list of matches found in the comparison of the two submissions
+ */
+public record ComparisonReport(@JsonProperty("id1") String firstSubmissionId, @JsonProperty("id2") String secondSubmissionId,
+        @JsonProperty("similarity") float matchPercentage, @JsonProperty("matches") List<Match> matches) {
 
-        this.firstSubmissionId = firstSubmissionId;
-        this.secondSubmissionId = secondSubmissionId;
-        this.matchPercentage = matchPercentage;
-        this.filesOfFirstSubmission = List.copyOf(filesOfFirstSubmission);
-        this.filesOfSecondSubmission = List.copyOf(filesOfSecondSubmission);
-        this.matches = matches;
-    }
 }

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

@@ -1,28 +0,0 @@
-package de.jplag.reporting.reportobject.model;
-
-import java.util.List;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-public class FilesOfSubmission {
-
-    @JsonProperty("file_name")
-    private final String fileName;
-
-    @JsonProperty("lines")
-    private final List<String> lines;
-
-    public FilesOfSubmission(String fileName, List<String> fileCode) {
-        this.fileName = fileName;
-        this.lines = List.copyOf(fileCode);
-    }
-
-    public String getFileName() {
-        return fileName;
-    }
-
-    public List<String> getLines() {
-        return lines;
-    }
-
-}

+ 0 - 10
jplag/src/main/java/de/jplag/reporting/reportobject/model/JPlagReport.java

@@ -1,10 +0,0 @@
-package de.jplag.reporting.reportobject.model;
-
-import java.util.List;
-
-public record JPlagReport(OverviewReport overviewReport, List<ComparisonReport> comparisons) {
-    public JPlagReport(OverviewReport overviewReport, List<ComparisonReport> comparisons) {
-        this.overviewReport = overviewReport;
-        this.comparisons = List.copyOf(comparisons);
-    }
-}

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

@@ -2,7 +2,7 @@ package de.jplag.reporting.reportobject.model;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
 
-public record Match(@JsonProperty("first_file_name") String firstFileName, @JsonProperty("second_file_name") String secondFileName,
-        @JsonProperty("start_in_first") int startInFirst, @JsonProperty("end_in_first") int endInFirst,
-        @JsonProperty("start_in_second") int startInSecond, @JsonProperty("end_in_second") int endInSecond, @JsonProperty("tokens") int tokens) {
+public record Match(@JsonProperty("file1") String firstFileName, @JsonProperty("file2") String secondFileName,
+        @JsonProperty("start1") int startInFirst, @JsonProperty("end1") int endInFirst, @JsonProperty("start2") int startInSecond,
+        @JsonProperty("end2") int endInSecond, @JsonProperty("tokens") int tokens) {
 }

+ 2 - 1
report-viewer/package.json

@@ -14,7 +14,8 @@
     "core-js": "^3.24.1",
     "gitart-vue-dialog": "^2.4.0",
     "highlight.js": "^11.6.0",
-    "jszip": "^3.10.1",
+    "jszip": "^3.10.0",
+    "node-polyfill-webpack-plugin": "^2.0.0",
     "vue": "^3.2.37",
     "vue-chart-3": "^3.1.8",
     "vue-draggable-next": "^2.1.1",

+ 1 - 1
report-viewer/src/components/ClusterRadarChart.vue

@@ -46,6 +46,7 @@ export default defineComponent({
     },
   },
   setup(props) {
+    let hasNoMember = false;
     const getIdOfFirstSubmission = () => {
       const firstMember = props.cluster.members.keys().next().value;
       hasNoMember = !firstMember;
@@ -83,7 +84,6 @@ export default defineComponent({
     });
 
     const options = ref(radarChartOptions);
-    let hasNoMember = false;
     watch(
       () => selectedMember.value,
       (val) => {

+ 40 - 21
report-viewer/src/components/ComparisonsTable.vue

@@ -87,7 +87,15 @@
           @click="toggleDialog(index)"
         />
       </td>
-      <GDialog v-model="dialog[index]">
+      <GDialog
+        v-if="
+          isInCluster(
+            comparison.firstSubmissionId,
+            comparison.secondSubmissionId
+          )
+        "
+        v-model="dialog[index]"
+      >
         <ClustersList
           :clusters="
             getClustersFor(
@@ -128,10 +136,10 @@ export default defineComponent({
   setup(props) {
     const store = useStore();
     let formattedMatchPercentage = (num: number) => num.toFixed(2);
-    const dialog :Ref<Array<boolean>> = ref([]);
+    const dialog: Ref<Array<boolean>> = ref([]);
     props.topComparisons.forEach(() => dialog.value.push(false));
 
-    const toggleDialog = (index:number) => {
+    const toggleDialog = (index: number) => {
       dialog.value[index] = true;
     };
 
@@ -154,8 +162,11 @@ export default defineComponent({
       return store.state.anonymous.has(id);
     };
 
-    const getParticipatingMatchesForId = (id: string, others : Array<string>) => {
-      let matches : Array<{matchedWith: string, percentage: number,}>= [];
+    const getParticipatingMatchesForId = (
+      id: string,
+      others: Array<string>
+    ) => {
+      let matches: Array<{ matchedWith: string; percentage: number }> = [];
       props.topComparisons.forEach((comparison) => {
         if (
           comparison.firstSubmissionId.includes(id) &&
@@ -178,25 +189,33 @@ export default defineComponent({
       return matches;
     };
 
-    const clustersWithParticipatingMatches: Array<ClusterListElement> = props.clusters.map((cluster) => {
-      let membersArray = new Map<string,Array<{matchedWith: string, percentage: number,}>>();
-      cluster.members.forEach((member: string) => {
-        let others = cluster.members.filter((m) => !m.includes(member));
-        membersArray.set(member ,getParticipatingMatchesForId(member, others));
-      });
+    const clustersWithParticipatingMatches: Array<ClusterListElement> =
+      props.clusters.map((cluster) => {
+        let membersArray = new Map<
+          string,
+          Array<{ matchedWith: string; percentage: number }>
+        >();
+        cluster.members.forEach((member: string) => {
+          let others = cluster.members.filter((m) => !m.includes(member));
+          membersArray.set(
+            member,
+            getParticipatingMatchesForId(member, others)
+          );
+        });
 
-      return {
-        averageSimilarity: cluster.averageSimilarity,
-        strength: cluster.strength,
-        members: membersArray,
-      };
-    });
+        return {
+          averageSimilarity: cluster.averageSimilarity,
+          strength: cluster.strength,
+          members: membersArray,
+        };
+      });
 
-    const getClustersFor = (id1: string, id2: string) : Array<ClusterListElement> => {
+    const getClustersFor = (
+      id1: string,
+      id2: string
+    ): Array<ClusterListElement> => {
       return clustersWithParticipatingMatches.filter(
-        (c) =>
-          c.members.has(id1) &&
-          c.members.has(id2)
+        (c) => c.members.has(id1) && c.members.has(id2)
       );
     };
 

+ 63 - 47
report-viewer/src/components/DistributionDiagram.vue

@@ -8,100 +8,116 @@
 </template>
 
 <script lang="ts">
-import {defineComponent, ref, watch} from "vue";
-import {BarChart} from "vue-chart-3"
-import {Chart, registerables} from 'chart.js';
-import ChartDataLabels from 'chartjs-plugin-datalabels';
+import { defineComponent, ref, watch } from "vue";
+import { BarChart } from "vue-chart-3";
+import { Chart, registerables } from "chart.js";
+import ChartDataLabels from "chartjs-plugin-datalabels";
 
 Chart.register(...registerables);
 Chart.register(ChartDataLabels);
 
 export default defineComponent({
   name: "DistributionDiagram",
-  components: {BarChart},
+  components: { BarChart },
   props: {
     distribution: {
       type: Array<number>,
-      required: true
-    }
+      required: true,
+    },
   },
   setup(props) {
     //Highest count of submissions in a percentage range. We set the diagrams maximum shown value to maxVal + 5,
     //otherwise maximum is set to the highest count of submissions and is one bar always reaches the end.
     let maxVal = ref(Math.max(...props.distribution));
-    const labels = ['0-10%', '11-20%', '21-30%', '31-40%', '41-50%', '51-60%', '61-70%', '71-80%', '81-90%', '91-100%']
+    const labels = [
+      "91-100%",
+      "81-90%",
+      "71-80%",
+      "61-70%",
+      "51-60%",
+      "41-50%",
+      "31-40%",
+      "21-30%",
+      "11-20%",
+      "0-10%",
+    ];
     const dataSetStyle = {
-      label: 'Count',
-      backgroundColor: 'rgba(149, 168, 241, 0.5)',
+      label: "Count",
+      backgroundColor: "rgba(149, 168, 241, 0.5)",
       borderWidth: 2,
-      borderColor: 'rgba(149, 168, 241, 1)',
-      tickColor: '#000000'
-    }
+      borderColor: "rgba(149, 168, 241, 1)",
+      tickColor: "#000000",
+    };
 
     let chartData = ref({
       labels: labels,
-      datasets: [{
-        ...dataSetStyle,
-        data: props.distribution,
-      }]
-    })
+      datasets: [
+        {
+          ...dataSetStyle,
+          data: props.distribution,
+        },
+      ],
+    });
 
     const options = ref({
       responsive: true,
       maintainAspectRatio: false,
-      indexAxis: 'y',
+      indexAxis: "y",
       scales: {
         x: {
           suggestedMax: maxVal.value + 5,
           ticks: {
-            color: '#000000',
-          }
+            color: "#000000",
+          },
         },
         y: {
           ticks: {
-            color: '#000000',
-          }
-        }
+            color: "#000000",
+          },
+        },
       },
       plugins: {
         datalabels: {
-          color: '#000000',
+          color: "#000000",
           font: {
-            weight: 'bold'
+            weight: "bold",
           },
-          anchor: 'end',
-          align: 'end',
-          clamp: true
+          anchor: "end",
+          align: "end",
+          clamp: true,
         },
         legend: {
           display: false,
-        }
-      }
-    })
+        },
+      },
+    });
 
     //We watch the given distributions parameter. When the distribution of another metric is passed, the diagram is
     //updated with the new data.
-    watch(() => props.distribution, (val) => {
-          chartData.value = {
-            labels: labels,
-            datasets: [{
+    watch(
+      () => props.distribution,
+      (val) => {
+        chartData.value = {
+          labels: labels,
+          datasets: [
+            {
               ...dataSetStyle,
               data: val,
-            }]
-          }
+            },
+          ],
+        };
 
-          maxVal.value = Math.max(...val)
-          options.value.scales.x.suggestedMax = maxVal.value + 5
-        }
-    )
+        maxVal.value = Math.max(...val);
+        options.value.scales.x.suggestedMax = maxVal.value + 5;
+      }
+    );
 
     return {
       chartData,
-      options
-    }
-  }
-
-})
+      options,
+    };
+  },
+});
 </script>
 
 <style scoped>

+ 28 - 20
report-viewer/src/model/factories/ComparisonFactory.ts

@@ -2,14 +2,22 @@ import { Comparison } from "../Comparison";
 import { Match } from "../Match";
 import { SubmissionFile } from "../SubmissionFile";
 import { MatchInSingleFile } from "../MatchInSingleFile";
+import store from "@/store/store";
 
 export class ComparisonFactory {
   static getComparison(json: Record<string, unknown>): Comparison {
-    const filesOfFirst = this.convertToFilesByName(
-      json.files_of_first_submission as Array<Record<string, unknown>>
+    const filesOfFirstSubmission = store.getters.filesOfSubmission(
+      json.id1
     );
-    const filesOfSecond = this.convertToFilesByName(
-      json.files_of_second_submission as Array<Record<string, unknown>>
+    const filesOfSecondSubmission = store.getters.filesOfSubmission(
+      json.id2
+    );
+
+    const filesOfFirstConverted = this.convertToFilesByName(
+      filesOfFirstSubmission
+    );
+    const filesOfSecondConverted = this.convertToFilesByName(
+      filesOfSecondSubmission
     );
 
     const matches = json.matches as Array<Record<string, unknown>>;
@@ -22,12 +30,12 @@ export class ComparisonFactory {
     const matchesInSecond = this.groupMatchesByFileName(coloredMatches, 2);
 
     const comparison = new Comparison(
-      json.first_submission_id as string,
-      json.second_submission_id as string,
-      json.match_percentage as number
+      json.id1 as string,
+      json.id2 as string,
+      json.similarity as number
     );
-    comparison.filesOfFirstSubmission = filesOfFirst;
-    comparison.filesOfSecondSubmission = filesOfSecond;
+    comparison.filesOfFirstSubmission = filesOfFirstConverted;
+    comparison.filesOfSecondSubmission = filesOfSecondConverted;
     comparison.colors = colors;
     comparison.allMatches = coloredMatches;
     comparison.matchesInFirstSubmission = matchesInFirst;
@@ -37,18 +45,18 @@ export class ComparisonFactory {
   }
 
   private static convertToFilesByName(
-    files: Array<Record<string, unknown>>
+    files: Array<{ name: string; value: string }>
   ): Map<string, SubmissionFile> {
     const map = new Map<string, SubmissionFile>();
     files.forEach((val) => {
-      if (!map.get(val.file_name as string)) {
-        map.set(val.file_name as string, {
+      if (!map.get(val.name)) {
+        map.set(val.name as string, {
           lines: [],
           collapsed: false,
         });
       }
-      map.set(val.file_name as string, {
-        lines: val.lines as Array<string>,
+      map.set(val.name as string, {
+        lines: val.value.split(/\r?\n/),
         collapsed: false,
       });
     });
@@ -108,12 +116,12 @@ export class ComparisonFactory {
     color: string
   ): Match {
     return {
-      firstFile: match.first_file_name as string,
-      secondFile: match.second_file_name as string,
-      startInFirst: match.start_in_first as number,
-      endInFirst: match.end_in_first as number,
-      startInSecond: match.start_in_second as number,
-      endInSecond: match.end_in_second as number,
+      firstFile: match.file1 as string,
+      secondFile: match.file2 as string,
+      startInFirst: match.start1 as number,
+      endInFirst: match.end1 as number,
+      startInSecond: match.start2 as number,
+      endInSecond: match.end2 as number,
       tokens: match.tokens as number,
       color: color,
     };

+ 1 - 1
report-viewer/src/model/factories/OverviewFactory.ts

@@ -34,7 +34,7 @@ export class OverviewFactory {
         metricThreshold: metric.threshold as number,
         distribution: metric.distribution as Array<number>,
         comparisons: comparisons,
-        description: metric.description as string
+        description: metric.description as string,
       });
     });
 

+ 23 - 0
report-viewer/src/store/store.ts

@@ -13,6 +13,7 @@ interface State {
    * Stored files if zip mode is used. Stores the files as key - file name, value - file string
    */
   files: Record<string, string>;
+  submissions: Record<string, Map<string, string>>;
   /**
    * Indicates whether local mode is used.
    */
@@ -35,6 +36,10 @@ interface File {
   fileName: string;
   data: string;
 }
+interface SubmissionFile {
+  name: string;
+  file: File;
+}
 
 interface LoadConfiguration {
   local: boolean;
@@ -53,6 +58,7 @@ const store = createStore<State>({
      * Stored files if zip mode is used. Stores the files as key - file name, value - file string
      */
     files: {},
+    submissions: {},
     /**
      * Indicates whether local mode is used.
      */
@@ -70,6 +76,14 @@ const store = createStore<State>({
      */
     fileString: "",
   },
+  getters: {
+    filesOfSubmission: (state) => (name: string) => {
+      return Array.from(state.submissions[name], ([name, value]) => ({
+        name,
+        value,
+      }));
+    },
+  },
   mutations: {
     addAnonymous(state: State, id) {
       for (let i = 0; i < id.length; i++) {
@@ -87,6 +101,15 @@ const store = createStore<State>({
     saveFile(state, file: File) {
       state.files[file.fileName] = file.data;
     },
+    saveSubmissionFile(state, submissionFile: SubmissionFile) {
+      if (!state.submissions[submissionFile.name]) {
+        state.submissions[submissionFile.name] = new Map();
+      }
+      state.submissions[submissionFile.name].set(
+        submissionFile.file.fileName,
+        submissionFile.file.data
+      );
+    },
     setLoadingType(state, payload: LoadConfiguration) {
       state.local = payload.local;
       state.zip = payload.zip;

+ 18 - 8
report-viewer/src/views/FileUploadView.vue

@@ -24,6 +24,7 @@ import jszip from "jszip";
 import router from "@/router";
 import store from "@/store/store";
 import { getFileExtension } from "@/utils/Utils";
+import path from "path";
 
 export default defineComponent({
   name: "FileUploadView",
@@ -43,7 +44,6 @@ export default defineComponent({
         name: "OverviewView",
       });
     };
-
     const navigateToComparisonView = (firstId: string, secondId: string) => {
       router.push({
         name: "ComparisonView",
@@ -60,10 +60,20 @@ export default defineComponent({
     const handleZipFile = (file: File) => {
       jszip.loadAsync(file).then(async (zip) => {
         for (const fileName of Object.keys(zip.files)) {
-          console.log(fileName);
-          await zip.files[fileName].async("string").then((data) => {
-            store.commit("saveFile", { fileName: fileName, data: data });
-          });
+          if (fileName.match(/submissions\/(.+)\/(.+)/)) {
+            const filePath = path.parse(fileName);
+            const submissionName = filePath.dir.split("/")[2];
+            await zip.files[fileName].async("string").then((data) => {
+              store.commit("saveSubmissionFile", {
+                name: submissionName,
+                file: { fileName: filePath.base, data: data },
+              });
+            });
+          } else {
+            await zip.files[fileName].async("string").then((data) => {
+              store.commit("saveFile", { fileName: fileName, data: data });
+            });
+          }
         }
         store.commit("setLoadingType", {
           local: false,
@@ -88,7 +98,7 @@ export default defineComponent({
           fileString: str,
         });
         navigateToOverview();
-      } else if (json["first_submission_id"] && json["second_submission_id"]) {
+      } else if (json["id1"] && json["id2"]) {
         store.commit("setLoadingType", {
           local: false,
           zip: false,
@@ -96,8 +106,8 @@ export default defineComponent({
           fileString: str,
         });
         navigateToComparisonView(
-          json["first_submission_id"],
-          json["second_submission_id"]
+          json["id1"],
+          json["id2"]
         );
       }
     };

+ 19 - 21
report-viewer/src/views/OverviewView.vue

@@ -4,7 +4,7 @@
 <template>
   <div class="container">
     <div class="column-container" style="width: 30%">
-      <h1>JPlag Report</h1>
+        <h1>JPlag Report</h1>
       <p class="section-title">Main Info:</p>
       <div id="basicInfo">
         <TextInformation
@@ -56,19 +56,19 @@
     </div>
 
     <div class="column-container" style="width: 35%">
-      <div id="metrics">
+        <div id="metrics">
         <p class="section-title">Metric:</p>
-        <div id="metrics-list">
-          <MetricButton
-            v-for="(metric, index) in overview.metrics"
-            :id="metric.metricName"
-            :key="metric.metricName"
-            :is-selected="selectedMetric[index]"
-            :metric="metric"
-            @click="selectMetric(index)"
-          />
+          <div id="metrics-list">
+            <MetricButton
+              v-for="(metric, index) in overview.metrics"
+              :id="metric.metricName"
+              :key="metric.metricName"
+              :is-selected="selectedMetric[index]"
+              :metric="metric"
+              @click="selectMetric(index)"
+            />
+          </div>
         </div>
-      </div>
       <p class="section-title">Distribution:</p>
       <DistributionDiagram
         :distribution="distributions[selectedMetricIndex]"
@@ -78,13 +78,13 @@
     <div class="column-container" style="width: 35%">
       <p class="section-title">Top Comparisons:</p>
       <div id="comparisonsList">
-        <ComparisonsTable
-          :clusters="overview.clusters"
-          :top-comparisons="topComps[selectedMetricIndex]"
-        />
-      </div>
+      <ComparisonsTable
+        :clusters="overview.clusters"
+        :top-comparisons="topComps[selectedMetricIndex]"
+      />
     </div>
   </div>
+  </div>
 </template>
 
 <script lang="ts">
@@ -182,12 +182,10 @@ export default defineComponent({
      * @type {Ref<UnwrapRef<number>>}
      */
     let selectedMetricIndex = ref(0);
-    selectedMetric.value[0] = true;
+    selectedMetric.value[selectedMetricIndex.value] = true;
 
     const selectMetric = (metric: number) => {
-      selectedMetric.value = selectedMetric.value.map(() => {
-        return false;
-      });
+      selectedMetric.value = selectedMetric.value.map(() => false);
       selectedMetric.value[metric] = true;
       selectedMetricIndex.value = metric;
     };

+ 14 - 3
report-viewer/vue.config.js

@@ -1,4 +1,15 @@
-module.exports = {
-    publicPath: '/JPlag'
-};
+const { defineConfig } = require("@vue/cli-service");
+const NodePolyfillPlugin = require("node-polyfill-webpack-plugin");
+module.exports = defineConfig({
+  publicPath: "/JPlag",
 
+  transpileDependencies: true,
+  configureWebpack: {
+    plugins: [new NodePolyfillPlugin()],
+    optimization: {
+      splitChunks: {
+        chunks: "all",
+      },
+    },
+  },
+});