diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 985bc704d5bf..b82fff75eebe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,7 +34,7 @@ on: # Keep in sync with codeql-analysis.yml and test.yml env: CI: true - node: 18 + node: 20 java: 17 RAW_URL: https://raw.githubusercontent.com/${{ github.repository }}/${{ github.sha }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 2c34c3719501..14aad25fb9ea 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -30,7 +30,7 @@ on: # Keep in sync with build.yml and test.yml env: CI: true - node: 18 + node: 20 java: 17 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1d08156968ce..dff9391b47e1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,7 +40,7 @@ concurrency: # Keep in sync with codeql-analysis.yml and build.yml env: CI: true - node: 18 + node: 20 java: 17 jobs: diff --git a/README.md b/README.md index 87d072b3f280..f6c63cf65786 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,11 @@ [![Coverage Status](https://app.codacy.com/project/badge/Coverage/89860aea5fa74d998ec884f1a875ed0c)](https://www.codacy.com/gh/ls1intum/Artemis?utm_source=github.com&utm_medium=referral&utm_content=ls1intum/Artemis&utm_campaign=Badge_Coverage) [![Latest version)](https://img.shields.io/github/v/tag/ls1intum/Artemis?label=%20Latest%20version&sort=semver)](https://github.com/ls1intum/Artemis/releases/latest) +Artemis brings interactive learning to life with instant, individual feedback on programming exercises, quizzes, modeling tasks, and more. Offering customization for instructors and real-time collaboration for students, this platform bridges creativity and education. Embrace a new era of engaging, adaptive learning and artificial intelligence support with Artemis, where innovation meets inclusivity. Find out more on https://artemisapp.github.io + ## Main features -1. **[Programming exercises](https://docs.artemis.cit.tum.de/user/exercises/programming/)** with version control, automatic individual feedback (and assessment) based on test cases and static code analysis (executed using continuous integration). +1. **[Programming exercises](https://docs.artemis.cit.tum.de/user/exercises/programming/)** with version control, automatic individual feedback (and assessment) based on test cases and static code analysis (executed using continuous integration) for `any programming language`. * **Instant**: Students receive immediate and individual feedback on submissions. Instructors can customize feedback messages easily, hide feedback during the working time (e.g., with hidden tests) * **Interactive:** Instructors integrate interactive instructions based on tasks and UML diagrams directly into the dynamic problem statements. They can define hints for difficult exercise parts. * **Independent**: Instructors can customize programming exercises to support any programming language. To simplify the setup, Artemis includes sophisticated templates for the most common languages (e.g., Java, Python, C, Haskell, Kotlin, VHDL, Assembler, Swift, Ocaml, ...) @@ -32,7 +34,7 @@ 6. **[Exam mode](https://docs.artemis.cit.tum.de/user/exam_mode/)**: Instructors can create online exams with exercise variants, integrated plagiarism checks, test runs and student reviews. You can find more information on [Exam mode student features](https://artemis.cit.tum.de/features/students) and on [Exam mode instructor features](https://artemis.cit.tum.de/features/instructors). 7. **[Grading](https://docs.artemis.cit.tum.de/user/grading/)**: Instructors can configure grade keys for courses and exams to automatically calculate grades and display them to students. Grades can be easily exported as csv files to upload them into university systems (such as Campus online). Instructors can optionally define bonus configurations for final exams to improve student grades according to their grades from a midterm exam or course exercises. 8. **[Assessment](https://docs.artemis.cit.tum.de/user/exercises/assessment/)**: Artemis uses double-blind grading and structured grading criteria to improve consistency and fairness. It integrates an assessment training process (based on example submissions and example assessments defined by the instructor), has a grading leader board, and allows students to rate the assessments. Students can complain or ask for more feedback. -9. **[Communication](https://docs.artemis.cit.tum.de/user/communication/)**: Instructors can post announcements. Students can ask questions, post comments, and react to other posts. Tutors can filter unanswered questions. +9. **[Communication](https://docs.artemis.cit.tum.de/user/communication/)**: Instructors can post announcements. Students can ask questions, post comments, and react to other posts in channels or private chats. Tutors can filter unanswered questions. 10. **[Notifications](https://docs.artemis.cit.tum.de/user/notifications)**: Artemis supports customizable web and email notifications. Users can enable and disable different notification types. 11. **[Team Exercises](https://docs.artemis.cit.tum.de/user/exercises/team-exercises/)**: Instructors can configure team exercises with real time collaboration and dedicated tutors per team. 12. **[Lectures](https://docs.artemis.cit.tum.de/user/lectures/)**: Instructors can upload lecture slides, divide lectures into units, integrate video streams, lecture recordings, and exercises into lectures, and define competencies. @@ -42,7 +44,7 @@ 16. **[Adaptive Learning](https://docs.artemis.cit.tum.de/user/adaptive-learning/)**: Artemis allows instructors and students to define and track competencies. Students can monitor their progress towards these goals, while instructors can provide tailored feedback. This approach integrates lectures and exercises under overarching learning objectives. 17. **[Tutorial Groups](https://docs.artemis.cit.tum.de/user/tutorialgroups/)**: Artemis support the management of tutorial groups of a course. This includes planning the sessions, assigning responsible tutors, registering students and tracking the attendance. 18. **[Iris](https://artemis.cit.tum.de/about-iris)**: Artemis integrates Iris, a chatbot that supports students and instructors with common questions and tasks. -19. **[Scalable](https://docs.artemis.cit.tum.de/user/scaling/)**: Artemis scales to multiple courses with thousands of students. In fact, the largest course had 2,400 students. Administrators can easily scale Artemis with additional build agents in the continuous integration environment. +19. **[Scalable](https://docs.artemis.cit.tum.de/user/scaling/)**: Artemis scales to multiple courses with thousands of students simultaneously using it. In fact, the largest course had 2,400 students. Administrators can easily scale Artemis with additional build agents in the continuous integration environment. 20. **[High user satisfaction](https://docs.artemis.cit.tum.de/user/user-experience/)**: Artemis is easy to use, provides guided tutorials. Developers focus on usability, user experience, and performance. 21. **Customizable**: It supports multiple instructors, editors, and tutors per course and allows instructors to customize many course settings 22. **[Open-source](https://docs.artemis.cit.tum.de/dev/open-source/)**: Free to use with a large community and many active maintainers. @@ -51,18 +53,18 @@ The Artemis development team prioritizes the following issues in the future. We welcome feature requests from students, tutors, instructors, and administrators. We are happy to discuss any suggestions for improvements. -* **Short term**: Further improve the communication features -* **Short term**: Add learning paths based on adaptive learning with different exercise difficulties and the automatic generation of hints -* **Short term**: Add more learning analytics features while preserving data privacy -* **Short term**: Add instructor assistance based on Generative AI -* **Medium term**: Simplify the setup of Artemis -* **Medium term**: Add feedback assistance based on Generative AI +* **Short term**: Further improve the communication features with mobile apps for iOS and Android +* **Short term**: Simplify the setup of Artemis with local modules for version control and continuous integration +* **Short term**: Add feedback assistance based on Generative AI (within [Athena](https://github.com/ls1intum/Athena)) +* **Medium term**: Add more learning analytics features while preserving data privacy +* **Medium term**: Improve the user experience, usability and navigation +* **Medium term**: Add automatic generation of hints +* **Medium term**: Add AI support for reviewing exercises * **Medium term**: Add the possibility to use Iris for questions on all exercise types, lectures, and learning performance aspects -* **Long term**: Microservices, Kubernetes based deployment, and micro frontends +* **Long term**: Explore the possibilities of microservices, Kubernetes based deployment, and micro frontends * **Long term**: Allow students to take notes on lecture slides and support the automatic updates of lecture slides * **Long term**: Develop an exchange platform for exercises - ## Setup, guides, and contributing ### Development setup, coding, and design guidelines @@ -79,21 +81,29 @@ You can find a guide on [how to write documentation](docs/README.md). ### Server setup -You can set up Artemis in conjunction with either [`GitLab and Jenkins`](https://docs.artemis.cit.tum.de/dev/setup/#jenkins-and-gitlab-setup), [`GitLab and GitLab CI (experimental)`](https://docs.artemis.cit.tum.de/dev/setup/#gitlab-ci-and-gitlab-setup), [`Jira, Bitbucket and Bamboo`](https://docs.artemis.cit.tum.de/dev/setup/#bamboo-bitbucket-and-jira-setup), or with [`local CI and local VC`](https://docs.artemis.cit.tum.de/dev/setup/#local-ci-and-local-vc-setup). +Setting up Artemis in your development environment or a demo production environment is really easy following the instructions on https://docs.artemis.cit.tum.de/dev/setup. When you want to support programming exercises, we recommend to use the modules for [Local CI and Local VC](https://docs.artemis.cit.tum.de/dev/setup/#local-ci-and-local-vc-setup). + +Artemis can also be set up in conjunction with external tools for version control and continuous integration: +1. [GitLab and Jenkins](https://docs.artemis.cit.tum.de/dev/setup/#jenkins-and-gitlab-setup) +2. [Jira, Bitbucket and Bamboo](https://docs.artemis.cit.tum.de/dev/setup/#bamboo-bitbucket-and-jira-setup) +3. [GitLab and GitLab CI (experimental)](https://docs.artemis.cit.tum.de/dev/setup/#gitlab-ci-and-gitlab-setup) + Artemis uses these external tools for user management and the configuration of programming exercises. ### Administration setup If needed, you can configure self service [user registration](https://docs.artemis.cit.tum.de/admin/registration). -### Contributing +### Contributing Please read the guide on [how to contribute](CONTRIBUTING.md) to Artemis. Once the PR is ready to merge, notify the responsible feature maintainer: #### Maintainers - + +The following members of the project management team are responsible for specific feature areas in Artemis. Contact them if you have questions or if you want to develop new features in this area. + | Feature / Aspect | Maintainer | |--------------------------------|-----------------------------------------------------------------------------------------------------| | Programming exercises | [@krusche](https://github.com/krusche) | @@ -145,7 +155,7 @@ Refer to [Using JHipster in production](http://www.jhipster.tech/production) for The following command can automate the deployment to a server. The example shows the deployment to the main Artemis test server (which runs a virtual machine): ```shell -./artemis-server-cli deploy username@artemistest.ase.in.tum.de -w build/libs/Artemis-4.4.5.war +./artemis-server-cli deploy username@artemistest.ase.in.tum.de -w build/libs/Artemis-6.7.5.war ``` ## Architecture @@ -154,11 +164,7 @@ The following diagram shows the top level design of Artemis which is decomposed ![Top-Level Design](docs/dev/system-design/TopLevelDesign.png "Top-Level Design") -While Artemis includes generic adapters to these three external systems with a defined protocol that can be instantiated to connect to any VCS, CIS, or UMS, it also provides 3 concrete implementations for these adapters to connect to: - -1. **VCS:** Atlassian Bitbucket Server -2. **CIS:** Atlassian Bamboo Server -3. **UMS:** Atlassian JIRA Server (more specifically Atlassian Crowd on the JIRA Server) +While Artemis includes generic adapters to these three external systems with a defined protocol that can be instantiated to connect to any VCS, CIS, or UMS, it also provides 3 concrete implementations for these adapters to connect to. ### Server architecture diff --git a/angular.json b/angular.json index abea334384c6..ccadecd96e26 100644 --- a/angular.json +++ b/angular.json @@ -123,7 +123,7 @@ "scripts": true, "styles": { "minify": true, - "inlineCritical": false + "inlineCritical": true }, "fonts": true }, @@ -136,9 +136,10 @@ }, "namedChunks": true, "aot": true, - "extractLicenses": true, + "extractLicenses": false, "serviceWorker": true, "ngswConfigPath": "src/main/webapp/ngsw-config.json", + "statsJson": false, "budgets": [ { "type": "initial", diff --git a/build.gradle b/build.gradle index d6aed27b39d6..b608ce2c6555 100644 --- a/build.gradle +++ b/build.gradle @@ -18,17 +18,17 @@ plugins { id "io.spring.dependency-management" version "1.1.4" id "com.google.cloud.tools.jib" version "3.4.0" id "com.github.node-gradle.node" version "${gradle_node_plugin_version}" - id "com.diffplug.spotless" version "6.22.0" + id "com.diffplug.spotless" version "6.23.3" // this allows us to find outdated dependencies via ./gradlew dependencyUpdates id "com.github.ben-manes.versions" version "0.50.0" id "com.github.andygoossens.modernizer" version "${modernizer_plugin_version}" id "com.gorylenko.gradle-git-properties" version "2.4.1" - id "org.owasp.dependencycheck" version "9.0.3" + id "org.owasp.dependencycheck" version "9.0.7" id "com.adarshr.test-logger" version "4.0.0" } group = "de.tum.in.www1.artemis" -version = "6.7.4" +version = "6.7.5" description = "Interactive Learning with Individual Feedback" java { @@ -99,7 +99,7 @@ modernizer { } // Execute the test cases: ./gradlew test -// Execute only architecture tests: ./gradlew test -DincludeTags='ArchitectureTest' +// Execute only architecture tests: ./gradlew test -DincludeTags="ArchitectureTest" test { if (System.getProperty("includeTags")) { useJUnitPlatform { @@ -187,16 +187,15 @@ repositories { maven { url "https://repo.gradle.org/gradle/libs-releases/" } + maven { + url "https://build.shibboleth.net/maven/releases/" + } } ext["jackson.version"] = fasterxml_version dependencies { - // This will make sure that e.g. src/main/java/de/tum/in/www1/artemis/config/BeanInfoProcessor.java is invoked during the build - annotationProcessor "com.google.auto.service:auto-service:1.1.1" - compileOnly "com.google.auto.service:auto-service:1.1.1" - // Note: jenkins-client is not well maintained and includes dependencies to libraries with critical security issues (e.g. CVE-2020-10683 for dom4j@1.6.1) // implementation "com.offbytwo.jenkins:jenkins-client:0.3.8" implementation files("libs/jenkins-client-0.4.0.jar") @@ -216,7 +215,7 @@ dependencies { } - implementation "org.gitlab4j:gitlab4j-api:5.4.0" + implementation "org.gitlab4j:gitlab4j-api:5.5.0" implementation "de.jplag:jplag:${jplag_version}" implementation "de.jplag:java:${jplag_version}" @@ -236,31 +235,45 @@ dependencies { } } - implementation "org.apache.logging.log4j:log4j-to-slf4j:2.22.0" + implementation "org.apache.logging.log4j:log4j-to-slf4j:2.22.1" - implementation "uk.ac.ox.ctl:spring-security-lti13:0.0.4" + implementation "uk.ac.ox.ctl:spring-security-lti13:0.1.11" // https://search.maven.org/artifact/org.eclipse.jgit/org.eclipse.jgit implementation "org.eclipse.jgit:org.eclipse.jgit:${jgit_version}" implementation "org.eclipse.jgit:org.eclipse.jgit.ssh.apache:${jgit_version}" implementation "org.eclipse.jgit:org.eclipse.jgit.http.server:${jgit_version}" // https://mvnrepository.com/artifact/net.sourceforge.plantuml/plantuml - implementation "net.sourceforge.plantuml:plantuml:1.2023.12" - implementation "org.imsglobal:basiclti-util:1.2.0" + implementation "net.sourceforge.plantuml:plantuml:1.2023.13" + implementation ("org.imsglobal:basiclti-util:1.2.0") { + // NOTE: this module contains security vulnerabilities and is not needed + exclude module: "jackson-mapper-asl" + } implementation "org.jasypt:jasypt:1.9.3" implementation "me.xdrop:fuzzywuzzy:1.4.0" implementation "com.atlassian.bamboo:bamboo-specs:9.3.5" implementation ("org.yaml:snakeyaml") { version { - strictly "2.0" // needed for Bamboo-specs and to reduce the number of vulnerabilities, also see https://mvnrepository.com/artifact/org.yaml/snakeyaml + strictly "2.2" // needed for Bamboo-specs and to reduce the number of vulnerabilities, also see https://mvnrepository.com/artifact/org.yaml/snakeyaml } } - implementation "com.thoughtworks.qdox:qdox:2.0.3" implementation "io.sentry:sentry-logback:${sentry_version}" implementation "io.sentry:sentry-spring-boot-starter:${sentry_version}" - implementation "org.jsoup:jsoup:1.17.1" + + // NOTE: the following six dependencies use the newer versions explicitly to avoid other dependencies to use older versions + implementation "ch.qos.logback:logback-classic:${logback_version}" + implementation "ch.qos.logback:logback-core:${logback_version}" + implementation "com.thoughtworks.xstream:xstream:1.4.20" + implementation "xerces:xercesImpl:2.12.2" + implementation "xalan:xalan:2.7.3" + implementation "xalan:serializer:2.7.3" + + // NOTE: use the latest 2.x version to avoid security vulnerabilities + implementation "org.apache.santuario:xmlsec:2.3.4" + + implementation "org.jsoup:jsoup:1.17.2" implementation "commons-codec:commons-codec:1.16.0" // needed for spring security saml2 implementation "org.springdoc:springdoc-openapi-ui:1.7.0" @@ -274,7 +287,7 @@ dependencies { implementation "tech.jhipster:jhipster-framework:${jhipster_dependencies_version}" implementation "org.springframework.boot:spring-boot-starter-cache:${spring_boot_version}" - implementation "io.micrometer:micrometer-registry-prometheus:1.12.0" + implementation "io.micrometer:micrometer-registry-prometheus:1.12.1" implementation "net.logstash.logback:logstash-logback-encoder:7.4" implementation "com.fasterxml.jackson.datatype:jackson-datatype-hppc:${fasterxml_version}" implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${fasterxml_version}" @@ -292,7 +305,7 @@ dependencies { implementation "org.apache.commons:commons-math3:3.6.1" implementation "javax.transaction:javax.transaction-api:1.3" implementation "org.hibernate:hibernate-entitymanager:${hibernate_version}" - // TODO: we cannot upgrade because 4.24.0 would not work with H2 in the tests due to the reserved keyword 'groups', see https://github.com/liquibase/liquibase/pull/4052 + // TODO: we cannot upgrade because 4.24.0 would not work with H2 in the tests due to the reserved keyword "groups", see https://github.com/liquibase/liquibase/pull/4052 implementation "org.liquibase:liquibase-core:4.23.2" implementation "org.springframework.boot:spring-boot-starter-validation:${spring_boot_version}" implementation "org.springframework.boot:spring-boot-loader-tools:${spring_boot_version}" @@ -312,24 +325,41 @@ dependencies { implementation "org.springframework.ldap:spring-ldap-core:2.4.1" implementation "org.springframework.data:spring-data-ldap:2.7.17" - implementation "org.springframework.cloud:spring-cloud-starter-netflix-eureka-client:3.1.7" - implementation "org.springframework.cloud:spring-cloud-starter-config:3.1.8" - implementation "org.springframework.boot:spring-boot-starter-cloud-connectors:2.2.13.RELEASE" + implementation ("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client:3.1.8") { + // NOTE: these modules contain security vulnerabilities and are not needed + exclude module: "commons-jxpath" + exclude module: "woodstox-core" + } + implementation "org.springframework.cloud:spring-cloud-starter:3.1.8" + implementation "org.springframework.cloud:spring-cloud-starter-config:3.1.9" + // TODO: updating to a later version breaks the build implementation "io.netty:netty-all:4.1.101.Final" - implementation "io.projectreactor.netty:reactor-netty:1.1.13" + implementation "io.projectreactor.netty:reactor-netty:1.1.14" implementation "org.springframework:spring-messaging:5.3.31" - implementation "org.springframework.retry:spring-retry:2.0.4" + implementation "org.springframework.retry:spring-retry:2.0.5" implementation "org.springframework.security:spring-security-config:${spring_security_version}" implementation "org.springframework.security:spring-security-data:${spring_security_version}" + implementation "org.springframework.security:spring-security-core:${spring_security_version}" + implementation "org.springframework.security:spring-security-oauth2-core:${spring_security_version}" + implementation "org.springframework.security:spring-security-oauth2-client:${spring_security_version}" + implementation "org.springframework.security:spring-security-oauth2-jose:${spring_security_version}" + implementation "org.springframework.security:spring-security-crypto:${spring_security_version}" implementation "org.springframework.security:spring-security-web:${spring_security_version}" implementation "org.springframework.security:spring-security-messaging:${spring_security_version}" implementation "org.springframework.security:spring-security-ldap:${spring_security_version}" implementation "org.springframework.security:spring-security-saml2-service-provider:${spring_security_version}" + + implementation "org.opensaml:opensaml-security-api:${opensaml_version}" + implementation "org.opensaml:opensaml-core:${opensaml_version}" + implementation "org.opensaml:opensaml-saml-impl:${opensaml_version}" + implementation "org.opensaml:opensaml-saml-api:${opensaml_version}" + implementation "org.xmlbeam:xmlprojector:1.4.24" implementation "io.jsonwebtoken:jjwt-api:0.12.3" - implementation "org.bouncycastle:bcprov-jdk15on:1.70" + implementation "org.bouncycastle:bcpkix-jdk18on:1.77" + implementation "org.bouncycastle:bcprov-jdk18on:1.77" runtimeOnly "io.jsonwebtoken:jjwt-impl:0.12.3" runtimeOnly "io.jsonwebtoken:jjwt-jackson:0.12.3" implementation ("io.springfox:springfox-swagger2:3.0.0") { @@ -342,7 +372,7 @@ dependencies { // zalando problem spring web can only be updated when we support Spring 6 implementation "org.zalando:problem-spring-web:0.27.0" - implementation "com.ibm.icu:icu4j:74.1" + implementation "com.ibm.icu:icu4j:74.2" implementation "com.github.seancfoley:ipaddress:5.4.0" implementation "org.apache.maven:maven-model:3.9.6" implementation "org.apache.pdfbox:pdfbox:3.0.1" @@ -352,8 +382,12 @@ dependencies { implementation "commons-fileupload:commons-fileupload:1.5" implementation "net.lingala.zip4j:zip4j:2.11.5" implementation "org.jgrapht:jgrapht-core:1.5.2" - implementation 'com.github.seancfoley:ipaddress:5.4.0' + implementation "com.github.seancfoley:ipaddress:5.4.0" + // make sure the dependencies use the latest version without security vulnerabilities + implementation "com.google.guava:guava:33.0.0-jre" + // make sure the dependencies use the latest version + implementation "com.google.code.gson:gson:2.10.1" annotationProcessor "org.hibernate:hibernate-jpamodelgen:${hibernate_version}" annotationProcessor ("org.glassfish.jaxb:jaxb-runtime:${jaxb_runtime_version}") { @@ -373,7 +407,7 @@ dependencies { } testImplementation "org.springframework.security:spring-security-test:${spring_security_version}" testImplementation "org.springframework.boot:spring-boot-test:${spring_boot_version}" - testImplementation "org.assertj:assertj-core:3.24.2" + testImplementation "org.assertj:assertj-core:3.25.0" testImplementation "org.junit.jupiter:junit-jupiter:${junit_version}" testImplementation "org.mockito:mockito-core:${mockito_version}" testImplementation "org.mockito:mockito-junit-jupiter:${mockito_version}" @@ -381,17 +415,19 @@ dependencies { testImplementation "org.awaitility:awaitility:4.2.0" testImplementation "org.apache.maven.shared:maven-invoker:3.2.0" testImplementation "org.gradle:gradle-tooling-api:8.5" - testImplementation "org.apache.maven.surefire:surefire-report-parser:3.2.2" + testImplementation "org.apache.maven.surefire:surefire-report-parser:3.2.3" testImplementation "com.opencsv:opencsv:5.9" testImplementation("io.zonky.test:embedded-database-spring-test:2.4.0") { - exclude group: 'org.testcontainers', module: 'mariadb' - exclude group: 'org.testcontainers', module: 'mssqlserver' + exclude group: "org.testcontainers", module: "mariadb" + exclude group: "org.testcontainers", module: "mssqlserver" } testImplementation "com.tngtech.archunit:archunit:1.2.1" - testImplementation "org.skyscreamer:jsonassert:1.5.1" + testImplementation ("org.skyscreamer:jsonassert:1.5.1") { + exclude module: "android-json" + } testImplementation ("net.bytebuddy:byte-buddy") { version { - strictly "1.14.10" + strictly "1.14.11" } } @@ -450,8 +486,8 @@ node { } // Set the npm cache (used in the Dockerfile) -tasks.register('npmSetCacheDockerfile', NpmTask) { - args = ['set', 'cache', '/opt/artemis/.npm'] +tasks.register("npmSetCacheDockerfile", NpmTask) { + args = ["set", "cache", "/opt/artemis/.npm"] } // Command to execute the JavaDoc checkstyle verification ./gradlew checkstyleMain @@ -484,6 +520,8 @@ tasks.named("dependencyUpdates").configure { // 2) Execute tests with coverage report: ./gradlew test jacocoTestReport -x webapp // 2a) Execute tests without coverage report: ./gradlew test -x webapp // 2b) Run a single test: ./gradlew test --tests ExamIntegrationTest -x webapp or ./gradlew test --tests ExamIntegrationTest.testGetExamScore -x webapp +// 2c) Execute tests with Postgres container: SPRING_PROFILES_INCLUDE=postgres ./gradlew test -x webapp +// 2d) Execute tests with MySQL container: SPRING_PROFILES_INCLUDE=mysql ./gradlew test -x webapp // 3) Verify code coverage (after tests): ./gradlew jacocoTestCoverageVerification // 4) Check Java code format: ./gradlew spotlessCheck -x webapp // 5) Apply Java code formatter: ./gradlew spotlessApply -x webapp diff --git a/docs/admin/database.rst b/docs/admin/database.rst index dd5c8bf47a04..f4776b3d9325 100644 --- a/docs/admin/database.rst +++ b/docs/admin/database.rst @@ -42,7 +42,7 @@ Migrating MySQL Data to PostgreSQL --- services: mysql: - image: docker.io/library/mysql:8 + image: docker.io/library/mysql:8.2.0 environment: - MYSQL_DATABASE=Artemis - MYSQL_ALLOW_EMPTY_PASSWORD=yes diff --git a/docs/dev/guidelines/server-tests.rst b/docs/dev/guidelines/server-tests.rst index 057cf568f97b..6061265bae96 100644 --- a/docs/dev/guidelines/server-tests.rst +++ b/docs/dev/guidelines/server-tests.rst @@ -4,6 +4,17 @@ Server Tests This section covers recommended practices for writing Artemis server tests. If you want to write tests for Artemis programming exercises to test students' submissions, check out :ref:`this `. +You can execute server tests with the following commands: + +* Execute tests with coverage report: ``./gradlew test jacocoTestReport -x webapp`` +* Execute tests without coverage report: ``./gradlew test -x webapp`` +* Run a single test: ``./gradlew test --tests ExamIntegrationTest -x webapp`` or ``./gradlew test --tests ExamIntegrationTest.testGetExamScore -x webapp`` + +To run the server tests against a real database in a Docker test container, use: + +* Execute tests with Postgres container: ``SPRING_PROFILES_INCLUDE=postgres ./gradlew test -x webapp`` +* Execute tests with MySQL container: ``SPRING_PROFILES_INCLUDE=mysql ./gradlew test -x webapp`` + 0. General testing tips ======================== Use appropriate and descriptive names for test cases so developers can easily understand what you test without looking deeper into it. diff --git a/docs/dev/setup.rst b/docs/dev/setup.rst index 8e01ac244856..1bfd48cb0de2 100644 --- a/docs/dev/setup.rst +++ b/docs/dev/setup.rst @@ -29,10 +29,10 @@ following dependencies/tools on your machine: 2. `MySQL Database Server 8 `__, or `PostgreSQL `_: Artemis uses Hibernate to store entities in an SQL database and Liquibase to automatically apply schema transformations when updating Artemis. -3. `Node.js `__: We use Node LTS (>=18.17.0 < 19) to compile +3. `Node.js `__: We use Node LTS (>=20.10.0 < 21) to compile and run the client Angular application. Depending on your system, you can install Node either from source or as a pre-packaged bundle. -4. `Npm `__: We use Npm (>=9.4.0) to +4. `Npm `__: We use Npm (>=10.2.3) to manage client side dependencies. Npm is typically bundled with Node.js, but can also be installed separately. 5. ( `Graphviz `__: We use Graphviz to generate graphs within exercise task diff --git a/docs/dev/setup/localci-localvc.rst b/docs/dev/setup/localci-localvc.rst index e222bfeaac0c..cc05c4d1de6d 100644 --- a/docs/dev/setup/localci-localvc.rst +++ b/docs/dev/setup/localci-localvc.rst @@ -36,7 +36,7 @@ Configure Artemis Create a file ``src/main/resources/config/application-local.yml`` with the following content: -.. code:: yaml +.. code-block:: yaml artemis: user-management: @@ -73,7 +73,7 @@ You can stop and remove the Bamboo and Bitbucket containers or just stop them in You also need to configure further settings in the ``src/main/resources/config/application-local.yml`` properties: -.. code:: yaml +.. code-block:: yaml artemis: user-management: @@ -147,7 +147,7 @@ Setup with Docker Compose You can also use Docker Compose to set up the local CI and local VC systems. Using the following command, you can start the Artemis and MySQL containers: -:: +.. code-block:: bash docker compose -f docker/artemis-dev-local-vc-local-ci-mysql.yml up @@ -155,3 +155,70 @@ You can also use Docker Compose to set up the local CI and local VC systems. Usi Unix systems: When running the Artemis container on a Unix system, you will have to give the user running the container permission to access the Docker socket by adding them to the ``docker`` group. You can do this by changing the value of ``services.artemis-app.group_add`` in the ``docker/artemis-dev-local-vc-local-ci-mysql.yml`` file to the group ID of the ``docker`` group on your system. You can find the group ID by running ``getent group docker | cut -d: -f3``. The default value is ``999``. Windows: If you want to run the Docker containers locally on Windows, you will have to change the value for the Docker connection URI. You can add ``ARTEMIS_CONTINUOUSINTEGRATION_DOCKERCONNECTIONURI="tcp://host.docker.internal:2375"`` to the environment file, found in ``docker/artemis/config/dev-local-vc-local-ci.env``. This overwrites the default value ``unix:///var/run/docker.sock`` for this property defined in ``src/main/resources/config/application-docker.yml``. + + +Podman as Docker alternative +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +`Podman `_ offers a container runtime that is API-compatible with Docker. +Rather than having a system-wide socket that runs with administrative permissions, Podman allows creating containers with only user permissions. +In single-user setups this might not be as relevant, but offers additional security in a production environment where the Artemis CI has to execute untrusted student code. + +.. admonition:: Podman is supported on a best-effort basis. + + We are relying on the API compatibility to provide support but are not actively testing against Podman on a test system or in continuous integration. + If you notice any issues, feel free to open an issue or pull request so that we can try to fix them. + +.. note:: + + These setup steps are mostly focused on Linux systems. + On Mac and Windows, both Docker and Podman run the containers in a small virtual machine anyway. + Therefore, there is little technical benefit relevant to Artemis for choosing one over the other in local development setups. + If in doubt, we recommend using Docker, since that solution is most likely to be tested by Artemis developers. + + +Linux setup +""""""""""" + +Podman itself should be available via your regular package manager. + +After the installation, you have to ensure that your user is allowed to create containers. +This is managed by the files ``/etc/subuid`` and ``/etc/subgid``. +Ensure both files contain a line starting with your username. +If not, you can generate the relevant lines by executing the following command: + +.. code-block:: bash + + #! /usr/bin/env sh + + printf "%s:%d:65536\n" "$USER" "$(( $(id -u) * 65536 ))" | tee -a /etc/subuid /etc/subgid + +After that, enable the Podman user socket that provides the API for the container management: + +.. code-block:: bash + + systemctl --user enable --now podman.socket + +Configure the connection to this socket in Artemis by replacing ``${UID}`` with your actual user id (``id -u``): + +.. code-block:: yaml + + artemis: + continuous-integration: + docker-connection-uri: "unix:///run/user/${UID}/podman/podman.sock" + # alternatively, if you use the `DOCKER_HOST` environment variable already + # to tell other tools to use the Podman socket instead of the Docker one: + # docker-connection-uri: "${DOCKER_HOST}" + + +Windows or Mac setup +"""""""""""""""""""" + +Podman offers a `desktop application `_ application similar to Docker desktop and `CLI tools `_ for Windows, macOS, and Linux. +As with Docker, to run containers on Windows or macOS, the runtime has to start a small virtual Linux machine that then actually runs the containers. +You can probably connect to this VM similarly as described in the regular setup steps above +(`additional Podman documentation `_). + +.. note:: + + If you try out Podman on a Windows or Mac system and have additional setup tips, feel free to submit a pull request to extend this documentation section. diff --git a/docs/dev/system-design/DataModel.svg b/docs/dev/system-design/DataModel.svg index 4c026b72401a..587cd71bccd6 100644 --- a/docs/dev/system-design/DataModel.svg +++ b/docs/dev/system-design/DataModel.svg @@ -173,7 +173,7 @@ Attributes - repositoryUrl + repositoryUri buildPlanId initializationDate presentationScore diff --git a/docs/dev/system-design/localvc-localci/localvc-localci-system-design.rst b/docs/dev/system-design/localvc-localci/localvc-localci-system-design.rst index e4692d210e43..733fc42c6987 100644 --- a/docs/dev/system-design/localvc-localci/localvc-localci-system-design.rst +++ b/docs/dev/system-design/localvc-localci/localvc-localci-system-design.rst @@ -38,7 +38,7 @@ This includes extracting the command and parameters from the client request and It reads objects and refs from the repository, updates the repository for push requests, and formats the results of the Git commands it executes into a response that it sends back to the client. This could involve sending objects and refs to the client in a packfile, or transmitting error messages. The ``Git Server`` delegates all logic connected to Artemis to the ``Local VC Servlet Service``. -This service resolves the repository from the file system depending on the repository URL. It also handles user authentication (only Basic Auth for now) and authorization. +This service resolves the repository from the file system depending on the repository URI. It also handles user authentication (only Basic Auth for now) and authorization. For authorization (e.g. "is the requesting user the owner of the repository?", "has the due date already passed?"), it uses the logic outsourced to the ``RepositoryAccessService`` that the existing online editor also uses. For push requests, the ``Local VC Servlet Service`` calls the ``processNewProgrammingSubmission()`` method of the ``Programming Submission Service`` to create a new submission and finally calls the local CI subsystem to trigger a new build. diff --git a/docs/user/exams/students_guide.rst b/docs/user/exams/students_guide.rst index 1ffbc442b9bf..45afd60199fe 100644 --- a/docs/user/exams/students_guide.rst +++ b/docs/user/exams/students_guide.rst @@ -301,7 +301,7 @@ If your instructor updates the problem statement for an exercise during the exam - **None** You can submit as many times as you want without any consequences. - **Lock Repository** There's a limit on the number of allowed submissions. Once you exceed the limit, your repository will be locked and further submissions will not be allowed. - + .. figure:: student/submission_policy_lock.png :alt: Effect of the Lock Repository Policy :align: center @@ -341,7 +341,7 @@ Summary - After you hand in, you can view the summary of your exam. - You always have access to the summary. You can find it by following the steps displayed in: `Accessing the Exam`_. - Further you have the opportunity to export the summary as a PDF file by clicking on |export_pdf|. -- The summary contains an aggregated view of all your submissions. For programming exercises, it also contains the latest commit hash and repository URL so you can review your code. +- The summary contains an aggregated view of all your submissions. For programming exercises, it also contains the latest commit hash and repository URI so you can review your code. .. figure:: student/summary.png :alt: Summary diff --git a/gradle.properties b/gradle.properties index 372d32564854..4df7e401e2e6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,13 +2,14 @@ rootProject.name=Artemis profile=dev # Build properties -node_version=18.17.0 -npm_version=9.8.0 +node_version=20.10.0 +npm_version=10.2.3 # Dependency versions jhipster_dependencies_version=7.9.3 spring_boot_version=2.7.18 -spring_security_version=5.7.11 +spring_security_version=5.8.9 +opensaml_version=4.3.0 hibernate_version=5.6.15.Final jaxb_runtime_version=4.0.4 hazelcast_version=5.3.6 @@ -17,11 +18,14 @@ mockito_version=5.8.0 # TODO: an update to 2.16.0 is currently not possible because it leads to test errors which do not make sense: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 optional type `java.util.Optional` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jdk8" to enable handling (through reference chain: de.tum.in.www1.artemis.service.connectors.ci.notification.dto.TestResultsDTO["commitHashFromTestsRepo"]) fasterxml_version=2.15.3 jgit_version=6.8.0.202311291450-r -checkstyle_version=10.12.5 +checkstyle_version=10.12.7 jplag_version=4.3.0 +# TODO: we can only upgrade to 2.x when updating Spring Boot to 3.x slf4j_version=1.7.36 -sentry_version=6.34.0 +sentry_version=7.1.0 docker_java_version=3.3.4 +# TODO: we can only upgrade to 1.4 in sync with slf4j 2.x and Spring Boot 3.x +logback_version=1.2.13 # gradle plugin version gradle_node_plugin_version=7.0.1 diff --git a/package-lock.json b/package-lock.json index 73d8a4d8dd3e..a50aa6f662a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "artemis", - "version": "6.7.4", + "version": "6.7.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "artemis", - "version": "6.7.4", + "version": "6.7.5", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -34,20 +34,20 @@ "@ng-bootstrap/ng-bootstrap": "16.0.0", "@ngx-translate/core": "15.0.0", "@ngx-translate/http-loader": "8.0.0", - "@sentry/angular-ivy": "7.91.0", - "@sentry/tracing": "7.91.0", - "@sentry/types": "7.91.0", + "@sentry/angular-ivy": "7.92.0", + "@sentry/tracing": "7.92.0", + "@sentry/types": "7.92.0", "@swimlane/ngx-charts": "20.5.0", - "@swimlane/ngx-graph": "8.2.3", - "ace-builds": "1.32.2", + "@swimlane/ngx-graph": "8.2.4", + "ace-builds": "1.32.3", "bootstrap": "5.3.2", "brace": "0.11.1", "compare-versions": "6.1.0", - "core-js": "3.34.0", + "core-js": "3.35.0", "crypto-js": "4.2.0", "dayjs": "1.11.10", "diff-match-patch-typescript": "1.0.8", - "dompurify": "3.0.6", + "dompurify": "3.0.8", "export-to-csv": "0.2.1", "franc-min": "6.1.0", "interactjs": "1.10.26", @@ -78,13 +78,13 @@ }, "devDependencies": { "@angular-builders/jest": "17.0.0", - "@angular-devkit/build-angular": "17.0.8", + "@angular-devkit/build-angular": "17.0.9", "@angular-eslint/builder": "17.1.1", "@angular-eslint/eslint-plugin": "17.1.1", "@angular-eslint/eslint-plugin-template": "17.1.1", "@angular-eslint/schematics": "17.1.1", "@angular-eslint/template-parser": "17.1.1", - "@angular/cli": "17.0.8", + "@angular/cli": "17.0.9", "@angular/compiler-cli": "17.0.8", "@angular/language-service": "17.0.8", "@types/crypto-js": "4.2.1", @@ -92,18 +92,18 @@ "@types/dompurify": "3.0.5", "@types/jest": "29.5.11", "@types/lodash-es": "4.17.12", - "@types/node": "20.10.5", + "@types/node": "20.10.6", "@types/papaparse": "5.3.14", "@types/showdown": "2.0.6", "@types/smoothscroll-polyfill": "0.3.3", "@types/sockjs-client": "1.5.4", "@types/uuid": "9.0.7", - "@typescript-eslint/eslint-plugin": "6.15.0", - "@typescript-eslint/parser": "6.15.0", + "@typescript-eslint/eslint-plugin": "6.17.0", + "@typescript-eslint/parser": "6.17.0", "eslint": "8.56.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-deprecation": "2.0.0", - "eslint-plugin-jest": "27.6.0", + "eslint-plugin-jest": "27.6.1", "eslint-plugin-jest-extended": "2.0.0", "eslint-plugin-prettier": "5.1.2", "folder-hash": "4.0.4", @@ -114,18 +114,17 @@ "jest-extended": "4.0.2", "jest-fail-on-console": "3.1.2", "jest-junit": "16.0.0", - "jest-preset-angular": "13.1.4", + "jest-preset-angular": "13.1.5", "lint-staged": "15.2.0", "ng-mocks": "14.12.1", "prettier": "3.1.1", - "sass": "1.69.5", + "sass": "1.69.7", "ts-jest": "29.1.1", "typescript": "5.2.2", - "weak-napi": "2.0.2", - "webpack-bundle-analyzer": "4.10.1" + "weak-napi": "2.0.2" }, "engines": { - "node": ">=18.17.0" + "node": ">=20.10.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -172,13 +171,41 @@ "jest": ">=29" } }, + "node_modules/@angular-builders/jest/node_modules/jest-preset-angular": { + "version": "13.1.4", + "resolved": "https://registry.npmjs.org/jest-preset-angular/-/jest-preset-angular-13.1.4.tgz", + "integrity": "sha512-XKeWa8Qt7p37SzlJ85qEXgig06SgkfrzV057X2GSMqfz/HLJmTUjMFkHJKe65ZaQumNQWCcXpxXREr6EfZ9bow==", + "dev": true, + "dependencies": { + "bs-logger": "^0.2.6", + "esbuild-wasm": ">=0.13.8", + "jest-environment-jsdom": "^29.0.0", + "jest-util": "^29.0.0", + "pretty-format": "^29.0.0", + "ts-jest": "^29.0.0" + }, + "engines": { + "node": "^14.15.0 || >=16.10.0" + }, + "optionalDependencies": { + "esbuild": ">=0.13.8" + }, + "peerDependencies": { + "@angular-devkit/build-angular": ">=13.0.0 <18.0.0", + "@angular/compiler-cli": ">=13.0.0 <18.0.0", + "@angular/core": ">=13.0.0 <18.0.0", + "@angular/platform-browser-dynamic": ">=13.0.0 <18.0.0", + "jest": "^29.0.0", + "typescript": ">=4.4" + } + }, "node_modules/@angular-devkit/architect": { - "version": "0.1700.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1700.8.tgz", - "integrity": "sha512-SWVr3CvwO6T0yW2ytszCwBT1g92vyFkwbVUxqE93urYnoD8PvP+81GH5YwVjHQTgvhP4eXQMGZ9hpHx57VOrWQ==", + "version": "0.1700.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1700.9.tgz", + "integrity": "sha512-B8OeUrvJj5JsfOJIibpoVjvuZzthPFxf1LvuUXTyQcqDUscJAe/RJBc2woT6ss13Iv/HWt8mgaMPP4CccckdNg==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.0.8", + "@angular-devkit/core": "17.0.9", "rxjs": "7.8.1" }, "engines": { @@ -188,15 +215,15 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "17.0.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.0.8.tgz", - "integrity": "sha512-u7R5yX92ZxOL/LfxiKGGqlBo86100sJ5Rabavn8DeGtYP8N0qgwCcNwlW2zaMoUlkw2geMnxcxIX5VJI4iFPUA==", + "version": "17.0.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.0.9.tgz", + "integrity": "sha512-yH6AfR2/CXrp05dIFQCroyl6Eaq8mS6tt4P7yS48+KXvAbQq2KzYW+TrDD4flFXe3qLVQGFpds3jE2auiwhHsA==", "dev": true, "dependencies": { "@ampproject/remapping": "2.2.1", - "@angular-devkit/architect": "0.1700.8", - "@angular-devkit/build-webpack": "0.1700.8", - "@angular-devkit/core": "17.0.8", + "@angular-devkit/architect": "0.1700.9", + "@angular-devkit/build-webpack": "0.1700.9", + "@angular-devkit/core": "17.0.9", "@babel/core": "7.23.2", "@babel/generator": "7.23.0", "@babel/helper-annotate-as-pure": "7.22.5", @@ -207,7 +234,7 @@ "@babel/preset-env": "7.23.2", "@babel/runtime": "7.23.2", "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "17.0.8", + "@ngtools/webpack": "17.0.9", "@vitejs/plugin-basic-ssl": "1.0.1", "ansi-colors": "4.1.3", "autoprefixer": "10.4.16", @@ -310,13 +337,36 @@ } } }, + "node_modules/@angular-devkit/build-angular/node_modules/immutable": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz", + "integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==", + "dev": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/sass": { + "version": "1.69.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.5.tgz", + "integrity": "sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1700.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1700.8.tgz", - "integrity": "sha512-GA7QlCAlYB3uBkRaUYgIC/Vfajb9jMmouwYiAAEm34ZyP3ThFjdqsYd/A/exnuESt5o6Bh++C/PI34sV3lawRA==", + "version": "0.1700.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1700.9.tgz", + "integrity": "sha512-NBpTb5kdnTePtNirsJQFXfOIFKTPdDqJe0b0sI3FI860po7uvUFu1m5pL5QSkJLmdqrjfPkNq7svGf7NlHQ8JA==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1700.8", + "@angular-devkit/architect": "0.1700.9", "rxjs": "7.8.1" }, "engines": { @@ -330,9 +380,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "17.0.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.0.8.tgz", - "integrity": "sha512-gI8+SOwGUwr0WOlFrhLjohLolMzcguuoR0LTZEcGjdXvQyPgH4NDSRIIrfWCdu+ZVhfy76o3zQYdYc9QN8NrjQ==", + "version": "17.0.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.0.9.tgz", + "integrity": "sha512-r5jqwpWOgowqe9KSDqJ3iSbmsEt2XPjSvRG4DSI2T9s31bReoMtreo8b7wkRa2B3hbcDnstFbn8q27VvJDqRaQ==", "dev": true, "dependencies": { "ajv": "8.12.0", @@ -357,12 +407,12 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "17.0.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.0.8.tgz", - "integrity": "sha512-syo814SVWfJvne448IijjZvpWbuqJsEutdNqHWLTewTfX2U3KrIAr/XRVcXQMuyMvLCDiuxjMgEJxOIP7mcIPw==", + "version": "17.0.9", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.0.9.tgz", + "integrity": "sha512-5ti7g45F2KjDJS0DbgnOGI1GyKxGpn4XsKTYJFJrSAWj6VpuvPy/DINRrXNuRVo09VPEkqA+IW7QwaG9icptQg==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.0.8", + "@angular-devkit/core": "17.0.9", "jsonc-parser": "3.2.0", "magic-string": "0.30.5", "ora": "5.4.1", @@ -503,15 +553,15 @@ } }, "node_modules/@angular/cli": { - "version": "17.0.8", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.0.8.tgz", - "integrity": "sha512-yZXYNLAFv9u2qypsVqtS+rRCsnjsIPYXr6TcI/r5buzOtC7UQ2lleYsWJqX47SsyGMk/o3gaYg5Bj2I5mmRDLA==", + "version": "17.0.9", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.0.9.tgz", + "integrity": "sha512-a1rLAu3TNU5d56ozBnx9UZchJDKC8qMvZL4ThJhcaTUJb0Cj//gqLJdNdMcB0p1Ve9lmmAQ3J17+2Xij1u3sNg==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1700.8", - "@angular-devkit/core": "17.0.8", - "@angular-devkit/schematics": "17.0.8", - "@schematics/angular": "17.0.8", + "@angular-devkit/architect": "0.1700.9", + "@angular-devkit/core": "17.0.9", + "@angular-devkit/schematics": "17.0.9", + "@schematics/angular": "17.0.9", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.3", "ini": "4.1.1", @@ -895,13 +945,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", - "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -910,9 +960,9 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.5.tgz", - "integrity": "sha512-QELlRWxSpgdwdJzSJn4WAhKC+hvw/AtHbbrIoncKHkhKKR/luAlKkgBDcri1EzWAo8f8VvYVryEHN4tax/V67A==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.7.tgz", + "integrity": "sha512-xCoqR/8+BoNnXOY7RVSgv6X+o7pmT5q1d+gGcRlXYkI+9B31glE4jeejhKVpA04O1AtzOt7OSQ6VYKP5FcRl9g==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", @@ -950,9 +1000,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.3.tgz", - "integrity": "sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==", + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.4.tgz", + "integrity": "sha512-QcJMILQCu2jm5TFPGA3lCpJJTeEP+mqeXooG/NZbg/h5FTFi6V0+99ahlRsW8/kRLyb24LZVCCiclDedhLKcBA==", "dev": true, "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", @@ -1164,13 +1214,13 @@ } }, "node_modules/@babel/helpers": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.5.tgz", - "integrity": "sha512-oO7us8FzTEsG3U6ag9MfdF1iA/7Z6dz+MtFhifZk8C8o453rGJFFWUP1t+ULM9TUIAzC9uxXEiXjOiVMyd7QPg==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.7.tgz", + "integrity": "sha512-6AMnjCoC8wjqBzDHkuqpa7jAKwvMo4dC+lr/TFBz+ucfulO1XMpDnwWPGBNwClOKZ8h6xn5N81W/R5OrcKtCbQ==", "dependencies": { "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.5", - "@babel/types": "^7.23.5" + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6" }, "engines": { "node": ">=6.9.0" @@ -1190,9 +1240,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.5.tgz", - "integrity": "sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", "bin": { "parser": "bin/babel-parser.js" }, @@ -1767,12 +1817,13 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.3.tgz", - "integrity": "sha512-X8jSm8X1CMwxmK878qsUGJRmbysKNbdpTv/O1/v0LuY/ZkZrng5WYiekYSdg9m09OTmDDUWeEDsTE+17WYbAZw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.6.tgz", + "integrity": "sha512-aYH4ytZ0qSuBbpfhuofbg/e96oQ7U2w1Aw/UQmKT+1l39uEhUPoFS3fHevDc1G0OvewyDudfMKY1OulczHzWIw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -2451,19 +2502,19 @@ } }, "node_modules/@babel/traverse": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.5.tgz", - "integrity": "sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz", + "integrity": "sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==", "dependencies": { "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.5", + "@babel/generator": "^7.23.6", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.5", - "@babel/types": "^7.23.5", - "debug": "^4.1.0", + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -2471,11 +2522,11 @@ } }, "node_modules/@babel/traverse/node_modules/@babel/generator": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.5.tgz", - "integrity": "sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dependencies": { - "@babel/types": "^7.23.5", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -2485,9 +2536,9 @@ } }, "node_modules/@babel/types": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.5.tgz", - "integrity": "sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", @@ -2980,10 +3031,20 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -3013,6 +3074,18 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/eslintrc/node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -3136,6 +3209,28 @@ "node": ">=10.10.0" } }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -4804,9 +4899,9 @@ } }, "node_modules/@ngtools/webpack": { - "version": "17.0.8", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.0.8.tgz", - "integrity": "sha512-wx0XBMrbpDeailK2uIhp/ZVMC3GK3BWwJjUu5SbT4BFrcoi2Zd9/9m0RCBAY54UXLBCqKd+ih7pJ6JSvprZmWw==", + "version": "17.0.9", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.0.9.tgz", + "integrity": "sha512-ilbzwW30NaccrhYbdY3jy/ZpbC0l7W6+L2Cd3dzHFQ1gZGckibDdMzjibW/vyq/vRf0xr25+oBVIqUn8kZ606g==", "dev": true, "engines": { "node": "^18.13.0 || >=20.9.0", @@ -4929,9 +5024,9 @@ } }, "node_modules/@npmcli/git": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.3.tgz", - "integrity": "sha512-UZp9NwK+AynTrKvHn5k3KviW/hA5eENmFsu3iAPe7sWRt0lFUdsY/wXIYjpDFe7cdSNwOIzbObfwgt6eL5/2zw==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.4.tgz", + "integrity": "sha512-nr6/WezNzuYUppzXRaYu/W4aT5rLxdXqEFupbh6e/ovlYFQ8hpu1UUPV3Ir/YTl+74iXl2ZOMlGzudh9ZPUchQ==", "dev": true, "dependencies": { "@npmcli/promise-spawn": "^7.0.0", @@ -5006,9 +5101,9 @@ } }, "node_modules/@npmcli/promise-spawn": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.0.tgz", - "integrity": "sha512-wBqcGsMELZna0jDblGd7UXgOby45TQaMWmbFwWX+SEotk4HV6zG2t6rT9siyLhPk4P6YYqgfL1UO8nMWDBVJXQ==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.1.tgz", + "integrity": "sha512-P4KkF9jX3y+7yFUxgcUdDtLy+t4OlDGuEBLNs57AZsfSfg+uV6MLndqGpnl4831ggaEdXwR50XFoZP4VFtHolg==", "dev": true, "dependencies": { "which": "^4.0.0" @@ -5042,9 +5137,9 @@ } }, "node_modules/@npmcli/run-script": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-7.0.2.tgz", - "integrity": "sha512-Omu0rpA8WXvcGeY6DDzyRoY1i5DkCBkzyJ+m2u7PD6quzb0TvSqdIPOkTn8ZBOj7LbbcbMfZ3c5skwSu6m8y2w==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-7.0.3.tgz", + "integrity": "sha512-ZMWGLHpzMq3rBGIwPyeaoaleaLMvrBrH8nugHxTi5ACkJZXTxXPtVuEH91ifgtss5hUwJQ2VDnzDBWPmz78rvg==", "dev": true, "dependencies": { "@npmcli/node-gyp": "^3.0.0", @@ -5291,19 +5386,11 @@ "node": ">=14" } }, - "node_modules/@pkgr/utils": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", - "integrity": "sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==", + "node_modules/@pkgr/core": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.0.tgz", + "integrity": "sha512-Zwq5OCzuwJC2jwqmpEQt7Ds1DTi6BWSwoGkbb1n9pO3hzb35BoJELx7c0T23iDkBGkh2e7tvOtjF3tr3OaQHDQ==", "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "fast-glob": "^3.3.0", - "is-glob": "^4.0.3", - "open": "^9.1.0", - "picocolors": "^1.0.0", - "tslib": "^2.6.0" - }, "engines": { "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, @@ -5311,42 +5398,6 @@ "url": "https://opencollective.com/unts" } }, - "node_modules/@pkgr/utils/node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@pkgr/utils/node_modules/open": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", - "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", - "dev": true, - "dependencies": { - "default-browser": "^4.0.0", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.24", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.24.tgz", - "integrity": "sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==", - "dev": true - }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -5358,9 +5409,9 @@ } }, "node_modules/@redux-saga/core": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.2.3.tgz", - "integrity": "sha512-U1JO6ncFBAklFTwoQ3mjAeQZ6QGutsJzwNBjgVLSWDpZTRhobUzuVDS1qH3SKGJD8fvqoaYOjp6XJ3gCmeZWgA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.3.0.tgz", + "integrity": "sha512-L+i+qIGuyWn7CIg7k1MteHGfttKPmxwZR5E7OsGikCL2LzYA0RERlaUY00Y3P3ZV2EYgrsYlBrGs6cJP5OKKqA==", "dependencies": { "@babel/runtime": "^7.6.3", "@redux-saga/deferred": "^1.2.1", @@ -5368,7 +5419,6 @@ "@redux-saga/is": "^1.1.3", "@redux-saga/symbols": "^1.1.3", "@redux-saga/types": "^1.2.1", - "redux": "^4.0.4", "typescript-tuple": "^2.2.1" }, "funding": { @@ -5409,13 +5459,13 @@ "integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==" }, "node_modules/@schematics/angular": { - "version": "17.0.8", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.0.8.tgz", - "integrity": "sha512-1h5mwKFv1B/L5JWZ0mxnC4ms06iwnSi/w+GgRZPeM3P5BpuZuvAkFiClNnM55iLlQJXRQioPNLM3sOsz7spR6w==", + "version": "17.0.9", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.0.9.tgz", + "integrity": "sha512-XPaHAhobxdQMswH8wSrfToKN7wmGJFh/K5jq/3J+78KeSBZStYxZkVIQbvJkSU8Y1MsdVaeMYKDE8rjFN83OYA==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.0.8", - "@angular-devkit/schematics": "17.0.8", + "@angular-devkit/core": "17.0.9", + "@angular-devkit/schematics": "17.0.9", "jsonc-parser": "3.2.0" }, "engines": { @@ -5425,39 +5475,39 @@ } }, "node_modules/@sentry-internal/feedback": { - "version": "7.91.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.91.0.tgz", - "integrity": "sha512-SJKTSaz68F5YIwF79EttBm915M2LnacgZMYRnRumyTmMKnebGhYQLwWbZdpaDvOa1U18dgRajDX8Qed/8A3tXw==", + "version": "7.92.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.92.0.tgz", + "integrity": "sha512-/jEALRtVqboxB9kcK2tag8QCO6XANTlGBb9RV3oeGXJe0DDNJXRq6wVZbfgztXJRrfgx4XVDcNt1pRVoGGG++g==", "dependencies": { - "@sentry/core": "7.91.0", - "@sentry/types": "7.91.0", - "@sentry/utils": "7.91.0" + "@sentry/core": "7.92.0", + "@sentry/types": "7.92.0", + "@sentry/utils": "7.92.0" }, "engines": { "node": ">=12" } }, "node_modules/@sentry-internal/tracing": { - "version": "7.91.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.91.0.tgz", - "integrity": "sha512-JH5y6gs6BS0its7WF2DhySu7nkhPDfZcdpAXldxzIlJpqFkuwQKLU5nkYJpiIyZz1NHYYtW5aum2bV2oCOdDRA==", + "version": "7.92.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.92.0.tgz", + "integrity": "sha512-ur55vPcUUUWFUX4eVLNP71ohswK7ZZpleNZw9Y1GfLqyI+0ILQUwjtzqItJrdClvVsdRZJMRmDV40Hp9Lbb9mA==", "dependencies": { - "@sentry/core": "7.91.0", - "@sentry/types": "7.91.0", - "@sentry/utils": "7.91.0" + "@sentry/core": "7.92.0", + "@sentry/types": "7.92.0", + "@sentry/utils": "7.92.0" }, "engines": { "node": ">=8" } }, "node_modules/@sentry/angular-ivy": { - "version": "7.91.0", - "resolved": "https://registry.npmjs.org/@sentry/angular-ivy/-/angular-ivy-7.91.0.tgz", - "integrity": "sha512-iVCwF+JMU4/nVgxTtWFbmWioX0oNb4qEOsRFsa03win8LEsIniGBXkpROrmXKhdL/aMUl/acHWzXG7Sjo0P8fw==", + "version": "7.92.0", + "resolved": "https://registry.npmjs.org/@sentry/angular-ivy/-/angular-ivy-7.92.0.tgz", + "integrity": "sha512-XZhqJ0JfjMeVp5n4a8NRHo2bgiO1x5nNf5Z1sNTND6WiQc5oHT+Bqbmoszkp7cy4vC27Knffg2RnESSQiQAwjg==", "dependencies": { - "@sentry/browser": "7.91.0", - "@sentry/types": "7.91.0", - "@sentry/utils": "7.91.0", + "@sentry/browser": "7.92.0", + "@sentry/types": "7.92.0", + "@sentry/utils": "7.92.0", "tslib": "^2.4.1" }, "engines": { @@ -5471,72 +5521,72 @@ } }, "node_modules/@sentry/browser": { - "version": "7.91.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.91.0.tgz", - "integrity": "sha512-lJv3x/xekzC/biiyAsVCioq2XnKNOZhI6jY3ZzLJZClYV8eKRi7D3KCsHRvMiCdGak1d/6sVp8F4NYY+YiWy1Q==", + "version": "7.92.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.92.0.tgz", + "integrity": "sha512-loMr02/zQ38u8aQhYLtIBg0i5n3ps2e3GUXrt3CdsJQdkRYfa62gcrE7SzvoEpMVHTk7VOI4fWGht8cWw/1k3A==", "dependencies": { - "@sentry-internal/feedback": "7.91.0", - "@sentry-internal/tracing": "7.91.0", - "@sentry/core": "7.91.0", - "@sentry/replay": "7.91.0", - "@sentry/types": "7.91.0", - "@sentry/utils": "7.91.0" + "@sentry-internal/feedback": "7.92.0", + "@sentry-internal/tracing": "7.92.0", + "@sentry/core": "7.92.0", + "@sentry/replay": "7.92.0", + "@sentry/types": "7.92.0", + "@sentry/utils": "7.92.0" }, "engines": { "node": ">=8" } }, "node_modules/@sentry/core": { - "version": "7.91.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.91.0.tgz", - "integrity": "sha512-tu+gYq4JrTdrR+YSh5IVHF0fJi/Pi9y0HZ5H9HnYy+UMcXIotxf6hIEaC6ZKGeLWkGXffz2gKpQLe/g6vy/lPA==", + "version": "7.92.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.92.0.tgz", + "integrity": "sha512-1Tly7YB2I1byI5xb0Cwrxs56Rhww+6mQ7m9P7rTmdC3/ijOzbEoohtYIUPwcooCEarpbEJe/tAayRx6BrH2UbQ==", "dependencies": { - "@sentry/types": "7.91.0", - "@sentry/utils": "7.91.0" + "@sentry/types": "7.92.0", + "@sentry/utils": "7.92.0" }, "engines": { "node": ">=8" } }, "node_modules/@sentry/replay": { - "version": "7.91.0", - "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.91.0.tgz", - "integrity": "sha512-XwbesnLLNtaVXKtDoyBB96GxJuhGi9zy3a662Ba/McmumCnkXrMQYpQPh08U7MgkTyDRgjDwm7PXDhiKpcb03g==", + "version": "7.92.0", + "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.92.0.tgz", + "integrity": "sha512-G1t9Uvc9cR8VpNkElwvHIMGzykjIKikb10n0tfVd3e+rBPMCCjCPWOduwG6jZYxcvCjTpqmJh6NSLXxL/Mt4JA==", "dependencies": { - "@sentry-internal/tracing": "7.91.0", - "@sentry/core": "7.91.0", - "@sentry/types": "7.91.0", - "@sentry/utils": "7.91.0" + "@sentry-internal/tracing": "7.92.0", + "@sentry/core": "7.92.0", + "@sentry/types": "7.92.0", + "@sentry/utils": "7.92.0" }, "engines": { "node": ">=12" } }, "node_modules/@sentry/tracing": { - "version": "7.91.0", - "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.91.0.tgz", - "integrity": "sha512-IlSAMvqfCL/2TwwN4Tmk6bGMgilGruv5oIJ1GMenVZk53bHwjpjzMbd0ms8+S5zJwAgTQXoCbRhaFFrNmptteQ==", + "version": "7.92.0", + "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.92.0.tgz", + "integrity": "sha512-1+TFFPVEdax4dNi68gin6MENiyGe9mOuNXfjulrP5eCzUEByus5HAxeDI/LLQ1hArfn048AzwSwKUsS2fO5sbg==", "dependencies": { - "@sentry-internal/tracing": "7.91.0" + "@sentry-internal/tracing": "7.92.0" }, "engines": { "node": ">=8" } }, "node_modules/@sentry/types": { - "version": "7.91.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.91.0.tgz", - "integrity": "sha512-bcQnb7J3P3equbCUc+sPuHog2Y47yGD2sCkzmnZBjvBT0Z1B4f36fI/5WjyZhTjLSiOdg3F2otwvikbMjmBDew==", + "version": "7.92.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.92.0.tgz", + "integrity": "sha512-APmSOuZuoRGpbPpPeYIbMSplPjiWNLZRQa73QiXuTflW4Tu/ItDlU8hOa2+A6JKVkJCuD2EN6yUrxDGSMyNXeg==", "engines": { "node": ">=8" } }, "node_modules/@sentry/utils": { - "version": "7.91.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.91.0.tgz", - "integrity": "sha512-fvxjrEbk6T6Otu++Ax9ntlQ0sGRiwSC179w68aC3u26Wr30FAIRKqHTCCdc2jyWk7Gd9uWRT/cq+g8NG/8BfSg==", + "version": "7.92.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.92.0.tgz", + "integrity": "sha512-3nEfrQ1z28b/2zgFGANPh5yMVtgwXmrasZxTvKbrAj+KWJpjrJHrIR84r9W277J44NMeZ5RhRW2uoDmuBslPnA==", "dependencies": { - "@sentry/types": "7.91.0" + "@sentry/types": "7.92.0" }, "engines": { "node": ">=8" @@ -5653,9 +5703,9 @@ } }, "node_modules/@swimlane/ngx-graph": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/@swimlane/ngx-graph/-/ngx-graph-8.2.3.tgz", - "integrity": "sha512-RnNGIAuC3ilO+8lwKA068Fd5sqv5NaW7d+Fz0zhLJp+jm+j6VG72nz65lAd002J0fCCuBV4KXUX9KzOVwBcpIQ==", + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/@swimlane/ngx-graph/-/ngx-graph-8.2.4.tgz", + "integrity": "sha512-/f/2JZc3AwRuGY3+1b8BgkXXy99GwcobV39tL3PneyWfO19U3ATG8gCW4z3xQdmh9094L03wcAoFxisnnDe5gA==", "dependencies": { "d3-dispatch": "^1.0.3", "d3-ease": "^1.0.5", @@ -5765,30 +5815,6 @@ "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/@tufjs/models/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@tufjs/models/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -5803,9 +5829,9 @@ } }, "node_modules/@types/babel__generator": { - "version": "7.6.7", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.7.tgz", - "integrity": "sha512-6Sfsq+EaaLrw4RmdFWE9Onp63TOUue71AWb4Gpa6JxzgTYtimbM086WnYTy2U67AofR++QKCo08ZP6pwx8YFHQ==", + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", "dev": true, "dependencies": { "@babel/types": "^7.0.0" @@ -5822,9 +5848,9 @@ } }, "node_modules/@types/babel__traverse": { - "version": "7.20.4", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.4.tgz", - "integrity": "sha512-mSM/iKUk5fDDrEV/e83qY+Cr3I1+Q3qqTuEn++HAWYjEa1+NxZr6CNrcJGf2ZTnq4HoFGC3zaTPZTobCzCFukA==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", + "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", "dev": true, "dependencies": { "@babel/types": "^7.20.7" @@ -5914,9 +5940,9 @@ } }, "node_modules/@types/eslint": { - "version": "8.44.8", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.8.tgz", - "integrity": "sha512-4K8GavROwhrYl2QXDXm0Rv9epkA8GBFu0EI+XrrnnuCl7u8CWBRusX7fXJfanhZTDWSAL24gDI/UqXyUM0Injw==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.1.tgz", + "integrity": "sha512-18PLWRzhy9glDQp3+wOgfLYRWlhgX0azxgJ63rdpoUHyrC9z0f5CkFburjQx4uD7ZCruw85ZtMt6K+L+R8fLJQ==", "dev": true, "dependencies": { "@types/estree": "*", @@ -6069,18 +6095,18 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.10.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz", - "integrity": "sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==", + "version": "20.10.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.6.tgz", + "integrity": "sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==", "dev": true, "dependencies": { "undici-types": "~5.26.4" } }, "node_modules/@types/node-forge": { - "version": "1.3.10", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.10.tgz", - "integrity": "sha512-y6PJDYN4xYBxwd22l+OVH35N+1fCYWiuC3aiP2SlXVE6Lo7SS+rSx9r89hLxrP4pn6n1lBGhHJ12pj3F3Mpttw==", + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", "dev": true, "dependencies": { "@types/node": "*" @@ -6113,9 +6139,9 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.2.42", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.42.tgz", - "integrity": "sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA==", + "version": "18.2.47", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.47.tgz", + "integrity": "sha512-xquNkkOirwyCgoClNk85BjP+aqnIS+ckAJ8i37gAbDs14jfW/J23f2GItAf33oiUPQnqNMALiFeoM9Y5mbjpVQ==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -6250,16 +6276,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.15.0.tgz", - "integrity": "sha512-j5qoikQqPccq9QoBAupOP+CBu8BaJ8BLjaXSioDISeTZkVO3ig7oSIKh3H+rEpee7xCXtWwSB4KIL5l6hWZzpg==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.17.0.tgz", + "integrity": "sha512-Vih/4xLXmY7V490dGwBQJTpIZxH4ZFH6eCVmQ4RFkB+wmaCTDAx4dtgoWwMNGKLkqRY1L6rPqzEbjorRnDo4rQ==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.15.0", - "@typescript-eslint/type-utils": "6.15.0", - "@typescript-eslint/utils": "6.15.0", - "@typescript-eslint/visitor-keys": "6.15.0", + "@typescript-eslint/scope-manager": "6.17.0", + "@typescript-eslint/type-utils": "6.17.0", + "@typescript-eslint/utils": "6.17.0", + "@typescript-eslint/visitor-keys": "6.17.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -6285,13 +6311,13 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.15.0.tgz", - "integrity": "sha512-CnmHKTfX6450Bo49hPg2OkIm/D/TVYV7jO1MCfPYGwf6x3GO0VU8YMO5AYMn+u3X05lRRxA4fWCz87GFQV6yVQ==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.17.0.tgz", + "integrity": "sha512-hDXcWmnbtn4P2B37ka3nil3yi3VCQO2QEB9gBiHJmQp5wmyQWqnjA85+ZcE8c4FqnaB6lBwMrPkgd4aBYz3iNg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.15.0", - "@typescript-eslint/utils": "6.15.0", + "@typescript-eslint/typescript-estree": "6.17.0", + "@typescript-eslint/utils": "6.17.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -6312,17 +6338,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.15.0.tgz", - "integrity": "sha512-eF82p0Wrrlt8fQSRL0bGXzK5nWPRV2dYQZdajcfzOD9+cQz9O7ugifrJxclB+xVOvWvagXfqS4Es7vpLP4augw==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.17.0.tgz", + "integrity": "sha512-LofsSPjN/ITNkzV47hxas2JCsNCEnGhVvocfyOcLzT9c/tSZE7SfhS/iWtzP1lKNOEfLhRTZz6xqI8N2RzweSQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.15.0", - "@typescript-eslint/types": "6.15.0", - "@typescript-eslint/typescript-estree": "6.15.0", + "@typescript-eslint/scope-manager": "6.17.0", + "@typescript-eslint/types": "6.17.0", + "@typescript-eslint/typescript-estree": "6.17.0", "semver": "^7.5.4" }, "engines": { @@ -6337,15 +6363,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.15.0.tgz", - "integrity": "sha512-MkgKNnsjC6QwcMdlNAel24jjkEO/0hQaMDLqP4S9zq5HBAUJNQB6y+3DwLjX7b3l2b37eNAxMPLwb3/kh8VKdA==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.17.0.tgz", + "integrity": "sha512-C4bBaX2orvhK+LlwrY8oWGmSl4WolCfYm513gEccdWZj0CwGadbIADb0FtVEcI+WzUyjyoBj2JRP8g25E6IB8A==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.15.0", - "@typescript-eslint/types": "6.15.0", - "@typescript-eslint/typescript-estree": "6.15.0", - "@typescript-eslint/visitor-keys": "6.15.0", + "@typescript-eslint/scope-manager": "6.17.0", + "@typescript-eslint/types": "6.17.0", + "@typescript-eslint/typescript-estree": "6.17.0", + "@typescript-eslint/visitor-keys": "6.17.0", "debug": "^4.3.4" }, "engines": { @@ -6365,13 +6391,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.15.0.tgz", - "integrity": "sha512-+BdvxYBltqrmgCNu4Li+fGDIkW9n//NrruzG9X1vBzaNK+ExVXPoGB71kneaVw/Jp+4rH/vaMAGC6JfMbHstVg==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.17.0.tgz", + "integrity": "sha512-RX7a8lwgOi7am0k17NUO0+ZmMOX4PpjLtLRgLmT1d3lBYdWH4ssBUbwdmc5pdRX8rXon8v9x8vaoOSpkHfcXGA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.15.0", - "@typescript-eslint/visitor-keys": "6.15.0" + "@typescript-eslint/types": "6.17.0", + "@typescript-eslint/visitor-keys": "6.17.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -6466,9 +6492,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.15.0.tgz", - "integrity": "sha512-yXjbt//E4T/ee8Ia1b5mGlbNj9fB9lJP4jqLbZualwpP2BCQ5is6BcWwxpIsY4XKAhmdv3hrW92GdtJbatC6dQ==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.17.0.tgz", + "integrity": "sha512-qRKs9tvc3a4RBcL/9PXtKSehI/q8wuU9xYJxe97WFxnzH8NWWtcW3ffNS+EWg8uPvIerhjsEZ+rHtDqOCiH57A==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -6479,16 +6505,17 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.15.0.tgz", - "integrity": "sha512-7mVZJN7Hd15OmGuWrp2T9UvqR2Ecg+1j/Bp1jXUEY2GZKV6FXlOIoqVDmLpBiEiq3katvj/2n2mR0SDwtloCew==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.17.0.tgz", + "integrity": "sha512-gVQe+SLdNPfjlJn5VNGhlOhrXz4cajwFd5kAgWtZ9dCZf4XJf8xmgCTLIqec7aha3JwgLI2CK6GY1043FRxZwg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.15.0", - "@typescript-eslint/visitor-keys": "6.15.0", + "@typescript-eslint/types": "6.17.0", + "@typescript-eslint/visitor-keys": "6.17.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", + "minimatch": "9.0.3", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" }, @@ -6605,12 +6632,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.15.0.tgz", - "integrity": "sha512-1zvtdC1a9h5Tb5jU9x3ADNXO9yjP8rXlaoChu0DQX40vf5ACVpYIVIZhIMZ6d5sDXH7vq4dsZBT1fEGj8D2n2w==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.17.0.tgz", + "integrity": "sha512-H6VwB/k3IuIeQOyYczyyKN8wH6ed8EwliaYHLxOIhyF0dYEIsN8+Bk3GE19qafeMKyZJJHP8+O1HiFhFLUNKSg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.15.0", + "@typescript-eslint/types": "6.17.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -6864,14 +6891,14 @@ } }, "node_modules/ace-builds": { - "version": "1.32.2", - "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.32.2.tgz", - "integrity": "sha512-mnJAc803p+7eeDt07r6XI7ufV7VdkpPq4gJZT8Jb3QsowkaBTVy4tdBgPrVT0WbXLm0toyEQXURKSVNj/7dfJQ==" + "version": "1.32.3", + "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.32.3.tgz", + "integrity": "sha512-ptSTUmDEU+LuwGiPY3/qQPmmAWE27vuv5sASL8swLRyLGJb7Ye7a8MrJ4NnAkFh1sJgVUqKTEGWRRFDmqYPw2Q==" }, "node_modules/acorn": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", - "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -7111,9 +7138,9 @@ } }, "node_modules/array-flatten": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", - "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "dev": true }, "node_modules/array-union": { @@ -7343,13 +7370,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.6.tgz", - "integrity": "sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==", + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.7.tgz", + "integrity": "sha512-LidDk/tEGDfuHW2DWh/Hgo4rmnw3cduK6ZkOI1NPFceSK3n/yAGeOsNT7FLnSGHkXj3RHGSEVkN3FsCTY6w2CQ==", "dev": true, "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.4.3", + "@babel/helper-define-polyfill-provider": "^0.4.4", "semver": "^6.3.1" }, "peerDependencies": { @@ -7357,12 +7384,12 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.8.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.6.tgz", - "integrity": "sha512-leDIc4l4tUgU7str5BWLS2h8q2N4Nf6lGZP6UrNDxdtfF2g69eJ5L0H7S8A5Ln/arfFAfHor5InAdZuIOwZdgQ==", + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.7.tgz", + "integrity": "sha512-KyDvZYxAzkC0Aj2dAPyDzi2Ym15e5JKZSK+maI7NAwSqofvuFglbSsxE7wUOvTg9oFVnHMzVzBKcqEb4PJgtOA==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.3", + "@babel/helper-define-polyfill-provider": "^0.4.4", "core-js-compat": "^3.33.1" }, "peerDependencies": { @@ -7370,12 +7397,12 @@ } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.3.tgz", - "integrity": "sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.4.tgz", + "integrity": "sha512-S/x2iOCvDaCASLYsOOgWOq4bCfKYVqvO/uxjkaYyZ3rVsVE3CeAI/c84NpyuBBymEgNvHgjEot3a9/Z/kXvqsg==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.3" + "@babel/helper-define-polyfill-provider": "^0.4.4" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -7487,15 +7514,6 @@ "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", "dev": true }, - "node_modules/big-integer": { - "version": "1.6.52", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", - "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", - "dev": true, - "engines": { - "node": ">=0.6" - } - }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -7600,13 +7618,11 @@ } }, "node_modules/bonjour-service": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz", - "integrity": "sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.0.tgz", + "integrity": "sha512-xdzMA6JGckxyJzZByjEWRcfKmDxXaGXZWVftah3FkCqdlePNS9DjHSUN5zkP4oEfz/t0EXXlro88EIhzwMB4zA==", "dev": true, "dependencies": { - "array-flatten": "^2.1.2", - "dns-equal": "^1.0.0", "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } @@ -7635,31 +7651,18 @@ "@popperjs/core": "^2.11.8" } }, - "node_modules/bplist-parser": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", - "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", - "dev": true, - "dependencies": { - "big-integer": "^1.6.44" - }, - "engines": { - "node": ">= 5.10.0" - } - }, "node_modules/brace": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/brace/-/brace-0.11.1.tgz", "integrity": "sha512-Fc8Ne62jJlKHiG/ajlonC4Sd66Pq68fFwK4ihJGNZpGqboc324SQk+lRvMzpPRuJOmfrJefdG8/7JdWX4bzJ2Q==" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -7982,21 +7985,6 @@ "semver": "^7.0.0" } }, - "node_modules/bundle-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", - "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", - "dev": true, - "dependencies": { - "run-applescript": "^5.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -8007,9 +7995,9 @@ } }, "node_modules/cacache": { - "version": "18.0.1", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.1.tgz", - "integrity": "sha512-g4Uf2CFZPaxtJKre6qr4zqLDOOPU7bNVhWjlNhvzc51xaTOx2noMOLhfFkTAqwtrAZAKQUuDfyjitzilpA8WsQ==", + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.2.tgz", + "integrity": "sha512-r3NU8h/P+4lVUHfeRw1dtgQYar3DZMm4/cm2bZgOvrFC/su7budSOeqh52VJIC4U4iG1WWwV6vRW0znqBvxNuw==", "dev": true, "dependencies": { "@npmcli/fs": "^3.1.0", @@ -8029,15 +8017,6 @@ "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/cacache/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/cacache/node_modules/glob": { "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", @@ -8069,21 +8048,6 @@ "node": "14 || >=16.14" } }, - "node_modules/cacache/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/call-bind": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", @@ -8125,9 +8089,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001566", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001566.tgz", - "integrity": "sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==", + "version": "1.0.30001574", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001574.tgz", + "integrity": "sha512-BtYEK4r/iHt/txm81KBudCUcTy7t+s9emrIaHqjYurQ10x71zJ5VQ9x1dYPcz/b+pKSp4y/v1xSI67A+LzpNyg==", "funding": [ { "type": "opencollective", @@ -8426,18 +8390,6 @@ "node": ">=6" } }, - "node_modules/clone-deep/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -8724,9 +8676,9 @@ } }, "node_modules/core-js": { - "version": "3.34.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.34.0.tgz", - "integrity": "sha512-aDdvlDder8QmY91H88GzNi9EtQi2TjvQhpCX6B1v/dAZHU1AuLgHvRh54RiOerpEhEW46Tkf+vgAViB/CWC0ag==", + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.35.0.tgz", + "integrity": "sha512-ntakECeqg81KqMueeGJ79Q5ZgQNR+6eaE8sxGCx62zMbAIj65q+uYvatToew3m6eAGdU4gNZwpZ34NMe4GYswg==", "hasInstallScript": true, "funding": { "type": "opencollective", @@ -8734,9 +8686,9 @@ } }, "node_modules/core-js-compat": { - "version": "3.34.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.34.0.tgz", - "integrity": "sha512-4ZIyeNbW/Cn1wkMMDy+mvrRUxrwFNjKwbhCfQpDd+eLgYipDqp8oGFGtLmhh18EDPKA0g3VUBYOxQGGwvWLVpA==", + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.0.tgz", + "integrity": "sha512-5blwFAddknKeNgsjBzilkdQ0+YK8L1PfqPYq40NOYMYFSS38qj+hpTcLLWwpIwA2A5bje/x5jmVn2tzUMg9IVw==", "dev": true, "dependencies": { "browserslist": "^4.22.2" @@ -9397,12 +9349,6 @@ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" }, - "node_modules/debounce": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", - "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", - "dev": true - }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -9454,157 +9400,13 @@ "node": ">=0.10.0" } }, - "node_modules/default-browser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz", - "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==", + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", "dev": true, "dependencies": { - "bundle-name": "^3.0.0", - "default-browser-id": "^3.0.0", - "execa": "^7.1.1", - "titleize": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser-id": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", - "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", - "dev": true, - "dependencies": { - "bplist-parser": "^0.2.0", - "untildify": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser/node_modules/execa": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", - "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.1", - "human-signals": "^4.3.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^3.0.7", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": "^14.18.0 || ^16.14.0 || >=18.0.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/default-browser/node_modules/human-signals": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", - "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", - "dev": true, - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/default-browser/node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser/node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser/node_modules/npm-run-path": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", - "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", - "dev": true, - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser/node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser/node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-gateway": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", - "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", - "dev": true, - "dependencies": { - "execa": "^5.0.0" + "execa": "^5.0.0" }, "engines": { "node": ">= 10" @@ -9744,12 +9546,6 @@ "node": ">=8" } }, - "node_modules/dns-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==", - "dev": true - }, "node_modules/dns-packet": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", @@ -9829,9 +9625,9 @@ } }, "node_modules/dompurify": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.6.tgz", - "integrity": "sha512-ilkD8YEnnGh1zJ240uJsW7AzE+2qpbOUYjacomn3AvJ6J4JhKGSZ2nh4wUIXPZrEPppaCLx5jFe8T89Rk8tQ7w==" + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.8.tgz", + "integrity": "sha512-b7uwreMYL2eZhrSCRC4ahLTeZcPZxSmYfmcQGXGkXiZSNW1X85v+SDM5KsWcpivIiUBH47Ji7NtyUdpLeF5JZQ==" }, "node_modules/domutils": { "version": "3.1.0", @@ -9996,9 +9792,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.608", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.608.tgz", - "integrity": "sha512-J2f/3iIIm3Mo0npneITZ2UPe4B1bg8fTNrFjD8715F/k1BvbviRuqYGkET1PgprrczXYTHFvotbBOmUp6KE0uA==" + "version": "1.4.623", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.623.tgz", + "integrity": "sha512-lKoz10iCYlP1WtRYdh5MvocQPWVRoI7ysp6qf18bmeBgR8abE6+I2CsfyNKztRDZvhdWc+krKT6wS7Neg8sw3A==" }, "node_modules/emittery": { "version": "0.13.1", @@ -10418,9 +10214,9 @@ } }, "node_modules/eslint-plugin-jest": { - "version": "27.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.6.0.tgz", - "integrity": "sha512-MTlusnnDMChbElsszJvrwD1dN3x6nZl//s4JD23BxB6MgR66TZlL064su24xEIS3VACfAoHV1vgyMgPw8nkdng==", + "version": "27.6.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.6.1.tgz", + "integrity": "sha512-WEYkyVXD9NlmFBKvrkmzrC+C9yZoz5pAml2hO19PlS3spJtoiwj4p2u8spd/7zx5IvRsZsCmsoImaAvBB9X93Q==", "dev": true, "dependencies": { "@typescript-eslint/utils": "^5.10.0" @@ -10796,6 +10592,16 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/eslint/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -10871,9 +10677,9 @@ } }, "node_modules/eslint/node_modules/globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -10927,6 +10733,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/eslint/node_modules/p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", @@ -11177,12 +10995,6 @@ "node": ">= 0.10.0" } }, - "node_modules/express/node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true - }, "node_modules/express/node_modules/cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", @@ -11377,9 +11189,9 @@ "dev": true }, "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", + "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", "dependencies": { "reusify": "^1.0.4" } @@ -11453,15 +11265,6 @@ "minimatch": "^5.0.1" } }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/filelist/node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -11577,15 +11380,6 @@ "node": ">=10.10.0" } }, - "node_modules/folder-hash/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/folder-hash/node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -11599,9 +11393,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", "dev": true, "funding": [ { @@ -11882,6 +11676,28 @@ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -11942,21 +11758,6 @@ "lodash": "^4.17.15" } }, - "node_modules/gzip-size": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", - "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", - "dev": true, - "dependencies": { - "duplexer": "^0.1.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -12099,9 +11900,9 @@ } }, "node_modules/html-encoder-decoder": { - "version": "1.3.9", - "resolved": "https://registry.npmjs.org/html-encoder-decoder/-/html-encoder-decoder-1.3.9.tgz", - "integrity": "sha512-dHv7bdOTEE69EIxXsM8Vslt+NW7QfEB5EGOC29BR14c7RQ9iHUgK76k3/aS23xNIwDg/xlZLWCSZ8lxol9bYlQ==", + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/html-encoder-decoder/-/html-encoder-decoder-1.3.10.tgz", + "integrity": "sha512-18SjgzQZ9U1mxb96rjcWgWMnTlEzNj2lU2wAU7OeUobdIWXTS6lOGc6419eLhMlX24sNQYDyQfgkSXWjyq/Ilg==", "dependencies": { "he": "^1.1.0", "iterate-object": "^1.3.2", @@ -12369,30 +12170,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/ignore-walk/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/ignore-walk/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/image-size": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", @@ -12671,39 +12448,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "dev": true, - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-inside-container/node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "dev": true, - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", @@ -12763,10 +12507,13 @@ } }, "node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, "engines": { "node": ">=0.10.0" } @@ -12996,6 +12743,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/jake/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/jake/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -13039,6 +12796,18 @@ "node": ">=8" } }, + "node_modules/jake/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/jake/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -13958,9 +13727,9 @@ } }, "node_modules/jest-preset-angular": { - "version": "13.1.4", - "resolved": "https://registry.npmjs.org/jest-preset-angular/-/jest-preset-angular-13.1.4.tgz", - "integrity": "sha512-XKeWa8Qt7p37SzlJ85qEXgig06SgkfrzV057X2GSMqfz/HLJmTUjMFkHJKe65ZaQumNQWCcXpxXREr6EfZ9bow==", + "version": "13.1.5", + "resolved": "https://registry.npmjs.org/jest-preset-angular/-/jest-preset-angular-13.1.5.tgz", + "integrity": "sha512-jslbUX0SXK+JjB3Kxx2cS3i2qQM1Gvf5WMbKKPOpWp93qAaUnoyLsa0lxIHLHDON1Q/D8vchCPRVY4nfyVaqkQ==", "dev": true, "dependencies": { "bs-logger": "^0.2.6", @@ -13977,10 +13746,10 @@ "esbuild": ">=0.13.8" }, "peerDependencies": { - "@angular-devkit/build-angular": ">=13.0.0 <18.0.0", - "@angular/compiler-cli": ">=13.0.0 <18.0.0", - "@angular/core": ">=13.0.0 <18.0.0", - "@angular/platform-browser-dynamic": ">=13.0.0 <18.0.0", + "@angular-devkit/build-angular": ">=15.0.0 <18.0.0", + "@angular/compiler-cli": ">=15.0.0 <18.0.0", + "@angular/core": ">=15.0.0 <18.0.0", + "@angular/platform-browser-dynamic": ">=15.0.0 <18.0.0", "jest": "^29.0.0", "typescript": ">=4.4" } @@ -15251,9 +15020,9 @@ } }, "node_modules/lint-staged/node_modules/npm-run-path": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", - "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz", + "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==", "dev": true, "dependencies": { "path-key": "^4.0.0" @@ -16102,15 +15871,18 @@ "dev": true }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -16417,13 +16189,12 @@ "dev": true }, "node_modules/needle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-3.2.0.tgz", - "integrity": "sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", "dev": true, "optional": true, "dependencies": { - "debug": "^3.2.6", "iconv-lite": "^0.6.3", "sax": "^1.2.4" }, @@ -16581,15 +16352,6 @@ "node-gyp-build-test": "build-test.js" } }, - "node_modules/node-gyp/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/node-gyp/node_modules/glob": { "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", @@ -16621,21 +16383,6 @@ "node": ">=16" } }, - "node_modules/node-gyp/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/node-gyp/node_modules/which": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", @@ -16931,6 +16678,16 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/nx/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/nx/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -17162,15 +16919,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/opener": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", - "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", - "dev": true, - "bin": { - "opener": "bin/opener-bin.js" - } - }, "node_modules/openurl": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/openurl/-/openurl-1.1.1.tgz", @@ -17891,9 +17639,9 @@ } }, "node_modules/postcss-modules-scope": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", - "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.1.0.tgz", + "integrity": "sha512-SaIbK8XW+MZbd0xHPf7kdfA/3eOt7vxJ72IRecn3EzuZVLr1r0orzf0MX/pN8m+NMDoo6X/SQd8oeKqGZd8PXg==", "dev": true, "dependencies": { "postcss-selector-parser": "^6.0.4" @@ -17921,9 +17669,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.0.13", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", - "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "version": "6.0.15", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz", + "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==", "dev": true, "dependencies": { "cssesc": "^3.0.0", @@ -18371,15 +18119,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/read-package-json/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/read-package-json/node_modules/glob": { "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", @@ -18411,21 +18150,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/read-package-json/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -18510,9 +18234,9 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regenerator-transform": { "version": "0.15.2", @@ -18529,9 +18253,9 @@ "integrity": "sha512-qEqf7uzW+iYcKNLMDFnMkghhQBnGdivT6KqVQyKsyjSWnoFyooXVnxrw9dtv3AFLnD6VBGXxtZGAQNFGFTnCqA==" }, "node_modules/regex-parser": { - "version": "2.2.11", - "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", - "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz", + "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==", "dev": true }, "node_modules/regexpu-core": { @@ -18693,6 +18417,28 @@ "node": ">= 0.8.0" } }, + "node_modules/resp-modifier/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/resp-modifier/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -18760,21 +18506,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/run-applescript": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", - "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==", - "dev": true, - "dependencies": { - "execa": "^5.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/run-async": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", @@ -18837,9 +18568,9 @@ "integrity": "sha512-LRneZZRXNgjzwG4bDQdOTSbze3fHm1EAKN/8bePxnlEZiBmkYEDggaHbuvHI9/hoqHbGfsEA7tWS9GhYHZBBsw==" }, "node_modules/sass": { - "version": "1.69.5", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.5.tgz", - "integrity": "sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==", + "version": "1.69.7", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.7.tgz", + "integrity": "sha512-rzj2soDeZ8wtE2egyLXgOOHQvaC2iosZrkF6v3EUG+tBwEvhqUCzm0VP3k9gHF9LXbSrRhT5SksoI56Iw8NPnQ==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -19341,20 +19072,6 @@ "node": "*" } }, - "node_modules/sirv": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.3.tgz", - "integrity": "sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==", - "dev": true, - "dependencies": { - "@polka/url": "^1.0.0-next.20", - "mrmime": "^1.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -19414,9 +19131,9 @@ "integrity": "sha512-TK5ZA9U5RqCwMpfoMq/l1mrH0JAR7y7KRvOBx0n2869aLxch+gT9GhN3yUfjiw+d/DiF1mKo14+hd62JyMmoBg==" }, "node_modules/socket.io": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.2.tgz", - "integrity": "sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==", + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.3.tgz", + "integrity": "sha512-SE+UIQXBQE+GPG2oszWMlsEmWtHVqw/h1VrYJGK5/MC7CH5p58N448HwIrtREcvR4jfdOJAY4ieQfxMr55qbbw==", "dev": true, "dependencies": { "accepts": "~1.3.4", @@ -19462,9 +19179,9 @@ } }, "node_modules/socket.io-client": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz", - "integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==", + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.3.tgz", + "integrity": "sha512-nU+ywttCyBitXIl9Xe0RSEfek4LneYkJxCeNnKCuhwoH4jGXO1ipIUw/VA/+Vvv2G1MTym11fzFC0SxkrcfXDw==", "dev": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", @@ -19990,12 +19707,12 @@ "dev": true }, "node_modules/synckit": { - "version": "0.8.6", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.6.tgz", - "integrity": "sha512-laHF2savN6sMeHCjLRkheIU4wo3Zg9Ln5YOjOo7sZ5dVQW8yF5pPE5SIw1dsPhq3TRp1jisKRCdPhfs/1WMqDA==", + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", + "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", "dev": true, "dependencies": { - "@pkgr/utils": "^2.4.2", + "@pkgr/core": "^0.1.0", "tslib": "^2.6.2" }, "engines": { @@ -20119,16 +19836,16 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.9", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", - "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", "dev": true, "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", + "@jridgewell/trace-mapping": "^0.3.20", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.1", - "terser": "^5.16.8" + "terser": "^5.26.0" }, "engines": { "node": ">= 10.13.0" @@ -20177,6 +19894,12 @@ "ajv": "^6.9.1" } }, + "node_modules/terser-webpack-plugin/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, "node_modules/terser-webpack-plugin/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -20239,6 +19962,24 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/terser-webpack-plugin/node_modules/terser": { + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.26.0.tgz", + "integrity": "sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -20259,6 +20000,28 @@ "node": ">=8" } }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -20282,18 +20045,6 @@ "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==" }, - "node_modules/titleize": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", - "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -20340,15 +20091,6 @@ "node": ">=0.6" } }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/tough-cookie": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", @@ -20746,15 +20488,6 @@ "node": ">= 0.8" } }, - "node_modules/untildify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", - "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", @@ -21490,75 +21223,6 @@ } } }, - "node_modules/webpack-bundle-analyzer": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.1.tgz", - "integrity": "sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==", - "dev": true, - "dependencies": { - "@discoveryjs/json-ext": "0.5.7", - "acorn": "^8.0.4", - "acorn-walk": "^8.0.0", - "commander": "^7.2.0", - "debounce": "^1.2.1", - "escape-string-regexp": "^4.0.0", - "gzip-size": "^6.0.0", - "html-escaper": "^2.0.2", - "is-plain-object": "^5.0.0", - "opener": "^1.5.2", - "picocolors": "^1.0.0", - "sirv": "^2.0.3", - "ws": "^7.3.1" - }, - "bin": { - "webpack-bundle-analyzer": "lib/bin/analyzer.js" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", - "dev": true, - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/webpack-dev-middleware": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.1.tgz", @@ -22004,9 +21668,9 @@ } }, "node_modules/ws": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", "dev": true, "engines": { "node": ">=10.0.0" diff --git a/package.json b/package.json index ef0be61f3956..0296a85238b9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "artemis", - "version": "6.7.4", + "version": "6.7.5", "description": "Interactive Learning with Individual Feedback", "private": true, "license": "MIT", @@ -13,7 +13,6 @@ "node_modules" ], "dependencies": { - "ngx-slider-v2": "17.0.0", "@angular/animations": "17.0.8", "@angular/cdk": "17.0.4", "@angular/common": "17.0.8", @@ -38,20 +37,20 @@ "@ng-bootstrap/ng-bootstrap": "16.0.0", "@ngx-translate/core": "15.0.0", "@ngx-translate/http-loader": "8.0.0", - "@sentry/angular-ivy": "7.91.0", - "@sentry/tracing": "7.91.0", - "@sentry/types": "7.91.0", + "@sentry/angular-ivy": "7.92.0", + "@sentry/tracing": "7.92.0", + "@sentry/types": "7.92.0", "@swimlane/ngx-charts": "20.5.0", - "@swimlane/ngx-graph": "8.2.3", - "ace-builds": "1.32.2", + "@swimlane/ngx-graph": "8.2.4", + "ace-builds": "1.32.3", "bootstrap": "5.3.2", "brace": "0.11.1", "compare-versions": "6.1.0", - "core-js": "3.34.0", + "core-js": "3.35.0", "crypto-js": "4.2.0", "dayjs": "1.11.10", "diff-match-patch-typescript": "1.0.8", - "dompurify": "3.0.6", + "dompurify": "3.0.8", "export-to-csv": "0.2.1", "franc-min": "6.1.0", "interactjs": "1.10.26", @@ -61,6 +60,7 @@ "lodash-es": "4.17.21", "mobile-drag-drop": "3.0.0-rc.0", "ngx-infinite-scroll": "17.0.0", + "ngx-slider-v2": "17.0.0", "ngx-webstorage": "13.0.1", "papaparse": "5.3.2", "process": "0.11.10", @@ -104,13 +104,13 @@ }, "devDependencies": { "@angular-builders/jest": "17.0.0", - "@angular-devkit/build-angular": "17.0.8", + "@angular-devkit/build-angular": "17.0.9", "@angular-eslint/builder": "17.1.1", "@angular-eslint/eslint-plugin": "17.1.1", "@angular-eslint/eslint-plugin-template": "17.1.1", "@angular-eslint/schematics": "17.1.1", "@angular-eslint/template-parser": "17.1.1", - "@angular/cli": "17.0.8", + "@angular/cli": "17.0.9", "@angular/compiler-cli": "17.0.8", "@angular/language-service": "17.0.8", "@types/crypto-js": "4.2.1", @@ -118,18 +118,18 @@ "@types/dompurify": "3.0.5", "@types/jest": "29.5.11", "@types/lodash-es": "4.17.12", - "@types/node": "20.10.5", + "@types/node": "20.10.6", "@types/papaparse": "5.3.14", "@types/showdown": "2.0.6", "@types/smoothscroll-polyfill": "0.3.3", "@types/sockjs-client": "1.5.4", "@types/uuid": "9.0.7", - "@typescript-eslint/eslint-plugin": "6.15.0", - "@typescript-eslint/parser": "6.15.0", + "@typescript-eslint/eslint-plugin": "6.17.0", + "@typescript-eslint/parser": "6.17.0", "eslint": "8.56.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-deprecation": "2.0.0", - "eslint-plugin-jest": "27.6.0", + "eslint-plugin-jest": "27.6.1", "eslint-plugin-jest-extended": "2.0.0", "eslint-plugin-prettier": "5.1.2", "folder-hash": "4.0.4", @@ -140,18 +140,17 @@ "jest-extended": "4.0.2", "jest-fail-on-console": "3.1.2", "jest-junit": "16.0.0", - "jest-preset-angular": "13.1.4", + "jest-preset-angular": "13.1.5", "lint-staged": "15.2.0", "ng-mocks": "14.12.1", "prettier": "3.1.1", - "sass": "1.69.5", + "sass": "1.69.7", "ts-jest": "29.1.1", "typescript": "5.2.2", - "weak-napi": "2.0.2", - "webpack-bundle-analyzer": "4.10.1" + "weak-napi": "2.0.2" }, "engines": { - "node": ">=18.17.0" + "node": ">=20.10.0" }, "scripts": { "postinstall": "husky install", @@ -182,13 +181,12 @@ "test:watch": "npm run prebuild && npm run test -- --watch", "webapp:build": "npm run clean-www && npm run webapp:build:dev", "webapp:build:dev": "npm run prebuild -- --develop && ng build --configuration development", - "webapp:build:prod": "npm run prebuild && ng build --configuration production --stats-json", + "webapp:build:prod": "npm run prebuild && ng build --configuration production", "webapp:dev": "npm run prebuild -- --develop && ng serve", "webapp:dev-verbose": "npm run prebuild -- --develop && ng serve --verbose", "webapp:dev-ssl": "npm run prebuild -- --develop && ng serve --ssl", "webapp:prod": "npm run clean-www && npm run webapp:build:prod", "webapp:test": "npm run test --", - "bundle-analyze": "webpack-bundle-analyzer 'build/resources/main/static/stats.json' --mode static --no-open --report 'build/resources/main/stats.html'", "prepare": "husky install", "java:war": "./gradlew bootWar -Pwar -x test -x integrationTest", "java:war:dev": "npm run java:war -- -Pdev,webapp", diff --git a/src/main/java/de/tum/in/www1/artemis/config/BeanInfoProcessor.java b/src/main/java/de/tum/in/www1/artemis/config/BeanInfoProcessor.java deleted file mode 100644 index f7f4c26182dd..000000000000 --- a/src/main/java/de/tum/in/www1/artemis/config/BeanInfoProcessor.java +++ /dev/null @@ -1,81 +0,0 @@ -package de.tum.in.www1.artemis.config; - -import java.io.IOException; -import java.io.PrintWriter; -import java.util.HashSet; -import java.util.Set; - -import javax.annotation.processing.AbstractProcessor; -import javax.annotation.processing.FilerException; -import javax.annotation.processing.Processor; -import javax.annotation.processing.RoundEnvironment; -import javax.annotation.processing.SupportedAnnotationTypes; -import javax.annotation.processing.SupportedSourceVersion; -import javax.lang.model.SourceVersion; -import javax.lang.model.element.Element; -import javax.lang.model.element.TypeElement; -import javax.tools.Diagnostic.Kind; -import javax.tools.JavaFileObject; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.google.auto.service.AutoService; - -// TODO this is not working yet -@AutoService(Processor.class) -@SupportedAnnotationTypes({ "javax.persistence.MappedSuperclass", "javax.persistence.Entity" }) -@SupportedSourceVersion(SourceVersion.RELEASE_17) -public class BeanInfoProcessor extends AbstractProcessor { - - private final Logger log = LoggerFactory.getLogger(BeanInfoProcessor.class); - - @Override - public boolean process(Set annotations, RoundEnvironment roundEnv) { - for (TypeElement annotation : annotations) { - Set annotatedElements = new HashSet<>(roundEnv.getElementsAnnotatedWith(annotation)); - annotatedElements.stream().filter(ele -> ele.getKind().isClass()).map(ele -> (TypeElement) ele).flatMap(ele -> findSupers(ele).stream()) - .forEach(ele -> generateBeanInfo(ele)); - } - return false; - } - - private Set findSupers(TypeElement element) { - Set ret = new HashSet<>(); - if (element == null) { - return ret; - } - ret.add(element); - ret.addAll(findSupers((TypeElement) processingEnv.getTypeUtils().asElement(element.getSuperclass()))); - element.getInterfaces().forEach(inter -> { - ret.addAll(findSupers((TypeElement) processingEnv.getTypeUtils().asElement(inter))); - }); - return ret; - } - - private void generateBeanInfo(TypeElement typeElement) { - String typeName = typeElement.getSimpleName().toString(); - String beanInfoFQN = typeElement.getQualifiedName().toString() + "BeanInfo"; - processingEnv.getMessager().printMessage(Kind.WARNING, "Generating " + beanInfoFQN); - String beanInfoClassName = typeName + "BeanInfo"; - String packageName = processingEnv.getElementUtils().getPackageOf(typeElement).getQualifiedName().toString(); - try { - JavaFileObject builderFile = processingEnv.getFiler().createSourceFile(beanInfoFQN); - try (PrintWriter out = new PrintWriter(builderFile.openWriter())) { - out.println("package " + packageName + ";"); - out.println("import java.beans.BeanDescriptor;\n" + "import java.beans.SimpleBeanInfo;"); - out.println("public class " + beanInfoClassName + " extends SimpleBeanInfo {"); - out.println(" public BeanDescriptor getBeanDescriptor() {"); - out.println(" return new BeanDescriptor(" + typeName + ".class, null);"); - out.println("}}"); - } - } - catch (FilerException fe) { - processingEnv.getMessager().printMessage(Kind.WARNING, "Cannot create " + beanInfoFQN + ", maybe it already exists?"); - } - catch (IOException e) { - log.error("Error while generating bean info", e); - processingEnv.getMessager().printMessage(Kind.ERROR, "IO exception"); - } - } -} diff --git a/src/main/java/de/tum/in/www1/artemis/config/SecurityConfiguration.java b/src/main/java/de/tum/in/www1/artemis/config/SecurityConfiguration.java index a1d323f04cdf..000216f6a045 100644 --- a/src/main/java/de/tum/in/www1/artemis/config/SecurityConfiguration.java +++ b/src/main/java/de/tum/in/www1/artemis/config/SecurityConfiguration.java @@ -102,12 +102,7 @@ public PasswordEncoder passwordEncoder() { @Bean RoleHierarchy roleHierarchy() { var roleHierarchy = new RoleHierarchyImpl(); - roleHierarchy.setHierarchy(""" - ROLE_ADMIN > ROLE_INSTRUCTOR - ROLE_INSTRUCTOR > ROLE_EDITOR - ROLE_EDITOR > ROLE_TA - ROLE_TA > ROLE_USER - """); + roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_INSTRUCTOR > ROLE_EDITOR > ROLE_TA > ROLE_USER > ROLE_ANONYMOUS"); return roleHierarchy; } diff --git a/src/main/java/de/tum/in/www1/artemis/config/localvcci/ArtemisGitServlet.java b/src/main/java/de/tum/in/www1/artemis/config/localvcci/ArtemisGitServlet.java index 24621314adb7..6bf965160593 100644 --- a/src/main/java/de/tum/in/www1/artemis/config/localvcci/ArtemisGitServlet.java +++ b/src/main/java/de/tum/in/www1/artemis/config/localvcci/ArtemisGitServlet.java @@ -20,8 +20,8 @@ public class ArtemisGitServlet extends GitServlet { * @param localVCServletService the service for authenticating and authorizing users and retrieving the repository from disk */ public ArtemisGitServlet(LocalVCServletService localVCServletService) { - this.setRepositoryResolver((req, name) -> { - // req – the current request, may be used to inspect session state including cookies or user authentication. + this.setRepositoryResolver((request, name) -> { + // request – the current request, may be used to inspect session state including cookies or user authentication. // name – name of the repository, as parsed out of the URL (everything after /git/). // Return the opened repository instance. @@ -32,8 +32,8 @@ public ArtemisGitServlet(LocalVCServletService localVCServletService) { this.addUploadPackFilter(new LocalVCFetchFilter(localVCServletService)); this.addReceivePackFilter(new LocalVCPushFilter(localVCServletService)); - this.setReceivePackFactory((req, db) -> { - ReceivePack receivePack = new ReceivePack(db); + this.setReceivePackFactory((request, repository) -> { + ReceivePack receivePack = new ReceivePack(repository); // Add a hook that prevents illegal actions on push (delete branch, rename branch, force push). receivePack.setPreReceiveHook(new LocalVCPrePushHook()); // Add a hook that triggers the creation of a new submission after the push went through successfully. diff --git a/src/main/java/de/tum/in/www1/artemis/config/localvcci/LocalCIConfiguration.java b/src/main/java/de/tum/in/www1/artemis/config/localvcci/LocalCIConfiguration.java index 5cac90734755..62cb30c71fe3 100644 --- a/src/main/java/de/tum/in/www1/artemis/config/localvcci/LocalCIConfiguration.java +++ b/src/main/java/de/tum/in/www1/artemis/config/localvcci/LocalCIConfiguration.java @@ -48,21 +48,13 @@ public class LocalCIConfiguration { @Value("${artemis.continuous-integration.specify-thread-pool-size:false}") boolean specifyThreadPoolSize; + @Value("${artemis.continuous-integration.build-container-prefix:local-ci-}") + private String buildContainerPrefix; + public LocalCIConfiguration(ProgrammingLanguageConfiguration programmingLanguageConfiguration) { this.programmingLanguageConfiguration = programmingLanguageConfiguration; } - /** - * Defines the thread pool size for the local CI ExecutorService based on system resources. - * - * @return The thread pool size bean. - */ - @Bean - public int calculatedThreadPoolSize() { - int availableProcessors = Runtime.getRuntime().availableProcessors(); - return Math.max(1, (availableProcessors - 2) / 2); - } - /** * Creates a HostConfig object that is used to configure the Docker container for build jobs. * The configuration is based on the default Docker flags for build jobs as specified in artemis.continuous-integration.build. @@ -101,11 +93,10 @@ public HostConfig hostConfig() { /** * Creates an executor service that manages the queue of build jobs. * - * @param calculatedThreadPoolSize The calculatedThreadPoolSize bean. * @return The executor service bean. */ @Bean - public ExecutorService localCIBuildExecutorService(int calculatedThreadPoolSize) { + public ExecutorService localCIBuildExecutorService() { int threadPoolSize; @@ -113,7 +104,8 @@ public ExecutorService localCIBuildExecutorService(int calculatedThreadPoolSize) threadPoolSize = fixedThreadPoolSize; } else { - threadPoolSize = calculatedThreadPoolSize; + int availableProcessors = Runtime.getRuntime().availableProcessors(); + threadPoolSize = Math.max(1, (availableProcessors - 2) / 2); } log.info("Using ExecutorService with thread pool size {} and a queue size limit of {}.", threadPoolSize, queueSizeLimit); @@ -153,6 +145,13 @@ public DockerClient dockerClient() { log.info("Docker client created with connection URI: {}", dockerConnectionUri); + // remove all stranded build containers + dockerClient.listContainersCmd().withShowAll(true).exec().forEach(container -> { + if (container.getNames()[0].startsWith("/" + buildContainerPrefix)) { + dockerClient.removeContainerCmd(container.getId()).withForce(true).exec(); + } + }); + return dockerClient; } diff --git a/src/main/java/de/tum/in/www1/artemis/config/lti/CustomLti13Configurer.java b/src/main/java/de/tum/in/www1/artemis/config/lti/CustomLti13Configurer.java index 15adf5c22a34..6650496c607b 100644 --- a/src/main/java/de/tum/in/www1/artemis/config/lti/CustomLti13Configurer.java +++ b/src/main/java/de/tum/in/www1/artemis/config/lti/CustomLti13Configurer.java @@ -1,11 +1,12 @@ package de.tum.in.www1.artemis.config.lti; +import java.time.Duration; + import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Profile; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; -import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository; import org.springframework.security.web.authentication.logout.LogoutFilter; import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; @@ -15,6 +16,8 @@ import uk.ac.ox.ctl.lti13.Lti13Configurer; import uk.ac.ox.ctl.lti13.security.oauth2.client.lti.authentication.OidcLaunchFlowAuthenticationProvider; import uk.ac.ox.ctl.lti13.security.oauth2.client.lti.web.OAuth2LoginAuthenticationFilter; +import uk.ac.ox.ctl.lti13.security.oauth2.client.lti.web.OptimisticAuthorizationRequestRepository; +import uk.ac.ox.ctl.lti13.security.oauth2.client.lti.web.StateAuthorizationRequestRepository; /** * Configures and registers Security Filters to handle LTI 1.3 Resource Link Launches @@ -47,12 +50,11 @@ public CustomLti13Configurer() { super.ltiPath(LTI13_BASE_PATH); super.loginInitiationPath(LOGIN_INITIATION_PATH); super.loginPath(LOGIN_PATH); - super.useState(true); } @Override public void configure(HttpSecurity http) { - AuthorizationRequestRepository authorizationRequestRepository = configureRequestRepository(); + OptimisticAuthorizationRequestRepository authorizationRequestRepository = configureRequestRepository(); OidcLaunchFlowAuthenticationProvider oidcLaunchFlowAuthenticationProvider = configureAuthenticationProvider(http); // Step 1 of the IMS SEC @@ -74,4 +76,12 @@ protected Lti13Service lti13Service(HttpSecurity http) { protected ClientRegistrationRepository clientRegistrationRepository(HttpSecurity http) { return http.getSharedObject(ApplicationContext.class).getBean(OnlineCourseConfigurationService.class); } + + @Override + protected OptimisticAuthorizationRequestRepository configureRequestRepository() { + HttpSessionOAuth2AuthorizationRequestRepository sessionRepository = new HttpSessionOAuth2AuthorizationRequestRepository(); + StateAuthorizationRequestRepository stateRepository = new StateAuthorizationRequestRepository(Duration.ofMinutes(1)); + stateRepository.setLimitIpAddress(limitIpAddresses); + return new StateBasedOptimisticAuthorizationRequestRepository(sessionRepository, stateRepository); + } } diff --git a/src/main/java/de/tum/in/www1/artemis/config/lti/StateBasedOptimisticAuthorizationRequestRepository.java b/src/main/java/de/tum/in/www1/artemis/config/lti/StateBasedOptimisticAuthorizationRequestRepository.java new file mode 100644 index 000000000000..2c7adbcb9b10 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/config/lti/StateBasedOptimisticAuthorizationRequestRepository.java @@ -0,0 +1,40 @@ +package de.tum.in.www1.artemis.config.lti; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; + +import uk.ac.ox.ctl.lti13.security.oauth2.client.lti.web.OptimisticAuthorizationRequestRepository; + +/** + * Custom repository for handling OAuth2 authorization requests in a state-based manner. + * This implementation prioritizes state parameters over session-based storage for + * managing authorization requests. + */ +public class StateBasedOptimisticAuthorizationRequestRepository extends OptimisticAuthorizationRequestRepository { + + private final AuthorizationRequestRepository stateBased; + + /** + * Constructs a StateBasedOptimisticAuthorizationRequestRepository with specified + * state-based and session-based repositories. + * + * @param sessionBased the session-based repository + * @param stateBased the state-based repository + */ + public StateBasedOptimisticAuthorizationRequestRepository(AuthorizationRequestRepository sessionBased, + AuthorizationRequestRepository stateBased) { + super(sessionBased, stateBased); + this.stateBased = stateBased; + } + + @Override + public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) { + // Overriding only the saveAuthorizationRequest to enforce state-based handling. + // This method is critical for initially capturing and storing the OAuth2 authorization request, + // ensuring it uses state-based logic rather than session-based. + stateBased.saveAuthorizationRequest(authorizationRequest, request, response); + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/config/migration/entries/BambooMigrationService.java b/src/main/java/de/tum/in/www1/artemis/config/migration/entries/BambooMigrationService.java index cfc97d6680f1..ecde63591aa8 100644 --- a/src/main/java/de/tum/in/www1/artemis/config/migration/entries/BambooMigrationService.java +++ b/src/main/java/de/tum/in/www1/artemis/config/migration/entries/BambooMigrationService.java @@ -27,7 +27,7 @@ import org.springframework.web.util.UriComponentsBuilder; import de.tum.in.www1.artemis.domain.AuxiliaryRepository; -import de.tum.in.www1.artemis.domain.VcsRepositoryUrl; +import de.tum.in.www1.artemis.domain.VcsRepositoryUri; import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; import de.tum.in.www1.artemis.exception.ContinuousIntegrationException; @@ -116,7 +116,7 @@ private static Optional getRepositoryNameById(String html, Long id) { } @Override - public void overrideBuildPlanNotification(String projectKey, String buildPlanKey, VcsRepositoryUrl vcsRepositoryUrl) { + public void overrideBuildPlanNotification(String projectKey, String buildPlanKey, VcsRepositoryUri repositoryUri) { Map notificationIds = getAllArtemisBuildPlanServerNotificationIds(buildPlanKey); log.info("Found {} notifications for build plan {}", notificationIds.size(), buildPlanKey); @@ -143,12 +143,12 @@ public void overrideBuildPlanNotification(String projectKey, String buildPlanKey } @Override - public void removeWebHook(VcsRepositoryUrl repositoryUrl) { + public void removeWebHook(VcsRepositoryUri repositoryUri) { // nothing to do } @Override - public void deleteBuildTriggers(String projectKey, String buildPlanKey, VcsRepositoryUrl repositoryUrl) { + public void deleteBuildTriggers(String projectKey, String buildPlanKey, VcsRepositoryUri repositoryUri) { if (buildPlanKey == null) { return; } @@ -179,7 +179,7 @@ public void checkPrerequisites() throws ContinuousIntegrationException { } @Override - public void overrideBuildPlanRepository(String buildPlanId, String name, String repositoryUrl, String defaultBranch) { + public void overrideBuildPlanRepository(String buildPlanId, String name, String repositoryUri, String defaultBranch) { if (this.sharedCredentialId.isEmpty()) { Optional credentialsId = getSharedCredential(); if (credentialsId.isEmpty()) { @@ -203,7 +203,7 @@ public void overrideBuildPlanRepository(String buildPlanId, String name, String deleteLinkedRepository(buildPlanId, repositoryId.get()); } log.debug("Adding repository {} for build plan {}", name, buildPlanId); - addGitRepository(buildPlanId, bambooInternalUrlService.toInternalVcsUrl(repositoryUrl), name, this.sharedCredentialId.orElseThrow(), defaultBranch); + addGitRepository(buildPlanId, bambooInternalUrlService.toInternalVcsUrl(repositoryUri), name, this.sharedCredentialId.orElseThrow(), defaultBranch); } /** diff --git a/src/main/java/de/tum/in/www1/artemis/config/migration/entries/CIVCSMigrationService.java b/src/main/java/de/tum/in/www1/artemis/config/migration/entries/CIVCSMigrationService.java index 46992d1bd9c4..b86631b8869c 100644 --- a/src/main/java/de/tum/in/www1/artemis/config/migration/entries/CIVCSMigrationService.java +++ b/src/main/java/de/tum/in/www1/artemis/config/migration/entries/CIVCSMigrationService.java @@ -6,7 +6,7 @@ import org.springframework.data.domain.Pageable; import de.tum.in.www1.artemis.domain.AuxiliaryRepository; -import de.tum.in.www1.artemis.domain.VcsRepositoryUrl; +import de.tum.in.www1.artemis.domain.VcsRepositoryUri; import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; import de.tum.in.www1.artemis.exception.ContinuousIntegrationException; @@ -23,31 +23,31 @@ public interface CIVCSMigrationService { * * @param projectKey The key of the project, e.g. 'EIST16W1', which is normally the programming exercise project key. * @param buildPlanKey The key of the build plan, which is usually the name combined with the project, e.g. 'EIST16W1-GA56HUR'. - * @param repositoryUrl the URL of the assignment repository + * @param repositoryUri the URI of the assignment repository */ - void overrideBuildPlanNotification(String projectKey, String buildPlanKey, VcsRepositoryUrl repositoryUrl); + void overrideBuildPlanNotification(String projectKey, String buildPlanKey, VcsRepositoryUri repositoryUri); /** * Deletes all build triggers for the given build plan. * * @param projectKey The key of the project, e.g. 'EIST16W1', which is normally the programming exercise project key. * @param buildPlanKey The key of the build plan, which is usually the name combined with the project, e.g. 'EIST16W1-GA56HUR'. - * @param repositoryUrl the URL of the repository that triggers the build + * @param repositoryUri the URI of the repository that triggers the build */ - void deleteBuildTriggers(String projectKey, String buildPlanKey, VcsRepositoryUrl repositoryUrl); + void deleteBuildTriggers(String projectKey, String buildPlanKey, VcsRepositoryUri repositoryUri); /** - * Overrides the existing repository URL for the given build plan. + * Overrides the existing repository URI for the given build plan. * * @param buildPlanKey The key of the build plan, which is usually the name combined with the project, e.g. 'EIST16W1-BASE'. * @param name The name of the repository - * @param repositoryUrl the URL of the repository + * @param repositoryUri the URI of the repository * @param defaultBranch the default branch of the exercise to be migrated */ - void overrideBuildPlanRepository(String buildPlanKey, String name, String repositoryUrl, String defaultBranch); + void overrideBuildPlanRepository(String buildPlanKey, String name, String repositoryUri, String defaultBranch); /** - * Overrides the existing repository URL for the given project that are checked out by the build plans. + * Overrides the existing repository URI for the given project that are checked out by the build plans. * * @param buildPlanKey The key of the build plan, which is usually the name combined with the project, e.g. 'EIST16W1-BASE'. * @param auxiliaryRepositoryList the list of auxiliary repositories @@ -76,9 +76,9 @@ Page getPageableStudentParticipations( /** * Remove the web hooks for the given repository. * - * @param repositoryUrl + * @param repositoryUri the repository uri for which the webhook should be removed */ - void removeWebHook(VcsRepositoryUrl repositoryUrl); + void removeWebHook(VcsRepositoryUri repositoryUri); /** * Checks if the build plan exists. diff --git a/src/main/java/de/tum/in/www1/artemis/config/migration/entries/GitLabJenkinsMigrationService.java b/src/main/java/de/tum/in/www1/artemis/config/migration/entries/GitLabJenkinsMigrationService.java index 4f221ef9a1dd..326579185bdb 100644 --- a/src/main/java/de/tum/in/www1/artemis/config/migration/entries/GitLabJenkinsMigrationService.java +++ b/src/main/java/de/tum/in/www1/artemis/config/migration/entries/GitLabJenkinsMigrationService.java @@ -21,13 +21,13 @@ import org.w3c.dom.Document; import de.tum.in.www1.artemis.domain.AuxiliaryRepository; -import de.tum.in.www1.artemis.domain.VcsRepositoryUrl; +import de.tum.in.www1.artemis.domain.VcsRepositoryUri; import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; import de.tum.in.www1.artemis.exception.ContinuousIntegrationException; import de.tum.in.www1.artemis.exception.JenkinsException; import de.tum.in.www1.artemis.repository.ProgrammingExerciseStudentParticipationRepository; -import de.tum.in.www1.artemis.service.UrlService; +import de.tum.in.www1.artemis.service.UriService; import de.tum.in.www1.artemis.service.connectors.gitlab.GitLabException; import de.tum.in.www1.artemis.service.connectors.jenkins.JenkinsService; import de.tum.in.www1.artemis.service.connectors.jenkins.jobs.JenkinsJobService; @@ -55,19 +55,19 @@ public class GitLabJenkinsMigrationService implements CIVCSMigrationService { private final JenkinsService jenkinsService; - private final UrlService urlService; + private final UriService uriService; private final GitLabApi gitlab; - public GitLabJenkinsMigrationService(JenkinsJobService jenkinsJobService, GitLabApi gitlab, JenkinsService jenkinsService, UrlService urlService) { + public GitLabJenkinsMigrationService(JenkinsJobService jenkinsJobService, GitLabApi gitlab, JenkinsService jenkinsService, UriService uriService) { this.jenkinsJobService = jenkinsJobService; this.gitlab = gitlab; this.jenkinsService = jenkinsService; - this.urlService = urlService; + this.uriService = uriService; } @Override - public void overrideBuildPlanNotification(String projectKey, String buildPlanKey, VcsRepositoryUrl repositoryUrl) { + public void overrideBuildPlanNotification(String projectKey, String buildPlanKey, VcsRepositoryUri repositoryUri) { try { Document currentConfig = jenkinsJobService.getJobConfig(projectKey, buildPlanKey); Document newConfig = replaceNotificationUrlInJobConfig(currentConfig); @@ -80,8 +80,8 @@ public void overrideBuildPlanNotification(String projectKey, String buildPlanKey } @Override - public void deleteBuildTriggers(String projectKey, String buildPlanKey, VcsRepositoryUrl repositoryUrl) { - removeWebHook(repositoryUrl); + public void deleteBuildTriggers(String projectKey, String buildPlanKey, VcsRepositoryUri repositoryUri) { + removeWebHook(repositoryUri); if (projectKey != null && buildPlanKey != null) { try { @@ -97,7 +97,7 @@ public void deleteBuildTriggers(String projectKey, String buildPlanKey, VcsRepos } @Override - public void overrideBuildPlanRepository(String buildPlanKey, String name, String repositoryUrl, String defaultBranch) { + public void overrideBuildPlanRepository(String buildPlanKey, String name, String repositoryUri, String defaultBranch) { // not needed for Jenkins } @@ -109,7 +109,7 @@ public void overrideRepositoriesToCheckout(String buildPlanKey, List getPageableStudentParticipations( ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository, Pageable pageable) { - return programmingExerciseStudentParticipationRepository.findAllWithRepositoryUrlOrBuildPlanId(pageable); + return programmingExerciseStudentParticipationRepository.findAllWithRepositoryUriOrBuildPlanId(pageable); } @Override @@ -123,8 +123,8 @@ public boolean buildPlanExists(String projectKey, String buildPlanKey) { } @Override - public void removeWebHook(VcsRepositoryUrl repositoryUrl) { - final var repositoryPath = urlService.getRepositoryPathFromRepositoryUrl(repositoryUrl); + public void removeWebHook(VcsRepositoryUri repositoryUri) { + final var repositoryPath = uriService.getRepositoryPathFromRepositoryUri(repositoryUri); try { List hooks = gitlab.getProjectApi().getHooks(repositoryPath); @@ -153,7 +153,7 @@ else if (internalJenkinsUrl != null) { } } catch (GitLabApiException e) { - throw new GitLabException("Unable to remove webhook for " + repositoryUrl, e); + throw new GitLabException("Unable to remove webhook for " + repositoryUri, e); } } diff --git a/src/main/java/de/tum/in/www1/artemis/config/migration/entries/MigrationEntry20230808_203400.java b/src/main/java/de/tum/in/www1/artemis/config/migration/entries/MigrationEntry20230808_203400.java index 56d71f3e7560..23a3e33f5bf9 100644 --- a/src/main/java/de/tum/in/www1/artemis/config/migration/entries/MigrationEntry20230808_203400.java +++ b/src/main/java/de/tum/in/www1/artemis/config/migration/entries/MigrationEntry20230808_203400.java @@ -26,11 +26,11 @@ import de.tum.in.www1.artemis.config.migration.MigrationEntry; import de.tum.in.www1.artemis.domain.AuxiliaryRepository; import de.tum.in.www1.artemis.domain.ProgrammingExercise; -import de.tum.in.www1.artemis.domain.VcsRepositoryUrl; +import de.tum.in.www1.artemis.domain.VcsRepositoryUri; import de.tum.in.www1.artemis.domain.participation.*; import de.tum.in.www1.artemis.exception.ContinuousIntegrationException; import de.tum.in.www1.artemis.repository.*; -import de.tum.in.www1.artemis.service.UrlService; +import de.tum.in.www1.artemis.service.UriService; import de.tum.in.www1.artemis.service.connectors.vcs.VersionControlService; @Component @@ -62,7 +62,7 @@ public class MigrationEntry20230808_203400 extends MigrationEntry { private final Environment environment; - private final UrlService urlService = new UrlService(); + private final UriService uriService = new UriService(); private final CopyOnWriteArrayList errorList = new CopyOnWriteArrayList<>(); @@ -346,18 +346,18 @@ private List getAuxiliaryRepositories(Long exerciseId) { */ private void migrateTestRepository(AbstractBaseProgrammingExerciseParticipation participation) { var exercise = participation.getProgrammingExercise(); - if (exercise.getTestRepositoryUrl() == null) { + if (exercise.getTestRepositoryUri() == null) { /* - * when the test repository url is null, we don't have to migrate the test build plan, saving multiple API calls + * when the test repository uri is null, we don't have to migrate the test build plan, saving multiple API calls * this is also only needed for Jenkins, as Bamboo does not have a separate test build plan */ return; } try { - ciMigrationService.orElseThrow().removeWebHook(exercise.getVcsTestRepositoryUrl()); + ciMigrationService.orElseThrow().removeWebHook(exercise.getVcsTestRepositoryUri()); } catch (Exception e) { - log.warn("Failed to delete build triggers in test repository for exercise {} with test repository {}", exercise.getId(), exercise.getVcsTestRepositoryUrl(), e); + log.warn("Failed to delete build triggers in test repository for exercise {} with test repository {}", exercise.getId(), exercise.getVcsTestRepositoryUri(), e); errorList.add(participation); } } @@ -369,19 +369,19 @@ private void migrateTestRepository(AbstractBaseProgrammingExerciseParticipation * @param auxiliaryRepositories The auxiliary repositories to migrate */ private void migrateStudentBuildPlan(ProgrammingExerciseStudentParticipation participation, List auxiliaryRepositories) { - VcsRepositoryUrl repositoryUrl; + VcsRepositoryUri repositoryUri; ProgrammingExercise exercise = participation.getProgrammingExercise(); try { - repositoryUrl = new VcsRepositoryUrl(urlService.getPlainUrlFromRepositoryUrl(participation.getVcsRepositoryUrl())); + repositoryUri = new VcsRepositoryUri(uriService.getPlainUriFromRepositoryUri(participation.getVcsRepositoryUri())); } catch (URISyntaxException e) { log.warn("Failed to convert git url {} for studentParticipationId {} exerciseId {} with buildPlanId {}, will abort migration for this Participation", - participation.getVcsRepositoryUrl(), participation.getId(), exercise.getId(), participation.getBuildPlanId(), e); + participation.getVcsRepositoryUri(), participation.getId(), exercise.getId(), participation.getBuildPlanId(), e); errorList.add(participation); return; } try { - ciMigrationService.orElseThrow().overrideBuildPlanNotification(exercise.getProjectKey(), participation.getBuildPlanId(), repositoryUrl); + ciMigrationService.orElseThrow().overrideBuildPlanNotification(exercise.getProjectKey(), participation.getBuildPlanId(), repositoryUri); } catch (Exception e) { log.warn("Failed to migrate build plan notifications for studentParticipationId {} with buildPlanId {} of exerciseId {} ", participation.getId(), @@ -389,7 +389,7 @@ private void migrateStudentBuildPlan(ProgrammingExerciseStudentParticipation par errorList.add(participation); } try { - ciMigrationService.orElseThrow().deleteBuildTriggers(exercise.getProjectKey(), participation.getBuildPlanId(), repositoryUrl); + ciMigrationService.orElseThrow().deleteBuildTriggers(exercise.getProjectKey(), participation.getBuildPlanId(), repositoryUri); } catch (Exception e) { log.warn("Failed to delete build triggers for studentParticipationId {} with buildPlanId {} of exerciseId {} ", participation.getId(), participation.getBuildPlanId(), @@ -397,7 +397,7 @@ private void migrateStudentBuildPlan(ProgrammingExerciseStudentParticipation par errorList.add(participation); } try { - ciMigrationService.orElseThrow().overrideBuildPlanRepository(participation.getBuildPlanId(), ASSIGNMENT_REPO_NAME, String.valueOf(repositoryUrl), + ciMigrationService.orElseThrow().overrideBuildPlanRepository(participation.getBuildPlanId(), ASSIGNMENT_REPO_NAME, String.valueOf(repositoryUri), participation.getBranch()); } catch (Exception e) { @@ -406,7 +406,7 @@ private void migrateStudentBuildPlan(ProgrammingExerciseStudentParticipation par errorList.add(participation); } try { - ciMigrationService.orElseThrow().overrideBuildPlanRepository(participation.getBuildPlanId(), TEST_REPO_NAME, exercise.getTestRepositoryUrl(), + ciMigrationService.orElseThrow().overrideBuildPlanRepository(participation.getBuildPlanId(), TEST_REPO_NAME, exercise.getTestRepositoryUri(), participation.getBranch()); } catch (Exception e) { @@ -416,10 +416,10 @@ private void migrateStudentBuildPlan(ProgrammingExerciseStudentParticipation par } // NOTE: this handles an edge case with HASKELL exercises, where the solution repository is checked out in the student build plan try { - // we dont have the solution repository url in the student participation, so we have to get it from the repository service - var solutionRepositoryUrl = solutionProgrammingExerciseParticipationRepository.findByProgrammingExerciseId(exercise.getId()).get().getVcsRepositoryUrl(); + // we dont have the solution repository uri in the student participation, so we have to get it from the repository service + var solutionRepositoryUri = solutionProgrammingExerciseParticipationRepository.findByProgrammingExerciseId(exercise.getId()).get().getVcsRepositoryUri(); ciMigrationService.orElseThrow().overrideBuildPlanRepository(participation.getBuildPlanId(), SOLUTION_REPO_NAME, - urlService.getPlainUrlFromRepositoryUrl(solutionRepositoryUrl), participation.getBranch()); + uriService.getPlainUriFromRepositoryUri(solutionRepositoryUri), participation.getBranch()); } catch (Exception e) { log.warn("Failed to replace solution repository in student build plan for studentParticipationId {} with buildPlanId {} of exerciseId {} ", participation.getId(), @@ -428,7 +428,7 @@ private void migrateStudentBuildPlan(ProgrammingExerciseStudentParticipation par } for (var auxiliary : auxiliaryRepositories) { try { - ciMigrationService.orElseThrow().overrideBuildPlanRepository(participation.getBuildPlanId(), auxiliary.getName(), auxiliary.getRepositoryUrl(), + ciMigrationService.orElseThrow().overrideBuildPlanRepository(participation.getBuildPlanId(), auxiliary.getName(), auxiliary.getRepositoryUri(), participation.getBranch()); } catch (Exception e) { @@ -456,21 +456,21 @@ private void migrateStudentBuildPlan(ProgrammingExerciseStudentParticipation par private void migrateSolutionBuildPlan(AbstractBaseProgrammingExerciseParticipation participation, List auxiliaryRepositories) { ProgrammingExercise exercise = participation.getProgrammingExercise(); try { - ciMigrationService.orElseThrow().overrideBuildPlanNotification(exercise.getProjectKey(), exercise.getSolutionBuildPlanId(), exercise.getVcsSolutionRepositoryUrl()); + ciMigrationService.orElseThrow().overrideBuildPlanNotification(exercise.getProjectKey(), exercise.getSolutionBuildPlanId(), exercise.getVcsSolutionRepositoryUri()); } catch (Exception e) { log.warn("Failed to migrate solution build plan for exercise id {} with buildPlanId {}", exercise.getId(), exercise.getSolutionBuildPlanId(), e); errorList.add(participation); } try { - ciMigrationService.orElseThrow().deleteBuildTriggers(exercise.getProjectKey(), exercise.getSolutionBuildPlanId(), exercise.getVcsSolutionRepositoryUrl()); + ciMigrationService.orElseThrow().deleteBuildTriggers(exercise.getProjectKey(), exercise.getSolutionBuildPlanId(), exercise.getVcsSolutionRepositoryUri()); } catch (Exception e) { log.warn("Failed to delete solution build triggers for exercise {} with buildPlanId {}", exercise.getId(), exercise.getSolutionBuildPlanId(), e); errorList.add(participation); } try { - ciMigrationService.orElseThrow().overrideBuildPlanRepository(exercise.getSolutionBuildPlanId(), ASSIGNMENT_REPO_NAME, exercise.getSolutionRepositoryUrl(), + ciMigrationService.orElseThrow().overrideBuildPlanRepository(exercise.getSolutionBuildPlanId(), ASSIGNMENT_REPO_NAME, exercise.getSolutionRepositoryUri(), exercise.getBranch()); } catch (Exception e) { @@ -478,14 +478,14 @@ private void migrateSolutionBuildPlan(AbstractBaseProgrammingExerciseParticipati errorList.add(participation); } try { - ciMigrationService.orElseThrow().overrideBuildPlanRepository(exercise.getSolutionBuildPlanId(), TEST_REPO_NAME, exercise.getTestRepositoryUrl(), exercise.getBranch()); + ciMigrationService.orElseThrow().overrideBuildPlanRepository(exercise.getSolutionBuildPlanId(), TEST_REPO_NAME, exercise.getTestRepositoryUri(), exercise.getBranch()); } catch (Exception e) { log.warn("Failed to replace tests repository in solution build plan for exercise {} with buildPlanId {}", exercise.getId(), exercise.getSolutionBuildPlanId(), e); errorList.add(participation); } try { - ciMigrationService.orElseThrow().overrideBuildPlanRepository(participation.getBuildPlanId(), SOLUTION_REPO_NAME, exercise.getSolutionRepositoryUrl(), + ciMigrationService.orElseThrow().overrideBuildPlanRepository(participation.getBuildPlanId(), SOLUTION_REPO_NAME, exercise.getSolutionRepositoryUri(), exercise.getBranch()); } catch (Exception e) { @@ -495,7 +495,7 @@ private void migrateSolutionBuildPlan(AbstractBaseProgrammingExerciseParticipati } for (var auxiliary : auxiliaryRepositories) { try { - ciMigrationService.orElseThrow().overrideBuildPlanRepository(exercise.getSolutionBuildPlanId(), auxiliary.getName(), auxiliary.getRepositoryUrl(), + ciMigrationService.orElseThrow().overrideBuildPlanRepository(exercise.getSolutionBuildPlanId(), auxiliary.getName(), auxiliary.getRepositoryUri(), exercise.getBranch()); } catch (Exception e) { @@ -522,21 +522,21 @@ private void migrateSolutionBuildPlan(AbstractBaseProgrammingExerciseParticipati private void migrateTemplateBuildPlan(AbstractBaseProgrammingExerciseParticipation participation, List auxiliaryRepositories) { ProgrammingExercise exercise = participation.getProgrammingExercise(); try { - ciMigrationService.orElseThrow().overrideBuildPlanNotification(exercise.getProjectKey(), exercise.getTemplateBuildPlanId(), exercise.getVcsTemplateRepositoryUrl()); + ciMigrationService.orElseThrow().overrideBuildPlanNotification(exercise.getProjectKey(), exercise.getTemplateBuildPlanId(), exercise.getVcsTemplateRepositoryUri()); } catch (Exception e) { log.warn("Failed to migrate template build plan for exercise {} with buildPlanId {}", exercise.getId(), exercise.getTemplateBuildPlanId(), e); errorList.add(participation); } try { - ciMigrationService.orElseThrow().deleteBuildTriggers(exercise.getProjectKey(), exercise.getTemplateBuildPlanId(), exercise.getVcsTemplateRepositoryUrl()); + ciMigrationService.orElseThrow().deleteBuildTriggers(exercise.getProjectKey(), exercise.getTemplateBuildPlanId(), exercise.getVcsTemplateRepositoryUri()); } catch (Exception e) { log.warn("Failed to delete template build triggers for exercise {} with buildPlanId {}", exercise.getId(), exercise.getTemplateBuildPlanId(), e); errorList.add(participation); } try { - ciMigrationService.orElseThrow().overrideBuildPlanRepository(exercise.getTemplateBuildPlanId(), ASSIGNMENT_REPO_NAME, exercise.getTemplateRepositoryUrl(), + ciMigrationService.orElseThrow().overrideBuildPlanRepository(exercise.getTemplateBuildPlanId(), ASSIGNMENT_REPO_NAME, exercise.getTemplateRepositoryUri(), exercise.getBranch()); } catch (Exception e) { @@ -544,7 +544,7 @@ private void migrateTemplateBuildPlan(AbstractBaseProgrammingExerciseParticipati errorList.add(participation); } try { - ciMigrationService.orElseThrow().overrideBuildPlanRepository(exercise.getTemplateBuildPlanId(), TEST_REPO_NAME, exercise.getTestRepositoryUrl(), exercise.getBranch()); + ciMigrationService.orElseThrow().overrideBuildPlanRepository(exercise.getTemplateBuildPlanId(), TEST_REPO_NAME, exercise.getTestRepositoryUri(), exercise.getBranch()); } catch (Exception e) { log.warn("Failed to replace tests repository in template build plan for exercise {} with buildPlanId {}", exercise.getId(), exercise.getTemplateBuildPlanId(), e); @@ -552,10 +552,10 @@ private void migrateTemplateBuildPlan(AbstractBaseProgrammingExerciseParticipati } // NOTE: this handles an edge case with HASKELL exercises, where the solution repository is checked out in the student build plan try { - // we do not have the solution repository url in the template participation, so we have to get it from the repository service - var solutionRepositoryUrl = solutionProgrammingExerciseParticipationRepository.findByProgrammingExerciseId(exercise.getId()).get().getVcsRepositoryUrl(); + // we do not have the solution repository uri in the template participation, so we have to get it from the repository service + var solutionRepositoryUri = solutionProgrammingExerciseParticipationRepository.findByProgrammingExerciseId(exercise.getId()).get().getVcsRepositoryUri(); ciMigrationService.orElseThrow().overrideBuildPlanRepository(participation.getBuildPlanId(), SOLUTION_REPO_NAME, - urlService.getPlainUrlFromRepositoryUrl(solutionRepositoryUrl), exercise.getBranch()); + uriService.getPlainUriFromRepositoryUri(solutionRepositoryUri), exercise.getBranch()); } catch (Exception e) { log.warn("Failed to replace solution repository in template build plan for studentParticipationId {} with buildPlanId {} of exerciseId {} ", participation.getId(), @@ -564,7 +564,7 @@ private void migrateTemplateBuildPlan(AbstractBaseProgrammingExerciseParticipati } for (var auxiliary : auxiliaryRepositories) { try { - ciMigrationService.orElseThrow().overrideBuildPlanRepository(exercise.getTemplateBuildPlanId(), auxiliary.getName(), auxiliary.getRepositoryUrl(), + ciMigrationService.orElseThrow().overrideBuildPlanRepository(exercise.getTemplateBuildPlanId(), auxiliary.getName(), auxiliary.getRepositoryUri(), exercise.getBranch()); } catch (Exception e) { diff --git a/src/main/java/de/tum/in/www1/artemis/config/migration/entries/MigrationEntry20230920_181600.java b/src/main/java/de/tum/in/www1/artemis/config/migration/entries/MigrationEntry20230920_181600.java index 33820bd5f583..f8b7b4f02e21 100644 --- a/src/main/java/de/tum/in/www1/artemis/config/migration/entries/MigrationEntry20230920_181600.java +++ b/src/main/java/de/tum/in/www1/artemis/config/migration/entries/MigrationEntry20230920_181600.java @@ -21,10 +21,10 @@ import de.tum.in.www1.artemis.config.migration.MigrationEntry; import de.tum.in.www1.artemis.domain.ProgrammingExercise; -import de.tum.in.www1.artemis.domain.VcsRepositoryUrl; +import de.tum.in.www1.artemis.domain.VcsRepositoryUri; import de.tum.in.www1.artemis.domain.participation.*; import de.tum.in.www1.artemis.repository.*; -import de.tum.in.www1.artemis.service.UrlService; +import de.tum.in.www1.artemis.service.UriService; import de.tum.in.www1.artemis.service.connectors.vcs.VersionControlService; @Component @@ -52,7 +52,7 @@ public class MigrationEntry20230920_181600 extends MigrationEntry { private final Environment environment; - private final UrlService urlService = new UrlService(); + private final UriService uriService = new UriService(); private final CopyOnWriteArrayList errorList = new CopyOnWriteArrayList<>(); @@ -264,19 +264,19 @@ private void migrateStudents(List parti * @param participation The participation to migrate */ private void migrateStudentBuildPlan(ProgrammingExerciseStudentParticipation participation) { - VcsRepositoryUrl repositoryUrl; + VcsRepositoryUri repositoryUri; ProgrammingExercise exercise = participation.getProgrammingExercise(); try { - repositoryUrl = new VcsRepositoryUrl(urlService.getPlainUrlFromRepositoryUrl(participation.getVcsRepositoryUrl())); + repositoryUri = new VcsRepositoryUri(uriService.getPlainUriFromRepositoryUri(participation.getVcsRepositoryUri())); } catch (URISyntaxException e) { log.warn("Failed to convert git url {} for studentParticipationId {} exerciseId {} with buildPlanId {}, will abort migration for this Participation", - participation.getVcsRepositoryUrl(), participation.getId(), exercise.getId(), participation.getBuildPlanId(), e); + participation.getVcsRepositoryUri(), participation.getId(), exercise.getId(), participation.getBuildPlanId(), e); errorList.add(participation); return; } try { - ciMigrationService.orElseThrow().overrideBuildPlanNotification(exercise.getProjectKey(), participation.getBuildPlanId(), repositoryUrl); + ciMigrationService.orElseThrow().overrideBuildPlanNotification(exercise.getProjectKey(), participation.getBuildPlanId(), repositoryUri); } catch (Exception e) { log.warn("Failed to migrate build plan notifications for studentParticipationId {} with buildPlanId {} of exerciseId {} ", participation.getId(), @@ -293,7 +293,7 @@ private void migrateStudentBuildPlan(ProgrammingExerciseStudentParticipation par private void migrateSolutionBuildPlan(AbstractBaseProgrammingExerciseParticipation participation) { ProgrammingExercise exercise = participation.getProgrammingExercise(); try { - ciMigrationService.orElseThrow().overrideBuildPlanNotification(exercise.getProjectKey(), exercise.getSolutionBuildPlanId(), exercise.getVcsSolutionRepositoryUrl()); + ciMigrationService.orElseThrow().overrideBuildPlanNotification(exercise.getProjectKey(), exercise.getSolutionBuildPlanId(), exercise.getVcsSolutionRepositoryUri()); } catch (Exception e) { log.warn("Failed to migrate solution build plan for exercise id {} with buildPlanId {}", exercise.getId(), exercise.getSolutionBuildPlanId(), e); @@ -309,7 +309,7 @@ private void migrateSolutionBuildPlan(AbstractBaseProgrammingExerciseParticipati private void migrateTemplateBuildPlan(AbstractBaseProgrammingExerciseParticipation participation) { ProgrammingExercise exercise = participation.getProgrammingExercise(); try { - ciMigrationService.orElseThrow().overrideBuildPlanNotification(exercise.getProjectKey(), exercise.getTemplateBuildPlanId(), exercise.getVcsTemplateRepositoryUrl()); + ciMigrationService.orElseThrow().overrideBuildPlanNotification(exercise.getProjectKey(), exercise.getTemplateBuildPlanId(), exercise.getVcsTemplateRepositoryUri()); } catch (Exception e) { log.warn("Failed to migrate template build plan for exercise {} with buildPlanId {}", exercise.getId(), exercise.getTemplateBuildPlanId(), e); diff --git a/src/main/java/de/tum/in/www1/artemis/config/websocket/WebsocketConfiguration.java b/src/main/java/de/tum/in/www1/artemis/config/websocket/WebsocketConfiguration.java index 91c45691f4dc..ef253d5cdc73 100644 --- a/src/main/java/de/tum/in/www1/artemis/config/websocket/WebsocketConfiguration.java +++ b/src/main/java/de/tum/in/www1/artemis/config/websocket/WebsocketConfiguration.java @@ -2,6 +2,8 @@ import static de.tum.in.www1.artemis.web.websocket.ResultWebsocketService.getExerciseIdFromNonPersonalExerciseResultDestination; import static de.tum.in.www1.artemis.web.websocket.ResultWebsocketService.isNonPersonalExerciseResultDestination; +import static de.tum.in.www1.artemis.web.websocket.localci.LocalCIBuildQueueWebsocketService.isBuildQueueAdminDestination; +import static de.tum.in.www1.artemis.web.websocket.localci.LocalCIBuildQueueWebsocketService.isBuildQueueCourseDestination; import static de.tum.in.www1.artemis.web.websocket.team.ParticipationTeamWebsocketService.*; import java.net.InetSocketAddress; @@ -46,13 +48,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.Iterators; +import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.Exercise; import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; -import de.tum.in.www1.artemis.repository.ExamRepository; -import de.tum.in.www1.artemis.repository.ExerciseRepository; -import de.tum.in.www1.artemis.repository.StudentParticipationRepository; -import de.tum.in.www1.artemis.repository.UserRepository; +import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.security.Role; import de.tum.in.www1.artemis.security.jwt.JWTFilter; import de.tum.in.www1.artemis.security.jwt.TokenProvider; @@ -96,9 +96,11 @@ public class WebsocketConfiguration extends DelegatingWebSocketMessageBrokerConf @Value("${spring.websocket.broker.password}") private String brokerPassword; + private final CourseRepository courseRepository; + public WebsocketConfiguration(MappingJackson2HttpMessageConverter springMvcJacksonConverter, TaskScheduler messageBrokerTaskScheduler, TokenProvider tokenProvider, StudentParticipationRepository studentParticipationRepository, AuthorizationCheckService authorizationCheckService, ExerciseRepository exerciseRepository, - UserRepository userRepository, ExamRepository examRepository) { + UserRepository userRepository, ExamRepository examRepository, CourseRepository courseRepository) { this.objectMapper = springMvcJacksonConverter.getObjectMapper(); this.messageBrokerTaskScheduler = messageBrokerTaskScheduler; this.tokenProvider = tokenProvider; @@ -107,6 +109,7 @@ public WebsocketConfiguration(MappingJackson2HttpMessageConverter springMvcJacks this.exerciseRepository = exerciseRepository; this.userRepository = userRepository; this.examRepository = examRepository; + this.courseRepository = courseRepository; } @Override @@ -262,12 +265,34 @@ public Message preSend(@NotNull Message message, @NotNull MessageChannel c /** * Returns whether the subscription of the given principal to the given destination is permitted + * Database calls should be avoided as much as possible in this method. + * Only for very specific topics, database calls are allowed. * * @param principal User principal of the user who wants to subscribe * @param destination Destination topic to which the user wants to subscribe * @return flag whether subscription is allowed */ private boolean allowSubscription(Principal principal, String destination) { + /* + * IMPORTANT: Avoid database calls in this method as much as possible (e.g. checking if the user + * is an instructor in a course) + * This method is called for every subscription request, so it should be as fast as possible. + * If you need to do a database call, make sure to first check if the destination is valid for your specific + * use case. + */ + + if (isBuildQueueAdminDestination(destination)) { + var user = userRepository.getUserWithAuthorities(principal.getName()); + return authorizationCheckService.isAdmin(user); + } + + Optional courseId = isBuildQueueCourseDestination(destination); + if (courseId.isPresent()) { + Course course = courseRepository.findByIdElseThrow(courseId.get()); + var user = userRepository.getUserWithGroupsAndAuthorities(principal.getName()); + return authorizationCheckService.isAtLeastInstructorInCourse(course, user); + } + if (isParticipationTeamDestination(destination)) { Long participationId = getParticipationIdFromDestination(destination); return isParticipationOwnedByUser(principal, participationId); @@ -288,7 +313,7 @@ private boolean allowSubscription(Principal principal, String destination) { var examId = getExamIdFromExamRootDestination(destination); if (examId.isPresent()) { var exam = examRepository.findByIdElseThrow(examId.get()); - User user = userRepository.getUserWithGroupsAndAuthorities(principal.getName()); + var user = userRepository.getUserWithGroupsAndAuthorities(principal.getName()); return authorizationCheckService.isAtLeastInstructorInCourse(exam.getCourse(), user); } return true; diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Authority.java b/src/main/java/de/tum/in/www1/artemis/domain/Authority.java index 941fb4759b3f..86b78588bf58 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Authority.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Authority.java @@ -27,15 +27,15 @@ public class Authority implements Serializable { @Serial private static final long serialVersionUID = 1L; - public static Authority ADMIN_AUTHORITY = new Authority(Role.ADMIN.getAuthority()); + public static final Authority ADMIN_AUTHORITY = new Authority(Role.ADMIN.getAuthority()); - public static Authority INSTRUCTOR_AUTHORITY = new Authority(Role.INSTRUCTOR.getAuthority()); + public static final Authority INSTRUCTOR_AUTHORITY = new Authority(Role.INSTRUCTOR.getAuthority()); - public static Authority EDITOR_AUTHORITY = new Authority(Role.EDITOR.getAuthority()); + public static final Authority EDITOR_AUTHORITY = new Authority(Role.EDITOR.getAuthority()); - public static Authority TA_AUTHORITY = new Authority(Role.TEACHING_ASSISTANT.getAuthority()); + public static final Authority TA_AUTHORITY = new Authority(Role.TEACHING_ASSISTANT.getAuthority()); - public static Authority USER_AUTHORITY = new Authority(Role.STUDENT.getAuthority()); + public static final Authority USER_AUTHORITY = new Authority(Role.STUDENT.getAuthority()); @NotNull @Size(max = 50) diff --git a/src/main/java/de/tum/in/www1/artemis/domain/AuxiliaryRepository.java b/src/main/java/de/tum/in/www1/artemis/domain/AuxiliaryRepository.java index 09176c47a323..474319f77af4 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/AuxiliaryRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/AuxiliaryRepository.java @@ -30,7 +30,7 @@ public class AuxiliaryRepository extends DomainObject { public static final int MAX_CHECKOUT_DIRECTORY_LENGTH = 100; @JsonIgnore - public static final int MAX_REPOSITORY_URL_LENGTH = 500; + public static final int MAX_REPOSITORY_URI_LENGTH = 500; @JsonIgnore public static final int MAX_DESCRIPTION_LENGTH = 500; @@ -49,9 +49,9 @@ public class AuxiliaryRepository extends DomainObject { @Column(name = "name") private String name; - @Size(max = MAX_REPOSITORY_URL_LENGTH) + @Size(max = MAX_REPOSITORY_URI_LENGTH) @Column(name = "repository_url") - private String repositoryUrl; + private String repositoryUri; /** * One programming exercise must not have multiple repositories @@ -69,12 +69,12 @@ public class AuxiliaryRepository extends DomainObject { @JsonIgnore private ProgrammingExercise exercise; - public String getRepositoryUrl() { - return repositoryUrl; + public String getRepositoryUri() { + return repositoryUri; } - public void setRepositoryUrl(String repositoryUrl) { - this.repositoryUrl = repositoryUrl; + public void setRepositoryUri(String repositoryUri) { + this.repositoryUri = repositoryUri; } public String getCheckoutDirectory() { @@ -122,21 +122,21 @@ public String getRepositoryName() { /** * Returns whether this auxiliary repository should be included in the exercise's build plan or not. - * The repository is included when the repository url and the checkout directory have appropriate + * The repository is included when the repository uri and the checkout directory have appropriate * values. * * @return true or false whether this repository should be included in the exercise build plan or not */ @JsonIgnore public boolean shouldBeIncludedInBuildPlan() { - return getCheckoutDirectory() != null && !getCheckoutDirectory().isBlank() && getRepositoryUrl() != null && !getRepositoryUrl().isBlank(); + return getCheckoutDirectory() != null && !getCheckoutDirectory().isBlank() && getRepositoryUri() != null && !getRepositoryUri().isBlank(); } /** - * Returns a copy of this auxiliary repository object without including the repository url. + * Returns a copy of this auxiliary repository object without including the repository uri. * This is used when importing auxiliary repositories from an old exercise to a new exercise. * - * @return A clone of this auxiliary repository object except the repository url + * @return A clone of this auxiliary repository object except the repository uri */ @JsonIgnore public AuxiliaryRepository cloneObjectForNewExercise() { @@ -148,35 +148,35 @@ public AuxiliaryRepository cloneObjectForNewExercise() { } /** - * Gets a URL of the repositoryUrl if there is one + * Gets a URL of the repositoryUri if there is one * - * @return a URL object of the repositoryUrl or null if there is no repositoryUrl + * @return a URL object of the repositoryUri or null if there is no repositoryUri */ @JsonIgnore - public VcsRepositoryUrl getVcsRepositoryUrl() { - String repositoryUrl = getRepositoryUrl(); - if (repositoryUrl == null || repositoryUrl.isEmpty()) { + public VcsRepositoryUri getVcsRepositoryUri() { + String repositoryUri = getRepositoryUri(); + if (repositoryUri == null || repositoryUri.isEmpty()) { return null; } try { - return new VcsRepositoryUrl(repositoryUrl); + return new VcsRepositoryUri(repositoryUri); } catch (URISyntaxException e) { - log.error("Malformed URI {} could not be used to instantiate VcsRepositoryUrl.", getRepositoryUrl(), e); + log.error("Malformed URI {} could not be used to instantiate VcsRepositoryUri.", getRepositoryUri(), e); } return null; } @Override public String toString() { - return "AuxiliaryRepository{id=%d, name='%s', checkoutDirectory='%s', repositoryUrl='%s', description='%s', exercise=%s}".formatted(getId(), getName(), - getCheckoutDirectory(), getRepositoryUrl(), getDescription(), exercise == null ? "null" : exercise.getId()); + return "AuxiliaryRepository{id=%d, name='%s', checkoutDirectory='%s', repositoryUri='%s', description='%s', exercise=%s}".formatted(getId(), getName(), + getCheckoutDirectory(), getRepositoryUri(), getDescription(), exercise == null ? "null" : exercise.getId()); } /** - * Used in Bamboo Service to map the name of an auxiliary repository to its repository url. + * Used in Bamboo Service to map the name of an auxiliary repository to its repository uri. */ - public record AuxRepoNameWithUrl(String name, VcsRepositoryUrl repositoryUrl) { + public record AuxRepoNameWithUri(String name, VcsRepositoryUri repositoryUri) { } public boolean containsEqualStringValues(AuxiliaryRepository other) { diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Commit.java b/src/main/java/de/tum/in/www1/artemis/domain/Commit.java index c9a4204a6e84..03924f97e6be 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Commit.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Commit.java @@ -1,54 +1,7 @@ package de.tum.in.www1.artemis.domain; -public class Commit { +import javax.annotation.Nullable; - private String commitHash; +public record Commit(@Nullable String commitHash, @Nullable String authorName, @Nullable String message, @Nullable String authorEmail, @Nullable String branch) { - private String authorName; - - private String message; - - private String authorEmail; - - private String branch; - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - - public String getCommitHash() { - return commitHash; - } - - public void setCommitHash(String commitHash) { - this.commitHash = commitHash; - } - - public String getAuthorName() { - return authorName; - } - - public void setAuthorName(String authorName) { - this.authorName = authorName; - } - - public String getAuthorEmail() { - return authorEmail; - } - - public void setAuthorEmail(String authorEmail) { - this.authorEmail = authorEmail; - } - - public String getBranch() { - return branch; - } - - public void setBranch(String branch) { - this.branch = branch; - } } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Lecture.java b/src/main/java/de/tum/in/www1/artemis/domain/Lecture.java index 23696199cc6e..9985cc3a3f0c 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Lecture.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Lecture.java @@ -39,17 +39,17 @@ public class Lecture extends DomainObject { @Column(name = "visible_date") private ZonedDateTime visibleDate; - @OneToMany(mappedBy = "lecture", cascade = CascadeType.REMOVE, orphanRemoval = true, fetch = FetchType.EAGER) + @OneToMany(mappedBy = "lecture", cascade = CascadeType.REMOVE, orphanRemoval = true) @JsonIgnoreProperties(value = "lecture", allowSetters = true) private Set attachments = new HashSet<>(); - @OneToMany(mappedBy = "lecture", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) + @OneToMany(mappedBy = "lecture", cascade = CascadeType.ALL, orphanRemoval = true) @OrderColumn(name = "lecture_unit_order") @JsonIgnoreProperties("lecture") @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) private List lectureUnits = new ArrayList<>(); - @OneToMany(mappedBy = "lecture", cascade = CascadeType.REMOVE, orphanRemoval = true, fetch = FetchType.LAZY) + @OneToMany(mappedBy = "lecture", cascade = CascadeType.REMOVE, orphanRemoval = true) @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) @JsonIncludeProperties({ "id" }) private Set posts = new HashSet<>(); diff --git a/src/main/java/de/tum/in/www1/artemis/domain/NotificationSetting.java b/src/main/java/de/tum/in/www1/artemis/domain/NotificationSetting.java index eb5feafeef89..b8814dcd5608 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/NotificationSetting.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/NotificationSetting.java @@ -1,7 +1,5 @@ package de.tum.in.www1.artemis.domain; -import java.util.Objects; - import javax.persistence.*; import org.hibernate.annotations.Cache; @@ -100,49 +98,4 @@ public boolean isPush() { public String toString() { return "NotificationSetting{" + ", settingId='" + settingId + '\'' + ", webapp=" + webapp + ", email=" + email + ", push=" + push + ", user=" + user + '}'; } - - @Override - public int hashCode() { - return Objects.hash(getSettingId(), getUser(), isWebapp(), isEmail()); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - DomainObject domainObject = (DomainObject) obj; - if (domainObject.getId() == null || getId() == null) { - return false; - } - boolean domainObjectCheck = Objects.equals(getId(), domainObject.getId()); - NotificationSetting providedSetting = (NotificationSetting) obj; - boolean userCheck = checkUser(this.user, providedSetting.user); - boolean settingIdCheck = checkSettingId(this.settingId, providedSetting.settingId); - return domainObjectCheck && userCheck && settingIdCheck && this.webapp == providedSetting.webapp && this.email == providedSetting.email - && this.push == providedSetting.push; - } - - private boolean checkUser(User thisUser, User providedUser) { - if (thisUser == null && providedUser == null) { - return true; - } - if (thisUser != null && providedUser != null) { - return thisUser.equals(providedUser); - } - return false; - } - - private boolean checkSettingId(String thisSettingId, String providedSettingId) { - if (thisSettingId == null && providedSettingId == null) { - return true; - } - if (thisSettingId != null) { - return thisSettingId.equals(providedSettingId); - } - return false; - } } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingExercise.java b/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingExercise.java index 83eaf8fac255..a5675882fc72 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingExercise.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingExercise.java @@ -50,7 +50,7 @@ public String getType() { private static final Logger log = LoggerFactory.getLogger(ProgrammingExercise.class); @Column(name = "test_repository_url") - private String testRepositoryUrl; + private String testRepositoryUri; @OneToMany(mappedBy = "exercise", cascade = CascadeType.ALL, orphanRemoval = true) @JsonIgnoreProperties(value = "exercise", allowSetters = true) @@ -161,16 +161,16 @@ public String getType() { */ // jhipster-needle-entity-add-field - Jhipster will add fields here, do not remove @JsonIgnore - public String getTemplateRepositoryUrl() { + public String getTemplateRepositoryUri() { if (templateParticipation != null && Hibernate.isInitialized(templateParticipation)) { - return templateParticipation.getRepositoryUrl(); + return templateParticipation.getRepositoryUri(); } return null; } - public void setTemplateRepositoryUrl(String templateRepositoryUrl) { + public void setTemplateRepositoryUri(String templateRepositoryUri) { if (templateParticipation != null && Hibernate.isInitialized(templateParticipation)) { - this.templateParticipation.setRepositoryUrl(templateRepositoryUrl); + this.templateParticipation.setRepositoryUri(templateRepositoryUri); } } @@ -180,25 +180,25 @@ public void setTemplateRepositoryUrl(String templateRepositoryUrl) { * @return The URL of the solution repository as a String */ @JsonIgnore - public String getSolutionRepositoryUrl() { + public String getSolutionRepositoryUri() { if (solutionParticipation != null && Hibernate.isInitialized(solutionParticipation)) { - return solutionParticipation.getRepositoryUrl(); + return solutionParticipation.getRepositoryUri(); } return null; } - public void setSolutionRepositoryUrl(String solutionRepositoryUrl) { + public void setSolutionRepositoryUri(String solutionRepositoryUri) { if (solutionParticipation != null && Hibernate.isInitialized(solutionParticipation)) { - this.solutionParticipation.setRepositoryUrl(solutionRepositoryUrl); + this.solutionParticipation.setRepositoryUri(solutionRepositoryUri); } } - public void setTestRepositoryUrl(String testRepositoryUrl) { - this.testRepositoryUrl = testRepositoryUrl; + public void setTestRepositoryUri(String testRepositoryUri) { + this.testRepositoryUri = testRepositoryUri; } - public String getTestRepositoryUrl() { - return testRepositoryUrl; + public String getTestRepositoryUri() { + return testRepositoryUri; } public List getAuxiliaryRepositories() { @@ -461,43 +461,43 @@ public void setSubmissionPolicy(SubmissionPolicy submissionPolicy) { // jhipster-needle-entity-add-getters-setters - Jhipster will add getters and setters here, do not remove /** - * Gets a URL of the templateRepositoryUrl if there is one + * Gets a URL of the templateRepositoryUri if there is one * - * @return a URL object of the templateRepositoryUrl or null if there is no templateRepositoryUrl + * @return a URL object of the templateRepositoryUri or null if there is no templateRepositoryUri */ @JsonIgnore - public VcsRepositoryUrl getVcsTemplateRepositoryUrl() { - var templateRepositoryUrl = getTemplateRepositoryUrl(); - if (templateRepositoryUrl == null || templateRepositoryUrl.isEmpty()) { + public VcsRepositoryUri getVcsTemplateRepositoryUri() { + var templateRepositoryUri = getTemplateRepositoryUri(); + if (templateRepositoryUri == null || templateRepositoryUri.isEmpty()) { return null; } try { - return new VcsRepositoryUrl(templateRepositoryUrl); + return new VcsRepositoryUri(templateRepositoryUri); } catch (URISyntaxException e) { - log.warn("Cannot create URI for templateRepositoryUrl: {} due to the following error: {}", templateRepositoryUrl, e.getMessage()); + log.warn("Cannot create URI for templateRepositoryUri: {} due to the following error: {}", templateRepositoryUri, e.getMessage()); } return null; } /** - * Gets a URL of the solutionRepositoryUrl if there is one + * Gets a URL of the solutionRepositoryUri if there is one * - * @return a URL object of the solutionRepositoryUrl or null if there is no solutionRepositoryUrl + * @return a URL object of the solutionRepositoryUri or null if there is no solutionRepositoryUri */ @JsonIgnore - public VcsRepositoryUrl getVcsSolutionRepositoryUrl() { - var solutionRepositoryUrl = getSolutionRepositoryUrl(); - if (solutionRepositoryUrl == null || solutionRepositoryUrl.isEmpty()) { + public VcsRepositoryUri getVcsSolutionRepositoryUri() { + var solutionRepositoryUri = getSolutionRepositoryUri(); + if (solutionRepositoryUri == null || solutionRepositoryUri.isEmpty()) { return null; } try { - return new VcsRepositoryUrl(solutionRepositoryUrl); + return new VcsRepositoryUri(solutionRepositoryUri); } catch (URISyntaxException e) { - log.warn("Cannot create URI for solutionRepositoryUrl: {} due to the following error: {}", solutionRepositoryUrl, e.getMessage()); + log.warn("Cannot create URI for solutionRepositoryUri: {} due to the following error: {}", solutionRepositoryUri, e.getMessage()); } return null; } @@ -505,35 +505,35 @@ public VcsRepositoryUrl getVcsSolutionRepositoryUrl() { /** * Gets a URL of the testRepositoryURL if there is one * - * @return a URL object of the testRepositoryURl or null if there is no testRepositoryUrl + * @return a URL object of the testRepositoryURl or null if there is no testRepositoryUri */ @JsonIgnore - public VcsRepositoryUrl getVcsTestRepositoryUrl() { - if (testRepositoryUrl == null || testRepositoryUrl.isEmpty()) { + public VcsRepositoryUri getVcsTestRepositoryUri() { + if (testRepositoryUri == null || testRepositoryUri.isEmpty()) { return null; } try { - return new VcsRepositoryUrl(testRepositoryUrl); + return new VcsRepositoryUri(testRepositoryUri); } catch (URISyntaxException e) { - log.warn("Cannot create URI for testRepositoryUrl: {} due to the following error: {}", testRepositoryUrl, e.getMessage()); + log.warn("Cannot create URI for testRepositoryUri: {} due to the following error: {}", testRepositoryUri, e.getMessage()); } return null; } /** - * Returns the repository url for the given repository type. + * Returns the repository uri for the given repository type. * * @param repositoryType The repository type for which the url should be returned - * @return The repository url + * @return The repository uri */ @JsonIgnore - public VcsRepositoryUrl getRepositoryURL(RepositoryType repositoryType) { + public VcsRepositoryUri getRepositoryURL(RepositoryType repositoryType) { return switch (repositoryType) { - case TEMPLATE -> this.getVcsTemplateRepositoryUrl(); - case SOLUTION -> this.getVcsSolutionRepositoryUrl(); - case TESTS -> this.getVcsTestRepositoryUrl(); + case TEMPLATE -> this.getVcsTemplateRepositoryUri(); + case SOLUTION -> this.getVcsSolutionRepositoryUri(); + case TESTS -> this.getVcsTestRepositoryUri(); default -> throw new UnsupportedOperationException("Can retrieve URL for repository type " + repositoryType); }; } @@ -646,9 +646,9 @@ public void setTestwiseCoverageEnabled(Boolean testwiseCoverageEnabled) { */ @Override public void filterSensitiveInformation() { - setTemplateRepositoryUrl(null); - setSolutionRepositoryUrl(null); - setTestRepositoryUrl(null); + setTemplateRepositoryUri(null); + setSolutionRepositoryUri(null); + setTestRepositoryUri(null); setTemplateBuildPlanId(null); setSolutionBuildPlanId(null); super.filterSensitiveInformation(); @@ -732,7 +732,7 @@ private boolean checkForAssessedResult(Result result) { @Override public String toString() { - return "ProgrammingExercise{" + "id=" + getId() + ", templateRepositoryUrl='" + getTemplateRepositoryUrl() + "'" + ", solutionRepositoryUrl='" + getSolutionRepositoryUrl() + return "ProgrammingExercise{" + "id=" + getId() + ", templateRepositoryUri='" + getTemplateRepositoryUri() + "'" + ", solutionRepositoryUri='" + getSolutionRepositoryUri() + "'" + ", templateBuildPlanId='" + getTemplateBuildPlanId() + "'" + ", solutionBuildPlanId='" + getSolutionBuildPlanId() + "'" + ", publishBuildPlanUrl='" + isPublishBuildPlanUrl() + "'" + ", allowOnlineEditor='" + isAllowOnlineEditor() + "'" + ", programmingLanguage='" + getProgrammingLanguage() + "'" + ", packageName='" + getPackageName() + "'" + ", testCasesChanged='" + testCasesChanged + "'" + "}"; diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Repository.java b/src/main/java/de/tum/in/www1/artemis/domain/Repository.java index c09135f772a6..7185c5f143ae 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Repository.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Repository.java @@ -8,12 +8,13 @@ import org.eclipse.jgit.lib.BaseRepositoryBuilder; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation; +import de.tum.in.www1.artemis.service.connectors.localvc.LocalVCRepositoryUri; /** * This class represents repositories cloned from the VC system to Artemis to then be used in the online editor. * These repositories are cloned from the remote VCS and are saved in the folder defined in the application properties at artemis.repo-clone-path. * Note: This class does not represent local VCS repositories. The local VCS is treated as a remote VCS in code (like Bitbucket and GitLab) and is represented by the - * {@link de.tum.in.www1.artemis.service.connectors.localvc.LocalVCRepositoryUrl} class. + * {@link LocalVCRepositoryUri} class. * Its repositories are saved as bare repositories in the folder defined in the application properties at artemis.version-control.local-vcs-repo-path. */ public class Repository extends org.eclipse.jgit.internal.storage.file.FileRepository { @@ -22,26 +23,26 @@ public class Repository extends org.eclipse.jgit.internal.storage.file.FileRepos private Path localPath; - private final VcsRepositoryUrl remoteRepositoryUrl; + private final VcsRepositoryUri remoteRepositoryUri; private Map filesAndFolders; private Collection files; - public Repository(File gitDir, VcsRepositoryUrl remoteRepositoryUrl) throws IOException { + public Repository(File gitDir, VcsRepositoryUri remoteRepositoryUri) throws IOException { super(gitDir); - this.remoteRepositoryUrl = remoteRepositoryUrl; + this.remoteRepositoryUri = remoteRepositoryUri; } - public Repository(String gitDir, VcsRepositoryUrl remoteRepositoryUrl) throws IOException { + public Repository(String gitDir, VcsRepositoryUri remoteRepositoryUri) throws IOException { super(gitDir); - this.remoteRepositoryUrl = remoteRepositoryUrl; + this.remoteRepositoryUri = remoteRepositoryUri; } - public Repository(BaseRepositoryBuilder options, Path localPath, VcsRepositoryUrl remoteRepositoryUrl) throws IOException { + public Repository(BaseRepositoryBuilder options, Path localPath, VcsRepositoryUri remoteRepositoryUri) throws IOException { super(options); this.localPath = localPath.normalize(); - this.remoteRepositoryUrl = remoteRepositoryUrl; + this.remoteRepositoryUri = remoteRepositoryUri; } /** @@ -97,7 +98,7 @@ public void closeBeforeDelete() { super.doClose(); } - public VcsRepositoryUrl getRemoteRepositoryUrl() { - return remoteRepositoryUrl; + public VcsRepositoryUri getRemoteRepositoryUri() { + return remoteRepositoryUri; } } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/VcsRepositoryUrl.java b/src/main/java/de/tum/in/www1/artemis/domain/VcsRepositoryUri.java similarity index 90% rename from src/main/java/de/tum/in/www1/artemis/domain/VcsRepositoryUrl.java rename to src/main/java/de/tum/in/www1/artemis/domain/VcsRepositoryUri.java index 1828f83ae38f..18f34783593a 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/VcsRepositoryUrl.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/VcsRepositoryUri.java @@ -4,28 +4,27 @@ import java.net.URISyntaxException; import java.util.Objects; -// TODO: we might want to rename this class to VcsRepositoryUri -public class VcsRepositoryUrl { +public class VcsRepositoryUri { protected String username; protected URI uri; - protected VcsRepositoryUrl() { + protected VcsRepositoryUri() { // NOTE: this constructor should not be used and only exists to prevent compile errors } // Create the url from a uriSpecString, e.g. https://ab123cd@bitbucket.ase.in.tum.de/scm/EIST2016RME/RMEXERCISE-ab123cd - public VcsRepositoryUrl(String uriSpecString) throws URISyntaxException { + public VcsRepositoryUri(String uriSpecString) throws URISyntaxException { this.uri = new URI(uriSpecString); } // Create the url from a file reference, e.g. C:/Users/Admin/AppData/Local/Temp/studentOriginRepo1644180397872264950 - public VcsRepositoryUrl(java.io.File file) { + public VcsRepositoryUri(java.io.File file) { this.uri = file.toURI(); } - public VcsRepositoryUrl withUser(final String username) { + public VcsRepositoryUri withUser(final String username) { this.username = username; return this; } @@ -41,7 +40,7 @@ public boolean equals(Object obj) { } // we explicitly allow subclasses (i.e. obj is a subclass of this) here (to avoid issues when comparing subclasses with the same url) // Note that this also includes the null check - if (!(obj instanceof VcsRepositoryUrl that)) { + if (!(obj instanceof VcsRepositoryUri that)) { return false; } return Objects.equals(username, that.username) && Objects.equals(uri, that.uri); @@ -58,7 +57,7 @@ public String toString() { return this.uri.toString(); } else { - return "VcsRepositoryUrl: empty"; + return "VcsRepositoryUri: empty"; } } @@ -75,7 +74,7 @@ public String toString() { * * @return the folderName as a string. */ - public String folderNameForRepositoryUrl() { + public String folderNameForRepositoryUri() { if ("file".equals(uri.getScheme())) { // Take the last element of the path final var segments = uri.getPath().split("/"); diff --git a/src/main/java/de/tum/in/www1/artemis/domain/exam/StudentExam.java b/src/main/java/de/tum/in/www1/artemis/domain/exam/StudentExam.java index 5bff813357fe..ac9fa5c92faa 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/exam/StudentExam.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/exam/StudentExam.java @@ -5,6 +5,7 @@ import javax.persistence.*; +import org.hibernate.Hibernate; import org.hibernate.annotations.Cache; import org.hibernate.annotations.CacheConcurrencyStrategy; @@ -15,6 +16,7 @@ import de.tum.in.www1.artemis.domain.AbstractAuditingEntity; import de.tum.in.www1.artemis.domain.Exercise; import de.tum.in.www1.artemis.domain.User; +import de.tum.in.www1.artemis.domain.quiz.QuizQuestion; @Entity @Table(name = "student_exam") @@ -63,6 +65,10 @@ public class StudentExam extends AbstractAuditingEntity { @JsonIgnoreProperties("studentExam") private Set examSessions = new HashSet<>(); + @ManyToMany + @JoinTable(name = "student_exam_quiz_question", joinColumns = @JoinColumn(name = "student_exam_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "quiz_question_id", referencedColumnName = "id")) + private List quizQuestions = new ArrayList<>(); + public Boolean isSubmitted() { return submitted; } @@ -160,6 +166,24 @@ public void setExamSessions(Set examSessions) { this.examSessions = examSessions; } + /** + * Returns a list of quiz questions associated with the student exam. + * If the quizQuestions list is not null and has been initialized, it returns the list of quiz questions. + * Otherwise, it returns an empty list. + * + * @return the list of quiz questions associated with the student exam + */ + public List getQuizQuestions() { + if (quizQuestions != null && Hibernate.isInitialized(quizQuestions)) { + return quizQuestions; + } + return Collections.emptyList(); + } + + public void setQuizQuestions(List quizQuestions) { + this.quizQuestions = quizQuestions; + } + /** * Adds the given exam session to the student exam * diff --git a/src/main/java/de/tum/in/www1/artemis/domain/hestia/ProgrammingExerciseSolutionEntry.java b/src/main/java/de/tum/in/www1/artemis/domain/hestia/ProgrammingExerciseSolutionEntry.java index aa377818a8bc..8f3425159657 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/hestia/ProgrammingExerciseSolutionEntry.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/hestia/ProgrammingExerciseSolutionEntry.java @@ -1,7 +1,5 @@ package de.tum.in.www1.artemis.domain.hestia; -import java.util.Objects; - import javax.persistence.*; import org.hibernate.annotations.Cache; @@ -125,22 +123,6 @@ public void setTestCase(ProgrammingExerciseTestCase testCase) { this.testCase = testCase; } - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - if (!super.equals(obj)) { - return false; - } - ProgrammingExerciseSolutionEntry that = (ProgrammingExerciseSolutionEntry) obj; - return Objects.equals(filePath, that.filePath) && Objects.equals(previousLine, that.previousLine) && Objects.equals(line, that.line) - && Objects.equals(previousCode, that.previousCode) && Objects.equals(code, that.code); - } - @Override public String toString() { return "ProgrammingExerciseSolutionEntry{" + "id=" + getId() + '\'' + ", filePath='" + filePath + '\'' + ", previousLine=" + previousLine + ", line=" + line diff --git a/src/main/java/de/tum/in/www1/artemis/domain/hestia/ProgrammingExerciseTask.java b/src/main/java/de/tum/in/www1/artemis/domain/hestia/ProgrammingExerciseTask.java index 6f2674294f60..485154481fe3 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/hestia/ProgrammingExerciseTask.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/hestia/ProgrammingExerciseTask.java @@ -1,7 +1,6 @@ package de.tum.in.www1.artemis.domain.hestia; import java.util.HashSet; -import java.util.Objects; import java.util.Set; import javax.persistence.*; @@ -79,19 +78,4 @@ public void setExercise(ProgrammingExercise exercise) { public String toString() { return "ProgrammingExerciseTask{" + "taskName='" + taskName + '\'' + ", testCases=" + testCases + '}'; } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - if (!super.equals(obj)) { - return false; - } - ProgrammingExerciseTask that = (ProgrammingExerciseTask) obj; - return Objects.equals(taskName, that.taskName) && Objects.equals(testCases, that.testCases); - } } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/iris/message/IrisTextMessageContent.java b/src/main/java/de/tum/in/www1/artemis/domain/iris/message/IrisTextMessageContent.java index d502eea9aa49..8236ae2242e6 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/iris/message/IrisTextMessageContent.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/iris/message/IrisTextMessageContent.java @@ -1,7 +1,5 @@ package de.tum.in.www1.artemis.domain.iris.message; -import java.util.Objects; - import javax.annotation.Nullable; import javax.persistence.Column; import javax.persistence.DiscriminatorValue; @@ -49,9 +47,4 @@ public void setTextContent(@Nullable String textContent) { public String toString() { return "IrisMessageContent{" + "message=" + (message == null ? "null" : message.getId()) + ", textContent='" + textContent + '\'' + '}'; } - - @Override - public boolean equals(Object obj) { - return super.equals(obj) && Objects.equals(this.textContent, ((IrisTextMessageContent) obj).textContent); - } } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/lti/Lti13AgsClaim.java b/src/main/java/de/tum/in/www1/artemis/domain/lti/Lti13AgsClaim.java index 3884f1fd65e4..6b9e1a431138 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/lti/Lti13AgsClaim.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/lti/Lti13AgsClaim.java @@ -20,33 +20,38 @@ public class Lti13AgsClaim { * * @param idToken to be parsed * @return an Ags-Claim if one was present in idToken. + * @throws IllegalStateException if the AGS claim is present but malformed or cannot be processed. */ public static Optional from(OidcIdToken idToken) { if (idToken.getClaim(Claims.AGS_CLAIM) == null) { return Optional.empty(); } - JsonObject agsClaimJson = JsonParser.parseString(idToken.getClaim(Claims.AGS_CLAIM).toString()).getAsJsonObject(); - - Lti13AgsClaim agsClaim = new Lti13AgsClaim(); - JsonArray scopes = agsClaimJson.get("scope").getAsJsonArray(); - - if (scopes == null) { - return Optional.empty(); - } - - if (scopes.contains(new JsonPrimitive(Scopes.AGS_SCORE))) { - agsClaim.setScope(Collections.singletonList(Scopes.AGS_SCORE)); - } - - JsonElement lineItem = agsClaimJson.get("lineitem"); - if (lineItem != null) { - agsClaim.setLineItem(lineItem.getAsString()); + try { + JsonObject agsClaimJson = new Gson().toJsonTree(idToken.getClaim(Claims.AGS_CLAIM)).getAsJsonObject(); + Lti13AgsClaim agsClaim = new Lti13AgsClaim(); + JsonArray scopes = agsClaimJson.get("scope").getAsJsonArray(); + + if (scopes == null) { + return Optional.empty(); + } + + if (scopes.contains(new JsonPrimitive(Scopes.AGS_SCORE))) { + agsClaim.setScope(Collections.singletonList(Scopes.AGS_SCORE)); + } + + JsonElement lineItem = agsClaimJson.get("lineitem"); + if (lineItem != null) { + agsClaim.setLineItem(lineItem.getAsString()); + } + else { + agsClaim.setLineItem(null); + } + return Optional.of(agsClaim); } - else { - agsClaim.setLineItem(null); + catch (IllegalStateException | ClassCastException ex) { + throw new IllegalStateException("Failed to parse LTI 1.3 ags claim.", ex); } - return Optional.of(agsClaim); } public List getScope() { diff --git a/src/main/java/de/tum/in/www1/artemis/domain/lti/Lti13DeepLinkingResponse.java b/src/main/java/de/tum/in/www1/artemis/domain/lti/Lti13DeepLinkingResponse.java index 5b79999a21e2..3217c0072dfe 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/lti/Lti13DeepLinkingResponse.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/lti/Lti13DeepLinkingResponse.java @@ -6,19 +6,29 @@ import org.springframework.security.oauth2.core.oidc.OidcIdToken; import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.Gson; import com.google.gson.JsonObject; -import com.google.gson.JsonParser; /** * Represents the LTI 1.3 Deep Linking Response. - * It encapsulates the necessary information to construct a valid deep linking response - * according to the LTI 1.3 specification. + * It encapsulates the necessary information to construct a valid deep linking response according to the LTI 1.3 specification. + * For more details, refer to LTI 1.3 Deep Linking Response Specification */ public class Lti13DeepLinkingResponse { + /** + * The 'aud' (Audience) field is used to specify the intended recipient of the response + * In the context of LTI Deep Linking, this field is set to the 'iss' (Issuer) value from the original request. + * This indicates that the response is specifically intended for the issuer of the original request. + */ @JsonProperty("aud") private String aud; + /** + * The 'iss' (Issuer) field identifies the principal that issued the response. + * For LTI Deep Linking, this field is set to the 'aud' (Audience) value from the original request. + * This reversal signifies that the tool (originally the audience) is now the issuer of the response. + */ @JsonProperty("iss") private String iss; @@ -54,6 +64,10 @@ public class Lti13DeepLinkingResponse { /** * Constructs an Lti13DeepLinkingResponse from an OIDC ID token and client registration ID. + * The 'aud' and 'iss' fields are reversed in the response compared to the request. This is a specific requirement in LTI 1.3 Deep Linking: + * - The 'iss' (Issuer) claim in the Deep Linking Request becomes the 'aud' (Audience) claim in the Response. + * - The 'aud' claim in the Request becomes the 'iss' claim in the Response. + * This reversal ensures that the response is directed to and validated by the correct entity, adhering to the OIDC and LTI 1.3 protocols. * * @param ltiIdToken the OIDC ID token * @param clientRegistrationId the client registration ID @@ -61,12 +75,17 @@ public class Lti13DeepLinkingResponse { public Lti13DeepLinkingResponse(OidcIdToken ltiIdToken, String clientRegistrationId) { validateClaims(ltiIdToken); - this.deepLinkingSettings = JsonParser.parseString(ltiIdToken.getClaim(Claims.DEEP_LINKING_SETTINGS).toString()).getAsJsonObject(); + Map deepLinkingSettings = ltiIdToken.getClaim(Claims.DEEP_LINKING_SETTINGS); + // convert the map to json + this.deepLinkingSettings = new Gson().toJsonTree(deepLinkingSettings).getAsJsonObject(); this.setReturnUrl(this.deepLinkingSettings.get("deep_link_return_url").getAsString()); this.clientRegistrationId = clientRegistrationId; + // the issuer claim in the deep linking request becomes the audience claim in the response this.setAud(ltiIdToken.getClaim("iss").toString()); + // the audience claim in the request becomes the issuer claim in the response this.setIss(ltiIdToken.getClaim("aud").toString().replace("[", "").replace("]", "")); + this.setExp(ltiIdToken.getClaim("exp").toString()); this.setIat(ltiIdToken.getClaim("iat").toString()); this.setNonce(ltiIdToken.getClaim("nonce").toString()); diff --git a/src/main/java/de/tum/in/www1/artemis/domain/lti/Lti13LaunchRequest.java b/src/main/java/de/tum/in/www1/artemis/domain/lti/Lti13LaunchRequest.java index 738804afe4bc..3f855e9c23c5 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/lti/Lti13LaunchRequest.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/lti/Lti13LaunchRequest.java @@ -3,7 +3,7 @@ import org.springframework.security.oauth2.core.oidc.OidcIdToken; import org.springframework.util.Assert; -import com.google.gson.JsonParser; +import com.google.gson.Gson; public class Lti13LaunchRequest { @@ -26,7 +26,7 @@ public Lti13LaunchRequest(OidcIdToken ltiIdToken, String clientRegistrationId) { var resourceLinkClaim = ltiIdToken.getClaim(Claims.RESOURCE_LINK); if (resourceLinkClaim != null) { - this.resourceLinkId = JsonParser.parseString(resourceLinkClaim.toString()).getAsJsonObject().get("id").getAsString(); + this.resourceLinkId = new Gson().toJsonTree(resourceLinkClaim).getAsJsonObject().get("id").getAsString(); } else { this.resourceLinkId = null; diff --git a/src/main/java/de/tum/in/www1/artemis/domain/participation/AbstractBaseProgrammingExerciseParticipation.java b/src/main/java/de/tum/in/www1/artemis/domain/participation/AbstractBaseProgrammingExerciseParticipation.java index fa70aecfaa31..cc669df8af94 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/participation/AbstractBaseProgrammingExerciseParticipation.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/participation/AbstractBaseProgrammingExerciseParticipation.java @@ -17,18 +17,18 @@ public abstract class AbstractBaseProgrammingExerciseParticipation extends Parti @Column(name = "repository_url") @JsonView(QuizView.Before.class) - private String repositoryUrl; + private String repositoryUri; @Column(name = "build_plan_id") @JsonView(QuizView.Before.class) private String buildPlanId; - public String getRepositoryUrl() { - return repositoryUrl; + public String getRepositoryUri() { + return repositoryUri; } - public void setRepositoryUrl(String repositoryUrl) { - this.repositoryUrl = repositoryUrl; + public void setRepositoryUri(String repositoryUri) { + this.repositoryUri = repositoryUri; } public String getBuildPlanId() { @@ -62,6 +62,6 @@ public void filterSensitiveInformation() { @Override public String toString() { - return getClass().getSimpleName() + "{" + "id=" + getId() + ", repositoryUrl='" + getRepositoryUrl() + "'" + ", buildPlanId='" + getBuildPlanId() + "}"; + return getClass().getSimpleName() + "{" + "id=" + getId() + ", repositoryUri='" + getRepositoryUri() + "'" + ", buildPlanId='" + getBuildPlanId() + "}"; } } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/participation/ProgrammingExerciseParticipation.java b/src/main/java/de/tum/in/www1/artemis/domain/participation/ProgrammingExerciseParticipation.java index cbf03dfcffe3..80dd8a4a295f 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/participation/ProgrammingExerciseParticipation.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/participation/ProgrammingExerciseParticipation.java @@ -14,15 +14,15 @@ import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.Result; -import de.tum.in.www1.artemis.domain.VcsRepositoryUrl; +import de.tum.in.www1.artemis.domain.VcsRepositoryUri; public interface ProgrammingExerciseParticipation extends ParticipationInterface { Logger log = LoggerFactory.getLogger(ProgrammingExerciseParticipation.class); - String getRepositoryUrl(); + String getRepositoryUri(); - void setRepositoryUrl(String repositoryUrl); + void setRepositoryUri(String repositoryUri); String getBuildPlanId(); @@ -41,36 +41,36 @@ public interface ProgrammingExerciseParticipation extends ParticipationInterface */ @Nullable @JsonInclude - default String getUserIndependentRepositoryUrl() { - if (getRepositoryUrl() == null) { + default String getUserIndependentRepositoryUri() { + if (getRepositoryUri() == null) { return null; } try { - URI repoUrl = new URI(getRepositoryUrl()); + URI repoUri = new URI(getRepositoryUri()); // Note: the following line reconstructs the URL without using the authority, it removes ’username@' before the host - return new URI(repoUrl.getScheme(), null, repoUrl.getHost(), repoUrl.getPort(), repoUrl.getPath(), null, null).toString(); + return new URI(repoUri.getScheme(), null, repoUri.getHost(), repoUri.getPort(), repoUri.getPath(), null, null).toString(); } catch (URISyntaxException e) { - log.debug("Cannot create user independent repository url from {} due to malformed URL exception", getRepositoryUrl(), e); + log.debug("Cannot create user independent repository uri from {} due to malformed URL exception", getRepositoryUri(), e); return null; } } /** - * @return the repository url of the programming exercise participation wrapped in an object + * @return the repository uri of the programming exercise participation wrapped in an object */ @JsonIgnore - default VcsRepositoryUrl getVcsRepositoryUrl() { - var repoUrl = getRepositoryUrl(); - if (repoUrl == null) { + default VcsRepositoryUri getVcsRepositoryUri() { + var repoUri = getRepositoryUri(); + if (repoUri == null) { return null; } try { - return new VcsRepositoryUrl(repoUrl); + return new VcsRepositoryUri(repoUri); } catch (URISyntaxException e) { - log.warn("Cannot create URI for repositoryUrl: {} due to the following error: {}", repoUrl, e.getMessage()); + log.warn("Cannot create URI for repositoryUri: {} due to the following error: {}", repoUri, e.getMessage()); } return null; } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/participation/ProgrammingExerciseStudentParticipation.java b/src/main/java/de/tum/in/www1/artemis/domain/participation/ProgrammingExerciseStudentParticipation.java index 9005fa049173..e9ff258532d8 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/participation/ProgrammingExerciseStudentParticipation.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/participation/ProgrammingExerciseStudentParticipation.java @@ -20,7 +20,7 @@ public class ProgrammingExerciseStudentParticipation extends StudentParticipatio @Column(name = "repository_url") @JsonView(QuizView.Before.class) - private String repositoryUrl; + private String repositoryUri; @Column(name = "build_plan_id") @JsonView(QuizView.Before.class) @@ -50,12 +50,12 @@ public ProgrammingExerciseStudentParticipation(String branch) { this.branch = branch; } - public String getRepositoryUrl() { - return repositoryUrl; + public String getRepositoryUri() { + return repositoryUri; } - public void setRepositoryUrl(String repositoryUrl) { - this.repositoryUrl = repositoryUrl; + public void setRepositoryUri(String repositoryUri) { + this.repositoryUri = repositoryUri; } public String getBuildPlanId() { @@ -115,7 +115,7 @@ public String getType() { @Override public String toString() { - return getClass().getSimpleName() + "{" + "id=" + getId() + ", repositoryUrl='" + getRepositoryUrl() + "'" + ", buildPlanId='" + getBuildPlanId() + "'" + return getClass().getSimpleName() + "{" + "id=" + getId() + ", repositoryUri='" + getRepositoryUri() + "'" + ", buildPlanId='" + getBuildPlanId() + "'" + ", initializationState='" + getInitializationState() + "'" + ", initializationDate='" + getInitializationDate() + "'" + ", individualDueDate=" + getIndividualDueDate() + "'" + ", presentationScore=" + getPresentationScore() + "}"; } 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 96538da81170..a89b3230c30b 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,7 +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; @@ -172,25 +171,4 @@ public int compareTo(@NotNull PlagiarismComparison otherComparison) { public String toString() { return "PlagiarismComparison{" + "similarity=" + similarity + ", status=" + status + '}'; } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - if (!super.equals(obj)) { - return false; - } - - PlagiarismComparison that = (PlagiarismComparison) obj; - return Double.compare(that.similarity, similarity) == 0 && status == that.status; - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), getSimilarity(), getStatus()); - } } 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 ba034ebedc5a..b11db8caf546 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 @@ -1,5 +1,7 @@ package de.tum.in.www1.artemis.domain.plagiarism; +import java.util.Objects; + import javax.persistence.Column; import javax.persistence.Embeddable; @@ -66,6 +68,43 @@ public void setLength(int length) { this.length = length; } + /** + * Overrides the equals method to ensure proper equality checks to not rely on the default object instance equality. + * + *

+ * This is important to ensure uniqueness, e.g. when loading objects from the database into a + * {@link java.util.Set}. + * + *

+ * Note: + * This is required here since unlike other domain classes this one does not extend + * {@link de.tum.in.www1.artemis.domain.DomainObject} since it only represents an {@link Embeddable} part of another + * entity. + * Therefore, it does inherit neither the database ID attribute nor the matching + * {@link de.tum.in.www1.artemis.domain.DomainObject#equals(Object)} implementation. + * Instead, we have to compare all relevant attributes here. + * + * @param other Some other object. + * @return True, if this and the other object are equal. + */ + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (other instanceof PlagiarismMatch that) { + return startA == that.startA && startB == that.startB && length == that.length; + } + + return false; + } + + @Override + public int hashCode() { + return Objects.hash(startA, startB, length); + } + @Override public String toString() { return "PlagiarismMatch{" + "startA=" + startA + ", startB=" + startB + ", length=" + length + '}'; 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 95cb416b4b7d..d09bf92fe670 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 @@ -197,25 +197,4 @@ public void setPlagiarismComparison(PlagiarismComparison plagiarismComparison public String toString() { return "PlagiarismSubmission{" + "submissionId=" + submissionId + ", studentLogin='" + studentLogin + '\'' + ", size=" + size + ", score=" + score + '}'; } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - if (!super.equals(obj)) { - return false; - } - PlagiarismSubmission that = (PlagiarismSubmission) obj; - return getSubmissionId() == that.getSubmissionId() && getSize() == that.getSize() && Objects.equals(getStudentLogin(), that.getStudentLogin()) - && Objects.equals(getScore(), that.getScore()); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), getSubmissionId(), getStudentLogin(), getSize(), getScore()); - } } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/quiz/DragAndDropQuestion.java b/src/main/java/de/tum/in/www1/artemis/domain/quiz/DragAndDropQuestion.java index a8c8e3ba5de8..5dc499a85dd5 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/quiz/DragAndDropQuestion.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/quiz/DragAndDropQuestion.java @@ -177,7 +177,7 @@ public void onDelete() { } catch (FilePathParsingException e) { // if the file path is invalid, we don't need to delete it - log.warn("Could not delete file with path {}. Assume already deleted, entity can be removed.", backgroundFilePath, e); + log.warn("Could not delete file with path {}. Assume already deleted, DragAndDropQuestion {} can be removed.", backgroundFilePath, getId()); } } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/quiz/DragItem.java b/src/main/java/de/tum/in/www1/artemis/domain/quiz/DragItem.java index a6de54e6dda4..ba58fafdd893 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/quiz/DragItem.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/quiz/DragItem.java @@ -144,7 +144,7 @@ public void onDelete() { } catch (FilePathParsingException e) { // if the file path is invalid, we don't need to delete it - log.warn("Could not delete file with path {}. Assume already deleted, entity can be removed.", pictureFilePath, e); + log.warn("Could not delete file with path {}. Assume already deleted, DragAndDropQuestion {} can be removed.", pictureFilePath, getId()); } } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/quiz/QuizPool.java b/src/main/java/de/tum/in/www1/artemis/domain/quiz/QuizPool.java index e3e088eda513..5b9d2e0af5bb 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/quiz/QuizPool.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/quiz/QuizPool.java @@ -1,7 +1,6 @@ package de.tum.in.www1.artemis.domain.quiz; -import java.util.ArrayList; -import java.util.List; +import java.util.*; import javax.persistence.CascadeType; import javax.persistence.Column; @@ -23,7 +22,7 @@ @Entity @Table(name = "quiz_pool") -@JsonInclude +@JsonInclude(JsonInclude.Include.NON_EMPTY) public class QuizPool extends DomainObject implements QuizConfiguration { @OneToOne @@ -75,10 +74,11 @@ public void setRandomizeQuestionOrder(Boolean randomizeQuestionOrder) { @Override public void setQuestionParent(QuizQuestion quizQuestion) { - // Do nothing since the relationship between QuizPool and QuizQuestion is defined in QuizPool. + // Do nothing since the relationship between QuizPool and QuizQuestion is already defined above. } @Override + // NOTE: there is no guarantee for a specific order because the underlying data is stored in a set without order column public List getQuizQuestions() { return this.quizQuestions; } @@ -88,6 +88,7 @@ public void setQuizQuestions(List quizQuestions) { } @JsonProperty(value = "quizGroups", access = JsonProperty.Access.READ_ONLY) + // NOTE: there is no guarantee for a specific order because the underlying data is stored in a set without order column public List getQuizGroups() { return quizGroups; } diff --git a/src/main/java/de/tum/in/www1/artemis/repository/LectureRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/LectureRepository.java index 1c57a0a774bf..3c7b862d3432 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/LectureRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/LectureRepository.java @@ -62,6 +62,7 @@ public interface LectureRepository extends JpaRepository { @Query(""" SELECT lecture FROM Lecture lecture + LEFT JOIN FETCH lecture.attachments LEFT JOIN FETCH lecture.posts LEFT JOIN FETCH lecture.lectureUnits lu LEFT JOIN FETCH lu.completedUsers cu @@ -70,7 +71,7 @@ public interface LectureRepository extends JpaRepository { LEFT JOIN FETCH exercise.competencies WHERE lecture.id = :lectureId """) - Optional findByIdWithPostsAndLectureUnitsAndCompetenciesAndCompletions(@Param("lectureId") Long lectureId); + Optional findByIdWithAttachmentsAndPostsAndLectureUnitsAndCompetenciesAndCompletions(@Param("lectureId") Long lectureId); @Query(""" SELECT lecture @@ -87,9 +88,10 @@ public interface LectureRepository extends JpaRepository { SELECT lecture FROM Lecture lecture LEFT JOIN FETCH lecture.lectureUnits + LEFT JOIN FETCH lecture.attachments WHERE lecture.id = :lectureId """) - Optional findByIdWithLectureUnits(@Param("lectureId") Long lectureId); + Optional findByIdWithLectureUnitsAndAttachments(@Param("lectureId") Long lectureId); @Query(""" SELECT lecture @@ -148,13 +150,13 @@ default Lecture findByIdWithLectureUnitsAndCompetenciesElseThrow(Long lectureId) } @NotNull - default Lecture findByIdWithPostsAndLectureUnitsAndCompetenciesAndCompletionsElseThrow(Long lectureId) { - return findByIdWithPostsAndLectureUnitsAndCompetenciesAndCompletions(lectureId).orElseThrow(() -> new EntityNotFoundException("Lecture", lectureId)); + default Lecture findByIdWithAttachmentsAndPostsAndLectureUnitsAndCompetenciesAndCompletionsElseThrow(Long lectureId) { + return findByIdWithAttachmentsAndPostsAndLectureUnitsAndCompetenciesAndCompletions(lectureId).orElseThrow(() -> new EntityNotFoundException("Lecture", lectureId)); } @NotNull - default Lecture findByIdWithLectureUnitsElseThrow(Long lectureId) { - return findByIdWithLectureUnits(lectureId).orElseThrow(() -> new EntityNotFoundException("Lecture", lectureId)); + default Lecture findByIdWithLectureUnitsAndAttachmentsElseThrow(Long lectureId) { + return findByIdWithLectureUnitsAndAttachments(lectureId).orElseThrow(() -> new EntityNotFoundException("Lecture", lectureId)); } @NotNull diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java index baf225e7a5af..0ded947d3079 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java @@ -179,7 +179,7 @@ default Optional findStudentParticipati @Query(""" SELECT DISTINCT p FROM ProgrammingExerciseStudentParticipation p - WHERE p.buildPlanId IS NOT NULL or p.repositoryUrl IS NOT NULL + WHERE p.buildPlanId IS NOT NULL or p.repositoryUri IS NOT NULL """) - Page findAllWithRepositoryUrlOrBuildPlanId(Pageable pageable); + Page findAllWithRepositoryUriOrBuildPlanId(Pageable pageable); } diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java index fcfd65dfbd99..9c190dce60d5 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java @@ -20,7 +20,9 @@ import de.tum.in.www1.artemis.domain.exam.ExerciseGroup; import de.tum.in.www1.artemis.domain.exam.StudentExam; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; +import de.tum.in.www1.artemis.domain.quiz.QuizQuestion; import de.tum.in.www1.artemis.service.ExerciseDateService; +import de.tum.in.www1.artemis.service.exam.ExamQuizQuestionsGenerator; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; /** @@ -376,11 +378,12 @@ default Integer findMaxWorkingTimeByExamIdElseThrow(Long examId) { /** * Generates random exams for each user in the given users set and saves them. * - * @param exam exam for which the individual student exams will be generated - * @param users users for which the individual exams will be generated + * @param exam exam for which the individual student exams will be generated + * @param users users for which the individual exams will be generated + * @param examQuizQuestionsGenerator generator to generate quiz questions for the exam * @return List of StudentExams generated for the given users */ - default List createRandomStudentExams(Exam exam, Set users) { + default List createRandomStudentExams(Exam exam, Set users, ExamQuizQuestionsGenerator examQuizQuestionsGenerator) { List studentExams = new ArrayList<>(); SecureRandom random = new SecureRandom(); long numberOfOptionalExercises = exam.getNumberOfExercisesInExam() - exam.getExerciseGroups().stream().filter(ExerciseGroup::getIsMandatory).count(); @@ -421,6 +424,8 @@ default List createRandomStudentExams(Exam exam, Set users) { if (Boolean.TRUE.equals(exam.getRandomizeExerciseOrder())) { Collections.shuffle(studentExam.getExercises()); } + List quizQuestions = examQuizQuestionsGenerator.generateQuizQuestionsForExam(exam.getId()); + studentExam.setQuizQuestions(quizQuestions); studentExams.add(studentExam); } @@ -453,10 +458,11 @@ private Exercise selectRandomExercise(SecureRandom random, ExerciseGroup exercis * Generates the student exams randomly based on the exam configuration and the exercise groups * Important: the passed exams needs to include the registered users, exercise groups and exercises (eagerly loaded) * - * @param exam with eagerly loaded registered users, exerciseGroups and exercises loaded + * @param exam with eagerly loaded registered users, exerciseGroups and exercises loaded + * @param examQuizQuestionsGenerator generator to generate quiz questions for the exam * @return the list of student exams with their corresponding users */ - default List generateStudentExams(final Exam exam) { + default List generateStudentExams(final Exam exam, ExamQuizQuestionsGenerator examQuizQuestionsGenerator) { final var existingStudentExams = findByExamId(exam.getId()); // https://jira.spring.io/browse/DATAJPA-1367 deleteInBatch does not work, because it does not cascade the deletion of existing exam sessions, therefore use deleteAll deleteAll(existingStudentExams); @@ -464,28 +470,34 @@ default List generateStudentExams(final Exam exam) { Set users = exam.getRegisteredUsers(); // StudentExams are saved in the called method - return createRandomStudentExams(exam, users); + return createRandomStudentExams(exam, users, examQuizQuestionsGenerator); } /** - * Generates the missing student exams randomly based on the exam configuration and the exercise groups. - * The difference between all registered users and the users who already have an individual exam is the set of users for which student exams will be created. - *

- * Important: the passed exams needs to include the registered users, exercise groups and exercises (eagerly loaded) + * Get all student exams for the given exam id with quiz questions. * - * @param exam with eagerly loaded registered users, exerciseGroups and exercises loaded - * @return the list of student exams with their corresponding users + * @param ids the ids of the student exams + * @return the list of student exams with quiz questions */ - default List generateMissingStudentExams(Exam exam) { - - // Get all users who already have an individual exam - Set usersWithStudentExam = findUsersWithStudentExamsForExam(exam.getId()); - - // Get all students who don't have an exam yet - Set missingUsers = exam.getRegisteredUsers(); - missingUsers.removeAll(usersWithStudentExam); + @Query(""" + SELECT DISTINCT se + FROM StudentExam se + LEFT JOIN FETCH se.quizQuestions qq + WHERE se.id IN :ids + """) + List findAllWithEagerQuizQuestionsById(List ids); - // StudentExams are saved in the called method - return createRandomStudentExams(exam, missingUsers); - } + /** + * Get all student exams for the given exam id with exercises. + * + * @param ids the ids of the student exams + * @return the list of student exams with exercises + */ + @Query(""" + SELECT DISTINCT se + FROM StudentExam se + LEFT JOIN FETCH se.exercises e + WHERE se.id IN :ids + """) + List findAllWithEagerExercisesById(List ids); } diff --git a/src/main/java/de/tum/in/www1/artemis/repository/UserRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/UserRepository.java index dbf6fc927c34..a24406cc6861 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/UserRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/UserRepository.java @@ -74,6 +74,9 @@ public interface UserRepository extends JpaRepository, JpaSpecificat @EntityGraph(type = LOAD, attributePaths = { "groups", "authorities" }) Optional findOneWithGroupsAndAuthoritiesByLogin(String login); + @EntityGraph(type = LOAD, attributePaths = { "authorities" }) + Optional findOneWithAuthoritiesByLogin(String login); + @EntityGraph(type = LOAD, attributePaths = { "groups", "authorities" }) Optional findOneWithGroupsAndAuthoritiesByEmail(String email); @@ -499,6 +502,17 @@ default User getUserWithGroupsAndAuthorities() { return unwrapOptionalUser(user, currentUserLogin); } + /** + * Get user with authorities of currently logged-in user + * + * @return currently logged-in user + */ + default User getUserWithAuthorities() { + String currentUserLogin = getCurrentUserLogin(); + Optional user = findOneWithAuthoritiesByLogin(currentUserLogin); + return unwrapOptionalUser(user, currentUserLogin); + } + /** * Get user with user groups, authorities and organizations of currently logged-in user * @@ -549,6 +563,17 @@ default User getUserWithGroupsAndAuthorities(@NotNull String username) { return unwrapOptionalUser(user, username); } + /** + * Get user with authorities with the username (i.e. user.getLogin() or principal.getName()) + * + * @param username the username of the user who should be retrieved from the database + * @return the user that belongs to the given principal with eagerly loaded authorities + */ + default User getUserWithAuthorities(@NotNull String username) { + Optional user = findOneWithAuthoritiesByLogin(username); + return unwrapOptionalUser(user, username); + } + /** * Finds a single user with groups and authorities using the registration number * diff --git a/src/main/java/de/tum/in/www1/artemis/repository/iris/IrisSettingsRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/iris/IrisSettingsRepository.java index fc402041bc34..fc0ae02e6b8d 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/iris/IrisSettingsRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/iris/IrisSettingsRepository.java @@ -7,7 +7,10 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; -import de.tum.in.www1.artemis.domain.iris.settings.*; +import de.tum.in.www1.artemis.domain.iris.settings.IrisCourseSettings; +import de.tum.in.www1.artemis.domain.iris.settings.IrisExerciseSettings; +import de.tum.in.www1.artemis.domain.iris.settings.IrisGlobalSettings; +import de.tum.in.www1.artemis.domain.iris.settings.IrisSettings; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; /** @@ -38,6 +41,12 @@ default IrisGlobalSettings findGlobalSettingsElseThrow() { """) Optional findCourseSettings(Long courseId); + /** + * Retrieves Iris exercise settings for a given exercise ID. + * + * @param exerciseId for which settings are to be retrieved. + * @return An Optional containing IrisExerciseSettings if found, otherwise empty. + */ @Query(""" SELECT irisSettings FROM IrisExerciseSettings irisSettings diff --git a/src/main/java/de/tum/in/www1/artemis/service/ConsistencyCheckService.java b/src/main/java/de/tum/in/www1/artemis/service/ConsistencyCheckService.java index eb8e43df22f9..12e3864e3a08 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ConsistencyCheckService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ConsistencyCheckService.java @@ -79,17 +79,17 @@ private List checkVCSConsistency(ProgrammingExercise progra result.add(new ConsistencyErrorDTO(programmingExercise, ConsistencyErrorDTO.ErrorType.VCS_PROJECT_MISSING)); } else { - if (!versionControl.repositoryUrlIsValid(programmingExercise.getVcsTemplateRepositoryUrl())) { + if (!versionControl.repositoryUriIsValid(programmingExercise.getVcsTemplateRepositoryUri())) { result.add(new ConsistencyErrorDTO(programmingExercise, ConsistencyErrorDTO.ErrorType.TEMPLATE_REPO_MISSING)); } - if (!versionControl.repositoryUrlIsValid(programmingExercise.getVcsTestRepositoryUrl())) { + if (!versionControl.repositoryUriIsValid(programmingExercise.getVcsTestRepositoryUri())) { result.add(new ConsistencyErrorDTO(programmingExercise, ConsistencyErrorDTO.ErrorType.TEST_REPO_MISSING)); } - if (!versionControl.repositoryUrlIsValid(programmingExercise.getVcsSolutionRepositoryUrl())) { + if (!versionControl.repositoryUriIsValid(programmingExercise.getVcsSolutionRepositoryUri())) { result.add(new ConsistencyErrorDTO(programmingExercise, ConsistencyErrorDTO.ErrorType.SOLUTION_REPO_MISSING)); } for (var auxiliaryRepository : programmingExercise.getAuxiliaryRepositories()) { - if (!versionControl.repositoryUrlIsValid(auxiliaryRepository.getVcsRepositoryUrl())) { + if (!versionControl.repositoryUriIsValid(auxiliaryRepository.getVcsRepositoryUri())) { result.add(new ConsistencyErrorDTO(programmingExercise, ConsistencyErrorDTO.ErrorType.AUXILIARY_REPO_MISSING)); } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/ExerciseService.java b/src/main/java/de/tum/in/www1/artemis/service/ExerciseService.java index 66f85b4c71a1..0d785d911c9f 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ExerciseService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ExerciseService.java @@ -418,7 +418,7 @@ public void filterForCourseDashboard(Exercise exercise, Set internalCiUrl, Optional internalVcs } /** - * Replaces the url of the vcs repository url to the internal url if it's + * Replaces the url of the vcs repository uri to the internal url if it's * defined. * - * @param vcsRepositoryUrl the vcs repository url - * @return the vcs repository url with the internal url + * @param vcsRepositoryUri the vcs repository uri + * @return the vcs repository uri with the internal url */ - public VcsRepositoryUrl toInternalVcsUrl(VcsRepositoryUrl vcsRepositoryUrl) { - if (vcsRepositoryUrl.getURI() == null) { + public VcsRepositoryUri toInternalVcsUrl(VcsRepositoryUri vcsRepositoryUri) { + if (vcsRepositoryUri.getURI() == null) { log.warn("Cannot replace url to internal url {} because the url is null.", internalVcsUrl); - return vcsRepositoryUrl; + return vcsRepositoryUri; } try { - String newInternalUrl = toInternalVcsUrl(vcsRepositoryUrl.getURI().toString()); - return new VcsRepositoryUrl(newInternalUrl); + String newInternalUrl = toInternalVcsUrl(vcsRepositoryUri.getURI().toString()); + return new VcsRepositoryUri(newInternalUrl); } catch (URISyntaxException e) { - log.warn("Cannot replace url {} to {}: {}. Falling back to original url.", vcsRepositoryUrl, internalVcsUrl, e.getMessage()); - return vcsRepositoryUrl; + log.warn("Cannot replace url {} to {}: {}. Falling back to original url.", vcsRepositoryUri, internalVcsUrl, e.getMessage()); + return vcsRepositoryUri; } } /** - * Replaces the url of the vcs repository url to the internal url if it's + * Replaces the url of the vcs repository uri to the internal url if it's * defined. * - * @param vcsRepositoryUrl the vcs repository url - * @return the vcs repository url with the internal url + * @param vcsRepositoryUri the vcs repository uri + * @return the vcs repository uri with the internal url */ - public String toInternalVcsUrl(String vcsRepositoryUrl) { + public String toInternalVcsUrl(String vcsRepositoryUri) { if (internalVcsUrl.isEmpty()) { - return vcsRepositoryUrl; + return vcsRepositoryUri; } - if (vcsRepositoryUrl == null) { + if (vcsRepositoryUri == null) { log.warn("Cannot replace url to internal url {} because the url is null.", internalVcsUrl); - return vcsRepositoryUrl; + return vcsRepositoryUri; } - return replaceUrl(vcsRepositoryUrl, internalVcsUrl.get()); + return replaceUrl(vcsRepositoryUri, internalVcsUrl.get()); } /** diff --git a/src/main/java/de/tum/in/www1/artemis/service/LectureUnitService.java b/src/main/java/de/tum/in/www1/artemis/service/LectureUnitService.java index 2474419f9985..b02b968cef08 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LectureUnitService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LectureUnitService.java @@ -88,7 +88,7 @@ public void removeLectureUnit(@NotNull LectureUnit lectureUnit) { }).toList()); } - Lecture lecture = lectureRepository.findByIdWithLectureUnitsElseThrow(lectureUnitToDelete.getLecture().getId()); + Lecture lecture = lectureRepository.findByIdWithLectureUnitsAndAttachmentsElseThrow(lectureUnitToDelete.getLecture().getId()); // Creating a new list of lecture units without the one we want to remove List lectureUnitsUpdated = new ArrayList<>(); for (LectureUnit unit : lecture.getLectureUnits()) { diff --git a/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java b/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java index 2755d00e1d7c..38bdb868ad92 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java @@ -50,7 +50,7 @@ public class ParticipationService { private final TeamRepository teamRepository; - private final UrlService urlService; + private final UriService uriService; private final ResultService resultService; @@ -69,7 +69,7 @@ public class ParticipationService { public ParticipationService(GitService gitService, Optional continuousIntegrationService, Optional versionControlService, BuildLogEntryService buildLogEntryService, ParticipationRepository participationRepository, StudentParticipationRepository studentParticipationRepository, ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository, ProgrammingExerciseRepository programmingExerciseRepository, - SubmissionRepository submissionRepository, TeamRepository teamRepository, UrlService urlService, ResultService resultService, + SubmissionRepository submissionRepository, TeamRepository teamRepository, UriService uriService, ResultService resultService, CoverageReportRepository coverageReportRepository, BuildLogStatisticsEntryRepository buildLogStatisticsEntryRepository, ParticipantScoreRepository participantScoreRepository, StudentScoreRepository studentScoreRepository, TeamScoreRepository teamScoreRepository, Optional localCISharedBuildJobQueueService) { @@ -83,7 +83,7 @@ public ParticipationService(GitService gitService, Optional { +public class QuizPoolService extends QuizService implements ExamQuizQuestionsGenerator { private static final String ENTITY_NAME = "quizPool"; @@ -97,7 +101,7 @@ public QuizPool update(Long examId, QuizPool quizPool) { * @param examId the id of the exam to be searched * @return optional quiz pool that belongs to the given exam id */ - public Optional findWithQuizQuestionsByExamId(Long examId) { + public Optional findWithQuizGroupsAndQuestionsByExamId(Long examId) { Optional quizPoolOptional = quizPoolRepository.findWithEagerQuizQuestionsByExamId(examId); if (quizPoolOptional.isPresent()) { QuizPool quizPool = quizPoolOptional.get(); @@ -150,4 +154,48 @@ private void removeUnusedQuizGroup(List existingQuizGroupIds, List generateQuizQuestionsForExam(long examId) { + Optional quizPoolOptional = findWithQuizGroupsAndQuestionsByExamId(examId); + if (quizPoolOptional.isPresent()) { + QuizPool quizPool = quizPoolOptional.get(); + List quizGroups = quizPool.getQuizGroups(); + List quizQuestions = quizPool.getQuizQuestions(); + + Map> quizGroupQuestionsMap = getQuizQuestionsGroup(quizGroups, quizQuestions); + return generateQuizQuestions(quizGroupQuestionsMap, quizGroups, quizQuestions); + } + else { + return new ArrayList<>(); + } + } + + private static Map> getQuizQuestionsGroup(List quizGroups, List quizQuestions) { + Map> quizGroupQuestionsMap = new HashMap<>(); + for (QuizGroup quizGroup : quizGroups) { + quizGroupQuestionsMap.put(quizGroup.getId(), new ArrayList<>()); + } + for (QuizQuestion quizQuestion : quizQuestions) { + if (quizQuestion.getQuizGroup() != null) { + quizGroupQuestionsMap.get(quizQuestion.getQuizGroup().getId()).add(quizQuestion); + } + } + return quizGroupQuestionsMap; + } + + private static List generateQuizQuestions(Map> quizGroupQuestionsMap, List quizGroups, List quizQuestions) { + SecureRandom random = new SecureRandom(); + List results = new ArrayList<>(); + for (QuizGroup quizGroup : quizGroups) { + List quizGroupQuestions = quizGroupQuestionsMap.get(quizGroup.getId()); + results.add(quizGroupQuestions.get(random.nextInt(quizGroupQuestions.size()))); + } + for (QuizQuestion quizQuestion : quizQuestions) { + if (quizQuestion.getQuizGroup() == null) { + results.add(quizQuestion); + } + } + return results; + } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/RepositoryAccessService.java b/src/main/java/de/tum/in/www1/artemis/service/RepositoryAccessService.java index 4b0bc207736c..79bfde0362ff 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/RepositoryAccessService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/RepositoryAccessService.java @@ -57,8 +57,9 @@ public void checkAccessRepositoryElseThrow(ProgrammingExerciseParticipation prog // Error case 1: The user does not have permissions to push into the repository and the user is not notified for a related plagiarism case. boolean hasPermissions = participationAuthCheckService.canAccessParticipation(programmingParticipation, user); - boolean userWasNotifiedAboutPlagiarismCase = plagiarismService.wasUserNotifiedByInstructor(programmingParticipation.getId(), user.getLogin()); - if (!hasPermissions && !userWasNotifiedAboutPlagiarismCase) { + var exerciseDueDate = programmingExercise.isExamExercise() ? programmingExercise.getExerciseGroup().getExam().getEndDate() : programmingExercise.getDueDate(); + boolean hasAccessToSubmission = plagiarismService.hasAccessToSubmission(programmingParticipation.getId(), user.getLogin(), exerciseDueDate); + if (!hasPermissions && !hasAccessToSubmission) { throw new AccessUnauthorizedException(); } @@ -106,7 +107,7 @@ else if (programmingExercise.getSubmissionPolicy() instanceof LockRepositoryPoli // to read the student's repository. // But the student should still be able to access if they are notified for a related plagiarism case. if ((isStudent || (isTeachingAssistant && repositoryActionType != RepositoryActionType.READ)) - && !examSubmissionService.isAllowedToSubmitDuringExam(programmingExercise, user, false) && !userWasNotifiedAboutPlagiarismCase) { + && !examSubmissionService.isAllowedToSubmitDuringExam(programmingExercise, user, false) && !hasAccessToSubmission) { throw new AccessForbiddenException(); } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/RepositoryService.java b/src/main/java/de/tum/in/www1/artemis/service/RepositoryService.java index ef39326d10da..18fe3e038be8 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/RepositoryService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/RepositoryService.java @@ -351,25 +351,25 @@ public void commitChanges(Repository repository, User user) throws GitAPIExcepti /** * Retrieve the status of the repository. Also pulls the repository. * - * @param repositoryUrl of the repository to check the status for. + * @param repositoryUri of the repository to check the status for. * @return a dto to determine the status of the repository. * @throws GitAPIException if the repository status can't be retrieved. */ - public boolean isClean(VcsRepositoryUrl repositoryUrl) throws GitAPIException { - Repository repository = gitService.getOrCheckoutRepository(repositoryUrl, true); + public boolean isClean(VcsRepositoryUri repositoryUri) throws GitAPIException { + Repository repository = gitService.getOrCheckoutRepository(repositoryUri, true); return gitService.isClean(repository); } /** * Retrieve the status of the repository. Also pulls the repository. * - * @param repositoryUrl of the repository to check the status for. + * @param repositoryUri of the repository to check the status for. * @param defaultBranch the already used default branch in the remote repository * @return a dto to determine the status of the repository. * @throws GitAPIException if the repository status can't be retrieved. */ - public boolean isClean(VcsRepositoryUrl repositoryUrl, String defaultBranch) throws GitAPIException { - Repository repository = gitService.getOrCheckoutRepository(repositoryUrl, true, defaultBranch); + public boolean isClean(VcsRepositoryUri repositoryUri, String defaultBranch) throws GitAPIException { + Repository repository = gitService.getOrCheckoutRepository(repositoryUri, true, defaultBranch); return gitService.isClean(repository); } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/TeamService.java b/src/main/java/de/tum/in/www1/artemis/service/TeamService.java index 7e521d778229..49f3e19d640c 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/TeamService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/TeamService.java @@ -86,13 +86,13 @@ public void updateRepositoryMembersIfNeeded(Long exerciseId, Team existingTeam, // Users in the existing team that are no longer in the updated team need to be removed Set usersToRemove = new HashSet<>(existingTeam.getStudents()); usersToRemove.removeAll(updatedTeam.getStudents()); - usersToRemove.forEach(user -> versionControlService.orElseThrow().removeMemberFromRepository(participation.getVcsRepositoryUrl(), user)); + usersToRemove.forEach(user -> versionControlService.orElseThrow().removeMemberFromRepository(participation.getVcsRepositoryUri(), user)); // Users in the updated team that were not yet part of the existing team need to be added Set usersToAdd = new HashSet<>(updatedTeam.getStudents()); usersToAdd.removeAll(existingTeam.getStudents()); usersToAdd.forEach( - user -> versionControlService.orElseThrow().addMemberToRepository(participation.getVcsRepositoryUrl(), user, VersionControlRepositoryPermission.REPO_WRITE)); + user -> versionControlService.orElseThrow().addMemberToRepository(participation.getVcsRepositoryUri(), user, VersionControlRepositoryPermission.REPO_WRITE)); }); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/UrlService.java b/src/main/java/de/tum/in/www1/artemis/service/UriService.java similarity index 52% rename from src/main/java/de/tum/in/www1/artemis/service/UrlService.java rename to src/main/java/de/tum/in/www1/artemis/service/UriService.java index 28013aa2f141..fe625ffe3b8a 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/UrlService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/UriService.java @@ -7,58 +7,58 @@ import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; -import de.tum.in.www1.artemis.domain.VcsRepositoryUrl; +import de.tum.in.www1.artemis.domain.VcsRepositoryUri; import de.tum.in.www1.artemis.exception.VersionControlException; @Service -public class UrlService { +public class UriService { - private final Logger log = LoggerFactory.getLogger(UrlService.class); + private final Logger log = LoggerFactory.getLogger(UriService.class); /** - * Gets the repository slug from the given repository URL, see {@link #getRepositorySlugFromUrl} + * Gets the repository slug from the given repository URI, see {@link #getRepositorySlugFromUri} * - * @param repositoryUrl The repository url object + * @param repositoryUri The repository uri object * @return The repository slug - * @throws VersionControlException if the URL is invalid and no repository slug could be extracted + * @throws VersionControlException if the URI is invalid and no repository slug could be extracted */ - public String getRepositorySlugFromRepositoryUrl(VcsRepositoryUrl repositoryUrl) throws VersionControlException { - return getRepositorySlugFromUrl(repositoryUrl.getURI()); + public String getRepositorySlugFromRepositoryUri(VcsRepositoryUri repositoryUri) throws VersionControlException { + return getRepositorySlugFromUri(repositoryUri.getURI()); } /** - * Gets the repository slug from the given repository URL string, see {@link #getRepositorySlugFromUrl} + * Gets the repository slug from the given repository URI string, see {@link #getRepositorySlugFromUri} * - * @param repositoryUrl The repository url as string + * @param repositoryUri The repository uri as string * @return The repository slug - * @throws VersionControlException if the URL is invalid and no repository slug could be extracted + * @throws VersionControlException if the URI is invalid and no repository slug could be extracted */ - public String getRepositorySlugFromRepositoryUrlString(String repositoryUrl) throws VersionControlException { + public String getRepositorySlugFromRepositoryUriString(String repositoryUri) throws VersionControlException { try { - return getRepositorySlugFromUrl(new URI(repositoryUrl)); + return getRepositorySlugFromUri(new URI(repositoryUri)); } catch (URISyntaxException e) { - log.error("Cannot get repository slug from repository url string {}", repositoryUrl, e); - throw new VersionControlException("Repository URL is not a git URL! Can't get repository slug for " + repositoryUrl); + log.error("Cannot get repository slug from repository uri string {}", repositoryUri, e); + throw new VersionControlException("Repository URI is not a git URI! Can't get repository slug for " + repositoryUri); } } /** - * Gets the repository slug from the given URL + * Gets the repository slug from the given URI * Example 1: https://ga42xab@bitbucket.ase.in.tum.de/scm/EIST2016RME/RMEXERCISE-ga42xab.git --> RMEXERCISE-ga42xab * Example 2: https://ga63fup@repobruegge.in.tum.de/scm/EIST2016RME/RMEXERCISE-ga63fup.git --> RMEXERCISE-ga63fup * Example 3: https://artemistest2gitlab.ase.in.tum.de/TESTADAPTER/testadapter-exercise.git --> testadapter-exercise * Example 4: https://turdiu@artemistest2gitlab.ase.in.tum.de/FTCSCAGRADING1/ftcscagrading1-turdiu.git --> ftcscagrading1-turdiu * - * @param url The complete repository url (including protocol, host and the complete path) - * @return The repository slug, i.e. the part of the url that identifies the repository (not the project) without .git in the end - * @throws VersionControlException if the URL is invalid and no repository slug could be extracted + * @param uri The complete repository uri (including protocol, host and the complete path) + * @return The repository slug, i.e. the part of the uri that identifies the repository (not the project) without .git in the end + * @throws VersionControlException if the URI is invalid and no repository slug could be extracted */ - private String getRepositorySlugFromUrl(URI url) throws VersionControlException { - // split the URL path in components using the separator "/" - final var pathComponents = url.getPath().split("/"); + private String getRepositorySlugFromUri(URI uri) throws VersionControlException { + // split the URI path in components using the separator "/" + final var pathComponents = uri.getPath().split("/"); if (pathComponents.length < 2) { - throw new VersionControlException("Repository URL is not a git URL! Can't get repository slug for " + url); + throw new VersionControlException("Repository URI is not a git URI! Can't get repository slug for " + uri); } // Note: pathComponents[0] = "" because the path always starts with "/" // take the last element @@ -72,30 +72,30 @@ private String getRepositorySlugFromUrl(URI url) throws VersionControlException } /** - * Gets the project key + repository slug from the given repository URL, ee {@link #getRepositoryPathFromUrl} + * Gets the project key + repository slug from the given repository URI, ee {@link #getRepositoryPathFromUri} * - * @param repositoryUrl The repository url object + * @param repositoryUri The repository uri object * @return / - * @throws VersionControlException if the URL is invalid and no project key could be extracted + * @throws VersionControlException if the URI is invalid and no project key could be extracted */ - public String getRepositoryPathFromRepositoryUrl(VcsRepositoryUrl repositoryUrl) throws VersionControlException { - return getRepositoryPathFromUrl(repositoryUrl.getURI()); + public String getRepositoryPathFromRepositoryUri(VcsRepositoryUri repositoryUri) throws VersionControlException { + return getRepositoryPathFromUri(repositoryUri.getURI()); } /** - * Gets the project key + repository slug from the given URL - * + * Gets the project key + repository slug from the given URI + *

* Example: https://artemistest2gitlab.ase.in.tum.de/TESTADAPTER/testadapter-exercise.git --> TESTADAPTER/testadapter-exercise * - * @param url The complete repository url (including protocol, host and the complete path) + * @param uri The complete repository uri (including protocol, host and the complete path) * @return / - * @throws VersionControlException if the URL is invalid and no project key could be extracted + * @throws VersionControlException if the URI is invalid and no project key could be extracted */ - private String getRepositoryPathFromUrl(URI url) throws VersionControlException { - // split the URL path in components using the separator "/" - final var pathComponents = url.getPath().split("/"); + private String getRepositoryPathFromUri(URI uri) throws VersionControlException { + // split the URI path in components using the separator "/" + final var pathComponents = uri.getPath().split("/"); if (pathComponents.length < 2) { - throw new VersionControlException("Repository URL is not a git URL! Can't get repository slug for " + url); + throw new VersionControlException("Repository URI is not a git URI! Can't get repository slug for " + uri); } // Note: pathComponents[0] = "" because the path always starts with "/" final var last = pathComponents.length - 1; @@ -103,32 +103,32 @@ private String getRepositoryPathFromUrl(URI url) throws VersionControlException } /** - * Gets the project key from the given repository URL, see {@link #getProjectKeyFromUrl} + * Gets the project key from the given repository URI, see {@link #getProjectKeyFromUri} * - * @param repositoryUrl The repository url object + * @param repositoryUri The repository uri object * @return The project key - * @throws VersionControlException if the URL is invalid and no project key could be extracted + * @throws VersionControlException if the URI is invalid and no project key could be extracted */ - public String getProjectKeyFromRepositoryUrl(VcsRepositoryUrl repositoryUrl) throws VersionControlException { - return getProjectKeyFromUrl(repositoryUrl.getURI()); + public String getProjectKeyFromRepositoryUri(VcsRepositoryUri repositoryUri) throws VersionControlException { + return getProjectKeyFromUri(repositoryUri.getURI()); } /** - * Gets the project key from the given URL + * Gets the project key from the given URI * * Examples: * https://ga42xab@bitbucket.ase.in.tum.de/scm/EIST2016RME/RMEXERCISE-ga42xab.git --> EIST2016RME * http://localhost:8080/git/TESTCOURSE1TESTEX1/testcourse1testex1-student1.git --> TESTCOURSE1TESTEX1 * - * @param url The complete repository url (including protocol, host and the complete path) + * @param uri The complete repository uri (including protocol, host and the complete path) * @return The project key - * @throws VersionControlException if the URL is invalid and no project key could be extracted + * @throws VersionControlException if the URI is invalid and no project key could be extracted */ - private String getProjectKeyFromUrl(URI url) throws VersionControlException { - // split the URL path in components using the separator "/" - final var pathComponents = url.getPath().split("/"); + private String getProjectKeyFromUri(URI uri) throws VersionControlException { + // split the URI path in components using the separator "/" + final var pathComponents = uri.getPath().split("/"); if (pathComponents.length <= 2) { - throw new VersionControlException("No project key could be found for " + url); + throw new VersionControlException("No project key could be found for " + uri); } // Note: pathComponents[0] = "" because the path always starts with "/" var projectKey = pathComponents[1]; @@ -140,15 +140,15 @@ private String getProjectKeyFromUrl(URI url) throws VersionControlException { } /** - * Gets the plain URL from the given repository URL, + * Gets the plain URI from the given repository URI, * https://ga42xab@bitbucket.ase.in.tum.de/scm/EIST2016RME/RMEXERCISE-ga42xab.git --> https://bitbucket.ase.in.tum.de/scm/EIST2016RME/RMEXERCISE-ga42xab.git * - * @param repositoryUrl The repository url object - * @return The plain URL - * @throws VersionControlException if the URL is invalid and no plain URL could be extracted + * @param repositoryUri The repository uri object + * @return The plain URI + * @throws VersionControlException if the URI is invalid and no plain URI could be extracted */ - public String getPlainUrlFromRepositoryUrl(VcsRepositoryUrl repositoryUrl) throws VersionControlException { - var uri = repositoryUrl.getURI(); + public String getPlainUriFromRepositoryUri(VcsRepositoryUri repositoryUri) throws VersionControlException { + var uri = repositoryUri.getURI(); try { var updatedUri = new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(), null, uri.getFragment()); return updatedUri.toString(); 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 01a406c909f5..1925b33a327a 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 @@ -54,7 +54,7 @@ import de.tum.in.www1.artemis.service.FileService; import de.tum.in.www1.artemis.service.ProfileService; import de.tum.in.www1.artemis.service.ZipFileService; -import de.tum.in.www1.artemis.service.connectors.localvc.LocalVCRepositoryUrl; +import de.tum.in.www1.artemis.service.connectors.localvc.LocalVCRepositoryUri; import de.tum.in.www1.artemis.web.rest.dto.CommitInfoDTO; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; @@ -254,37 +254,37 @@ private boolean useSsh() { // password is optional and will only be applied if the ssh private key was encrypted using a password } - private String getGitUriAsString(VcsRepositoryUrl vcsRepositoryUrl) throws URISyntaxException { - return getGitUri(vcsRepositoryUrl).toString(); + private String getGitUriAsString(VcsRepositoryUri vcsRepositoryUri) throws URISyntaxException { + return getGitUri(vcsRepositoryUri).toString(); } /** - * Get the URI for a {@link VcsRepositoryUrl}. This either retrieves the SSH URI, if SSH is used, the HTTP(S) URI, or the path to the repository's folder if the local VCS is + * Get the URI for a {@link VcsRepositoryUri}. This either retrieves the SSH URI, if SSH is used, the HTTP(S) URI, or the path to the repository's folder if the local VCS is * used. * This method is for internal use (getting the URI for cloning the repository into the Artemis file system). * For Bitbucket and GitLab, the URI is the same internally as the one that is used by the students to clone the repository using their local Git client. * For the local VCS however, the repository is cloned from the folder defined in the environment variable "artemis.version-control.local-vcs-repo-path". * - * @param vcsRepositoryUrl the {@link VcsRepositoryUrl} for which to get the URI + * @param vcsRepositoryUri the {@link VcsRepositoryUri} for which to get the URI * @return the URI (SSH, HTTP(S), or local path) * @throws URISyntaxException if SSH is used and the SSH URI could not be retrieved. */ - private URI getGitUri(VcsRepositoryUrl vcsRepositoryUrl) throws URISyntaxException { + private URI getGitUri(VcsRepositoryUri vcsRepositoryUri) throws URISyntaxException { if (profileService.isLocalVcsCi()) { - // Create less generic LocalVCRepositoryUrl out of VcsRepositoryUrl. - LocalVCRepositoryUrl localVCRepositoryUrl = new LocalVCRepositoryUrl(vcsRepositoryUrl.toString(), gitUrl); + // Create less generic LocalVCRepositoryUri out of VcsRepositoryUri. + LocalVCRepositoryUri localVCRepositoryUri = new LocalVCRepositoryUri(vcsRepositoryUri.toString(), gitUrl); String localVCBasePath = environment.getProperty("artemis.version-control.local-vcs-repo-path"); - return localVCRepositoryUrl.getLocalRepositoryPath(localVCBasePath).toUri(); + return localVCRepositoryUri.getLocalRepositoryPath(localVCBasePath).toUri(); } - return useSsh() ? getSshUri(vcsRepositoryUrl) : vcsRepositoryUrl.getURI(); + return useSsh() ? getSshUri(vcsRepositoryUri) : vcsRepositoryUri.getURI(); } - private URI getSshUri(VcsRepositoryUrl vcsRepositoryUrl) throws URISyntaxException { + private URI getSshUri(VcsRepositoryUri vcsRepositoryUri) throws URISyntaxException { URI templateUri = new URI(sshUrlTemplate.orElseThrow()); // Example Bitbucket: ssh://git@bitbucket.ase.in.tum.de:7999/se2021w07h02/se2021w07h02-ga27yox.git // Example Gitlab: ssh://git@gitlab.ase.in.tum.de:2222/se2021w07h02/se2021w07h02-ga27yox.git - final var repositoryUri = vcsRepositoryUrl.getURI(); - // Bitbucket repository urls (until now mainly used with username and password authentication) include "/scm" in the url, which cannot be used in ssh urls, + final var repositoryUri = vcsRepositoryUri.getURI(); + // Bitbucket repository uris (until now mainly used with username and password authentication) include "/scm" in the url, which cannot be used in ssh urls, // therefore we need to replace it here final var path = repositoryUri.getPath().replace("/scm", ""); return new URI(templateUri.getScheme(), templateUri.getUserInfo(), templateUri.getHost(), templateUri.getPort(), path, null, repositoryUri.getFragment()); @@ -313,8 +313,8 @@ public Repository getOrCheckoutRepository(ProgrammingExerciseParticipation parti * @throws GitException if the same repository is attempted to be cloned multiple times. */ public Repository getOrCheckoutRepository(ProgrammingExerciseParticipation participation, String targetPath) throws GitAPIException, GitException { - var repoUrl = participation.getVcsRepositoryUrl(); - Repository repository = getOrCheckoutRepository(repoUrl, targetPath, true); + var repoUri = participation.getVcsRepositoryUri(); + Repository repository = getOrCheckoutRepository(repoUri, targetPath, true); repository.setParticipation(participation); return repository; } @@ -333,46 +333,46 @@ public Repository getOrCheckoutRepository(ProgrammingExerciseParticipation parti * @throws InvalidPathException if the repository could not be checked out Because it contains unmappable characters. */ public Repository getOrCheckoutRepositoryForJPlag(ProgrammingExerciseParticipation participation, String targetPath) throws GitAPIException, InvalidPathException { - var repoUrl = participation.getVcsRepositoryUrl(); - String repoFolderName = repoUrl.folderNameForRepositoryUrl(); + var repoUri = participation.getVcsRepositoryUri(); + String repoFolderName = repoUri.folderNameForRepositoryUri(); // Replace the exercise name in the repository folder name with the participation ID. // This is necessary to be able to refer back to the correct participation after the JPlag detection run. String updatedRepoFolderName = repoFolderName.replaceAll("/[a-zA-Z0-9]*-", "/" + participation.getId() + "-"); Path localPath = Path.of(targetPath, updatedRepoFolderName); - Repository repository = getOrCheckoutRepository(repoUrl, localPath, true); + Repository repository = getOrCheckoutRepository(repoUri, localPath, true); repository.setParticipation(participation); return repository; } /** - * Get the local repository for a given remote repository URL. If the local repo does not exist yet, it will be checked out. + * Get the local repository for a given remote repository URI. If the local repo does not exist yet, it will be checked out. * Saves the repo in the default path * - * @param repoUrl The remote repository. + * @param repoUri The remote repository. * @param pullOnGet Pull from the remote on the checked out repository, if it does not need to be cloned. * @return the repository if it could be checked out. * @throws GitAPIException if the repository could not be checked out. */ - public Repository getOrCheckoutRepository(VcsRepositoryUrl repoUrl, boolean pullOnGet) throws GitAPIException { - return getOrCheckoutRepository(repoUrl, repoClonePath, pullOnGet); + public Repository getOrCheckoutRepository(VcsRepositoryUri repoUri, boolean pullOnGet) throws GitAPIException { + return getOrCheckoutRepository(repoUri, repoClonePath, pullOnGet); } /** - * Get the local repository for a given remote repository URL. If the local repo does not exist yet, it will be checked out. + * Get the local repository for a given remote repository URI. If the local repo does not exist yet, it will be checked out. * - * @param repoUrl The remote repository. + * @param repoUri The remote repository. * @param targetPath path where the repo is located on disk * @param pullOnGet Pull from the remote on the checked out repository, if it does not need to be cloned. * @return the repository if it could be checked out. * @throws GitAPIException if the repository could not be checked out. * @throws GitException if the same repository is attempted to be cloned multiple times. */ - public Repository getOrCheckoutRepository(VcsRepositoryUrl repoUrl, String targetPath, boolean pullOnGet) throws GitAPIException, GitException { - Path localPath = getLocalPathOfRepo(targetPath, repoUrl); - return getOrCheckoutRepository(repoUrl, localPath, pullOnGet); + public Repository getOrCheckoutRepository(VcsRepositoryUri repoUri, String targetPath, boolean pullOnGet) throws GitAPIException, GitException { + Path localPath = getLocalPathOfRepo(targetPath, repoUri); + return getOrCheckoutRepository(repoUri, localPath, pullOnGet); } /** @@ -393,52 +393,52 @@ public Repository checkoutRepositoryAtCommit(Repository repository, String commi } /** - * Get the local repository for a given remote repository URL. + * Get the local repository for a given remote repository URI. *

* If the local repo does not exist yet, it will be checked out. * After retrieving the repository, the commit for the given hash will be checked out. * - * @param vcsRepositoryUrl the url of the remote repository + * @param vcsRepositoryUri the url of the remote repository * @param commitHash the hash of the commit to checkout * @param pullOnGet pull from the remote on the checked out repository, if it does not need to be cloned * @return the repository if it could be checked out * @throws GitAPIException if the repository could not be checked out */ - public Repository checkoutRepositoryAtCommit(VcsRepositoryUrl vcsRepositoryUrl, String commitHash, boolean pullOnGet) throws GitAPIException { - var repository = getOrCheckoutRepository(vcsRepositoryUrl, pullOnGet); + public Repository checkoutRepositoryAtCommit(VcsRepositoryUri vcsRepositoryUri, String commitHash, boolean pullOnGet) throws GitAPIException { + var repository = getOrCheckoutRepository(vcsRepositoryUri, pullOnGet); return checkoutRepositoryAtCommit(repository, commitHash); } /** - * Get the local repository for a given remote repository URL. If the local repo does not exist yet, it will be checked out. + * Get the local repository for a given remote repository URI. If the local repo does not exist yet, it will be checked out. * - * @param repoUrl The remote repository. + * @param repoUri The remote repository. * @param pullOnGet Pull from the remote on the checked out repository, if it does not need to be cloned. * @param defaultBranch The default branch of the target repository. * @return the repository if it could be checked out. * @throws GitAPIException if the repository could not be checked out. * @throws GitException if the same repository is attempted to be cloned multiple times. */ - public Repository getOrCheckoutRepository(VcsRepositoryUrl repoUrl, boolean pullOnGet, String defaultBranch) throws GitAPIException, GitException { - Path localPath = getLocalPathOfRepo(repoClonePath, repoUrl); - return getOrCheckoutRepository(repoUrl, repoUrl, localPath, pullOnGet, defaultBranch); + public Repository getOrCheckoutRepository(VcsRepositoryUri repoUri, boolean pullOnGet, String defaultBranch) throws GitAPIException, GitException { + Path localPath = getLocalPathOfRepo(repoClonePath, repoUri); + return getOrCheckoutRepository(repoUri, repoUri, localPath, pullOnGet, defaultBranch); } - public Repository getOrCheckoutRepositoryIntoTargetDirectory(VcsRepositoryUrl repoUrl, VcsRepositoryUrl targetUrl, boolean pullOnGet) + public Repository getOrCheckoutRepositoryIntoTargetDirectory(VcsRepositoryUri repoUri, VcsRepositoryUri targetUrl, boolean pullOnGet) throws GitAPIException, GitException, InvalidPathException { Path localPath = getDefaultLocalPathOfRepo(targetUrl); - return getOrCheckoutRepository(repoUrl, targetUrl, localPath, pullOnGet); + return getOrCheckoutRepository(repoUri, targetUrl, localPath, pullOnGet); } - public Repository getOrCheckoutRepository(VcsRepositoryUrl repoUrl, Path localPath, boolean pullOnGet) throws GitAPIException, GitException, InvalidPathException { - return getOrCheckoutRepository(repoUrl, repoUrl, localPath, pullOnGet); + public Repository getOrCheckoutRepository(VcsRepositoryUri repoUri, Path localPath, boolean pullOnGet) throws GitAPIException, GitException, InvalidPathException { + return getOrCheckoutRepository(repoUri, repoUri, localPath, pullOnGet); } /** - * Get the local repository for a given remote repository URL. If the local repo does not exist yet, it will be checked out. + * Get the local repository for a given remote repository URI. If the local repo does not exist yet, it will be checked out. * - * @param sourceRepoUrl The source remote repository. - * @param targetRepoUrl The target remote repository. + * @param sourceRepoUri The source remote repository. + * @param targetRepoUri The target remote repository. * @param localPath The local path to clone the repository to. * @param pullOnGet Pull from the remote on the checked out repository, if it does not need to be cloned. * @return the repository if it could be checked out. @@ -446,16 +446,16 @@ public Repository getOrCheckoutRepository(VcsRepositoryUrl repoUrl, Path localPa * @throws GitException if the same repository is attempted to be cloned multiple times. * @throws InvalidPathException if the repository could not be checked out Because it contains unmappable characters. */ - public Repository getOrCheckoutRepository(VcsRepositoryUrl sourceRepoUrl, VcsRepositoryUrl targetRepoUrl, Path localPath, boolean pullOnGet) + public Repository getOrCheckoutRepository(VcsRepositoryUri sourceRepoUri, VcsRepositoryUri targetRepoUri, Path localPath, boolean pullOnGet) throws GitAPIException, GitException, InvalidPathException { - return getOrCheckoutRepository(sourceRepoUrl, targetRepoUrl, localPath, pullOnGet, defaultBranch); + return getOrCheckoutRepository(sourceRepoUri, targetRepoUri, localPath, pullOnGet, defaultBranch); } /** - * Get the local repository for a given remote repository URL. If the local repo does not exist yet, it will be checked out. + * Get the local repository for a given remote repository URI. If the local repo does not exist yet, it will be checked out. * - * @param sourceRepoUrl The source remote repository. - * @param targetRepoUrl The target remote repository. + * @param sourceRepoUri The source remote repository. + * @param targetRepoUri The target remote repository. * @param localPath The local path to clone the repository to. * @param pullOnGet Pull from the remote on the checked out repository, if it does not need to be cloned. * @param defaultBranch The default branch of the target repository @@ -464,11 +464,11 @@ public Repository getOrCheckoutRepository(VcsRepositoryUrl sourceRepoUrl, VcsRep * @throws GitException if the same repository is attempted to be cloned multiple times. * @throws InvalidPathException if the repository could not be checked out Because it contains unmappable characters. */ - public Repository getOrCheckoutRepository(VcsRepositoryUrl sourceRepoUrl, VcsRepositoryUrl targetRepoUrl, Path localPath, boolean pullOnGet, String defaultBranch) + public Repository getOrCheckoutRepository(VcsRepositoryUri sourceRepoUri, VcsRepositoryUri targetRepoUri, Path localPath, boolean pullOnGet, String defaultBranch) throws GitAPIException, GitException, InvalidPathException { // First try to just retrieve the git repository from our server, as it might already be checked out. - // If the sourceRepoUrl differs from the targetRepoUrl, we attempt to clone the source repo into the target directory - Repository repository = getExistingCheckedOutRepositoryByLocalPath(localPath, targetRepoUrl, defaultBranch); + // If the sourceRepoUri differs from the targetRepoUri, we attempt to clone the source repo into the target directory + Repository repository = getExistingCheckedOutRepositoryByLocalPath(localPath, targetRepoUri, defaultBranch); // Note: in case the actual git repository in the file system is corrupt (e.g. by accident), we will get an exception here // the exception will then delete the folder, so that the next attempt would be successful. @@ -495,7 +495,7 @@ public Repository getOrCheckoutRepository(VcsRepositoryUrl sourceRepoUrl, VcsRep // Clone repository. try { - var gitUriAsString = getGitUriAsString(sourceRepoUrl); + var gitUriAsString = getGitUriAsString(sourceRepoUri); log.debug("Cloning from {} to {}", gitUriAsString, localPath); cloneInProgressOperations.put(localPath, localPath); // make sure the directory to copy into is empty @@ -515,7 +515,7 @@ public Repository getOrCheckoutRepository(VcsRepositoryUrl sourceRepoUrl, VcsRep // make sure that cloneInProgress is released cloneInProgressOperations.remove(localPath); } - return getExistingCheckedOutRepositoryByLocalPath(localPath, targetRepoUrl, defaultBranch); + return getExistingCheckedOutRepositoryByLocalPath(localPath, targetRepoUri, defaultBranch); } } @@ -553,11 +553,11 @@ private void waitUntilPathNotBusy(final Path localPath) throws CanceledException * Checks whether the repository is cached. * This method does only support repositories that use the repoClonePath which is set in the application-artemis.yml file! * - * @param repositoryUrl the url of the repository + * @param repositoryUri the url of the repository * @return returns true if the repository is already cached */ - public boolean isRepositoryCached(VcsRepositoryUrl repositoryUrl) { - Path localPath = getLocalPathOfRepo(repoClonePath, repositoryUrl); + public boolean isRepositoryCached(VcsRepositoryUri repositoryUri) { + Path localPath = getLocalPathOfRepo(repoClonePath, repositoryUri); if (localPath == null) { return false; } @@ -568,15 +568,15 @@ public boolean isRepositoryCached(VcsRepositoryUrl repositoryUrl) { /** * Combine all commits of the given repository into one. * - * @param repoUrl of the repository to combine. + * @param repoUri of the repository to combine. * @throws GitAPIException If the checkout fails */ - public void combineAllCommitsOfRepositoryIntoOne(VcsRepositoryUrl repoUrl) throws GitAPIException { - Repository exerciseRepository = getOrCheckoutRepository(repoUrl, true); + public void combineAllCommitsOfRepositoryIntoOne(VcsRepositoryUri repoUri) throws GitAPIException { + Repository exerciseRepository = getOrCheckoutRepository(repoUri, true); combineAllCommitsIntoInitialCommit(exerciseRepository); } - public Path getDefaultLocalPathOfRepo(VcsRepositoryUrl targetUrl) { + public Path getDefaultLocalPathOfRepo(VcsRepositoryUri targetUrl) { return getLocalPathOfRepo(repoClonePath, targetUrl); } @@ -587,11 +587,11 @@ public Path getDefaultLocalPathOfRepo(VcsRepositoryUrl targetUrl) { * @param targetUrl url of the repository * @return path of the local file system */ - public Path getLocalPathOfRepo(String targetPath, VcsRepositoryUrl targetUrl) { + public Path getLocalPathOfRepo(String targetPath, VcsRepositoryUri targetUrl) { if (targetUrl == null) { return null; } - return Path.of(targetPath.replaceAll("^\\." + Pattern.quote(java.io.File.separator), ""), targetUrl.folderNameForRepositoryUrl()); + return Path.of(targetPath.replaceAll("^\\." + Pattern.quote(java.io.File.separator), ""), targetUrl.folderNameForRepositoryUri()); } /** @@ -599,11 +599,11 @@ public Path getLocalPathOfRepo(String targetPath, VcsRepositoryUrl targetUrl) { * from cachedRepositories. Side effect: This method caches retrieved repositories in a HashMap, so continuous retrievals can be avoided (reduces load). * * @param localPath to git repo on server. - * @param remoteRepositoryUrl the remote repository url for the git repository, will be added to the Repository object for later use, can be null + * @param remoteRepositoryUri the remote repository uri for the git repository, will be added to the Repository object for later use, can be null * @return the git repository in the localPath or **null** if it does not exist on the server. */ - public Repository getExistingCheckedOutRepositoryByLocalPath(@NotNull Path localPath, @Nullable VcsRepositoryUrl remoteRepositoryUrl) { - return getExistingCheckedOutRepositoryByLocalPath(localPath, remoteRepositoryUrl, defaultBranch); + public Repository getExistingCheckedOutRepositoryByLocalPath(@NotNull Path localPath, @Nullable VcsRepositoryUri remoteRepositoryUri) { + return getExistingCheckedOutRepositoryByLocalPath(localPath, remoteRepositoryUri, defaultBranch); } /** @@ -611,11 +611,11 @@ public Repository getExistingCheckedOutRepositoryByLocalPath(@NotNull Path local * from cachedRepositories. Side effect: This method caches retrieved repositories in a HashMap, so continuous retrievals can be avoided (reduces load). * * @param localPath to git repo on server. - * @param remoteRepositoryUrl the remote repository url for the git repository, will be added to the Repository object for later use, can be null + * @param remoteRepositoryUri the remote repository uri for the git repository, will be added to the Repository object for later use, can be null * @param defaultBranch the name of the branch that should be used as default branch * @return the git repository in the localPath or **null** if it does not exist on the server. */ - public Repository getExistingCheckedOutRepositoryByLocalPath(@NotNull Path localPath, @Nullable VcsRepositoryUrl remoteRepositoryUrl, String defaultBranch) { + public Repository getExistingCheckedOutRepositoryByLocalPath(@NotNull Path localPath, @Nullable VcsRepositoryUri remoteRepositoryUri, String defaultBranch) { try { // Check if there is a folder with the provided path of the git repository. if (!Files.exists(localPath)) { @@ -636,7 +636,7 @@ public Repository getExistingCheckedOutRepositoryByLocalPath(@NotNull Path local FileRepositoryBuilder builder = new FileRepositoryBuilder(); builder.setGitDir(gitPath.toFile()).setInitialBranch(defaultBranch).readEnvironment().findGitDir().setup(); // scan environment GIT_* variables - Repository repository = createRepository(localPath, remoteRepositoryUrl, defaultBranch, builder); + Repository repository = createRepository(localPath, remoteRepositoryUri, defaultBranch, builder); RefUpdate refUpdate = repository.getRefDatabase().newUpdate(Constants.HEAD, false); refUpdate.setForceUpdate(true); @@ -656,16 +656,16 @@ public Repository getExistingCheckedOutRepositoryByLocalPath(@NotNull Path local * Creates a new Repository with the given parameters and saves the Repository's StoredConfig. * * @param localPath The local path of the repository. - * @param remoteRepositoryUrl The remote repository url for the git repository. + * @param remoteRepositoryUri The remote repository uri for the git repository. * @param defaultBranch The default branch of the repository. * @param builder The FileRepositoryBuilder. * @return The created Repository. * @throws IOException if the configuration file cannot be accessed. */ @NotNull - private static Repository createRepository(Path localPath, VcsRepositoryUrl remoteRepositoryUrl, String defaultBranch, FileRepositoryBuilder builder) throws IOException { + private static Repository createRepository(Path localPath, VcsRepositoryUri remoteRepositoryUri, String defaultBranch, FileRepositoryBuilder builder) throws IOException { // Create the JGit repository object - Repository repository = new Repository(builder, localPath, remoteRepositoryUrl); + Repository repository = new Repository(builder, localPath, remoteRepositoryUri); // disable auto garbage collection because it can lead to problems (especially with deleting local repositories) // see https://stackoverflow.com/questions/45266021/java-jgit-files-delete-fails-to-delete-a-file-but-file-delete-succeeds // and https://git-scm.com/docs/git-gc for an explanation of the parameter @@ -737,15 +737,15 @@ public void commitAndPush(Repository repo, String message, boolean emptyCommit, * The content to be copied then gets pushed to the new repo. * * @param targetRepo Local target repo - * @param targetRepoUrl URI of targets repo + * @param targetRepoUri URI of targets repo * @param oldBranch default branch that was used when the exercise was created (might differ from the default branch of a participation) * @throws GitAPIException if the repo could not be pushed */ - public void pushSourceToTargetRepo(Repository targetRepo, VcsRepositoryUrl targetRepoUrl, String oldBranch) throws GitAPIException { + public void pushSourceToTargetRepo(Repository targetRepo, VcsRepositoryUri targetRepoUri, String oldBranch) throws GitAPIException { try (Git git = new Git(targetRepo)) { // overwrite the old remote uri with the target uri - git.remoteSetUrl().setRemoteName(REMOTE_NAME).setRemoteUri(new URIish(getGitUriAsString(targetRepoUrl))).call(); - log.debug("pushSourceToTargetRepo -> Push {}", targetRepoUrl.getURI()); + git.remoteSetUrl().setRemoteName(REMOTE_NAME).setRemoteUri(new URIish(getGitUriAsString(targetRepoUri))).call(); + log.debug("pushSourceToTargetRepo -> Push {}", targetRepoUri.getURI()); if (!defaultBranch.equals(oldBranch)) { targetRepo.getConfig().unsetSection(ConfigConstants.CONFIG_BRANCH_SECTION, oldBranch); @@ -804,19 +804,19 @@ public void fetchAll(Repository repo) throws GitAPIException { } /** - * Change the remote repository url to the currently used authentication mechanism (either ssh or https) + * Change the remote repository uri to the currently used authentication mechanism (either ssh or https) * * @param repo the git repository for which the remote url should be change */ private void setRemoteUrl(Repository repo) { - if (repo == null || repo.getRemoteRepositoryUrl() == null) { + if (repo == null || repo.getRemoteRepositoryUri() == null) { log.warn("Cannot set remoteUrl because it is null!"); return; } // Note: we reset the remote url, because it might have changed from https to ssh or ssh to https try { var existingRemoteUrl = repo.getConfig().getString(ConfigConstants.CONFIG_REMOTE_SECTION, REMOTE_NAME, "url"); - var newRemoteUrl = getGitUriAsString(repo.getRemoteRepositoryUrl()); + var newRemoteUrl = getGitUriAsString(repo.getRemoteRepositoryUri()); if (!Objects.equals(newRemoteUrl, existingRemoteUrl)) { log.info("Replace existing remote url {} with new remote url {}", existingRemoteUrl, newRemoteUrl); repo.getConfig().setString(ConfigConstants.CONFIG_REMOTE_SECTION, REMOTE_NAME, "url", newRemoteUrl); @@ -903,7 +903,7 @@ public void resetToOriginHead(Repository repo) { reset(repo, "origin/" + originHead); } catch (GitAPIException | JGitInternalException ex) { - log.error("Cannot fetch/hard reset the repo {} with url {} to origin/HEAD due to the following exception", repo.getLocalPath(), repo.getRemoteRepositoryUrl(), ex); + log.error("Cannot fetch/hard reset the repo {} with url {} to origin/HEAD due to the following exception", repo.getLocalPath(), repo.getRemoteRepositoryUri(), ex); } } @@ -922,18 +922,18 @@ public void switchBackToDefaultBranchHead(Repository repository) throws GitAPIEx /** * Get last commit hash from HEAD * - * @param repoUrl to get the latest hash from. + * @param repoUri to get the latest hash from. * @return the latestHash of the given repo. * @throws EntityNotFoundException if retrieving the latestHash from the git repo failed. */ - public ObjectId getLastCommitHash(VcsRepositoryUrl repoUrl) throws EntityNotFoundException { - if (repoUrl == null || repoUrl.getURI() == null) { + public ObjectId getLastCommitHash(VcsRepositoryUri repoUri) throws EntityNotFoundException { + if (repoUri == null || repoUri.getURI() == null) { return null; } // Get HEAD ref of repo without cloning it locally try { - log.debug("getLastCommitHash {}", repoUrl); - var headRef = lsRemoteCommand().setRemote(getGitUriAsString(repoUrl)).callAsMap().get(Constants.HEAD); + log.debug("getLastCommitHash {}", repoUri); + var headRef = lsRemoteCommand().setRemote(getGitUriAsString(repoUri)).callAsMap().get(Constants.HEAD); if (headRef == null) { return null; @@ -942,7 +942,7 @@ public ObjectId getLastCommitHash(VcsRepositoryUrl repoUrl) throws EntityNotFoun return headRef.getObjectId(); } catch (GitAPIException | URISyntaxException ex) { - throw new EntityNotFoundException("Could not retrieve the last commit hash for repoUrl " + repoUrl + " due to the following exception: " + ex); + throw new EntityNotFoundException("Could not retrieve the last commit hash for repoUri " + repoUri + " due to the following exception: " + ex); } } @@ -1002,7 +1002,7 @@ public void combineAllStudentCommits(Repository repository, ProgrammingExercise try (Git studentGit = new Git(repository)) { setRemoteUrl(repository); // Get last commit hash from template repo - ObjectId latestHash = getLastCommitHash(programmingExercise.getVcsTemplateRepositoryUrl()); + ObjectId latestHash = getLastCommitHash(programmingExercise.getVcsTemplateRepositoryUri()); if (latestHash == null) { // Template Repository is somehow empty. Should never happen @@ -1050,7 +1050,7 @@ public void anonymizeStudentCommits(Repository repository, ProgrammingExercise p String headName = "HEAD"; // Get last commit hash from template repo - ObjectId latestHash = getLastCommitHash(programmingExercise.getVcsTemplateRepositoryUrl()); + ObjectId latestHash = getLastCommitHash(programmingExercise.getVcsTemplateRepositoryUri()); if (latestHash == null) { // Template Repository is somehow empty. Should never happen @@ -1267,15 +1267,15 @@ public void deleteLocalRepository(Repository repository) throws IOException { } /** - * Deletes a local repository folder for a repoUrl. + * Deletes a local repository folder for a repoUri. * - * @param repoUrl url of the repository. + * @param repoUri url of the repository. */ - public void deleteLocalRepository(VcsRepositoryUrl repoUrl) { + public void deleteLocalRepository(VcsRepositoryUri repoUri) { try { - if (repoUrl != null && repositoryAlreadyExists(repoUrl)) { + if (repoUri != null && repositoryAlreadyExists(repoUri)) { // We need to close the possibly still open repository otherwise an IOException will be thrown on Windows - Repository repo = getOrCheckoutRepository(repoUrl, false); + Repository repo = getOrCheckoutRepository(repoUri, false); deleteLocalRepository(repo); } } @@ -1357,11 +1357,11 @@ public Path zipFiles(Path contentRootPath, String zipFilename, String zipDir, @N /** * Checks if repo was already checked out and is present on disk * - * @param repoUrl URL of the remote repository. + * @param repoUri URL of the remote repository. * @return True if repo exists on disk */ - public boolean repositoryAlreadyExists(VcsRepositoryUrl repoUrl) { - Path localPath = getDefaultLocalPathOfRepo(repoUrl); + public boolean repositoryAlreadyExists(VcsRepositoryUri repoUri) { + Path localPath = getDefaultLocalPathOfRepo(repoUri); return Files.exists(localPath); } @@ -1406,16 +1406,16 @@ public > C authenticate(TransportCommand command) } /** - * Checkout a repository and get the git log for a given repository url. + * Checkout a repository and get the git log for a given repository uri. * - * @param vcsRepositoryUrl the repository url for which the git log should be retrieved + * @param vcsRepositoryUri the repository uri for which the git log should be retrieved * @return a list of commit info DTOs containing author, timestamp, commit message, and hash * @throws GitAPIException if an error occurs while retrieving the git log */ - public List getCommitInfos(VcsRepositoryUrl vcsRepositoryUrl) throws GitAPIException { + public List getCommitInfos(VcsRepositoryUri vcsRepositoryUri) throws GitAPIException { List commitInfos = new ArrayList<>(); - try (var repo = getOrCheckoutRepository(vcsRepositoryUrl, true); var git = new Git(repo)) { + try (var repo = getOrCheckoutRepository(vcsRepositoryUri, true); var git = new Git(repo)) { var commits = git.log().call(); commits.forEach(commit -> { var commitInfo = CommitInfoDTO.of(commit); diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/aeolus/AeolusBuildPlanService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/aeolus/AeolusBuildPlanService.java index 21a139f553d2..9e35aee24e34 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/aeolus/AeolusBuildPlanService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/aeolus/AeolusBuildPlanService.java @@ -24,7 +24,7 @@ import com.google.gson.Gson; import de.tum.in.www1.artemis.domain.AuxiliaryRepository; -import de.tum.in.www1.artemis.domain.VcsRepositoryUrl; +import de.tum.in.www1.artemis.domain.VcsRepositoryUri; import de.tum.in.www1.artemis.domain.enumeration.AeolusTarget; import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; import de.tum.in.www1.artemis.exception.ContinuousIntegrationBuildPlanException; @@ -118,29 +118,29 @@ public String publishBuildPlan(Windfile windfile, AeolusTarget target) { * @param programmingLanguage the programming language of the exercise * @param branch the branch of the exercise * @param checkoutSolutionRepository whether the solution repository should be checked out (only used in OCAML and Haskell exercises) - * @param repositoryUrl the url of the assignment repository - * @param testRepositoryUrl the url of the test repository - * @param solutionRepositoryUrl the url of the solution repository + * @param repositoryUri the uri of the assignment repository + * @param testRepositoryUri the uri of the test repository + * @param solutionRepositoryUri the uri of the solution repository * @param auxiliaryRepositories List of auxiliary repositories to be included in the build plan * @return a map of repositories used in Aeolus to create the checkout task in the custom build plan */ public Map createRepositoryMapForWindfile(ProgrammingLanguage programmingLanguage, String branch, boolean checkoutSolutionRepository, - VcsRepositoryUrl repositoryUrl, VcsRepositoryUrl testRepositoryUrl, VcsRepositoryUrl solutionRepositoryUrl, - List auxiliaryRepositories) { + VcsRepositoryUri repositoryUri, VcsRepositoryUri testRepositoryUri, VcsRepositoryUri solutionRepositoryUri, + List auxiliaryRepositories) { if (bambooInternalUrlService.isEmpty()) { throw new ContinuousIntegrationBuildPlanException("Internal URL service for Bamboo is not configured"); } Map repositoryMap = new HashMap<>(); - repositoryMap.put(ASSIGNMENT_REPO_NAME, new AeolusRepository(bambooInternalUrlService.get().toInternalVcsUrl(repositoryUrl).toString(), branch, + repositoryMap.put(ASSIGNMENT_REPO_NAME, new AeolusRepository(bambooInternalUrlService.get().toInternalVcsUrl(repositoryUri).toString(), branch, ContinuousIntegrationService.RepositoryCheckoutPath.ASSIGNMENT.forProgrammingLanguage(programmingLanguage))); if (checkoutSolutionRepository) { - repositoryMap.put(SOLUTION_REPO_NAME, new AeolusRepository(bambooInternalUrlService.get().toInternalVcsUrl(solutionRepositoryUrl).toString(), branch, + repositoryMap.put(SOLUTION_REPO_NAME, new AeolusRepository(bambooInternalUrlService.get().toInternalVcsUrl(solutionRepositoryUri).toString(), branch, ContinuousIntegrationService.RepositoryCheckoutPath.SOLUTION.forProgrammingLanguage(programmingLanguage))); } - repositoryMap.put(TEST_REPO_NAME, new AeolusRepository(bambooInternalUrlService.get().toInternalVcsUrl(testRepositoryUrl).toString(), branch, + repositoryMap.put(TEST_REPO_NAME, new AeolusRepository(bambooInternalUrlService.get().toInternalVcsUrl(testRepositoryUri).toString(), branch, ContinuousIntegrationService.RepositoryCheckoutPath.TEST.forProgrammingLanguage(programmingLanguage))); for (var auxRepo : auxiliaryRepositories) { - repositoryMap.put(auxRepo.name(), new AeolusRepository(auxRepo.repositoryUrl().toString(), branch, auxRepo.name())); + repositoryMap.put(auxRepo.name(), new AeolusRepository(auxRepo.repositoryUri().toString(), branch, auxRepo.name())); } return repositoryMap; } diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/bamboo/BambooBuildPlanService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/bamboo/BambooBuildPlanService.java index 3e3f7ecafe14..3ed269609b04 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/bamboo/BambooBuildPlanService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/bamboo/BambooBuildPlanService.java @@ -51,7 +51,7 @@ import de.tum.in.www1.artemis.domain.enumeration.*; import de.tum.in.www1.artemis.exception.ContinuousIntegrationBuildPlanException; import de.tum.in.www1.artemis.service.ResourceLoaderService; -import de.tum.in.www1.artemis.service.UrlService; +import de.tum.in.www1.artemis.service.UriService; import de.tum.in.www1.artemis.service.connectors.aeolus.AeolusBuildPlanService; import de.tum.in.www1.artemis.service.connectors.aeolus.AeolusRepository; import de.tum.in.www1.artemis.service.connectors.aeolus.Windfile; @@ -88,16 +88,16 @@ public class BambooBuildPlanService { private final Optional aeolusBuildPlanService; - private final UrlService urlService; + private final UriService uriService; public BambooBuildPlanService(ResourceLoaderService resourceLoaderService, BambooServer bambooServer, Optional versionControlService, - ProgrammingLanguageConfiguration programmingLanguageConfiguration, UrlService urlService, BambooInternalUrlService bambooInternalUrlService, + ProgrammingLanguageConfiguration programmingLanguageConfiguration, UriService uriService, BambooInternalUrlService bambooInternalUrlService, Optional aeolusBuildPlanService) { this.resourceLoaderService = resourceLoaderService; this.bambooServer = bambooServer; this.versionControlService = versionControlService; this.programmingLanguageConfiguration = programmingLanguageConfiguration; - this.urlService = urlService; + this.uriService = uriService; this.bambooInternalUrlService = bambooInternalUrlService; this.aeolusBuildPlanService = aeolusBuildPlanService; } @@ -108,14 +108,14 @@ public BambooBuildPlanService(ResourceLoaderService resourceLoaderService, Bambo * @param programmingExercise programming exercise with the required * information to create the base build plan * @param planKey the key of the build plan - * @param repositoryUrl the url of the assignment repository - * @param testRepositoryUrl the url of the test repository - * @param solutionRepositoryUrl the url of the solution repository + * @param repositoryUri the uri of the assignment repository + * @param testRepositoryUri the uri of the test repository + * @param solutionRepositoryUri the uri of the solution repository * @param auxiliaryRepositories List of auxiliary repositories to be included in * the build plan */ - public void createBuildPlanForExercise(ProgrammingExercise programmingExercise, String planKey, VcsRepositoryUrl repositoryUrl, VcsRepositoryUrl testRepositoryUrl, - VcsRepositoryUrl solutionRepositoryUrl, List auxiliaryRepositories) { + public void createBuildPlanForExercise(ProgrammingExercise programmingExercise, String planKey, VcsRepositoryUri repositoryUri, VcsRepositoryUri testRepositoryUri, + VcsRepositoryUri solutionRepositoryUri, List auxiliaryRepositories) { final String planDescription = planKey + " Build Plan for Exercise " + programmingExercise.getTitle(); final String projectKey = programmingExercise.getProjectKey(); final String projectName = programmingExercise.getProjectName(); @@ -124,13 +124,13 @@ public void createBuildPlanForExercise(ProgrammingExercise programmingExercise, String assignedKey = null; if (aeolusBuildPlanService.isPresent()) { - assignedKey = createCustomAeolusBuildPlanForExercise(programmingExercise, projectKey + "-" + planKey, planDescription, repositoryUrl, testRepositoryUrl, - solutionRepositoryUrl, auxiliaryRepositories); + assignedKey = createCustomAeolusBuildPlanForExercise(programmingExercise, projectKey + "-" + planKey, planDescription, repositoryUri, testRepositoryUri, + solutionRepositoryUri, auxiliaryRepositories); } if (assignedKey == null) { - Plan plan = createDefaultBuildPlan(planKey, planDescription, projectKey, projectName, repositoryUrl, testRepositoryUrl, - programmingExercise.getCheckoutSolutionRepository(), solutionRepositoryUrl, auxiliaryRepositories) + Plan plan = createDefaultBuildPlan(planKey, planDescription, projectKey, projectName, repositoryUri, testRepositoryUri, + programmingExercise.getCheckoutSolutionRepository(), solutionRepositoryUri, auxiliaryRepositories) .stages(createBuildStage(programmingExercise.getProgrammingLanguage(), programmingExercise.getProjectType(), programmingExercise.getPackageName(), programmingExercise.hasSequentialTestRuns(), programmingExercise.isStaticCodeAnalysisEnabled(), programmingExercise.getCheckoutSolutionRepository(), recordTestwiseCoverage, programmingExercise.getAuxiliaryRepositoriesForBuildPlan())); @@ -191,10 +191,10 @@ private Stage createBuildStage(ProgrammingLanguage programmingLanguage, ProjectT // exist in Artemis yet) boolean isMavenProject = ProjectType.isMavenProject(projectType); - var defaultTasks = new ArrayList>(); + List> defaultTasks = new ArrayList<>(); defaultTasks.add(checkoutTask); - var finalTasks = new ArrayList>(); - var artifacts = new ArrayList(); + List> finalTasks = new ArrayList<>(); + List artifacts = new ArrayList<>(); if (Boolean.TRUE.equals(staticCodeAnalysisEnabled)) { modifyBuildConfigurationForStaticCodeAnalysisForJavaAndKotlinExercise(isMavenProject, finalTasks, artifacts); @@ -207,16 +207,9 @@ private Stage createBuildStage(ProgrammingLanguage programmingLanguage, ProjectT modifyBuildConfigurationForSequentialTestsForJavaAndKotlinExercise(isMavenProject, defaultTasks, finalTasks); } - // This conversion is required because the attributes are passed as varargs-parameter which is only possible - // for array collections - var defaultTasksArray = defaultTasks.toArray(new Task[0]); - var finalTasksArray = finalTasks.toArray(new Task[0]); - var artifactsArray = artifacts.toArray(new Artifact[0]); - - // assign tasks and artifacts to job - defaultJob.tasks(defaultTasksArray); - defaultJob.finalTasks(finalTasksArray); - defaultJob.artifacts(artifactsArray); + defaultJob.tasks(defaultTasks.toArray(Task[]::new)); + defaultJob.finalTasks(finalTasks.toArray(Task[]::new)); + defaultJob.artifacts(artifacts.toArray(Artifact[]::new)); return defaultStage.jobs(defaultJob); } @@ -228,7 +221,7 @@ private Stage createBuildStage(ProgrammingLanguage programmingLanguage, ProjectT final Optional projectTypeSubdirectory = Optional.of(Path.of(projectType.name().toLowerCase())); final var tasks = readScriptTasksFromTemplate(programmingLanguage, projectTypeSubdirectory, null, sequentialBuildRuns, false); tasks.add(0, checkoutTask); - defaultJob.tasks(tasks.toArray(new Task[0])); + defaultJob.tasks(tasks.toArray(Task[]::new)); // Final tasks: final TestParserTask testParserTask = new TestParserTask(TestParserTaskProperties.TestType.JUNIT).resultDirectories("test-reports/*results.xml"); @@ -242,7 +235,7 @@ private Stage createBuildStage(ProgrammingLanguage programmingLanguage, ProjectT .toArray(Artifact[]::new); defaultJob.artifacts(artifacts); final var scaTasks = readScriptTasksFromTemplate(programmingLanguage, Optional.empty(), null, false, true); - defaultJob.finalTasks(scaTasks.toArray(new Task[0])); + defaultJob.finalTasks(scaTasks.toArray(Task[]::new)); } // Do not remove target, so the report can be sent to Artemis @@ -272,7 +265,7 @@ private Stage createBuildStage(ProgrammingLanguage programmingLanguage, ProjectT } final var tasks = readScriptTasksFromTemplate(programmingLanguage, subDirectory, replacements, sequentialBuildRuns, false); tasks.add(0, checkoutTask); - defaultJob.tasks(tasks.toArray(new Task[0])).finalTasks(testParserTask); + defaultJob.tasks(tasks.toArray(Task[]::new)).finalTasks(testParserTask); if (Boolean.TRUE.equals(staticCodeAnalysisEnabled)) { // Create artifacts and a final task for the execution of static code analysis final List staticCodeAnalysisTools = StaticCodeAnalysisTool.getToolsForProgrammingLanguage(ProgrammingLanguage.SWIFT); @@ -281,7 +274,7 @@ private Stage createBuildStage(ProgrammingLanguage programmingLanguage, ProjectT .toArray(Artifact[]::new); defaultJob.artifacts(artifacts); final var scaTasks = readScriptTasksFromTemplate(programmingLanguage, subDirectory, null, false, true); - defaultJob.finalTasks(scaTasks.toArray(new Task[0])); + defaultJob.finalTasks(scaTasks.toArray(Task[]::new)); } if (isXcodeProject) { // add a requirement to be able to run the Xcode build tasks using fastlane @@ -396,28 +389,27 @@ private Stage createDefaultStage(ProgrammingLanguage programmingLanguage, boolea final var testParserTask = new TestParserTask(TestParserTaskProperties.TestType.JUNIT).resultDirectories(resultDirectories); final var tasks = readScriptTasksFromTemplate(programmingLanguage, Optional.empty(), null, sequentialBuildRuns, false); tasks.add(0, checkoutTask); - return defaultStage.jobs(defaultJob.tasks(tasks.toArray(new Task[0])).finalTasks(testParserTask)); + return defaultStage.jobs(defaultJob.tasks(tasks.toArray(Task[]::new)).finalTasks(testParserTask)); } - // TODO: aux repos also need to have a URL and not a slug - private Plan createDefaultBuildPlan(String planKey, String planDescription, String projectKey, String projectName, VcsRepositoryUrl assignmentRepoUrl, - VcsRepositoryUrl testRepoUrl, boolean checkoutSolutionRepository, VcsRepositoryUrl solutionRepoUrl, - List auxiliaryRepositories) { + private Plan createDefaultBuildPlan(String planKey, String planDescription, String projectKey, String projectName, VcsRepositoryUri assignmentRepoUri, + VcsRepositoryUri testRepoUri, boolean checkoutSolutionRepository, VcsRepositoryUri solutionRepoUri, + List auxiliaryRepositories) { VersionControlService versionControl = versionControlService.orElseThrow(); - List> planRepositories = new ArrayList<>(); - planRepositories.add(createBuildPlanRepository(ASSIGNMENT_REPO_NAME, bambooInternalUrlService.toInternalVcsUrl(assignmentRepoUrl).toString(), - versionControl.getDefaultBranchOfRepository(projectKey, urlService.getRepositorySlugFromRepositoryUrl(assignmentRepoUrl)))); - planRepositories.add(createBuildPlanRepository(TEST_REPO_NAME, bambooInternalUrlService.toInternalVcsUrl(testRepoUrl).toString(), - versionControl.getDefaultBranchOfRepository(projectKey, urlService.getRepositorySlugFromRepositoryUrl(testRepoUrl)))); + List planRepositories = new ArrayList<>(); + planRepositories.add(createBuildPlanGitRepository(ASSIGNMENT_REPO_NAME, bambooInternalUrlService.toInternalVcsUrl(assignmentRepoUri).toString(), + versionControl.getDefaultBranchOfRepository(projectKey, uriService.getRepositorySlugFromRepositoryUri(assignmentRepoUri)))); + planRepositories.add(createBuildPlanGitRepository(TEST_REPO_NAME, bambooInternalUrlService.toInternalVcsUrl(testRepoUri).toString(), + versionControl.getDefaultBranchOfRepository(projectKey, uriService.getRepositorySlugFromRepositoryUri(testRepoUri)))); for (var auxRepo : auxiliaryRepositories) { - planRepositories.add(createBuildPlanRepository(auxRepo.name(), bambooInternalUrlService.toInternalVcsUrl(auxRepo.repositoryUrl()).toString(), - versionControl.getDefaultBranchOfRepository(projectKey, urlService.getRepositorySlugFromRepositoryUrl(auxRepo.repositoryUrl())))); + planRepositories.add(createBuildPlanGitRepository(auxRepo.name(), bambooInternalUrlService.toInternalVcsUrl(auxRepo.repositoryUri()).toString(), + versionControl.getDefaultBranchOfRepository(projectKey, uriService.getRepositorySlugFromRepositoryUri(auxRepo.repositoryUri())))); } if (checkoutSolutionRepository) { - planRepositories.add(createBuildPlanRepository(SOLUTION_REPO_NAME, bambooInternalUrlService.toInternalVcsUrl(solutionRepoUrl).toString(), - versionControl.getDefaultBranchOfRepository(projectKey, urlService.getRepositorySlugFromRepositoryUrl(solutionRepoUrl)))); + planRepositories.add(createBuildPlanGitRepository(SOLUTION_REPO_NAME, bambooInternalUrlService.toInternalVcsUrl(solutionRepoUri).toString(), + versionControl.getDefaultBranchOfRepository(projectKey, uriService.getRepositorySlugFromRepositoryUri(solutionRepoUri)))); } return new Plan(createBuildProject(projectName, projectKey), planKey, planKey).description(planDescription) @@ -450,11 +442,9 @@ private Notification createNotification() { .recipientString(artemisServerUrl + NEW_RESULT_RESOURCE_API_PATH)); } - private GitRepository createBuildPlanRepository(String name, String repositoryUrl, String branch) { + private GitRepository createBuildPlanGitRepository(String name, String repositoryUri, String branch) { return new GitRepository().name(name).branch(branch).authentication(new SharedCredentialsIdentifier(gitUser).scope(SharedCredentialsScope.GLOBAL)) - .url(repositoryUrl.toLowerCase()).shallowClonesEnabled(true).remoteAgentCacheEnabled(false) - // TODO: can we leave this empty? - .changeDetection(new VcsChangeDetection()); + .url(repositoryUri.toLowerCase()).shallowClonesEnabled(true).remoteAgentCacheEnabled(false).changeDetection(new VcsChangeDetection()); } private PlanPermissions generatePlanPermissions(String bambooProjectKey, String bambooPlanKey, @Nullable String teachingAssistantGroupName, @Nullable String editorGroupName, @@ -543,8 +533,8 @@ else if (sequentialBuildRuns) { /** * Assembles a bamboo docker configuration for a given programming exercise and project type * - * @param programmingLanguage - * @param projectType + * @param programmingLanguage the chosen language of the programming exercise + * @param projectType the chosen project type of the programming exercise * @return bamboo docker configuration */ private DockerConfiguration dockerConfigurationFor(ProgrammingLanguage programmingLanguage, Optional projectType) { @@ -563,7 +553,7 @@ private DockerConfiguration dockerConfigurationFor(ProgrammingLanguage programmi * @return An array of string containing all the configured docker run argument key-value pairs prefixed with two dashes */ private String[] getDefaultDockerRunArguments() { - return programmingLanguageConfiguration.getDefaultDockerFlags().toArray(new String[0]); + return programmingLanguageConfiguration.getDefaultDockerFlags().toArray(String[]::new); } /** @@ -575,14 +565,14 @@ private String[] getDefaultDockerRunArguments() { * @param programmingExercise the programming exercise for which to create the build plan * @param buildPlanId the id of the build plan * @param planDescription the description of the build plan - * @param repositoryUrl the url of the assignment repository - * @param testRepositoryUrl the url of the test repository - * @param solutionRepositoryUrl the url of the solution repository + * @param repositoryUri the url of the assignment repository + * @param testRepositoryUri the url of the test repository + * @param solutionRepositoryUri the url of the solution repository * @param auxiliaryRepositories List of auxiliary repositories to be included in the build plan * @return the key of the created build plan, or null if it could not be created */ - private String createCustomAeolusBuildPlanForExercise(ProgrammingExercise programmingExercise, String buildPlanId, String planDescription, VcsRepositoryUrl repositoryUrl, - VcsRepositoryUrl testRepositoryUrl, VcsRepositoryUrl solutionRepositoryUrl, List auxiliaryRepositories) + private String createCustomAeolusBuildPlanForExercise(ProgrammingExercise programmingExercise, String buildPlanId, String planDescription, VcsRepositoryUri repositoryUri, + VcsRepositoryUri testRepositoryUri, VcsRepositoryUri solutionRepositoryUri, List auxiliaryRepositories) throws ContinuousIntegrationBuildPlanException { if (aeolusBuildPlanService.isEmpty()) { return null; @@ -595,7 +585,7 @@ private String createCustomAeolusBuildPlanForExercise(ProgrammingExercise progra try { Windfile windfile = programmingExercise.getWindfile(); Map repositories = aeolusBuildPlanService.get().createRepositoryMapForWindfile(programmingExercise.getProgrammingLanguage(), - programmingExercise.getBranch(), programmingExercise.getCheckoutSolutionRepository(), repositoryUrl, testRepositoryUrl, solutionRepositoryUrl, + programmingExercise.getBranch(), programmingExercise.getCheckoutSolutionRepository(), repositoryUri, testRepositoryUri, solutionRepositoryUri, auxiliaryRepositories); String resultHookUrl = artemisServerUrl + NEW_RESULT_RESOURCE_API_PATH; diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/bamboo/BambooService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/bamboo/BambooService.java index af84eef88db9..e9ebba150126 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/bamboo/BambooService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/bamboo/BambooService.java @@ -43,7 +43,7 @@ import de.tum.in.www1.artemis.repository.FeedbackRepository; import de.tum.in.www1.artemis.repository.ProgrammingSubmissionRepository; import de.tum.in.www1.artemis.service.BuildLogEntryService; -import de.tum.in.www1.artemis.service.UrlService; +import de.tum.in.www1.artemis.service.UriService; import de.tum.in.www1.artemis.service.connectors.*; import de.tum.in.www1.artemis.service.connectors.bamboo.dto.*; import de.tum.in.www1.artemis.service.connectors.ci.AbstractContinuousIntegrationService; @@ -66,7 +66,7 @@ public class BambooService extends AbstractContinuousIntegrationService { private final ObjectMapper mapper; - private final UrlService urlService; + private final UriService uriService; private final RestTemplate restTemplate; @@ -74,43 +74,43 @@ public class BambooService extends AbstractContinuousIntegrationService { public BambooService(ProgrammingSubmissionRepository programmingSubmissionRepository, Optional continuousIntegrationUpdateService, BambooBuildPlanService bambooBuildPlanService, FeedbackRepository feedbackRepository, @Qualifier("bambooRestTemplate") RestTemplate restTemplate, - @Qualifier("shortTimeoutBambooRestTemplate") RestTemplate shortTimeoutRestTemplate, ObjectMapper mapper, UrlService urlService, BuildLogEntryService buildLogService, + @Qualifier("shortTimeoutBambooRestTemplate") RestTemplate shortTimeoutRestTemplate, ObjectMapper mapper, UriService uriService, BuildLogEntryService buildLogService, TestwiseCoverageService testwiseCoverageService, BuildLogStatisticsEntryRepository buildLogStatisticsEntryRepository) { super(programmingSubmissionRepository, feedbackRepository, buildLogService, buildLogStatisticsEntryRepository, testwiseCoverageService); this.continuousIntegrationUpdateService = continuousIntegrationUpdateService; this.bambooBuildPlanService = bambooBuildPlanService; this.mapper = mapper; - this.urlService = urlService; + this.uriService = uriService; this.restTemplate = restTemplate; this.shortTimeoutRestTemplate = shortTimeoutRestTemplate; } @Override - public void createBuildPlanForExercise(ProgrammingExercise programmingExercise, String planKey, VcsRepositoryUrl sourceCodeRepositoryURL, VcsRepositoryUrl testRepositoryURL, - VcsRepositoryUrl solutionRepositoryURL) { + public void createBuildPlanForExercise(ProgrammingExercise programmingExercise, String planKey, VcsRepositoryUri repositoryUri, VcsRepositoryUri testRepositoryUri, + VcsRepositoryUri solutionRepositoryUri) { var additionalRepositories = programmingExercise.getAuxiliaryRepositoriesForBuildPlan().stream() - .map(repo -> new AuxiliaryRepository.AuxRepoNameWithUrl(repo.getName(), repo.getVcsRepositoryUrl())).toList(); - bambooBuildPlanService.createBuildPlanForExercise(programmingExercise, planKey, sourceCodeRepositoryURL, testRepositoryURL, solutionRepositoryURL, additionalRepositories); + .map(repo -> new AuxiliaryRepository.AuxRepoNameWithUri(repo.getName(), repo.getVcsRepositoryUri())).toList(); + bambooBuildPlanService.createBuildPlanForExercise(programmingExercise, planKey, repositoryUri, testRepositoryUri, solutionRepositoryUri, additionalRepositories); } @Override public void recreateBuildPlansForExercise(ProgrammingExercise exercise) { deleteBuildPlan(exercise.getProjectKey(), exercise.getTemplateBuildPlanId()); deleteBuildPlan(exercise.getProjectKey(), exercise.getSolutionBuildPlanId()); - createBuildPlanForExercise(exercise, BuildPlanType.TEMPLATE.getName(), exercise.getVcsTemplateRepositoryUrl(), exercise.getVcsTestRepositoryUrl(), - exercise.getVcsSolutionRepositoryUrl()); - createBuildPlanForExercise(exercise, BuildPlanType.SOLUTION.getName(), exercise.getVcsSolutionRepositoryUrl(), exercise.getVcsTestRepositoryUrl(), - exercise.getVcsSolutionRepositoryUrl()); + createBuildPlanForExercise(exercise, BuildPlanType.TEMPLATE.getName(), exercise.getVcsTemplateRepositoryUri(), exercise.getVcsTestRepositoryUri(), + exercise.getVcsSolutionRepositoryUri()); + createBuildPlanForExercise(exercise, BuildPlanType.SOLUTION.getName(), exercise.getVcsSolutionRepositoryUri(), exercise.getVcsTestRepositoryUri(), + exercise.getVcsSolutionRepositoryUri()); } @Override public void configureBuildPlan(ProgrammingExerciseParticipation participation, String branch) { String buildPlanId = participation.getBuildPlanId(); - VcsRepositoryUrl repositoryUrl = participation.getVcsRepositoryUrl(); + VcsRepositoryUri repositoryUri = participation.getVcsRepositoryUri(); String projectKey = getProjectKeyFromBuildPlanId(buildPlanId); - String repoProjectName = urlService.getProjectKeyFromRepositoryUrl(repositoryUrl); - String plainRepositoryUrl = urlService.getPlainUrlFromRepositoryUrl(repositoryUrl); - updatePlanRepository(projectKey, buildPlanId, ASSIGNMENT_REPO_NAME, repoProjectName, plainRepositoryUrl, null /* not needed */, branch); + String repoProjectName = uriService.getProjectKeyFromRepositoryUri(repositoryUri); + String plainRepositoryUri = uriService.getPlainUriFromRepositoryUri(repositoryUri); + updatePlanRepository(projectKey, buildPlanId, ASSIGNMENT_REPO_NAME, repoProjectName, plainRepositoryUri, null /* not needed */, branch); enablePlan(projectKey, buildPlanId); // allow student or team access to the build plan in case this option was specified (only available for course exercises) @@ -439,9 +439,9 @@ public void enablePlan(String projectKey, String planKey) throws BambooException } @Override - public void updatePlanRepository(String buildProjectKey, String buildPlanKey, String ciRepoName, String repoProjectKey, String newRepoUrl, String existingRepoUrl, + public void updatePlanRepository(String buildProjectKey, String buildPlanKey, String ciRepoName, String repoProjectKey, String newRepoUri, String existingRepoUri, String newBranch) throws BambooException { - continuousIntegrationUpdateService.orElseThrow().updatePlanRepository(buildPlanKey, ciRepoName, newRepoUrl, newBranch); + continuousIntegrationUpdateService.orElseThrow().updatePlanRepository(buildPlanKey, ciRepoName, newRepoUri, newBranch); } /** diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/bitbucket/BambooBuildPlanUpdateService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/bitbucket/BambooBuildPlanUpdateService.java index 38a6825d74ad..1e4ec1c2a129 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/bitbucket/BambooBuildPlanUpdateService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/bitbucket/BambooBuildPlanUpdateService.java @@ -7,7 +7,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import javax.annotation.Nullable; import javax.validation.constraints.NotNull; import org.apache.commons.lang3.StringUtils; @@ -37,8 +36,6 @@ public class BambooBuildPlanUpdateService implements ContinuousIntegrationUpdate private final BambooInternalUrlService bambooInternalUrlService; - private static final String OLD_ASSIGNMENT_REPO_NAME = "Assignment"; - private final Logger log = LoggerFactory.getLogger(BambooBuildPlanUpdateService.class); private final RestTemplate bambooRestTemplate; @@ -49,16 +46,16 @@ public BambooBuildPlanUpdateService(@Qualifier("bambooRestTemplate") RestTemplat } @Override - public void updatePlanRepository(String buildPlanKey, String bambooRepositoryName, String newRepoUrl, String branchName) { + public void updatePlanRepository(String buildPlanKey, String bambooRepositoryName, String newRepoUri, String branchName) { try { log.debug("Update plan repository for build plan {}", buildPlanKey); - BambooRepositoryDTO bambooRepository = findBambooRepository(bambooRepositoryName, OLD_ASSIGNMENT_REPO_NAME, buildPlanKey); + BambooRepositoryDTO bambooRepository = findBambooRepository(bambooRepositoryName, buildPlanKey); if (bambooRepository == null) { throw new BambooException("Something went wrong while updating the template repository of the build plan " + buildPlanKey + " to the student repository : Could not find assignment nor Assignment repository"); } - updateBambooPlanRepository(bambooRepository, buildPlanKey, branchName, bambooInternalUrlService.toInternalVcsUrl(newRepoUrl)); + updateBambooPlanRepository(bambooRepository, buildPlanKey, branchName, bambooInternalUrlService.toInternalVcsUrl(newRepoUri)); log.info("Update plan repository for build plan {} was successful", buildPlanKey); } @@ -75,7 +72,7 @@ public void updatePlanRepository(String buildPlanKey, String bambooRepositoryNam * @param bambooRepository the bamboo repository which was obtained before * @param buildPlanKey the complete name of the plan */ - private void updateBambooPlanRepository(@NotNull BambooRepositoryDTO bambooRepository, String buildPlanKey, String branchName, String newRepoUrl) { + private void updateBambooPlanRepository(@NotNull BambooRepositoryDTO bambooRepository, String buildPlanKey, String branchName, String newRepoUri) { MultiValueMap parameters = new LinkedMultiValueMap<>(); parameters.add("planKey", buildPlanKey); parameters.add("selectedRepository", "com.atlassian.bamboo.plugins.atlassian-bamboo-plugin-git:gitv2"); @@ -86,7 +83,7 @@ private void updateBambooPlanRepository(@NotNull BambooRepositoryDTO bambooRepos parameters.add("save", "Save repository"); parameters.add("bamboo.successReturnMode", "json"); parameters.add("repository.git.branch", branchName); - parameters.add("repository.git.repositoryUrl", newRepoUrl); + parameters.add("repository.git.repositoryUrl", newRepoUri); parameters.add("repository.git.authenticationType", "PASSWORD"); parameters.add("repository.git.passwordCredentialsSource", "SHARED_CREDENTIALS"); parameters.add("___advancedOptionsPresent___", "true"); @@ -110,21 +107,15 @@ private void updateBambooPlanRepository(@NotNull BambooRepositoryDTO bambooRepos } /** - * Fetches the list of repositories in the given build plan, then tries to find the repository in the bamboo build plan, first using the firstRepositoryName, - * second using the secondRepositoryName + * Fetches the list of repositories in the given build plan, then tries to find the repository in the bamboo build plan using the repository name * - * @param firstRepositoryName the repository name that should be found first - * @param secondRepositoryName in case firstRepositoryName is not found, the repository name that should be found second - * @param buildPlanKey the bamboo build plan key in which the repository with the specified name should be found + * @param repositoryName the repository name that should be found first + * @param buildPlanKey the bamboo build plan key in which the repository with the specified name should be found * @return the found bamboo repository in the build plan or null in case the repository with the specified name could not be found */ - private BambooRepositoryDTO findBambooRepository(String firstRepositoryName, @Nullable String secondRepositoryName, String buildPlanKey) { + private BambooRepositoryDTO findBambooRepository(String repositoryName, String buildPlanKey) { List list = this.getBuildPlanRepositoryList(buildPlanKey); - var bambooRepository = this.lookupRepository(firstRepositoryName, list); - if (bambooRepository == null && secondRepositoryName != null) { - bambooRepository = this.lookupRepository(secondRepositoryName, list); - } - return bambooRepository; + return lookupRepository(repositoryName, list); } private BambooRepositoryDTO lookupRepository(String name, List list) { diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/bitbucket/BitbucketService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/bitbucket/BitbucketService.java index 402c652c8cb7..efbe4e18080d 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/bitbucket/BitbucketService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/bitbucket/BitbucketService.java @@ -8,10 +8,7 @@ import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import javax.annotation.Nullable; import javax.validation.constraints.NotNull; @@ -40,7 +37,7 @@ import de.tum.in.www1.artemis.exception.BitbucketException; import de.tum.in.www1.artemis.exception.VersionControlException; import de.tum.in.www1.artemis.repository.*; -import de.tum.in.www1.artemis.service.UrlService; +import de.tum.in.www1.artemis.service.UriService; import de.tum.in.www1.artemis.service.connectors.ConnectorHealth; import de.tum.in.www1.artemis.service.connectors.GitService; import de.tum.in.www1.artemis.service.connectors.bitbucket.dto.*; @@ -70,11 +67,11 @@ public class BitbucketService extends AbstractVersionControlService { private final RestTemplate shortTimeoutRestTemplate; - public BitbucketService(@Qualifier("bitbucketRestTemplate") RestTemplate restTemplate, UserRepository userRepository, UrlService urlService, + public BitbucketService(@Qualifier("bitbucketRestTemplate") RestTemplate restTemplate, UserRepository userRepository, UriService uriService, @Qualifier("shortTimeoutBitbucketRestTemplate") RestTemplate shortTimeoutRestTemplate, GitService gitService, ApplicationContext applicationContext, ProgrammingExerciseStudentParticipationRepository studentParticipationRepository, ProgrammingExerciseRepository programmingExerciseRepository, TemplateProgrammingExerciseParticipationRepository templateProgrammingExerciseParticipationRepository) { - super(applicationContext, gitService, urlService, studentParticipationRepository, programmingExerciseRepository, templateProgrammingExerciseParticipationRepository); + super(gitService, uriService, studentParticipationRepository, programmingExerciseRepository, templateProgrammingExerciseParticipationRepository); this.userRepository = userRepository; this.restTemplate = restTemplate; this.shortTimeoutRestTemplate = shortTimeoutRestTemplate; @@ -92,26 +89,26 @@ public void configureRepository(ProgrammingExercise exercise, ProgrammingExercis if (allowAccess) { final VersionControlRepositoryPermission permissions = determineRepositoryPermissions(exercise); - addMemberToRepository(participation.getVcsRepositoryUrl(), user, permissions); + addMemberToRepository(participation.getVcsRepositoryUri(), user, permissions); } } // TODO: we should separate access (above) from protecting branches - protectBranches(urlService.getProjectKeyFromRepositoryUrl(participation.getVcsRepositoryUrl()), - urlService.getRepositorySlugFromRepositoryUrl(participation.getVcsRepositoryUrl())); + protectBranches(uriService.getProjectKeyFromRepositoryUri(participation.getVcsRepositoryUri()), + uriService.getRepositorySlugFromRepositoryUri(participation.getVcsRepositoryUri())); } @Override - public void addMemberToRepository(VcsRepositoryUrl repositoryUrl, User user, VersionControlRepositoryPermission permissions) { - final String projectKey = urlService.getProjectKeyFromRepositoryUrl(repositoryUrl); - final String urlSlug = urlService.getRepositorySlugFromRepositoryUrl(repositoryUrl); + public void addMemberToRepository(VcsRepositoryUri repositoryUri, User user, VersionControlRepositoryPermission permissions) { + final String projectKey = uriService.getProjectKeyFromRepositoryUri(repositoryUri); + final String urlSlug = uriService.getRepositorySlugFromRepositoryUri(repositoryUri); giveRepoPermission(projectKey, urlSlug, user.getLogin(), permissions); } @Override - public void removeMemberFromRepository(VcsRepositoryUrl repositoryUrl, User user) { - removeStudentRepositoryAccess(repositoryUrl, urlService.getProjectKeyFromRepositoryUrl(repositoryUrl), user.getLogin()); + public void removeMemberFromRepository(VcsRepositoryUri repositoryUri, User user) { + removeStudentRepositoryAccess(repositoryUri, uriService.getProjectKeyFromRepositoryUri(repositoryUri), user.getLogin()); } /** @@ -149,14 +146,14 @@ private void protectBranches(String projectKey, String repositorySlug) { } @Override - protected void addWebHook(VcsRepositoryUrl repositoryUrl, String notificationUrl, String webHookName) { - if (!webHookExists(urlService.getProjectKeyFromRepositoryUrl(repositoryUrl), urlService.getRepositorySlugFromRepositoryUrl(repositoryUrl))) { - createWebHook(urlService.getProjectKeyFromRepositoryUrl(repositoryUrl), urlService.getRepositorySlugFromRepositoryUrl(repositoryUrl), notificationUrl, webHookName); + protected void addWebHook(VcsRepositoryUri repositoryUri, String notificationUrl, String webHookName) { + if (!webHookExists(uriService.getProjectKeyFromRepositoryUri(repositoryUri), uriService.getRepositorySlugFromRepositoryUri(repositoryUri))) { + createWebHook(uriService.getProjectKeyFromRepositoryUri(repositoryUri), uriService.getRepositorySlugFromRepositoryUri(repositoryUri), notificationUrl, webHookName); } } @Override - protected void addAuthenticatedWebHook(VcsRepositoryUrl repositoryUrl, String notificationUrl, String webHookName, String secretToken) { + protected void addAuthenticatedWebHook(VcsRepositoryUri repositoryUri, String notificationUrl, String webHookName, String secretToken) { // Not needed for Bitbucket throw new UnsupportedOperationException("Authenticated webhooks with Bitbucket are not supported!"); } @@ -175,9 +172,9 @@ public void deleteProject(String projectKey) { } @Override - public void deleteRepository(VcsRepositoryUrl repositoryUrl) { - final String projectKey = urlService.getProjectKeyFromRepositoryUrl(repositoryUrl); - final String repositorySlug = urlService.getRepositorySlugFromRepositoryUrl(repositoryUrl); + public void deleteRepository(VcsRepositoryUri repositoryUri) { + final String projectKey = uriService.getProjectKeyFromRepositoryUri(repositoryUri); + final String repositorySlug = uriService.getRepositorySlugFromRepositoryUri(repositoryUri); final String baseUrl = bitbucketServerUrl + "/rest/api/latest/projects/" + projectKey + "/repos/" + repositorySlug.toLowerCase(); log.info("Delete repository {}", baseUrl); try { @@ -189,8 +186,8 @@ public void deleteRepository(VcsRepositoryUrl repositoryUrl) { } @Override - public VcsRepositoryUrl getCloneRepositoryUrl(String projectKey, String repositorySlug) { - return new BitbucketRepositoryUrl(projectKey, repositorySlug); + public VcsRepositoryUri getCloneRepositoryUri(String projectKey, String repositorySlug) { + return new BitbucketRepositoryUri(projectKey, repositorySlug); } private BitbucketProjectDTO getBitbucketProject(String projectKey) { @@ -459,20 +456,20 @@ private void giveRepoPermission(String projectKey, String repositorySlug, String } @Override - public void setRepositoryPermissionsToReadOnly(VcsRepositoryUrl repositoryUrl, String projectKey, Set users) throws BitbucketException { - users.forEach(user -> setStudentRepositoryPermission(repositoryUrl, projectKey, user.getLogin(), VersionControlRepositoryPermission.REPO_READ)); + public void setRepositoryPermissionsToReadOnly(VcsRepositoryUri repositoryUri, String projectKey, Set users) throws BitbucketException { + users.forEach(user -> setStudentRepositoryPermission(repositoryUri, projectKey, user.getLogin(), VersionControlRepositoryPermission.REPO_READ)); } /** * Get the default branch of the repository * - * @param repositoryUrl The repository url to get the default branch for. + * @param repositoryUri The repository uri to get the default branch for. * @return the name of the default branch, e.g. 'main' */ @Override - public String getDefaultBranchOfRepository(VcsRepositoryUrl repositoryUrl) throws BitbucketException { - String projectKey = urlService.getProjectKeyFromRepositoryUrl(repositoryUrl); - String repositorySlug = urlService.getRepositorySlugFromRepositoryUrl(repositoryUrl); + public String getDefaultBranchOfRepository(VcsRepositoryUri repositoryUri) throws BitbucketException { + String projectKey = uriService.getProjectKeyFromRepositoryUri(repositoryUri); + String repositorySlug = uriService.getRepositorySlugFromRepositoryUri(repositoryUri); var getDefaultBranchUrl = bitbucketServerUrl + "/rest/api/latest/projects/" + projectKey + "/repos/" + repositorySlug.toLowerCase() + "/default-branch"; try { @@ -493,21 +490,21 @@ public String getDefaultBranchOfRepository(VcsRepositoryUrl repositoryUrl) throw } @Override - public void unprotectBranch(VcsRepositoryUrl repositoryUrl, String branch) throws VersionControlException { + public void unprotectBranch(VcsRepositoryUri repositoryUri, String branch) throws VersionControlException { // Not implemented because it's not needed in Bitbucket for the current use case, because the main branch is not protected by default } /** * Set the permission of a student for a repository * - * @param repositoryUrl The complete repository-url (including protocol, host and the complete path) + * @param repositoryUri The complete repository-url (including protocol, host and the complete path) * @param projectKey The project key of the repository's project. * @param username The username of the user whom to assign a permission level * @param repositoryPermission Repository permission to set for the user (e.g. READ_ONLY, WRITE) */ - private void setStudentRepositoryPermission(VcsRepositoryUrl repositoryUrl, String projectKey, String username, VersionControlRepositoryPermission repositoryPermission) + private void setStudentRepositoryPermission(VcsRepositoryUri repositoryUri, String projectKey, String username, VersionControlRepositoryPermission repositoryPermission) throws BitbucketException { - String repositorySlug = urlService.getRepositorySlugFromRepositoryUrl(repositoryUrl); + String repositorySlug = uriService.getRepositorySlugFromRepositoryUri(repositoryUri); String baseUrl = bitbucketServerUrl + "/rest/api/latest/projects/" + projectKey + "/repos/" + repositorySlug + "/permissions/users?name="; // NAME&PERMISSION String url = baseUrl + username + "&permission=" + repositoryPermission; try { @@ -522,12 +519,12 @@ private void setStudentRepositoryPermission(VcsRepositoryUrl repositoryUrl, Stri /** * Remove all permissions of a student for a repository * - * @param repositoryUrl The complete repository-url (including protocol, host and the complete path) + * @param repositoryUri The complete repository-url (including protocol, host and the complete path) * @param projectKey The project key of the repository's project. * @param username The username of the user whom to remove access */ - private void removeStudentRepositoryAccess(VcsRepositoryUrl repositoryUrl, String projectKey, String username) throws BitbucketException { - String repositorySlug = urlService.getRepositorySlugFromRepositoryUrl(repositoryUrl); + private void removeStudentRepositoryAccess(VcsRepositoryUri repositoryUri, String projectKey, String username) throws BitbucketException { + String repositorySlug = uriService.getRepositorySlugFromRepositoryUri(repositoryUri); String baseUrl = bitbucketServerUrl + "/rest/api/latest/projects/" + projectKey + "/repos/" + repositorySlug + "/permissions/users?name="; // NAME String url = baseUrl + username; try { @@ -731,15 +728,15 @@ private void createWebHook(String projectKey, String repositorySlug, String noti } @Override - public Boolean repositoryUrlIsValid(@Nullable VcsRepositoryUrl repositoryUrl) { - if (repositoryUrl == null || repositoryUrl.getURI() == null) { + public Boolean repositoryUriIsValid(@Nullable VcsRepositoryUri repositoryUri) { + if (repositoryUri == null || repositoryUri.getURI() == null) { return false; } String projectKey; String repositorySlug; try { - projectKey = urlService.getProjectKeyFromRepositoryUrl(repositoryUrl); - repositorySlug = urlService.getRepositorySlugFromRepositoryUrl(repositoryUrl); + projectKey = uriService.getProjectKeyFromRepositoryUri(repositoryUri); + repositorySlug = uriService.getRepositorySlugFromRepositoryUri(repositoryUri); } catch (BitbucketException e) { // Either the project Key or the repository slug could not be extracted, therefore this can't be a valid URL @@ -762,37 +759,35 @@ public Commit getLastCommitDetails(Object requestBody) throws BitbucketException // {"eventKey":"...","date":"...","actor":{...},"repository":{...},"changes":[{"ref":{...},"refId":"refs/heads/main", // "fromHash":"5626436a443eb898a5c5f74b6352f26ea2b7c84e","toHash":"662868d5e16406d1dd4dcfa8ac6c46ee3d677924","type":"UPDATE"}]} // we are interested in the toHash - Commit commit = new Commit(); try { // TODO: use a DTO (e.g. something similar to CommitDTO) final var commitData = new ObjectMapper().convertValue(requestBody, JsonNode.class); var lastChange = commitData.get("changes").get(0); var ref = lastChange.get("ref"); + String branch = null; if (ref != null) { - var branch = ref.get("displayId").asText(); - commit.setBranch(branch); + branch = ref.get("displayId").asText(); } var hash = lastChange.get("toHash").asText(); - commit.setCommitHash(hash); var actor = commitData.get("actor"); String name = actor.get("name").asText(); String email = actor.get("emailAddress").asText(); + String message = null; if (artemisGitName.equalsIgnoreCase(name)) { final var commitInfo = fetchCommitInfo(commitData, hash); if (commitInfo != null) { - commit.setMessage(commitInfo.get("message").asText()); + message = commitInfo.get("message").asText(); name = commitInfo.get("author").get("name").asText(); email = commitInfo.get("author").get("emailAddress").asText(); } } - commit.setAuthorName(name); - commit.setAuthorEmail(email); + return new Commit(hash, name, message, email, branch); } catch (Exception e) { // silently fail because this step is not absolutely necessary log.error("Error when getting hash of last commit. Able to continue.", e); } - return commit; + return new Commit(null, null, null, null, null); } @Override @@ -818,7 +813,7 @@ public ZonedDateTime getPushDate(ProgrammingExerciseParticipation participation, try { UriComponents builder = UriComponentsBuilder.fromUri(bitbucketServerUrl.toURI()) .pathSegment("rest", "api", "latest", "projects", participation.getProgrammingExercise().getProjectKey(), "repos", - urlService.getRepositorySlugFromRepositoryUrl(participation.getVcsRepositoryUrl()), "ref-change-activities") + uriService.getRepositorySlugFromRepositoryUri(participation.getVcsRepositoryUri()), "ref-change-activities") .queryParam("start", start).queryParam("limit", perPage).queryParam("ref", "refs/heads/" + defaultBranch).build(); final var response = restTemplate.exchange(builder.toUri(), HttpMethod.GET, null, BitbucketChangeActivitiesDTO.class); if (response.getStatusCode() != HttpStatus.OK || response.getBody() == null) { @@ -844,18 +839,18 @@ public ZonedDateTime getPushDate(ProgrammingExerciseParticipation participation, private JsonNode fetchCommitInfo(JsonNode commitData, String hash) { try { var cloneLinks = commitData.get("repository").get("links").get("clone"); - VcsRepositoryUrl repositoryURL; + VcsRepositoryUri repositoryUri; // it might be the case that cloneLinks contains two URLs and the first one is 'ssh'. Then we are interested in http // we use contains here, because it could be the case that https is used here as well in the future. // It should not be possible that the cloneLinks array is empty. if (cloneLinks.size() > 1 && !cloneLinks.get(0).get("name").asText().contains("http")) { - repositoryURL = new VcsRepositoryUrl(cloneLinks.get(1).get("href").asText()); + repositoryUri = new VcsRepositoryUri(cloneLinks.get(1).get("href").asText()); } else { - repositoryURL = new VcsRepositoryUrl(cloneLinks.get(0).get("href").asText()); + repositoryUri = new VcsRepositoryUri(cloneLinks.get(0).get("href").asText()); } - final var projectKey = urlService.getProjectKeyFromRepositoryUrl(repositoryURL); - final var slug = urlService.getRepositorySlugFromRepositoryUrl(repositoryURL); + final var projectKey = uriService.getProjectKeyFromRepositoryUri(repositoryUri); + final var slug = uriService.getRepositorySlugFromRepositoryUri(repositoryUri); final var uriBuilder = UriComponentsBuilder.fromUri(bitbucketServerUrl.toURI()) .pathSegment("rest", "api", "1.0", "projects", projectKey, "repos", slug, "commits", hash).build(); final var commitInfo = restTemplate.exchange(uriBuilder.toUri(), HttpMethod.GET, null, JsonNode.class).getBody(); @@ -876,31 +871,31 @@ public void createRepository(String entityName, String topLevelEntity, String pa createRepository(entityName, topLevelEntity); } - public final class BitbucketRepositoryUrl extends VcsRepositoryUrl { + public final class BitbucketRepositoryUri extends VcsRepositoryUri { - public BitbucketRepositoryUrl(String projectKey, String repositorySlug) { + public BitbucketRepositoryUri(String projectKey, String repositorySlug) { final var urlString = bitbucketServerUrl.getProtocol() + "://" + bitbucketServerUrl.getAuthority() + buildRepositoryPath(projectKey, repositorySlug); try { this.uri = new URI(urlString); } catch (URISyntaxException e) { - throw new BitbucketException("Could not Bitbucket Repository URL", e); + throw new BitbucketException("Could not Bitbucket Repository URI", e); } } - private BitbucketRepositoryUrl(String urlString) { + private BitbucketRepositoryUri(String urlString) { try { this.uri = new URI(urlString); } catch (URISyntaxException e) { - throw new BitbucketException("Could not Bitbucket Repository URL", e); + throw new BitbucketException("Could not Bitbucket Repository URI", e); } } @Override - public VcsRepositoryUrl withUser(String username) { + public VcsRepositoryUri withUser(String username) { this.username = username; - return new BitbucketRepositoryUrl(uri.toString().replaceAll("(https?://)(.*)", "$1" + username + "@$2")); + return new BitbucketRepositoryUri(uri.toString().replaceAll("(https?://)(.*)", "$1" + username + "@$2")); } private String buildRepositoryPath(String projectKey, String repositorySlug) { diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/ci/ContinuousIntegrationService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/ci/ContinuousIntegrationService.java index 0b4f6ee86279..f08dc8ee9640 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/ci/ContinuousIntegrationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/ci/ContinuousIntegrationService.java @@ -36,12 +36,12 @@ enum BuildStatus { * * @param exercise a programming exercise with the required information to create the base build plan * @param planKey the key of the plan - * @param repositoryURL the URL of the assignment repository (used to separate between exercise and solution) - * @param testRepositoryURL the URL of the test repository - * @param solutionRepositoryURL the URL of the solution repository. Only used for HASKELL exercises with checkoutSolutionRepository=true. Otherwise ignored. + * @param repositoryUri the URI of the assignment repository (used to separate between exercise and solution) + * @param testRepositoryUri the URI of the test repository + * @param solutionRepositoryUri the URI of the solution repository. Only used for HASKELL exercises with checkoutSolutionRepository=true. Otherwise ignored. */ - void createBuildPlanForExercise(ProgrammingExercise exercise, String planKey, VcsRepositoryUrl repositoryURL, VcsRepositoryUrl testRepositoryURL, - VcsRepositoryUrl solutionRepositoryURL); + void createBuildPlanForExercise(ProgrammingExercise exercise, String planKey, VcsRepositoryUri repositoryUri, VcsRepositoryUri testRepositoryUri, + VcsRepositoryUri solutionRepositoryUri); /** * Recreates BASE and SOLUTION Build Plan for the given programming exercise @@ -150,11 +150,11 @@ String copyBuildPlan(ProgrammingExercise sourceExercise, String sourcePlanName, * @param buildPlanKey The key of the build plan, which is usually the name combined with the project, e.g. 'EIST16W1-GA56HUR'. * @param ciRepoName The name of the configured repository in the CI plan, normally 'assignment' (or 'test'). * @param repoProjectKey The key of the project that contains the repository, e.g. 'EIST16W1', which is normally the programming exercise project key. - * @param newRepoUrl The url of the newly to be referenced repository. - * @param existingRepoUrl The url of the existing repository (which should be replaced). + * @param newRepoUri The url of the newly to be referenced repository. + * @param existingRepoUri The url of the existing repository (which should be replaced). * @param newBranch The default branch for the new repository */ - void updatePlanRepository(String buildProjectKey, String buildPlanKey, String ciRepoName, String repoProjectKey, String newRepoUrl, String existingRepoUrl, String newBranch); + void updatePlanRepository(String buildProjectKey, String buildPlanKey, String ciRepoName, String repoProjectKey, String newRepoUri, String existingRepoUri, String newBranch); /** * Gives overall roles permissions for the defined project. A role can e.g. be all logged-in users diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/ci/ContinuousIntegrationUpdateService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/ci/ContinuousIntegrationUpdateService.java index 396c905b003e..d0b2c2504818 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/ci/ContinuousIntegrationUpdateService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/ci/ContinuousIntegrationUpdateService.java @@ -10,8 +10,8 @@ public interface ContinuousIntegrationUpdateService { * * @param buildPlanKey The key of the buildPlan, which is usually the username, e.g. 'ga56hur'. * @param ciRepoName The name of the configured repository in the continuous integration plan, normally 'assignment' or 'test' - * @param newRepoUrl The new repository URL + * @param newRepoUri The new repository URI * @param branchName The name of the default name of the repository */ - void updatePlanRepository(String buildPlanKey, String ciRepoName, String newRepoUrl, String branchName); + void updatePlanRepository(String buildPlanKey, String ciRepoName, String newRepoUri, String branchName); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlab/GitLabService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlab/GitLabService.java index e0932e2dd608..fad7c0458008 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlab/GitLabService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlab/GitLabService.java @@ -40,7 +40,7 @@ import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; import de.tum.in.www1.artemis.exception.VersionControlException; import de.tum.in.www1.artemis.repository.*; -import de.tum.in.www1.artemis.service.UrlService; +import de.tum.in.www1.artemis.service.UriService; import de.tum.in.www1.artemis.service.connectors.ConnectorHealth; import de.tum.in.www1.artemis.service.connectors.GitService; import de.tum.in.www1.artemis.service.connectors.gitlab.dto.GitLabPushNotificationDTO; @@ -70,11 +70,11 @@ public class GitLabService extends AbstractVersionControlService { private final ScheduledExecutorService scheduler; - public GitLabService(UserRepository userRepository, @Qualifier("shortTimeoutGitlabRestTemplate") RestTemplate shortTimeoutRestTemplate, GitLabApi gitlab, UrlService urlService, + public GitLabService(UserRepository userRepository, @Qualifier("shortTimeoutGitlabRestTemplate") RestTemplate shortTimeoutRestTemplate, GitLabApi gitlab, UriService uriService, GitLabUserManagementService gitLabUserManagementService, GitService gitService, ApplicationContext applicationContext, ProgrammingExerciseStudentParticipationRepository studentParticipationRepository, ProgrammingExerciseRepository programmingExerciseRepository, TemplateProgrammingExerciseParticipationRepository templateProgrammingExerciseParticipationRepository) { - super(applicationContext, gitService, urlService, studentParticipationRepository, programmingExerciseRepository, templateProgrammingExerciseParticipationRepository); + super(gitService, uriService, studentParticipationRepository, programmingExerciseRepository, templateProgrammingExerciseParticipationRepository); this.userRepository = userRepository; this.shortTimeoutRestTemplate = shortTimeoutRestTemplate; this.gitlab = gitlab; @@ -94,7 +94,7 @@ public void configureRepository(ProgrammingExercise exercise, ProgrammingExercis if (allowAccess) { final VersionControlRepositoryPermission permissions = determineRepositoryPermissions(exercise); - addMemberToRepository(participation.getVcsRepositoryUrl(), user, permissions); + addMemberToRepository(participation.getVcsRepositoryUri(), user, permissions); } // Validate that the access token exist, if it is required @@ -103,12 +103,12 @@ public void configureRepository(ProgrammingExercise exercise, ProgrammingExercis // TODO: we should separate access (above) from protecting branches String branch = getOrRetrieveBranchOfStudentParticipation(participation); - protectBranch(participation.getVcsRepositoryUrl(), branch); + protectBranch(participation.getVcsRepositoryUri(), branch); } @Override - public void addMemberToRepository(VcsRepositoryUrl repositoryUrl, User user, VersionControlRepositoryPermission permissions) { - final String repositoryPath = urlService.getRepositoryPathFromRepositoryUrl(repositoryUrl); + public void addMemberToRepository(VcsRepositoryUri repositoryUri, User user, VersionControlRepositoryPermission permissions) { + final String repositoryPath = uriService.getRepositoryPathFromRepositoryUri(repositoryUri); final Long userId = gitLabUserManagementService.getUserId(user.getLogin()); final AccessLevel repositoryPermissions = permissionsToAccessLevel(permissions); @@ -120,14 +120,14 @@ public void addMemberToRepository(VcsRepositoryUrl repositoryUrl, User user, Ver // A resource conflict status code is returned if the member // already exists in the repository if (e.getHttpStatus() == 409) { - updateMemberPermissionInRepository(repositoryUrl, user, permissions); + updateMemberPermissionInRepository(repositoryUri, user, permissions); } else if (e.getValidationErrors() != null && e.getValidationErrors().containsKey("access_level") && e.getValidationErrors().get("access_level").stream().anyMatch(s -> s.contains("should be greater than or equal to"))) { log.warn("Member already has the requested permissions! Permission stays the same"); } else { - throw new GitLabException("Error while trying to add user to repository: " + user.getLogin() + " to repo " + repositoryUrl, e); + throw new GitLabException("Error while trying to add user to repository: " + user.getLogin() + " to repo " + repositoryUri, e); } } } @@ -140,27 +140,27 @@ private static AccessLevel permissionsToAccessLevel(final VersionControlReposito } @Override - public void removeMemberFromRepository(VcsRepositoryUrl repositoryUrl, User user) { - final var repositoryPath = urlService.getRepositoryPathFromRepositoryUrl(repositoryUrl); + public void removeMemberFromRepository(VcsRepositoryUri repositoryUri, User user) { + final var repositoryPath = uriService.getRepositoryPathFromRepositoryUri(repositoryUri); final var userId = gitLabUserManagementService.getUserId(user.getLogin()); try { gitlab.getProjectApi().removeMember(repositoryPath, userId); } catch (GitLabApiException e) { - throw new GitLabException("Error while trying to remove user from repository: " + user.getLogin() + " from repo " + repositoryUrl, e); + throw new GitLabException("Error while trying to remove user from repository: " + user.getLogin() + " from repo " + repositoryUri, e); } } /** * Get the default branch of the repository * - * @param repositoryUrl The repository url to get the default branch for. + * @param repositoryUri The repository uri to get the default branch for. * @return the name of the default branch, e.g. 'main' */ @Override - public String getDefaultBranchOfRepository(VcsRepositoryUrl repositoryUrl) throws GitLabException { - var repositoryId = getPathIDFromRepositoryURL(repositoryUrl); + public String getDefaultBranchOfRepository(VcsRepositoryUri repositoryUri) throws GitLabException { + var repositoryId = getPathIDFromRepositoryURL(repositoryUri); try { return gitlab.getProjectApi().getProject(repositoryId).getDefaultBranch(); @@ -170,8 +170,8 @@ public String getDefaultBranchOfRepository(VcsRepositoryUrl repositoryUrl) throw } } - private String getPathIDFromRepositoryURL(VcsRepositoryUrl repositoryUrl) { - final var namespaces = repositoryUrl.getURI().toString().split("/"); + private String getPathIDFromRepositoryURL(VcsRepositoryUri repositoryUri) { + final var namespaces = repositoryUri.getURI().toString().split("/"); final var last = namespaces.length - 1; return namespaces[last - 1] + "/" + namespaces[last].replace(".git", ""); } @@ -179,12 +179,12 @@ private String getPathIDFromRepositoryURL(VcsRepositoryUrl repositoryUrl) { /** * Protects a branch from the repository, so that developers cannot change the history * - * @param repositoryUrl The repository url of the repository to update. It contains the project key & the repository name. + * @param repositoryUri The repository uri of the repository to update. It contains the project key & the repository name. * @param branch The name of the branch to protect (e.g "main") * @throws VersionControlException If the communication with the VCS fails. */ - private void protectBranch(VcsRepositoryUrl repositoryUrl, String branch) { - final var repositoryPath = urlService.getRepositoryPathFromRepositoryUrl(repositoryUrl); + private void protectBranch(VcsRepositoryUri repositoryUri, String branch) { + final var repositoryPath = uriService.getRepositoryPathFromRepositoryUri(repositoryUri); // we have to first unprotect the branch in order to set the correct access level, this is the case, because the main branch is protected for maintainers by default // Unprotect the branch in 8 seconds first and then protect the branch in 12 seconds. // We do this to wait on any async calls to Gitlab and make sure that the branch really exists before protecting it. @@ -213,8 +213,8 @@ private void protectBranch(String repositoryPath, String branch, Long delayTime, } @Override - public void unprotectBranch(VcsRepositoryUrl repositoryUrl, String branch) throws VersionControlException { - final var repositoryPath = urlService.getRepositoryPathFromRepositoryUrl(repositoryUrl); + public void unprotectBranch(VcsRepositoryUri repositoryUri, String branch) throws VersionControlException { + final var repositoryPath = uriService.getRepositoryPathFromRepositoryUri(repositoryUri); // Unprotect the branch in 10 seconds. We do this to wait on any async calls to Gitlab and make sure that the branch really exists before unprotecting it. unprotectBranch(repositoryPath, branch, 10L, TimeUnit.SECONDS); } @@ -240,13 +240,13 @@ private void unprotectBranch(String repositoryPath, String branch, Long delayTim } @Override - protected void addWebHook(VcsRepositoryUrl repositoryUrl, String notificationUrl, String webHookName) { - addAuthenticatedWebHook(repositoryUrl, notificationUrl, webHookName, "noSecretNeeded"); + protected void addWebHook(VcsRepositoryUri repositoryUri, String notificationUrl, String webHookName) { + addAuthenticatedWebHook(repositoryUri, notificationUrl, webHookName, "noSecretNeeded"); } @Override - protected void addAuthenticatedWebHook(VcsRepositoryUrl repositoryUrl, String notificationUrl, String webHookName, String secretToken) { - final var repositoryPath = urlService.getRepositoryPathFromRepositoryUrl(repositoryUrl); + protected void addAuthenticatedWebHook(VcsRepositoryUri repositoryUri, String notificationUrl, String webHookName, String secretToken) { + final var repositoryPath = uriService.getRepositoryPathFromRepositoryUri(repositoryUri); final var hook = new ProjectHook().withPushEvents(true).withIssuesEvents(false).withMergeRequestsEvents(false).withWikiPageEvents(false) // Note: Trigger hook on push events for matching branches only (this avoids unnecessary Jenkins builds for pushes to other branches) .withPushEventsBranchFilter(defaultBranch); @@ -255,7 +255,7 @@ protected void addAuthenticatedWebHook(VcsRepositoryUrl repositoryUrl, String no gitlab.getProjectApi().addHook(repositoryPath, notificationUrl, hook, false, secretToken); } catch (GitLabApiException e) { - throw new GitLabException("Unable to add webhook for " + repositoryUrl, e); + throw new GitLabException("Unable to add webhook for " + repositoryUri, e); } } @@ -273,9 +273,9 @@ public void deleteProject(String projectKey) { } @Override - public void deleteRepository(VcsRepositoryUrl repositoryUrl) { - final var repositoryPath = urlService.getRepositoryPathFromRepositoryUrl(repositoryUrl); - final var repositoryName = urlService.getRepositorySlugFromRepositoryUrl(repositoryUrl); + public void deleteRepository(VcsRepositoryUri repositoryUri) { + final var repositoryPath = uriService.getRepositoryPathFromRepositoryUri(repositoryUri); + final var repositoryName = uriService.getRepositorySlugFromRepositoryUri(repositoryUri); try { gitlab.getProjectApi().deleteProject(repositoryPath); } @@ -288,21 +288,21 @@ public void deleteRepository(VcsRepositoryUrl repositoryUrl) { } @Override - public VcsRepositoryUrl getCloneRepositoryUrl(String projectKey, String repositorySlug) { - return new GitLabRepositoryUrl(projectKey, repositorySlug); + public VcsRepositoryUri getCloneRepositoryUri(String projectKey, String repositorySlug) { + return new GitLabRepositoryUri(projectKey, repositorySlug); } @Override - public Boolean repositoryUrlIsValid(@Nullable VcsRepositoryUrl repositoryUrl) { - if (repositoryUrl == null || repositoryUrl.getURI() == null) { + public Boolean repositoryUriIsValid(@Nullable VcsRepositoryUri repositoryUri) { + if (repositoryUri == null || repositoryUri.getURI() == null) { return false; } - final var repositoryPath = urlService.getRepositoryPathFromRepositoryUrl(repositoryUrl); + final var repositoryPath = uriService.getRepositoryPathFromRepositoryUri(repositoryUri); try { gitlab.getProjectApi().getProject(repositoryPath); } catch (Exception emAll) { - log.warn("Invalid repository VcsRepositoryUrl {}", repositoryUrl); + log.warn("Invalid repository VcsRepositoryUri {}", repositoryUri); return false; } @@ -312,33 +312,29 @@ public Boolean repositoryUrlIsValid(@Nullable VcsRepositoryUrl repositoryUrl) { @Override public Commit getLastCommitDetails(Object requestBody) throws VersionControlException { final var details = GitLabPushNotificationDTO.convert(requestBody); - final var commit = new Commit(); // Gitlab specifically provide the previous latest commit and the new latest commit after the given push event // Here we retrieve the hash of the new latest commit final var gitLabCommitHash = details.getNewHash(); - commit.setCommitHash(gitLabCommitHash); // Here we search for the commit details for the given commit hash // Technically these details should always be present but as this could change, we handle the edge case final var firstMatchingCommit = details.getCommits().stream().filter(com -> gitLabCommitHash.equals(com.getHash())).findFirst(); if (firstMatchingCommit.isPresent()) { // Fill commit with commit details final var gitLabCommit = firstMatchingCommit.get(); - commit.setMessage(gitLabCommit.getMessage()); - commit.setAuthorEmail(gitLabCommit.getAuthor().getEmail()); - commit.setAuthorName(gitLabCommit.getAuthor().getName()); final var ref = details.getRef().split("/"); - commit.setBranch(ref[ref.length - 1]); + var branch = ref[ref.length - 1]; + var author = gitLabCommit.getAuthor(); + return new Commit(gitLabCommitHash, author.getName(), gitLabCommit.getMessage(), author.getEmail(), branch); } - - return commit; + return new Commit(gitLabCommitHash, null, null, null, null); } @Override public ZonedDateTime getPushDate(ProgrammingExerciseParticipation participation, String commitHash, Object eventObject) { // Gitlab never provides the push date initially so we can ignore the eventObject try { - String repositoryUrl = urlService.getRepositoryPathFromRepositoryUrl(participation.getVcsRepositoryUrl()); - Stream eventStream = gitlab.getEventsApi().getProjectEventsStream(repositoryUrl, Constants.ActionType.PUSHED, null, null, null, Constants.SortOrder.DESC); + String repositoryUri = uriService.getRepositoryPathFromRepositoryUri(participation.getVcsRepositoryUri()); + Stream eventStream = gitlab.getEventsApi().getProjectEventsStream(repositoryUri, Constants.ActionType.PUSHED, null, null, null, Constants.SortOrder.DESC); var eventOptional = eventStream.filter(event -> commitHash.equals(event.getPushData().getCommitTo())).findFirst(); if (eventOptional.isPresent()) { @@ -451,20 +447,20 @@ public boolean checkIfProjectExists(String projectKey, String projectName) { } @Override - public void setRepositoryPermissionsToReadOnly(VcsRepositoryUrl repositoryUrl, String projectKey, Set users) { - users.forEach(user -> updateMemberPermissionInRepository(repositoryUrl, user, VersionControlRepositoryPermission.REPO_READ)); + public void setRepositoryPermissionsToReadOnly(VcsRepositoryUri repositoryUri, String projectKey, Set users) { + users.forEach(user -> updateMemberPermissionInRepository(repositoryUri, user, VersionControlRepositoryPermission.REPO_READ)); } /** * Updates the access level of the user if it's a member of the repository. * - * @param repositoryUrl The url of the repository + * @param repositoryUri The url of the repository * @param user The GitLab user * @param permissions The new access level for the user */ - private void updateMemberPermissionInRepository(VcsRepositoryUrl repositoryUrl, User user, VersionControlRepositoryPermission permissions) { + private void updateMemberPermissionInRepository(VcsRepositoryUri repositoryUri, User user, VersionControlRepositoryPermission permissions) { final var userId = gitLabUserManagementService.getUserId(user.getLogin()); - final var repositoryPath = urlService.getRepositoryPathFromRepositoryUrl(repositoryUrl); + final var repositoryPath = uriService.getRepositoryPathFromRepositoryUri(repositoryUri); try { final Optional member = gitlab.getProjectApi().getOptionalMember(repositoryPath, userId); if (member.isPresent()) { @@ -472,7 +468,7 @@ private void updateMemberPermissionInRepository(VcsRepositoryUrl repositoryUrl, gitlab.getProjectApi().updateMember(repositoryPath, userId, accessLevel); } else { - addMemberToRepository(repositoryUrl, user, permissions); + addMemberToRepository(repositoryUri, user, permissions); } } catch (GitLabApiException e) { @@ -527,16 +523,16 @@ public UriComponentsBuilder buildEndpoint(String baseUrl, Object... args) { } } - public final class GitLabRepositoryUrl extends VcsRepositoryUrl { + public final class GitLabRepositoryUri extends VcsRepositoryUri { - public GitLabRepositoryUrl(String projectKey, String repositorySlug) { + public GitLabRepositoryUri(String projectKey, String repositorySlug) { final var path = projectKey + "/" + repositorySlug; final var urlString = gitlabServerUrl + "/" + path + ".git"; stringToURL(urlString); } - private GitLabRepositoryUrl(String urlString) { + private GitLabRepositoryUri(String urlString) { stringToURL(urlString); } @@ -550,9 +546,9 @@ private void stringToURL(String urlString) { } @Override - public VcsRepositoryUrl withUser(String username) { + public VcsRepositoryUri withUser(String username) { this.username = username; - return new GitLabRepositoryUrl(uri.toString().replaceAll("(https?://)(.*)", "$1" + username + "@$2")); + return new GitLabRepositoryUri(uri.toString().replaceAll("(https?://)(.*)", "$1" + username + "@$2")); } } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCIService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCIService.java index 5a31f279ec2a..398ab8b6d326 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCIService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCIService.java @@ -23,13 +23,13 @@ import de.tum.in.www1.artemis.domain.BuildPlan; import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.ProgrammingSubmission; -import de.tum.in.www1.artemis.domain.VcsRepositoryUrl; +import de.tum.in.www1.artemis.domain.VcsRepositoryUri; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation; import de.tum.in.www1.artemis.exception.ContinuousIntegrationException; import de.tum.in.www1.artemis.exception.GitLabCIException; import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.service.BuildLogEntryService; -import de.tum.in.www1.artemis.service.UrlService; +import de.tum.in.www1.artemis.service.UriService; import de.tum.in.www1.artemis.service.connectors.ConnectorHealth; import de.tum.in.www1.artemis.service.connectors.ci.AbstractContinuousIntegrationService; import de.tum.in.www1.artemis.service.connectors.ci.CIPermission; @@ -70,7 +70,7 @@ public class GitLabCIService extends AbstractContinuousIntegrationService { private final GitLabApi gitlab; - private final UrlService urlService; + private final UriService uriService; private final BuildPlanRepository buildPlanRepository; @@ -97,27 +97,27 @@ public class GitLabCIService extends AbstractContinuousIntegrationService { private String gitlabToken; public GitLabCIService(ProgrammingSubmissionRepository programmingSubmissionRepository, FeedbackRepository feedbackRepository, BuildLogEntryService buildLogService, - GitLabApi gitlab, UrlService urlService, BuildPlanRepository buildPlanRepository, GitLabCIBuildPlanService buildPlanService, + GitLabApi gitlab, UriService uriService, BuildPlanRepository buildPlanRepository, GitLabCIBuildPlanService buildPlanService, ProgrammingLanguageConfiguration programmingLanguageConfiguration, BuildLogStatisticsEntryRepository buildLogStatisticsEntryRepository, TestwiseCoverageService testwiseCoverageService) { super(programmingSubmissionRepository, feedbackRepository, buildLogService, buildLogStatisticsEntryRepository, testwiseCoverageService); this.gitlab = gitlab; - this.urlService = urlService; + this.uriService = uriService; this.buildPlanRepository = buildPlanRepository; this.buildPlanService = buildPlanService; this.programmingLanguageConfiguration = programmingLanguageConfiguration; } @Override - public void createBuildPlanForExercise(ProgrammingExercise exercise, String planKey, VcsRepositoryUrl repositoryURL, VcsRepositoryUrl testRepositoryURL, - VcsRepositoryUrl solutionRepositoryURL) { + public void createBuildPlanForExercise(ProgrammingExercise exercise, String planKey, VcsRepositoryUri repositoryUri, VcsRepositoryUri testRepositoryUri, + VcsRepositoryUri solutionRepositoryUri) { addBuildPlanToProgrammingExerciseIfUnset(exercise); - setupGitLabCIConfiguration(repositoryURL, exercise, planKey); - // TODO: triggerBuild(repositoryURL, exercise.getBranch()); + setupGitLabCIConfiguration(repositoryUri, exercise, planKey); + // TODO: triggerBuild(repositoryUri, exercise.getBranch()); } - private void setupGitLabCIConfiguration(VcsRepositoryUrl repositoryURL, ProgrammingExercise exercise, String buildPlanId) { - final String repositoryPath = urlService.getRepositoryPathFromRepositoryUrl(repositoryURL); + private void setupGitLabCIConfiguration(VcsRepositoryUri repositoryUri, ProgrammingExercise exercise, String buildPlanId) { + final String repositoryPath = uriService.getRepositoryPathFromRepositoryUri(repositoryUri); ProjectApi projectApi = gitlab.getProjectApi(); try { Project project = projectApi.getProject(repositoryPath); @@ -132,7 +132,7 @@ private void setupGitLabCIConfiguration(VcsRepositoryUrl repositoryURL, Programm projectApi.updateProject(project); } catch (GitLabApiException e) { - throw new GitLabCIException("Error enabling CI for " + repositoryURL.toString(), e); + throw new GitLabCIException("Error enabling CI for " + repositoryUri, e); } try { @@ -149,14 +149,14 @@ private void setupGitLabCIConfiguration(VcsRepositoryUrl repositoryURL, Programm updateVariable(repositoryPath, VARIABLE_NOTIFICATION_URL_NAME, artemisServerUrl.toExternalForm() + NEW_RESULT_RESOURCE_API_PATH); updateVariable(repositoryPath, VARIABLE_SUBMISSION_GIT_BRANCH_NAME, exercise.getBranch()); updateVariable(repositoryPath, VARIABLE_TEST_GIT_BRANCH_NAME, exercise.getBranch()); - updateVariable(repositoryPath, VARIABLE_TEST_GIT_REPOSITORY_SLUG_NAME, urlService.getRepositorySlugFromRepositoryUrlString(exercise.getTestRepositoryUrl())); + updateVariable(repositoryPath, VARIABLE_TEST_GIT_REPOSITORY_SLUG_NAME, uriService.getRepositorySlugFromRepositoryUriString(exercise.getTestRepositoryUri())); // TODO: Use a token that is only valid for the test repository for each programming exercise updateVariable(repositoryPath, VARIABLE_TEST_GIT_TOKEN, gitlabToken); updateVariable(repositoryPath, VARIABLE_TEST_GIT_USER, gitlabUser); updateVariable(repositoryPath, VARIABLE_TEST_RESULTS_DIR_NAME, "target/surefire-reports"); } catch (GitLabApiException e) { - log.error("Error creating variable for {} The variables may already have been created.", repositoryURL, e); + log.error("Error creating variable for {} The variables may already have been created.", repositoryUri, e); } } @@ -183,11 +183,11 @@ private void addBuildPlanToProgrammingExerciseIfUnset(ProgrammingExercise progra public void recreateBuildPlansForExercise(ProgrammingExercise exercise) { addBuildPlanToProgrammingExerciseIfUnset(exercise); - VcsRepositoryUrl templateUrl = exercise.getVcsTemplateRepositoryUrl(); + VcsRepositoryUri templateUrl = exercise.getVcsTemplateRepositoryUri(); setupGitLabCIConfiguration(templateUrl, exercise, exercise.getTemplateBuildPlanId()); // TODO: triggerBuild(templateUrl, exercise.getBranch()); - VcsRepositoryUrl solutionUrl = exercise.getVcsSolutionRepositoryUrl(); + VcsRepositoryUri solutionUrl = exercise.getVcsSolutionRepositoryUri(); setupGitLabCIConfiguration(solutionUrl, exercise, exercise.getSolutionBuildPlanId()); // TODO: triggerBuild(solutionUrl, exercise.getBranch()); } @@ -205,7 +205,7 @@ public String copyBuildPlan(ProgrammingExercise sourceExercise, String sourcePla @Override public void configureBuildPlan(ProgrammingExerciseParticipation participation, String defaultBranch) { - setupGitLabCIConfiguration(participation.getVcsRepositoryUrl(), participation.getProgrammingExercise(), participation.getBuildPlanId()); + setupGitLabCIConfiguration(participation.getVcsRepositoryUri(), participation.getProgrammingExercise(), participation.getBuildPlanId()); } @Override @@ -249,7 +249,7 @@ private BuildStatus convertPipelineStatusToBuildStatus(PipelineStatus status) { } private Optional getLatestPipeline(final ProgrammingExerciseParticipation participation) throws GitLabApiException { - final String repositoryPath = urlService.getRepositoryPathFromRepositoryUrl(participation.getVcsRepositoryUrl()); + final String repositoryPath = uriService.getRepositoryPathFromRepositoryUri(participation.getVcsRepositoryUri()); final Optional commitHash = participation.findLatestSubmission().map(ProgrammingSubmission.class::cast).map(ProgrammingSubmission::getCommitHash); if (commitHash.isEmpty()) { return Optional.empty(); @@ -282,7 +282,7 @@ public void enablePlan(String projectKey, String planKey) { } @Override - public void updatePlanRepository(String buildProjectKey, String buildPlanKey, String ciRepoName, String repoProjectKey, String newRepoUrl, String existingRepoUrl, + public void updatePlanRepository(String buildProjectKey, String buildPlanKey, String ciRepoName, String repoProjectKey, String newRepoUri, String existingRepoUri, String newDefaultBranch) { log.error("Unsupported action: GitLabCIService.updatePlanRepository()"); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCITriggerService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCITriggerService.java index 5d197f3e219e..099e76e55ccd 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCITriggerService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCITriggerService.java @@ -6,11 +6,11 @@ import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; -import de.tum.in.www1.artemis.domain.VcsRepositoryUrl; +import de.tum.in.www1.artemis.domain.VcsRepositoryUri; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation; import de.tum.in.www1.artemis.exception.ContinuousIntegrationException; import de.tum.in.www1.artemis.exception.GitLabCIException; -import de.tum.in.www1.artemis.service.UrlService; +import de.tum.in.www1.artemis.service.UriService; import de.tum.in.www1.artemis.service.connectors.ci.ContinuousIntegrationTriggerService; @Profile("gitlabci") @@ -19,20 +19,20 @@ public class GitLabCITriggerService implements ContinuousIntegrationTriggerServi private final GitLabApi gitlab; - private final UrlService urlService; + private final UriService uriService; - public GitLabCITriggerService(GitLabApi gitlab, UrlService urlService) { + public GitLabCITriggerService(GitLabApi gitlab, UriService uriService) { this.gitlab = gitlab; - this.urlService = urlService; + this.uriService = uriService; } @Override public void triggerBuild(ProgrammingExerciseParticipation participation) throws ContinuousIntegrationException { - triggerBuild(participation.getVcsRepositoryUrl(), participation.getProgrammingExercise().getBranch()); + triggerBuild(participation.getVcsRepositoryUri(), participation.getProgrammingExercise().getBranch()); } - private void triggerBuild(VcsRepositoryUrl vcsRepositoryUrl, String branch) { - final String repositoryPath = urlService.getRepositoryPathFromRepositoryUrl(vcsRepositoryUrl); + private void triggerBuild(VcsRepositoryUri vcsRepositoryUri, String branch) { + final String repositoryPath = uriService.getRepositoryPathFromRepositoryUri(vcsRepositoryUri); try { Trigger trigger = gitlab.getPipelineApi().createPipelineTrigger(repositoryPath, "Trigger build"); gitlab.getPipelineApi().triggerPipeline(repositoryPath, trigger, branch, null); diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsService.java index 2cef017be790..d0bdf15b4179 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsService.java @@ -19,7 +19,7 @@ import com.offbytwo.jenkins.JenkinsServer; import de.tum.in.www1.artemis.domain.ProgrammingExercise; -import de.tum.in.www1.artemis.domain.VcsRepositoryUrl; +import de.tum.in.www1.artemis.domain.VcsRepositoryUri; import de.tum.in.www1.artemis.domain.enumeration.BuildPlanType; import de.tum.in.www1.artemis.domain.enumeration.RepositoryType; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation; @@ -72,9 +72,9 @@ public JenkinsService(JenkinsServer jenkinsServer, ProgrammingSubmissionReposito } @Override - public void createBuildPlanForExercise(ProgrammingExercise exercise, String planKey, VcsRepositoryUrl repositoryURL, VcsRepositoryUrl testRepositoryURL, - VcsRepositoryUrl solutionRepositoryURL) { - jenkinsBuildPlanService.createBuildPlanForExercise(exercise, planKey, repositoryURL); + public void createBuildPlanForExercise(ProgrammingExercise exercise, String planKey, VcsRepositoryUri repositoryUri, VcsRepositoryUri testRepositoryUri, + VcsRepositoryUri solutionRepositoryUri) { + jenkinsBuildPlanService.createBuildPlanForExercise(exercise, planKey, repositoryUri); } @Override @@ -109,9 +109,9 @@ public void configureBuildPlan(ProgrammingExerciseParticipation participation, S } @Override - public void updatePlanRepository(String buildProjectKey, String buildPlanKey, String ciRepoName, String repoProjectKey, String newRepoUrl, String existingRepoUrl, + public void updatePlanRepository(String buildProjectKey, String buildPlanKey, String ciRepoName, String repoProjectKey, String newRepoUri, String existingRepoUri, String newDefaultBranch) { - jenkinsBuildPlanService.updateBuildPlanRepositories(buildProjectKey, buildPlanKey, newRepoUrl, existingRepoUrl); + jenkinsBuildPlanService.updateBuildPlanRepositories(buildProjectKey, buildPlanKey, newRepoUri, existingRepoUri); } @Override diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsXmlConfigBuilder.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsXmlConfigBuilder.java index 3734f2f3a2ff..f3be82b5d662 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsXmlConfigBuilder.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsXmlConfigBuilder.java @@ -4,7 +4,7 @@ import org.w3c.dom.Document; -import de.tum.in.www1.artemis.domain.VcsRepositoryUrl; +import de.tum.in.www1.artemis.domain.VcsRepositoryUri; import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; import de.tum.in.www1.artemis.domain.enumeration.ProjectType; @@ -13,17 +13,17 @@ public interface JenkinsXmlConfigBuilder { /** * Contains the URLs required to set up the build plan. *

- * The URLs should haven been converted to the internal URL format using {@link JenkinsInternalUrlService#toInternalVcsUrl(VcsRepositoryUrl)}. + * The URLs should haven been converted to the internal URL format using {@link JenkinsInternalUrlService#toInternalVcsUrl(VcsRepositoryUri)}. * - * @param assignmentRepositoryUrl The URL to the VCS repository of the student. - * @param testRepositoryUrl The URL to the VCS repository of the tests for the exercise. - * @param solutionRepositoryUrl The URL to the VCS repository of the solution of the exercise. + * @param assignmentRepositoryUri The URL to the VCS repository of the student. + * @param testRepositoryUri The URL to the VCS repository of the tests for the exercise. + * @param solutionRepositoryUri The URL to the VCS repository of the solution of the exercise. */ - record InternalVcsRepositoryURLs(VcsRepositoryUrl assignmentRepositoryUrl, VcsRepositoryUrl testRepositoryUrl, VcsRepositoryUrl solutionRepositoryUrl) { + record InternalVcsRepositoryURLs(VcsRepositoryUri assignmentRepositoryUri, VcsRepositoryUri testRepositoryUri, VcsRepositoryUri solutionRepositoryUri) { } /** - * Creates a basic build config for Jenkins based on the given repository URLs. I.e. a build that tests the assignment + * Creates a basic build config for Jenkins based on the given repository URIs. I.e. a build that tests the assignment * code and exports the build results to Artemis afterwards. If static code analysis is activated, the plan will additionally * execute supported static code analysis tools. * diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsBuildPlanCreator.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsBuildPlanCreator.java index a24727dca8a9..de9fff3e4ba1 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsBuildPlanCreator.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsBuildPlanCreator.java @@ -137,9 +137,9 @@ private String makeSafeForXml(final String script) { private Map getReplacements(final InternalVcsRepositoryURLs internalVcsRepositoryURLs, final boolean checkoutSolution, final String buildPlanUrl) { final Map replacements = new HashMap<>(); - replacements.put(REPLACE_TEST_REPO, internalVcsRepositoryURLs.testRepositoryUrl().getURI().toString()); - replacements.put(REPLACE_ASSIGNMENT_REPO, internalVcsRepositoryURLs.assignmentRepositoryUrl().getURI().toString()); - replacements.put(REPLACE_SOLUTION_REPO, internalVcsRepositoryURLs.solutionRepositoryUrl().getURI().toString()); + replacements.put(REPLACE_TEST_REPO, internalVcsRepositoryURLs.testRepositoryUri().getURI().toString()); + replacements.put(REPLACE_ASSIGNMENT_REPO, internalVcsRepositoryURLs.assignmentRepositoryUri().getURI().toString()); + replacements.put(REPLACE_SOLUTION_REPO, internalVcsRepositoryURLs.solutionRepositoryUri().getURI().toString()); replacements.put(REPLACE_GIT_CREDENTIALS, gitCredentialsKey); replacements.put(REPLACE_ASSIGNMENT_CHECKOUT_PATH, Constants.ASSIGNMENT_CHECKOUT_PATH); replacements.put(REPLACE_TESTS_CHECKOUT_PATH, Constants.TESTS_CHECKOUT_PATH); diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsBuildPlanService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsBuildPlanService.java index adfbae97df68..d620a543ce39 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsBuildPlanService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsBuildPlanService.java @@ -102,16 +102,16 @@ public JenkinsBuildPlanService(@Qualifier("jenkinsRestTemplate") RestTemplate re * * @param exercise the programming exercise * @param planKey the name of the plan - * @param repositoryURL the url of the vcs repository + * @param repositoryUri the uri of the vcs repository */ - public void createBuildPlanForExercise(ProgrammingExercise exercise, String planKey, VcsRepositoryUrl repositoryURL) { - final JenkinsXmlConfigBuilder.InternalVcsRepositoryURLs internalRepositoryUrls = getInternalRepositoryUrls(exercise, repositoryURL); + public void createBuildPlanForExercise(ProgrammingExercise exercise, String planKey, VcsRepositoryUri repositoryUri) { + final JenkinsXmlConfigBuilder.InternalVcsRepositoryURLs internalRepositoryUris = getInternalRepositoryUris(exercise, repositoryUri); final ProgrammingLanguage programmingLanguage = exercise.getProgrammingLanguage(); final var configBuilder = builderFor(programmingLanguage, exercise.getProjectType()); final String buildPlanUrl = jenkinsPipelineScriptCreator.generateBuildPlanURL(exercise); final boolean checkoutSolution = exercise.getCheckoutSolutionRepository(); - final Document jobConfig = configBuilder.buildBasicConfig(programmingLanguage, Optional.ofNullable(exercise.getProjectType()), internalRepositoryUrls, checkoutSolution, + final Document jobConfig = configBuilder.buildBasicConfig(programmingLanguage, Optional.ofNullable(exercise.getProjectType()), internalRepositoryUris, checkoutSolution, buildPlanUrl); // create build plan in database first, otherwise the job in Jenkins cannot find it for the initial build @@ -125,10 +125,10 @@ public void createBuildPlanForExercise(ProgrammingExercise exercise, String plan triggerBuild(jobFolder, job); } - private JenkinsXmlConfigBuilder.InternalVcsRepositoryURLs getInternalRepositoryUrls(final ProgrammingExercise exercise, final VcsRepositoryUrl assignmentRepositoryUrl) { - final VcsRepositoryUrl assignmentUrl = jenkinsInternalUrlService.toInternalVcsUrl(assignmentRepositoryUrl); - final VcsRepositoryUrl testUrl = jenkinsInternalUrlService.toInternalVcsUrl(exercise.getRepositoryURL(RepositoryType.TESTS)); - final VcsRepositoryUrl solutionUrl = jenkinsInternalUrlService.toInternalVcsUrl(exercise.getRepositoryURL(RepositoryType.SOLUTION)); + private JenkinsXmlConfigBuilder.InternalVcsRepositoryURLs getInternalRepositoryUris(final ProgrammingExercise exercise, final VcsRepositoryUri assignmentRepositoryUri) { + final VcsRepositoryUri assignmentUrl = jenkinsInternalUrlService.toInternalVcsUrl(assignmentRepositoryUri); + final VcsRepositoryUri testUrl = jenkinsInternalUrlService.toInternalVcsUrl(exercise.getRepositoryURL(RepositoryType.TESTS)); + final VcsRepositoryUri solutionUrl = jenkinsInternalUrlService.toInternalVcsUrl(exercise.getRepositoryURL(RepositoryType.SOLUTION)); return new JenkinsXmlConfigBuilder.InternalVcsRepositoryURLs(assignmentUrl, testUrl, solutionUrl); } @@ -166,8 +166,8 @@ public void configureBuildPlanForParticipation(ProgrammingExerciseParticipation String projectKey = programmingExercise.getProjectKey(); String planKey = participation.getBuildPlanId(); - String templateRepoUrl = programmingExercise.getTemplateRepositoryUrl(); - updateBuildPlanRepositories(projectKey, planKey, participation.getRepositoryUrl(), templateRepoUrl); + String templateRepoUri = programmingExercise.getTemplateRepositoryUri(); + updateBuildPlanRepositories(projectKey, planKey, participation.getRepositoryUri(), templateRepoUri); enablePlan(projectKey, planKey); // Students currently always have access to the build plan in Jenkins @@ -178,19 +178,19 @@ public void configureBuildPlanForParticipation(ProgrammingExerciseParticipation * * @param buildProjectKey the project key of the programming exercise * @param buildPlanKey the build plan id of the participation - * @param newRepoUrl the repository url that will replace the old url - * @param existingRepoUrl the old repository url that will be replaced + * @param newRepoUri the repository uri that will replace the old url + * @param existingRepoUri the old repository uri that will be replaced */ - public void updateBuildPlanRepositories(String buildProjectKey, String buildPlanKey, String newRepoUrl, String existingRepoUrl) { - newRepoUrl = jenkinsInternalUrlService.toInternalVcsUrl(newRepoUrl); - existingRepoUrl = jenkinsInternalUrlService.toInternalVcsUrl(existingRepoUrl); + public void updateBuildPlanRepositories(String buildProjectKey, String buildPlanKey, String newRepoUri, String existingRepoUri) { + newRepoUri = jenkinsInternalUrlService.toInternalVcsUrl(newRepoUri); + existingRepoUri = jenkinsInternalUrlService.toInternalVcsUrl(existingRepoUri); - // remove potential username from repo URL. Jenkins uses the Artemis Admin user and will fail if other usernames are in the URL - final var repoUrl = newRepoUrl.replaceAll("(https?://)(.*@)(.*)", "$1$3"); + // remove potential username from repo URI. Jenkins uses the Artemis Admin user and will fail if other usernames are in the URI + final var repoUri = newRepoUri.replaceAll("(https?://)(.*@)(.*)", "$1$3"); final Document jobConfig = jenkinsJobService.getJobConfigForJobInFolder(buildProjectKey, buildPlanKey); try { - JenkinsBuildPlanUtils.replaceScriptParameters(jobConfig, existingRepoUrl, repoUrl); + JenkinsBuildPlanUtils.replaceScriptParameters(jobConfig, existingRepoUri, repoUri); } catch (IllegalArgumentException e) { log.error("Pipeline Script not found", e); diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsBuildPlanUtils.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsBuildPlanUtils.java index 834d73ab52b3..f4cd04d35e8b 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsBuildPlanUtils.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/build_plan/JenkinsBuildPlanUtils.java @@ -8,7 +8,7 @@ public class JenkinsBuildPlanUtils { private static final String PIPELINE_SCRIPT_DETECTION_COMMENT = "// ARTEMIS: JenkinsPipeline"; /** - * Replaces either one of the previous repository urls or the build plan url written within the Jenkins pipeline + * Replaces either one of the previous repository uris or the build plan url written within the Jenkins pipeline * script with the value specified by newUrl. * * @param jobXmlDocument the Jenkins pipeline diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIBuildJobExecutionService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIBuildJobExecutionService.java index 3c4cd6963fbb..b2da7f0cca57 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIBuildJobExecutionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIBuildJobExecutionService.java @@ -50,7 +50,7 @@ import de.tum.in.www1.artemis.service.connectors.localci.scaparser.exception.UnsupportedToolException; import de.tum.in.www1.artemis.service.connectors.localci.scaparser.strategy.ParserPolicy; import de.tum.in.www1.artemis.service.connectors.localci.scaparser.strategy.ParserStrategy; -import de.tum.in.www1.artemis.service.connectors.localvc.LocalVCRepositoryUrl; +import de.tum.in.www1.artemis.service.connectors.localvc.LocalVCRepositoryUri; import de.tum.in.www1.artemis.service.connectors.vcs.VersionControlService; import de.tum.in.www1.artemis.service.dto.StaticCodeAnalysisReportDTO; import de.tum.in.www1.artemis.service.programming.ProgrammingLanguageFeature; @@ -179,24 +179,24 @@ public LocalCIBuildResult runBuildJob(ProgrammingExerciseParticipation participa // Retrieve the paths to the repositories that the build job needs. // This includes the assignment repository (the one to be tested, e.g. the student's repository, or the template repository), and the tests repository which includes // the tests to be executed. - LocalVCRepositoryUrl assignmentRepositoryUrl; - LocalVCRepositoryUrl testsRepositoryUrl; - LocalVCRepositoryUrl[] auxiliaryRepositoriesUrls; + LocalVCRepositoryUri assignmentRepositoryUri; + LocalVCRepositoryUri testsRepositoryUri; + LocalVCRepositoryUri[] auxiliaryRepositoriesUris; Path[] auxiliaryRepositoriesPaths; String[] auxiliaryRepositoryCheckoutDirectories; try { - assignmentRepositoryUrl = new LocalVCRepositoryUrl(participation.getRepositoryUrl(), localVCBaseUrl); - testsRepositoryUrl = new LocalVCRepositoryUrl(participation.getProgrammingExercise().getTestRepositoryUrl(), localVCBaseUrl); + assignmentRepositoryUri = new LocalVCRepositoryUri(participation.getRepositoryUri(), localVCBaseUrl); + testsRepositoryUri = new LocalVCRepositoryUri(participation.getProgrammingExercise().getTestRepositoryUri(), localVCBaseUrl); if (!auxiliaryRepositories.isEmpty()) { - auxiliaryRepositoriesUrls = new LocalVCRepositoryUrl[auxiliaryRepositories.size()]; + auxiliaryRepositoriesUris = new LocalVCRepositoryUri[auxiliaryRepositories.size()]; auxiliaryRepositoriesPaths = new Path[auxiliaryRepositories.size()]; auxiliaryRepositoryCheckoutDirectories = new String[auxiliaryRepositories.size()]; for (int i = 0; i < auxiliaryRepositories.size(); i++) { - auxiliaryRepositoriesUrls[i] = new LocalVCRepositoryUrl(auxiliaryRepositories.get(i).getRepositoryUrl(), localVCBaseUrl); - auxiliaryRepositoriesPaths[i] = auxiliaryRepositoriesUrls[i].getRepoClonePath(repoClonePath).toAbsolutePath(); + auxiliaryRepositoriesUris[i] = new LocalVCRepositoryUri(auxiliaryRepositories.get(i).getRepositoryUri(), localVCBaseUrl); + auxiliaryRepositoriesPaths[i] = auxiliaryRepositoriesUris[i].getRepoClonePath(repoClonePath).toAbsolutePath(); auxiliaryRepositoryCheckoutDirectories[i] = auxiliaryRepositories.get(i).getCheckoutDirectory(); } } @@ -206,11 +206,11 @@ public LocalCIBuildResult runBuildJob(ProgrammingExerciseParticipation participa } } catch (LocalVCInternalException e) { - throw new LocalCIException("Error while creating LocalVCRepositoryUrl", e); + throw new LocalCIException("Error while creating LocalVCRepositoryUri", e); } - Path assignmentRepositoryPath = assignmentRepositoryUrl.getRepoClonePath(repoClonePath).toAbsolutePath(); - Path testsRepositoryPath = testsRepositoryUrl.getRepoClonePath(repoClonePath).toAbsolutePath(); + Path assignmentRepositoryPath = assignmentRepositoryUri.getRepoClonePath(repoClonePath).toAbsolutePath(); + Path testsRepositoryPath = testsRepositoryUri.getRepoClonePath(repoClonePath).toAbsolutePath(); Path solutionRepositoryPath = null; if (participation.getProgrammingExercise().getCheckoutSolutionRepository()) { try { @@ -220,14 +220,14 @@ public LocalCIBuildResult runBuildJob(ProgrammingExerciseParticipation participa if (programmingLanguageFeature.checkoutSolutionRepositoryAllowed()) { var solutionParticipation = solutionProgrammingExerciseParticipationRepository.findByProgrammingExerciseId(participation.getProgrammingExercise().getId()); if (solutionParticipation.isPresent()) { - solutionRepositoryPath = new LocalVCRepositoryUrl(solutionParticipation.get().getRepositoryUrl(), localVCBaseUrl).getRepoClonePath(repoClonePath) + solutionRepositoryPath = new LocalVCRepositoryUri(solutionParticipation.get().getRepositoryUri(), localVCBaseUrl).getRepoClonePath(repoClonePath) .toAbsolutePath(); } } } } catch (Exception e) { - throw new LocalCIException("Error while creating solution LocalVCRepositoryUrl", e); + throw new LocalCIException("Error while creating solution LocalVCRepositoryUri", e); } } @@ -347,7 +347,7 @@ private LocalCIBuildResult runScriptAndParseResults(ProgrammingExerciseParticipa // Set the build status to "INACTIVE" to indicate that the build is not running anymore. localCIBuildPlanService.updateBuildPlanStatus(participation, ContinuousIntegrationService.BuildStatus.INACTIVE); - log.info("Building and testing submission for repository {} and commit hash {} took {}", participation.getRepositoryUrl(), commitHash, + log.info("Building and testing submission for repository {} and commit hash {} took {}", participation.getRepositoryUri(), commitHash, TimeLogUtil.formatDurationFrom(timeNanoStart)); return buildResult; @@ -619,7 +619,7 @@ private LocalCIBuildResult constructBuildResult(List executeBuildJob(ProgrammingExercise localCIDockerService.pullDockerImage(dockerImage); // Prepare the Docker container name before submitting the build job to the executor service, so we can remove the container if something goes wrong. - String containerName = "local-ci-" + participation.getId() + "-" + ZonedDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")); + String containerName = buildContainerPrefix + participation.getId() + "-" + ZonedDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")); // Prepare a Callable that will later be called. It contains the actual steps needed to execute the build job. Callable buildJob = () -> localCIBuildJobExecutionService.runBuildJob(participation, commitHash, isPushToTestRepository, containerName, dockerImage); @@ -152,7 +155,7 @@ private CompletableFuture createCompletableFuture(Supplier queue; + + private final IMap processingJobs; + + private final IMap buildAgentInformation; + + private final LocalCIBuildQueueWebsocketService localCIBuildQueueWebsocketService; + + private final LocalCISharedBuildJobQueueService localCISharedBuildJobQueueService; + + /** + * Instantiates a new Local ci queue websocket service. + * + * @param hazelcastInstance the hazelcast instance + * @param localCIBuildQueueWebsocketService the local ci build queue websocket service + * @param localCISharedBuildJobQueueService the local ci shared build job queue service + */ + public LocalCIQueueWebsocketService(HazelcastInstance hazelcastInstance, LocalCIBuildQueueWebsocketService localCIBuildQueueWebsocketService, + LocalCISharedBuildJobQueueService localCISharedBuildJobQueueService) { + this.hazelcastInstance = hazelcastInstance; + this.localCIBuildQueueWebsocketService = localCIBuildQueueWebsocketService; + this.localCISharedBuildJobQueueService = localCISharedBuildJobQueueService; + this.queue = this.hazelcastInstance.getQueue("buildJobQueue"); + this.processingJobs = this.hazelcastInstance.getMap("processingJobs"); + this.buildAgentInformation = this.hazelcastInstance.getMap("buildAgentInformation"); + } + + /** + * Add listeners for build job queue changes. + */ + @PostConstruct + public void addListeners() { + this.queue.addItemListener(new QueuedBuildJobItemListener(), true); + this.processingJobs.addLocalEntryListener(new ProcessingBuildJobItemListener()); + this.buildAgentInformation.addLocalEntryListener(new BuildAgentListener()); + // localCIBuildQueueWebsocketService will be autowired only if scheduling is active + Objects.requireNonNull(localCIBuildQueueWebsocketService, "localCIBuildQueueWebsocketService must be non-null when scheduling is active."); + } + + private void sendQueuedJobsOverWebsocket(long courseId) { + localCIBuildQueueWebsocketService.sendQueuedBuildJobs(localCISharedBuildJobQueueService.getQueuedJobs()); + localCIBuildQueueWebsocketService.sendQueuedBuildJobsForCourse(courseId, localCISharedBuildJobQueueService.getQueuedJobsForCourse(courseId)); + } + + private void sendProcessingJobsOverWebsocket(long courseId) { + localCIBuildQueueWebsocketService.sendRunningBuildJobs(localCISharedBuildJobQueueService.getProcessingJobs()); + localCIBuildQueueWebsocketService.sendRunningBuildJobsForCourse(courseId, localCISharedBuildJobQueueService.getProcessingJobsForCourse(courseId)); + } + + private void sendBuildAgentInformationOverWebsocket() { + localCIBuildQueueWebsocketService.sendBuildAgentInformation(localCISharedBuildJobQueueService.getBuildAgentInformation()); + } + + private class QueuedBuildJobItemListener implements ItemListener { + + @Override + public void itemAdded(ItemEvent event) { + sendQueuedJobsOverWebsocket(event.getItem().getCourseId()); + } + + @Override + public void itemRemoved(ItemEvent event) { + sendQueuedJobsOverWebsocket(event.getItem().getCourseId()); + } + } + + private class ProcessingBuildJobItemListener implements EntryAddedListener, EntryRemovedListener { + + @Override + public void entryAdded(com.hazelcast.core.EntryEvent event) { + log.debug("CIBuildJobQueueItem added to processing jobs: {}", event.getValue()); + sendProcessingJobsOverWebsocket(event.getValue().getCourseId()); + } + + @Override + public void entryRemoved(com.hazelcast.core.EntryEvent event) { + log.debug("CIBuildJobQueueItem removed from processing jobs: {}", event.getOldValue()); + sendProcessingJobsOverWebsocket(event.getOldValue().getCourseId()); + } + } + + private class BuildAgentListener implements EntryAddedListener, EntryRemovedListener { + + @Override + public void entryAdded(com.hazelcast.core.EntryEvent event) { + log.debug("Build agent added: {}", event.getValue()); + sendBuildAgentInformationOverWebsocket(); + } + + @Override + public void entryRemoved(com.hazelcast.core.EntryEvent event) { + log.debug("Build agent removed: {}", event.getOldValue()); + sendBuildAgentInformationOverWebsocket(); + } + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIService.java index 7fb2546fa978..212b43cc0a6a 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIService.java @@ -11,7 +11,7 @@ import org.springframework.stereotype.Service; import de.tum.in.www1.artemis.domain.ProgrammingExercise; -import de.tum.in.www1.artemis.domain.VcsRepositoryUrl; +import de.tum.in.www1.artemis.domain.VcsRepositoryUri; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation; import de.tum.in.www1.artemis.exception.BambooException; import de.tum.in.www1.artemis.exception.LocalCIException; @@ -41,8 +41,8 @@ public LocalCIService(ProgrammingSubmissionRepository programmingSubmissionRepos } @Override - public void createBuildPlanForExercise(ProgrammingExercise programmingExercise, String planKey, VcsRepositoryUrl sourceCodeRepositoryURL, VcsRepositoryUrl testRepositoryURL, - VcsRepositoryUrl solutionRepositoryURL) { + public void createBuildPlanForExercise(ProgrammingExercise programmingExercise, String planKey, VcsRepositoryUri repositoryUri, VcsRepositoryUri testRepositoryUri, + VcsRepositoryUri solutionRepositoryUri) { // Not implemented for local CI. no build plans must be created, because all the information for building // a submission and running tests is contained in the participation. } @@ -119,7 +119,7 @@ public void enablePlan(String projectKey, String planKey) throws LocalCIExceptio } @Override - public void updatePlanRepository(String buildProjectKey, String buildPlanKey, String ciRepoName, String repoProjectKey, String newRepoUrl, String existingRepoUrl, + public void updatePlanRepository(String buildProjectKey, String buildPlanKey, String ciRepoName, String repoProjectKey, String newRepoUri, String existingRepoUri, String newBranch) throws LocalCIException { // Not implemented for local CI. No build plans exist. // When a student pushes to a repository, a build is triggered using the information contained in the participation which includes the relevant repository. diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCISharedBuildJobQueueService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCISharedBuildJobQueueService.java index 4c2e70d06dc9..b37a1cfb9ac7 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCISharedBuildJobQueueService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCISharedBuildJobQueueService.java @@ -6,9 +6,10 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; +import javax.annotation.PostConstruct; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Profile; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @@ -62,7 +63,7 @@ public class LocalCISharedBuildJobQueueService { private final IMap buildAgentInformation; - private AtomicInteger localProcessingJobs = new AtomicInteger(0); + private final AtomicInteger localProcessingJobs = new AtomicInteger(0); /** * Lock to prevent multiple nodes from processing the same build job. @@ -74,7 +75,6 @@ public class LocalCISharedBuildJobQueueService { */ private final ReentrantLock instanceLock = new ReentrantLock(); - @Autowired public LocalCISharedBuildJobQueueService(HazelcastInstance hazelcastInstance, ExecutorService localCIBuildExecutorService, LocalCIBuildJobManagementService localCIBuildJobManagementService, ParticipationRepository participationRepository, ProgrammingExerciseGradingService programmingExerciseGradingService, ProgrammingMessagingService programmingMessagingService, @@ -90,7 +90,14 @@ public LocalCISharedBuildJobQueueService(HazelcastInstance hazelcastInstance, Ex this.processingJobs = this.hazelcastInstance.getMap("processingJobs"); this.sharedLock = this.hazelcastInstance.getCPSubsystem().getLock("buildJobQueueLock"); this.queue = this.hazelcastInstance.getQueue("buildJobQueue"); - this.queue.addItemListener(new BuildJobItemListener(), true); + } + + /** + * Add listener to the shared build job queue. + */ + @PostConstruct + public void addListener() { + this.queue.addItemListener(new QueuedBuildJobItemListener(), true); } /** @@ -185,7 +192,7 @@ private void checkAvailabilityAndProcessNextBuild() { updateLocalBuildAgentInformation(); } - log.info("Node has no available threads currently"); + log.debug("Node has no available threads currently"); return; } @@ -348,6 +355,7 @@ private void processBuild(LocalCIBuildJobQueueItem buildJob) { log.warn("Requeueing failed build job: {}", buildJob); buildJob.setRetryCount(buildJob.getRetryCount() + 1); queue.add(buildJob); + checkAvailabilityAndProcessNextBuild(); } else { log.warn("Participation with id {} has been deleted. Cancelling the requeueing of the build job.", participation.getId()); @@ -398,17 +406,17 @@ private ProgrammingExerciseParticipation retrieveParticipationWithRetry(Long par throw new IllegalStateException("Could not retrieve participation with id " + participationId + " from database after " + maxRetries + " retries."); } - private class BuildJobItemListener implements ItemListener { + private class QueuedBuildJobItemListener implements ItemListener { @Override - public void itemAdded(ItemEvent item) { - log.debug("CIBuildJobQueueItem added to queue: {}", item.getItem()); + public void itemAdded(ItemEvent event) { + log.debug("CIBuildJobQueueItem added to queue: {}", event.getItem()); checkAvailabilityAndProcessNextBuild(); } @Override - public void itemRemoved(ItemEvent item) { - log.debug("CIBuildJobQueueItem removed from queue: {}", item.getItem()); + public void itemRemoved(ItemEvent event) { + log.debug("CIBuildJobQueueItem removed from queue: {}", event.getItem()); } } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCITriggerService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCITriggerService.java index 63171c50115a..930f6e58da64 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCITriggerService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCITriggerService.java @@ -47,7 +47,7 @@ public void triggerBuild(ProgrammingExerciseParticipation participation, String ProgrammingExercise programmingExercise = participation.getProgrammingExercise(); long courseId = programmingExercise.getCourseViaExerciseGroupOrCourseMember().getId(); - String repositoryTypeOrUserName = participation.getVcsRepositoryUrl().repositoryNameWithoutProjectKey(); + String repositoryTypeOrUserName = participation.getVcsRepositoryUri().repositoryNameWithoutProjectKey(); if (Objects.equals(repositoryTypeOrUserName, "exercise")) { repositoryTypeOrUserName = "BASE"; diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/dto/LocalCIBuildAgentInformation.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/dto/LocalCIBuildAgentInformation.java index e380d55f4239..8fd5d8cdbb44 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/dto/LocalCIBuildAgentInformation.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/dto/LocalCIBuildAgentInformation.java @@ -4,61 +4,9 @@ import java.io.Serializable; import java.util.List; -public class LocalCIBuildAgentInformation implements Serializable { +public record LocalCIBuildAgentInformation(String name, int maxNumberOfConcurrentBuildJobs, int numberOfCurrentBuildJobs, List runningBuildJobs) + implements Serializable { @Serial private static final long serialVersionUID = 1L; - - private String name; - - private int maxNumberOfConcurrentBuildJobs; - - private int numberOfCurrentBuildJobs; - - private List runningBuildJobs; - - public LocalCIBuildAgentInformation(String name, int maxNumberOfConcurrentBuildJobs, int numberOfCurrentBuildJobs, List runningBuildJobs) { - this.name = name; - this.maxNumberOfConcurrentBuildJobs = maxNumberOfConcurrentBuildJobs; - this.numberOfCurrentBuildJobs = numberOfCurrentBuildJobs; - this.runningBuildJobs = runningBuildJobs; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getMaxNumberOfConcurrentBuildJobs() { - return maxNumberOfConcurrentBuildJobs; - } - - public void setMaxNumberOfConcurrentBuildJobs(int maxNumberOfConcurrentBuildJobs) { - this.maxNumberOfConcurrentBuildJobs = maxNumberOfConcurrentBuildJobs; - } - - public int getNumberOfCurrentBuildJobs() { - return numberOfCurrentBuildJobs; - } - - public void setNumberOfCurrentBuildJobs(int numberOfCurrentBuildJobs) { - this.numberOfCurrentBuildJobs = numberOfCurrentBuildJobs; - } - - public List getRunningBuildJobs() { - return runningBuildJobs; - } - - public void setRunningBuildJobs(List runningBuildJobs) { - this.runningBuildJobs = runningBuildJobs; - } - - @Override - public String toString() { - return "LocalCIBuildAgentInformation{" + "name='" + name + '\'' + ", maxNumberOfConcurrentBuildJobs=" + maxNumberOfConcurrentBuildJobs + ", numberOfCurrentBuildJobs=" - + numberOfCurrentBuildJobs + ", runningBuildJobs=" + runningBuildJobs + '}'; - } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/dto/LocalCIBuildJobQueueItem.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/dto/LocalCIBuildJobQueueItem.java index 936ef041cc5b..e44f67543d9e 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/dto/LocalCIBuildJobQueueItem.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/dto/LocalCIBuildJobQueueItem.java @@ -4,35 +4,36 @@ import java.io.Serializable; import java.time.ZonedDateTime; +// TODO: we should convert this into a record to make objects immutable public class LocalCIBuildJobQueueItem implements Serializable { @Serial private static final long serialVersionUID = 1L; - private long id; + private final long id; - private String name; + private final String name; private String buildAgentAddress; - private long participationId; + private final long participationId; - private String repositoryTypeOrUserName; + private final String repositoryTypeOrUserName; - private String commitHash; + private final String commitHash; - private ZonedDateTime submissionDate; + private final ZonedDateTime submissionDate; private int retryCount; private ZonedDateTime buildStartDate; // 1-5, 1 is highest priority - private int priority; + private final int priority; - private long courseId; + private final long courseId; - private boolean isPushToTestRepository; + private final boolean isPushToTestRepository; public LocalCIBuildJobQueueItem(String name, long participationId, String repositoryTypeOrUserName, String commitHash, ZonedDateTime submissionDate, int priority, long courseId, boolean isPushToTestRepository) { @@ -51,18 +52,10 @@ public long getId() { return id; } - public void setId(long id) { - this.id = id; - } - public String getName() { return name; } - public void setName(String name) { - this.name = name; - } - public String getBuildAgentAddress() { return buildAgentAddress; } @@ -75,34 +68,18 @@ public long getParticipationId() { return participationId; } - public void setParticipationId(long participationId) { - this.participationId = participationId; - } - public String getRepositoryTypeOrUserName() { return repositoryTypeOrUserName; } - public void setRepositoryTypeOrUserName(String repositoryTypeOrUserName) { - this.repositoryTypeOrUserName = repositoryTypeOrUserName; - } - public String getCommitHash() { return commitHash; } - public void setCommitHash(String commitHash) { - this.commitHash = commitHash; - } - public ZonedDateTime getSubmissionDate() { return submissionDate; } - public void setSubmissionDate(ZonedDateTime submissionDate) { - this.submissionDate = submissionDate; - } - public ZonedDateTime getBuildStartDate() { return buildStartDate; } @@ -115,10 +92,6 @@ public int getPriority() { return priority; } - public void setPriority(int priority) { - this.priority = priority; - } - public int getRetryCount() { return retryCount; } @@ -131,10 +104,6 @@ public long getCourseId() { return courseId; } - public void setCourseId(long courseId) { - this.courseId = courseId; - } - /** * When pushing to the test repository, build jobs are triggered for the template and solution repository. * However, getCommitHash() then returns the commit hash of the test repository. @@ -146,10 +115,6 @@ public boolean isPushToTestRepository() { return isPushToTestRepository; } - public void setPushToTestRepository(boolean isPushToTestRepository) { - this.isPushToTestRepository = isPushToTestRepository; - } - @Override public String toString() { return "LocalCIBuildJobQueueItem{" + "id='" + id + '\'' + ", name='" + name + '\'' + ", participationId=" + participationId + ", repositoryTypeOrUserName='" diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/dto/LocalCIBuildResult.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/dto/LocalCIBuildResult.java index 108cc6320aff..c3d431ce8023 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/dto/LocalCIBuildResult.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/dto/LocalCIBuildResult.java @@ -33,7 +33,7 @@ public class LocalCIBuildResult extends AbstractBuildResultNotificationDTO { private List buildLogEntries = new ArrayList<>(); - private List staticCodeAnalysisReports; + private final List staticCodeAnalysisReports; private boolean hasLogs = false; @@ -55,7 +55,7 @@ public ZonedDateTime getBuildRunDate() { @Override public Optional getCommitHashFromAssignmentRepo() { - if (assignmentRepoCommitHash.length() == 0) { + if (assignmentRepoCommitHash.isEmpty()) { return Optional.empty(); } return Optional.of(assignmentRepoCommitHash); @@ -63,7 +63,7 @@ public Optional getCommitHashFromAssignmentRepo() { @Override public Optional getCommitHashFromTestsRepo() { - if (testsRepoCommitHash.length() == 0) { + if (testsRepoCommitHash.isEmpty()) { return Optional.empty(); } return Optional.of(testsRepoCommitHash); diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCRepositoryUrl.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCRepositoryUri.java similarity index 90% rename from src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCRepositoryUrl.java rename to src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCRepositoryUri.java index 68cacd5648a0..593efcbf7a0f 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCRepositoryUrl.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCRepositoryUri.java @@ -6,13 +6,13 @@ import java.nio.file.Path; import java.nio.file.Paths; -import de.tum.in.www1.artemis.domain.VcsRepositoryUrl; +import de.tum.in.www1.artemis.domain.VcsRepositoryUri; import de.tum.in.www1.artemis.exception.localvc.LocalVCInternalException; /** * Represents a URL to a local VC repository. */ -public class LocalVCRepositoryUrl extends VcsRepositoryUrl { +public class LocalVCRepositoryUri extends VcsRepositoryUri { private final String projectKey; @@ -23,20 +23,20 @@ public class LocalVCRepositoryUrl extends VcsRepositoryUrl { private final boolean isPracticeRepository; /** - * Constructor that builds a LocalVCRepositoryUrl from a project key and a repository slug. + * Constructor that builds a LocalVCRepositoryUri from a project key and a repository slug. * * @param projectKey the project key. * @param repositorySlug the repository slug. * @param localVCBaseUrl the base URL of the local VC server defined in an environment variable. * @throws LocalVCInternalException if the project key or repository slug are invalid. */ - public LocalVCRepositoryUrl(String projectKey, String repositorySlug, URL localVCBaseUrl) { + public LocalVCRepositoryUri(String projectKey, String repositorySlug, URL localVCBaseUrl) { final String urlString = localVCBaseUrl + buildRepositoryPath(projectKey, repositorySlug); try { this.uri = new URI(urlString); } catch (URISyntaxException e) { - throw new LocalVCInternalException("Could not create local VC Repository URL", e); + throw new LocalVCInternalException("Could not create local VC Repository URI", e); } this.projectKey = projectKey; @@ -46,21 +46,21 @@ public LocalVCRepositoryUrl(String projectKey, String repositorySlug, URL localV } /** - * Constructor that builds a LocalVCRepositoryUrl from a URL string. + * Constructor that builds a LocalVCRepositoryUri from a URL string. * * @param urlString the enire URL string (should already contain the base URL, otherwise an exception is thrown). * @param localVCBaseUrl the base URL of the local VC server defined in an environment variable. * @throws LocalVCInternalException if the URL string is invalid. */ - public LocalVCRepositoryUrl(String urlString, URL localVCBaseUrl) { + public LocalVCRepositoryUri(String urlString, URL localVCBaseUrl) { if (!urlString.startsWith(localVCBaseUrl.toString())) { - throw new LocalVCInternalException("Invalid local VC Repository URL: " + urlString); + throw new LocalVCInternalException("Invalid local VC Repository URI: " + urlString); } Path urlPath = Paths.get(urlString.replaceFirst(localVCBaseUrl.toString(), "")); if (!("git".equals(urlPath.getName(0).toString())) || !urlPath.getName(2).toString().endsWith(".git")) { - throw new LocalVCInternalException("Invalid local VC Repository URL: " + urlString); + throw new LocalVCInternalException("Invalid local VC Repository URI: " + urlString); } this.projectKey = urlPath.getName(1).toString(); @@ -71,7 +71,7 @@ public LocalVCRepositoryUrl(String urlString, URL localVCBaseUrl) { this.uri = new URI(urlString); } catch (URISyntaxException e) { - throw new LocalVCInternalException("Could not create local VC Repository URL", e); + throw new LocalVCInternalException("Could not create local VC Repository URI", e); } this.repositoryTypeOrUserName = getRepositoryTypeOrUserName(repositorySlug, projectKey); @@ -79,13 +79,13 @@ public LocalVCRepositoryUrl(String urlString, URL localVCBaseUrl) { } /** - * Constructor that builds a LocalVCRepositoryUrl from a repository path. + * Constructor that builds a LocalVCRepositoryUri from a repository path. * * @param repositoryPath the path to the repository, also works with a path to a local checked out repository. * @param localVCServerUrl the base URL of the local VC server defined in an environment variable. * @throws LocalVCInternalException if the repository path is invalid. */ - public LocalVCRepositoryUrl(Path repositoryPath, URL localVCServerUrl) { + public LocalVCRepositoryUri(Path repositoryPath, URL localVCServerUrl) { if (".git".equals(repositoryPath.getFileName().toString())) { // This is the case when a local repository path is passed instead of a path to a remote repository in the "local-vcs-repos" folder. // In this case we remove the ".git" suffix. @@ -101,7 +101,7 @@ public LocalVCRepositoryUrl(Path repositoryPath, URL localVCServerUrl) { this.uri = new URI(urlString); } catch (URISyntaxException e) { - throw new LocalVCInternalException("Could not create local VC Repository URL", e); + throw new LocalVCInternalException("Could not create local VC Repository URI", e); } this.repositoryTypeOrUserName = getRepositoryTypeOrUserName(repositorySlug, projectKey); diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCService.java index 0e786e4ef840..ae0c0a72f5cd 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCService.java @@ -33,7 +33,7 @@ import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseStudentParticipationRepository; import de.tum.in.www1.artemis.repository.TemplateProgrammingExerciseParticipationRepository; -import de.tum.in.www1.artemis.service.UrlService; +import de.tum.in.www1.artemis.service.UriService; import de.tum.in.www1.artemis.service.connectors.ConnectorHealth; import de.tum.in.www1.artemis.service.connectors.GitService; import de.tum.in.www1.artemis.service.connectors.vcs.AbstractVersionControlService; @@ -54,38 +54,38 @@ public class LocalVCService extends AbstractVersionControlService { @Value("${artemis.version-control.local-vcs-repo-path}") private String localVCBasePath; - public LocalVCService(UrlService urlService, GitService gitService, ApplicationContext applicationContext, + public LocalVCService(UriService uriService, GitService gitService, ApplicationContext applicationContext, ProgrammingExerciseStudentParticipationRepository studentParticipationRepository, ProgrammingExerciseRepository programmingExerciseRepository, TemplateProgrammingExerciseParticipationRepository templateProgrammingExerciseParticipationRepository) { - super(applicationContext, gitService, urlService, studentParticipationRepository, programmingExerciseRepository, templateProgrammingExerciseParticipationRepository); + super(gitService, uriService, studentParticipationRepository, programmingExerciseRepository, templateProgrammingExerciseParticipationRepository); } @Override public void configureRepository(ProgrammingExercise exercise, ProgrammingExerciseStudentParticipation participation, boolean allowAccess) { // For Bitbucket and GitLab, users are added to the respective repository to allow them to fetch from there and push to it // if the exercise allows for usage of an offline IDE. - // For local VCS, users are allowed to access the repository by default if they have access to the repository URL. + // For local VCS, users are allowed to access the repository by default if they have access to the repository URI. // Instead, the LocalVCFetchFilter and LocalVCPushFilter block requests if offline IDE usage is not allowed. } @Override - public void addMemberToRepository(VcsRepositoryUrl repositoryUrl, User user, VersionControlRepositoryPermission permissions) { + public void addMemberToRepository(VcsRepositoryUri repositoryUri, User user, VersionControlRepositoryPermission permissions) { // Members cannot be added to a local repository. Authenticated users have access by default and are authorized in the LocalVCFetchFilter and LocalVCPushFilter. } @Override - public void removeMemberFromRepository(VcsRepositoryUrl repositoryUrl, User user) { + public void removeMemberFromRepository(VcsRepositoryUri repositoryUri, User user) { // Members cannot be removed from a local repository. // Authorization is checked in the LocalVCFetchFilter and LocalVCPushFilter. } @Override - protected void addWebHook(VcsRepositoryUrl repositoryUrl, String notificationUrl, String webHookName) { + protected void addWebHook(VcsRepositoryUri repositoryUri, String notificationUrl, String webHookName) { // Webhooks must not be added for the local VC system. The LocalVCPostPushHook notifies Artemis on every push. } @Override - protected void addAuthenticatedWebHook(VcsRepositoryUrl repositoryUrl, String notificationUrl, String webHookName, String secretToken) { + protected void addAuthenticatedWebHook(VcsRepositoryUri repositoryUri, String notificationUrl, String webHookName, String secretToken) { // Webhooks must not be added for the local VC system. The LocalVCPostPushHook notifies Artemis on every push. } @@ -107,16 +107,16 @@ public void deleteProject(String projectKey) { } /** - * Delete the repository at the given repository URL + * Delete the repository at the given repository URI * - * @param repositoryUrl of the repository that should be deleted + * @param repositoryUri of the repository that should be deleted * @throws LocalVCInternalException if the repository cannot be deleted */ @Override - public void deleteRepository(VcsRepositoryUrl repositoryUrl) { + public void deleteRepository(VcsRepositoryUri repositoryUri) { - LocalVCRepositoryUrl localVCRepositoryUrl = new LocalVCRepositoryUrl(repositoryUrl.toString(), localVCBaseUrl); - Path localRepositoryPath = localVCRepositoryUrl.getLocalRepositoryPath(localVCBasePath); + LocalVCRepositoryUri localVCRepositoryUri = new LocalVCRepositoryUri(repositoryUri.toString(), localVCBaseUrl); + Path localRepositoryPath = localVCRepositoryUri.getLocalRepositoryPath(localVCBasePath); try { FileUtils.deleteDirectory(localRepositoryPath.toFile()); @@ -127,34 +127,34 @@ public void deleteRepository(VcsRepositoryUrl repositoryUrl) { } /** - * Get the VcsRepositoryUrl for the given project key and repository slug + * Get the VcsRepositoryUri for the given project key and repository slug * * @param projectKey The project key * @param repositorySlug The repository slug - * @return The VcsRepositoryUrl - * @throws LocalVCInternalException if the repository URL cannot be constructed + * @return The VcsRepositoryUri + * @throws LocalVCInternalException if the repository URI cannot be constructed */ @Override - public VcsRepositoryUrl getCloneRepositoryUrl(String projectKey, String repositorySlug) { - return new LocalVCRepositoryUrl(projectKey, repositorySlug, localVCBaseUrl); + public VcsRepositoryUri getCloneRepositoryUri(String projectKey, String repositorySlug) { + return new LocalVCRepositoryUri(projectKey, repositorySlug, localVCBaseUrl); } @Override - public void setRepositoryPermissionsToReadOnly(VcsRepositoryUrl repositoryUrl, String projectKey, Set users) { + public void setRepositoryPermissionsToReadOnly(VcsRepositoryUri repositoryUri, String projectKey, Set users) { // Not implemented for local VC. All checks for whether a student can access a repository are conducted in the LocalVCFetchFilter and LocalVCPushFilter. } /** * Get the default branch of the repository * - * @param repositoryUrl The repository url to get the default branch for. + * @param repositoryUri The repository uri to get the default branch for. * @return the name of the default branch, e.g. 'main' * @throws LocalVCInternalException if the default branch cannot be determined */ @Override - public String getDefaultBranchOfRepository(VcsRepositoryUrl repositoryUrl) { - LocalVCRepositoryUrl localVCRepositoryUrl = new LocalVCRepositoryUrl(repositoryUrl.toString(), localVCBaseUrl); - String localRepositoryPath = localVCRepositoryUrl.getLocalRepositoryPath(localVCBasePath).toString(); + public String getDefaultBranchOfRepository(VcsRepositoryUri repositoryUri) { + LocalVCRepositoryUri localVCRepositoryUri = new LocalVCRepositoryUri(repositoryUri.toString(), localVCBaseUrl); + String localRepositoryPath = localVCRepositoryUri.getLocalRepositoryPath(localVCBasePath).toString(); Map remoteRepositoryRefs; try { remoteRepositoryRefs = Git.lsRemoteRepository().setRemote(localRepositoryPath).callAsMap(); @@ -172,7 +172,7 @@ public String getDefaultBranchOfRepository(VcsRepositoryUrl repositoryUrl) { } @Override - public void unprotectBranch(VcsRepositoryUrl repositoryUrl, String branch) { + public void unprotectBranch(VcsRepositoryUri repositoryUri, String branch) { // Not implemented. It is not needed for local VC for the current use // case, because the main branch is unprotected by default. } @@ -233,9 +233,9 @@ public void createRepository(String projectKey, String repositorySlug, String pa private void createRepository(String projectKey, String repositorySlug) { - LocalVCRepositoryUrl localVCRepositoryUrl = new LocalVCRepositoryUrl(projectKey, repositorySlug, localVCBaseUrl); + LocalVCRepositoryUri localVCRepositoryUri = new LocalVCRepositoryUri(projectKey, repositorySlug, localVCBaseUrl); - Path remoteDirPath = localVCRepositoryUrl.getLocalRepositoryPath(localVCBasePath); + Path remoteDirPath = localVCRepositoryUri.getLocalRepositoryPath(localVCBasePath); try { Files.createDirectories(remoteDirPath); @@ -259,13 +259,13 @@ private void createRepository(String projectKey, String repositorySlug) { } @Override - public Boolean repositoryUrlIsValid(@Nullable VcsRepositoryUrl repositoryUrl) { - if (repositoryUrl == null || repositoryUrl.getURI() == null) { + public Boolean repositoryUriIsValid(@Nullable VcsRepositoryUri repositoryUri) { + if (repositoryUri == null || repositoryUri.getURI() == null) { return false; } try { - new LocalVCRepositoryUrl(repositoryUrl.toString(), localVCBaseUrl); + new LocalVCRepositoryUri(repositoryUri.toString(), localVCBaseUrl); } catch (LocalVCInternalException e) { return false; @@ -301,7 +301,7 @@ public ZonedDateTime getPushDate(ProgrammingExerciseParticipation participation, repository = gitService.getOrCheckoutRepository(participation); } catch (GitAPIException e) { - throw new LocalVCInternalException("Unable to get the repository from participation " + participation.getId() + ": " + participation.getRepositoryUrl(), e); + throw new LocalVCInternalException("Unable to get the repository from participation " + participation.getId() + ": " + participation.getRepositoryUri(), e); } try (RevWalk revWalk = new RevWalk(repository)) { @@ -313,7 +313,7 @@ public ZonedDateTime getPushDate(ProgrammingExerciseParticipation participation, return ZonedDateTime.ofInstant(instant, zoneId); } catch (IOException e) { - throw new LocalVCInternalException("Unable to get the push date from participation " + participation.getId() + ": " + participation.getRepositoryUrl(), e); + throw new LocalVCInternalException("Unable to get the push date from participation " + participation.getId() + ": " + participation.getRepositoryUri(), e); } } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCServletService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCServletService.java index 0b318447bd3b..b8c4d8bd0ce8 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCServletService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCServletService.java @@ -112,6 +112,8 @@ public LocalVCServletService(AuthenticationManagerBuilder authenticationManagerB } /** + * Resolves the repository for the given path by first trying to use a cached one. + * If the cache does not hit, it creates a JGit repository and opens the local repository. * * @param repositoryPath the path of the repository, as parsed out of the URL (everything after /git). * @return the opened repository instance. @@ -153,33 +155,32 @@ public Repository resolveRepository(String repositoryPath) throws RepositoryNotF /** * Determines whether a given request to access a local VC repository (either via fetch of push) is authenticated and authorized. * - * @param servletRequest The object containing all information about the incoming request. - * @param repositoryActionType Indicates whether the method should authenticate a fetch or a push request. For a push request, additional checks are conducted. + * @param request The object containing all information about the incoming request. + * @param repositoryAction Indicates whether the method should authenticate a fetch or a push request. For a push request, additional checks are conducted. * @throws LocalVCAuthException If the user authentication fails or the user is not authorized to access a certain repository. * @throws LocalVCForbiddenException If the user is not allowed to access the repository, e.g. because offline IDE usage is not allowed or the due date has passed. - * @throws LocalVCInternalException If an internal error occurs, e.g. because the LocalVCRepositoryUrl could not be created. + * @throws LocalVCInternalException If an internal error occurs, e.g. because the LocalVCRepositoryUri could not be created. */ - public void authenticateAndAuthorizeGitRequest(HttpServletRequest servletRequest, RepositoryActionType repositoryActionType) - throws LocalVCAuthException, LocalVCForbiddenException { + public void authenticateAndAuthorizeGitRequest(HttpServletRequest request, RepositoryActionType repositoryAction) throws LocalVCAuthException, LocalVCForbiddenException { long timeNanoStart = System.nanoTime(); - User user = authenticateUser(servletRequest.getHeader(LocalVCServletService.AUTHORIZATION_HEADER)); + User user = authenticateUser(request.getHeader(LocalVCServletService.AUTHORIZATION_HEADER)); // Optimization. // For each git command (i.e. 'git fetch' or 'git push'), the git client sends three requests. - // The URLs of the first two requests end on '[repository URL]/info/refs'. The third one ends on '[repository URL]/git-receive-pack' (for push) and '[repository + // The URLs of the first two requests end on '[repository URI]/info/refs'. The third one ends on '[repository URI]/git-receive-pack' (for push) and '[repository // URL]/git-upload-pack' (for fetch). // The following checks will only be conducted for the second request, so we do not have to access the database too often. // The first request does not contain credentials and will thus already be blocked by the 'authenticateUser' method above. - if (!servletRequest.getRequestURI().endsWith("/info/refs")) { + if (!request.getRequestURI().endsWith("/info/refs")) { return; } - LocalVCRepositoryUrl localVCRepositoryUrl = new LocalVCRepositoryUrl(servletRequest.getRequestURL().toString().replace("/info/refs", ""), localVCBaseUrl); + LocalVCRepositoryUri localVCRepositoryUri = new LocalVCRepositoryUri(request.getRequestURL().toString().replace("/info/refs", ""), localVCBaseUrl); - String projectKey = localVCRepositoryUrl.getProjectKey(); - String repositoryTypeOrUserName = localVCRepositoryUrl.getRepositoryTypeOrUserName(); + String projectKey = localVCRepositoryUri.getProjectKey(); + String repositoryTypeOrUserName = localVCRepositoryUri.getRepositoryTypeOrUserName(); ProgrammingExercise exercise; @@ -195,9 +196,9 @@ public void authenticateAndAuthorizeGitRequest(HttpServletRequest servletRequest throw new LocalVCForbiddenException(); } - authorizeUser(repositoryTypeOrUserName, user, exercise, repositoryActionType, localVCRepositoryUrl.isPracticeRepository()); + authorizeUser(repositoryTypeOrUserName, user, exercise, repositoryAction, localVCRepositoryUri.isPracticeRepository()); - log.info("Authorizing user {} for repository {} took {}", user.getLogin(), localVCRepositoryUrl, TimeLogUtil.formatDurationFrom(timeNanoStart)); + log.info("Authorizing user {} for repository {} took {}", user.getLogin(), localVCRepositoryUri, TimeLogUtil.formatDurationFrom(timeNanoStart)); } private User authenticateUser(String authorizationHeader) throws LocalVCAuthException { @@ -214,7 +215,7 @@ private User authenticateUser(String authorizationHeader) throws LocalVCAuthExce try { SecurityUtils.checkUsernameAndPasswordValidity(username, password); - // Try to authenticate the user. + // Try to authenticate the user based on the configured options, this can include sending the data to an external system (e.g. LDAP) or using internal authentication. UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); authenticationManagerBuilder.getObject().authenticate(authenticationToken); } @@ -279,10 +280,10 @@ private void authorizeUser(String repositoryTypeOrUserName, User user, Programmi * Returns the HTTP status code for the given exception thrown by the above method "authenticateAndAuthorizeGitRequest". * * @param exception The exception thrown. - * @param repositoryUrl The URL of the repository that was accessed. + * @param repositoryUri The URL of the repository that was accessed. * @return The HTTP status code. */ - public int getHttpStatusForException(Exception exception, String repositoryUrl) { + public int getHttpStatusForException(Exception exception, String repositoryUri) { if (exception instanceof LocalVCAuthException) { return HttpStatus.UNAUTHORIZED.value(); } @@ -290,7 +291,7 @@ else if (exception instanceof LocalVCForbiddenException) { return HttpStatus.FORBIDDEN.value(); } else { - log.error("Internal server error while trying to access repository {}: {}", repositoryUrl, exception.getMessage()); + log.error("Internal server error while trying to access repository {}: {}", repositoryUri, exception.getMessage()); return HttpStatus.INTERNAL_SERVER_ERROR.value(); } } @@ -308,14 +309,14 @@ public void processNewPush(String commitHash, Repository repository) { Path repositoryFolderPath = repository.getDirectory().toPath(); - LocalVCRepositoryUrl localVCRepositoryUrl = getLocalVCRepositoryUrl(repositoryFolderPath); + LocalVCRepositoryUri localVCRepositoryUri = getLocalVCRepositoryUri(repositoryFolderPath); - String repositoryTypeOrUserName = localVCRepositoryUrl.getRepositoryTypeOrUserName(); - String projectKey = localVCRepositoryUrl.getProjectKey(); + String repositoryTypeOrUserName = localVCRepositoryUri.getRepositoryTypeOrUserName(); + String projectKey = localVCRepositoryUri.getProjectKey(); ProgrammingExercise exercise = getProgrammingExercise(projectKey); - ProgrammingExerciseParticipation participation = getProgrammingExerciseParticipation(localVCRepositoryUrl, repositoryTypeOrUserName, exercise); + ProgrammingExerciseParticipation participation = getProgrammingExerciseParticipation(localVCRepositoryUri, repositoryTypeOrUserName, exercise); try { if (commitHash == null) { @@ -336,18 +337,18 @@ public void processNewPush(String commitHash, Repository repository) { // This catch clause does not catch exceptions that happen during runBuildJob() as that method is called asynchronously. // For exceptions happening inside runBuildJob(), the user is notified. See the addBuildJobToQueue() method in the LocalCIBuildJobManagementService for that. throw new VersionControlException( - "Could not process new push to repository " + localVCRepositoryUrl.getURI() + " and commit " + commitHash + ". No build job was queued.", e); + "Could not process new push to repository " + localVCRepositoryUri.getURI() + " and commit " + commitHash + ". No build job was queued.", e); } - log.info("New push processed to repository {} for commit {} in {}. A build job was queued.", localVCRepositoryUrl.getURI(), commitHash, + log.info("New push processed to repository {} for commit {} in {}. A build job was queued.", localVCRepositoryUri.getURI(), commitHash, TimeLogUtil.formatDurationFrom(timeNanoStart)); } - private ProgrammingExerciseParticipation getProgrammingExerciseParticipation(LocalVCRepositoryUrl localVCRepositoryUrl, String repositoryTypeOrUserName, + private ProgrammingExerciseParticipation getProgrammingExerciseParticipation(LocalVCRepositoryUri localVCRepositoryUri, String repositoryTypeOrUserName, ProgrammingExercise exercise) { ProgrammingExerciseParticipation participation; try { - participation = programmingExerciseParticipationService.getParticipationForRepository(exercise, repositoryTypeOrUserName, localVCRepositoryUrl.isPracticeRepository(), + participation = programmingExerciseParticipationService.getParticipationForRepository(exercise, repositoryTypeOrUserName, localVCRepositoryUri.isPracticeRepository(), true); } catch (EntityNotFoundException e) { @@ -367,13 +368,13 @@ private ProgrammingExercise getProgrammingExercise(String projectKey) { return exercise; } - private LocalVCRepositoryUrl getLocalVCRepositoryUrl(Path repositoryFolderPath) { + private LocalVCRepositoryUri getLocalVCRepositoryUri(Path repositoryFolderPath) { try { - return new LocalVCRepositoryUrl(repositoryFolderPath, localVCBaseUrl); + return new LocalVCRepositoryUri(repositoryFolderPath, localVCBaseUrl); } catch (LocalVCInternalException e) { // This means something is misconfigured. - throw new VersionControlException("Could not create valid repository URL from path " + repositoryFolderPath, e); + throw new VersionControlException("Could not create valid repository URI from path " + repositoryFolderPath, e); } } @@ -431,6 +432,7 @@ private ProgrammingSubmission getProgrammingSubmission(ProgrammingExercise exerc } /** + * TODO: this could be done asynchronously to shorten the duration of the push operation * Process a new push to a student's repository or to the template or solution repository of the exercise. * * @param participation the participation for which the push was made @@ -480,13 +482,7 @@ private Commit extractCommitInfo(String commitHash, Repository repository) throw throw new VersionControlException("Something went wrong retrieving the revCommit or the branch."); } - Commit commit = new Commit(); - commit.setCommitHash(commitHash); - commit.setAuthorName(revCommit.getAuthorIdent().getName()); - commit.setAuthorEmail(revCommit.getAuthorIdent().getEmailAddress()); - commit.setBranch(branch); - commit.setMessage(revCommit.getFullMessage()); - - return commit; + var author = revCommit.getAuthorIdent(); + return new Commit(commitHash, author.getName(), revCommit.getFullMessage(), author.getEmailAddress(), branch); } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/AbstractVersionControlService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/AbstractVersionControlService.java index 8523a53018e6..8df3826e349c 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/AbstractVersionControlService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/AbstractVersionControlService.java @@ -11,11 +11,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.ApplicationContext; import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.Repository; -import de.tum.in.www1.artemis.domain.VcsRepositoryUrl; +import de.tum.in.www1.artemis.domain.VcsRepositoryUri; import de.tum.in.www1.artemis.domain.enumeration.InitializationState; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; @@ -23,9 +22,8 @@ import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseStudentParticipationRepository; import de.tum.in.www1.artemis.repository.TemplateProgrammingExerciseParticipationRepository; -import de.tum.in.www1.artemis.service.UrlService; +import de.tum.in.www1.artemis.service.UriService; import de.tum.in.www1.artemis.service.connectors.GitService; -import de.tum.in.www1.artemis.service.connectors.ci.ContinuousIntegrationService; public abstract class AbstractVersionControlService implements VersionControlService { @@ -37,11 +35,9 @@ public abstract class AbstractVersionControlService implements VersionControlSer @Value("${artemis.version-control.default-branch:main}") protected String defaultBranch; - private final ApplicationContext applicationContext; - protected final GitService gitService; - protected final UrlService urlService; + protected final UriService uriService; protected final ProgrammingExerciseStudentParticipationRepository studentParticipationRepository; @@ -49,12 +45,10 @@ public abstract class AbstractVersionControlService implements VersionControlSer protected final TemplateProgrammingExerciseParticipationRepository templateProgrammingExerciseParticipationRepository; - public AbstractVersionControlService(ApplicationContext applicationContext, GitService gitService, UrlService urlService, - ProgrammingExerciseStudentParticipationRepository studentParticipationRepository, ProgrammingExerciseRepository programmingExerciseRepository, - TemplateProgrammingExerciseParticipationRepository templateProgrammingExerciseParticipationRepository) { - this.applicationContext = applicationContext; + public AbstractVersionControlService(GitService gitService, UriService uriService, ProgrammingExerciseStudentParticipationRepository studentParticipationRepository, + ProgrammingExerciseRepository programmingExerciseRepository, TemplateProgrammingExerciseParticipationRepository templateProgrammingExerciseParticipationRepository) { this.gitService = gitService; - this.urlService = urlService; + this.uriService = uriService; this.studentParticipationRepository = studentParticipationRepository; this.programmingExerciseRepository = programmingExerciseRepository; this.templateProgrammingExerciseParticipationRepository = templateProgrammingExerciseParticipationRepository; @@ -63,26 +57,21 @@ public AbstractVersionControlService(ApplicationContext applicationContext, GitS /** * Adds a webhook for the specified repository to the given notification URL. * - * @param repositoryUrl The repository to which the webhook should get added to + * @param repositoryUri The repository to which the webhook should get added to * @param notificationUrl The URL the hook should notify * @param webHookName Any arbitrary name for the webhook */ - protected abstract void addWebHook(VcsRepositoryUrl repositoryUrl, String notificationUrl, String webHookName); + protected abstract void addWebHook(VcsRepositoryUri repositoryUri, String notificationUrl, String webHookName); /** * Adds an authenticated webhook for the specified repository to the given notification URL. * - * @param repositoryUrl The repository to which the webhook should get added to + * @param repositoryUri The repository to which the webhook should get added to * @param notificationUrl The URL the hook should notify * @param webHookName Any arbitrary name for the webhook * @param secretToken A secret token that authenticates the webhook against the system behind the notification URL */ - protected abstract void addAuthenticatedWebHook(VcsRepositoryUrl repositoryUrl, String notificationUrl, String webHookName, String secretToken); - - protected ContinuousIntegrationService getContinuousIntegrationService() { - // We need to get the CI service from the context, because Bamboo and Bitbucket would end up in a circular dependency otherwise - return applicationContext.getBean(ContinuousIntegrationService.class); - } + protected abstract void addAuthenticatedWebHook(VcsRepositoryUri repositoryUri, String notificationUrl, String webHookName, String secretToken); @Override public void addWebHooksForExercise(ProgrammingExercise exercise) { @@ -91,46 +80,46 @@ public void addWebHooksForExercise(ProgrammingExercise exercise) { final var artemisTestsHookPath = ARTEMIS_SERVER_URL + "/api/public/programming-exercises/test-cases-changed/" + exercise.getId(); // first add web hooks from the version control service to Artemis, so that Artemis is notified and can create ProgrammingSubmission when instructors push their template or // solution code - addWebHook(exercise.getVcsTemplateRepositoryUrl(), artemisTemplateHookPath, "Artemis WebHook"); - addWebHook(exercise.getVcsSolutionRepositoryUrl(), artemisSolutionHookPath, "Artemis WebHook"); - addWebHook(exercise.getVcsTestRepositoryUrl(), artemisTestsHookPath, "Artemis WebHook"); + addWebHook(exercise.getVcsTemplateRepositoryUri(), artemisTemplateHookPath, "Artemis WebHook"); + addWebHook(exercise.getVcsSolutionRepositoryUri(), artemisSolutionHookPath, "Artemis WebHook"); + addWebHook(exercise.getVcsTestRepositoryUri(), artemisTestsHookPath, "Artemis WebHook"); } @Override public void addWebHookForParticipation(ProgrammingExerciseParticipation participation) { if (!participation.getInitializationState().hasCompletedState(InitializationState.INITIALIZED)) { // first add a web hook from the version control service to Artemis, so that Artemis is notified can create a ProgrammingSubmission when students push their code - addWebHook(participation.getVcsRepositoryUrl(), ARTEMIS_SERVER_URL + "/api/public/programming-submissions/" + participation.getId(), "Artemis WebHook"); + addWebHook(participation.getVcsRepositoryUri(), ARTEMIS_SERVER_URL + "/api/public/programming-submissions/" + participation.getId(), "Artemis WebHook"); } } @Override - public VcsRepositoryUrl copyRepository(String sourceProjectKey, String sourceRepositoryName, String sourceBranch, String targetProjectKey, String targetRepositoryName) + public VcsRepositoryUri copyRepository(String sourceProjectKey, String sourceRepositoryName, String sourceBranch, String targetProjectKey, String targetRepositoryName) throws VersionControlException { sourceRepositoryName = sourceRepositoryName.toLowerCase(); targetRepositoryName = targetRepositoryName.toLowerCase(); final String targetRepoSlug = targetProjectKey.toLowerCase() + "-" + targetRepositoryName; // get the remote url of the source repo - final var sourceRepoUrl = getCloneRepositoryUrl(sourceProjectKey, sourceRepositoryName); + final var sourceRepoUri = getCloneRepositoryUri(sourceProjectKey, sourceRepositoryName); // get the remote url of the target repo - final var targetRepoUrl = getCloneRepositoryUrl(targetProjectKey, targetRepoSlug); + final var targetRepoUri = getCloneRepositoryUri(targetProjectKey, targetRepoSlug); Repository targetRepo = null; try { // create the new target repo createRepository(targetProjectKey, targetRepoSlug, null); // clone the source repo to the target directory - targetRepo = gitService.getOrCheckoutRepositoryIntoTargetDirectory(sourceRepoUrl, targetRepoUrl, true); + targetRepo = gitService.getOrCheckoutRepositoryIntoTargetDirectory(sourceRepoUri, targetRepoUri, true); // copy by pushing the source's content to the target's repo - gitService.pushSourceToTargetRepo(targetRepo, targetRepoUrl, sourceBranch); + gitService.pushSourceToTargetRepo(targetRepo, targetRepoUri, sourceBranch); } catch (GitAPIException | VersionControlException ex) { if (isReadFullyShortReadOfBlockException(ex)) { // NOTE: we ignore this particular error: it sometimes happens when pushing code that includes binary files, however the push operation typically worked correctly // TODO: verify that the push operation actually worked correctly, e.g. by comparing the number of commits in the source and target repo - log.warn("TransportException/EOFException with 'Short read of block' when copying repository {} to {}. Will ignore it", sourceRepoUrl, targetRepoUrl); - return targetRepoUrl; + log.warn("TransportException/EOFException with 'Short read of block' when copying repository {} to {}. Will ignore it", sourceRepoUri, targetRepoUri); + return targetRepoUri; } - Path localPath = gitService.getDefaultLocalPathOfRepo(targetRepoUrl); + Path localPath = gitService.getDefaultLocalPathOfRepo(targetRepoUri); // clean up in case of an error try { if (targetRepo != null) { @@ -149,7 +138,7 @@ public VcsRepositoryUrl copyRepository(String sourceProjectKey, String sourceRep throw new VersionControlException("Could not copy repository " + sourceRepositoryName + " to the target repository " + targetRepositoryName, ex); } - return targetRepoUrl; + return targetRepoUri; } /** @@ -179,7 +168,7 @@ public String getOrRetrieveBranchOfParticipation(ProgrammingExerciseParticipatio @Override public String getOrRetrieveBranchOfStudentParticipation(ProgrammingExerciseStudentParticipation participation) { if (participation.getBranch() == null) { - String branch = getDefaultBranchOfRepository(participation.getVcsRepositoryUrl()); + String branch = getDefaultBranchOfRepository(participation.getVcsRepositoryUri()); participation.setBranch(branch); studentParticipationRepository.save(participation); } @@ -193,7 +182,7 @@ public String getOrRetrieveBranchOfExercise(ProgrammingExercise programmingExerc if (!Hibernate.isInitialized(programmingExercise.getTemplateParticipation())) { programmingExercise.setTemplateParticipation(templateProgrammingExerciseParticipationRepository.findByProgrammingExerciseIdElseThrow(programmingExercise.getId())); } - String branch = getDefaultBranchOfRepository(programmingExercise.getVcsTemplateRepositoryUrl()); + String branch = getDefaultBranchOfRepository(programmingExercise.getVcsTemplateRepositoryUri()); programmingExercise.setBranch(branch); programmingExerciseRepository.save(programmingExercise); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/VersionControlService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/VersionControlService.java index ced6392a9c93..d2253e17bc69 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/VersionControlService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/VersionControlService.java @@ -45,11 +45,11 @@ public interface VersionControlService { void deleteProject(String projectKey); /** - * Deletes the repository at the given url + * Deletes the repository at the given uri * - * @param repositoryUrl of the repository that should be deleted + * @param repositoryUri of the repository that should be deleted */ - void deleteRepository(VcsRepositoryUrl repositoryUrl); + void deleteRepository(VcsRepositoryUri repositoryUri); /** * Get the clone URL used for cloning @@ -58,15 +58,15 @@ public interface VersionControlService { * @param repositorySlug The repository slug * @return The clone URL */ - VcsRepositoryUrl getCloneRepositoryUrl(String projectKey, String repositorySlug); + VcsRepositoryUri getCloneRepositoryUri(String projectKey, String repositorySlug); /** - * Check if the given repository url is valid and accessible. + * Check if the given repository uri is valid and accessible. * - * @param repositoryUrl the VCS repository URL + * @param repositoryUri the VCS repository URI * @return whether the repository is valid */ - Boolean repositoryUrlIsValid(@Nullable VcsRepositoryUrl repositoryUrl); + Boolean repositoryUriIsValid(@Nullable VcsRepositoryUri repositoryUri); /** * Get the last commit details that are included in the given requestBody that notifies about a push @@ -125,43 +125,43 @@ public interface VersionControlService { * @return The URL for cloning the repository * @throws VersionControlException if the repository could not be copied on the VCS server (e.g. because the source repo does not exist) */ - VcsRepositoryUrl copyRepository(String sourceProjectKey, String sourceRepositoryName, String sourceBranch, String targetProjectKey, String targetRepositoryName) + VcsRepositoryUri copyRepository(String sourceProjectKey, String sourceRepositoryName, String sourceBranch, String targetProjectKey, String targetRepositoryName) throws VersionControlException; /** * Add the user to the repository * - * @param repositoryUrl The repository url of the repository to which to add the user. It contains the project key & the repository name. + * @param repositoryUri The repository uri of the repository to which to add the user. It contains the project key & the repository name. * @param user User which to add to the repository * @param permissions The permissions the user should get for the repository. */ - void addMemberToRepository(VcsRepositoryUrl repositoryUrl, User user, VersionControlRepositoryPermission permissions); + void addMemberToRepository(VcsRepositoryUri repositoryUri, User user, VersionControlRepositoryPermission permissions); /** * Remove the user from the repository * - * @param repositoryUrl The repository url of the repository from which to remove the user. It contains the project key & the repository name. + * @param repositoryUri The repository uri of the repository from which to remove the user. It contains the project key & the repository name. * @param user User which to remove from the repository */ - void removeMemberFromRepository(VcsRepositoryUrl repositoryUrl, User user); + void removeMemberFromRepository(VcsRepositoryUri repositoryUri, User user); /** * Removes the user's write permissions for a repository. * - * @param repositoryUrl The repository url of the repository to update. It contains the project key & the repository name. + * @param repositoryUri The repository uri of the repository to update. It contains the project key & the repository name. * @param projectKey The projectKey that the repo is part of in the VCS. * @param users Set of users for which to change permissions * @throws VersionControlException If the communication with the VCS fails. */ - void setRepositoryPermissionsToReadOnly(VcsRepositoryUrl repositoryUrl, String projectKey, Set users) throws VersionControlException; + void setRepositoryPermissionsToReadOnly(VcsRepositoryUri repositoryUri, String projectKey, Set users) throws VersionControlException; /** * Get the default branch of the repository * - * @param repositoryUrl The repository url to get the default branch for. + * @param repositoryUri The repository uri to get the default branch for. * @return the name of the default branch, e.g. 'main' */ - String getDefaultBranchOfRepository(VcsRepositoryUrl repositoryUrl) throws VersionControlException; + String getDefaultBranchOfRepository(VcsRepositoryUri repositoryUri) throws VersionControlException; /** * Get the default branch of the repository @@ -171,17 +171,17 @@ VcsRepositoryUrl copyRepository(String sourceProjectKey, String sourceRepository * @return the name of the default branch, e.g. 'main' */ default String getDefaultBranchOfRepository(String projectKey, String repositorySlug) throws VersionControlException { - return getDefaultBranchOfRepository(getCloneRepositoryUrl(projectKey, repositorySlug)); + return getDefaultBranchOfRepository(getCloneRepositoryUri(projectKey, repositorySlug)); } /** * Unprotects a branch from the repository, so that the history can be changed (important for combine template commits). * - * @param repositoryUrl The repository url of the repository to update. It contains the project key & the repository name. + * @param repositoryUri The repository uri of the repository to update. It contains the project key & the repository name. * @param branch The name of the branch to unprotect (e.g "main") * @throws VersionControlException If the communication with the VCS fails. */ - void unprotectBranch(VcsRepositoryUrl repositoryUrl, String branch) throws VersionControlException; + void unprotectBranch(VcsRepositoryUri repositoryUri, String branch) throws VersionControlException; /** * Checks if the underlying VCS server is up and running and gives some additional information about the running diff --git a/src/main/java/de/tum/in/www1/artemis/service/dto/athena/ProgrammingExerciseDTO.java b/src/main/java/de/tum/in/www1/artemis/service/dto/athena/ProgrammingExerciseDTO.java index 2792226f8839..20729a8acc17 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/dto/athena/ProgrammingExerciseDTO.java +++ b/src/main/java/de/tum/in/www1/artemis/service/dto/athena/ProgrammingExerciseDTO.java @@ -13,7 +13,7 @@ * A DTO representing a ProgrammingExercise, for transferring data to Athena */ public record ProgrammingExerciseDTO(long id, String title, double maxPoints, double bonusPoints, String gradingInstructions, List gradingCriteria, - String problemStatement, String programmingLanguage, String solutionRepositoryUrl, String templateRepositoryUrl, String testsRepositoryUrl) implements ExerciseDTO { + String problemStatement, String programmingLanguage, String solutionRepositoryUri, String templateRepositoryUri, String testsRepositoryUri) implements ExerciseDTO { /** * Create a new TextExerciseDTO from a TextExercise diff --git a/src/main/java/de/tum/in/www1/artemis/service/dto/athena/ProgrammingSubmissionDTO.java b/src/main/java/de/tum/in/www1/artemis/service/dto/athena/ProgrammingSubmissionDTO.java index e5045067f23e..19eede1641e2 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/dto/athena/ProgrammingSubmissionDTO.java +++ b/src/main/java/de/tum/in/www1/artemis/service/dto/athena/ProgrammingSubmissionDTO.java @@ -9,7 +9,7 @@ /** * A DTO representing a ProgrammingSubmission, for transferring data to Athena */ -public record ProgrammingSubmissionDTO(long id, long exerciseId, String repositoryUrl) implements SubmissionDTO { +public record ProgrammingSubmissionDTO(long id, long exerciseId, String repositoryUri) implements SubmissionDTO { /** * Creates a new ProgrammingSubmissionDTO from a ProgrammingSubmission. The DTO also contains the exerciseId of the exercise the submission belongs to. diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamQuizQuestionsGenerator.java b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamQuizQuestionsGenerator.java new file mode 100644 index 000000000000..6aa4ae36060b --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamQuizQuestionsGenerator.java @@ -0,0 +1,19 @@ +package de.tum.in.www1.artemis.service.exam; + +import java.util.List; + +import de.tum.in.www1.artemis.domain.quiz.QuizQuestion; + +/** + * Service Interface for generating quiz questions for an exam + */ +public interface ExamQuizQuestionsGenerator { + + /** + * Generates quiz questions for an exam + * + * @param examId the id of the exam for which quiz questions should be generated + * @return the list of generated quiz questions + */ + List generateQuizQuestionsForExam(long examId); +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamService.java index 11e97c99ed93..c2d878f6baf8 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamService.java @@ -424,7 +424,8 @@ private Map calculateExamScoresAsBonusSource(Long ex StudentExam studentExam = studentExamRepository.findWithExercisesByUserIdAndExamId(targetUser.getId(), examId, IS_TEST_RUN) .orElseThrow(() -> new EntityNotFoundException("No student exam found for examId " + examId + " and userId " + studentId)); - StudentExamWithGradeDTO studentExamWithGradeDTO = getStudentExamGradesForSummaryAsStudent(targetUser, studentExam, IS_TEST_RUN); + StudentExamWithGradeDTO studentExamWithGradeDTO = getStudentExamGradesForSummary(targetUser, studentExam, + authorizationCheckService.isAtLeastInstructorInCourse(studentExam.getExam().getCourse(), targetUser)); var studentResult = studentExamWithGradeDTO.studentResult(); return Map.of(studentId, new BonusSourceResultDTO(studentResult.overallPointsAchieved(), studentResult.mostSeverePlagiarismVerdict(), null, null, Boolean.TRUE.equals(studentResult.submitted()))); @@ -443,20 +444,22 @@ private Map calculateExamScoresAsBonusSource(Long ex *

* See {@link StudentExamWithGradeDTO} for more explanation. * - * @param targetUser the user who submitted the studentExam - * @param studentExam the student exam to be evaluated - * @param isTestRun set if an instructor executes a test run + * @param targetUser the user who submitted the studentExam + * @param studentExam the student exam to be evaluated + * @param accessingUserIsAtLeastInstructor is passed to decide the access (e.g. for test runs access will be needed regardless of submission or published dates) * @return the student exam result with points and grade */ - public StudentExamWithGradeDTO getStudentExamGradesForSummaryAsStudent(User targetUser, StudentExam studentExam, boolean isTestRun) { + public StudentExamWithGradeDTO getStudentExamGradesForSummary(User targetUser, StudentExam studentExam, boolean accessingUserIsAtLeastInstructor) { loadQuizExercisesForStudentExam(studentExam); + boolean accessToSummaryAlwaysAllowed = studentExam.isTestRun() || accessingUserIsAtLeastInstructor; + // check that the studentExam has been submitted, otherwise /student-exams/conduction should be used - if (!Boolean.TRUE.equals(studentExam.isSubmitted()) && !isTestRun) { + if (!Boolean.TRUE.equals(studentExam.isSubmitted()) && !accessToSummaryAlwaysAllowed) { throw new AccessForbiddenException(NOT_ALLOWED_TO_ACCESS_THE_GRADE_SUMMARY + "which was NOT submitted!"); } - if (!studentExam.areResultsPublishedYet() && !isTestRun) { + if (!studentExam.areResultsPublishedYet() && !accessToSummaryAlwaysAllowed) { throw new AccessForbiddenException(NOT_ALLOWED_TO_ACCESS_THE_GRADE_SUMMARY + "before the release date of results"); } @@ -562,7 +565,7 @@ public void filterParticipationForExercise(StudentExam studentExam, Exercise exe } if (exercise instanceof ProgrammingExercise programmingExercise) { - programmingExercise.setTestRepositoryUrl(null); + programmingExercise.setTestRepositoryUri(null); } // get user's participation for the exercise @@ -1321,7 +1324,7 @@ public void combineTemplateCommitsOfAllProgrammingExercisesInExam(Exam exam) { try { ProgrammingExercise programmingExerciseWithTemplateParticipation = programmingExerciseRepository .findByIdWithTemplateAndSolutionParticipationElseThrow(exercise.getId()); - gitService.combineAllCommitsOfRepositoryIntoOne(programmingExerciseWithTemplateParticipation.getTemplateParticipation().getVcsRepositoryUrl()); + gitService.combineAllCommitsOfRepositoryIntoOne(programmingExerciseWithTemplateParticipation.getTemplateParticipation().getVcsRepositoryUri()); log.debug("Finished combination of template commits for programming exercise {}", programmingExerciseWithTemplateParticipation); } catch (GitAPIException e) { diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java index 3b7bc810cbfd..c09945996c8f 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java @@ -22,6 +22,7 @@ import org.springframework.cache.CacheManager; import org.springframework.scheduling.TaskScheduler; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.InitializationState; @@ -89,13 +90,15 @@ public class StudentExamService { private final TaskScheduler scheduler; + private final ExamQuizQuestionsGenerator examQuizQuestionsGenerator; + public StudentExamService(StudentExamRepository studentExamRepository, UserRepository userRepository, ParticipationService participationService, QuizSubmissionRepository quizSubmissionRepository, SubmittedAnswerRepository submittedAnswerRepository, TextSubmissionRepository textSubmissionRepository, ModelingSubmissionRepository modelingSubmissionRepository, SubmissionVersionService submissionVersionService, ProgrammingExerciseParticipationService programmingExerciseParticipationService, SubmissionService submissionService, StudentParticipationRepository studentParticipationRepository, ExamQuizService examQuizService, ProgrammingExerciseRepository programmingExerciseRepository, ProgrammingTriggerService programmingTriggerService, ExamRepository examRepository, CacheManager cacheManager, WebsocketMessagingService websocketMessagingService, - @Qualifier("taskScheduler") TaskScheduler scheduler) { + @Qualifier("taskScheduler") TaskScheduler scheduler, QuizPoolService quizPoolService) { this.participationService = participationService; this.studentExamRepository = studentExamRepository; this.userRepository = userRepository; @@ -114,6 +117,7 @@ public StudentExamService(StudentExamRepository studentExamRepository, UserRepos this.cacheManager = cacheManager; this.websocketMessagingService = websocketMessagingService; this.scheduler = scheduler; + this.examQuizQuestionsGenerator = quizPoolService; } /** @@ -782,6 +786,48 @@ private StudentExam generateIndividualStudentExam(Exam exam, User student) { // StudentExams are saved in the called method HashSet userHashSet = new HashSet<>(); userHashSet.add(student); - return studentExamRepository.createRandomStudentExams(exam, userHashSet).get(0); + return studentExamRepository.createRandomStudentExams(exam, userHashSet, examQuizQuestionsGenerator).get(0); + } + + /** + * Generates the student exams randomly based on the exam configuration and the exercise groups + * Important: the passed exams needs to include the registered users, exercise groups and exercises (eagerly loaded) + * + * @param exam with eagerly loaded registered users, exerciseGroups and exercises loaded + * @return the list of student exams with their corresponding users + */ + @Transactional + public List generateStudentExams(final Exam exam) { + final var existingStudentExams = studentExamRepository.findByExamId(exam.getId()); + // https://jira.spring.io/browse/DATAJPA-1367 deleteInBatch does not work, because it does not cascade the deletion of existing exam sessions, therefore use deleteAll + studentExamRepository.deleteAll(existingStudentExams); + + Set users = exam.getRegisteredUsers(); + + // StudentExams are saved in the called method + return studentExamRepository.createRandomStudentExams(exam, users, examQuizQuestionsGenerator); + } + + /** + * Generates the missing student exams randomly based on the exam configuration and the exercise groups. + * The difference between all registered users and the users who already have an individual exam is the set of users for which student exams will be created. + *

+ * Important: the passed exams needs to include the registered users, exercise groups and exercises (eagerly loaded) + * + * @param exam with eagerly loaded registered users, exerciseGroups and exercises loaded + * @return the list of student exams with their corresponding users + */ + @Transactional + public List generateMissingStudentExams(Exam exam) { + + // Get all users who already have an individual exam + Set usersWithStudentExam = studentExamRepository.findUsersWithStudentExamsForExam(exam.getId()); + + // Get all students who don't have an exam yet + Set missingUsers = exam.getRegisteredUsers(); + missingUsers.removeAll(usersWithStudentExam); + + // StudentExams are saved in the called method + return studentExamRepository.createRandomStudentExams(exam, missingUsers, examQuizQuestionsGenerator); } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/export/DataExportExamCreationService.java b/src/main/java/de/tum/in/www1/artemis/service/export/DataExportExamCreationService.java index ab6078131f47..f88deb076a91 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/export/DataExportExamCreationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/export/DataExportExamCreationService.java @@ -116,7 +116,7 @@ private void addExamScores(StudentExam studentExam, Path examWorkingDir) throws var gradingScale = gradingScaleRepository.findByExamId(studentExam.getExam().getId()); List headers = new ArrayList<>(); var examResults = getExamResultsStreamToPrint(studentResult, headers, gradingScale); - CSVFormat csvFormat = CSVFormat.DEFAULT.builder().setHeader(headers.toArray(new String[0])).build(); + CSVFormat csvFormat = CSVFormat.DEFAULT.builder().setHeader(headers.toArray(String[]::new)).build(); try (final CSVPrinter printer = new CSVPrinter( Files.newBufferedWriter(examWorkingDir.resolve(EXAM_DIRECTORY_PREFIX + studentExam.getId() + "_result" + CSV_FILE_EXTENSION)), csvFormat)) { printer.printRecord(examResults); @@ -169,7 +169,7 @@ private Stream getExamResultsStreamToPrint(ExamScoresDTO.StudentResult studen private void addGeneralExamInformation(StudentExam studentExam, Path examWorkingDir) throws IOException { List headers = new ArrayList<>(); var generalExamInformation = getGeneralExamInformationStreamToPrint(studentExam, headers); - CSVFormat csvFormat = CSVFormat.DEFAULT.builder().setHeader(headers.toArray(new String[0])).build(); + CSVFormat csvFormat = CSVFormat.DEFAULT.builder().setHeader(headers.toArray(String[]::new)).build(); try (CSVPrinter printer = new CSVPrinter(Files.newBufferedWriter(examWorkingDir.resolve(EXAM_DIRECTORY_PREFIX + studentExam.getId() + CSV_FILE_EXTENSION)), csvFormat)) { printer.printRecord(generalExamInformation); diff --git a/src/main/java/de/tum/in/www1/artemis/service/export/DataExportExerciseCreationService.java b/src/main/java/de/tum/in/www1/artemis/service/export/DataExportExerciseCreationService.java index 54f106ba5c6c..b7263df54c94 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/export/DataExportExerciseCreationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/export/DataExportExerciseCreationService.java @@ -375,7 +375,7 @@ private void addComplaintData(Complaint complaint, Path outputDir) throws IOExce headers.add("accepted"); dataStreamBuilder.add(complaint.isAccepted()); } - CSVFormat csvFormat = CSVFormat.DEFAULT.builder().setHeader(headers.toArray(new String[0])).build(); + CSVFormat csvFormat = CSVFormat.DEFAULT.builder().setHeader(headers.toArray(String[]::new)).build(); var prefix = complaint.getComplaintType() == ComplaintType.COMPLAINT ? "complaint_" : "more_feedback_"; try (final var printer = new CSVPrinter(Files.newBufferedWriter(outputDir.resolve(prefix + complaint.getId() + CSV_FILE_EXTENSION)), csvFormat)) { @@ -420,7 +420,7 @@ private void createPlagiarismCaseInfoExport(Exercise exercise, Path exercisePath else if (plagiarismCase.getVerdict() == PlagiarismVerdict.WARNING) { dataStreamBuilder.add(plagiarismCase.getVerdictMessage()); } - CSVFormat csvFormat = CSVFormat.DEFAULT.builder().setHeader(headers.toArray(new String[0])).build(); + CSVFormat csvFormat = CSVFormat.DEFAULT.builder().setHeader(headers.toArray(String[]::new)).build(); try (final CSVPrinter printer = new CSVPrinter(Files.newBufferedWriter(exercisePath.resolve("plagiarism_case_" + plagiarismCase.getId() + CSV_FILE_EXTENSION)), csvFormat)) { diff --git a/src/main/java/de/tum/in/www1/artemis/service/export/ProgrammingExerciseExportService.java b/src/main/java/de/tum/in/www1/artemis/service/export/ProgrammingExerciseExportService.java index d042786c1e29..ab0ef44b4b35 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/export/ProgrammingExerciseExportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/export/ProgrammingExerciseExportService.java @@ -364,8 +364,8 @@ public Optional exportInstructorRepositoryForExercise(long exerciseId, Rep } var exercise = exerciseOrEmpty.get(); String zippedRepoName = getZippedRepoName(exercise, repositoryType.getName()); - var repositoryUrl = exercise.getRepositoryURL(repositoryType); - return exportRepository(repositoryUrl, repositoryType.getName(), zippedRepoName, exercise, workingDir, outputDir, null, exportErrors); + var repositoryUri = exercise.getRepositoryURL(repositoryType); + return exportRepository(repositoryUri, repositoryType.getName(), zippedRepoName, exercise, workingDir, outputDir, null, exportErrors); } /** @@ -386,8 +386,8 @@ public Optional exportInstructorAuxiliaryRepositoryForExercise(long exerci } var exercise = exerciseOrEmpty.get(); String zippedRepoName = getZippedRepoName(exercise, auxiliaryRepository.getRepositoryName()); - var repositoryUrl = auxiliaryRepository.getVcsRepositoryUrl(); - return exportRepository(repositoryUrl, auxiliaryRepository.getName(), zippedRepoName, exercise, workingDir, outputDir, null, exportErrors); + var repositoryUri = auxiliaryRepository.getVcsRepositoryUri(); + return exportRepository(repositoryUri, auxiliaryRepository.getName(), zippedRepoName, exercise, workingDir, outputDir, null, exportErrors); } /** @@ -414,8 +414,8 @@ public Optional exportStudentRequestedRepository(long exerciseId, boolean return exportSolutionAndTestStudentRepositoryForExercise(zippedRepoName, exercise, uniquePath, gitDirFilter, exportErrors); } else { - var repositoryUrl = exercise.getRepositoryURL(repositoryType); - return exportRepository(repositoryUrl, repositoryType.getName(), zippedRepoName, exercise, uniquePath, uniquePath, gitDirFilter, exportErrors); + var repositoryUri = exercise.getRepositoryURL(repositoryType); + return exportRepository(repositoryUri, repositoryType.getName(), zippedRepoName, exercise, uniquePath, uniquePath, gitDirFilter, exportErrors); } } @@ -442,25 +442,25 @@ private String getZippedRepoName(ProgrammingExercise exercise, String repository /** * Exports a given repository and stores it in a zip file. * - * @param repositoryUrl the url of the repository + * @param repositoryUri the url of the repository * @param zippedRepoName the name of the zip file * @param workingDir the directory used to clone the repository * @param outputDir the directory used for store the zip file * @param contentFilter a filter for the content of the zip file * @return an optional containing the path to the zip file if the export was successful */ - private Optional exportRepository(VcsRepositoryUrl repositoryUrl, String repositoryName, String zippedRepoName, ProgrammingExercise exercise, Path workingDir, + private Optional exportRepository(VcsRepositoryUri repositoryUri, String repositoryName, String zippedRepoName, ProgrammingExercise exercise, Path workingDir, Path outputDir, @Nullable Predicate contentFilter, List exportErrors) { try { - // It's not guaranteed that the repository url is defined (old courses). - if (repositoryUrl == null) { - var error = "Failed to export instructor repository " + repositoryName + " because the repository url is not defined."; + // It's not guaranteed that the repository uri is defined (old courses). + if (repositoryUri == null) { + var error = "Failed to export instructor repository " + repositoryName + " because the repository uri is not defined."; log.error(error); exportErrors.add(error); return Optional.empty(); } - Path zippedRepo = createZipForRepository(repositoryUrl, zippedRepoName, workingDir, outputDir, contentFilter); + Path zippedRepo = createZipForRepository(repositoryUri, zippedRepoName, workingDir, outputDir, contentFilter); if (zippedRepo != null) { return Optional.of(zippedRepo.toFile()); } @@ -475,8 +475,8 @@ private Optional exportRepository(VcsRepositoryUrl repositoryUrl, String r private Optional exportSolutionAndTestStudentRepositoryForExercise(String zippedRepoName, ProgrammingExercise exercise, Path uniquePath, @Nullable Predicate contentFilter, List exportErrors) { - if (exercise.getVcsSolutionRepositoryUrl() == null || exercise.getVcsTestRepositoryUrl() == null) { - var error = "Failed to export repository of exercise " + exercise.getTitle() + " because the repository url is not defined."; + if (exercise.getVcsSolutionRepositoryUri() == null || exercise.getVcsTestRepositoryUri() == null) { + var error = "Failed to export repository of exercise " + exercise.getTitle() + " because the repository uri is not defined."; log.error(error); exportErrors.add(error); return Optional.empty(); @@ -486,16 +486,16 @@ private Optional exportSolutionAndTestStudentRepositoryForExercise(String Path zipPath = uniquePath.resolve("zip"); try { - gitService.getOrCheckoutRepository(exercise.getVcsTestRepositoryUrl(), clonePath, true); + gitService.getOrCheckoutRepository(exercise.getVcsTestRepositoryUri(), clonePath, true); if (!clonePath.toFile().exists()) { Files.createDirectories(clonePath); } String assignmentPath = RepositoryCheckoutPath.ASSIGNMENT.forProgrammingLanguage(exercise.getProgrammingLanguage()); FileUtils.deleteDirectory(clonePath.resolve(assignmentPath).toFile()); - gitService.getOrCheckoutRepository(exercise.getVcsSolutionRepositoryUrl(), clonePath.resolve(assignmentPath), true); + gitService.getOrCheckoutRepository(exercise.getVcsSolutionRepositoryUri(), clonePath.resolve(assignmentPath), true); for (AuxiliaryRepository auxRepo : exercise.getAuxiliaryRepositoriesForBuildPlan()) { FileUtils.deleteDirectory(clonePath.resolve(auxRepo.getCheckoutDirectory()).toFile()); - gitService.getOrCheckoutRepository(auxRepo.getVcsRepositoryUrl(), clonePath.resolve(auxRepo.getCheckoutDirectory()), true); + gitService.getOrCheckoutRepository(auxRepo.getVcsRepositoryUri(), clonePath.resolve(auxRepo.getCheckoutDirectory()), true); } return Optional.of(gitService.zipFiles(clonePath, zippedRepoName, zipPath.toString(), contentFilter).toFile()); @@ -588,7 +588,7 @@ public List exportStudentRepositories(ProgrammingExercise programmingExerc /** * Creates a zip file with the contents of the git repository. Note that the zip file is deleted in 5 minutes. * - * @param repositoryUrl The url of the repository to zip + * @param repositoryUri The url of the repository to zip * @param zipFilename The name of the zip file * @param outputDir The directory used to store the zip file * @param contentFilter The path filter to exclude some files, can be null to include everything @@ -596,13 +596,13 @@ public List exportStudentRepositories(ProgrammingExercise programmingExerc * @throws IOException if the zip file couldn't be created * @throws GitAPIException if the repo couldn't get checked out */ - private Path createZipForRepository(VcsRepositoryUrl repositoryUrl, String zipFilename, Path workingDir, Path outputDir, @Nullable Predicate contentFilter) + private Path createZipForRepository(VcsRepositoryUri repositoryUri, String zipFilename, Path workingDir, Path outputDir, @Nullable Predicate contentFilter) throws IOException, GitAPIException, GitException, UncheckedIOException { var repositoryDir = fileService.getTemporaryUniquePathWithoutPathCreation(workingDir, 5); Path localRepoPath; // Checkout the repository - try (Repository repository = gitService.getOrCheckoutRepository(repositoryUrl, repositoryDir, false)) { + try (Repository repository = gitService.getOrCheckoutRepository(repositoryUri, repositoryDir, false)) { gitService.resetToOriginHead(repository); localRepoPath = repository.getLocalPath(); } @@ -648,8 +648,8 @@ private File createZipWithAllRepositories(ProgrammingExercise programmingExercis */ public Path createZipForRepositoryWithParticipation(final ProgrammingExercise programmingExercise, final ProgrammingExerciseStudentParticipation participation, final RepositoryExportOptionsDTO repositoryExportOptions, Path workingDir, Path outputDir) throws IOException, UncheckedIOException { - if (participation.getVcsRepositoryUrl() == null) { - log.warn("Ignore participation {} for export, because its repository URL is null", participation.getId()); + if (participation.getVcsRepositoryUri() == null) { + log.warn("Ignore participation {} for export, because its repository URI is null", participation.getId()); return null; } 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 1bf3c90949bc..8e38745bf93b 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 @@ -180,7 +180,7 @@ public ProgrammingExerciseGitDiffReport createReportForSubmissionWithTemplate(Pr var templateParticipation = templateProgrammingExerciseParticipationRepository.findByProgrammingExerciseId(exercise.getId()).orElseThrow(); Repository templateRepo = prepareTemplateRepository(templateParticipation); - var repo1 = gitService.checkoutRepositoryAtCommit(((ProgrammingExerciseParticipation) submission.getParticipation()).getVcsRepositoryUrl(), submission.getCommitHash(), + var repo1 = gitService.checkoutRepositoryAtCommit(((ProgrammingExerciseParticipation) submission.getParticipation()).getVcsRepositoryUri(), submission.getCommitHash(), false); var oldTreeParser = new FileTreeIterator(templateRepo); var newTreeParser = new FileTreeIterator(repo1); @@ -198,7 +198,7 @@ public ProgrammingExerciseGitDiffReport createReportForSubmissionWithTemplate(Pr * @param localPathRepoB local path to the checked out instance of the second repo to compare * @return cumulative number of lines in the git diff of given repositories */ - public int calculateNumberOfDiffLinesBetweenRepos(VcsRepositoryUrl urlRepoA, Path localPathRepoA, VcsRepositoryUrl urlRepoB, Path localPathRepoB) { + public int calculateNumberOfDiffLinesBetweenRepos(VcsRepositoryUri urlRepoA, Path localPathRepoA, VcsRepositoryUri urlRepoB, Path localPathRepoB) { var repoA = gitService.getExistingCheckedOutRepositoryByLocalPath(localPathRepoA, urlRepoA); var repoB = gitService.getExistingCheckedOutRepositoryByLocalPath(localPathRepoB, urlRepoB); @@ -228,7 +228,7 @@ public int calculateNumberOfDiffLinesBetweenRepos(VcsRepositoryUrl urlRepoA, Pat private ProgrammingExerciseGitDiffReport generateReport(TemplateProgrammingExerciseParticipation templateParticipation, SolutionProgrammingExerciseParticipation solutionParticipation) throws GitAPIException, IOException { Repository templateRepo = prepareTemplateRepository(templateParticipation); - var solutionRepo = gitService.getOrCheckoutRepository(solutionParticipation.getVcsRepositoryUrl(), true); + var solutionRepo = gitService.getOrCheckoutRepository(solutionParticipation.getVcsRepositoryUri(), true); gitService.resetToOriginHead(solutionRepo); gitService.pullIgnoreConflicts(solutionRepo); @@ -246,7 +246,7 @@ private ProgrammingExerciseGitDiffReport generateReport(TemplateProgrammingExerc * @throws GitAPIException If an error occurs while accessing the git repository */ private Repository prepareTemplateRepository(TemplateProgrammingExerciseParticipation templateParticipation) throws GitAPIException { - var templateRepo = gitService.getOrCheckoutRepository(templateParticipation.getVcsRepositoryUrl(), true); + var templateRepo = gitService.getOrCheckoutRepository(templateParticipation.getVcsRepositoryUri(), true); gitService.resetToOriginHead(templateRepo); gitService.pullIgnoreConflicts(templateRepo); return templateRepo; @@ -262,13 +262,13 @@ private Repository prepareTemplateRepository(TemplateProgrammingExerciseParticip * @throws IOException If an error occurs while accessing the file system */ public ProgrammingExerciseGitDiffReport generateReportForSubmissions(ProgrammingSubmission submission1, ProgrammingSubmission submission2) throws GitAPIException, IOException { - var repositoryUrl = ((ProgrammingExerciseParticipation) submission1.getParticipation()).getVcsRepositoryUrl(); - var repo1 = gitService.getOrCheckoutRepository(repositoryUrl, true); + var repositoryUri = ((ProgrammingExerciseParticipation) submission1.getParticipation()).getVcsRepositoryUri(); + var repo1 = gitService.getOrCheckoutRepository(repositoryUri, true); var repo1Path = repo1.getLocalPath(); var repo2Path = fileService.getTemporaryUniqueSubfolderPath(repo1Path.getParent(), 5); FileSystemUtils.copyRecursively(repo1Path, repo2Path); repo1 = gitService.checkoutRepositoryAtCommit(repo1, submission1.getCommitHash()); - var repo2 = gitService.getExistingCheckedOutRepositoryByLocalPath(repo2Path, repositoryUrl); + var repo2 = gitService.getExistingCheckedOutRepositoryByLocalPath(repo2Path, repositoryUri); repo2 = gitService.checkoutRepositoryAtCommit(repo2, submission2.getCommitHash()); return parseFilesAndCreateReport(repo1, repo2); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/hestia/TestwiseCoverageService.java b/src/main/java/de/tum/in/www1/artemis/service/hestia/TestwiseCoverageService.java index 61ba073e54d8..cbe9088285de 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/hestia/TestwiseCoverageService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/hestia/TestwiseCoverageService.java @@ -211,7 +211,7 @@ public void createTestwiseCoverageReport(Map> fi private Map getLineCountByFilePath(ProgrammingSubmission submission) { try { var solutionParticipation = (SolutionProgrammingExerciseParticipation) submission.getParticipation(); - var solutionRepo = gitService.getOrCheckoutRepository(solutionParticipation.getVcsRepositoryUrl(), true); + var solutionRepo = gitService.getOrCheckoutRepository(solutionParticipation.getVcsRepositoryUri(), true); gitService.resetToOriginHead(solutionRepo); gitService.pullIgnoreConflicts(solutionRepo); var solutionFiles = repositoryService.getFilesWithContent(solutionRepo); @@ -255,8 +255,8 @@ private double calculateAggregatedLineCoverage(Map lineCountByF * the same lines, but referencing a different test case. This mapping is still required, but simple summing may * count the same covered lines multiple times. * - * @param report the report for which the line counts of its file reports should be caluclated and saved - * @return the number of covered lines by file path + * @param report the report for which the line counts of its file reports should be calculated and saved + * @return a map with the number of covered lines (value) by file path (key) */ private Map calculateAndSaveUniqueLineCountsByFilePath(CoverageReport report) { var coveredLinesByFilePath = new HashMap(); @@ -272,10 +272,10 @@ private Map calculateAndSaveUniqueLineCountsByFilePath(Coverage } /** - * Return the testwise coverage report for the latest solution submission for a programming exercise without the file reports. + * Return the test-wise coverage report for the latest solution submission for a programming exercise without the file reports. * * @param programmingExercise the exercise for which the latest coverage report should be retrieved - * @return an Optional of the testwise coverage report for the latest solution submission without the file reports + * @return an Optional of the test-wise coverage report for the latest solution submission without the file reports * if a report exists for the latest submission, otherwise an empty Optional */ public Optional getCoverageReportForLatestSolutionSubmissionFromProgrammingExercise(ProgrammingExercise programmingExercise) { @@ -287,10 +287,10 @@ public Optional getCoverageReportForLatestSolutionSubmissionFrom } /** - * Return the full testwise coverage report for the latest solution submission for a programming exercise containing all file reports + * Return the full test-wise coverage report for the latest solution submission for a programming exercise containing all file reports * * @param programmingExercise the exercise for which the latest coverage report should be retrieved - * @return an Optional of the full testwise coverage report for the latest solution submission with all file reports + * @return an Optional of the full test-wise coverage report for the latest solution submission with all file reports * if a report exists for the latest submission, otherwise an empty Optional */ public Optional getFullCoverageReportForLatestSolutionSubmissionFromProgrammingExercise(ProgrammingExercise programmingExercise) { diff --git a/src/main/java/de/tum/in/www1/artemis/service/hestia/behavioral/BehavioralBlackboard.java b/src/main/java/de/tum/in/www1/artemis/service/hestia/behavioral/BehavioralBlackboard.java index bd1ef3c557c4..13e95d073494 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/hestia/behavioral/BehavioralBlackboard.java +++ b/src/main/java/de/tum/in/www1/artemis/service/hestia/behavioral/BehavioralBlackboard.java @@ -7,7 +7,7 @@ import de.tum.in.www1.artemis.domain.hestia.ProgrammingExerciseSolutionEntry; /** - * The blackboard for creating SolutionEntries for behavioral test cases utilizing the git-diff and teswise coverage report. + * The blackboard for creating SolutionEntries for behavioral test cases utilizing the git-diff and test-wise coverage report. */ public class BehavioralBlackboard { diff --git a/src/main/java/de/tum/in/www1/artemis/service/hestia/behavioral/BehavioralTestCaseService.java b/src/main/java/de/tum/in/www1/artemis/service/hestia/behavioral/BehavioralTestCaseService.java index 275c33d4aaa6..9cad795bd8a0 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/hestia/behavioral/BehavioralTestCaseService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/hestia/behavioral/BehavioralTestCaseService.java @@ -169,7 +169,7 @@ private Map readSolutionRepo(ProgrammingExercise programmingExer return Collections.emptyMap(); } var solutionParticipation = solutionParticipationOptional.get(); - var solutionRepo = gitService.getOrCheckoutRepository(solutionParticipation.getVcsRepositoryUrl(), true); + var solutionRepo = gitService.getOrCheckoutRepository(solutionParticipation.getVcsRepositoryUri(), true); gitService.resetToOriginHead(solutionRepo); gitService.pullIgnoreConflicts(solutionRepo); diff --git a/src/main/java/de/tum/in/www1/artemis/service/hestia/behavioral/GroupedFile.java b/src/main/java/de/tum/in/www1/artemis/service/hestia/behavioral/GroupedFile.java index a749a3e96f76..3b0ab9a170e0 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/hestia/behavioral/GroupedFile.java +++ b/src/main/java/de/tum/in/www1/artemis/service/hestia/behavioral/GroupedFile.java @@ -118,63 +118,24 @@ public boolean equals(Object obj) { && Objects.equals(commonChanges, that.commonChanges); } - public static class ChangeBlock implements Comparable { - - private SortedSet lines; - - private boolean isPotential; + public record ChangeBlock(SortedSet lines, boolean isPotential) implements Comparable { public ChangeBlock(Collection lines) { - this.lines = new TreeSet<>(lines); - this.isPotential = false; + this(new TreeSet<>(lines), false); } public ChangeBlock(Collection lines, boolean isPotential) { - this.lines = new TreeSet<>(lines); - this.isPotential = isPotential; - } - - public boolean intersectsOrTouches(GroupedFile.ChangeBlock other) { - return (this.lines.first() > other.lines.first() && this.lines.first() <= other.lines.last() + 1) - || (this.lines.first() < other.lines.first() && this.lines.last() >= other.lines.first() - 1); + this(new TreeSet<>(lines), isPotential); } @Override public int compareTo(GroupedFile.ChangeBlock other) { - return this.lines.first().compareTo(other.lines.first()); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - ChangeBlock that = (ChangeBlock) obj; - return isPotential == that.isPotential && lines.equals(that.lines); - } - - @Override - public int hashCode() { - return Objects.hash(lines, isPotential); - } - - public SortedSet getLines() { - return lines; - } - - public void setLines(Collection lines) { - this.lines = new TreeSet<>(lines); - } - - public boolean isPotential() { - return isPotential; + return Integer.compare(this.lines.first(), other.lines.first()); } - public void setPotential(boolean potential) { - isPotential = potential; + public boolean intersectsOrTouches(ChangeBlock other) { + return (this.lines.first() > other.lines.first() && this.lines.first() <= other.lines.last() + 1) + || (this.lines.first() < other.lines.first() && this.lines.last() >= other.lines.first() - 1); } } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/hestia/behavioral/knowledgesource/AddUncoveredLinesAsPotentialCodeBlocks.java b/src/main/java/de/tum/in/www1/artemis/service/hestia/behavioral/knowledgesource/AddUncoveredLinesAsPotentialCodeBlocks.java index 25fe433da41f..531639d227f8 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/hestia/behavioral/knowledgesource/AddUncoveredLinesAsPotentialCodeBlocks.java +++ b/src/main/java/de/tum/in/www1/artemis/service/hestia/behavioral/knowledgesource/AddUncoveredLinesAsPotentialCodeBlocks.java @@ -41,12 +41,12 @@ public boolean executeAction() { var newChangeBlocks = new TreeSet(); for (ChangeBlock commonChange : groupedFile.getCommonChanges()) { - var firstLine = commonChange.getLines().first(); + var firstLine = commonChange.lines().first(); var potentialPrefix = getPotentialPrefix(firstLine, groupedFile.getFileContent()); if (potentialPrefix != null) { newChangeBlocks.add(potentialPrefix); } - var lastLine = commonChange.getLines().last(); + var lastLine = commonChange.lines().last(); var potentialPostfix = getPotentialPostfix(lastLine, groupedFile.getFileContent()); if (potentialPostfix != null) { newChangeBlocks.add(potentialPostfix); diff --git a/src/main/java/de/tum/in/www1/artemis/service/hestia/behavioral/knowledgesource/CombineChangeBlocks.java b/src/main/java/de/tum/in/www1/artemis/service/hestia/behavioral/knowledgesource/CombineChangeBlocks.java index d592cd9e86c3..afeab1d485c8 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/hestia/behavioral/knowledgesource/CombineChangeBlocks.java +++ b/src/main/java/de/tum/in/www1/artemis/service/hestia/behavioral/knowledgesource/CombineChangeBlocks.java @@ -38,8 +38,8 @@ public boolean executeAction() throws BehavioralSolutionEntryGenerationException if (i < currentChangeBlocks.size() - 1) { var nextChangeBlock = currentChangeBlocks.get(i + 1); if (currentChangeBlock.intersectsOrTouches(nextChangeBlock)) { - var lines = new TreeSet<>(currentChangeBlock.getLines()); - lines.addAll(nextChangeBlock.getLines()); + var lines = new TreeSet<>(currentChangeBlock.lines()); + lines.addAll(nextChangeBlock.lines()); newChangeBlocks.add(new GroupedFile.ChangeBlock(lines)); // Skip the next change block, as it has already been processed i++; diff --git a/src/main/java/de/tum/in/www1/artemis/service/hestia/behavioral/knowledgesource/CreateSolutionEntries.java b/src/main/java/de/tum/in/www1/artemis/service/hestia/behavioral/knowledgesource/CreateSolutionEntries.java index 332c9cf0c772..7411b2d3e0c2 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/hestia/behavioral/knowledgesource/CreateSolutionEntries.java +++ b/src/main/java/de/tum/in/www1/artemis/service/hestia/behavioral/knowledgesource/CreateSolutionEntries.java @@ -44,12 +44,12 @@ private ProgrammingExerciseSolutionEntry createSolutionEntry(GroupedFile grouped var solutionEntry = new ProgrammingExerciseSolutionEntry(); // Set temporary id, as equals checks won't work otherwise solutionEntry.setId(0L); - solutionEntry.setLine(changeBlock.getLines().first()); + solutionEntry.setLine(changeBlock.lines().first()); solutionEntry.setFilePath(groupedFile.getFilePath()); solutionEntry.setTestCase(groupedFile.getTestCase()); var fileContent = groupedFile.getFileContent(); if (fileContent != null) { - var code = Arrays.stream(fileContent.split("\n")).skip(changeBlock.getLines().first() - 1).limit(changeBlock.getLines().size()).collect(Collectors.joining("\n")); + var code = Arrays.stream(fileContent.split("\n")).skip(changeBlock.lines().first() - 1).limit(changeBlock.lines().size()).collect(Collectors.joining("\n")); solutionEntry.setCode(code); } return solutionEntry; diff --git a/src/main/java/de/tum/in/www1/artemis/service/hestia/structural/StructuralTestCaseService.java b/src/main/java/de/tum/in/www1/artemis/service/hestia/structural/StructuralTestCaseService.java index 5bdd6f7ce06c..3d520c16e448 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/hestia/structural/StructuralTestCaseService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/hestia/structural/StructuralTestCaseService.java @@ -81,8 +81,8 @@ public List generateStructuralSolutionEntries( if (solutionParticipation.isEmpty()) { return Collections.emptyList(); } - solutionRepository = gitService.getOrCheckoutRepository(solutionParticipation.get().getVcsRepositoryUrl(), true); - testRepository = gitService.getOrCheckoutRepository(programmingExercise.getVcsTestRepositoryUrl(), true); + solutionRepository = gitService.getOrCheckoutRepository(solutionParticipation.get().getVcsRepositoryUri(), true); + testRepository = gitService.getOrCheckoutRepository(programmingExercise.getVcsTestRepositoryUri(), true); gitService.resetToOriginHead(solutionRepository); gitService.pullIgnoreConflicts(solutionRepository); diff --git a/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisChatSessionService.java b/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisChatSessionService.java index c3f7dbe06423..c67685c0bd30 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisChatSessionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisChatSessionService.java @@ -204,7 +204,7 @@ private void addDiffAndTemplatesForStudentAndExerciseIfPossible(User student, Pr } if (studentParticipations.isEmpty()) { try { - templateRepo = gitService.getOrCheckoutRepository(templateParticipation.get().getVcsRepositoryUrl(), true); + templateRepo = gitService.getOrCheckoutRepository(templateParticipation.get().getVcsRepositoryUri(), true); } catch (GitAPIException e) { throw new InternalServerErrorException("Iris cannot function without template participation"); @@ -214,8 +214,8 @@ private void addDiffAndTemplatesForStudentAndExerciseIfPossible(User student, Pr } try { - templateRepo = gitService.getOrCheckoutRepository(templateParticipation.get().getVcsRepositoryUrl(), true); - studentRepo = gitService.getOrCheckoutRepository(studentParticipations.get(studentParticipations.size() - 1).getVcsRepositoryUrl(), true); + templateRepo = gitService.getOrCheckoutRepository(templateParticipation.get().getVcsRepositoryUri(), true); + studentRepo = gitService.getOrCheckoutRepository(studentParticipations.get(studentParticipations.size() - 1).getVcsRepositoryUri(), true); } catch (GitAPIException e) { throw new InternalServerErrorException("Could not fetch existing student or template participation"); diff --git a/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisCodeEditorSessionService.java b/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisCodeEditorSessionService.java index b93f70c7692d..dd30c5b898a8 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisCodeEditorSessionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisCodeEditorSessionService.java @@ -383,7 +383,7 @@ private Repository templateRepository(ProgrammingExercise exercise) { * @return The test repository */ private Repository testRepository(ProgrammingExercise exercise) { - return Optional.ofNullable(exercise.getVcsTestRepositoryUrl()).map(this::repositoryAt).orElseThrow(); + return Optional.ofNullable(exercise.getVcsTestRepositoryUri()).map(this::repositoryAt).orElseThrow(); } private Repository repositoryFor(ProgrammingExercise exercise, ExerciseComponent component) { @@ -403,7 +403,7 @@ private Repository repositoryFor(ProgrammingExercise exercise, ExerciseComponent * @return The repository */ private Repository repositoryAt(ProgrammingExerciseParticipation participation) { - var url = participation.getVcsRepositoryUrl(); + var url = participation.getVcsRepositoryUri(); try { // This check reduces the amount of REST-calls that retrieve the default branch of a repository. // Retrieving the default branch is not necessary if the repository is already cached. @@ -427,7 +427,7 @@ private Repository repositoryAt(ProgrammingExerciseParticipation participation) * @param url The URL to fetch the repository for * @return The repository */ - private Repository repositoryAt(VcsRepositoryUrl url) { + private Repository repositoryAt(VcsRepositoryUri url) { try { return gitService.getOrCheckoutRepository(url, true); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/notifications/push_notifications/FirebasePushNotificationService.java b/src/main/java/de/tum/in/www1/artemis/service/notifications/push_notifications/FirebasePushNotificationService.java index d2cb8e520729..fae2c844865b 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/notifications/push_notifications/FirebasePushNotificationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/notifications/push_notifications/FirebasePushNotificationService.java @@ -38,7 +38,7 @@ void sendNotificationRequestsToEndpoint(List requests, // The relay server accepts at most 500 messages per batch List> batches = Lists.partition(requests, 500); var futures = batches.stream().map(batch -> CompletableFuture.runAsync(() -> sendSpecificNotificationRequestsToEndpoint(batch, relayBaseUrl))).toList() - .toArray(new CompletableFuture[0]); + .toArray(CompletableFuture[]::new); CompletableFuture.allOf(futures); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/notifications/push_notifications/PushNotificationService.java b/src/main/java/de/tum/in/www1/artemis/service/notifications/push_notifications/PushNotificationService.java index cc5dfaec53b3..1048403e5ce5 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/notifications/push_notifications/PushNotificationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/notifications/push_notifications/PushNotificationService.java @@ -73,7 +73,7 @@ protected PushNotificationService(RestTemplate restTemplate) { void sendNotificationRequestsToEndpoint(List requests, String relayServerBaseUrl) { var futures = requests.stream() .map(request -> CompletableFuture.runAsync(() -> sendSpecificNotificationRequestsToEndpoint(Collections.singletonList(request), relayServerBaseUrl))).toList() - .toArray(new CompletableFuture[0]); + .toArray(CompletableFuture[]::new); CompletableFuture.allOf(futures); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismService.java b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismService.java index f4a5ea141cae..4d5cc113231c 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismService.java @@ -1,7 +1,13 @@ package de.tum.in.www1.artemis.service.plagiarism; +import static java.util.function.Predicate.isEqual; +import static java.util.function.Predicate.not; + +import java.time.ZonedDateTime; import java.util.HashSet; +import java.util.Objects; import java.util.Set; +import java.util.stream.Stream; import org.springframework.stereotype.Service; @@ -38,11 +44,12 @@ public PlagiarismService(PlagiarismComparisonRepository plagiarismComparisonRepo * Anonymize the submission for the student view. * A student should not see sensitive information but be able to retrieve both answers from both students for the comparison * - * @param submission the submission to anonymize. - * @param userLogin the user login of the student asking to see his plagiarism comparison. + * @param submission the submission to anonymize. + * @param userLogin the user login of the student asking to see his plagiarism comparison. + * @param exerciseDueDate due date of the exercise. */ - public void checkAccessAndAnonymizeSubmissionForStudent(Submission submission, String userLogin) { - if (!wasUserNotifiedByInstructor(submission.getId(), userLogin)) { + public void checkAccessAndAnonymizeSubmissionForStudent(Submission submission, String userLogin, ZonedDateTime exerciseDueDate) { + if (!hasAccessToSubmission(submission.getId(), userLogin, exerciseDueDate)) { throw new AccessForbiddenException("This plagiarism submission is not related to the requesting user or the user has not been notified yet."); } submission.setParticipation(null); @@ -50,29 +57,43 @@ public void checkAccessAndAnonymizeSubmissionForStudent(Submission submission, S submission.setSubmissionDate(null); } + /** + * A student should not see both answers from both students for the comparison before the due date + * + * @param submissionId the id of the submission to check. + * @param userLogin the user login of the student asking to see his plagiarism comparison. + * @param exerciseDueDate due date of the exercise. + * @return true is the user has access to the submission + */ + public boolean hasAccessToSubmission(Long submissionId, String userLogin, ZonedDateTime exerciseDueDate) { + var comparisonOptional = plagiarismComparisonRepository.findBySubmissionA_SubmissionIdOrSubmissionB_SubmissionId(submissionId, submissionId); + return comparisonOptional.filter(not(Set::isEmpty)).isPresent() + && isOwnSubmissionOrIsAfterExerciseDueDate(submissionId, userLogin, comparisonOptional.get(), exerciseDueDate) + && wasUserNotifiedByInstructor(userLogin, comparisonOptional.get()); + } + + private boolean isOwnSubmissionOrIsAfterExerciseDueDate(Long submissionId, String userLogin, Set> comparisons, ZonedDateTime exerciseDueDate) { + var isOwnSubmission = comparisons.stream().flatMap(it -> Stream.of(it.getSubmissionA(), it.getSubmissionB())).filter(Objects::nonNull) + .filter(it -> it.getSubmissionId() == submissionId).findFirst().map(PlagiarismSubmission::getStudentLogin).filter(isEqual(userLogin)).isPresent(); + return isOwnSubmission || exerciseDueDate.isBefore(ZonedDateTime.now()); + } + /** * Checks whether the student with the given user login is involved in a plagiarism case which contains the given submissionId and the student is notified by the instructor. * - * @param submissionId the id of a submissions that will be checked in plagiarism cases - * @param userLogin the user login of the student + * @param userLogin the user login of the student * @return true if the student with user login owns one of the submissions in a PlagiarismComparison which contains the given submissionId and is notified by the instructor, * otherwise false */ - public boolean wasUserNotifiedByInstructor(Long submissionId, String userLogin) { - var comparisonOptional = plagiarismComparisonRepository.findBySubmissionA_SubmissionIdOrSubmissionB_SubmissionId(submissionId, submissionId); - + private boolean wasUserNotifiedByInstructor(String userLogin, Set> comparisons) { // disallow requests from users who are not notified about this case: - if (comparisonOptional.isPresent()) { - var comparisons = comparisonOptional.get(); - return comparisons.stream() - .anyMatch(comparison -> (comparison.getSubmissionA().getPlagiarismCase() != null - && (comparison.getSubmissionA().getPlagiarismCase().getPost() != null || comparison.getSubmissionA().getPlagiarismCase().getVerdict() != null) - && (comparison.getSubmissionA().getStudentLogin().equals(userLogin))) - || (comparison.getSubmissionB().getPlagiarismCase() != null - && (comparison.getSubmissionB().getPlagiarismCase().getPost() != null || comparison.getSubmissionB().getPlagiarismCase().getVerdict() != null) - && (comparison.getSubmissionB().getStudentLogin().equals(userLogin)))); - } - return false; + return comparisons.stream() + .anyMatch(comparison -> (comparison.getSubmissionA().getPlagiarismCase() != null + && (comparison.getSubmissionA().getPlagiarismCase().getPost() != null || comparison.getSubmissionA().getPlagiarismCase().getVerdict() != null) + && (comparison.getSubmissionA().getStudentLogin().equals(userLogin))) + || (comparison.getSubmissionB().getPlagiarismCase() != null + && (comparison.getSubmissionB().getPlagiarismCase().getPost() != null || comparison.getSubmissionB().getPlagiarismCase().getVerdict() != null) + && (comparison.getSubmissionB().getStudentLogin().equals(userLogin)))); } /** 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 25b13108afb9..da375c843e01 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 @@ -38,7 +38,7 @@ import de.tum.in.www1.artemis.repository.StudentParticipationRepository; import de.tum.in.www1.artemis.service.AuthorizationCheckService; import de.tum.in.www1.artemis.service.FileService; -import de.tum.in.www1.artemis.service.UrlService; +import de.tum.in.www1.artemis.service.UriService; import de.tum.in.www1.artemis.service.connectors.GitService; import de.tum.in.www1.artemis.service.export.ProgrammingExerciseExportService; import de.tum.in.www1.artemis.service.hestia.ProgrammingExerciseGitDiffReportService; @@ -70,7 +70,7 @@ public class ProgrammingPlagiarismDetectionService { private final PlagiarismCacheService plagiarismCacheService; - private final UrlService urlService; + private final UriService uriService; private final ProgrammingExerciseGitDiffReportService programmingExerciseGitDiffReportService; @@ -78,7 +78,7 @@ public class ProgrammingPlagiarismDetectionService { public ProgrammingPlagiarismDetectionService(FileService fileService, ProgrammingExerciseRepository programmingExerciseRepository, GitService gitService, StudentParticipationRepository studentParticipationRepository, ProgrammingExerciseExportService programmingExerciseExportService, - PlagiarismWebsocketService plagiarismWebsocketService, PlagiarismCacheService plagiarismCacheService, UrlService urlService, + PlagiarismWebsocketService plagiarismWebsocketService, PlagiarismCacheService plagiarismCacheService, UriService uriService, ProgrammingExerciseGitDiffReportService programmingExerciseGitDiffReportService, AuthorizationCheckService authCheckService) { this.fileService = fileService; this.programmingExerciseRepository = programmingExerciseRepository; @@ -87,7 +87,7 @@ public ProgrammingPlagiarismDetectionService(FileService fileService, Programmin this.programmingExerciseExportService = programmingExerciseExportService; this.plagiarismWebsocketService = plagiarismWebsocketService; this.plagiarismCacheService = plagiarismCacheService; - this.urlService = urlService; + this.uriService = uriService; this.programmingExerciseGitDiffReportService = programmingExerciseGitDiffReportService; this.authCheckService = authCheckService; } @@ -188,7 +188,7 @@ private JPlagResult computeJPlagResult(ProgrammingExercise programmingExercise, final var projectKey = programmingExercise.getProjectKey(); final var repoFolder = targetPath.resolve(projectKey).toFile(); final var programmingLanguage = getJPlagProgrammingLanguage(programmingExercise); - final var templateRepoName = urlService.getRepositorySlugFromRepositoryUrl(programmingExercise.getTemplateParticipation().getVcsRepositoryUrl()); + final var templateRepoName = uriService.getRepositorySlugFromRepositoryUri(programmingExercise.getTemplateParticipation().getVcsRepositoryUri()); JPlagOptions options = new JPlagOptions(programmingLanguage, Set.of(repoFolder), Set.of()) // JPlag expects a value between 0.0 and 1.0 @@ -325,7 +325,7 @@ public List filterStudentParticipationsForComp return studentParticipations.parallelStream().filter(participation -> !participation.isPracticeMode()) .filter(participation -> participation instanceof ProgrammingExerciseParticipation).filter(participation -> participation.getStudent().isPresent()) .filter(participation -> !authCheckService.isAtLeastTeachingAssistantForExercise(programmingExercise, participation.getStudent().get())) - .map(participation -> (ProgrammingExerciseParticipation) participation).filter(participation -> participation.getVcsRepositoryUrl() != null) + .map(participation -> (ProgrammingExerciseParticipation) participation).filter(participation -> participation.getVcsRepositoryUri() != null) .filter(participation -> { Submission submission = participation.findLatestSubmission().orElse(null); // filter empty submissions @@ -344,7 +344,7 @@ private Optional cloneTemplateRepository(ProgrammingExercise program return Optional.of(templateRepo); } catch (GitException | GitAPIException ex) { - log.error("Clone template repository {} in exercise '{}' did not work as expected: {}", programmingExercise.getTemplateParticipation().getVcsRepositoryUrl(), + log.error("Clone template repository {} in exercise '{}' did not work as expected: {}", programmingExercise.getTemplateParticipation().getVcsRepositoryUri(), programmingExercise.getTitle(), ex.getMessage()); return Optional.empty(); } @@ -355,8 +355,8 @@ private boolean shouldAddRepo(int minimumSize, Repository repo, Optional= minimumSize; } @@ -388,7 +388,7 @@ private List downloadRepositories(ProgrammingExercise programmingExe } } catch (GitException | GitAPIException | InvalidPathException ex) { - log.error("Clone student repository {} in exercise '{}' did not work as expected: {}", participation.getVcsRepositoryUrl(), programmingExercise.getTitle(), + log.error("Clone student repository {} in exercise '{}' did not work as expected: {}", participation.getVcsRepositoryUri(), programmingExercise.getTitle(), ex.getMessage()); } }); diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportBasicService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportBasicService.java index 183442276126..f139eabc44f4 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportBasicService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportBasicService.java @@ -179,14 +179,14 @@ private void prepareBasicExerciseInformation(final ProgrammingExercise templateE } /** - * Sets up the test repository for a new exercise by setting the repository URL. This does not create the actual + * Sets up the test repository for a new exercise by setting the repository URI. This does not create the actual * repository on the version control server! * * @param newExercise the new exercises that should be created during import */ private void setupTestRepository(ProgrammingExercise newExercise) { final var testRepoName = newExercise.generateRepositoryName(RepositoryType.TESTS); - newExercise.setTestRepositoryUrl(versionControlService.orElseThrow().getCloneRepositoryUrl(newExercise.getProjectKey(), testRepoName).toString()); + newExercise.setTestRepositoryUri(versionControlService.orElseThrow().getCloneRepositoryUri(newExercise.getProjectKey(), testRepoName).toString()); } /** diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportFromFileService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportFromFileService.java index d8574352dd60..ee09242058cc 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportFromFileService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportFromFileService.java @@ -157,9 +157,9 @@ private void copyEmbeddedFiles(Path importExerciseDir) throws IOException { private void importRepositoriesFromFile(ProgrammingExercise newExercise, Path basePath, String oldExerciseShortName, User user) throws IOException, GitAPIException, URISyntaxException { - Repository templateRepo = gitService.getOrCheckoutRepository(new VcsRepositoryUrl(newExercise.getTemplateRepositoryUrl()), false); - Repository solutionRepo = gitService.getOrCheckoutRepository(new VcsRepositoryUrl(newExercise.getSolutionRepositoryUrl()), false); - Repository testRepo = gitService.getOrCheckoutRepository(new VcsRepositoryUrl(newExercise.getTestRepositoryUrl()), false); + Repository templateRepo = gitService.getOrCheckoutRepository(new VcsRepositoryUri(newExercise.getTemplateRepositoryUri()), false); + Repository solutionRepo = gitService.getOrCheckoutRepository(new VcsRepositoryUri(newExercise.getSolutionRepositoryUri()), false); + Repository testRepo = gitService.getOrCheckoutRepository(new VcsRepositoryUri(newExercise.getTestRepositoryUri()), false); copyImportedExerciseContentToRepositories(templateRepo, solutionRepo, testRepo, basePath); replaceImportedExerciseShortName(Map.of(oldExerciseShortName, newExercise.getShortName()), templateRepo, solutionRepo, testRepo); diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportService.java index 14b42786a60d..a88218afc681 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportService.java @@ -23,7 +23,7 @@ import de.tum.in.www1.artemis.repository.AuxiliaryRepositoryRepository; import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.service.FileService; -import de.tum.in.www1.artemis.service.UrlService; +import de.tum.in.www1.artemis.service.UriService; import de.tum.in.www1.artemis.service.connectors.GitService; import de.tum.in.www1.artemis.service.connectors.ci.ContinuousIntegrationService; import de.tum.in.www1.artemis.service.connectors.ci.ContinuousIntegrationTriggerService; @@ -53,7 +53,7 @@ public class ProgrammingExerciseImportService { private final AuxiliaryRepositoryRepository auxiliaryRepositoryRepository; - private final UrlService urlService; + private final UriService uriService; private final TemplateUpgradePolicy templateUpgradePolicy; @@ -62,7 +62,7 @@ public class ProgrammingExerciseImportService { public ProgrammingExerciseImportService(Optional versionControlService, Optional continuousIntegrationService, Optional continuousIntegrationTriggerService, ProgrammingExerciseService programmingExerciseService, ProgrammingExerciseTaskService programmingExerciseTaskService, GitService gitService, FileService fileService, UserRepository userRepository, - AuxiliaryRepositoryRepository auxiliaryRepositoryRepository, UrlService urlService, TemplateUpgradePolicy templateUpgradePolicy, + AuxiliaryRepositoryRepository auxiliaryRepositoryRepository, UriService uriService, TemplateUpgradePolicy templateUpgradePolicy, ProgrammingExerciseImportBasicService programmingExerciseImportBasicService) { this.versionControlService = versionControlService; this.continuousIntegrationService = continuousIntegrationService; @@ -73,7 +73,7 @@ public ProgrammingExerciseImportService(Optional versionC this.fileService = fileService; this.userRepository = userRepository; this.auxiliaryRepositoryRepository = auxiliaryRepositoryRepository; - this.urlService = urlService; + this.uriService = uriService; this.templateUpgradePolicy = templateUpgradePolicy; this.programmingExerciseImportBasicService = programmingExerciseImportBasicService; } @@ -93,9 +93,9 @@ public void importRepositories(final ProgrammingExercise templateExercise, final VersionControlService versionControl = versionControlService.orElseThrow(); versionControl.createProjectForExercise(newExercise); // Copy all repositories - String templateRepoName = urlService.getRepositorySlugFromRepositoryUrlString(templateExercise.getTemplateRepositoryUrl()); - String testRepoName = urlService.getRepositorySlugFromRepositoryUrlString(templateExercise.getTestRepositoryUrl()); - String solutionRepoName = urlService.getRepositorySlugFromRepositoryUrlString(templateExercise.getSolutionRepositoryUrl()); + String templateRepoName = uriService.getRepositorySlugFromRepositoryUriString(templateExercise.getTemplateRepositoryUri()); + String testRepoName = uriService.getRepositorySlugFromRepositoryUriString(templateExercise.getTestRepositoryUri()); + String solutionRepoName = uriService.getRepositorySlugFromRepositoryUriString(templateExercise.getSolutionRepositoryUri()); String sourceBranch = versionControl.getOrRetrieveBranchOfExercise(templateExercise); @@ -107,16 +107,16 @@ public void importRepositories(final ProgrammingExercise templateExercise, final List auxRepos = templateExercise.getAuxiliaryRepositories(); for (int i = 0; i < auxRepos.size(); i++) { AuxiliaryRepository auxRepo = auxRepos.get(i); - var repoUrl = versionControl.copyRepository(sourceProjectKey, auxRepo.getRepositoryName(), sourceBranch, targetProjectKey, auxRepo.getName()).toString(); + var repoUri = versionControl.copyRepository(sourceProjectKey, auxRepo.getRepositoryName(), sourceBranch, targetProjectKey, auxRepo.getName()).toString(); AuxiliaryRepository newAuxRepo = newExercise.getAuxiliaryRepositories().get(i); - newAuxRepo.setRepositoryUrl(repoUrl); + newAuxRepo.setRepositoryUri(repoUri); auxiliaryRepositoryRepository.save(newAuxRepo); } // Unprotect the default branch of the template exercise repo. - VcsRepositoryUrl templateVcsRepositoryUrl = newExercise.getVcsTemplateRepositoryUrl(); + VcsRepositoryUri templateVcsRepositoryUri = newExercise.getVcsTemplateRepositoryUri(); String templateVcsRepositoryBranch = versionControl.getOrRetrieveBranchOfExercise(templateExercise); - versionControl.unprotectBranch(templateVcsRepositoryUrl, templateVcsRepositoryBranch); + versionControl.unprotectBranch(templateVcsRepositoryUri, templateVcsRepositoryBranch); // Add the necessary hooks notifying Artemis about changes after commits have been pushed versionControl.addWebHooksForExercise(newExercise); @@ -142,12 +142,12 @@ public void importBuildPlans(final ProgrammingExercise templateExercise, final P final var solutionParticipation = newExercise.getSolutionParticipation(); final var targetExerciseProjectKey = newExercise.getProjectKey(); - // Clone all build plans, enable them and set up the initial participations, i.e. setting the correct repo URLs and + // Clone all build plans, enable them and set up the initial participations, i.e. setting the correct repo URIs and // running the plan for the first time cloneAndEnableAllBuildPlans(templateExercise, newExercise); - updatePlanRepositoriesInBuildPlans(newExercise, templateParticipation, solutionParticipation, targetExerciseProjectKey, templateExercise.getTemplateRepositoryUrl(), - templateExercise.getSolutionRepositoryUrl(), templateExercise.getTestRepositoryUrl(), templateExercise.getAuxiliaryRepositoriesForBuildPlan()); + updatePlanRepositoriesInBuildPlans(newExercise, templateParticipation, solutionParticipation, targetExerciseProjectKey, templateExercise.getTemplateRepositoryUri(), + templateExercise.getSolutionRepositoryUri(), templateExercise.getTestRepositoryUri(), templateExercise.getAuxiliaryRepositoriesForBuildPlan()); ContinuousIntegrationTriggerService triggerService = continuousIntegrationTriggerService.orElseThrow(); triggerService.triggerBuild(templateParticipation); @@ -155,26 +155,26 @@ public void importBuildPlans(final ProgrammingExercise templateExercise, final P } private void updatePlanRepositoriesInBuildPlans(ProgrammingExercise newExercise, TemplateProgrammingExerciseParticipation templateParticipation, - SolutionProgrammingExerciseParticipation solutionParticipation, String targetExerciseProjectKey, String oldExerciseRepoUrl, String oldSolutionRepoUrl, - String oldTestRepoUrl, List oldBuildPlanAuxiliaryRepositories) { + SolutionProgrammingExerciseParticipation solutionParticipation, String targetExerciseProjectKey, String oldExerciseRepoUri, String oldSolutionRepoUri, + String oldTestRepoUri, List oldBuildPlanAuxiliaryRepositories) { String newExerciseBranch = versionControlService.orElseThrow().getOrRetrieveBranchOfExercise(newExercise); // update 2 repositories for the BASE build plan --> adapt the triggers so that only the assignment repo (and not the tests' repo) will trigger the BASE build plan ContinuousIntegrationService continuousIntegration = continuousIntegrationService.orElseThrow(); continuousIntegration.updatePlanRepository(targetExerciseProjectKey, templateParticipation.getBuildPlanId(), ASSIGNMENT_REPO_NAME, targetExerciseProjectKey, - newExercise.getTemplateRepositoryUrl(), oldExerciseRepoUrl, newExerciseBranch); + newExercise.getTemplateRepositoryUri(), oldExerciseRepoUri, newExerciseBranch); continuousIntegration.updatePlanRepository(targetExerciseProjectKey, templateParticipation.getBuildPlanId(), TEST_REPO_NAME, targetExerciseProjectKey, - newExercise.getTestRepositoryUrl(), oldTestRepoUrl, newExerciseBranch); + newExercise.getTestRepositoryUri(), oldTestRepoUri, newExerciseBranch); updateAuxiliaryRepositoriesForNewExercise(newExercise.getAuxiliaryRepositoriesForBuildPlan(), oldBuildPlanAuxiliaryRepositories, templateParticipation, targetExerciseProjectKey, newExercise); // update 2 repositories for the SOLUTION build plan continuousIntegration.updatePlanRepository(targetExerciseProjectKey, solutionParticipation.getBuildPlanId(), ASSIGNMENT_REPO_NAME, targetExerciseProjectKey, - newExercise.getSolutionRepositoryUrl(), oldSolutionRepoUrl, newExerciseBranch); + newExercise.getSolutionRepositoryUri(), oldSolutionRepoUri, newExerciseBranch); continuousIntegration.updatePlanRepository(targetExerciseProjectKey, solutionParticipation.getBuildPlanId(), TEST_REPO_NAME, targetExerciseProjectKey, - newExercise.getTestRepositoryUrl(), oldTestRepoUrl, newExerciseBranch); + newExercise.getTestRepositoryUri(), oldTestRepoUri, newExerciseBranch); updateAuxiliaryRepositoriesForNewExercise(newExercise.getAuxiliaryRepositoriesForBuildPlan(), oldBuildPlanAuxiliaryRepositories, solutionParticipation, targetExerciseProjectKey, newExercise); @@ -187,7 +187,7 @@ private void updateAuxiliaryRepositoriesForNewExercise(List AuxiliaryRepository oldAuxiliaryRepository = oldRepositories.get(i); String auxiliaryBranch = versionControlService.orElseThrow().getOrRetrieveBranchOfExercise(newExercise); continuousIntegrationService.orElseThrow().updatePlanRepository(targetExerciseProjectKey, participation.getBuildPlanId(), newAuxiliaryRepository.getName(), - targetExerciseProjectKey, newAuxiliaryRepository.getRepositoryUrl(), oldAuxiliaryRepository.getRepositoryUrl(), auxiliaryBranch); + targetExerciseProjectKey, newAuxiliaryRepository.getRepositoryUri(), oldAuxiliaryRepository.getRepositoryUri(), auxiliaryBranch); } } @@ -253,8 +253,8 @@ private void adjustProjectNames(ProgrammingExercise templateExercise, Programmin * @throws GitAPIException If the checkout/push of one repository fails */ private void adjustProjectName(Map replacements, String projectKey, String repositoryName, User user) throws GitAPIException { - final var repositoryUrl = versionControlService.orElseThrow().getCloneRepositoryUrl(projectKey, repositoryName); - Repository repository = gitService.getOrCheckoutRepository(repositoryUrl, true); + final var repositoryUri = versionControlService.orElseThrow().getCloneRepositoryUri(projectKey, repositoryName); + Repository repository = gitService.getOrCheckoutRepository(repositoryUri, true); fileService.replaceVariablesInFileRecursive(repository.getLocalPath().toAbsolutePath(), replacements, List.of("gradle-wrapper.jar")); gitService.stageAllChanges(repository); gitService.commitAndPush(repository, "Template adjusted by Artemis", true, user); diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseParticipationService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseParticipationService.java index 8512cda337f3..94484c09c4a7 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseParticipationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseParticipationService.java @@ -208,7 +208,7 @@ public ProgrammingExerciseParticipation findProgrammingExerciseParticipationWith /** * Setup the initial solution participation for an exercise. Creates the new participation entity and sets - * the correct build plan ID and repository URL. Saves the participation after all values have been set. + * the correct build plan ID and repository URI. Saves the participation after all values have been set. * * @param newExercise The new exercise for which a participation should be generated */ @@ -217,14 +217,14 @@ public void setupInitialSolutionParticipation(ProgrammingExercise newExercise) { SolutionProgrammingExerciseParticipation solutionParticipation = new SolutionProgrammingExerciseParticipation(); newExercise.setSolutionParticipation(solutionParticipation); solutionParticipation.setBuildPlanId(newExercise.generateBuildPlanId(BuildPlanType.SOLUTION)); - solutionParticipation.setRepositoryUrl(versionControlService.orElseThrow().getCloneRepositoryUrl(newExercise.getProjectKey(), solutionRepoName).toString()); + solutionParticipation.setRepositoryUri(versionControlService.orElseThrow().getCloneRepositoryUri(newExercise.getProjectKey(), solutionRepoName).toString()); solutionParticipation.setProgrammingExercise(newExercise); solutionParticipationRepository.save(solutionParticipation); } /** * Setup the initial template participation for an exercise. Creates the new participation entity and sets - * the correct build plan ID and repository URL. Saves the participation after all values have been set. + * the correct build plan ID and repository URI. Saves the participation after all values have been set. * * @param newExercise The new exercise for which a participation should be generated */ @@ -232,7 +232,7 @@ public void setupInitialTemplateParticipation(ProgrammingExercise newExercise) { final String exerciseRepoName = newExercise.generateRepositoryName(RepositoryType.TEMPLATE); TemplateProgrammingExerciseParticipation templateParticipation = new TemplateProgrammingExerciseParticipation(); templateParticipation.setBuildPlanId(newExercise.generateBuildPlanId(BuildPlanType.TEMPLATE)); - templateParticipation.setRepositoryUrl(versionControlService.orElseThrow().getCloneRepositoryUrl(newExercise.getProjectKey(), exerciseRepoName).toString()); + templateParticipation.setRepositoryUri(versionControlService.orElseThrow().getCloneRepositoryUri(newExercise.getProjectKey(), exerciseRepoName).toString()); templateParticipation.setProgrammingExercise(newExercise); newExercise.setTemplateParticipation(templateParticipation); templateParticipationRepository.save(templateParticipation); @@ -247,7 +247,7 @@ public void setupInitialTemplateParticipation(ProgrammingExercise newExercise) { */ public void lockStudentRepository(ProgrammingExercise programmingExercise, ProgrammingExerciseStudentParticipation participation) { if (participation.getInitializationState().hasCompletedState(InitializationState.REPO_CONFIGURED)) { - versionControlService.orElseThrow().setRepositoryPermissionsToReadOnly(participation.getVcsRepositoryUrl(), programmingExercise.getProjectKey(), + versionControlService.orElseThrow().setRepositoryPermissionsToReadOnly(participation.getVcsRepositoryUri(), programmingExercise.getProjectKey(), participation.getStudents()); } else { @@ -316,7 +316,7 @@ public void lockStudentRepositoryAndParticipation(ProgrammingExercise programmin public void unlockStudentRepository(ProgrammingExerciseStudentParticipation participation) { if (participation.getInitializationState().hasCompletedState(InitializationState.REPO_CONFIGURED)) { for (User user : participation.getStudents()) { - versionControlService.orElseThrow().addMemberToRepository(participation.getVcsRepositoryUrl(), user, VersionControlRepositoryPermission.REPO_WRITE); + versionControlService.orElseThrow().addMemberToRepository(participation.getVcsRepositoryUri(), user, VersionControlRepositoryPermission.REPO_WRITE); } } else { @@ -377,7 +377,7 @@ public void stashChangesInStudentRepositoryAfterDueDateHasPassed(ProgrammingExer * @param targetURL the repository where all files should be replaced * @param sourceURL the repository that should be used as source for all files */ - public void resetRepository(VcsRepositoryUrl targetURL, VcsRepositoryUrl sourceURL) throws GitAPIException, IOException { + public void resetRepository(VcsRepositoryUri targetURL, VcsRepositoryUri sourceURL) throws GitAPIException, IOException { Repository targetRepo = gitService.getOrCheckoutRepository(targetURL, true); Repository sourceRepo = gitService.getOrCheckoutRepository(sourceURL, true); @@ -404,8 +404,8 @@ public void resetRepository(VcsRepositoryUrl targetURL, VcsRepositoryUrl sourceU * participation. * * @param exercise the exercise for which to get the participation. - * @param repositoryTypeOrUserName the repository type ("exercise", "solution", or "tests") or username (e.g. "artemis_test_user_1") as extracted from the repository URL. - * @param isPracticeRepository whether the repository is a practice repository, i.e. the repository URL contains "-practice-". + * @param repositoryTypeOrUserName the repository type ("exercise", "solution", or "tests") or username (e.g. "artemis_test_user_1") as extracted from the repository URI. + * @param isPracticeRepository whether the repository is a practice repository, i.e. the repository URI contains "-practice-". * @param withSubmissions whether submissions should be loaded with the participation. This is needed for the local CI system. * @return the participation. * @throws EntityNotFoundException if the participation could not be found. @@ -443,7 +443,7 @@ public ProgrammingExerciseParticipation getParticipationForRepository(Programmin // If the exercise is an exam exercise and the repository's owner is at least an editor, the repository could be a test run repository, or it could be the instructor's // assignment repository. - // There is no way to tell from the repository URL, and only one participation will be created, even if both are used. + // There is no way to tell from the repository URI, and only one participation will be created, even if both are used. // This participation has "testRun = true" set if the test run was created first, and "testRun = false" set if the instructor's assignment repository was created first. // If the exercise is an exam exercise, and the repository's owner is at least an editor, get the participation without regard for the testRun flag. boolean isExamEditorRepository = exercise.isExamExercise() @@ -467,10 +467,10 @@ public ProgrammingExerciseParticipation getParticipationForRepository(Programmin */ public List getCommitInfos(ProgrammingExerciseStudentParticipation participation) { try { - return gitService.getCommitInfos(participation.getVcsRepositoryUrl()); + return gitService.getCommitInfos(participation.getVcsRepositoryUri()); } catch (GitAPIException e) { - log.error("Could not get commit infos for participation " + participation.getId() + " with repository url " + participation.getVcsRepositoryUrl()); + log.error("Could not get commit infos for participation " + participation.getId() + " with repository uri " + participation.getVcsRepositoryUri()); return List.of(); } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseRepositoryService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseRepositoryService.java index 2a02704debb3..f4c09ef3d395 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseRepositoryService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseRepositoryService.java @@ -118,8 +118,8 @@ private RepositoryResources getRepositoryResources(final ProgrammingExercise pro final ProjectType projectType = programmingExercise.getProjectType(); final Path projectTypeTemplateDir = getTemplateDirectoryForRepositoryType(repositoryType); - final VcsRepositoryUrl repoUrl = programmingExercise.getRepositoryURL(repositoryType); - final Repository repo = gitService.getOrCheckoutRepository(repoUrl, true); + final VcsRepositoryUri repoUri = programmingExercise.getRepositoryURL(repositoryType); + final Repository repo = gitService.getOrCheckoutRepository(repoUri, true); // Get path, files and prefix for the programming-language dependent files. They are copied first. final Path generalTemplatePath = ProgrammingExerciseService.getProgrammingLanguageTemplatePath(programmingExercise.getProgrammingLanguage()) @@ -180,10 +180,10 @@ private void setupRepositories(final ProgrammingExercise programmingExercise, fi try { setupTemplateAndPush(exerciseResources, "Exercise", programmingExercise, exerciseCreator); // The template repo can be re-written, so we can unprotect the default branch. - final var templateVcsRepositoryUrl = programmingExercise.getVcsTemplateRepositoryUrl(); + final var templateVcsRepositoryUri = programmingExercise.getVcsTemplateRepositoryUri(); VersionControlService versionControl = versionControlService.orElseThrow(); final String templateBranch = versionControl.getOrRetrieveBranchOfExercise(programmingExercise); - versionControl.unprotectBranch(templateVcsRepositoryUrl, templateBranch); + versionControl.unprotectBranch(templateVcsRepositoryUri, templateBranch); setupTemplateAndPush(solutionResources, "Solution", programmingExercise, exerciseCreator); setupTestTemplateAndPush(testResources, programmingExercise, exerciseCreator); @@ -221,9 +221,9 @@ private void createAndInitializeAuxiliaryRepositories(final String projectKey, f for (final AuxiliaryRepository repo : programmingExercise.getAuxiliaryRepositories()) { final String repositoryName = programmingExercise.generateRepositoryName(repo.getName()); versionControlService.orElseThrow().createRepository(projectKey, repositoryName, null); - repo.setRepositoryUrl(versionControlService.orElseThrow().getCloneRepositoryUrl(programmingExercise.getProjectKey(), repositoryName).toString()); + repo.setRepositoryUri(versionControlService.orElseThrow().getCloneRepositoryUri(programmingExercise.getProjectKey(), repositoryName).toString()); - final Repository vcsRepository = gitService.getOrCheckoutRepository(repo.getVcsRepositoryUrl(), true); + final Repository vcsRepository = gitService.getOrCheckoutRepository(repo.getVcsRepositoryUri(), true); gitService.commitAndPush(vcsRepository, SETUP_COMMIT_MESSAGE, true, null); } } @@ -721,23 +721,23 @@ else if (moreLenientStartDate || moreLenientDueDate) { * @param programmingExercise The programming exercise for which the repositories should be deleted. */ void deleteRepositories(final ProgrammingExercise programmingExercise) { - if (programmingExercise.getTemplateRepositoryUrl() != null) { - final var templateRepositoryUrlAsUrl = programmingExercise.getVcsTemplateRepositoryUrl(); - versionControlService.orElseThrow().deleteRepository(templateRepositoryUrlAsUrl); + if (programmingExercise.getTemplateRepositoryUri() != null) { + final var templateRepositoryUriAsUrl = programmingExercise.getVcsTemplateRepositoryUri(); + versionControlService.orElseThrow().deleteRepository(templateRepositoryUriAsUrl); } - if (programmingExercise.getSolutionRepositoryUrl() != null) { - final var solutionRepositoryUrlAsUrl = programmingExercise.getVcsSolutionRepositoryUrl(); - versionControlService.orElseThrow().deleteRepository(solutionRepositoryUrlAsUrl); + if (programmingExercise.getSolutionRepositoryUri() != null) { + final var solutionRepositoryUriAsUrl = programmingExercise.getVcsSolutionRepositoryUri(); + versionControlService.orElseThrow().deleteRepository(solutionRepositoryUriAsUrl); } - if (programmingExercise.getTestRepositoryUrl() != null) { - final var testRepositoryUrlAsUrl = programmingExercise.getVcsTestRepositoryUrl(); - versionControlService.orElseThrow().deleteRepository(testRepositoryUrlAsUrl); + if (programmingExercise.getTestRepositoryUri() != null) { + final var testRepositoryUriAsUrl = programmingExercise.getVcsTestRepositoryUri(); + versionControlService.orElseThrow().deleteRepository(testRepositoryUriAsUrl); } // We also want to delete any auxiliary repositories programmingExercise.getAuxiliaryRepositories().forEach(repo -> { - if (repo.getRepositoryUrl() != null) { - versionControlService.orElseThrow().deleteRepository(repo.getVcsRepositoryUrl()); + if (repo.getRepositoryUri() != null) { + versionControlService.orElseThrow().deleteRepository(repo.getVcsRepositoryUri()); } }); @@ -758,17 +758,17 @@ void deleteRepositories(final ProgrammingExercise programmingExercise) { * @param programmingExercise The exercise for which the local repository copies should be deleted. */ void deleteLocalRepoCopies(final ProgrammingExercise programmingExercise) { - if (programmingExercise.getTemplateRepositoryUrl() != null) { - final var templateRepositoryUrlAsUrl = programmingExercise.getVcsTemplateRepositoryUrl(); - gitService.deleteLocalRepository(templateRepositoryUrlAsUrl); + if (programmingExercise.getTemplateRepositoryUri() != null) { + final var templateRepositoryUriAsUrl = programmingExercise.getVcsTemplateRepositoryUri(); + gitService.deleteLocalRepository(templateRepositoryUriAsUrl); } - if (programmingExercise.getSolutionRepositoryUrl() != null) { - final var solutionRepositoryUrlAsUrl = programmingExercise.getVcsSolutionRepositoryUrl(); - gitService.deleteLocalRepository(solutionRepositoryUrlAsUrl); + if (programmingExercise.getSolutionRepositoryUri() != null) { + final var solutionRepositoryUriAsUrl = programmingExercise.getVcsSolutionRepositoryUri(); + gitService.deleteLocalRepository(solutionRepositoryUriAsUrl); } - if (programmingExercise.getTestRepositoryUrl() != null) { - final var testRepositoryUrlAsUrl = programmingExercise.getVcsTestRepositoryUrl(); - gitService.deleteLocalRepository(testRepositoryUrlAsUrl); + if (programmingExercise.getTestRepositoryUri() != null) { + final var testRepositoryUriAsUrl = programmingExercise.getVcsTestRepositoryUri(); + gitService.deleteLocalRepository(testRepositoryUriAsUrl); } } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseService.java index 7647e6331af8..93de161be767 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseService.java @@ -373,16 +373,16 @@ public void validateStaticCodeAnalysisSettings(ProgrammingExercise programmingEx public void setupBuildPlansForNewExercise(ProgrammingExercise programmingExercise, boolean isImportedFromFile) { String projectKey = programmingExercise.getProjectKey(); // Get URLs for repos - var exerciseRepoUrl = programmingExercise.getVcsTemplateRepositoryUrl(); - var testsRepoUrl = programmingExercise.getVcsTestRepositoryUrl(); - var solutionRepoUrl = programmingExercise.getVcsSolutionRepositoryUrl(); + var exerciseRepoUri = programmingExercise.getVcsTemplateRepositoryUri(); + var testsRepoUri = programmingExercise.getVcsTestRepositoryUri(); + var solutionRepoUri = programmingExercise.getVcsSolutionRepositoryUri(); ContinuousIntegrationService continuousIntegration = continuousIntegrationService.orElseThrow(); continuousIntegration.createProjectForExercise(programmingExercise); // template build plan - continuousIntegration.createBuildPlanForExercise(programmingExercise, TEMPLATE.getName(), exerciseRepoUrl, testsRepoUrl, solutionRepoUrl); + continuousIntegration.createBuildPlanForExercise(programmingExercise, TEMPLATE.getName(), exerciseRepoUri, testsRepoUri, solutionRepoUri); // solution build plan - continuousIntegration.createBuildPlanForExercise(programmingExercise, SOLUTION.getName(), solutionRepoUrl, testsRepoUrl, solutionRepoUrl); + continuousIntegration.createBuildPlanForExercise(programmingExercise, SOLUTION.getName(), solutionRepoUri, testsRepoUri, solutionRepoUri); // Give appropriate permissions for CI projects continuousIntegration.removeAllDefaultProjectPermissions(projectKey); @@ -440,15 +440,15 @@ private void setURLsAndBuildPlanIDsForNewExercise(ProgrammingExercise programmin VersionControlService versionControl = versionControlService.orElseThrow(); templateParticipation.setBuildPlanId(templatePlanId); // Set build plan id to newly created BaseBuild plan - templateParticipation.setRepositoryUrl(versionControl.getCloneRepositoryUrl(projectKey, exerciseRepoName).toString()); + templateParticipation.setRepositoryUri(versionControl.getCloneRepositoryUri(projectKey, exerciseRepoName).toString()); solutionParticipation.setBuildPlanId(solutionPlanId); - solutionParticipation.setRepositoryUrl(versionControl.getCloneRepositoryUrl(projectKey, solutionRepoName).toString()); - programmingExercise.setTestRepositoryUrl(versionControl.getCloneRepositoryUrl(projectKey, testRepoName).toString()); + solutionParticipation.setRepositoryUri(versionControl.getCloneRepositoryUri(projectKey, solutionRepoName).toString()); + programmingExercise.setTestRepositoryUri(versionControl.getCloneRepositoryUri(projectKey, testRepoName).toString()); } private void setURLsForAuxiliaryRepositoriesOfExercise(ProgrammingExercise programmingExercise) { - programmingExercise.getAuxiliaryRepositories().forEach(repo -> repo.setRepositoryUrl(versionControlService.orElseThrow() - .getCloneRepositoryUrl(programmingExercise.getProjectKey(), programmingExercise.generateRepositoryName(repo.getName())).toString())); + programmingExercise.getAuxiliaryRepositories().forEach(repo -> repo.setRepositoryUri(versionControlService.orElseThrow() + .getCloneRepositoryUri(programmingExercise.getProjectKey(), programmingExercise.generateRepositoryName(repo.getName())).toString())); } public static Path getProgrammingLanguageProjectTypePath(ProgrammingLanguage programmingLanguage, ProjectType projectType) { @@ -569,20 +569,20 @@ public ProgrammingExercise updateProblemStatement(ProgrammingExercise programmin * This method calls the StructureOracleGenerator, generates the string out of the JSON representation of the structure oracle of the programming exercise and returns true if * the file was updated or generated, false otherwise. This can happen if the contents of the file have not changed. * - * @param solutionRepoURL The URL of the solution repository. - * @param exerciseRepoURL The URL of the exercise repository. - * @param testRepoURL The URL of the tests' repository. + * @param solutionRepoUri The URL of the solution repository. + * @param exerciseRepoUri The URL of the exercise repository. + * @param testRepoUri The URL of the tests' repository. * @param testsPath The path to the tests' folder, e.g. the path inside the repository where the structure oracle file will be saved in. * @param user The user who has initiated the action * @return True, if the structure oracle was successfully generated or updated, false if no changes to the file were made. * @throws IOException If the URLs cannot be converted to actual {@link Path paths} * @throws GitAPIException If the checkout fails */ - public boolean generateStructureOracleFile(VcsRepositoryUrl solutionRepoURL, VcsRepositoryUrl exerciseRepoURL, VcsRepositoryUrl testRepoURL, String testsPath, User user) + public boolean generateStructureOracleFile(VcsRepositoryUri solutionRepoUri, VcsRepositoryUri exerciseRepoUri, VcsRepositoryUri testRepoUri, String testsPath, User user) throws IOException, GitAPIException { - Repository solutionRepository = gitService.getOrCheckoutRepository(solutionRepoURL, true); - Repository exerciseRepository = gitService.getOrCheckoutRepository(exerciseRepoURL, true); - Repository testRepository = gitService.getOrCheckoutRepository(testRepoURL, true); + Repository solutionRepository = gitService.getOrCheckoutRepository(solutionRepoUri, true); + Repository exerciseRepository = gitService.getOrCheckoutRepository(exerciseRepoUri, true); + Repository testRepository = gitService.getOrCheckoutRepository(testRepoUri, true); gitService.resetToOriginHead(solutionRepository); gitService.pullIgnoreConflicts(solutionRepository); diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingSubmissionService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingSubmissionService.java index 0586a54cfa05..b42531af670c 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingSubmissionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingSubmissionService.java @@ -122,8 +122,7 @@ public ProgrammingSubmission processNewProgrammingSubmission(ProgrammingExercise // we can find this out by looking into the requestBody, e.g. changes=[{ref={id=refs/heads/BitbucketStationSupplies, displayId=BitbucketStationSupplies, type=BRANCH} // if the branch is different from main, throw an IllegalArgumentException, but make sure the REST call still returns 200 to Bitbucket commit = versionControl.getLastCommitDetails(requestBody); - log.info("NotifyPush invoked due to the commit {} by {} with {} in branch {}", commit.getCommitHash(), commit.getAuthorName(), commit.getAuthorEmail(), - commit.getBranch()); + log.info("NotifyPush invoked due to the commit {} by {} with {} in branch {}", commit.commitHash(), commit.authorName(), commit.authorEmail(), commit.branch()); } catch (Exception ex) { log.error("Commit could not be parsed for submission from participation {}", participation, ex); @@ -131,13 +130,12 @@ public ProgrammingSubmission processNewProgrammingSubmission(ProgrammingExercise } String branch = versionControl.getOrRetrieveBranchOfParticipation(participation); - if (commit.getBranch() != null && !commit.getBranch().equalsIgnoreCase(branch)) { + if (commit.branch() != null && !commit.branch().equalsIgnoreCase(branch)) { // if the commit was made in a branch different from the default, ignore this throw new VersionControlException( - "Submission for participation id " + participation.getId() + " in branch " + commit.getBranch() + " will be ignored! Only the default branch is considered"); + "Submission for participation id " + participation.getId() + " in branch " + commit.branch() + " will be ignored! Only the default branch is considered"); } - if (artemisGitName.equalsIgnoreCase(commit.getAuthorName()) && artemisGitEmail.equalsIgnoreCase(commit.getAuthorEmail()) - && SETUP_COMMIT_MESSAGE.equals(commit.getMessage())) { + if (artemisGitName.equalsIgnoreCase(commit.authorName()) && artemisGitEmail.equalsIgnoreCase(commit.authorEmail()) && SETUP_COMMIT_MESSAGE.equals(commit.message())) { // if the commit was made by Artemis and the message is "Setup" (this means it is an empty setup commit), we ignore this as well and do not create a submission! throw new IllegalStateException("Submission for participation id " + participation.getId() + " based on an empty setup commit by Artemis will be ignored!"); } @@ -155,7 +153,7 @@ public ProgrammingSubmission processNewProgrammingSubmission(ProgrammingExercise // TODO: there might be cases in which Artemis should NOT trigger the build try { - continuousIntegrationTriggerService.orElseThrow().triggerBuild(participation, commit.getCommitHash(), false); + continuousIntegrationTriggerService.orElseThrow().triggerBuild(participation, commit.commitHash(), false); } catch (ContinuousIntegrationException ex) { // TODO: This case is currently not handled. The correct handling would be creating the submission and informing the user that the build trigger failed. @@ -163,14 +161,14 @@ public ProgrammingSubmission processNewProgrammingSubmission(ProgrammingExercise // There can't be two submissions for the same participation and commitHash! ProgrammingSubmission programmingSubmission = programmingSubmissionRepository.findFirstByParticipationIdAndCommitHashOrderByIdDesc(participation.getId(), - commit.getCommitHash()); + commit.commitHash()); if (programmingSubmission != null) { - throw new IllegalStateException("Submission for participation id " + participation.getId() + " and commitHash " + commit.getCommitHash() + " already exists!"); + throw new IllegalStateException("Submission for participation id " + participation.getId() + " and commitHash " + commit.commitHash() + " already exists!"); } programmingSubmission = new ProgrammingSubmission(); - programmingSubmission.setCommitHash(commit.getCommitHash()); - log.info("Create new programmingSubmission with commitHash: {} for participation {}", commit.getCommitHash(), participation.getId()); + programmingSubmission.setCommitHash(commit.commitHash()); + log.info("Create new programmingSubmission with commitHash: {} for participation {}", commit.commitHash(), participation.getId()); programmingSubmission.setSubmitted(true); programmingSubmission.setSubmissionDate(submissionDate); @@ -359,7 +357,7 @@ public ProgrammingSubmission getOrCreateSubmissionWithLastCommitHashForParticipa private String getLastCommitHashForParticipation(ProgrammingExerciseParticipation participation) throws IllegalStateException { try { - return gitService.getLastCommitHash(participation.getVcsRepositoryUrl()).getName(); + return gitService.getLastCommitHash(participation.getVcsRepositoryUri()).getName(); } catch (EntityNotFoundException ex) { var message = "Last commit hash for participation " + participation.getId() + " could not be retrieved due to exception: " + ex.getMessage(); @@ -384,7 +382,7 @@ public ProgrammingSubmission createSolutionParticipationSubmissionWithTypeTest(L if (commitHash == null) { ProgrammingExercise programmingExercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationElseThrow(programmingExerciseId); try { - commitHash = gitService.getLastCommitHash(programmingExercise.getVcsTestRepositoryUrl()).getName(); + commitHash = gitService.getLastCommitHash(programmingExercise.getVcsTestRepositoryUri()).getName(); } catch (EntityNotFoundException ex) { throw new IllegalStateException("Last commit hash for test repository of programming exercise with id " + programmingExercise.getId() + " could not be retrieved"); @@ -625,7 +623,7 @@ public void createInitialSubmissions(ProgrammingExercise programmingExercise) { */ private void createInitialSubmission(ProgrammingExercise programmingExercise, AbstractBaseProgrammingExerciseParticipation participation) { ProgrammingSubmission submission = (ProgrammingSubmission) submissionRepository.initializeSubmission(participation, programmingExercise, SubmissionType.INSTRUCTOR); - var latestHash = gitService.getLastCommitHash(participation.getVcsRepositoryUrl()); + var latestHash = gitService.getLastCommitHash(participation.getVcsRepositoryUri()); submission.setCommitHash(latestHash.getName()); submission.setSubmissionDate(ZonedDateTime.now()); submissionRepository.save(submission); diff --git a/src/main/java/de/tum/in/www1/artemis/service/scheduled/AutomaticProgrammingExerciseCleanupService.java b/src/main/java/de/tum/in/www1/artemis/service/scheduled/AutomaticProgrammingExerciseCleanupService.java index 8cde9c4f1daf..3e1ae98b682e 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/scheduled/AutomaticProgrammingExerciseCleanupService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/scheduled/AutomaticProgrammingExerciseCleanupService.java @@ -105,7 +105,7 @@ public void cleanupGitRepositoriesOnArtemisServer() { for (var programmingExercise : programmingExercises) { for (var studentParticipation : programmingExercise.getStudentParticipations()) { var programmingExerciseParticipation = (ProgrammingExerciseStudentParticipation) studentParticipation; - gitService.deleteLocalRepository(programmingExerciseParticipation.getVcsRepositoryUrl()); + gitService.deleteLocalRepository(programmingExerciseParticipation.getVcsRepositoryUri()); } } @@ -116,9 +116,9 @@ public void cleanupGitRepositoriesOnArtemisServer() { log.info("Found {} programming exercise to clean local template, test and solution: {}", programmingExercises.size(), programmingExercises.stream().map(ProgrammingExercise::getProjectKey).collect(Collectors.joining(", "))); for (var programmingExercise : programmingExercises) { - gitService.deleteLocalRepository(programmingExercise.getVcsTemplateRepositoryUrl()); - gitService.deleteLocalRepository(programmingExercise.getVcsSolutionRepositoryUrl()); - gitService.deleteLocalRepository(programmingExercise.getVcsTestRepositoryUrl()); + gitService.deleteLocalRepository(programmingExercise.getVcsTemplateRepositoryUri()); + gitService.deleteLocalRepository(programmingExercise.getVcsSolutionRepositoryUri()); + gitService.deleteLocalRepository(programmingExercise.getVcsTestRepositoryUri()); gitService.deleteLocalProgrammingExerciseReposFolder(programmingExercise); } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/scheduled/ProgrammingExerciseScheduleService.java b/src/main/java/de/tum/in/www1/artemis/service/scheduled/ProgrammingExerciseScheduleService.java index 89b9dbeea633..a5fbd555dfd4 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/scheduled/ProgrammingExerciseScheduleService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/scheduled/ProgrammingExerciseScheduleService.java @@ -421,7 +421,7 @@ private Runnable combineTemplateCommitsForExercise(ProgrammingExercise exercise) try { ProgrammingExercise programmingExerciseWithTemplateParticipation = programmingExerciseRepository .findByIdWithTemplateAndSolutionParticipationElseThrow(exercise.getId()); - gitService.combineAllCommitsOfRepositoryIntoOne(programmingExerciseWithTemplateParticipation.getTemplateParticipation().getVcsRepositoryUrl()); + gitService.combineAllCommitsOfRepositoryIntoOne(programmingExerciseWithTemplateParticipation.getTemplateParticipation().getVcsRepositoryUri()); log.debug("Combined template repository commits of programming exercise {}.", programmingExerciseWithTemplateParticipation.getId()); } catch (GitAPIException e) { diff --git a/src/main/java/de/tum/in/www1/artemis/service/util/UrlUtils.java b/src/main/java/de/tum/in/www1/artemis/service/util/UrlUtils.java index 0671fac0ac80..7076e08d4bc6 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/util/UrlUtils.java +++ b/src/main/java/de/tum/in/www1/artemis/service/util/UrlUtils.java @@ -42,6 +42,6 @@ public static UriComponentsBuilder buildEndpoint(String baseUrl, List pa throw new IllegalArgumentException("Unable to build endpoint. Too many arguments! " + Arrays.toString(args)); } - return UriComponentsBuilder.fromHttpUrl(baseUrl).pathSegment(parsedSegments.toArray(new String[0])); + return UriComponentsBuilder.fromHttpUrl(baseUrl).pathSegment(parsedSegments.toArray(String[]::new)); } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ComplaintResponseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ComplaintResponseResource.java index 3e052bd6716c..36b9bca73f1d 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ComplaintResponseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ComplaintResponseResource.java @@ -1,7 +1,5 @@ package de.tum.in.www1.artemis.web.rest; -import java.security.Principal; -import java.util.Objects; import java.util.Optional; import org.slf4j.Logger; @@ -11,18 +9,11 @@ import org.springframework.web.bind.annotation.*; import de.tum.in.www1.artemis.domain.*; -import de.tum.in.www1.artemis.domain.participation.Participant; -import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.repository.ComplaintRepository; -import de.tum.in.www1.artemis.repository.ComplaintResponseRepository; import de.tum.in.www1.artemis.repository.UserRepository; -import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastStudent; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastTutor; -import de.tum.in.www1.artemis.service.AuthorizationCheckService; import de.tum.in.www1.artemis.service.ComplaintResponseService; import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException; -import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; -import tech.jhipster.web.util.ResponseUtil; /** * REST controller for managing complaints. @@ -35,22 +26,15 @@ public class ComplaintResponseResource { public static final String ENTITY_NAME = "complaintResponse"; - private final ComplaintResponseRepository complaintResponseRepository; - private final ComplaintRepository complaintRepository; private final ComplaintResponseService complaintResponseService; - private final AuthorizationCheckService authorizationCheckService; - private final UserRepository userRepository; - public ComplaintResponseResource(ComplaintResponseRepository complaintResponseRepository, ComplaintResponseService complaintResponseService, - AuthorizationCheckService authorizationCheckService, UserRepository userRepository, ComplaintRepository complaintRepository) { - this.complaintResponseRepository = complaintResponseRepository; + public ComplaintResponseResource(ComplaintResponseService complaintResponseService, UserRepository userRepository, ComplaintRepository complaintRepository) { this.complaintResponseService = complaintResponseService; this.complaintRepository = complaintRepository; - this.authorizationCheckService = authorizationCheckService; this.userRepository = userRepository; } @@ -121,69 +105,6 @@ public ResponseEntity resolveComplaint(@RequestBody Complaint return ResponseEntity.ok().body(updatedComplaintResponse); } - /** - * Get /complaint-responses/complaint/:id get a complaint response associated with the complaint "id" - * - * @param complaintId the id of the complaint for which we want to find a linked response - * @param principal the user who called the method - * @return the ResponseEntity with status 200 (OK) and with body the complaint response, or with status 404 (Not Found) - */ - // TODO: change URL to /complaint-responses?complaintId={complaintId} - @GetMapping("/complaint-responses/complaint/{complaintId}") - @EnforceAtLeastStudent - public ResponseEntity getComplaintResponseByComplaintId(@PathVariable long complaintId, Principal principal) { - log.debug("REST request to get ComplaintResponse associated to complaint : {}", complaintId); - Optional complaintResponse = complaintResponseRepository.findByComplaint_Id(complaintId); - return handleComplaintResponse(complaintId, principal, complaintResponse); - } - - private ResponseEntity handleComplaintResponse(long complaintId, Principal principal, Optional optionalComplaintResponse) { - if (optionalComplaintResponse.isEmpty()) { - throw new EntityNotFoundException("ComplaintResponse with " + complaintId + " was not found!"); - } - var user = userRepository.getUserWithGroupsAndAuthorities(); - var complaintResponse = optionalComplaintResponse.get(); - // All tutors and higher can see this, and also the students who first open the complaint - Participant originalAuthor = complaintResponse.getComplaint().getParticipant(); - StudentParticipation studentParticipation = (StudentParticipation) complaintResponse.getComplaint().getResult().getParticipation(); - Exercise exercise = studentParticipation.getExercise(); - var atLeastTA = authorizationCheckService.isAtLeastTeachingAssistantForExercise(exercise, user); - if (!atLeastTA && !isOriginalAuthor(principal, originalAuthor)) { - throw new AccessForbiddenException("Insufficient permission for this complaint response"); - } - - if (!authorizationCheckService.isAtLeastInstructorForExercise(exercise, user)) { - complaintResponse.getComplaint().setParticipant(null); - } - - if (!atLeastTA) { - complaintResponse.setReviewer(null); - } - - if (isOriginalAuthor(principal, originalAuthor)) { - // hide complaint completely if the user is the student who created the complaint - complaintResponse.setComplaint(null); - } - else { - // hide unnecessary information - complaintResponse.getComplaint().getResult().setParticipation(null); - complaintResponse.getComplaint().getResult().setSubmission(null); - } - return ResponseUtil.wrapOrNotFound(optionalComplaintResponse); - } - - private boolean isOriginalAuthor(Principal principal, Participant originalAuthor) { - if (originalAuthor instanceof User) { - return Objects.equals(((User) originalAuthor).getLogin(), principal.getName()); - } - else if (originalAuthor instanceof Team) { - return ((Team) originalAuthor).hasStudentWithLogin(principal.getName()); - } - else { - throw new Error("Unknown Participant type"); - } - } - private Complaint getComplaintFromDatabaseAndCheckAccessRights(long complaintId) { Optional complaintFromDatabaseOptional = complaintRepository.findByIdWithEagerAssessor(complaintId); if (complaintFromDatabaseOptional.isEmpty()) { diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ExamResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ExamResource.java index 617fbf7dd10a..904346a7716b 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ExamResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ExamResource.java @@ -115,12 +115,15 @@ public class ExamResource { private final ExamLiveEventsService examLiveEventsService; + private final StudentExamService studentExamService; + public ExamResource(ProfileService profileService, UserRepository userRepository, CourseRepository courseRepository, ExamService examService, ExamDeletionService examDeletionService, ExamAccessService examAccessService, InstanceMessageSendService instanceMessageSendService, ExamRepository examRepository, SubmissionService submissionService, AuthorizationCheckService authCheckService, ExamDateService examDateService, TutorParticipationRepository tutorParticipationRepository, AssessmentDashboardService assessmentDashboardService, ExamRegistrationService examRegistrationService, StudentExamRepository studentExamRepository, ExamImportService examImportService, CustomAuditEventRepository auditEventRepository, ChannelService channelService, - ChannelRepository channelRepository, ExerciseRepository exerciseRepository, ExamSessionService examSessionRepository, ExamLiveEventsService examLiveEventsService) { + ChannelRepository channelRepository, ExerciseRepository exerciseRepository, ExamSessionService examSessionRepository, ExamLiveEventsService examLiveEventsService, + StudentExamService studentExamService) { this.profileService = profileService; this.userRepository = userRepository; this.courseRepository = courseRepository; @@ -143,6 +146,7 @@ public ExamResource(ProfileService profileService, UserRepository userRepository this.exerciseRepository = exerciseRepository; this.examSessionService = examSessionRepository; this.examLiveEventsService = examLiveEventsService; + this.studentExamService = studentExamService; } /** @@ -867,7 +871,7 @@ public ResponseEntity> generateStudentExams(@PathVariable Long // Reset existing student exams & participations in case they already exist examDeletionService.deleteStudentExamsAndExistingParticipationsForExam(exam.getId()); - List studentExams = studentExamRepository.generateStudentExams(exam); + List studentExams = studentExamService.generateStudentExams(exam); // we need to break a cycle for the serialization breakCyclesForSerialization(studentExams); @@ -911,7 +915,7 @@ public ResponseEntity> generateMissingStudentExams(@PathVariab log.info("REST request to generate missing student exams for exam {}", examId); final var exam = checkAccessForStudentExamGenerationAndLogAuditEvent(courseId, examId, Constants.GENERATE_MISSING_STUDENT_EXAMS); - List studentExams = studentExamRepository.generateMissingStudentExams(exam); + List studentExams = studentExamService.generateMissingStudentExams(exam); // we need to break a cycle for the serialization breakCyclesForSerialization(studentExams); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LectureResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LectureResource.java index 2a43ac61cba5..a03ab28511fb 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/LectureResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LectureResource.java @@ -122,7 +122,7 @@ public ResponseEntity updateLecture(@RequestBody Lecture lecture) { authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, lecture.getCourse(), null); // Make sure that the original references are preserved. - Lecture originalLecture = lectureRepository.findByIdWithLectureUnitsElseThrow(lecture.getId()); + Lecture originalLecture = lectureRepository.findByIdWithLectureUnitsAndAttachmentsElseThrow(lecture.getId()); // NOTE: Make sure that all references are preserved here lecture.setLectureUnits(originalLecture.getLectureUnits()); @@ -228,7 +228,7 @@ public ResponseEntity getLecture(@PathVariable Long lectureId) { @EnforceAtLeastEditor public ResponseEntity importLecture(@PathVariable long sourceLectureId, @RequestParam long courseId) throws URISyntaxException { final var user = userRepository.getUserWithGroupsAndAuthorities(); - final var sourceLecture = lectureRepository.findByIdWithLectureUnitsElseThrow(sourceLectureId); + final var sourceLecture = lectureRepository.findByIdWithLectureUnitsAndAttachmentsElseThrow(sourceLectureId); final var destinationCourse = courseRepository.findByIdWithLecturesElseThrow(courseId); Course course = sourceLecture.getCourse(); @@ -256,7 +256,7 @@ public ResponseEntity importLecture(@PathVariable long sourceLectureId, @EnforceAtLeastStudent public ResponseEntity getLectureWithDetails(@PathVariable Long lectureId) { log.debug("REST request to get lecture {} with details", lectureId); - Lecture lecture = lectureRepository.findByIdWithPostsAndLectureUnitsAndCompetenciesAndCompletionsElseThrow(lectureId); + Lecture lecture = lectureRepository.findByIdWithAttachmentsAndPostsAndLectureUnitsAndCompetenciesAndCompletionsElseThrow(lectureId); Course course = lecture.getCourse(); if (course == null) { return ResponseEntity.badRequest().build(); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingSubmissionResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingSubmissionResource.java index 9620d8799343..3ee392a4b975 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingSubmissionResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingSubmissionResource.java @@ -187,7 +187,7 @@ public ResponseEntity getModelingSubmission(@PathVariable Lo if (!authCheckService.isAllowedToAssessExercise(modelingExercise, user, resultId)) { // anonymize and throw exception if not authorized to view submission - plagiarismService.checkAccessAndAnonymizeSubmissionForStudent(modelingSubmission, userRepository.getUser().getLogin()); + plagiarismService.checkAccessAndAnonymizeSubmissionForStudent(modelingSubmission, userRepository.getUser().getLogin(), modelingExercise.getDueDate()); return ResponseEntity.ok(modelingSubmission); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/NotificationSettingsResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/NotificationSettingsResource.java index ffc4e35171a8..293ecdaf7cdb 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/NotificationSettingsResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/NotificationSettingsResource.java @@ -90,7 +90,7 @@ public ResponseEntity saveNotificationSettingsForCurrentU if (resultAsList.isEmpty()) { throw new BadRequestAlertException("Error occurred during saving of Notification Settings", "NotificationSettings", "notificationSettingsEmptyAfterSave"); } - NotificationSetting[] resultAsArray = resultAsList.toArray(new NotificationSetting[0]); + NotificationSetting[] resultAsArray = resultAsList.toArray(NotificationSetting[]::new); return ResponseEntity.ok().headers(HeaderUtil.createEntityUpdateAlert(applicationName, true, "notificationSetting", "test")).body(resultAsArray); } @@ -101,7 +101,7 @@ public ResponseEntity saveNotificationSettingsForCurrentU */ @GetMapping("muted-conversations") @EnforceAtLeastStudent - public ResponseEntity> searchMembersOfConversation() { + public ResponseEntity> getMutedConversations() { User user = userRepository.getUser(); Set mutedConversations = notificationSettingRepository.findMutedConversations(user.getId()); return ResponseEntity.ok(mutedConversations); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ParticipationResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ParticipationResource.java index d0f41c78f81c..9d11fa47c33c 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ParticipationResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ParticipationResource.java @@ -599,7 +599,7 @@ public ResponseEntity> getAllParticipationsForCourse( if (exercise instanceof ProgrammingExercise programmingExercise) { programmingExercise.setSolutionParticipation(null); programmingExercise.setTemplateParticipation(null); - programmingExercise.setTestRepositoryUrl(null); + programmingExercise.setTestRepositoryUri(null); programmingExercise.setShortName(null); programmingExercise.setPublishBuildPlanUrl(null); programmingExercise.setProgrammingLanguage(null); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseExportImportResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseExportImportResource.java index ec3b44f36e89..26f06b0ff356 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseExportImportResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseExportImportResource.java @@ -387,7 +387,7 @@ public ResponseEntity exportSubmissionsByStudentLogins(@PathVariable l List exportedStudentParticipations = new ArrayList<>(); for (StudentParticipation studentParticipation : programmingExercise.getStudentParticipations()) { ProgrammingExerciseStudentParticipation programmingStudentParticipation = (ProgrammingExerciseStudentParticipation) studentParticipation; - if (repositoryExportOptions.isExportAllParticipants() || (programmingStudentParticipation.getRepositoryUrl() != null && studentParticipation.getParticipant() != null + if (repositoryExportOptions.isExportAllParticipants() || (programmingStudentParticipation.getRepositoryUri() != null && studentParticipation.getParticipant() != null && participantIdentifierList.contains(studentParticipation.getParticipantIdentifier()))) { exportedStudentParticipations.add(programmingStudentParticipation); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseParticipationResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseParticipationResource.java index ecc18403be55..43b5d3662e40 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseParticipationResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseParticipationResource.java @@ -14,7 +14,7 @@ import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.ProgrammingSubmission; import de.tum.in.www1.artemis.domain.Result; -import de.tum.in.www1.artemis.domain.VcsRepositoryUrl; +import de.tum.in.www1.artemis.domain.VcsRepositoryUri; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; import de.tum.in.www1.artemis.repository.ParticipationRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; @@ -201,18 +201,18 @@ public ResponseEntity resetRepository(@PathVariable Long participationId, throw new BadRequestAlertException("Cannot reset repository in an exam", ENTITY_NAME, "noRepoResetInExam"); } - VcsRepositoryUrl sourceURL; + VcsRepositoryUri sourceURL; if (gradedParticipationId != null) { ProgrammingExerciseStudentParticipation gradedParticipation = programmingExerciseStudentParticipationRepository.findByIdElseThrow(gradedParticipationId); participationAuthCheckService.checkCanAccessParticipationElseThrow(gradedParticipation); - sourceURL = gradedParticipation.getVcsRepositoryUrl(); + sourceURL = gradedParticipation.getVcsRepositoryUri(); } else { - sourceURL = exercise.getVcsTemplateRepositoryUrl(); + sourceURL = exercise.getVcsTemplateRepositoryUri(); } - programmingExerciseParticipationService.resetRepository(participation.getVcsRepositoryUrl(), sourceURL); + programmingExerciseParticipationService.resetRepository(participation.getVcsRepositoryUri(), sourceURL); return ResponseEntity.ok().build(); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseResource.java index 47ecb3699653..7aec667f119c 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseResource.java @@ -159,16 +159,16 @@ private void checkProgrammingExerciseForError(ProgrammingExercise exercise) { if (!continuousIntegration.checkIfBuildPlanExists(exercise.getProjectKey(), exercise.getTemplateBuildPlanId())) { throw new BadRequestAlertException("The Template Build Plan ID seems to be invalid.", "Exercise", ProgrammingExerciseResourceErrorKeys.INVALID_TEMPLATE_BUILD_PLAN_ID); } - if (exercise.getVcsTemplateRepositoryUrl() == null || !versionControl.repositoryUrlIsValid(exercise.getVcsTemplateRepositoryUrl())) { - throw new BadRequestAlertException("The Template Repository URL seems to be invalid.", "Exercise", + if (exercise.getVcsTemplateRepositoryUri() == null || !versionControl.repositoryUriIsValid(exercise.getVcsTemplateRepositoryUri())) { + throw new BadRequestAlertException("The Template Repository URI seems to be invalid.", "Exercise", ProgrammingExerciseResourceErrorKeys.INVALID_TEMPLATE_REPOSITORY_URL); } if (exercise.getSolutionBuildPlanId() != null && !continuousIntegration.checkIfBuildPlanExists(exercise.getProjectKey(), exercise.getSolutionBuildPlanId())) { throw new BadRequestAlertException("The Solution Build Plan ID seems to be invalid.", "Exercise", ProgrammingExerciseResourceErrorKeys.INVALID_SOLUTION_BUILD_PLAN_ID); } - var solutionRepositoryUrl = exercise.getVcsSolutionRepositoryUrl(); - if (solutionRepositoryUrl != null && !versionControl.repositoryUrlIsValid(solutionRepositoryUrl)) { - throw new BadRequestAlertException("The Solution Repository URL seems to be invalid.", "Exercise", + var solutionRepositoryUri = exercise.getVcsSolutionRepositoryUri(); + if (solutionRepositoryUri != null && !versionControl.repositoryUriIsValid(solutionRepositoryUri)) { + throw new BadRequestAlertException("The Solution Repository URI seems to be invalid.", "Exercise", ProgrammingExerciseResourceErrorKeys.INVALID_SOLUTION_REPOSITORY_URL); } @@ -501,8 +501,8 @@ public ResponseEntity combineTemplateRepositoryCommits(@PathVariable long var programmingExercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesElseThrow(exerciseId); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.EDITOR, programmingExercise, null); try { - var exerciseRepoURL = programmingExercise.getVcsTemplateRepositoryUrl(); - gitService.combineAllCommitsOfRepositoryIntoOne(exerciseRepoURL); + var exerciseRepoUri = programmingExercise.getVcsTemplateRepositoryUri(); + gitService.combineAllCommitsOfRepositoryIntoOne(exerciseRepoUri); return new ResponseEntity<>(HttpStatus.OK); } catch (IllegalStateException | GitAPIException ex) { @@ -529,15 +529,15 @@ public ResponseEntity generateStructureOracleForExercise(@PathVariable l "This is a linked exercise and generating the structure oracle for this exercise is not possible.", "couldNotGenerateStructureOracle")).body(null); } - var solutionRepoURL = programmingExercise.getVcsSolutionRepositoryUrl(); - var exerciseRepoURL = programmingExercise.getVcsTemplateRepositoryUrl(); - var testRepoURL = programmingExercise.getVcsTestRepositoryUrl(); + var solutionRepoUri = programmingExercise.getVcsSolutionRepositoryUri(); + var exerciseRepoUri = programmingExercise.getVcsTemplateRepositoryUri(); + var testRepoUri = programmingExercise.getVcsTestRepositoryUri(); try { String testsPath = Path.of("test", programmingExercise.getPackageFolderName()).toString(); // Atm we only have one folder that can have structural tests, but this could change. testsPath = programmingExercise.hasSequentialTestRuns() ? Path.of("structural", testsPath).toString() : testsPath; - boolean didGenerateOracle = programmingExerciseService.generateStructureOracleFile(solutionRepoURL, exerciseRepoURL, testRepoURL, testsPath, user); + boolean didGenerateOracle = programmingExerciseService.generateStructureOracleFile(solutionRepoUri, exerciseRepoUri, testRepoUri, testsPath, user); if (didGenerateOracle) { HttpHeaders responseHeaders = new HttpHeaders(); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/QuizPoolResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/QuizPoolResource.java index 544f5da0b592..84481c51d464 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/QuizPoolResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/QuizPoolResource.java @@ -82,7 +82,7 @@ public ResponseEntity getQuizPool(@PathVariable Long courseId, @PathVa log.info("REST request to get QuizPool given examId : {}", examId); validateCourseRole(courseId); - QuizPool quizPool = quizPoolService.findWithQuizQuestionsByExamId(examId).orElse(null); + QuizPool quizPool = quizPoolService.findWithQuizGroupsAndQuestionsByExamId(examId).orElse(null); return ResponseEntity.ok().body(quizPool); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java index bf38d19cca4c..32d7bc9b7477 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java @@ -537,12 +537,8 @@ public ResponseEntity getStudentExamGradesForSummary(@P if (!isAtLeastInstructor && !currentUser.getId().equals(targetUser.getId())) { throw new AccessForbiddenException("Current user cannot access grade info for target user"); } - boolean nonInstructorSetsTestRunToTrue = !isAtLeastInstructor && isTestRun; - if (nonInstructorSetsTestRunToTrue) { - throw new AccessForbiddenException("Test runs are only accessible for instructors"); - } - StudentExamWithGradeDTO studentExamWithGradeDTO = examService.getStudentExamGradesForSummaryAsStudent(targetUser, studentExam, isTestRun); + StudentExamWithGradeDTO studentExamWithGradeDTO = examService.getStudentExamGradesForSummary(targetUser, studentExam, isAtLeastInstructor); log.info("getStudentExamGradesForSummary done in {}ms for {} exercises for target user {} by caller user {}", System.currentTimeMillis() - start, studentExam.getExercises().size(), targetUser.getLogin(), currentUser.getLogin()); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/TextSubmissionResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/TextSubmissionResource.java index e5657e4b519f..372f310a84ba 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/TextSubmissionResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/TextSubmissionResource.java @@ -147,7 +147,8 @@ public ResponseEntity getTextSubmissionWithResults(@PathVariable if (!authCheckService.isAtLeastTeachingAssistantForExercise(textSubmission.getParticipation().getExercise())) { // anonymize and throw exception if not authorized to view submission - plagiarismService.checkAccessAndAnonymizeSubmissionForStudent(textSubmission, userRepository.getUser().getLogin()); + plagiarismService.checkAccessAndAnonymizeSubmissionForStudent(textSubmission, userRepository.getUser().getLogin(), + textSubmission.getParticipation().getExercise().getDueDate()); return ResponseEntity.ok(textSubmission); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/AttachmentUnitResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/AttachmentUnitResource.java index d9a23c2cf7f4..a076a72741d2 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/AttachmentUnitResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/AttachmentUnitResource.java @@ -146,7 +146,7 @@ public ResponseEntity createAttachmentUnit(@PathVariable Long le throw new BadRequestAlertException("A new attachment cannot already have an ID", ENTITY_NAME, "idexists"); } - Lecture lecture = lectureRepository.findByIdWithLectureUnitsElseThrow(lectureId); + Lecture lecture = lectureRepository.findByIdWithLectureUnitsAndAttachmentsElseThrow(lectureId); if (lecture.getCourse() == null) { throw new ConflictException("Specified lecture is not part of a course", "AttachmentUnit", "courseMissing"); } @@ -211,7 +211,7 @@ public ResponseEntity> createAttachmentUnits(@PathVariable try { byte[] fileBytes = fileService.getFileForPath(filePath); List savedAttachmentUnits = lectureUnitProcessingService.splitAndSaveUnits(lectureUnitInformationDTO, fileBytes, - lectureRepository.findByIdWithLectureUnitsElseThrow(lectureId)); + lectureRepository.findByIdWithLectureUnitsAndAttachmentsElseThrow(lectureId)); savedAttachmentUnits.forEach(attachmentUnitService::prepareAttachmentUnitForClient); savedAttachmentUnits.forEach(competencyProgressService::updateProgressByLearningObjectAsync); return ResponseEntity.ok().body(savedAttachmentUnits); @@ -297,7 +297,7 @@ private void checkAttachmentUnitCourseAndLecture(AttachmentUnit attachmentUnit, * @param lectureId The id of the lecture */ private void checkLecture(Long lectureId) { - Lecture lecture = lectureRepository.findByIdWithLectureUnitsElseThrow(lectureId); + Lecture lecture = lectureRepository.findByIdWithLectureUnitsAndAttachmentsElseThrow(lectureId); if (lecture.getCourse() == null) { throw new ConflictException("Specified lecture is not part of a course", "AttachmentUnit", "courseMissing"); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/ExerciseUnitResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/ExerciseUnitResource.java index dd28d692e564..e854bbe7a0b5 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/ExerciseUnitResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/ExerciseUnitResource.java @@ -59,7 +59,7 @@ public ResponseEntity createExerciseUnit(@PathVariable Long lectur if (exerciseUnit.getId() != null) { throw new BadRequestException(); } - Lecture lecture = lectureRepository.findByIdWithLectureUnitsElseThrow(lectureId); + Lecture lecture = lectureRepository.findByIdWithLectureUnitsAndAttachmentsElseThrow(lectureId); if (lecture.getCourse() == null) { throw new ConflictException("Specified lecture is not part of a course", "ExerciseUnit", "courseMissing"); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/LectureUnitResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/LectureUnitResource.java index 57fbd7abb92d..756d1d6646a5 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/LectureUnitResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/LectureUnitResource.java @@ -71,7 +71,7 @@ public LectureUnitResource(AuthorizationCheckService authorizationCheckService, @EnforceAtLeastEditor public ResponseEntity> updateLectureUnitsOrder(@PathVariable Long lectureId, @RequestBody List orderedLectureUnitIds) { log.debug("REST request to update the order of lecture units of lecture: {}", lectureId); - final Lecture lecture = lectureRepository.findByIdWithLectureUnitsElseThrow(lectureId); + final Lecture lecture = lectureRepository.findByIdWithLectureUnitsAndAttachmentsElseThrow(lectureId); if (lecture.getCourse() == null) { throw new ConflictException("Specified lecture is not part of a course", "LectureUnit", "courseMissing"); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/OnlineUnitResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/OnlineUnitResource.java index 68daa47b01e7..78b8abcaaf3f 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/OnlineUnitResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/OnlineUnitResource.java @@ -112,7 +112,7 @@ public ResponseEntity createOnlineUnit(@PathVariable Long lectureId, validateUrl(onlineUnit); - Lecture lecture = lectureRepository.findByIdWithLectureUnitsElseThrow(lectureId); + Lecture lecture = lectureRepository.findByIdWithLectureUnitsAndAttachmentsElseThrow(lectureId); if (lecture.getCourse() == null) { throw new ConflictException("Specified lecture is not part of a course", "onlineUnit", "courseMissing"); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/TextUnitResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/TextUnitResource.java index c33105f06780..e61fca37614f 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/TextUnitResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/TextUnitResource.java @@ -119,7 +119,7 @@ public ResponseEntity createTextUnit(@PathVariable Long lectureId, @Re throw new BadRequestAlertException("A new text unit cannot have an id", ENTITY_NAME, "idExists"); } - Lecture lecture = lectureRepository.findByIdWithLectureUnitsElseThrow(lectureId); + Lecture lecture = lectureRepository.findByIdWithLectureUnitsAndAttachmentsElseThrow(lectureId); if (lecture.getCourse() == null || (textUnit.getLecture() != null && !lecture.getId().equals(textUnit.getLecture().getId()))) { throw new ConflictException("Input data not valid", ENTITY_NAME, "inputInvalid"); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/VideoUnitResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/VideoUnitResource.java index 01d39701aaea..7b85b5f51dc5 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/VideoUnitResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/VideoUnitResource.java @@ -113,7 +113,7 @@ public ResponseEntity createVideoUnit(@PathVariable Long lectureId, @ normalizeVideoUrl(videoUnit); validateVideoUrl(videoUnit); - Lecture lecture = lectureRepository.findByIdWithLectureUnitsElseThrow(lectureId); + Lecture lecture = lectureRepository.findByIdWithLectureUnitsAndAttachmentsElseThrow(lectureId); if (lecture.getCourse() == null) { throw new ConflictException("Specified lecture is not part of a course", "VideoUnit", "courseMissing"); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/open/PublicProgrammingSubmissionResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/open/PublicProgrammingSubmissionResource.java index 43f47ed95b31..553bf0ece28b 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/open/PublicProgrammingSubmissionResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/open/PublicProgrammingSubmissionResource.java @@ -147,7 +147,7 @@ public ResponseEntity testCaseChanged(@PathVariable Long exerciseId, @Requ String lastCommitHash = null; try { Commit commit = versionControlService.orElseThrow().getLastCommitDetails(requestBody); - lastCommitHash = commit.getCommitHash(); + lastCommitHash = commit.commitHash(); log.info("create new programmingSubmission with commitHash: {} for exercise {}", lastCommitHash, exerciseId); } catch (Exception ex) { diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/repository/RepositoryProgrammingExerciseParticipationResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/repository/RepositoryProgrammingExerciseParticipationResource.java index 2ca65ee42a56..99285c5814e4 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/repository/RepositoryProgrammingExerciseParticipationResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/repository/RepositoryProgrammingExerciseParticipationResource.java @@ -99,22 +99,22 @@ Repository getRepository(Long participationId, RepositoryActionType repositoryAc throw new AccessForbiddenException(e); } - var repositoryUrl = programmingParticipation.getVcsRepositoryUrl(); + var repositoryUri = programmingParticipation.getVcsRepositoryUri(); // This check reduces the amount of REST-calls that retrieve the default branch of a repository. // Retrieving the default branch is not necessary if the repository is already cached. - if (gitService.isRepositoryCached(repositoryUrl)) { - return gitService.getOrCheckoutRepository(repositoryUrl, pullOnGet); + if (gitService.isRepositoryCached(repositoryUri)) { + return gitService.getOrCheckoutRepository(repositoryUri, pullOnGet); } else { String branch = versionControlService.orElseThrow().getOrRetrieveBranchOfParticipation(programmingParticipation); - return gitService.getOrCheckoutRepository(repositoryUrl, pullOnGet, branch); + return gitService.getOrCheckoutRepository(repositoryUri, pullOnGet, branch); } } @Override - VcsRepositoryUrl getRepositoryUrl(Long participationId) throws IllegalArgumentException { - return getProgrammingExerciseParticipation(participationId).getVcsRepositoryUrl(); + VcsRepositoryUri getRepositoryUri(Long participationId) throws IllegalArgumentException { + return getProgrammingExerciseParticipation(participationId).getVcsRepositoryUri(); } /** @@ -189,7 +189,7 @@ public ResponseEntity> getFilesAtCommit(@PathVariable long p throw new AccessForbiddenException(e); } return executeAndCheckForExceptions(() -> { - Repository repository = gitService.checkoutRepositoryAtCommit(getRepositoryUrl(participationId), commitId, true); + Repository repository = gitService.checkoutRepositoryAtCommit(getRepositoryUri(participationId), commitId, true); Map filesWithContent = super.repositoryService.getFilesWithContent(repository); gitService.switchBackToDefaultBranchHead(repository); return new ResponseEntity<>(filesWithContent, HttpStatus.OK); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/repository/RepositoryResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/repository/RepositoryResource.java index d11adb9f41bb..062fed7d098d 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/repository/RepositoryResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/repository/RepositoryResource.java @@ -99,9 +99,9 @@ public RepositoryResource(ProfileService profileService, UserRepository userRepo * Get the url for a repository. * * @param domainId that serves as an abstract identifier for retrieving the repository. - * @return the repositoryUrl. + * @return the repositoryUri. */ - abstract VcsRepositoryUrl getRepositoryUrl(Long domainId); + abstract VcsRepositoryUri getRepositoryUri(Long domainId); /** * Check if the current user can access the given repository. @@ -301,18 +301,18 @@ public ResponseEntity getStatus(Long domainId) throws GitAP } RepositoryStatusDTOType repositoryStatus; - VcsRepositoryUrl repositoryUrl = getRepositoryUrl(domainId); + VcsRepositoryUri repositoryUri = getRepositoryUri(domainId); try { boolean isClean; // This check reduces the amount of REST-calls that retrieve the default branch of a repository. // Retrieving the default branch is not necessary if the repository is already cached. - if (gitService.isRepositoryCached(repositoryUrl)) { - isClean = repositoryService.isClean(repositoryUrl); + if (gitService.isRepositoryCached(repositoryUri)) { + isClean = repositoryService.isClean(repositoryUri); } else { String branch = getOrRetrieveBranchOfDomainObject(domainId); - isClean = repositoryService.isClean(repositoryUrl, branch); + isClean = repositoryService.isClean(repositoryUri, branch); } repositoryStatus = isClean ? CLEAN : UNCOMMITTED_CHANGES; } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/repository/TestRepositoryResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/repository/TestRepositoryResource.java index bad42af629d5..2eea6eda53f4 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/repository/TestRepositoryResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/repository/TestRepositoryResource.java @@ -52,14 +52,14 @@ Repository getRepository(Long exerciseId, RepositoryActionType repositoryActionT final var exercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationElseThrow(exerciseId); User user = userRepository.getUserWithGroupsAndAuthorities(); repositoryAccessService.checkAccessTestOrAuxRepositoryElseThrow(false, exercise, user, "test"); - final var repoUrl = exercise.getVcsTestRepositoryUrl(); - return gitService.getOrCheckoutRepository(repoUrl, pullOnGet); + final var repoUri = exercise.getVcsTestRepositoryUri(); + return gitService.getOrCheckoutRepository(repoUri, pullOnGet); } @Override - VcsRepositoryUrl getRepositoryUrl(Long exerciseId) { + VcsRepositoryUri getRepositoryUri(Long exerciseId) { ProgrammingExercise exercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationElseThrow(exerciseId); - return exercise.getVcsTestRepositoryUrl(); + return exercise.getVcsTestRepositoryUri(); } @Override @@ -179,7 +179,7 @@ public ResponseEntity> updateTestFiles(@PathVariable("exerci Repository repository; try { repositoryAccessService.checkAccessTestOrAuxRepositoryElseThrow(true, exercise, userRepository.getUserWithGroupsAndAuthorities(principal.getName()), "test"); - repository = gitService.getOrCheckoutRepository(exercise.getVcsTestRepositoryUrl(), true); + repository = gitService.getOrCheckoutRepository(exercise.getVcsTestRepositoryUri(), true); } catch (AccessForbiddenException e) { FileSubmissionError error = new FileSubmissionError(exerciseId, "noPermissions"); diff --git a/src/main/java/de/tum/in/www1/artemis/web/websocket/localci/LocalCIBuildQueueWebsocketService.java b/src/main/java/de/tum/in/www1/artemis/web/websocket/localci/LocalCIBuildQueueWebsocketService.java new file mode 100644 index 000000000000..1694ba02110c --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/web/websocket/localci/LocalCIBuildQueueWebsocketService.java @@ -0,0 +1,129 @@ +package de.tum.in.www1.artemis.web.websocket.localci; + +import java.util.List; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import de.tum.in.www1.artemis.service.WebsocketMessagingService; +import de.tum.in.www1.artemis.service.connectors.localci.dto.LocalCIBuildAgentInformation; +import de.tum.in.www1.artemis.service.connectors.localci.dto.LocalCIBuildJobQueueItem; + +/** + * This service sends out websocket messages for the local continuous integration system. + * It is used to send queued and running build jobs to the client. + * It is also used to send build agent information to the client. + */ +@Service +@Profile("localci") +public class LocalCIBuildQueueWebsocketService { + + private final Logger log = LoggerFactory.getLogger(LocalCIBuildQueueWebsocketService.class); + + private final WebsocketMessagingService websocketMessagingService; + + private static final Pattern COURSE_DESTINATION_PATTERN = Pattern.compile("^/topic/courses/(\\d+)/(queued-jobs|running-jobs)$"); + + /** + * Constructor for dependency injection + * + * @param websocketMessagingService the websocket messaging service + */ + public LocalCIBuildQueueWebsocketService(WebsocketMessagingService websocketMessagingService) { + this.websocketMessagingService = websocketMessagingService; + } + + /** + * Sends the queued build jobs for the given course over websocket. + * + * @param courseId the id of the course for which to send the queued build jobs + * @param buildJobQueue the queued build jobs + */ + + public void sendQueuedBuildJobsForCourse(long courseId, List buildJobQueue) { + String channel = "/topic/courses/" + courseId + "/queued-jobs"; + log.debug("Sending message on topic {}: {}", channel, buildJobQueue); + websocketMessagingService.sendMessage(channel, buildJobQueue); + } + + /** + * Sends the running build jobs for the given course over websocket. + * + * @param courseId the id of the course for which to send the running build jobs + * @param buildJobsRunning the running build jobs + */ + public void sendRunningBuildJobsForCourse(long courseId, List buildJobsRunning) { + String channel = "/topic/courses/" + courseId + "/running-jobs"; + log.debug("Sending message on topic {}: {}", channel, buildJobsRunning); + websocketMessagingService.sendMessage(channel, buildJobsRunning); + } + + /** + * Sends the queued build jobs over websocket. This is only allowed for admins. + * + * @param buildJobQueue the queued build jobs + */ + public void sendQueuedBuildJobs(List buildJobQueue) { + String channel = "/topic/admin/queued-jobs"; + log.debug("Sending message on topic {}: {}", channel, buildJobQueue); + websocketMessagingService.sendMessage(channel, buildJobQueue); + } + + /** + * Sends the running build jobs over websocket. This is only allowed for admins. + * + * @param buildJobQueue the running build jobs + */ + public void sendRunningBuildJobs(List buildJobQueue) { + String channel = "/topic/admin/running-jobs"; + log.debug("Sending message on topic {}: {}", channel, buildJobQueue); + websocketMessagingService.sendMessage(channel, buildJobQueue); + } + + /** + * Sends the build agent information over websocket. This is only allowed for admins. + * + * @param buildAgentInfo the build agent information + */ + public void sendBuildAgentInformation(List buildAgentInfo) { + String channel = "/topic/admin/build-agents"; + log.debug("Sending message on topic {}: {}", channel, buildAgentInfo); + websocketMessagingService.sendMessage(channel, buildAgentInfo); + } + + /** + * Checks if the given destination is a build queue admin destination. + * This is the case if the destination is either /topic/admin/queued-jobs or /topic/admin/running-jobs. + * + * @param destination the destination to check + * @return true if the destination is a build queue admin destination, false otherwise + */ + public static boolean isBuildQueueAdminDestination(String destination) { + return "/topic/admin/queued-jobs".equals(destination) || "/topic/admin/running-jobs".equals(destination); + } + + /** + * Checks if the given destination is a build queue course destination. This is the case if the destination is either + * /topic/courses/{courseId}/queued-jobs or /topic/courses/{courseId}/running-jobs. + * If the destination is a build queue course destination, the courseId is returned. + * + * @param destination the destination to check + * @return the courseId if the destination is a build queue course destination, empty otherwise + */ + public static Optional isBuildQueueCourseDestination(String destination) { + // Define a pattern to match the expected course-related topic format + Matcher matcher = COURSE_DESTINATION_PATTERN.matcher(destination); + + // Check if the destination matches the pattern + if (matcher.matches()) { + // Extract the courseId from the matched groups + return Optional.of(Long.parseLong(matcher.group(1))); + } + return Optional.empty(); + } +} diff --git a/src/main/resources/config/application-localci.yml b/src/main/resources/config/application-localci.yml index 90ca419e63f6..5c631e0d58e1 100644 --- a/src/main/resources/config/application-localci.yml +++ b/src/main/resources/config/application-localci.yml @@ -19,6 +19,8 @@ artemis: queue-size-limit: 100 # The path to the local CI build scripts. This path is relative to the Artemis root directory. local-cis-build-scripts-path: local-ci-scripts + # The prefix that is used for the Docker containers that are created by the local CI system. + build-container-prefix: local-ci- # In case you need to use a proxy to access the internet from the Docker container (e.g., due to firewall constraints), set use-system-proxy to true and configure the proxy settings below. proxies: use-system-proxy: false diff --git a/src/main/resources/config/liquibase/changelog/20231211112800_changelog.xml b/src/main/resources/config/liquibase/changelog/20231211112800_changelog.xml new file mode 100644 index 000000000000..3cf081d7bd54 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20231211112800_changelog.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index 99142f54d90e..e025c6df88c3 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -74,6 +74,7 @@ + diff --git a/src/main/webapp/app/assessment/assessment-instructions/assessment-instructions/assessment-instructions.component.html b/src/main/webapp/app/assessment/assessment-instructions/assessment-instructions/assessment-instructions.component.html index 568c7aa97858..71388f4275f6 100644 --- a/src/main/webapp/app/assessment/assessment-instructions/assessment-instructions/assessment-instructions.component.html +++ b/src/main/webapp/app/assessment/assessment-instructions/assessment-instructions/assessment-instructions.component.html @@ -27,7 +27,7 @@

{{ exercise.title }}

@switch (exercise.type) { @case (ExerciseType.PROGRAMMING) { diff --git a/src/main/webapp/app/complaints/complaint-response.service.ts b/src/main/webapp/app/complaints/complaint-response.service.ts index 1617793ac53b..e924737bd3ac 100644 --- a/src/main/webapp/app/complaints/complaint-response.service.ts +++ b/src/main/webapp/app/complaints/complaint-response.service.ts @@ -73,12 +73,6 @@ export class ComplaintResponseService { .pipe(map((res: EntityResponseType) => this.convertComplaintResponseEntityResponseDatesFromServer(res))); } - findByComplaintId(complaintId: number): Observable { - return this.http - .get(`${this.resourceUrl}/complaint/${complaintId}`, { observe: 'response' }) - .pipe(map((res: EntityResponseType) => this.convertComplaintResponseEntityResponseDatesFromServer(res))); - } - public convertComplaintResponseDatesFromClient(complaintResponse: ComplaintResponse): ComplaintResponse { return Object.assign({}, complaintResponse, { submittedTime: convertDateFromClient(complaintResponse.submittedTime), diff --git a/src/main/webapp/app/course/plagiarism-cases/shared/review/plagiarism-case-review.component.html b/src/main/webapp/app/course/plagiarism-cases/shared/review/plagiarism-case-review.component.html index 6ef732f504c4..dad09c520113 100644 --- a/src/main/webapp/app/course/plagiarism-cases/shared/review/plagiarism-case-review.component.html +++ b/src/main/webapp/app/course/plagiarism-cases/shared/review/plagiarism-case-review.component.html @@ -12,6 +12,7 @@ [exercise]="plagiarismCase.exercise!" [sortByStudentLogin]="forStudent ? 'Your submission' : plagiarismCase.student!.login!" [splitControlSubject]="splitControlSubject" + [forStudent]="forStudent" > diff --git a/src/main/webapp/app/course/plagiarism-cases/student-view/detail-view/plagiarism-case-student-detail-view.component.html b/src/main/webapp/app/course/plagiarism-cases/student-view/detail-view/plagiarism-case-student-detail-view.component.html index 095ec0130e45..62379b945c23 100644 --- a/src/main/webapp/app/course/plagiarism-cases/student-view/detail-view/plagiarism-case-student-detail-view.component.html +++ b/src/main/webapp/app/course/plagiarism-cases/student-view/detail-view/plagiarism-case-student-detail-view.component.html @@ -84,7 +84,7 @@

{{ 'artemisApp.plagiarism.plagiarismCases.conversation' | artemisTranslate }

{{ 'artemisApp.plagiarism.plagiarismCases.comparisons' | artemisTranslate }}

- +
diff --git a/src/main/webapp/app/entities/participation/programming-exercise-student-participation.model.ts b/src/main/webapp/app/entities/participation/programming-exercise-student-participation.model.ts index 7e9d6b61049e..6a6700137ece 100644 --- a/src/main/webapp/app/entities/participation/programming-exercise-student-participation.model.ts +++ b/src/main/webapp/app/entities/participation/programming-exercise-student-participation.model.ts @@ -2,14 +2,14 @@ import { StudentParticipation } from 'app/entities/participation/student-partici import { ParticipationType } from 'app/entities/participation/participation.model'; export class ProgrammingExerciseStudentParticipation extends StudentParticipation { - public repositoryUrl?: string; + public repositoryUri?: string; public buildPlanId?: string; public branch?: string; public locked?: boolean; // helper attribute public buildPlanUrl?: string; - public userIndependentRepositoryUrl?: string; + public userIndependentRepositoryUri?: string; constructor() { super(ParticipationType.PROGRAMMING); diff --git a/src/main/webapp/app/entities/participation/solution-programming-exercise-participation.model.ts b/src/main/webapp/app/entities/participation/solution-programming-exercise-participation.model.ts index d0715826f58f..d9db5fc2b9d3 100644 --- a/src/main/webapp/app/entities/participation/solution-programming-exercise-participation.model.ts +++ b/src/main/webapp/app/entities/participation/solution-programming-exercise-participation.model.ts @@ -3,7 +3,7 @@ import { ProgrammingExercise } from 'app/entities/programming-exercise.model'; export class SolutionProgrammingExerciseParticipation extends Participation { public programmingExercise?: ProgrammingExercise; - public repositoryUrl?: string; + public repositoryUri?: string; public buildPlanId?: string; public buildPlanUrl?: string; diff --git a/src/main/webapp/app/entities/participation/template-programming-exercise-participation.model.ts b/src/main/webapp/app/entities/participation/template-programming-exercise-participation.model.ts index 6000c50275d1..248a30b3e3f4 100644 --- a/src/main/webapp/app/entities/participation/template-programming-exercise-participation.model.ts +++ b/src/main/webapp/app/entities/participation/template-programming-exercise-participation.model.ts @@ -3,7 +3,7 @@ import { ProgrammingExercise } from 'app/entities/programming-exercise.model'; export class TemplateProgrammingExerciseParticipation extends Participation { public programmingExercise?: ProgrammingExercise; - public repositoryUrl?: string; + public repositoryUri?: string; public buildPlanId?: string; public buildPlanUrl?: string; diff --git a/src/main/webapp/app/entities/programming-exercise-auxiliary-repository-model.ts b/src/main/webapp/app/entities/programming-exercise-auxiliary-repository-model.ts index 574f534d9213..3c77e7675bb1 100644 --- a/src/main/webapp/app/entities/programming-exercise-auxiliary-repository-model.ts +++ b/src/main/webapp/app/entities/programming-exercise-auxiliary-repository-model.ts @@ -4,6 +4,6 @@ export class AuxiliaryRepository implements BaseEntity { public id?: number; public name?: string; public checkoutDirectory?: string; - public repositoryUrl?: string; + public repositoryUri?: string; public description?: string; } diff --git a/src/main/webapp/app/entities/programming-exercise.model.ts b/src/main/webapp/app/entities/programming-exercise.model.ts index a315a126516c..3ec9138f12be 100644 --- a/src/main/webapp/app/entities/programming-exercise.model.ts +++ b/src/main/webapp/app/entities/programming-exercise.model.ts @@ -85,7 +85,7 @@ export class ProgrammingExercise extends Exercise { public projectKey?: string; public templateParticipation?: TemplateProgrammingExerciseParticipation; public solutionParticipation?: SolutionProgrammingExerciseParticipation; - public testRepositoryUrl?: string; + public testRepositoryUri?: string; public publishBuildPlanUrl?: boolean; public customizeBuildPlanWithAeolus?: boolean; public allowOnlineEditor?: boolean; diff --git a/src/main/webapp/app/exam/manage/exercise-groups/exercise-groups.component.html b/src/main/webapp/app/exam/manage/exercise-groups/exercise-groups.component.html index 3f9858ee3efa..ecd41d3fc144 100644 --- a/src/main/webapp/app/exam/manage/exercise-groups/exercise-groups.component.html +++ b/src/main/webapp/app/exam/manage/exercise-groups/exercise-groups.component.html @@ -294,7 +294,7 @@

{{ exerciseGroup.title }}
@if (exercise.type === exerciseType.PROGRAMMING) { - + } diff --git a/src/main/webapp/app/exam/manage/exercise-groups/programming-exercise-cell/programming-exercise-group-cell.component.html b/src/main/webapp/app/exam/manage/exercise-groups/programming-exercise-cell/programming-exercise-group-cell.component.html index 88dacd59bad2..05dafc5135d3 100644 --- a/src/main/webapp/app/exam/manage/exercise-groups/programming-exercise-cell/programming-exercise-group-cell.component.html +++ b/src/main/webapp/app/exam/manage/exercise-groups/programming-exercise-cell/programming-exercise-group-cell.component.html @@ -4,15 +4,15 @@ {{ programmingExercise.shortName || '' }} } - @if (displayRepositoryUrl) { + @if (displayRepositoryUri) {
- @if (programmingExercise.templateParticipation?.repositoryUrl) { + @if (programmingExercise.templateParticipation?.repositoryUri) { @if (!localVCEnabled) { - Template + Template } @else { Template @@ -27,10 +27,10 @@ }
- @if (programmingExercise.solutionParticipation?.repositoryUrl) { + @if (programmingExercise.solutionParticipation?.repositoryUri) { @if (!localVCEnabled) { - Solution + Solution } @else { Solution @@ -45,10 +45,10 @@ }
- @if (programmingExercise.testRepositoryUrl) { + @if (programmingExercise.testRepositoryUri) { @if (!localVCEnabled) { - Test + Test } @else { Test diff --git a/src/main/webapp/app/exam/manage/exercise-groups/programming-exercise-cell/programming-exercise-group-cell.component.ts b/src/main/webapp/app/exam/manage/exercise-groups/programming-exercise-cell/programming-exercise-group-cell.component.ts index f62ffbccff34..949ab1482efe 100644 --- a/src/main/webapp/app/exam/manage/exercise-groups/programming-exercise-cell/programming-exercise-group-cell.component.ts +++ b/src/main/webapp/app/exam/manage/exercise-groups/programming-exercise-cell/programming-exercise-group-cell.component.ts @@ -26,7 +26,7 @@ export class ProgrammingExerciseGroupCellComponent implements OnInit { @Input() displayShortName = false; @Input() - displayRepositoryUrl = false; + displayRepositoryUri = false; @Input() displayTemplateUrls = false; @Input() diff --git a/src/main/webapp/app/exam/participate/general-information/exam-general-information.component.html b/src/main/webapp/app/exam/participate/general-information/exam-general-information.component.html index fd38d8508290..17a9fe8d8752 100644 --- a/src/main/webapp/app/exam/participate/general-information/exam-general-information.component.html +++ b/src/main/webapp/app/exam/participate/general-information/exam-general-information.component.html @@ -4,7 +4,7 @@

{{ 'artemisApp.exam.examSummary.generalInformation' | artemisTranslate }}

} -
+
@if (isTestExam) { diff --git a/src/main/webapp/app/exam/participate/general-information/exam-general-information.component.scss b/src/main/webapp/app/exam/participate/general-information/exam-general-information.component.scss index da1940bc4929..928532768f00 100644 --- a/src/main/webapp/app/exam/participate/general-information/exam-general-information.component.scss +++ b/src/main/webapp/app/exam/participate/general-information/exam-general-information.component.scss @@ -1,7 +1,3 @@ -table { - width: fit-content; -} - th, td { text-align: start; diff --git a/src/main/webapp/app/exam/participate/summary/collapsible-card.component.html b/src/main/webapp/app/exam/participate/summary/collapsible-card.component.html new file mode 100644 index 000000000000..fffa152a93ac --- /dev/null +++ b/src/main/webapp/app/exam/participate/summary/collapsible-card.component.html @@ -0,0 +1,12 @@ +
+
+ + +
+ +
+ +
+
diff --git a/src/main/webapp/app/exam/participate/summary/exercises/header/exam-result-summary-exercise-card-header.component.scss b/src/main/webapp/app/exam/participate/summary/collapsible-card.component.scss similarity index 100% rename from src/main/webapp/app/exam/participate/summary/exercises/header/exam-result-summary-exercise-card-header.component.scss rename to src/main/webapp/app/exam/participate/summary/collapsible-card.component.scss diff --git a/src/main/webapp/app/exam/participate/summary/collapsible-card.component.ts b/src/main/webapp/app/exam/participate/summary/collapsible-card.component.ts new file mode 100644 index 000000000000..d63ca18cc8c4 --- /dev/null +++ b/src/main/webapp/app/exam/participate/summary/collapsible-card.component.ts @@ -0,0 +1,19 @@ +import { Component, Input } from '@angular/core'; +import { faAngleRight } from '@fortawesome/free-solid-svg-icons'; + +@Component({ + selector: 'jhi-collapsible-card', + templateUrl: './collapsible-card.component.html', + styleUrls: [ + '../../../course/manage/course-exercise-card.component.scss', + '../../../exercises/quiz/shared/quiz.scss', + 'exam-result-summary.component.scss', + 'collapsible-card.component.scss', + ], +}) +export class CollapsibleCardComponent { + @Input() isCardContentCollapsed: boolean; + @Input() toggleCollapse: () => void; + + faAngleRight = faAngleRight; +} diff --git a/src/main/webapp/app/exam/participate/summary/exam-result-summary.component.html b/src/main/webapp/app/exam/participate/summary/exam-result-summary.component.html index 1ab96c869c6b..a4abf8223ef4 100644 --- a/src/main/webapp/app/exam/participate/summary/exam-result-summary.component.html +++ b/src/main/webapp/app/exam/participate/summary/exam-result-summary.component.html @@ -9,6 +9,13 @@

+@if (studentExam && !studentExam.submitted) { +
+ + {{ 'artemisApp.exam.examSummary.youAreViewingAnUnsubmittedExam' | artemisTranslate }} + +
+} @if (studentExam?.exam) { @for (exercise of studentExam?.exercises; track exercise; let i = $index) {
-
- -
+ +
+ +
+
@if (plagiarismCaseInfos[exercise.id!]) { @@ -183,7 +192,7 @@

} }

-
+
}
diff --git a/src/main/webapp/app/exam/participate/summary/exercises/header/exam-result-summary-exercise-card-header.component.ts b/src/main/webapp/app/exam/participate/summary/exercises/header/exam-result-summary-exercise-card-header.component.ts index bca57b16e669..9002b2cdfe30 100644 --- a/src/main/webapp/app/exam/participate/summary/exercises/header/exam-result-summary-exercise-card-header.component.ts +++ b/src/main/webapp/app/exam/participate/summary/exercises/header/exam-result-summary-exercise-card-header.component.ts @@ -1,13 +1,11 @@ import { Component, Input } from '@angular/core'; import { Exercise } from 'app/entities/exercise.model'; -import { faAngleRight } from '@fortawesome/free-solid-svg-icons'; import { ResultSummaryExerciseInfo } from 'app/exam/participate/summary/exam-result-summary.component'; import { SubmissionType } from 'app/entities/submission.model'; @Component({ selector: 'jhi-result-summary-exercise-card-header', templateUrl: './exam-result-summary-exercise-card-header.component.html', - styleUrls: ['./exam-result-summary-exercise-card-header.component.scss'], }) export class ExamResultSummaryExerciseCardHeaderComponent { @Input() index: number; @@ -15,11 +13,5 @@ export class ExamResultSummaryExerciseCardHeaderComponent { @Input() exerciseInfo?: ResultSummaryExerciseInfo; @Input() resultsPublished: boolean; - faAngleRight = faAngleRight; - - toggleCollapseExercise() { - this.exerciseInfo!.isCollapsed = !this.exerciseInfo!.isCollapsed; - } - readonly SUBMISSION_TYPE_ILLEGAL = SubmissionType.ILLEGAL; } diff --git a/src/main/webapp/app/exam/participate/summary/exercises/programming-exam-summary/programming-exam-summary.component.html b/src/main/webapp/app/exam/participate/summary/exercises/programming-exam-summary/programming-exam-summary.component.html index 093c516afa26..b1ee2eebbcf5 100644 --- a/src/main/webapp/app/exam/participate/summary/exercises/programming-exam-summary/programming-exam-summary.component.html +++ b/src/main/webapp/app/exam/participate/summary/exercises/programming-exam-summary/programming-exam-summary.component.html @@ -4,7 +4,7 @@
{{ 'artemisApp.exam.examSummary.yourSubmission' | artemisTranslate }}
- +
{{ 'artemisApp.exam.examSummary.submissionLinkedToCommit' | artemisTranslate }} diff --git a/src/main/webapp/app/exam/participate/summary/result-overview/exam-result-overview.component.html b/src/main/webapp/app/exam/participate/summary/result-overview/exam-result-overview.component.html index fa89d470a3b8..ec5107e8e8ed 100644 --- a/src/main/webapp/app/exam/participate/summary/result-overview/exam-result-overview.component.html +++ b/src/main/webapp/app/exam/participate/summary/result-overview/exam-result-overview.component.html @@ -3,103 +3,110 @@

{{ 'artemisApp.exam.examSummary.points.overview' | artemisTranslate }}

-
-
- - - - - @if (showIncludedInScoreColumn) { - - } - - - - @if (studentExamWithGrade?.maxBonusPoints) { - - } - - - - @for (exercise of studentExamWithGrade?.studentExam?.exercises; track exercise; let exerciseIndex = $index) { - - - + + +
+ {{ 'artemisApp.exam.examSummary.resultTable' | artemisTranslate }} +
+ +
+
#{{ 'artemisApp.exam.examSummary.points.exercise' | artemisTranslate }}{{ 'artemisApp.exercise.includedInOverallScore' | artemisTranslate }}{{ 'artemisApp.exam.examSummary.points.yourPoints' | artemisTranslate }}{{ 'artemisApp.exam.examSummary.points.maxPoints' | artemisTranslate }}{{ 'artemisApp.exam.examSummary.points.achievedPercentage' | artemisTranslate }}{{ 'artemisApp.exam.examSummary.points.maxBonus' | artemisTranslate }}
{{ exerciseIndex + 1 }} -   - @if (exercise?.type && exerciseInfos[exercise.id!] !== undefined) { -
- -
- } -   - {{ exercise?.exerciseGroup?.title ?? '-' }} -
+ + + + @if (showIncludedInScoreColumn) { - + } - - - + + + @if (studentExamWithGrade?.maxBonusPoints) { + + } + + + + @for (exercise of studentExamWithGrade?.studentExam?.exercises; track exercise; let exerciseIndex = $index) { + + - } - - } - - - - - @if (showIncludedInScoreColumn) { - + @if (showIncludedInScoreColumn) { + + } + + + + @if (studentExamWithGrade?.maxBonusPoints) { + + } + } - - - + + + @if (showIncludedInScoreColumn) { + } - - @if (studentExamWithGrade?.maxBonusPoints) { - - } - - -
#{{ 'artemisApp.exam.examSummary.points.exercise' | artemisTranslate }}{{ exerciseService.isIncludedInScore(exercise) }}{{ 'artemisApp.exercise.includedInOverallScore' | artemisTranslate }} - {{ studentExamWithGrade?.achievedPointsPerExercise?.[exercise.id!] ?? 0 }} - - @if (exercise.maxPoints != undefined) { - {{ exercise.includedInOverallScore === IncludedInOverallScore.INCLUDED_AS_BONUS ? 0 : exercise.maxPoints }} - } - - @if (exerciseInfos[exercise.id!] !== undefined && exerciseInfos[exercise.id!].achievedPercentage !== undefined) { - {{ exerciseInfos[exercise.id!].achievedPercentage }} % - } @else { - - - } - {{ 'artemisApp.exam.examSummary.points.yourPoints' | artemisTranslate }}{{ 'artemisApp.exam.examSummary.points.maxPoints' | artemisTranslate }}{{ 'artemisApp.exam.examSummary.points.achievedPercentage' | artemisTranslate }}{{ 'artemisApp.exam.examSummary.points.maxBonus' | artemisTranslate }}
{{ exerciseIndex + 1 }} - @if (exercise.bonusPoints != undefined) { - {{ exercise.includedInOverallScore === IncludedInOverallScore.INCLUDED_AS_BONUS ? exercise.maxPoints : exercise.bonusPoints }} +   + @if (exercise?.type && exerciseInfos[exercise.id!] !== undefined) { +
+ +
} +   + {{ exercise?.exerciseGroup?.title ?? '-' }}
{{ 'artemisApp.exam.examSummary.points.total' | artemisTranslate }}-{{ exerciseService.isIncludedInScore(exercise) }} + {{ studentExamWithGrade?.achievedPointsPerExercise?.[exercise.id!] ?? 0 }} + + @if (exercise.maxPoints != undefined) { + {{ exercise.includedInOverallScore === IncludedInOverallScore.INCLUDED_AS_BONUS ? 0 : exercise.maxPoints }} + } + + @if (exerciseInfos[exercise.id!] !== undefined && exerciseInfos[exercise.id!].achievedPercentage !== undefined) { + {{ exerciseInfos[exercise.id!].achievedPercentage }} % + } @else { + - + } + + @if (exercise.bonusPoints != undefined) { + {{ + exercise.includedInOverallScore === IncludedInOverallScore.INCLUDED_AS_BONUS ? exercise.maxPoints : exercise.bonusPoints + }} + } +
- {{ overallAchievedPoints }} - - {{ maxPoints }} - - @if (studentExamWithGrade.studentResult.overallScoreAchieved !== undefined) { - {{ overallAchievedPercentageRoundedByCourseSettings }} % - } @else { - - + +
{{ 'artemisApp.exam.examSummary.points.total' | artemisTranslate }}- - {{ studentExamWithGrade?.maxBonusPoints }} + + {{ overallAchievedPoints }}
-
-
- @if (studentExamWithGrade?.maxBonusPoints) { -
+ + {{ maxPoints }} + + + @if (studentExamWithGrade.studentResult.overallScoreAchieved !== undefined) { + {{ overallAchievedPercentageRoundedByCourseSettings }} % + } @else { + - + } + + @if (studentExamWithGrade?.maxBonusPoints) { + + {{ studentExamWithGrade?.maxBonusPoints }} + + } + + + +
+
+ @if (studentExamWithGrade?.maxBonusPoints) { {{ 'artemisApp.exam.examSummary.points.youAchievedWithBonus' | artemisTranslate @@ -108,10 +115,8 @@
normalPoints: maxPoints } }} -
- } - @if (studentExamWithGrade?.maxBonusPoints) { -
+ } + @if (studentExamWithGrade?.maxBonusPoints) { {{ 'artemisApp.exam.examSummary.points.youAchieved' | artemisTranslate @@ -120,10 +125,8 @@
normalPoints: maxPoints } }} -
- } - @if (studentExamWithGrade.studentResult.gradeWithBonus != undefined) { -
+ } + @if (studentExamWithGrade.studentResult.gradeWithBonus != undefined) { {{ 'artemisApp.exam.examSummary.points.youAchievedFromBonus.' + studentExamWithGrade.studentResult.gradeWithBonus.bonusStrategy | artemisTranslate @@ -132,86 +135,39 @@
bonusFromTitle: studentExamWithGrade.studentResult.gradeWithBonus.bonusFromTitle } }} -
- } -
- @if (gradingScaleExists) { -
- - @if (!isBonus) { - - - - - } - @if (isBonus) { - - - - - } - @if (studentExamWithGrade.studentResult.gradeWithBonus != undefined) { - - - - - } -
- {{ - 'artemisApp.exam.examSummary.' + (studentExamWithGrade.studentResult.gradeWithBonus !== undefined ? 'gradeBeforeBonus' : 'grade') - | artemisTranslate - }}: - - {{ grade }} -
{{ 'artemisApp.exam.examSummary.bonus' | artemisTranslate }}:{{ grade }}
{{ 'artemisApp.exam.examSummary.gradeAfterBonus' | artemisTranslate }}: - - {{ studentExamWithGrade.studentResult.gradeWithBonus.finalGrade }} -
-
- } -
- @if (gradingScaleExists) { -
-
-
- -
- -
-
- @if (isBonusGradingKeyDisplayed) { -
- -
- -
-
}
+
+ + + @if (gradingScaleExists) { + +
+   + {{ 'artemisApp.exam.examSummary.gradeKey' | artemisTranslate }} +
+ +
+ +
+
+ + @if (isBonusGradingKeyDisplayed) { + +
+   + {{ 'artemisApp.exam.examSummary.bonusGradeKey' | artemisTranslate }} +
+ +
+ +
+
} -
+ }
} diff --git a/src/main/webapp/app/exam/participate/summary/result-overview/exam-result-overview.component.scss b/src/main/webapp/app/exam/participate/summary/result-overview/exam-result-overview.component.scss index f053df75c6b5..53dd74ed2c52 100644 --- a/src/main/webapp/app/exam/participate/summary/result-overview/exam-result-overview.component.scss +++ b/src/main/webapp/app/exam/participate/summary/result-overview/exam-result-overview.component.scss @@ -30,19 +30,6 @@ border-top-width: 2px; } -.chevron-position { - display: inline-block; - vertical-align: middle; -} - -.rotate-icon { - transition: transform 0.3s ease; -} - -.rotate-icon.rotated { - transform: rotate(90deg); -} - .icon-container { display: inline-block; vertical-align: middle; @@ -55,3 +42,7 @@ width: 24px; height: 24px; } + +.grade-results-table-container { + width: fit-content; +} diff --git a/src/main/webapp/app/exam/participate/summary/result-overview/exam-result-overview.component.ts b/src/main/webapp/app/exam/participate/summary/result-overview/exam-result-overview.component.ts index 4f8d10913e43..9dd49d98af7c 100644 --- a/src/main/webapp/app/exam/participate/summary/result-overview/exam-result-overview.component.ts +++ b/src/main/webapp/app/exam/participate/summary/result-overview/exam-result-overview.component.ts @@ -18,6 +18,8 @@ type ExerciseInfo = { colorClass?: string; }; +type ResultOverviewSection = 'grading-table' | 'grading-key' | 'bonus-grading-key'; + @Component({ selector: 'jhi-exam-result-overview', styleUrls: ['./exam-result-overview.component.scss'], @@ -61,6 +63,12 @@ export class ExamResultOverviewComponent implements OnInit, OnChanges { */ showResultOverview = false; + isCollapsed: Record = { + 'grading-table': false, + 'grading-key': true, + 'bonus-grading-key': true, + }; + constructor( private serverDateService: ArtemisServerDateService, public exerciseService: ExerciseService, @@ -89,11 +97,55 @@ export class ExamResultOverviewComponent implements OnInit, OnChanges { this.maxPoints = this.studentExamWithGrade?.maxPoints ?? 0; this.isBonusGradingKeyDisplayed = this.studentExamWithGrade.studentResult.gradeWithBonus?.bonusGrade != undefined; - this.overallAchievedPoints = this.studentExamWithGrade?.studentResult.overallPointsAchieved ?? 0; - this.overallAchievedPercentageRoundedByCourseSettings = roundScorePercentSpecifiedByCourseSettings( - (this.studentExamWithGrade.studentResult.overallScoreAchieved ?? 0) / 100, - this.studentExamWithGrade.studentExam?.exam?.course, - ); + this.overallAchievedPoints = this.getOverallAchievedPoints(); + this.overallAchievedPercentageRoundedByCourseSettings = this.getOverallAchievedPercentageRoundedByCourseSettings(); + } + + /** + * used as fallback if not pre-calculated by the server + */ + private sumExerciseScores() { + return (this.studentExamWithGrade.studentExam?.exercises ?? []).reduce((exerciseScoreSum, exercise) => { + const achievedPoints = this.studentExamWithGrade?.achievedPointsPerExercise?.[exercise.id!] ?? 0; + return exerciseScoreSum + achievedPoints; + }, 0); + } + + private getOverallAchievedPoints() { + const overallAchievedPoints = this.studentExamWithGrade?.studentResult.overallPointsAchieved; + if (overallAchievedPoints === undefined || overallAchievedPoints === 0) { + return this.sumExerciseScores(); + } + + return overallAchievedPoints; + } + + private getOverallAchievedPercentageRoundedByCourseSettings() { + let overallScoreAchieved = this.studentExamWithGrade.studentResult.overallScoreAchieved; + if (overallScoreAchieved === undefined || overallScoreAchieved === 0) { + overallScoreAchieved = this.summedAchievedExerciseScorePercentage(); + } + + return roundScorePercentSpecifiedByCourseSettings(overallScoreAchieved / 100, this.studentExamWithGrade.studentExam?.exam?.course); + } + + /** + * used as fallback if not pre-calculated by the server + */ + private summedAchievedExerciseScorePercentage() { + let summedPercentages = 0; + let numberOfExercises = 0; + + Object.entries(this.exerciseInfos).forEach(([, exerciseInfo]) => { + summedPercentages += exerciseInfo.achievedPercentage ?? 0; + numberOfExercises++; + }); + + if (numberOfExercises === 0) { + return 0; + } + + return summedPercentages / numberOfExercises; } /** @@ -175,4 +227,8 @@ export class ExamResultOverviewComponent implements OnInit, OnChanges { toggleBonusGradingKey(): void { this.isBonusGradingKeyCollapsed = !this.isBonusGradingKeyCollapsed; } + + toggleCollapse(resultOverviewSection: ResultOverviewSection) { + return () => (this.isCollapsed[resultOverviewSection] = !this.isCollapsed[resultOverviewSection]); + } } diff --git a/src/main/webapp/app/exercises/programming/assess/code-editor-tutor-assessment-container.component.html b/src/main/webapp/app/exercises/programming/assess/code-editor-tutor-assessment-container.component.html index bc68d53a8697..1dd2b0990401 100644 --- a/src/main/webapp/app/exercises/programming/assess/code-editor-tutor-assessment-container.component.html +++ b/src/main/webapp/app/exercises/programming/assess/code-editor-tutor-assessment-container.component.html @@ -107,7 +107,7 @@ @if (!localVCEnabled) { {{ selectedRepository }}