diff --git a/jplag.cli/src/main/java/de/jplag/CLI.java b/jplag.cli/src/main/java/de/jplag/CLI.java index 87606d657..9b2c88b5b 100644 --- a/jplag.cli/src/main/java/de/jplag/CLI.java +++ b/jplag.cli/src/main/java/de/jplag/CLI.java @@ -64,7 +64,8 @@ public static void main(String[] args) { JPlag program = new JPlag(options); logger.info("JPlag initialized"); JPlagResult result = program.run(); - ReportObjectFactory.createAndSaveReport(result, arguments.getString(RESULT_FOLDER.flagWithoutDash())); + ReportObjectFactory reportObjectFactory = new ReportObjectFactory(); + reportObjectFactory.createAndSaveReport(result, arguments.getString(RESULT_FOLDER.flagWithoutDash())); } catch (ExitException exception) { logger.error(exception.getMessage(), exception); diff --git a/jplag/src/main/java/de/jplag/reporting/jsonfactory/ComparisonReportWriter.java b/jplag/src/main/java/de/jplag/reporting/jsonfactory/ComparisonReportWriter.java new file mode 100644 index 000000000..e2489e709 --- /dev/null +++ b/jplag/src/main/java/de/jplag/reporting/jsonfactory/ComparisonReportWriter.java @@ -0,0 +1,111 @@ +package de.jplag.reporting.jsonfactory; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import de.jplag.*; +import de.jplag.reporting.reportobject.model.ComparisonReport; +import de.jplag.reporting.reportobject.model.Match; + +public class ComparisonReportWriter { + + private static final FileWriter FILE_WRITER = new FileWriter(); + private final Function submissionToIdFunction; + private final Map> submissionIdToComparisonFileName = new HashMap<>(); + + public ComparisonReportWriter(Function submissionToIdFunction) { + this.submissionToIdFunction = submissionToIdFunction; + } + + /** + * Generates detailed ComparisonReport DTO for each comparison in a JPlagResult and writes them to the disk as json + * files. + * @param jPlagResult The JPlagResult to generate the comparison reports from. contains information about a comparison + * @param path The path to write the comparison files to + * @return Nested map that associates each pair of submissions (by their ids) to their comparison file name. The + * comparison file name for submission with id id1 and id2 can be fetched by executing get two times: + * map.get(id1).get(id2). The nested map is symmetrical therefore, both map.get(id1).get(id2) and map.get(id2).get(id1) + * yield the same result. + */ + public Map> writeComparisonReports(JPlagResult jPlagResult, String path) { + int numberOfComparisons = jPlagResult.getOptions().getMaximumNumberOfComparisons(); + List comparisons = jPlagResult.getComparisons(numberOfComparisons); + writeComparisons(jPlagResult, path, comparisons); + return submissionIdToComparisonFileName; + } + + private void writeComparisons(JPlagResult jPlagResult, String path, List comparisons) { + for (JPlagComparison comparison : comparisons) { + String firstSubmissionId = submissionToIdFunction.apply(comparison.getFirstSubmission()); + String secondSubmissionId = submissionToIdFunction.apply(comparison.getSecondSubmission()); + String fileName = generateComparisonName(firstSubmissionId, secondSubmissionId); + addToLookUp(firstSubmissionId, secondSubmissionId, fileName); + var comparisonReport = new ComparisonReport(firstSubmissionId, secondSubmissionId, comparison.similarity(), + convertMatchesToReportMatches(jPlagResult, comparison)); + FILE_WRITER.saveAsJSON(comparisonReport, path, fileName); + } + } + + private void addToLookUp(String firstSubmissionId, String secondSubmissionId, String fileName) { + writeToMap(secondSubmissionId, firstSubmissionId, fileName); + writeToMap(firstSubmissionId, secondSubmissionId, fileName); + } + + private void writeToMap(String id1, String id2, String comparisonFileName) { + if (submissionIdToComparisonFileName.containsKey(id1)) { + submissionIdToComparisonFileName.get(id1).put(id2, comparisonFileName); + } else { + HashMap map = new HashMap<>(); + map.put(id2, comparisonFileName); + submissionIdToComparisonFileName.put(id1, map); + } + } + + private String generateComparisonName(String firstSubmissionId, String secondSubmissionId) { + String name = concatenate(firstSubmissionId, secondSubmissionId); + String finalName = name; + var timesNameAlreadyExists = submissionIdToComparisonFileName.values().stream().filter(map -> map.containsValue(finalName)).count(); + if (timesNameAlreadyExists > 0) { + name = concatenate(firstSubmissionId, secondSubmissionId, timesNameAlreadyExists + 1); + } + return name; + + } + + private String concatenate(String firstSubmissionId, String secondSubmissionId, long index) { + return firstSubmissionId.concat("-").concat(secondSubmissionId).concat(index > 0 ? "-" + index : "").concat(".json"); + } + + private String concatenate(String firstSubmissionId, String secondSubmissionId) { + return concatenate(firstSubmissionId, secondSubmissionId, 0); + } + + private List 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 languageSupportsColumnsAndLines) { + 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 = getPosition(languageSupportsColumnsAndLines, startTokenFirst); + int endFirst = getPosition(languageSupportsColumnsAndLines, endTokenFirst); + int startSecond = getPosition(languageSupportsColumnsAndLines, startTokenSecond); + int endSecond = getPosition(languageSupportsColumnsAndLines, endTokenSecond); + int tokens = match.length(); + + return new Match(startTokenFirst.getFile(), startTokenSecond.getFile(), startFirst, endFirst, startSecond, endSecond, tokens); + } + + private int getPosition(boolean languageSupportsColumnsAndLines, Token token) { + return languageSupportsColumnsAndLines ? token.getLine() : token.getIndex(); + } + +} diff --git a/jplag/src/main/java/de/jplag/reporting/jsonfactory/DirectoryManager.java b/jplag/src/main/java/de/jplag/reporting/jsonfactory/DirectoryManager.java new file mode 100644 index 000000000..72c012349 --- /dev/null +++ b/jplag/src/main/java/de/jplag/reporting/jsonfactory/DirectoryManager.java @@ -0,0 +1,90 @@ +package de.jplag.reporting.jsonfactory; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Comparator; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DirectoryManager { + private static final Logger logger = LoggerFactory.getLogger(DirectoryManager.class); + + /** + * Creates a directory. + * @param path The path under which the new directory ought to be created + * @param name The name of the new directory + * @return The created directory + */ + public static File createDirectory(String path, String name) throws IOException { + File directory = new File(path.concat("/").concat(name)); + if (!directory.exists() && !directory.mkdirs()) { + throw new IOException("Failed to create dir."); + } + return directory; + } + + /** + * Create a directory with the given path + * @param path The path of the new directory + */ + public static void createDirectory(String path) throws IOException { + createDirectory(path, ""); + } + + /** + * Delete the directory and all of its contents, identified by the given path + * @param path The path that identifies the directory to delete + */ + public static void deleteDirectory(String path) { + try (var f = Files.walk(Path.of(path))) { + f.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); + } catch (IOException e) { + logger.error("Could not delete folder " + path, e); + } + } + + /** + * Zip the directory identified by the given path + * @param path The path that identifies the directory to zip + * @return True if zip was successful, false otherwise + */ + public static boolean zipDirectory(String path) { + Path p = Path.of(path); + String zipName = path + ".zip"; + + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipName))) { + + Files.walkFileTree(p, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Path targetFile = p.relativize(file); + zos.putNextEntry(new ZipEntry(targetFile.toString())); + + byte[] bytes = Files.readAllBytes(file); + zos.write(bytes, 0, bytes.length); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { + logger.error("Unable to zip " + file, exc); + throw exc; + } + }); + } catch (IOException e) { + logger.error(e.getMessage(), e); + deleteDirectory(zipName); + return false; + } + return true; + } +} diff --git a/jplag/src/main/java/de/jplag/reporting/reportobject/ReportObjectFactory.java b/jplag/src/main/java/de/jplag/reporting/reportobject/ReportObjectFactory.java index 580d94e5b..1a0698c0c 100644 --- a/jplag/src/main/java/de/jplag/reporting/reportobject/ReportObjectFactory.java +++ b/jplag/src/main/java/de/jplag/reporting/reportobject/ReportObjectFactory.java @@ -1,15 +1,15 @@ package de.jplag.reporting.reportobject; +import static de.jplag.reporting.jsonfactory.DirectoryManager.*; +import static de.jplag.reporting.reportobject.mapper.SubmissionNameToIdMapper.buildSubmissionNameToIdMap; + import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; +import java.util.function.Function; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -19,87 +19,77 @@ import de.jplag.JPlagResult; import de.jplag.Language; import de.jplag.Submission; +import de.jplag.reporting.jsonfactory.ComparisonReportWriter; 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.Metric; import de.jplag.reporting.reportobject.model.OverviewReport; /** - * Factory class, responsible for converting a JPlagResult object to Overview and Comparison DTO classes. + * Factory class, responsible for converting a JPlagResult object to Overview and Comparison DTO classes and writing it + * to the disk. */ public class ReportObjectFactory { private static final 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"; + private Map submissionNameToIdMap; + private Function submissionToIdFunction; + private Map> submissionNameToNameToComparisonFileName; /** + * Creates all necessary report viewer files, writes them to the disk as zip. * @param result The JPlagResult to be converted into a report. * @param path The Path to save the report to */ - public static void createAndSaveReport(JPlagResult result, String path) { - createDirectory(path); - writeOverview(result, path); - copySubmissionFilesToReport(path, result); - comparisonReportMapper.writeComparisonReports(result, path); - } + public void createAndSaveReport(JPlagResult result, String path) { - 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; - } + try { + createDirectory(path); + buildSubmissionToIdMap(result); - private static void createDirectory(String path) { - createDirectory(path, ""); - } + copySubmissionFilesToReport(path, result); - private static void writeOverview(JPlagResult result, String path) { - List comparisons = getComparisons(result); - OverviewReport overviewReport = new OverviewReport(); + writeComparisons(result, path); + writeOverview(result, path); - // TODO: Consider to treat entries that were checked differently from old entries with prior work. - List folders = new ArrayList<>(); - folders.addAll(result.getOptions().getSubmissionDirectories()); - folders.addAll(result.getOptions().getOldSubmissionDirectories()); - overviewReport.setSubmissionFolderPath(folders); + zipAndDelete(path); + } catch (IOException e) { + logger.error("Could not create directory " + path + " for report viewer generation", e); + } - String baseCodePath = result.getOptions().hasBaseCode() ? result.getOptions().getBaseCodeSubmissionName().orElse("") : ""; - overviewReport.setBaseCodeFolderPath(baseCodePath); - - overviewReport.setLanguage(result.getOptions().getLanguage().getName()); - overviewReport.setFileExtensions(List.of(result.getOptions().getFileSuffixes())); - overviewReport.setSubmissionIds(extractSubmissionNames(comparisons)); - overviewReport.setFailedSubmissionNames(List.of()); // No number of failed submissions - overviewReport.setExcludedFiles(result.getOptions().getExcludedFiles()); - overviewReport.setMatchSensitivity(result.getOptions().getMinimumTokenMatch()); - overviewReport.setDateOfExecution(getDate()); - overviewReport.setExecutionTime(result.getDuration()); - overviewReport.setComparisonNames(getComparisonNames(comparisons)); - overviewReport.setMetrics(getMetrics(result)); - overviewReport.setClusters(clusteringResultMapper.map(result)); + } - fileWriter.saveAsJSON(overviewReport, path, OVERVIEW_FILE_NAME); + private void zipAndDelete(String path) { + boolean zipWasSuccessful = zipDirectory(path); + if (zipWasSuccessful) { + deleteDirectory(path); + } else { + logger.error("Could not zip results. The results are still available uncompressed at " + path); + } + } + private void buildSubmissionToIdMap(JPlagResult result) { + submissionNameToIdMap = buildSubmissionNameToIdMap(result); + submissionToIdFunction = (Submission submission) -> submissionNameToIdMap.get(submission.getName()); } - private static void copySubmissionFilesToReport(String path, JPlagResult result) { + private void copySubmissionFilesToReport(String path, JPlagResult result) { List comparisons = result.getComparisons(result.getOptions().getMaximumNumberOfComparisons()); - var submissions = getSubmissions(comparisons); - var submissionsPath = createDirectory(path, SUBMISSIONS_FOLDER); + Set submissions = getSubmissions(comparisons); + File submissionsPath = createSubmissionsDirectory(path); + if (submissionsPath == null) + return; Language language = result.getOptions().getLanguage(); - for (var submission : submissions) { - File directory = createDirectory(submissionsPath.getPath(), submission.getName()); - for (var file : submission.getFiles()) { - var fileToCopy = language.useViewFiles() ? new File(file.getPath() + language.viewFileSuffix()) : file; + for (Submission submission : submissions) { + File directory = createSubmissionDirectory(path, submissionsPath, submission); + if (directory == null) + continue; + for (File file : submission.getFiles()) { + File fileToCopy = language.useViewFiles() ? new File(file.getPath() + language.viewFileSuffix()) : file; try { Files.copy(fileToCopy.toPath(), (new File(directory, file.getName())).toPath(), StandardCopyOption.REPLACE_EXISTING); } catch (IOException e) { @@ -109,40 +99,65 @@ private static void copySubmissionFilesToReport(String path, JPlagResult result) } } - private static Set getSubmissions(List comparisons) { - var submissions = comparisons.stream().map(JPlagComparison::getFirstSubmission).collect(Collectors.toSet()); - Set secondSubmissions = comparisons.stream().map(JPlagComparison::getSecondSubmission).collect(Collectors.toSet()); - submissions.addAll(secondSubmissions); - return submissions; + private File createSubmissionDirectory(String path, File submissionsPath, Submission submission) { + File directory; + try { + directory = createDirectory(submissionsPath.getPath(), submissionToIdFunction.apply(submission)); + } catch (IOException e) { + logger.error("Could not create directory " + path + " for report viewer generation", e); + return null; + } + return directory; } - private static List getComparisons(JPlagResult result) { - int numberOfComparisons = result.getOptions().getMaximumNumberOfComparisons(); - return result.getComparisons(numberOfComparisons); + private File createSubmissionsDirectory(String path) { + File submissionsPath; + try { + submissionsPath = createDirectory(path, SUBMISSIONS_FOLDER); + } catch (IOException e) { + logger.error("Could not create directory " + path + " for report viewer generation", e); + return null; + } + return submissionsPath; } - /** - * Gets the names of all submissions. - * @return A list containing all submission names. - */ - private static List extractSubmissionNames(List comparisons) { - HashSet names = new HashSet<>(); - comparisons.forEach(comparison -> { - names.add(comparison.getFirstSubmission().getName()); - names.add(comparison.getSecondSubmission().getName()); - }); - return new ArrayList<>(names); + private void writeComparisons(JPlagResult result, String path) { + ComparisonReportWriter comparisonReportWriter = new ComparisonReportWriter(submissionToIdFunction); + submissionNameToNameToComparisonFileName = comparisonReportWriter.writeComparisonReports(result, path); } - /** - * Gets the names of all comparison. - * @return A list containing all comparisons. - */ - private static List getComparisonNames(List comparisons) { - List names = new ArrayList<>(); - comparisons.forEach( - comparison -> names.add(String.join("-", comparison.getFirstSubmission().getName(), comparison.getSecondSubmission().getName()))); - return names; + private void writeOverview(JPlagResult result, String path) { + + List folders = new ArrayList<>(); + folders.addAll(result.getOptions().getSubmissionDirectories()); + folders.addAll(result.getOptions().getOldSubmissionDirectories()); + + String baseCodePath = result.getOptions().hasBaseCode() ? result.getOptions().getBaseCodeSubmissionName().orElse("") : ""; + ClusteringResultMapper clusteringResultMapper = new ClusteringResultMapper(submissionToIdFunction); + + OverviewReport overviewReport = new OverviewReport(folders, // submissionFolderPath + baseCodePath, // baseCodeFolderPath + result.getOptions().getLanguage().getName(), // language + List.of(result.getOptions().getFileSuffixes()), // fileExtensions + submissionNameToIdMap.entrySet().stream().collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey)), // submissionIds + submissionNameToNameToComparisonFileName, // result.getOptions().getMinimumTokenMatch(), + List.of(), // failedSubmissionNames + result.getOptions().getExcludedFiles(), // excludedFiles + result.getOptions().getMinimumTokenMatch(), // matchSensitivity + getDate(),// dateOfExecution + result.getDuration(), // executionTime + getMetrics(result),// metrics + clusteringResultMapper.map(result)); // clusters + + fileWriter.saveAsJSON(overviewReport, path, OVERVIEW_FILE_NAME); + + } + + private Set getSubmissions(List comparisons) { + Set submissions = comparisons.stream().map(JPlagComparison::getFirstSubmission).collect(Collectors.toSet()); + Set secondSubmissions = comparisons.stream().map(JPlagComparison::getSecondSubmission).collect(Collectors.toSet()); + submissions.addAll(secondSubmissions); + return submissions; } /** @@ -150,11 +165,12 @@ private static List getComparisonNames(List comparisons * Metric. * @return A list contains Metric DTOs. */ - private static List getMetrics(JPlagResult result) { + private List getMetrics(JPlagResult result) { + MetricMapper metricMapper = new MetricMapper(submissionToIdFunction); return List.of(metricMapper.getAverageMetric(result), metricMapper.getMaxMetric(result)); } - private static String getDate() { + private String getDate() { SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yy"); Date date = new Date(); return dateFormat.format(date); diff --git a/jplag/src/main/java/de/jplag/reporting/reportobject/mapper/ClusteringResultMapper.java b/jplag/src/main/java/de/jplag/reporting/reportobject/mapper/ClusteringResultMapper.java index ea75f2cbd..1596a91b4 100644 --- a/jplag/src/main/java/de/jplag/reporting/reportobject/mapper/ClusteringResultMapper.java +++ b/jplag/src/main/java/de/jplag/reporting/reportobject/mapper/ClusteringResultMapper.java @@ -2,6 +2,7 @@ import java.util.Collection; import java.util.List; +import java.util.function.Function; import de.jplag.JPlagResult; import de.jplag.Submission; @@ -12,6 +13,12 @@ * Extracts and maps the clusters from the JPlagResult to the corresponding JSON DTO */ public class ClusteringResultMapper { + private final Function submissionToIdFunction; + + public ClusteringResultMapper(Function submissionToIdFunction) { + this.submissionToIdFunction = submissionToIdFunction; + } + public List map(JPlagResult result) { var clusteringResult = result.getClusteringResult(); return clusteringResult.stream().map(ClusteringResult::getClusters).flatMap(Collection::stream).map(this::convertCluster).toList(); @@ -20,7 +27,7 @@ public List map(JPlagResult result) { private Cluster convertCluster(de.jplag.clustering.Cluster from) { var strength = from.getCommunityStrength(); var avgSimilarity = from.getAverageSimilarity(); - var member = from.getMembers().stream().map(Submission::getName).toList(); + var member = from.getMembers().stream().map(submissionToIdFunction).toList(); return new Cluster(avgSimilarity, strength, member); } } diff --git a/jplag/src/main/java/de/jplag/reporting/reportobject/mapper/ComparisonReportMapper.java b/jplag/src/main/java/de/jplag/reporting/reportobject/mapper/ComparisonReportMapper.java deleted file mode 100644 index 1cc7d410d..000000000 --- a/jplag/src/main/java/de/jplag/reporting/reportobject/mapper/ComparisonReportMapper.java +++ /dev/null @@ -1,60 +0,0 @@ -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 comparisons = jPlagResult.getComparisons(numberOfComparisons); - writeComparisons(jPlagResult, path, comparisons); - } - - private void writeComparisons(JPlagResult jPlagResult, String path, List 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 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 languageSupportsColumnsAndLines) { - 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 = getPosition(languageSupportsColumnsAndLines, startTokenFirst); - int endFirst = getPosition(languageSupportsColumnsAndLines, endTokenFirst); - int startSecond = getPosition(languageSupportsColumnsAndLines, startTokenSecond); - int endSecond = getPosition(languageSupportsColumnsAndLines, endTokenSecond); - int tokens = match.length(); - - return new Match(startTokenFirst.getFile(), startTokenSecond.getFile(), startFirst, endFirst, startSecond, endSecond, tokens); - } - - private int getPosition(boolean languageSupportsColumnsAndLines, Token token) { - return languageSupportsColumnsAndLines ? token.getLine() : token.getIndex(); - } - -} diff --git a/jplag/src/main/java/de/jplag/reporting/reportobject/mapper/MetricMapper.java b/jplag/src/main/java/de/jplag/reporting/reportobject/mapper/MetricMapper.java index c761a9a59..61b257572 100644 --- a/jplag/src/main/java/de/jplag/reporting/reportobject/mapper/MetricMapper.java +++ b/jplag/src/main/java/de/jplag/reporting/reportobject/mapper/MetricMapper.java @@ -9,6 +9,7 @@ import de.jplag.JPlagComparison; import de.jplag.JPlagResult; import de.jplag.Messages; +import de.jplag.Submission; import de.jplag.options.SimilarityMetric; import de.jplag.reporting.reportobject.model.Metric; import de.jplag.reporting.reportobject.model.TopComparison; @@ -17,6 +18,12 @@ * Extracts and maps metrics from the JPlagResult to the corresponding JSON DTO */ public class MetricMapper { + private final Function submissionToIdFunction; + + public MetricMapper(Function submissionToIdFunction) { + this.submissionToIdFunction = submissionToIdFunction; + } + public Metric getAverageMetric(JPlagResult result) { return new Metric(SimilarityMetric.AVG.name(), intArrayToList(result.getSimilarityDistribution()), getTopComparisons(getComparisons(result)), Messages.getString("SimilarityMetric.Avg.Description")); @@ -32,22 +39,22 @@ private List getComparisons(JPlagResult result) { return result.getComparisons(maxNumberOfComparisons); } - private static List intArrayToList(int[] array) { + private List intArrayToList(int[] array) { return Arrays.stream(array).boxed().collect(Collectors.toList()); } - private static List getTopComparisons(List comparisons, Function similarityExtractor) { + private List getTopComparisons(List comparisons, Function similarityExtractor) { return comparisons.stream().sorted(Comparator.comparing(similarityExtractor).reversed()) - .map(comparison -> new TopComparison(comparison.getFirstSubmission().getName(), comparison.getSecondSubmission().getName(), - similarityExtractor.apply(comparison))) + .map(comparison -> new TopComparison(submissionToIdFunction.apply(comparison.getFirstSubmission()), + submissionToIdFunction.apply(comparison.getSecondSubmission()), similarityExtractor.apply(comparison))) .toList(); } - private static List getTopComparisons(List comparisons) { + private List getTopComparisons(List comparisons) { return getTopComparisons(comparisons, JPlagComparison::similarity); } - private static List getMaxSimilarityTopComparisons(List comparisons) { + private List getMaxSimilarityTopComparisons(List comparisons) { return getTopComparisons(comparisons, JPlagComparison::maximalSimilarity); } diff --git a/jplag/src/main/java/de/jplag/reporting/reportobject/mapper/SubmissionNameToIdMapper.java b/jplag/src/main/java/de/jplag/reporting/reportobject/mapper/SubmissionNameToIdMapper.java new file mode 100644 index 000000000..d77163323 --- /dev/null +++ b/jplag/src/main/java/de/jplag/reporting/reportobject/mapper/SubmissionNameToIdMapper.java @@ -0,0 +1,35 @@ +package de.jplag.reporting.reportobject.mapper; + +import java.io.File; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import de.jplag.JPlagComparison; +import de.jplag.JPlagResult; +import de.jplag.Submission; + +public class SubmissionNameToIdMapper { + /** + * Builds a map that associates a Submission by its JPlag Id ({@link Submission#getName()}) to its report viewer id. + * @return A Map of containing an entry [name of submission -> report viewer id of submission] for each submission of + * the submission set. + */ + public static Map buildSubmissionNameToIdMap(JPlagResult result) { + HashMap idToName = new HashMap<>(); + getComparisons(result).forEach(comparison -> { + idToName.put(comparison.getFirstSubmission().getName(), sanitizeNameOf(comparison.getFirstSubmission())); + idToName.put(comparison.getSecondSubmission().getName(), sanitizeNameOf(comparison.getSecondSubmission())); + }); + return idToName; + } + + private static String sanitizeNameOf(Submission comparison) { + return comparison.getName().replace(File.separator, "_"); + } + + private static List getComparisons(JPlagResult result) { + int numberOfComparisons = result.getOptions().getMaximumNumberOfComparisons(); + return result.getComparisons(numberOfComparisons); + } +} diff --git a/jplag/src/main/java/de/jplag/reporting/reportobject/model/OverviewReport.java b/jplag/src/main/java/de/jplag/reporting/reportobject/model/OverviewReport.java index eb3201c06..62136ab4a 100644 --- a/jplag/src/main/java/de/jplag/reporting/reportobject/model/OverviewReport.java +++ b/jplag/src/main/java/de/jplag/reporting/reportobject/model/OverviewReport.java @@ -1,174 +1,35 @@ package de.jplag.reporting.reportobject.model; import java.util.List; +import java.util.Map; import java.util.Set; import com.fasterxml.jackson.annotation.JsonProperty; -public class OverviewReport { +public record OverviewReport( - @JsonProperty("submission_folder_path") - private List submissionFolderPath; + @JsonProperty("submission_folder_path") List submissionFolderPath, - @JsonProperty("base_code_folder_path") - private String baseCodeFolderPath; + @JsonProperty("base_code_folder_path") String baseCodeFolderPath, - @JsonProperty("language") - private String language; + @JsonProperty("language") String language, - @JsonProperty("file_extensions") - private List fileExtensions; + @JsonProperty("file_extensions") List fileExtensions, - @JsonProperty("submission_ids") - private List submissionIds; + @JsonProperty("submission_id_to_display_name") Map submissionIds, + @JsonProperty("submission_ids_to_comparison_file_name") Map> submissionIdsToComparisonFileName, - @JsonProperty("failed_submission_names") - private List failedSubmissionNames; + @JsonProperty("failed_submission_names") List failedSubmissionNames, - @JsonProperty("excluded_files") - private Set excludedFiles; + @JsonProperty("excluded_files") Set excludedFiles, - @JsonProperty("match_sensitivity") - private int matchSensitivity; + @JsonProperty("match_sensitivity") int matchSensitivity, - @JsonProperty("date_of_execution") - private String dateOfExecution; + @JsonProperty("date_of_execution") String dateOfExecution, - @JsonProperty("execution_time") - private long executionTime; + @JsonProperty("execution_time") long executionTime, - @JsonProperty("comparison_names") - private List comparisonNames; + @JsonProperty("metrics") List metrics, - @JsonProperty("metrics") - private List metrics; - - @JsonProperty("clusters") - private List clusters; - - public OverviewReport() { - submissionFolderPath = List.of(); - baseCodeFolderPath = ""; - language = ""; - fileExtensions = List.of(); - submissionIds = List.of(); - failedSubmissionNames = List.of(); - excludedFiles = Set.of(); - matchSensitivity = 0; - dateOfExecution = ""; - executionTime = 0; - clusters = List.of(); - } - - /******************************* - * GETTERS - *******************************/ - - public List getSubmissionFolderPath() { - return submissionFolderPath; - } - - public String getBaseCodeFolderPath() { - return baseCodeFolderPath; - } - - public String getLanguage() { - return language; - } - - public List getFileExtensions() { - return fileExtensions; - } - - public List getSubmissionIds() { - return submissionIds; - } - - public List getFailedSubmissionNames() { - return failedSubmissionNames; - } - - public Set getExcludedFiles() { - return excludedFiles; - } - - public int getMatchSensitivity() { - return matchSensitivity; - } - - public String getDateOfExecution() { - return dateOfExecution; - } - - public long getExecutionTime() { - return executionTime; - } - - public List getComparisonNames() { - return comparisonNames; - } - - public List getMetrics() { - return metrics; - } - - public List getClusters() { - return clusters; - } - - /******************************* - * SETTERS - *******************************/ - - public void setSubmissionFolderPath(List submissionFolderPath) { - this.submissionFolderPath = submissionFolderPath; - } - - public void setBaseCodeFolderPath(String baseCodeFolderPath) { - this.baseCodeFolderPath = baseCodeFolderPath; - } - - public void setLanguage(String language) { - this.language = language; - } - - public void setFileExtensions(List fileExtensions) { - this.fileExtensions = List.copyOf(fileExtensions); - } - - public void setSubmissionIds(List submissionIds) { - this.submissionIds = List.copyOf(submissionIds); - } - - public void setFailedSubmissionNames(List failedSubmissionNames) { - this.failedSubmissionNames = List.copyOf(failedSubmissionNames); - } - - public void setExcludedFiles(Set excludedFiles) { - this.excludedFiles = Set.copyOf(excludedFiles); - } - - public void setMatchSensitivity(int matchSensitivity) { - this.matchSensitivity = matchSensitivity; - } - - public void setDateOfExecution(String dateOfExecution) { - this.dateOfExecution = dateOfExecution; - } - - public void setExecutionTime(long executionTime) { - this.executionTime = executionTime; - } - - public void setComparisonNames(List comparisonNames) { - this.comparisonNames = List.copyOf(comparisonNames); - } - - public void setMetrics(List metrics) { - this.metrics = List.copyOf(metrics); - } - - public void setClusters(List clusters) { - this.clusters = List.copyOf(clusters); - } + @JsonProperty("clusters") List clusters) { } diff --git a/jplag/src/test/java/de/jplag/reporting/reportobject/mapper/ClusteringResultMapperTest.java b/jplag/src/test/java/de/jplag/reporting/reportobject/mapper/ClusteringResultMapperTest.java index 103126841..fa72484e3 100644 --- a/jplag/src/test/java/de/jplag/reporting/reportobject/mapper/ClusteringResultMapperTest.java +++ b/jplag/src/test/java/de/jplag/reporting/reportobject/mapper/ClusteringResultMapperTest.java @@ -15,7 +15,7 @@ import de.jplag.clustering.ClusteringResult; public class ClusteringResultMapperTest { - private final ClusteringResultMapper clusteringResultMapper = new ClusteringResultMapper(); + private final ClusteringResultMapper clusteringResultMapper = new ClusteringResultMapper(Submission::getName); @Test public void test() { diff --git a/jplag/src/test/java/de/jplag/reporting/reportobject/mapper/ComparisonReportWriterTest.java b/jplag/src/test/java/de/jplag/reporting/reportobject/mapper/ComparisonReportWriterTest.java new file mode 100644 index 000000000..247927f09 --- /dev/null +++ b/jplag/src/test/java/de/jplag/reporting/reportobject/mapper/ComparisonReportWriterTest.java @@ -0,0 +1,55 @@ +package de.jplag.reporting.reportobject.mapper; + +import java.util.Map; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import de.jplag.JPlagResult; +import de.jplag.Submission; +import de.jplag.TestBase; +import de.jplag.exceptions.ExitException; +import de.jplag.reporting.jsonfactory.ComparisonReportWriter; + +public class ComparisonReportWriterTest extends TestBase { + + @Test + public void firsLevelOfLookupMapComplete() throws ExitException { + JPlagResult result = runJPlagWithDefaultOptions("PartialPlagiarism"); + var mapper = new ComparisonReportWriter(Submission::getName); + + Map> stringMapMap = mapper.writeComparisonReports(result, ""); + + firstLevelOfMapContains(stringMapMap, "A", "B", "C", "D", "E"); + } + + @Test + public void secondLevelOfLookupMapComplete() throws ExitException { + JPlagResult result = runJPlagWithDefaultOptions("PartialPlagiarism"); + var mapper = new ComparisonReportWriter(Submission::getName); + + Map> stringMapMap = mapper.writeComparisonReports(result, ""); + + secondLevelOfMapContains(stringMapMap, "A", "B", "C", "D", "E"); + secondLevelOfMapContains(stringMapMap, "B", "A", "C", "D", "E"); + secondLevelOfMapContains(stringMapMap, "C", "B", "A", "D", "E"); + secondLevelOfMapContains(stringMapMap, "D", "B", "C", "A", "E"); + secondLevelOfMapContains(stringMapMap, "E", "B", "C", "D", "A"); + } + + private void secondLevelOfMapContains(Map> stringMapMap, String firstLevelSubmission, + String... secondLevelSubmissions) { + for (String secondLevelSubmission : secondLevelSubmissions) { + Assertions.assertNotNull(stringMapMap.get(firstLevelSubmission).get(secondLevelSubmission)); + Assertions.assertFalse(stringMapMap.get(firstLevelSubmission).get(secondLevelSubmission).isEmpty()); + + } + + } + + private void firstLevelOfMapContains(Map> stringMapMap, String... names) { + for (String name : names) { + Assertions.assertNotNull(stringMapMap.get(name)); + } + } +} diff --git a/jplag/src/test/java/de/jplag/reporting/reportobject/mapper/MetricMapperTest.java b/jplag/src/test/java/de/jplag/reporting/reportobject/mapper/MetricMapperTest.java index 0d9a7aadf..4c6397622 100644 --- a/jplag/src/test/java/de/jplag/reporting/reportobject/mapper/MetricMapperTest.java +++ b/jplag/src/test/java/de/jplag/reporting/reportobject/mapper/MetricMapperTest.java @@ -15,7 +15,7 @@ import de.jplag.reporting.reportobject.model.TopComparison; public class MetricMapperTest { - private final MetricMapper metricMapper = new MetricMapper(); + private final MetricMapper metricMapper = new MetricMapper(Submission::getName); @Test public void test_getAverageMetric() { diff --git a/report-viewer/src/components/ComparisonsTable.vue b/report-viewer/src/components/ComparisonsTable.vue index 7eba55b09..6bc7fd777 100644 --- a/report-viewer/src/components/ComparisonsTable.vue +++ b/report-viewer/src/components/ComparisonsTable.vue @@ -18,59 +18,37 @@ comparison.matchPercentage " class="selectable" + @click=" + navigateToComparisonView( + comparison.firstSubmissionId, + comparison.secondSubmissionId + ) + " > - - {{ index + 1 }}. - + {{ index + 1 }}. {{ isAnonymous(comparison.firstSubmissionId) ? "Hidden" - : comparison.firstSubmissionId + : displayName(comparison.firstSubmissionId) }} - + >> {{ isAnonymous(comparison.secondSubmissionId) ? "Hidden" - : comparison.secondSubmissionId + : displayName(comparison.secondSubmissionId) }} {{ formattedMatchPercentage(comparison.matchPercentage) }} @@ -138,6 +116,8 @@ export default defineComponent({ let formattedMatchPercentage = (num: number) => num.toFixed(2); const dialog: Ref> = ref([]); props.topComparisons.forEach(() => dialog.value.push(false)); + const displayName = (submissionId: string) => + store.getters.submissionDisplayName(submissionId); const toggleDialog = (index: number) => { dialog.value[index] = true; @@ -223,6 +203,7 @@ export default defineComponent({ clustersWithParticipatingMatches, dialog, + displayName, isAnonymous, getClustersFor, toggleDialog, diff --git a/report-viewer/src/components/IDsList.vue b/report-viewer/src/components/IDsList.vue index 20fb4a418..31f42cf55 100644 --- a/report-viewer/src/components/IDsList.vue +++ b/report-viewer/src/components/IDsList.vue @@ -6,32 +6,33 @@

- {{ id }} + @click="emitIdsSent([id])"> + {{ store.getters.submissionDisplayName(id) }}

- + -