diff --git a/build.gradle b/build.gradle index e1f70b00ac68..ec1e1fdc73ab 100644 --- a/build.gradle +++ b/build.gradle @@ -170,7 +170,28 @@ dependencies { implementation "com.offbytwo.jenkins:jenkins-client:0.3.8" implementation "org.gitlab4j:gitlab4j-api:5.0.1" - implementation "de.jplag:jplag:3.0.0" + implementation "de.jplag:jplag:${jplag_version}" + implementation "de.jplag:java:${jplag_version}" + implementation "de.jplag:kotlin:${jplag_version}" + implementation "de.jplag:cpp:${jplag_version}" + implementation "de.jplag:swift:${jplag_version}" + implementation "de.jplag:java:${jplag_version}" + implementation "de.jplag:python-3:${jplag_version}" + implementation "de.jplag:text:${jplag_version}" + + + implementation "org.slf4j:jcl-over-slf4j:${slf4j_version}" + implementation "org.slf4j:jul-to-slf4j:${slf4j_version}" + implementation ("org.slf4j:slf4j-api") { + version { + strictly "${slf4j_version}" + } + } + implementation "org.apache.logging.log4j:log4j-to-slf4j:2.17.2" +// implementation "org.apache.logging.log4j:log4j-to-slf4j:2.19.0" +// implementation "ch.qos.logback:logback-classic:1.4.1" + + // https://search.maven.org/artifact/org.eclipse.jgit/org.eclipse.jgit diff --git a/docs/user/exercises/programming-exercise-setup.inc b/docs/user/exercises/programming-exercise-setup.inc index 8d878e9aee42..9014f81761ce 100644 --- a/docs/user/exercises/programming-exercise-setup.inc +++ b/docs/user/exercises/programming-exercise-setup.inc @@ -47,17 +47,17 @@ Instructors can still use those templates to generate programming exercises and +----------------------+----------------------+----------------------+------------------+--------------+--------------+------------------------------+--------------------------------------------+ | C | no | yes | yes | no | no | no | no | +----------------------+----------------------+----------------------+------------------+--------------+--------------+------------------------------+--------------------------------------------+ - | C (FACT framework) | no | no | no | no | no | no | no | + | C (FACT framework) | no | no | yes | no | no | no | no | +----------------------+----------------------+----------------------+------------------+--------------+--------------+------------------------------+--------------------------------------------+ | Haskell | yes | no | no | no | no | yes | no | +----------------------+----------------------+----------------------+------------------+--------------+--------------+------------------------------+--------------------------------------------+ - | Kotlin | yes | no | no | yes | no | no | yes | + | Kotlin | yes | no | yes | yes | no | no | yes | +----------------------+----------------------+----------------------+------------------+--------------+--------------+------------------------------+--------------------------------------------+ | VHDL | no | no | no | no | no | no | no | +----------------------+----------------------+----------------------+------------------+--------------+--------------+------------------------------+--------------------------------------------+ | Assembler | no | no | no | no | no | no | no | +----------------------+----------------------+----------------------+------------------+--------------+--------------+------------------------------+--------------------------------------------+ - | Swift | no | no | no | no | no | no | no | + | Swift | no | no | yes | no | no | no | no | +----------------------+----------------------+----------------------+------------------+--------------+--------------+------------------------------+--------------------------------------------+ | OCaml | no | no | no | no | no | yes | no | +----------------------+----------------------+----------------------+------------------+--------------+--------------+------------------------------+--------------------------------------------+ diff --git a/gradle.properties b/gradle.properties index 97c76323599e..94b9a219c377 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,6 +17,8 @@ mockito_version=4.8.0 fasterxml_version=2.13.4 jgit_version=6.3.0.202209071007-r checkstyle_version=10.3.3 +jplag_version=4.0.0 +slf4j_version=1.7.36 # gradle plugin version jib_plugin_version=2.0.0 diff --git a/src/main/java/de/tum/in/www1/artemis/domain/BonusStrategy.java b/src/main/java/de/tum/in/www1/artemis/domain/BonusStrategy.java index 471812a87841..85e4d902b2d1 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/BonusStrategy.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/BonusStrategy.java @@ -3,7 +3,7 @@ import static de.tum.in.www1.artemis.service.util.RoundingUtil.roundScoreSpecifiedByCourseSettings; -import org.apache.commons.lang.NotImplementedException; +import org.apache.commons.lang3.NotImplementedException; import de.tum.in.www1.artemis.repository.GradingScaleRepository; import de.tum.in.www1.artemis.web.rest.dto.BonusExampleDTO; diff --git a/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/PlagiarismComparison.java b/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/PlagiarismComparison.java index c14342c84de2..739dcb4aba15 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/PlagiarismComparison.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/PlagiarismComparison.java @@ -1,5 +1,6 @@ package de.tum.in.www1.artemis.domain.plagiarism; +import java.io.File; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -14,6 +15,7 @@ import de.jplag.JPlagComparison; import de.tum.in.www1.artemis.domain.DomainObject; +import de.tum.in.www1.artemis.domain.Exercise; import de.tum.in.www1.artemis.domain.plagiarism.text.TextSubmissionElement; /** @@ -63,7 +65,7 @@ public class PlagiarismComparison extends protected Set matches; /** - * Similarity of the compared submissions (between 0 and 1). + * Similarity of the compared submissions in percentage (between 0 and 100). */ @Column(name = "similarity") private double similarity; @@ -79,14 +81,17 @@ public class PlagiarismComparison extends * * @param jplagComparison JPlag comparison to map to the new PlagiarismComparison instance * @return a new instance with the content of the JPlagComparison + * @param exercise the exercise to which the comparison belongs, either Text or Programming + * @param submissionDirectory the directory to which all student submissions have been downloaded / stored */ - public static PlagiarismComparison fromJPlagComparison(JPlagComparison jplagComparison) { + public static PlagiarismComparison fromJPlagComparison(JPlagComparison jplagComparison, Exercise exercise, File submissionDirectory) { PlagiarismComparison comparison = new PlagiarismComparison<>(); - comparison.setSubmissionA(PlagiarismSubmission.fromJPlagSubmission(jplagComparison.getFirstSubmission())); - comparison.setSubmissionB(PlagiarismSubmission.fromJPlagSubmission(jplagComparison.getSecondSubmission())); - comparison.setMatches(jplagComparison.getMatches().stream().map(PlagiarismMatch::fromJPlagMatch).collect(Collectors.toSet())); - comparison.setSimilarity(jplagComparison.similarity()); + comparison.setSubmissionA(PlagiarismSubmission.fromJPlagSubmission(jplagComparison.firstSubmission(), exercise, submissionDirectory)); + comparison.setSubmissionB(PlagiarismSubmission.fromJPlagSubmission(jplagComparison.secondSubmission(), exercise, submissionDirectory)); + comparison.setMatches(jplagComparison.matches().stream().map(PlagiarismMatch::fromJPlagMatch).collect(Collectors.toSet())); + // Note: JPlag returns a value between 0 and 1, we assume and store a value between 0 and 100 (percentage) in the database + comparison.setSimilarity(jplagComparison.similarity() * 100); comparison.setStatus(PlagiarismStatus.NONE); return comparison; diff --git a/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/PlagiarismMatch.java b/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/PlagiarismMatch.java index 539d46fed04b..ba034ebedc5a 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/PlagiarismMatch.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/PlagiarismMatch.java @@ -36,9 +36,9 @@ public class PlagiarismMatch { */ public static PlagiarismMatch fromJPlagMatch(Match jplagMatch) { PlagiarismMatch match = new PlagiarismMatch(); - match.setStartA(jplagMatch.getStartOfFirst()); - match.setStartB(jplagMatch.getStartOfSecond()); - match.setLength(jplagMatch.getLength()); + match.setStartA(jplagMatch.startOfFirst()); + match.setStartB(jplagMatch.startOfSecond()); + match.setLength(jplagMatch.length()); return match; } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/PlagiarismSubmission.java b/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/PlagiarismSubmission.java index 98c7faa7e154..d0be3cfe22ff 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/PlagiarismSubmission.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/PlagiarismSubmission.java @@ -1,10 +1,10 @@ package de.tum.in.www1.artemis.domain.plagiarism; +import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; -import java.util.stream.StreamSupport; import javax.persistence.*; @@ -15,6 +15,7 @@ import de.jplag.Submission; import de.tum.in.www1.artemis.domain.DomainObject; +import de.tum.in.www1.artemis.domain.Exercise; import de.tum.in.www1.artemis.domain.modeling.ModelingSubmission; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.domain.plagiarism.modeling.ModelingSubmissionElement; @@ -84,9 +85,11 @@ public class PlagiarismSubmission extends * Create a new PlagiarismSubmission instance from an existing JPlag Submission * * @param jplagSubmission the JPlag Submission to create the PlagiarismSubmission from + * @param exercise the exercise to which the comparison belongs, either Text or Programming + * @param submissionDirectory the directory to which all student submissions have been downloaded / stored * @return a new PlagiarismSubmission instance */ - public static PlagiarismSubmission fromJPlagSubmission(Submission jplagSubmission) { + public static PlagiarismSubmission fromJPlagSubmission(Submission jplagSubmission, Exercise exercise, File submissionDirectory) { PlagiarismSubmission submission = new PlagiarismSubmission<>(); String[] submissionIdAndStudentLogin = jplagSubmission.getName().split("[-.]"); @@ -106,8 +109,8 @@ public static PlagiarismSubmission fromJPlagSubmission(Su } submission.setStudentLogin(studentLogin); - submission.setElements(StreamSupport.stream(jplagSubmission.getTokenList().allTokens().spliterator(), false).filter(Objects::nonNull) - .map(token -> TextSubmissionElement.fromJPlagToken(token, submission)).collect(Collectors.toCollection(ArrayList::new))); + submission.setElements(jplagSubmission.getTokenList().stream().filter(Objects::nonNull) + .map(token -> TextSubmissionElement.fromJPlagToken(token, submission, exercise, submissionDirectory)).collect(Collectors.toCollection(ArrayList::new))); submission.setSubmissionId(submissionId); submission.setSize(jplagSubmission.getNumberOfTokens()); submission.setScore(null); // TODO diff --git a/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/text/TextPlagiarismResult.java b/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/text/TextPlagiarismResult.java index 505bccd0e6d0..bcebb824ae60 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/text/TextPlagiarismResult.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/text/TextPlagiarismResult.java @@ -1,11 +1,11 @@ package de.tum.in.www1.artemis.domain.plagiarism.text; -import java.util.Comparator; - import javax.persistence.Entity; -import de.jplag.JPlagComparison; +import org.apache.commons.lang3.ArrayUtils; + import de.jplag.JPlagResult; +import de.tum.in.www1.artemis.domain.Exercise; import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismComparison; import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismResult; @@ -18,17 +18,23 @@ public class TextPlagiarismResult extends PlagiarismResult plagiarismSubmission) { + public static TextSubmissionElement fromJPlagToken(Token token, PlagiarismSubmission plagiarismSubmission, Exercise exercise, File submissionDirectory) { TextSubmissionElement textSubmissionElement = new TextSubmissionElement(); textSubmissionElement.setColumn(token.getColumn()); textSubmissionElement.setLine(token.getLine()); - textSubmissionElement.setFile(token.file); - textSubmissionElement.setType(token.type); + if (exercise instanceof ProgrammingExercise) { + // Note: for text submissions 'file' must be null + // Note: we want to get the relative path within the repository and not the absolute path + var submissionDirectoryAbsoluteFile = submissionDirectory.getAbsoluteFile(); + var tokenAbsoluteFile = token.getFile().getAbsoluteFile(); + var filePath = submissionDirectoryAbsoluteFile.toPath().relativize(tokenAbsoluteFile.toPath()); + // remove the first element, because it is the parent folder in which the whole repo was saved + var fileStringWithinRepository = filePath.toString(); + if (filePath.getNameCount() > 1) { + fileStringWithinRepository = filePath.subpath(1, filePath.getNameCount()).toString(); + } + textSubmissionElement.setFile(fileStringWithinRepository); + } textSubmissionElement.setLength(token.getLength()); textSubmissionElement.setPlagiarismSubmission(plagiarismSubmission); diff --git a/src/main/java/de/tum/in/www1/artemis/service/QuizBatchService.java b/src/main/java/de/tum/in/www1/artemis/service/QuizBatchService.java index 90af185bc85e..889a128c2183 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/QuizBatchService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/QuizBatchService.java @@ -6,7 +6,7 @@ import javax.annotation.Nullable; -import org.apache.commons.lang.RandomStringUtils; +import org.apache.commons.lang3.RandomStringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; diff --git a/src/main/java/de/tum/in/www1/artemis/service/ResourceLoaderService.java b/src/main/java/de/tum/in/www1/artemis/service/ResourceLoaderService.java index 735384ba8e11..a5ed4df9f948 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ResourceLoaderService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ResourceLoaderService.java @@ -7,7 +7,7 @@ import java.util.List; import java.util.Optional; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; diff --git a/src/main/java/de/tum/in/www1/artemis/service/SubmissionExportService.java b/src/main/java/de/tum/in/www1/artemis/service/SubmissionExportService.java index 797c6a2e294a..f139e6b9b091 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/SubmissionExportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/SubmissionExportService.java @@ -10,7 +10,7 @@ import javax.annotation.Nullable; -import org.apache.commons.lang.mutable.MutableInt; +import org.apache.commons.lang3.mutable.MutableInt; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; diff --git a/src/main/java/de/tum/in/www1/artemis/service/SubmissionPolicyService.java b/src/main/java/de/tum/in/www1/artemis/service/SubmissionPolicyService.java index c2c0fb9eccc4..11669d7f3965 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/SubmissionPolicyService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/SubmissionPolicyService.java @@ -1,6 +1,6 @@ package de.tum.in.www1.artemis.service; -import org.apache.commons.lang.NotImplementedException; +import org.apache.commons.lang3.NotImplementedException; import org.springframework.stereotype.Service; import de.tum.in.www1.artemis.domain.*; diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/GitService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/GitService.java index 81b61fa4fd3f..428537d5a1df 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/GitService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/GitService.java @@ -26,7 +26,7 @@ import org.apache.commons.io.FileUtils; import org.apache.commons.io.filefilter.HiddenFileFilter; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.eclipse.jgit.api.*; import org.eclipse.jgit.api.errors.CanceledException; import org.eclipse.jgit.api.errors.GitAPIException; diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/bamboo/BambooProgrammingLanguageFeatureService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/bamboo/BambooProgrammingLanguageFeatureService.java index 8983d12c9623..f8f0d857b653 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/bamboo/BambooProgrammingLanguageFeatureService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/bamboo/BambooProgrammingLanguageFeatureService.java @@ -22,10 +22,10 @@ public BambooProgrammingLanguageFeatureService() { programmingLanguageFeatures.put(PYTHON, new ProgrammingLanguageFeature(PYTHON, true, false, true, false, false, List.of())); programmingLanguageFeatures.put(C, new ProgrammingLanguageFeature(C, false, true, true, false, false, List.of(FACT, GCC))); programmingLanguageFeatures.put(HASKELL, new ProgrammingLanguageFeature(HASKELL, true, false, false, false, true, List.of())); - programmingLanguageFeatures.put(KOTLIN, new ProgrammingLanguageFeature(KOTLIN, true, false, false, true, false, List.of())); + programmingLanguageFeatures.put(KOTLIN, new ProgrammingLanguageFeature(KOTLIN, true, false, true, true, false, List.of())); programmingLanguageFeatures.put(VHDL, new ProgrammingLanguageFeature(VHDL, false, false, false, false, false, List.of())); programmingLanguageFeatures.put(ASSEMBLER, new ProgrammingLanguageFeature(ASSEMBLER, false, false, false, false, false, List.of())); - programmingLanguageFeatures.put(SWIFT, new ProgrammingLanguageFeature(SWIFT, false, true, false, true, false, List.of(PLAIN, XCODE))); + programmingLanguageFeatures.put(SWIFT, new ProgrammingLanguageFeature(SWIFT, false, true, true, true, false, List.of(PLAIN, XCODE))); programmingLanguageFeatures.put(OCAML, new ProgrammingLanguageFeature(OCAML, false, false, false, false, true, List.of())); } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/bamboo/dto/BambooBuildResultNotificationDTO.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/bamboo/dto/BambooBuildResultNotificationDTO.java index 2f426e13cc80..b5322fe7bf30 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/bamboo/dto/BambooBuildResultNotificationDTO.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/bamboo/dto/BambooBuildResultNotificationDTO.java @@ -7,7 +7,7 @@ import java.util.List; import java.util.Optional; -import org.apache.commons.lang.StringEscapeUtils; +import org.apache.commons.text.StringEscapeUtils; import com.fasterxml.jackson.annotation.*; @@ -104,7 +104,7 @@ public List extractBuildLogs(ProgrammingLanguage programmingLangu for (var job : getBuild().jobs()) { for (var bambooLog : job.logs()) { // We have to unescape the HTML as otherwise symbols like '<' are not displayed correctly - buildLogEntries.add(new BuildLogEntry(bambooLog.date(), StringEscapeUtils.unescapeHtml(bambooLog.log()))); + buildLogEntries.add(new BuildLogEntry(bambooLog.date(), StringEscapeUtils.unescapeHtml4(bambooLog.log()))); } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsBuildLogParseUtils.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsBuildLogParseUtils.java index 3b51118d3032..8102ba998e35 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsBuildLogParseUtils.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsBuildLogParseUtils.java @@ -7,7 +7,7 @@ import java.util.LinkedList; import java.util.List; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.jsoup.nodes.Element; import org.jsoup.nodes.Node; import org.jsoup.nodes.TextNode; diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsProgrammingLanguageFeatureService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsProgrammingLanguageFeatureService.java index 4d2c3c2638c5..bdbfa0749a1a 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsProgrammingLanguageFeatureService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsProgrammingLanguageFeatureService.java @@ -19,10 +19,10 @@ public JenkinsProgrammingLanguageFeatureService() { // Must be extended once a new programming language is added programmingLanguageFeatures.put(EMPTY, new ProgrammingLanguageFeature(EMPTY, false, false, false, false, false, List.of())); programmingLanguageFeatures.put(JAVA, new ProgrammingLanguageFeature(JAVA, true, true, true, true, false, List.of(PLAIN_GRADLE, GRADLE_GRADLE, PLAIN_MAVEN, MAVEN_MAVEN))); - programmingLanguageFeatures.put(KOTLIN, new ProgrammingLanguageFeature(KOTLIN, true, false, false, true, false, List.of())); + programmingLanguageFeatures.put(KOTLIN, new ProgrammingLanguageFeature(KOTLIN, true, false, true, true, false, List.of())); programmingLanguageFeatures.put(PYTHON, new ProgrammingLanguageFeature(PYTHON, false, false, true, false, false, List.of())); // Jenkins is not supporting XCODE at the moment - programmingLanguageFeatures.put(SWIFT, new ProgrammingLanguageFeature(SWIFT, false, true, false, true, false, List.of(PLAIN))); + programmingLanguageFeatures.put(SWIFT, new ProgrammingLanguageFeature(SWIFT, false, true, true, true, false, List.of(PLAIN))); programmingLanguageFeatures.put(C, new ProgrammingLanguageFeature(C, false, false, true, false, false, List.of(FACT, GCC))); programmingLanguageFeatures.put(HASKELL, new ProgrammingLanguageFeature(HASKELL, false, false, false, false, false, List.of())); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingPlagiarismDetectionService.java b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingPlagiarismDetectionService.java index 442b49342118..bd716b9c6a5b 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingPlagiarismDetectionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingPlagiarismDetectionService.java @@ -6,10 +6,13 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import javax.validation.constraints.NotNull; + import org.eclipse.jgit.api.errors.GitAPIException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,11 +21,11 @@ import de.jplag.JPlag; import de.jplag.JPlagResult; -import de.jplag.exceptions.BasecodeException; +import de.jplag.Language; +import de.jplag.clustering.ClusteringOptions; import de.jplag.exceptions.ExitException; import de.jplag.options.JPlagOptions; -import de.jplag.options.LanguageOption; -import de.jplag.reporting.Report; +import de.jplag.reporting.reportobject.ReportObjectFactory; import de.tum.in.www1.artemis.domain.PlagiarismCheckState; import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.Repository; @@ -35,7 +38,6 @@ import de.tum.in.www1.artemis.repository.plagiarism.PlagiarismResultRepository; import de.tum.in.www1.artemis.service.FileService; import de.tum.in.www1.artemis.service.UrlService; -import de.tum.in.www1.artemis.service.ZipFileService; import de.tum.in.www1.artemis.service.connectors.GitService; import de.tum.in.www1.artemis.service.plagiarism.cache.PlagiarismCacheService; import de.tum.in.www1.artemis.service.programming.ProgrammingExerciseExportService; @@ -54,8 +56,6 @@ public class ProgrammingPlagiarismDetectionService { private final ProgrammingExerciseRepository programmingExerciseRepository; - private final ZipFileService zipFileService; - private final GitService gitService; private final StudentParticipationRepository studentParticipationRepository; @@ -72,13 +72,12 @@ public class ProgrammingPlagiarismDetectionService { private final UrlService urlService; - public ProgrammingPlagiarismDetectionService(ProgrammingExerciseRepository programmingExerciseRepository, FileService fileService, ZipFileService zipFileService, - GitService gitService, StudentParticipationRepository studentParticipationRepository, PlagiarismResultRepository plagiarismResultRepository, + public ProgrammingPlagiarismDetectionService(ProgrammingExerciseRepository programmingExerciseRepository, FileService fileService, GitService gitService, + StudentParticipationRepository studentParticipationRepository, PlagiarismResultRepository plagiarismResultRepository, ProgrammingExerciseExportService programmingExerciseExportService, PlagiarismWebsocketService plagiarismWebsocketService, PlagiarismCacheService plagiarismCacheService, UrlService urlService) { this.programmingExerciseRepository = programmingExerciseRepository; this.fileService = fileService; - this.zipFileService = zipFileService; this.gitService = gitService; this.studentParticipationRepository = studentParticipationRepository; this.programmingExerciseExportService = programmingExerciseExportService; @@ -92,7 +91,7 @@ public ProgrammingPlagiarismDetectionService(ProgrammingExerciseRepository progr * downloads all repos of the exercise and runs JPlag * * @param programmingExerciseId the id of the programming exercises which should be checked - * @param similarityThreshold ignore comparisons whose similarity is below this threshold (%) + * @param similarityThreshold ignore comparisons whose similarity is below this threshold (in % between 0 and 100) * @param minimumScore consider only submissions whose score is greater or equal to this value * @return the text plagiarism result container with up to 500 comparisons with the highest similarity values * @throws ExitException is thrown if JPlag exits unexpectedly @@ -113,8 +112,8 @@ public TextPlagiarismResult checkPlagiarism(long programmingExerciseId, float si } plagiarismCacheService.setActivePlagiarismCheck(courseId); - JPlagResult result = getJPlagResult(programmingExercise, similarityThreshold, minimumScore); - if (result == null) { + JPlagResult jPlagResult = computeJPlagResult(programmingExercise, similarityThreshold, minimumScore); + if (jPlagResult == null) { log.info("Insufficient amount of submissions for plagiarism detection. Return empty result."); TextPlagiarismResult textPlagiarismResult = new TextPlagiarismResult(); textPlagiarismResult.setExercise(programmingExercise); @@ -127,10 +126,9 @@ public TextPlagiarismResult checkPlagiarism(long programmingExerciseId, float si return textPlagiarismResult; } - log.info("JPlag programming comparison finished with {} comparisons for programming exercise {}", result.getComparisons().size(), programmingExerciseId); + log.info("JPlag programming comparison finished with {} comparisons for programming exercise {}", jPlagResult.getAllComparisons().size(), programmingExerciseId); TextPlagiarismResult textPlagiarismResult = new TextPlagiarismResult(); - textPlagiarismResult.convertJPlagResult(result); - textPlagiarismResult.setExercise(programmingExercise); + textPlagiarismResult.convertJPlagResult(jPlagResult, programmingExercise); log.info("JPlag programming comparison done in {}", TimeLogUtil.formatDurationFrom(start)); plagiarismWebsocketService.notifyInstructorAboutPlagiarismState(topic, PlagiarismCheckState.COMPLETED, List.of()); @@ -146,22 +144,17 @@ public TextPlagiarismResult checkPlagiarism(long programmingExerciseId, float si * downloads all repos of the exercise and runs JPlag * * @param programmingExerciseId the id of the programming exercises which should be checked - * @param similarityThreshold ignore comparisons whose similarity is below this threshold (%) + * @param similarityThreshold ignore comparisons whose similarity is below this threshold (in % between 0 and 100) * @param minimumScore consider only submissions whose score is greater or equal to this value * @return a zip file that can be returned to the client - * @throws ExitException is thrown if JPlag exits unexpectedly - * @throws IOException is created the zip failed */ - public File checkPlagiarismWithJPlagReport(long programmingExerciseId, float similarityThreshold, int minimumScore) throws ExitException, IOException { + public File checkPlagiarismWithJPlagReport(long programmingExerciseId, float similarityThreshold, int minimumScore) { long start = System.nanoTime(); final var programmingExercise = programmingExerciseRepository.findWithAllParticipationsById(programmingExerciseId).get(); - JPlagResult result = getJPlagResult(programmingExercise, similarityThreshold, minimumScore); - if (result == null) { - return null; - } + JPlagResult result = computeJPlagResult(programmingExercise, similarityThreshold, minimumScore); - log.info("JPlag programming comparison finished with {} comparisons in {}", result.getComparisons().size(), TimeLogUtil.formatDurationFrom(start)); + log.info("JPlag programming comparison finished with {} comparisons in {}", result.getAllComparisons().size(), TimeLogUtil.formatDurationFrom(start)); return generateJPlagReportZip(result, programmingExercise); } @@ -169,12 +162,12 @@ public File checkPlagiarismWithJPlagReport(long programmingExerciseId, float sim * Checks for plagiarism and returns a JPlag result * * @param programmingExercise the programming exercise to check - * @param similarityThreshold the similarity threshold + * @param similarityThreshold the similarity threshold (in % between 0 and 100) * @param minimumScore the minimum score * @return the JPlag result or null if there are not enough participations - * @throws ExitException in case JPlag fails */ - private JPlagResult getJPlagResult(ProgrammingExercise programmingExercise, float similarityThreshold, int minimumScore) throws ExitException { + @NotNull + private JPlagResult computeJPlagResult(ProgrammingExercise programmingExercise, float similarityThreshold, int minimumScore) { long programmingExerciseId = programmingExercise.getId(); final var numberOfParticipations = programmingExercise.getStudentParticipations().size(); @@ -184,41 +177,48 @@ private JPlagResult getJPlagResult(ProgrammingExercise programmingExercise, floa List participations = filterStudentParticipationsForComparison(programmingExercise, minimumScore); if (participations.size() < 2) { - return null; + throw new BadRequestAlertException("Insufficient amount of valid and long enough submissions available for comparison", "Plagiarism Check", "notEnoughSubmissions"); } List repositories = downloadRepositories(programmingExercise, participations, targetPath); log.info("Downloading repositories done for programming exercise {}", programmingExerciseId); final var projectKey = programmingExercise.getProjectKey(); - final var repoFolder = Path.of(targetPath, projectKey).toString(); - final LanguageOption programmingLanguage = getJPlagProgrammingLanguage(programmingExercise); - + final var repoFolder = Path.of(targetPath, projectKey).toFile(); + final var programmingLanguage = getJPlagProgrammingLanguage(programmingExercise); final var templateRepoName = urlService.getRepositorySlugFromRepositoryUrl(programmingExercise.getTemplateParticipation().getVcsRepositoryUrl()); - JPlagOptions options = new JPlagOptions(repoFolder, programmingLanguage); + JPlagOptions options = new JPlagOptions(programmingLanguage, Set.of(repoFolder), Set.of()) + // JPlag expects a value between 0.0 and 1.0 + .withSimilarityThreshold(similarityThreshold / 100.0).withClusteringOptions(new ClusteringOptions().withEnabled(false)); if (templateRepoName != null) { - options.setBaseCodeSubmissionName(templateRepoName); + var templateFolder = Path.of(targetPath, projectKey, templateRepoName).toFile(); + options = options.withBaseCodeSubmissionDirectory(templateFolder); } - options.setSimilarityThreshold(similarityThreshold); - log.info("Start JPlag programming comparison for programming exercise {}", programmingExerciseId); String topic = plagiarismWebsocketService.getProgrammingExercisePlagiarismCheckTopic(programmingExerciseId); plagiarismWebsocketService.notifyInstructorAboutPlagiarismState(topic, PlagiarismCheckState.RUNNING, List.of("Running JPlag...")); JPlag jplag = new JPlag(options); - JPlagResult result = null; + JPlagResult result; try { result = jplag.run(); } - catch (BasecodeException e) { + catch (Exception e) { // Handling small or invalid base codes log.error(e.getMessage(), e); - log.info("Retrying JPlag Plagiarism Check without BaseCode"); - options.setBaseCodeSubmissionName(null); - jplag = new JPlag(options); - result = jplag.run(); + log.warn("Retrying JPlag Plagiarism Check without BaseCode"); + try { + options = options.withBaseCodeSubmissionDirectory(null); + jplag = new JPlag(options); + result = jplag.run(); + } + catch (Exception ex) { + log.info("FAILED: Retrying JPlag Plagiarism Check without BaseCode"); + log.error(ex.getMessage(), ex); + throw new BadRequestAlertException(ex.getMessage(), "Plagiarism Check", "jplagException"); + } } cleanupResourcesAsync(programmingExercise, repositories, targetPath); @@ -244,52 +244,30 @@ private void limitAndSavePlagiarismResult(TextPlagiarismResult textPlagiarismRes * @param jPlagResult The JPlag result * @param programmingExercise the programming exercise * @return the zip file - * @throws ExitException if JPlag fails - * @throws IOException if the zip file cannot be created */ - public File generateJPlagReportZip(JPlagResult jPlagResult, ProgrammingExercise programmingExercise) throws ExitException, IOException { + public File generateJPlagReportZip(JPlagResult jPlagResult, ProgrammingExercise programmingExercise) { final var targetPath = fileService.getUniquePathString(repoDownloadClonePath); - final var outputFolder = Path.of(targetPath, programmingExercise.getProjectKey() + "-output").toString(); - final var outputFolderFile = new File(outputFolder); + final var reportFolder = Path.of(targetPath, programmingExercise.getProjectKey() + " JPlag Report").toString(); + final var reportFolderFile = new File(reportFolder); // Create directories. - if (!outputFolderFile.mkdirs()) { - log.error("Cannot generate JPlag report because directories couldn't be created: {}", outputFolder); + if (!reportFolderFile.mkdirs()) { + log.error("Cannot generate JPlag report because directories couldn't be created: {}", reportFolder); return null; } // Write JPlag report result to the file. - log.info("Write JPlag report to file system"); - Report jplagReport = new Report(outputFolderFile, jPlagResult.getOptions()); - jplagReport.writeResult(jPlagResult); - - // Zip the file - var zipFile = zipJPlagReport(programmingExercise, targetPath, Path.of(outputFolder)); + log.info("Write JPlag report to file system and zip it"); + ReportObjectFactory reportObjectFactory = new ReportObjectFactory(); + reportObjectFactory.createAndSaveReport(jPlagResult, reportFolder); + // JPlag automatically zips the report + var zipFile = new File(reportFolder + ".zip"); + fileService.scheduleForDeletion(zipFile.getAbsoluteFile().toPath(), 1); fileService.scheduleForDirectoryDeletion(Path.of(targetPath), 2); return zipFile; } - /** - * Zips a JPlag report. - * - * @param programmingExercise the programming exercise - * @param targetPath the path where the zip file will be created - * @param outputFolderPath the path of the Jplag report - * @return the zip file - * @throws IOException if the zip file cannot be created - */ - private File zipJPlagReport(ProgrammingExercise programmingExercise, String targetPath, Path outputFolderPath) throws IOException { - log.info("JPlag report zipping to {}", targetPath); - final var courseShortName = programmingExercise.getCourseViaExerciseGroupOrCourseMember().getShortName(); - final var filename = courseShortName + "-" + programmingExercise.getShortName() + "-" + System.currentTimeMillis() + "-Jplag-Analysis-Output.zip"; - final var zipFilePath = Path.of(targetPath, filename); - zipFileService.createZipFileWithFolderContent(zipFilePath, outputFolderPath, null); - log.info("JPlag report zipped. Schedule deletion of zip file in 1 minute"); - fileService.scheduleForDeletion(zipFilePath, 1); - return new File(zipFilePath.toString()); - } - private void cleanupResourcesAsync(final ProgrammingExercise programmingExercise, final List repositories, final String targetPath) { executor.schedule(() -> { log.info("Will delete local repositories for programming exercise {}", programmingExercise.getId()); @@ -330,11 +308,13 @@ public void deleteTempLocalRepository(Repository repository) { } } - private LanguageOption getJPlagProgrammingLanguage(ProgrammingExercise programmingExercise) { + private Language getJPlagProgrammingLanguage(ProgrammingExercise programmingExercise) { return switch (programmingExercise.getProgrammingLanguage()) { - case JAVA -> LanguageOption.JAVA; - case C -> LanguageOption.C_CPP; - case PYTHON -> LanguageOption.PYTHON_3; + case JAVA -> new de.jplag.java.Language(); + case C -> new de.jplag.cpp.Language(); + case PYTHON -> new de.jplag.python3.Language(); + case SWIFT -> new de.jplag.swift.Language(); + case KOTLIN -> new de.jplag.kotlin.Language(); default -> throw new BadRequestAlertException("Programming language " + programmingExercise.getProgrammingLanguage() + " not supported for plagiarism check.", "ProgrammingExercise", "notSupported"); }; @@ -370,7 +350,7 @@ private List downloadRepositories(ProgrammingExercise programmingExe var topic = plagiarismWebsocketService.getProgrammingExercisePlagiarismCheckTopic(programmingExercise.getId()); List downloadedRepositories = new ArrayList<>(); - participations.forEach(participation -> { + participations.parallelStream().forEach(participation -> { try { var progressMessage = "Downloading repositories: " + (downloadedRepositories.size() + 1) + "/" + participations.size(); plagiarismWebsocketService.notifyInstructorAboutPlagiarismState(topic, PlagiarismCheckState.RUNNING, List.of(progressMessage)); diff --git a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/TextPlagiarismDetectionService.java b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/TextPlagiarismDetectionService.java index fe1a63d81caa..8b8e56241f68 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/TextPlagiarismDetectionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/TextPlagiarismDetectionService.java @@ -5,6 +5,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import org.slf4j.Logger; @@ -14,9 +15,10 @@ import de.jplag.JPlag; import de.jplag.JPlagResult; +import de.jplag.Language; +import de.jplag.clustering.ClusteringOptions; import de.jplag.exceptions.ExitException; import de.jplag.options.JPlagOptions; -import de.jplag.options.LanguageOption; import de.tum.in.www1.artemis.domain.PlagiarismCheckState; import de.tum.in.www1.artemis.domain.TextExercise; import de.tum.in.www1.artemis.domain.TextSubmission; @@ -71,7 +73,7 @@ public List textSubmissionsForComparison(TextExercise exerciseWi * Download all submissions of the exercise, run JPlag, and return the result * * @param textExercise to detect plagiarism for - * @param similarityThreshold ignore comparisons whose similarity is below this threshold (%) + * @param similarityThreshold ignore comparisons whose similarity is below this threshold (in % between 0 and 100) * @param minimumScore consider only submissions whose score is greater or equal to this value * @param minimumSize consider only submissions whose size is greater or equal to this value * @return a zip file that can be returned to the client @@ -100,12 +102,8 @@ public TextPlagiarismResult checkPlagiarism(TextExercise textExercise, float sim log.info("Save text submissions for JPlag text comparison with {} submissions", submissionsSize); if (textSubmissions.size() < 2) { - log.info("Insufficient amount of submissions for plagiarism detection. Return empty result."); - TextPlagiarismResult textPlagiarismResult = new TextPlagiarismResult(); - textPlagiarismResult.setExercise(textExercise); - textPlagiarismResult.setSimilarityDistribution(new int[0]); - - return textPlagiarismResult; + log.info("Insufficient amount of submissions for plagiarism detection. Inform the client with a bad request response."); + throw new BadRequestAlertException("Insufficient amount of valid and long enough submissions available for comparison", "Plagiarism Check", "notEnoughSubmissions"); } AtomicInteger processedSubmissionCount = new AtomicInteger(1); @@ -135,17 +133,17 @@ public TextPlagiarismResult checkPlagiarism(TextExercise textExercise, float sim log.info("Saving text submissions done"); - JPlagOptions options = new JPlagOptions(submissionsFolderName, LanguageOption.TEXT); - options.setMinimumTokenMatch(minimumSize); - // Important: for large courses with more than 1000 students, we might get more than one million results and 10 million files in the file system due to many 0% results, // therefore we limit the results to at least 50% or 0.5 similarity, the passed threshold is between 0 and 100% - options.setSimilarityThreshold(similarityThreshold); + Language language = new de.jplag.text.Language(); + JPlagOptions options = new JPlagOptions(language, Set.of(submissionFolderFile), Set.of()) + // JPlag expects a value between 0.0 and 1.0 + .withSimilarityThreshold(similarityThreshold / 100.0).withClusteringOptions(new ClusteringOptions().withEnabled(false)); log.info("Start JPlag Text comparison"); JPlag jplag = new JPlag(options); JPlagResult jPlagResult = jplag.run(); - log.info("JPlag Text comparison finished with {} comparisons. Will limit the number of comparisons to 500", jPlagResult.getComparisons().size()); + log.info("JPlag Text comparison finished with {} comparisons. Will limit the number of comparisons to 500", jPlagResult.getAllComparisons().size()); log.info("Delete submission folder"); if (submissionFolderFile.exists()) { @@ -153,13 +151,16 @@ public TextPlagiarismResult checkPlagiarism(TextExercise textExercise, float sim } TextPlagiarismResult textPlagiarismResult = new TextPlagiarismResult(); - textPlagiarismResult.convertJPlagResult(jPlagResult); - textPlagiarismResult.setExercise(textExercise); + textPlagiarismResult.convertJPlagResult(jPlagResult, textExercise); log.info("JPlag text comparison for {} submissions done in {}", submissionsSize, TimeLogUtil.formatDurationFrom(start)); plagiarismWebsocketService.notifyInstructorAboutPlagiarismState(topic, PlagiarismCheckState.COMPLETED, List.of()); return textPlagiarismResult; } + catch (Exception ex) { + log.warn("Text plagiarism detection NOT successful", ex); + throw new BadRequestAlertException(ex.getMessage(), "Plagiarism Check", "jplagException"); + } finally { plagiarismCacheService.setInactivePlagiarismCheck(courseId); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/FileResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/FileResource.java index b914bd529671..6e2fb5dc167f 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/FileResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/FileResource.java @@ -12,7 +12,7 @@ import javax.activation.MimetypesFileTypeMap; import org.apache.commons.io.IOUtils; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.io.Resource; diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingExerciseResource.java index c05ed93ab0a4..17548feeb9e7 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingExerciseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingExerciseResource.java @@ -418,7 +418,7 @@ public ResponseEntity getPlagiarismResult(@PathVariabl * Start the automated plagiarism detection for the given exercise and return its result. * * @param exerciseId for which all submission should be checked - * @param similarityThreshold ignore comparisons whose similarity is below this threshold (%) + * @param similarityThreshold ignore comparisons whose similarity is below this threshold (in % between 0 and 100) * @param minimumScore consider only submissions whose score is greater or equal to this * value * @param minimumSize consider only submissions whose size is greater or equal to this diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExercisePlagiarismResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExercisePlagiarismResource.java index c94f94eb40c0..a5b6ce3f880e 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExercisePlagiarismResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExercisePlagiarismResource.java @@ -31,7 +31,7 @@ import de.tum.in.www1.artemis.service.programming.ProgrammingLanguageFeature; import de.tum.in.www1.artemis.service.programming.ProgrammingLanguageFeatureService; import de.tum.in.www1.artemis.service.util.TimeLogUtil; -import de.tum.in.www1.artemis.web.rest.util.HeaderUtil; +import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; /** * REST controller for managing ProgrammingExercise. @@ -89,7 +89,7 @@ public ResponseEntity getPlagiarismResult(@PathVariable lo * GET /programming-exercises/{exerciseId}/check-plagiarism : Start the automated plagiarism detection for the given exercise and return its result. * * @param exerciseId The ID of the programming exercise for which the plagiarism check should be executed - * @param similarityThreshold ignore comparisons whose similarity is below this threshold (%) + * @param similarityThreshold ignore comparisons whose similarity is below this threshold (in % between 0 and 100) * @param minimumScore consider only submissions whose score is greater or equal to this value * @return the ResponseEntity with status 200 (OK) and the list of at most 500 pair-wise submissions with a similarity above the given threshold (e.g. 50%). * @throws ExitException is thrown if JPlag exits unexpectedly @@ -106,8 +106,8 @@ public ResponseEntity checkPlagiarism(@PathVariable long e ProgrammingLanguageFeature programmingLanguageFeature = programmingLanguageFeatureService.get().getProgrammingLanguageFeatures(language); if (!programmingLanguageFeature.isPlagiarismCheckSupported()) { - return ResponseEntity.badRequest().headers(HeaderUtil.createFailureAlert(applicationName, true, ENTITY_NAME, "programmingLanguageNotSupported", - "Artemis does not support plagiarism checks for the programming language " + language)).body(null); + throw new BadRequestAlertException("Artemis does not support plagiarism checks for the programming language " + programmingExercise.getProgrammingLanguage(), + ENTITY_NAME, "programmingLanguageNotSupported"); } long start = System.nanoTime(); @@ -123,32 +123,28 @@ public ResponseEntity checkPlagiarism(@PathVariable long e * GET /programming-exercises/{exerciseId}/plagiarism-check : Uses JPlag to check for plagiarism and returns the generated output as zip file * * @param exerciseId The ID of the programming exercise for which the plagiarism check should be executed - * @param similarityThreshold ignore comparisons whose similarity is below this threshold (%) + * @param similarityThreshold ignore comparisons whose similarity is below this threshold (in % between 0 and 100) * @param minimumScore consider only submissions whose score is greater or equal to this value * @return The ResponseEntity with status 201 (Created) or with status 400 (Bad Request) if the parameters are invalid - * @throws ExitException is thrown if JPlag exits unexpectedly * @throws IOException is thrown for file handling errors */ - @GetMapping(value = CHECK_PLAGIARISM_JPLAG_REPORT, produces = MediaType.TEXT_PLAIN_VALUE) + @GetMapping(value = CHECK_PLAGIARISM_JPLAG_REPORT) @PreAuthorize("hasRole('EDITOR')") @FeatureToggle(Feature.ProgrammingExercises) public ResponseEntity checkPlagiarismWithJPlagReport(@PathVariable long exerciseId, @RequestParam float similarityThreshold, @RequestParam int minimumScore) - throws ExitException, IOException { + throws IOException { log.debug("REST request to check plagiarism for ProgrammingExercise with id: {}", exerciseId); ProgrammingExercise programmingExercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.EDITOR, programmingExercise, null); - ProgrammingLanguageFeature programmingLanguageFeature = programmingLanguageFeatureService.get() - .getProgrammingLanguageFeatures(programmingExercise.getProgrammingLanguage()); + var programmingLanguageFeature = programmingLanguageFeatureService.get().getProgrammingLanguageFeatures(programmingExercise.getProgrammingLanguage()); if (!programmingLanguageFeature.isPlagiarismCheckSupported()) { - return ResponseEntity.badRequest().headers(HeaderUtil.createFailureAlert(applicationName, true, ENTITY_NAME, "programmingLanguageNotSupported", - "Artemis does not support plagiarism checks for the programming language " + programmingExercise.getProgrammingLanguage())).body(null); + throw new BadRequestAlertException("Artemis does not support plagiarism checks for the programming language " + programmingExercise.getProgrammingLanguage(), + "Plagiarism Check", "programmingLanguageNotSupported"); } File zipFile = programmingPlagiarismDetectionService.checkPlagiarismWithJPlagReport(exerciseId, similarityThreshold, minimumScore); if (zipFile == null) { - return ResponseEntity.badRequest().headers( - HeaderUtil.createFailureAlert(applicationName, true, ENTITY_NAME, "internalServerError", "Insufficient amount of comparisons available for comparison.")) - .body(null); + throw new BadRequestAlertException("Insufficient amount of valid and long enough submissions available for comparison.", "Plagiarism Check", "notEnoughSubmissions"); } InputStreamResource resource = new InputStreamResource(new FileInputStream(zipFile)); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java index 54f771616d04..73efa2a422eb 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java @@ -474,7 +474,7 @@ public ResponseEntity getPlagiarismResult(@PathVariable lo * Start the automated plagiarism detection for the given exercise and return its result. * * @param exerciseId ID of the exercise for which to detect plagiarism - * @param similarityThreshold ignore comparisons whose similarity is below this threshold (%) + * @param similarityThreshold ignore comparisons whose similarity is below this threshold (in % between 0 and 100) * @param minimumScore consider only submissions whose score is greater or equal to this value * @param minimumSize consider only submissions whose size is greater or equal to this value * @return the ResponseEntity with status 200 (OK) and the list of at most 500 pair-wise submissions with a similarity above the given threshold (e.g. 50%). diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/util/StringUtil.java b/src/main/java/de/tum/in/www1/artemis/web/rest/util/StringUtil.java index 69c0592b7d8c..9679153e1be9 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/util/StringUtil.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/util/StringUtil.java @@ -1,6 +1,6 @@ package de.tum.in.www1.artemis.web.rest.util; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; /** * Utility class for String manipulation diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.html b/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.html index 78b9df4a2aaa..7dfc2fd3fd07 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.html +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.html @@ -75,7 +75,7 @@

Programming Exercise Dashboard (); dialogError$ = this.dialogErrorSource.asObservable(); @@ -116,6 +119,7 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { private programmingExerciseGradingService: ProgrammingExerciseGradingService, private codeHintService: CodeHintService, private router: Router, + private programmingLanguageFeatureService: ProgrammingLanguageFeatureService, ) {} ngOnInit() { @@ -197,6 +201,10 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { }); this.setLatestCoveredLineRatio(); + + this.plagiarismCheckSupported = this.programmingLanguageFeatureService.getProgrammingLanguageFeature( + programmingExercise.programmingLanguage, + ).plagiarismCheckSupported; }); this.statisticsService.getExerciseStatistics(exerciseId!).subscribe((statistics: ExerciseManagementStatisticsDto) => { @@ -296,7 +304,7 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { }); } - public deleteProgrammingExercise(event: { [key: string]: boolean }) { + deleteProgrammingExercise(event: { [key: string]: boolean }) { this.programmingExerciseService.delete(this.programmingExercise.id!, event.deleteStudentReposBuildPlans, event.deleteBaseReposBuildPlans).subscribe({ next: () => { this.eventManager.broadcast({ @@ -330,7 +338,7 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { /** * Unlocks all repositories that belong to the exercise */ - private unlockAllRepositories() { + unlockAllRepositories() { this.lockingOrUnlockingRepositories = true; this.programmingExerciseService.unlockAllRepositories(this.programmingExercise.id!).subscribe({ next: (res) => { diff --git a/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-inspector/plagiarism-inspector.component.html b/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-inspector/plagiarism-inspector.component.html index 3dcd049b25e7..7627511f62a8 100644 --- a/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-inspector/plagiarism-inspector.component.html +++ b/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-inspector/plagiarism-inspector.component.html @@ -88,7 +88,7 @@