From 6926ebf30d7fa5834b176b9899bba6493c230587 Mon Sep 17 00:00:00 2001 From: jakubriegel Date: Thu, 7 Sep 2023 19:53:43 +0200 Subject: [PATCH 01/95] extract plagiarism detection service --- .../plagiarism/PlagiarismDetectionConfig.java | 33 +++++ .../PlagiarismDetectionService.java | 123 ++++++++++++++++ ...portedForPlagiarismDetectionException.java | 10 ++ ...ProgrammingPlagiarismDetectionService.java | 2 +- .../TextPlagiarismDetectionService.java | 2 +- .../web/rest/ModelingExerciseResource.java | 26 ++-- ...ProgrammingExercisePlagiarismResource.java | 67 +++++---- .../web/rest/TextExerciseResource.java | 25 ++-- .../PlagiarismDetectionServiceTest.java | 138 ++++++++++++++++++ 9 files changed, 362 insertions(+), 64 deletions(-) create mode 100644 src/main/java/de/tum/in/www1/artemis/domain/plagiarism/PlagiarismDetectionConfig.java create mode 100644 src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionService.java create mode 100644 src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingLanguageNotSupportedForPlagiarismDetectionException.java create mode 100644 src/test/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionServiceTest.java diff --git a/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/PlagiarismDetectionConfig.java b/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/PlagiarismDetectionConfig.java new file mode 100644 index 000000000000..f4bd4a97bf73 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/PlagiarismDetectionConfig.java @@ -0,0 +1,33 @@ +package de.tum.in.www1.artemis.domain.plagiarism; + +import de.tum.in.www1.artemis.domain.DomainObject; + +/** + * Stores configuration for manual and continuous plagiarism control. + */ +public class PlagiarismDetectionConfig extends DomainObject { + + private final float similarityThreshold; + + private final int minimumScore; + + private final int minimumSize; + + public PlagiarismDetectionConfig(float similarityThreshold, int minimumScore, int minimumSize) { + this.similarityThreshold = similarityThreshold; + this.minimumScore = minimumScore; + this.minimumSize = minimumSize; + } + + public float getSimilarityThreshold() { + return similarityThreshold; + } + + public int getMinimumScore() { + return minimumScore; + } + + public int getMinimumSize() { + return minimumSize; + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionService.java b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionService.java new file mode 100644 index 000000000000..2a85de2228ee --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionService.java @@ -0,0 +1,123 @@ +package de.tum.in.www1.artemis.service.plagiarism; + +import java.io.File; +import java.io.IOException; +import java.util.Optional; + +import org.jvnet.hk2.annotations.Service; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import de.jplag.exceptions.ExitException; +import de.tum.in.www1.artemis.domain.ProgrammingExercise; +import de.tum.in.www1.artemis.domain.TextExercise; +import de.tum.in.www1.artemis.domain.modeling.ModelingExercise; +import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismDetectionConfig; +import de.tum.in.www1.artemis.domain.plagiarism.modeling.ModelingPlagiarismResult; +import de.tum.in.www1.artemis.domain.plagiarism.text.TextPlagiarismResult; +import de.tum.in.www1.artemis.repository.plagiarism.PlagiarismResultRepository; +import de.tum.in.www1.artemis.service.programming.ProgrammingLanguageFeatureService; + +@Service +@Component +public class PlagiarismDetectionService { + + private static final Logger log = LoggerFactory.getLogger(PlagiarismDetectionService.class); + + private final TextPlagiarismDetectionService textPlagiarismDetectionService; + + private final Optional programmingLanguageFeatureService; + + private final ProgrammingPlagiarismDetectionService programmingPlagiarismDetectionService; + + private final ModelingPlagiarismDetectionService modelingPlagiarismDetectionService; + + private final PlagiarismResultRepository plagiarismResultRepository; + + public PlagiarismDetectionService(TextPlagiarismDetectionService textPlagiarismDetectionService, Optional programmingLanguageFeatureService, + ProgrammingPlagiarismDetectionService programmingPlagiarismDetectionService, ModelingPlagiarismDetectionService modelingPlagiarismDetectionService, + PlagiarismResultRepository plagiarismResultRepository) { + this.textPlagiarismDetectionService = textPlagiarismDetectionService; + this.programmingLanguageFeatureService = programmingLanguageFeatureService; + this.programmingPlagiarismDetectionService = programmingPlagiarismDetectionService; + this.modelingPlagiarismDetectionService = modelingPlagiarismDetectionService; + this.plagiarismResultRepository = plagiarismResultRepository; + } + + /** + * Check plagiarism in given text exercise + * + * @param exercise exercise to check plagiarism + * @return result of plagiarism checks + */ + public TextPlagiarismResult checkTextExercise(TextExercise exercise, PlagiarismDetectionConfig config) throws ExitException { + var plagiarismResult = textPlagiarismDetectionService.checkPlagiarism(exercise, config.getSimilarityThreshold(), config.getMinimumScore(), config.getMinimumSize()); + log.info("Finished textPlagiarismDetectionService.checkPlagiarism for exercise {} with {} comparisons,", exercise.getId(), plagiarismResult.getComparisons().size()); + + // TODO: limit the amount temporarily because of database issues + plagiarismResult.sortAndLimit(100); + plagiarismResultRepository.savePlagiarismResultAndRemovePrevious(plagiarismResult); + + plagiarismResultRepository.prepareResultForClient(plagiarismResult); + return plagiarismResult; + } + + private void checkProgrammingLanguageSupport(ProgrammingExercise exercise) throws ProgrammingLanguageNotSupportedForPlagiarismDetectionException { + var language = exercise.getProgrammingLanguage(); + var programmingLanguageFeature = programmingLanguageFeatureService.orElseThrow().getProgrammingLanguageFeatures(language); + if (!programmingLanguageFeature.plagiarismCheckSupported()) { + throw new ProgrammingLanguageNotSupportedForPlagiarismDetectionException(language); + } + } + + /** + * Check plagiarism in given programing exercise + * + * @param exercise exercise to check plagiarism + * @return result of plagiarism checks + */ + public TextPlagiarismResult checkProgrammingExercise(ProgrammingExercise exercise, PlagiarismDetectionConfig config) + throws ExitException, IOException, ProgrammingLanguageNotSupportedForPlagiarismDetectionException { + checkProgrammingLanguageSupport(exercise); + + var plagiarismResult = programmingPlagiarismDetectionService.checkPlagiarism(exercise.getId(), config.getSimilarityThreshold(), config.getMinimumScore()); + log.info("Finished programmingExerciseExportService.checkPlagiarism call for {} comparisons", plagiarismResult.getComparisons().size()); + + plagiarismResultRepository.prepareResultForClient(plagiarismResult); + + // make sure that participation is included in the exercise + plagiarismResult.setExercise(exercise); + return plagiarismResult; + } + + /** + * Check plagiarism in given programing exercise and outputs a Jplag report + * + * @param exercise exercise to check plagiarism + * @return Jplag report of plagiarism checks + */ + public File checkProgrammingExerciseWithJplagReport(ProgrammingExercise exercise, PlagiarismDetectionConfig config) + throws ProgrammingLanguageNotSupportedForPlagiarismDetectionException { + checkProgrammingLanguageSupport(exercise); + return programmingPlagiarismDetectionService.checkPlagiarismWithJPlagReport(exercise.getId(), config.getSimilarityThreshold(), config.getMinimumScore()); + } + + /** + * Check plagiarism in given modeling exercise + * + * @param exercise exercise to check plagiarism + * @return result of plagiarism checks + */ + public ModelingPlagiarismResult checkModelingExercise(ModelingExercise exercise, PlagiarismDetectionConfig config) { + var plagiarismResult = modelingPlagiarismDetectionService.checkPlagiarism(exercise, config.getSimilarityThreshold(), config.getMinimumSize(), config.getMinimumScore()); + log.info("Finished modelingPlagiarismDetectionService.checkPlagiarism call for {} comparisons", plagiarismResult.getComparisons().size()); + + // TODO: limit the amount temporarily because of database issues + plagiarismResult.sortAndLimit(100); + plagiarismResultRepository.savePlagiarismResultAndRemovePrevious(plagiarismResult); + + plagiarismResultRepository.prepareResultForClient(plagiarismResult); + return plagiarismResult; + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingLanguageNotSupportedForPlagiarismDetectionException.java b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingLanguageNotSupportedForPlagiarismDetectionException.java new file mode 100644 index 000000000000..756feb454337 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingLanguageNotSupportedForPlagiarismDetectionException.java @@ -0,0 +1,10 @@ +package de.tum.in.www1.artemis.service.plagiarism; + +import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; + +public class ProgrammingLanguageNotSupportedForPlagiarismDetectionException extends Exception { + + ProgrammingLanguageNotSupportedForPlagiarismDetectionException(ProgrammingLanguage language) { + super("Artemis does not support plagiarism checks for the programming language " + language); + } +} 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 6ecdb4db702e..dc013841915c 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 @@ -45,7 +45,7 @@ import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; @Service -public class ProgrammingPlagiarismDetectionService { +class ProgrammingPlagiarismDetectionService { @Value("${artemis.repo-download-clone-path}") private Path repoDownloadClonePath; 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 8b8e56241f68..f10d60b81c17 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 @@ -31,7 +31,7 @@ import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; @Service -public class TextPlagiarismDetectionService { +class TextPlagiarismDetectionService { private final Logger log = LoggerFactory.getLogger(TextPlagiarismDetectionService.class); 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 b6225019b87d..cebd595846ee 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 @@ -20,6 +20,7 @@ import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.metis.conversation.Channel; import de.tum.in.www1.artemis.domain.modeling.ModelingExercise; +import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismDetectionConfig; import de.tum.in.www1.artemis.domain.plagiarism.modeling.ModelingPlagiarismResult; import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.repository.metis.conversation.ChannelRepository; @@ -34,7 +35,7 @@ import de.tum.in.www1.artemis.service.metis.conversation.ChannelService; import de.tum.in.www1.artemis.service.metis.conversation.ConversationService; import de.tum.in.www1.artemis.service.notifications.GroupNotificationScheduleService; -import de.tum.in.www1.artemis.service.plagiarism.ModelingPlagiarismDetectionService; +import de.tum.in.www1.artemis.service.plagiarism.PlagiarismDetectionService; import de.tum.in.www1.artemis.service.util.TimeLogUtil; import de.tum.in.www1.artemis.web.rest.dto.PageableSearchDTO; import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; @@ -86,7 +87,7 @@ public class ModelingExerciseResource { private final GradingCriterionRepository gradingCriterionRepository; - private final ModelingPlagiarismDetectionService modelingPlagiarismDetectionService; + private final PlagiarismDetectionService plagiarismDetectionService; private final ChannelService channelService; @@ -99,8 +100,7 @@ public ModelingExerciseResource(ModelingExerciseRepository modelingExerciseRepos ModelingExerciseService modelingExerciseService, ExerciseDeletionService exerciseDeletionService, PlagiarismResultRepository plagiarismResultRepository, ModelingExerciseImportService modelingExerciseImportService, SubmissionExportService modelingSubmissionExportService, ExerciseService exerciseService, GroupNotificationScheduleService groupNotificationScheduleService, GradingCriterionRepository gradingCriterionRepository, - ModelingPlagiarismDetectionService modelingPlagiarismDetectionService, ChannelService channelService, ConversationService conversationService, - ChannelRepository channelRepository) { + PlagiarismDetectionService plagiarismDetectionService, ChannelService channelService, ConversationService conversationService, ChannelRepository channelRepository) { this.modelingExerciseRepository = modelingExerciseRepository; this.courseService = courseService; this.modelingExerciseService = modelingExerciseService; @@ -115,7 +115,7 @@ public ModelingExerciseResource(ModelingExerciseRepository modelingExerciseRepos this.groupNotificationScheduleService = groupNotificationScheduleService; this.exerciseService = exerciseService; this.gradingCriterionRepository = gradingCriterionRepository; - this.modelingPlagiarismDetectionService = modelingPlagiarismDetectionService; + this.plagiarismDetectionService = plagiarismDetectionService; this.channelService = channelService; this.conversationService = conversationService; this.channelRepository = channelRepository; @@ -396,17 +396,11 @@ public ResponseEntity checkPlagiarism(@PathVariable lo var modelingExercise = modelingExerciseRepository.findByIdWithStudentParticipationsSubmissionsResultsElseThrow(exerciseId); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.INSTRUCTOR, modelingExercise, null); long start = System.nanoTime(); - log.info("Start modelingPlagiarismDetectionService.checkPlagiarism for exercise {}", exerciseId); - var plagiarismResult = modelingPlagiarismDetectionService.checkPlagiarism(modelingExercise, similarityThreshold / 100, minimumSize, minimumScore); - log.info("Finished modelingPlagiarismDetectionService.checkPlagiarism call for {} comparisons in {}", plagiarismResult.getComparisons().size(), - TimeLogUtil.formatDurationFrom(start)); - // TODO: limit the amount temporarily because of database issues - plagiarismResult.sortAndLimit(100); - log.info("Limited number of comparisons to {} to avoid performance issues when saving to database", plagiarismResult.getComparisons().size()); - start = System.nanoTime(); - plagiarismResultRepository.savePlagiarismResultAndRemovePrevious(plagiarismResult); - log.info("Finished plagiarismResultRepository.savePlagiarismResultAndRemovePrevious call in {}", TimeLogUtil.formatDurationFrom(start)); - plagiarismResultRepository.prepareResultForClient(plagiarismResult); + log.info("Started manual plagiarism checks for modeling exercise: exerciseId={}.", exerciseId); + var config = new PlagiarismDetectionConfig(similarityThreshold, minimumScore, minimumSize); + var plagiarismResult = plagiarismDetectionService.checkModelingExercise(modelingExercise, config); + log.info("Finished manual plagiarism checks for modeling exercise: exerciseId={}, elapsed={}.", exerciseId, TimeLogUtil.formatDurationFrom(start)); + return ResponseEntity.ok(plagiarismResult); } 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 b170639fbb0d..de1f3c777eb6 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 @@ -2,7 +2,6 @@ import static de.tum.in.www1.artemis.web.rest.ProgrammingExerciseResourceEndpoints.*; -import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.Optional; @@ -18,7 +17,7 @@ import de.jplag.exceptions.ExitException; import de.tum.in.www1.artemis.domain.ProgrammingExercise; -import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; +import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismDetectionConfig; import de.tum.in.www1.artemis.domain.plagiarism.text.TextPlagiarismResult; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.plagiarism.PlagiarismResultRepository; @@ -27,8 +26,8 @@ import de.tum.in.www1.artemis.service.AuthorizationCheckService; import de.tum.in.www1.artemis.service.feature.Feature; import de.tum.in.www1.artemis.service.feature.FeatureToggle; -import de.tum.in.www1.artemis.service.plagiarism.ProgrammingPlagiarismDetectionService; -import de.tum.in.www1.artemis.service.programming.ProgrammingLanguageFeature; +import de.tum.in.www1.artemis.service.plagiarism.PlagiarismDetectionService; +import de.tum.in.www1.artemis.service.plagiarism.ProgrammingLanguageNotSupportedForPlagiarismDetectionException; 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.errors.BadRequestAlertException; @@ -55,16 +54,16 @@ public class ProgrammingExercisePlagiarismResource { private final Optional programmingLanguageFeatureService; - private final ProgrammingPlagiarismDetectionService programmingPlagiarismDetectionService; + private final PlagiarismDetectionService plagiarismDetectionService; public ProgrammingExercisePlagiarismResource(ProgrammingExerciseRepository programmingExerciseRepository, AuthorizationCheckService authCheckService, PlagiarismResultRepository plagiarismResultRepository, Optional programmingLanguageFeatureService, - ProgrammingPlagiarismDetectionService programmingPlagiarismDetectionService) { + PlagiarismDetectionService plagiarismDetectionService) { this.programmingExerciseRepository = programmingExerciseRepository; this.authCheckService = authCheckService; this.plagiarismResultRepository = plagiarismResultRepository; this.programmingLanguageFeatureService = programmingLanguageFeatureService; - this.programmingPlagiarismDetectionService = programmingPlagiarismDetectionService; + this.plagiarismDetectionService = plagiarismDetectionService; } /** @@ -102,21 +101,20 @@ public ResponseEntity checkPlagiarism(@PathVariable long e throws ExitException, IOException { ProgrammingExercise programmingExercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.EDITOR, programmingExercise, null); - ProgrammingLanguage language = programmingExercise.getProgrammingLanguage(); - ProgrammingLanguageFeature programmingLanguageFeature = programmingLanguageFeatureService.orElseThrow().getProgrammingLanguageFeatures(language); - - if (!programmingLanguageFeature.plagiarismCheckSupported()) { - throw new BadRequestAlertException("Artemis does not support plagiarism checks for the programming language " + programmingExercise.getProgrammingLanguage(), - ENTITY_NAME, "programmingLanguageNotSupported"); - } long start = System.nanoTime(); - log.info("Start programmingPlagiarismDetectionService.checkPlagiarism for exercise {}", exerciseId); - var plagiarismResult = programmingPlagiarismDetectionService.checkPlagiarism(exerciseId, similarityThreshold, minimumScore); - log.info("Finished programmingExerciseExportService.checkPlagiarism call for {} comparisons in {}", plagiarismResult.getComparisons().size(), - TimeLogUtil.formatDurationFrom(start)); - plagiarismResultRepository.prepareResultForClient(plagiarismResult); - return ResponseEntity.ok(plagiarismResult); + log.info("Started manual plagiarism checks for programming exercise: exerciseId={}.", exerciseId); + var config = new PlagiarismDetectionConfig(similarityThreshold, minimumScore, 0); + try { + var plagiarismResult = plagiarismDetectionService.checkProgrammingExercise(programmingExercise, config); + return ResponseEntity.ok(plagiarismResult); + } + catch (ProgrammingLanguageNotSupportedForPlagiarismDetectionException e) { + throw new BadRequestAlertException(e.getMessage(), ENTITY_NAME, "programmingLanguageNotSupported"); + } + finally { + log.info("Finished manual plagiarism checks for programming exercise: exerciseId={}, elapsed={}.", exerciseId, TimeLogUtil.formatDurationFrom(start)); + } } /** @@ -136,18 +134,25 @@ public ResponseEntity checkPlagiarismWithJPlagReport(@PathVariable lon log.debug("REST request to check plagiarism for ProgrammingExercise with id: {}", exerciseId); ProgrammingExercise programmingExercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.EDITOR, programmingExercise, null); - var programmingLanguageFeature = programmingLanguageFeatureService.orElseThrow().getProgrammingLanguageFeatures(programmingExercise.getProgrammingLanguage()); - if (!programmingLanguageFeature.plagiarismCheckSupported()) { - 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) { - throw new BadRequestAlertException("Insufficient amount of valid and long enough submissions available for comparison.", "Plagiarism Check", "notEnoughSubmissions"); + long start = System.nanoTime(); + log.info("Started manual plagiarism checks with Jplag report for programming exercise: exerciseId={}.", exerciseId); + var config = new PlagiarismDetectionConfig(similarityThreshold, minimumScore, 0); + try { + var zipFile = plagiarismDetectionService.checkProgrammingExerciseWithJplagReport(programmingExercise, config); + if (zipFile == null) { + throw new BadRequestAlertException("Insufficient amount of valid and long enough submissions available for comparison.", "Plagiarism Check", + "notEnoughSubmissions"); + } + + var resource = new InputStreamResource(new FileInputStream(zipFile)); + return ResponseEntity.ok().contentLength(zipFile.length()).contentType(MediaType.APPLICATION_OCTET_STREAM).header("filename", zipFile.getName()).body(resource); + } + catch (ProgrammingLanguageNotSupportedForPlagiarismDetectionException e) { + throw new BadRequestAlertException(e.getMessage(), ENTITY_NAME, "programmingLanguageNotSupported"); + } + finally { + log.info("Finished manual plagiarism checks with Jplag report for programming exercise: exerciseId={}, elapsed={}.", exerciseId, TimeLogUtil.formatDurationFrom(start)); } - - InputStreamResource resource = new InputStreamResource(new FileInputStream(zipFile)); - return ResponseEntity.ok().contentLength(zipFile.length()).contentType(MediaType.APPLICATION_OCTET_STREAM).header("filename", zipFile.getName()).body(resource); } } 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 95bd8747cf66..bdeff6fd8431 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 @@ -16,6 +16,7 @@ import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.metis.conversation.Channel; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; +import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismDetectionConfig; import de.tum.in.www1.artemis.domain.plagiarism.text.TextPlagiarismResult; import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.repository.metis.conversation.ChannelRepository; @@ -29,7 +30,7 @@ import de.tum.in.www1.artemis.service.metis.conversation.ChannelService; import de.tum.in.www1.artemis.service.metis.conversation.ConversationService; import de.tum.in.www1.artemis.service.notifications.GroupNotificationScheduleService; -import de.tum.in.www1.artemis.service.plagiarism.TextPlagiarismDetectionService; +import de.tum.in.www1.artemis.service.plagiarism.PlagiarismDetectionService; import de.tum.in.www1.artemis.service.util.TimeLogUtil; import de.tum.in.www1.artemis.web.rest.dto.PageableSearchDTO; import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; @@ -93,7 +94,7 @@ public class TextExerciseResource { private final InstanceMessageSendService instanceMessageSendService; - private final TextPlagiarismDetectionService textPlagiarismDetectionService; + private final PlagiarismDetectionService plagiarismDetectionService; private final CourseRepository courseRepository; @@ -109,7 +110,7 @@ public TextExerciseResource(TextExerciseRepository textExerciseRepository, TextE ParticipationRepository participationRepository, ResultRepository resultRepository, TextExerciseImportService textExerciseImportService, TextSubmissionExportService textSubmissionExportService, ExampleSubmissionRepository exampleSubmissionRepository, ExerciseService exerciseService, GradingCriterionRepository gradingCriterionRepository, TextBlockRepository textBlockRepository, GroupNotificationScheduleService groupNotificationScheduleService, - InstanceMessageSendService instanceMessageSendService, TextPlagiarismDetectionService textPlagiarismDetectionService, CourseRepository courseRepository, + InstanceMessageSendService instanceMessageSendService, PlagiarismDetectionService plagiarismDetectionService, CourseRepository courseRepository, ChannelService channelService, ChannelRepository channelRepository, ConversationService conversationService) { this.feedbackRepository = feedbackRepository; this.exerciseDeletionService = exerciseDeletionService; @@ -130,7 +131,7 @@ public TextExerciseResource(TextExerciseRepository textExerciseRepository, TextE this.exerciseService = exerciseService; this.gradingCriterionRepository = gradingCriterionRepository; this.instanceMessageSendService = instanceMessageSendService; - this.textPlagiarismDetectionService = textPlagiarismDetectionService; + this.plagiarismDetectionService = plagiarismDetectionService; this.courseRepository = courseRepository; this.channelService = channelService; this.conversationService = conversationService; @@ -496,18 +497,12 @@ public ResponseEntity checkPlagiarism(@PathVariable long e @RequestParam int minimumSize) throws ExitException { TextExercise textExercise = textExerciseRepository.findByIdWithStudentParticipationsAndSubmissionsElseThrow(exerciseId); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.EDITOR, textExercise, null); - log.info("Start textPlagiarismDetectionService.checkPlagiarism for exercise {}", exerciseId); + long start = System.nanoTime(); - var plagiarismResult = textPlagiarismDetectionService.checkPlagiarism(textExercise, similarityThreshold, minimumScore, minimumSize); - log.info("Finished textPlagiarismDetectionService.checkPlagiarism for exercise {} with {} comparisons in {}", exerciseId, plagiarismResult.getComparisons().size(), - TimeLogUtil.formatDurationFrom(start)); - // TODO: limit the amount temporarily because of database issues - plagiarismResult.sortAndLimit(100); - log.info("Limited number of comparisons to {} to avoid performance issues when saving to database", plagiarismResult.getComparisons().size()); - start = System.nanoTime(); - plagiarismResultRepository.savePlagiarismResultAndRemovePrevious(plagiarismResult); - log.info("Finished plagiarismResultRepository.savePlagiarismResultAndRemovePrevious call in {}", TimeLogUtil.formatDurationFrom(start)); - plagiarismResultRepository.prepareResultForClient(plagiarismResult); + log.info("Started manual plagiarism checks for text exercise: exerciseId={}.", exerciseId); + var config = new PlagiarismDetectionConfig(similarityThreshold, minimumScore, minimumSize); + var plagiarismResult = plagiarismDetectionService.checkTextExercise(textExercise, config); + log.info("Finished manual plagiarism checks for text exercise: exerciseId={}, elapsed={}.", exerciseId, TimeLogUtil.formatDurationFrom(start)); return ResponseEntity.ok(plagiarismResult); } diff --git a/src/test/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionServiceTest.java new file mode 100644 index 000000000000..95f3f3a0857f --- /dev/null +++ b/src/test/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionServiceTest.java @@ -0,0 +1,138 @@ +package de.tum.in.www1.artemis.service.plagiarism; + +import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.io.IOException; +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import de.jplag.exceptions.ExitException; +import de.tum.in.www1.artemis.domain.ProgrammingExercise; +import de.tum.in.www1.artemis.domain.TextExercise; +import de.tum.in.www1.artemis.domain.modeling.ModelingExercise; +import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismDetectionConfig; +import de.tum.in.www1.artemis.domain.plagiarism.modeling.ModelingPlagiarismResult; +import de.tum.in.www1.artemis.domain.plagiarism.text.TextPlagiarismResult; +import de.tum.in.www1.artemis.repository.plagiarism.PlagiarismResultRepository; +import de.tum.in.www1.artemis.service.programming.ProgrammingLanguageFeature; +import de.tum.in.www1.artemis.service.programming.ProgrammingLanguageFeatureService; + +class PlagiarismDetectionServiceTest { + + private final PlagiarismDetectionConfig config = new PlagiarismDetectionConfig(0.5f, 1, 2); + + private final TextPlagiarismDetectionService textPlagiarismDetectionService = mock(); + + private final ProgrammingLanguageFeatureService programmingLanguageFeatureService = mock(); + + private final ProgrammingPlagiarismDetectionService programmingPlagiarismDetectionService = mock(); + + private final ModelingPlagiarismDetectionService modelingPlagiarismDetectionService = mock(); + + private final PlagiarismResultRepository plagiarismResultRepository = mock(); + + private final PlagiarismDetectionService service = new PlagiarismDetectionService(textPlagiarismDetectionService, Optional.of(programmingLanguageFeatureService), + programmingPlagiarismDetectionService, modelingPlagiarismDetectionService, plagiarismResultRepository); + + @Test + void shouldExecuteChecksForTextExercise() throws ExitException { + // given + var textExercise = new TextExercise(); + var textPlagiarismResult = new TextPlagiarismResult(); + textPlagiarismResult.setComparisons(emptySet()); + when(textPlagiarismDetectionService.checkPlagiarism(textExercise, config.getSimilarityThreshold(), config.getMinimumScore(), config.getMinimumSize())) + .thenReturn(textPlagiarismResult); + + // when + var result = service.checkTextExercise(textExercise, config); + + // then + assertThat(result).isEqualTo(textPlagiarismResult); + } + + @Test + void shouldExecuteChecksForModelingExercise() { + // given + var modelingExercise = new ModelingExercise(); + var modelingPlagiarismResult = new ModelingPlagiarismResult(); + modelingPlagiarismResult.setComparisons(emptySet()); + when(modelingPlagiarismDetectionService.checkPlagiarism(modelingExercise, config.getSimilarityThreshold(), config.getMinimumSize(), config.getMinimumScore())) + .thenReturn(modelingPlagiarismResult); + + // when + var result = service.checkModelingExercise(modelingExercise, config); + + // then + assertThat(result).isEqualTo(modelingPlagiarismResult); + } + + @Test + void shouldExecuteChecksForProgrammingExercise() throws IOException, ExitException, ProgrammingLanguageNotSupportedForPlagiarismDetectionException { + // given + var programmingExercise = new ProgrammingExercise(); + programmingExercise.setId(1L); + var programmingPlagiarismResult = new TextPlagiarismResult(); + programmingPlagiarismResult.setComparisons(emptySet()); + when(programmingPlagiarismDetectionService.checkPlagiarism(1L, config.getSimilarityThreshold(), config.getMinimumScore())).thenReturn(programmingPlagiarismResult); + + // and + var programmingLanguageFeature = new ProgrammingLanguageFeature(null, false, false, true, false, false, emptyList(), false, false, false); + when(programmingLanguageFeatureService.getProgrammingLanguageFeatures(any())).thenReturn(programmingLanguageFeature); + + // when + var result = service.checkProgrammingExercise(programmingExercise, config); + + // then + assertThat(result).isEqualTo(programmingPlagiarismResult); + } + + @Test + void shouldThrowExceptionOnUnsupportedProgrammingLanguage() { + // given + var programmingExercise = new ProgrammingExercise(); + var programmingLanguageFeature = new ProgrammingLanguageFeature(null, false, false, false, false, false, emptyList(), false, false, false); + when(programmingLanguageFeatureService.getProgrammingLanguageFeatures(any())).thenReturn(programmingLanguageFeature); + + // expect + assertThatThrownBy(() -> service.checkProgrammingExercise(programmingExercise, config)).isInstanceOf(ProgrammingLanguageNotSupportedForPlagiarismDetectionException.class); + } + + @Test + void shouldExecuteChecksWithJplagReportForProgrammingExercise() throws ProgrammingLanguageNotSupportedForPlagiarismDetectionException { + // given + var programmingExercise = new ProgrammingExercise(); + programmingExercise.setId(1L); + var zipFile = new File(""); + when(programmingPlagiarismDetectionService.checkPlagiarismWithJPlagReport(eq(1L), anyFloat(), anyInt())).thenReturn(zipFile); + + // and + var programmingLanguageFeature = new ProgrammingLanguageFeature(null, false, false, true, false, false, emptyList(), false, false, false); + when(programmingLanguageFeatureService.getProgrammingLanguageFeatures(any())).thenReturn(programmingLanguageFeature); + + // when + var result = service.checkProgrammingExerciseWithJplagReport(programmingExercise, config); + + // then + assertThat(result).isEqualTo(zipFile); + } + + @Test + void shouldThrowExceptionOnUnsupportedProgrammingLanguageForChecksWithJplagReport() { + // given + var programmingExercise = new ProgrammingExercise(); + var programmingLanguageFeature = new ProgrammingLanguageFeature(null, false, false, false, false, false, emptyList(), false, false, false); + when(programmingLanguageFeatureService.getProgrammingLanguageFeatures(any())).thenReturn(programmingLanguageFeature); + + // expect + assertThatThrownBy(() -> service.checkProgrammingExerciseWithJplagReport(programmingExercise, config)) + .isInstanceOf(ProgrammingLanguageNotSupportedForPlagiarismDetectionException.class); + } +} From 4051833fb95f9eb21d80873170b12e8381a1d9e7 Mon Sep 17 00:00:00 2001 From: jakubriegel Date: Thu, 7 Sep 2023 20:26:08 +0200 Subject: [PATCH 02/95] fix similarity for modeling exercises --- .../service/plagiarism/ModelingPlagiarismDetectionService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ModelingPlagiarismDetectionService.java b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ModelingPlagiarismDetectionService.java index 24ddbfbd39bb..026799d8661c 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ModelingPlagiarismDetectionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ModelingPlagiarismDetectionService.java @@ -149,7 +149,7 @@ public ModelingPlagiarismResult checkPlagiarism(List modelin final double similarity = model1.similarity(model2); log.debug("Compare result {} with {}: {}", i, j, similarity); - if (similarity < minimumSimilarity) { + if (similarity * 100 < minimumSimilarity) { // ignore comparison results with too small similarity continue; } From fc8c70eb9eba216640bc816afa4d1f0756221444 Mon Sep 17 00:00:00 2001 From: jakubriegel Date: Thu, 7 Sep 2023 21:28:49 +0200 Subject: [PATCH 03/95] fix code tests --- .../plagiarism/PlagiarismDetectionConfig.java | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/PlagiarismDetectionConfig.java b/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/PlagiarismDetectionConfig.java index f4bd4a97bf73..4981528ffe8b 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/PlagiarismDetectionConfig.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/PlagiarismDetectionConfig.java @@ -7,11 +7,14 @@ */ public class PlagiarismDetectionConfig extends DomainObject { - private final float similarityThreshold; + private float similarityThreshold; - private final int minimumScore; + private int minimumScore; - private final int minimumSize; + private int minimumSize; + + public PlagiarismDetectionConfig() { + } public PlagiarismDetectionConfig(float similarityThreshold, int minimumScore, int minimumSize) { this.similarityThreshold = similarityThreshold; @@ -23,11 +26,23 @@ public float getSimilarityThreshold() { return similarityThreshold; } + public void setSimilarityThreshold(float similarityThreshold) { + this.similarityThreshold = similarityThreshold; + } + public int getMinimumScore() { return minimumScore; } + public void setMinimumScore(int minimumScore) { + this.minimumScore = minimumScore; + } + public int getMinimumSize() { return minimumSize; } + + public void setMinimumSize(int minimumSize) { + this.minimumSize = minimumSize; + } } From 9dfc8916ec21acb4bdf62220306761549f556451 Mon Sep 17 00:00:00 2001 From: jakubriegel Date: Thu, 7 Sep 2023 21:29:19 +0200 Subject: [PATCH 04/95] extract save logic to a method --- .../PlagiarismDetectionService.java | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionService.java b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionService.java index 2a85de2228ee..07fb3c6a538a 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionService.java @@ -14,6 +14,7 @@ import de.tum.in.www1.artemis.domain.TextExercise; import de.tum.in.www1.artemis.domain.modeling.ModelingExercise; import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismDetectionConfig; +import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismResult; import de.tum.in.www1.artemis.domain.plagiarism.modeling.ModelingPlagiarismResult; import de.tum.in.www1.artemis.domain.plagiarism.text.TextPlagiarismResult; import de.tum.in.www1.artemis.repository.plagiarism.PlagiarismResultRepository; @@ -55,22 +56,10 @@ public TextPlagiarismResult checkTextExercise(TextExercise exercise, PlagiarismD var plagiarismResult = textPlagiarismDetectionService.checkPlagiarism(exercise, config.getSimilarityThreshold(), config.getMinimumScore(), config.getMinimumSize()); log.info("Finished textPlagiarismDetectionService.checkPlagiarism for exercise {} with {} comparisons,", exercise.getId(), plagiarismResult.getComparisons().size()); - // TODO: limit the amount temporarily because of database issues - plagiarismResult.sortAndLimit(100); - plagiarismResultRepository.savePlagiarismResultAndRemovePrevious(plagiarismResult); - - plagiarismResultRepository.prepareResultForClient(plagiarismResult); + trimAndSavePlagiarismResult(plagiarismResult); return plagiarismResult; } - private void checkProgrammingLanguageSupport(ProgrammingExercise exercise) throws ProgrammingLanguageNotSupportedForPlagiarismDetectionException { - var language = exercise.getProgrammingLanguage(); - var programmingLanguageFeature = programmingLanguageFeatureService.orElseThrow().getProgrammingLanguageFeatures(language); - if (!programmingLanguageFeature.plagiarismCheckSupported()) { - throw new ProgrammingLanguageNotSupportedForPlagiarismDetectionException(language); - } - } - /** * Check plagiarism in given programing exercise * @@ -113,11 +102,23 @@ public ModelingPlagiarismResult checkModelingExercise(ModelingExercise exercise, var plagiarismResult = modelingPlagiarismDetectionService.checkPlagiarism(exercise, config.getSimilarityThreshold(), config.getMinimumSize(), config.getMinimumScore()); log.info("Finished modelingPlagiarismDetectionService.checkPlagiarism call for {} comparisons", plagiarismResult.getComparisons().size()); - // TODO: limit the amount temporarily because of database issues + trimAndSavePlagiarismResult(plagiarismResult); + return plagiarismResult; + } + + private void trimAndSavePlagiarismResult(PlagiarismResult plagiarismResult) { + // Limit the amount temporarily because of database issues plagiarismResult.sortAndLimit(100); plagiarismResultRepository.savePlagiarismResultAndRemovePrevious(plagiarismResult); plagiarismResultRepository.prepareResultForClient(plagiarismResult); - return plagiarismResult; + } + + private void checkProgrammingLanguageSupport(ProgrammingExercise exercise) throws ProgrammingLanguageNotSupportedForPlagiarismDetectionException { + var language = exercise.getProgrammingLanguage(); + var programmingLanguageFeature = programmingLanguageFeatureService.orElseThrow().getProgrammingLanguageFeatures(language); + if (!programmingLanguageFeature.plagiarismCheckSupported()) { + throw new ProgrammingLanguageNotSupportedForPlagiarismDetectionException(language); + } } } From c83eefbcd8aed2618a994cf08b4755c1ae8829f7 Mon Sep 17 00:00:00 2001 From: jakubriegel Date: Thu, 7 Sep 2023 21:46:42 +0200 Subject: [PATCH 05/95] remove unused field --- .../artemis/web/rest/ProgrammingExercisePlagiarismResource.java | 2 -- 1 file changed, 2 deletions(-) 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 de1f3c777eb6..7ac08ba89ae9 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 @@ -52,8 +52,6 @@ public class ProgrammingExercisePlagiarismResource { private final PlagiarismResultRepository plagiarismResultRepository; - private final Optional programmingLanguageFeatureService; - private final PlagiarismDetectionService plagiarismDetectionService; public ProgrammingExercisePlagiarismResource(ProgrammingExerciseRepository programmingExerciseRepository, AuthorizationCheckService authCheckService, From c23635b9ca93188097f55b87f8fd6e04e46b270c Mon Sep 17 00:00:00 2001 From: jakubriegel Date: Thu, 7 Sep 2023 21:52:00 +0200 Subject: [PATCH 06/95] add doc --- .../artemis/service/plagiarism/PlagiarismDetectionService.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionService.java b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionService.java index 07fb3c6a538a..0791a4f625df 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionService.java @@ -20,6 +20,9 @@ import de.tum.in.www1.artemis.repository.plagiarism.PlagiarismResultRepository; import de.tum.in.www1.artemis.service.programming.ProgrammingLanguageFeatureService; +/** + * Service for triggering plagiarism checks. + */ @Service @Component public class PlagiarismDetectionService { From c80f975041c83bdcd37b77f99ca19e95a94d5ecf Mon Sep 17 00:00:00 2001 From: jakubriegel Date: Thu, 7 Sep 2023 21:54:44 +0200 Subject: [PATCH 07/95] add missing @param --- .../service/plagiarism/PlagiarismDetectionService.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionService.java b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionService.java index 0791a4f625df..f81741ac5855 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionService.java @@ -53,6 +53,7 @@ public PlagiarismDetectionService(TextPlagiarismDetectionService textPlagiarismD * Check plagiarism in given text exercise * * @param exercise exercise to check plagiarism + * @param config configuration for plagiarism detection * @return result of plagiarism checks */ public TextPlagiarismResult checkTextExercise(TextExercise exercise, PlagiarismDetectionConfig config) throws ExitException { @@ -67,6 +68,7 @@ public TextPlagiarismResult checkTextExercise(TextExercise exercise, PlagiarismD * Check plagiarism in given programing exercise * * @param exercise exercise to check plagiarism + * @param config configuration for plagiarism detection * @return result of plagiarism checks */ public TextPlagiarismResult checkProgrammingExercise(ProgrammingExercise exercise, PlagiarismDetectionConfig config) @@ -87,6 +89,7 @@ public TextPlagiarismResult checkProgrammingExercise(ProgrammingExercise exercis * Check plagiarism in given programing exercise and outputs a Jplag report * * @param exercise exercise to check plagiarism + * @param config configuration for plagiarism detection * @return Jplag report of plagiarism checks */ public File checkProgrammingExerciseWithJplagReport(ProgrammingExercise exercise, PlagiarismDetectionConfig config) @@ -99,6 +102,7 @@ public File checkProgrammingExerciseWithJplagReport(ProgrammingExercise exercise * Check plagiarism in given modeling exercise * * @param exercise exercise to check plagiarism + * @param config configuration for plagiarism detection * @return result of plagiarism checks */ public ModelingPlagiarismResult checkModelingExercise(ModelingExercise exercise, PlagiarismDetectionConfig config) { From c433abcfeae6e5664fbe79cee4d26d12a9527f5e Mon Sep 17 00:00:00 2001 From: jakubriegel Date: Fri, 8 Sep 2023 10:51:10 +0200 Subject: [PATCH 08/95] fix field removal --- .../web/rest/ProgrammingExercisePlagiarismResource.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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 7ac08ba89ae9..99a617a0727e 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 @@ -4,7 +4,6 @@ import java.io.FileInputStream; import java.io.IOException; -import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,7 +27,6 @@ import de.tum.in.www1.artemis.service.feature.FeatureToggle; import de.tum.in.www1.artemis.service.plagiarism.PlagiarismDetectionService; import de.tum.in.www1.artemis.service.plagiarism.ProgrammingLanguageNotSupportedForPlagiarismDetectionException; -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.errors.BadRequestAlertException; @@ -55,12 +53,10 @@ public class ProgrammingExercisePlagiarismResource { private final PlagiarismDetectionService plagiarismDetectionService; public ProgrammingExercisePlagiarismResource(ProgrammingExerciseRepository programmingExerciseRepository, AuthorizationCheckService authCheckService, - PlagiarismResultRepository plagiarismResultRepository, Optional programmingLanguageFeatureService, - PlagiarismDetectionService plagiarismDetectionService) { + PlagiarismResultRepository plagiarismResultRepository, PlagiarismDetectionService plagiarismDetectionService) { this.programmingExerciseRepository = programmingExerciseRepository; this.authCheckService = authCheckService; this.plagiarismResultRepository = plagiarismResultRepository; - this.programmingLanguageFeatureService = programmingLanguageFeatureService; this.plagiarismDetectionService = plagiarismDetectionService; } From ede87eafa139f5efa96ed9ea98a608ab8d85255d Mon Sep 17 00:00:00 2001 From: jakubriegel Date: Sun, 10 Sep 2023 10:43:37 +0200 Subject: [PATCH 09/95] add minimum length for programming exercises --- ...ogrammingExerciseGitDiffReportService.java | 26 ++++++++++++++----- .../PlagiarismDetectionService.java | 6 +++-- ...ProgrammingPlagiarismDetectionService.java | 25 +++++++++++++----- ...ProgrammingExercisePlagiarismResource.java | 12 ++++----- .../plagiarism-inspector.component.html | 2 +- .../PlagiarismDetectionServiceTest.java | 6 +++-- 6 files changed, 54 insertions(+), 23 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/hestia/ProgrammingExerciseGitDiffReportService.java b/src/main/java/de/tum/in/www1/artemis/service/hestia/ProgrammingExerciseGitDiffReportService.java index f34867ea864c..86bfe48c7f22 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/hestia/ProgrammingExerciseGitDiffReportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/hestia/ProgrammingExerciseGitDiffReportService.java @@ -2,10 +2,8 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; +import java.nio.file.Path; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -17,8 +15,7 @@ import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; -import de.tum.in.www1.artemis.domain.DomainObject; -import de.tum.in.www1.artemis.domain.ProgrammingExercise; +import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.hestia.ProgrammingExerciseGitDiffEntry; import de.tum.in.www1.artemis.domain.hestia.ProgrammingExerciseGitDiffReport; import de.tum.in.www1.artemis.domain.participation.SolutionProgrammingExerciseParticipation; @@ -158,6 +155,23 @@ public ProgrammingExerciseGitDiffReport getOrCreateReportOfExercise(ProgrammingE } } + public int calculateNumberOfDiffLinesBetweenRepos(VcsRepositoryUrl urlRepoA, Path localPathRepoA, VcsRepositoryUrl urlRepoB, Path localPathRepoB) { + var repoA = gitService.getExistingCheckedOutRepositoryByLocalPath(localPathRepoA, urlRepoA); + var repoB = gitService.getExistingCheckedOutRepositoryByLocalPath(localPathRepoB, urlRepoB); + + var treeParserRepoA = new FileTreeIterator(repoA); + var treeParserRepoB = new FileTreeIterator(repoB); + + try (var diffOutputStream = new ByteArrayOutputStream(); var git = Git.wrap(repoA)) { + git.diff().setOldTree(treeParserRepoA).setNewTree(treeParserRepoB).setOutputStream(diffOutputStream).call(); + var diff = diffOutputStream.toString(); + return extractDiffEntries(diff).stream().mapToInt(ProgrammingExerciseGitDiffEntry::getLineCount).sum(); + } + catch (IOException | GitAPIException e) { + return Integer.MAX_VALUE; + } + } + /** * Creates a new ProgrammingExerciseGitDiffReport for an exercise. * It will take the git-diff between the template and solution repositories and return all changes. diff --git a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionService.java b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionService.java index f81741ac5855..0e6ebc8b237a 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionService.java @@ -75,7 +75,8 @@ public TextPlagiarismResult checkProgrammingExercise(ProgrammingExercise exercis throws ExitException, IOException, ProgrammingLanguageNotSupportedForPlagiarismDetectionException { checkProgrammingLanguageSupport(exercise); - var plagiarismResult = programmingPlagiarismDetectionService.checkPlagiarism(exercise.getId(), config.getSimilarityThreshold(), config.getMinimumScore()); + var plagiarismResult = programmingPlagiarismDetectionService.checkPlagiarism(exercise.getId(), config.getSimilarityThreshold(), config.getMinimumScore(), + config.getMinimumSize()); log.info("Finished programmingExerciseExportService.checkPlagiarism call for {} comparisons", plagiarismResult.getComparisons().size()); plagiarismResultRepository.prepareResultForClient(plagiarismResult); @@ -95,7 +96,8 @@ public TextPlagiarismResult checkProgrammingExercise(ProgrammingExercise exercis public File checkProgrammingExerciseWithJplagReport(ProgrammingExercise exercise, PlagiarismDetectionConfig config) throws ProgrammingLanguageNotSupportedForPlagiarismDetectionException { checkProgrammingLanguageSupport(exercise); - return programmingPlagiarismDetectionService.checkPlagiarismWithJPlagReport(exercise.getId(), config.getSimilarityThreshold(), config.getMinimumScore()); + return programmingPlagiarismDetectionService.checkPlagiarismWithJPlagReport(exercise.getId(), config.getSimilarityThreshold(), config.getMinimumScore(), + config.getMinimumSize()); } /** 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 dc013841915c..e986d2c7d545 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 @@ -39,6 +39,7 @@ import de.tum.in.www1.artemis.service.FileService; import de.tum.in.www1.artemis.service.UrlService; import de.tum.in.www1.artemis.service.connectors.GitService; +import de.tum.in.www1.artemis.service.hestia.ProgrammingExerciseGitDiffReportService; import de.tum.in.www1.artemis.service.plagiarism.cache.PlagiarismCacheService; import de.tum.in.www1.artemis.service.programming.ProgrammingExerciseExportService; import de.tum.in.www1.artemis.service.util.TimeLogUtil; @@ -72,10 +73,12 @@ class ProgrammingPlagiarismDetectionService { private final UrlService urlService; + private final ProgrammingExerciseGitDiffReportService programmingExerciseGitDiffReportService; + public ProgrammingPlagiarismDetectionService(ProgrammingExerciseRepository programmingExerciseRepository, FileService fileService, GitService gitService, StudentParticipationRepository studentParticipationRepository, PlagiarismResultRepository plagiarismResultRepository, ProgrammingExerciseExportService programmingExerciseExportService, PlagiarismWebsocketService plagiarismWebsocketService, PlagiarismCacheService plagiarismCacheService, - UrlService urlService) { + UrlService urlService, ProgrammingExerciseGitDiffReportService programmingExerciseGitDiffReportService) { this.programmingExerciseRepository = programmingExerciseRepository; this.fileService = fileService; this.gitService = gitService; @@ -85,6 +88,7 @@ public ProgrammingPlagiarismDetectionService(ProgrammingExerciseRepository progr this.plagiarismWebsocketService = plagiarismWebsocketService; this.plagiarismCacheService = plagiarismCacheService; this.urlService = urlService; + this.programmingExerciseGitDiffReportService = programmingExerciseGitDiffReportService; } /** @@ -97,7 +101,7 @@ public ProgrammingPlagiarismDetectionService(ProgrammingExerciseRepository progr * @throws ExitException is thrown if JPlag exits unexpectedly * @throws IOException is thrown for file handling errors */ - public TextPlagiarismResult checkPlagiarism(long programmingExerciseId, float similarityThreshold, int minimumScore) throws ExitException, IOException { + public TextPlagiarismResult checkPlagiarism(long programmingExerciseId, float similarityThreshold, int minimumScore, int minimumSize) throws ExitException, IOException { long start = System.nanoTime(); String topic = plagiarismWebsocketService.getProgrammingExercisePlagiarismCheckTopic(programmingExerciseId); @@ -112,7 +116,7 @@ public TextPlagiarismResult checkPlagiarism(long programmingExerciseId, float si } plagiarismCacheService.setActivePlagiarismCheck(courseId); - JPlagResult jPlagResult = computeJPlagResult(programmingExercise, similarityThreshold, minimumScore); + JPlagResult jPlagResult = computeJPlagResult(programmingExercise, similarityThreshold, minimumScore, minimumSize); if (jPlagResult == null) { log.info("Insufficient amount of submissions for plagiarism detection. Return empty result."); TextPlagiarismResult textPlagiarismResult = new TextPlagiarismResult(); @@ -148,11 +152,11 @@ public TextPlagiarismResult checkPlagiarism(long programmingExerciseId, float si * @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 */ - public File checkPlagiarismWithJPlagReport(long programmingExerciseId, float similarityThreshold, int minimumScore) { + public File checkPlagiarismWithJPlagReport(long programmingExerciseId, float similarityThreshold, int minimumScore, int minimumSize) { long start = System.nanoTime(); final var programmingExercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationElseThrow(programmingExerciseId); - JPlagResult result = computeJPlagResult(programmingExercise, similarityThreshold, minimumScore); + JPlagResult result = computeJPlagResult(programmingExercise, similarityThreshold, minimumScore, minimumSize); log.info("JPlag programming comparison finished with {} comparisons in {}", result.getAllComparisons().size(), TimeLogUtil.formatDurationFrom(start)); return generateJPlagReportZip(result, programmingExercise); @@ -167,7 +171,7 @@ public File checkPlagiarismWithJPlagReport(long programmingExerciseId, float sim * @return the JPlag result or null if there are not enough participations */ @NotNull - private JPlagResult computeJPlagResult(ProgrammingExercise programmingExercise, float similarityThreshold, int minimumScore) { + private JPlagResult computeJPlagResult(ProgrammingExercise programmingExercise, float similarityThreshold, int minimumScore, int minimumSize) { long programmingExerciseId = programmingExercise.getId(); final var targetPath = fileService.getTemporaryUniquePath(repoDownloadClonePath, 60); List participations = filterStudentParticipationsForComparison(programmingExercise, minimumScore); @@ -188,9 +192,18 @@ private JPlagResult computeJPlagResult(ProgrammingExercise programmingExercise, 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) { + var templateFolder = targetPath.resolve(projectKey).resolve(templateRepoName).toFile(); options = options.withBaseCodeSubmissionDirectory(templateFolder); + + repositories = repositories.stream().filter(repository -> { + var diffToTemplate = programmingExerciseGitDiffReportService.calculateNumberOfDiffLinesBetweenRepos( + programmingExercise.getTemplateParticipation().getVcsRepositoryUrl(), templateFolder.toPath(), repository.getRemoteRepositoryUrl(), + repository.getLocalPath()); + return diffToTemplate >= minimumSize; + }).toList(); } log.info("Start JPlag programming comparison for programming exercise {}", programmingExerciseId); 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 99a617a0727e..9a05b4f1460e 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 @@ -91,14 +91,14 @@ public ResponseEntity getPlagiarismResult(@PathVariable lo @GetMapping(CHECK_PLAGIARISM) @EnforceAtLeastEditor @FeatureToggle({ Feature.ProgrammingExercises, Feature.PlagiarismChecks }) - public ResponseEntity checkPlagiarism(@PathVariable long exerciseId, @RequestParam float similarityThreshold, @RequestParam int minimumScore) - throws ExitException, IOException { + public ResponseEntity checkPlagiarism(@PathVariable long exerciseId, @RequestParam float similarityThreshold, @RequestParam int minimumScore, + @RequestParam int minimumSize) throws ExitException, IOException { ProgrammingExercise programmingExercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.EDITOR, programmingExercise, null); long start = System.nanoTime(); log.info("Started manual plagiarism checks for programming exercise: exerciseId={}.", exerciseId); - var config = new PlagiarismDetectionConfig(similarityThreshold, minimumScore, 0); + var config = new PlagiarismDetectionConfig(similarityThreshold, minimumScore, minimumSize); try { var plagiarismResult = plagiarismDetectionService.checkProgrammingExercise(programmingExercise, config); return ResponseEntity.ok(plagiarismResult); @@ -123,15 +123,15 @@ public ResponseEntity checkPlagiarism(@PathVariable long e @GetMapping(value = CHECK_PLAGIARISM_JPLAG_REPORT) @EnforceAtLeastEditor @FeatureToggle(Feature.ProgrammingExercises) - public ResponseEntity checkPlagiarismWithJPlagReport(@PathVariable long exerciseId, @RequestParam float similarityThreshold, @RequestParam int minimumScore) - throws IOException { + public ResponseEntity checkPlagiarismWithJPlagReport(@PathVariable long exerciseId, @RequestParam float similarityThreshold, @RequestParam int minimumScore, + @RequestParam int minimumSize) 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); long start = System.nanoTime(); log.info("Started manual plagiarism checks with Jplag report for programming exercise: exerciseId={}.", exerciseId); - var config = new PlagiarismDetectionConfig(similarityThreshold, minimumScore, 0); + var config = new PlagiarismDetectionConfig(similarityThreshold, minimumScore, minimumSize); try { var zipFile = plagiarismDetectionService.checkProgrammingExerciseWithJplagReport(programmingExercise, config); if (zipFile == null) { 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 b0cddf737e1b..9bd26fc8c330 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 @@ -91,7 +91,7 @@