diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ba583836..ee7ef1b8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ jobs: uses: actions/setup-java@v3 with: distribution: 'temurin' - java-version: '21' + java-version: '25' - name: Check out Git repository uses: actions/checkout@v3 @@ -47,7 +47,7 @@ jobs: uses: actions/setup-java@v3 with: distribution: 'temurin' - java-version: '21' + java-version: '25' - name: Check out Git repository uses: actions/checkout@v3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d4c8c45..20e194e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,28 +4,28 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## [0.0.1 - 2025-02-11] +## [1.0.0 - 2026-05-27] ### Added - First version of the project - Spring Application -- State Machine for project +- State Machine for a project - Project states and event states - Project Service - OAuth2 Security -- OIDC <> Organisation Roles Mapping +- OIDC <> Organization Roles Mapping - Session User Info - Session User Info email - Multi bridgehead user -- Test user if security disabled +- Test user if security is disabled - Bridgehead configuration - User Service - Project Role Constraints Aspect - Extract path roles map for Security Configuration - Project State Constraints Aspect - Frontend service -- Set user as developer, pilot or final -- REST Services: Design and Create project -- REST Service: Accept, reject, archive, start develop, start pilot, start final, finish +- Set the user as a developer, pilot or final +- REST Services: Design and Create a project +- REST Service: Accept, reject, archive, start developer, start pilot, start final, finish - Project Query and Query Format - REST Services: Create Query and Create CQL DATA Query - Document Service @@ -33,7 +33,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Project Type - HTTP Method for every endpoint in Security Configuration - IS_TEST_ENVIRONMENT variable disables CSRF -- Create CREATOR user after creating project +- Create CREATOR user after creating a project - Send and execute query REST Services - Label, description, template-id and output-format in Query - Token Manager Service @@ -42,7 +42,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Exporter Service implementation - Bridgehead configuration with explorer and exporter mapping - Bridgehead Operation -- Create query and draft project (Lens) +- Create a query and draft a project (Lens) - humanReadable in Bridgehead Config - Email Service - Email Templates @@ -55,29 +55,29 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Module names - Http Method and Params for Actions - Test endpoint -- Download application form +- Download the application form - Edit project and query - Fetch output format, query format and templates - Query Context -- Download document rights to developer, pilot and final users +- Download document rights to a developer, pilot and final users - Fetch Projects -- Modified Desc as parameter of fetch projects +- Modified Desc as a parameter of fetch projects - Upload and download documents by document type - Fetch publications and other documents - Fetch project types - Accept and reject project bridgehead - Fetch project bridgeheads -- Accept, reject and request changes in script +- Accept, reject and request changes in a script - Fetch all actions - Accept, Reject and request changes in project results - Notification Service - Token Manager services - Integration in Focus and Beam - Frontend DTO -- Email as Mime message +- Email as a Mime message - EmailSenderIfError annotation - Frontend DTO converters -- Fetch all user visible notifications +- Fetch all user-visible notifications - Accepted state requirement for setting user as developer or pilot - Notification User Action - Replace Bridgehead Operation through Notification @@ -89,16 +89,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Add notifications for services: document, project bridgehead, project event, project, query, user - DataSHIELD Token Manager Job - Check Expired Active Projects Job -- Check if session scope is active before creating a notification for changing project state -- Fetch token status and project status of token manager -- New email if authentication script for DataSHIELD has changed +- Check if the session scope is active before creating a notification for changing the project state +- Fetch token status and project status of a token manager +- New email if an authentication script for DataSHIELD has changed - Explorer URL in CORS - Oauth2 Resource Server configuration - Allow required headers - Project Constraints: Project Type -- Email template for Accept/Reject/Request changes in project +- Email template for Accept/Reject/Request changes in a project - User Autocomplete -- Explorer Ids to create and edit query +- Explorer Ids to create and edit a query - Decode redirect explorer URL - Fetch project users - rejectProject state constraints @@ -106,14 +106,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Flag ENABLE_TOKEN_MANAGER - Content Disposition in exposed headers - Exists Votum and Application Form -- Exists Script +- Script exists - Explorer Redirect URI - User State in Frontend DTO - Fetch project configurations -- Exists authentication script +- There exists an authentication script - Remove inactive projects in Opal - Project Bridgehead DataSHIELD info -- Set project bridgehead datashield removed to false while sending query of DATASHIELD project to exporter +- Set project bridgehead datashield removed to false while sending a query of a DATASHIELD project to exporter - Fetch current project configuration - Custom project configuration - Fetch visible bridgeheads and fetch project bridgeheads @@ -121,50 +121,50 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fetch Project Roles - Email Types: NEW_PROJECT_ACCEPTED, NEW_VOTUM, REQUEST_TECHNICAL_APPROVAL, NEW_PUBLICATION - Email config for types NEW_PROJECT_ACCEPTED, NEW_VOTUM, REQUEST_TECHNICAL_APPROVAL, NEW_PUBLICATION -- Fetch label of application form, label and script +- Fetch label of an application form, label and script - Service exist invited users - R-Studio Group Manager - Email Sender: ALL_DEVELOP, ALL_PILOT, ALL_FINAL - Send email to all involved users after a publication has been uploaded - Project and project view url in email context -- Create notification after sending an email -- Send message while requesting changes in script or project -- Send message while rejecting a project +- Create a notification after sending an email +- Send a message while requesting changes in a script or project +- Send a message while rejecting a project - Exporter Job - Check Queries in Exporter Job -- Max time to wait focus task in minutes +- Max time to wait a focus task in minutes - Check export execution status - Coder Service - RStudio in Coder - Research environment project type - Upload file to Coder - Delete Coder workspace job -- Resend export files to research environment +- Resend export files to a research environment - Query Code in Explorer URL -- Decode human readable in base 64 -- Log bridgeheads at start -- Replace hypen in bridgeheads +- Decode human-readable in base 64 +- Log bridgeheads at the start +- Replace hype in bridgeheads - Log bridgeheads sorted - Replace hyphen in frontend sites - Human Readable Bridgehead for frontend dto - Http proxy configuration - Email as HTML -- Provide also human readable by fetch all registered bridgeheads +- Provide also human-readable by fetching all registered bridgeheads - DataSHIELD Project Status Error - Catch token manager exceptions and return ERROR status -- Send emails in async execution pool +- Send emails in the async execution pool - Notification async execution pool - Exporter async execution pool - Action explanations -- Integration of action explanatios in fetch Actions +- Integration of action examples in fetch Actions - Action explanations templates - Email Context - Extend email context - Thymeleaf Dialect for email context variables -- Combine first and last name in name -- Replace email context variables in email subject -- Default and Default var in html tags -- Fetch human readable bridgehead for emails +- Combine first and last name in a name +- Replace email context variables in the email subject +- Default and Default var in HTML tags +- Fetch human-readable bridgehead for emails - Email context project bridgeheads variable - Decode Base64 values of email context - Enable minus in variables of email context @@ -173,35 +173,35 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Process variables recursively in Thymeleaf - Creator name in Frontend Project - Action explanation priority -- Configurations as file +- Configurations as a file - File Description for Frontend -- Fetch Project-States for dashboard site +- Fetch Project-States for the dashboard site - Primary and Test Mail Server -- Email if project rejected -- Send email when the query has arrived to the exporter or it has been executed +- Email if a project is rejected +- Send email when the query has arrived to the exporter, or it has been executed - Export dispatch counter - Title and description in project configuration - Annotation Ignore Project Configuration Match -- Send error email if error while sending query to bridgehead -- Mailing Black List -- Sort Mailing Black List by first and last name +- Send error email if error while sending a query to bridgehead +- Mailing Blacklist +- Sort Mailing Blacklist by first and last name - Results -- DRN in exporter label +- DRN in the exporter label - Final User State in results - Create State in Frontend DTO's (for project and project bridgehead) - Fetch email message and subject HTTP endpoint - Send project results only if there are final users - User Project State in State Constraints - Bridgehead Admin info in the variables for the email templates -- Results URL in frontend dto +- Result URL in frontend dto - EmailContextKey enum - Votum for all - Configuration Site - REST Service: is project manager admin - Research Environment URL key for Email Context - REST Service: Fetch research environment URL -- REST Service: Exists research environment workspace for user -- View only last reseach environment workspace for a project +- REST Service: There exists a research environment workspace for user +- View only the last research environment workspace for a project - Valid Email Aspect - EncryptionConverter to encrypt results url - Coder Configuration @@ -217,3 +217,81 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Log App Register Service - Removal conditions for ProjectBridgeheadDataShield - Check message for HTTP requests to exporter +- Request Variable annotation +- Optimize JPA configuration +- Compatibility with Authentik +- JWT Decoder configuration +- AST and AST_DATA as query format +- Project Form +- Project Form Services +- Language annotation +- Form PDF Service +- External Execution Not Allowed Profile +- Download Form PDF +- Arrange form fields in the right order in PDF +- DataType in Form PDF +- Form Variables Config +- Fetch Form Templates +- Fetch Form Template Filename +- Current Date in Form Template +- Project Form Fields +- Bridgehead Affiliation +- Form Data Type Date and Timestamp +- HTTP Get methods fetch RequestParam and HTTP Post methods fetch RequestVariable except for uploading files +- Check organizational roles for any bridgehead +- Form Repository +- Fetch all project states depending on project code +- Make user role thread safe +- Log frontend config +- SAMPLES project type +- Project And Form Configuration +- Multiple types pro-project +- Remove the project output service +- Use global conversion service of spring +- Annotated Parameter Wrapper +- Request Parameter +- Synchronization between query outputs and bridgehead executions +- Project context in form template context +- Template Filename +- Send a query with different project types to bridgehead +- List parameters in a controller +- Fetch best form templates +- Description document and related services +- Cohort definition in a query +- Send Query To Bridgehead Event +- Send Query To Bridgehead Event scheduled job +- Send attachments in emails +- Final report services + + +### Changed +- Rename project state CREATED and ACCEPTED as REVIEW and APPROVAL +- Update to Maven 4 +- Update to Java 25 +- Remove project bridgehead user with role creator +- View also own-created projects even if they don't belong to own bridgeheads +- View all project bridgeheads for creator +- RequestVariableMethodArgumentResolver for complex datatypes +- Edit form instead of just adding forms to the project +- Add and rename forms instead of editing services +- Notifications only for Project Manager Admin +- Filter templates by selected forms +- Samples project can generate exports not visible to researcher +- there isCustomConfig in the Project as nullable +- Ethical Approval in Project Context +- Data Type Enum for Form Config +- Fetch selected forms returns also preconfigured titles +- Query Details +- There Exists Publication + + +### Removed +- RStudio Keycloak Groups +- Application Form Services + +### Fixed +- Init flyway migration in spring 4 +- Jwt decoder configuration +- Full name if the first or last name is missing +- Only one user session at the same time +- Update query outputs correctly diff --git a/Dockerfile b/Dockerfile index ed8b332b..569590ee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM eclipse-temurin:21-jre +FROM eclipse-temurin:25-jre COPY target/project-manager.jar /app/ diff --git a/pom.xml b/pom.xml index 5fc11f97..613c4b81 100644 --- a/pom.xml +++ b/pom.xml @@ -5,8 +5,8 @@ org.springframework.boot spring-boot-starter-parent - 3.2.1 - + 4.0.5 + de.samply project-manager @@ -14,13 +14,17 @@ project-manager Samply Project Manager - 21 - 2.2.0 - 3.2.1 - 1.18.30 - 3.9.6 - 6.1.1 - 1.14.11 + 25 + 3.0.0 + 4.0.1 + 1.18.42 + 3.9.15 + 5.0.0.Alpha2 + 1.1.36 + 4.0.0-M2 + 1.6.3 + 3.15.0 + 7.7.0 @@ -46,6 +50,7 @@ org.springframework.boot spring-boot-starter-aop + ${spring-boot-starter-aop.version} org.springframework.boot @@ -60,9 +65,14 @@ springdoc-openapi-starter-webmvc-ui ${openapi.version} + + org.springframework.boot + spring-boot-starter-flyway + org.flywaydb - flyway-core + flyway-database-postgresql + runtime org.springframework.statemachine @@ -80,6 +90,11 @@ maven-model ${maven-model.version} + + io.netty + netty-transport-native-epoll + ${netty-transport-native-epoll.version} + org.postgresql @@ -96,11 +111,62 @@ reactor-test test + + io.github.openhtmltopdf + openhtmltopdf-pdfbox + ${openhtmltopdf-pdfbox.version} + + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + provided + + + + net.javacrumbs.shedlock + shedlock-spring + ${shedlock-spring.version} + + + net.javacrumbs.shedlock + shedlock-provider-jdbc-template + ${shedlock-spring.version} + + project-manager + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${java.version} + ${java.version} + + + org.projectlombok + lombok + ${lombok.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + + + org.springframework.boot spring-boot-maven-plugin @@ -108,4 +174,6 @@ + + diff --git a/src/main/java/de/samply/annotations/Language.java b/src/main/java/de/samply/annotations/Language.java new file mode 100644 index 00000000..11eddc7d --- /dev/null +++ b/src/main/java/de/samply/annotations/Language.java @@ -0,0 +1,11 @@ +package de.samply.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface Language { +} diff --git a/src/main/java/de/samply/annotations/RequestParameter.java b/src/main/java/de/samply/annotations/RequestParameter.java new file mode 100644 index 00000000..93495186 --- /dev/null +++ b/src/main/java/de/samply/annotations/RequestParameter.java @@ -0,0 +1,20 @@ +package de.samply.annotations; + +import de.samply.app.ProjectManagerConst; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) // Applies to method parameters +@Retention(RetentionPolicy.RUNTIME) // Retain at runtime for reflection +public @interface RequestParameter { + String name(); + + boolean required() default true; + + boolean notEmpty() default false; + + String defaultValue() default ProjectManagerConst.NOT_SET; +} diff --git a/src/main/java/de/samply/annotations/RequestVariable.java b/src/main/java/de/samply/annotations/RequestVariable.java new file mode 100644 index 00000000..8bea81f8 --- /dev/null +++ b/src/main/java/de/samply/annotations/RequestVariable.java @@ -0,0 +1,87 @@ +package de.samply.annotations; + +import de.samply.app.ProjectManagerConst; +import de.samply.resolvers.RequestVariableAndParameterMethodArgumentResolver; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Custom annotation to bind request parameters from either the request body or query parameters. + * This allows flexible handling of request variables by attempting to extract values + * from both the query parameters and the JSON body, providing an elegant fallback mechanism. + * + *

+ * The annotation can be used on controller method parameters, and the corresponding values + * will be retrieved either from the query parameters or the JSON body (if the parameter is missing + * in the query parameters). It simplifies code and ensures that the necessary parameters are + * available regardless of where they are specified in the request. + *

+ * + *

Example of JSON Body

+ * + *
{@code
+ * POST /create HTTP/1.1
+ * Content-Type: application/json
+ *
+ * {
+ *   "query": "SELECT * FROM projects WHERE status = 'active'",
+ *   "queryFormat": "SQL",
+ *   "label": "Active Projects",
+ *   "description": "Projects that are currently active."
+ * }
+ * }
+ * + *

In the above example:

+ * + * + *

How it works

+ * + * + *

Usage Example

+ * + *
{@code
+ * {@literal @}PostMapping("/create")
+ * public ResponseEntity createProject(
+ *     {@literal @}RequestVariable(name = "query", required = true) String query,
+ *     {@literal @}RequestVariable(name = "queryFormat", required = true) String queryFormat,
+ *     {@literal @}RequestVariable(name = "label", required = false) String label,
+ *     {@literal @}RequestVariable(name = "description", required = false) String description
+ * ) {
+ *     return ResponseEntity.ok("ProjectCode created successfully!");
+ * }
+ * }
+ * + *

In this example:

+ * + * + * @see RequestVariableAndParameterMethodArgumentResolver + */ + + +@Target(ElementType.PARAMETER) // Applies to method parameters +@Retention(RetentionPolicy.RUNTIME) // Retain at runtime for reflection +public @interface RequestVariable { + String name(); + + boolean required() default true; + + boolean notEmpty() default false; + + String defaultValue() default ProjectManagerConst.NOT_SET; +} diff --git a/src/main/java/de/samply/annotations/RoleConstraints.java b/src/main/java/de/samply/annotations/RoleConstraints.java index 1ae24897..cacae1da 100644 --- a/src/main/java/de/samply/annotations/RoleConstraints.java +++ b/src/main/java/de/samply/annotations/RoleConstraints.java @@ -19,7 +19,7 @@ */ OrganisationRole[] organisationRoles() default {}; - // Project Role are checked in ProjectRoleConstrainstAspects + // ProjectCode Role are checked in ProjectRoleConstraintAspects ProjectRole[] projectRoles() default {}; diff --git a/src/main/java/de/samply/aop/ConstraintsService.java b/src/main/java/de/samply/aop/ConstraintsService.java index 514ea130..0a77b0d3 100644 --- a/src/main/java/de/samply/aop/ConstraintsService.java +++ b/src/main/java/de/samply/aop/ConstraintsService.java @@ -6,20 +6,15 @@ import de.samply.db.model.Project; import de.samply.db.model.ProjectBridgehead; import de.samply.db.model.ProjectBridgeheadUser; -import de.samply.db.repository.ProjectBridgeheadRepository; -import de.samply.db.repository.ProjectBridgeheadUserRepository; -import de.samply.db.repository.ProjectRepository; -import de.samply.project.ProjectType; +import de.samply.project.ProjectBridgeheadUserService; import de.samply.project.state.ProjectBridgeheadState; import de.samply.project.state.ProjectState; import de.samply.project.state.UserProjectState; -import de.samply.query.QueryState; import de.samply.security.SessionUser; import de.samply.user.roles.OrganisationRole; import de.samply.user.roles.OrganisationRoleToProjectRoleMapper; import de.samply.user.roles.ProjectRole; import de.samply.user.roles.UserProjectRoles; -import de.samply.utils.AspectUtils; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; @@ -28,13 +23,13 @@ import java.util.Map; import java.util.Optional; +@SuppressWarnings("rawtypes") // For Optional. Otherwise, it would be too complex @Service public class ConstraintsService { - private final ProjectRepository projectRepository; + private final ProjectBridgeheadUserService projectBridgeheadUserService; + private final OrganisationRoleToProjectRoleMapper organisationRoleToProjectRoleMapper; - private final ProjectBridgeheadRepository projectBridgeheadRepository; - private final ProjectBridgeheadUserRepository projectBridgeheadUserRepository; private final SessionUser sessionUser; private final Map temporalProjectRoleProjectStateMap = Map.of( @@ -42,28 +37,24 @@ public class ConstraintsService { ProjectRole.PILOT, ProjectState.PILOT, ProjectRole.FINAL, ProjectState.FINAL); - public ConstraintsService(ProjectRepository projectRepository, + public ConstraintsService(ProjectBridgeheadUserService projectBridgeheadUserService, OrganisationRoleToProjectRoleMapper organisationRoleToProjectRoleMapper, - ProjectBridgeheadRepository projectBridgeheadRepository, - ProjectBridgeheadUserRepository projectBridgeheadUserRepository, SessionUser sessionUser) { - this.projectRepository = projectRepository; + this.projectBridgeheadUserService = projectBridgeheadUserService; this.organisationRoleToProjectRoleMapper = organisationRoleToProjectRoleMapper; - this.projectBridgeheadRepository = projectBridgeheadRepository; - this.projectBridgeheadUserRepository = projectBridgeheadUserRepository; this.sessionUser = sessionUser; } - public Optional checkRoleConstraints(Optional roleConstraints, Optional stateConstraints, Optional projectCode, Optional bridgehead) { - Optional result = checkOrganisationRoleConstraints(roleConstraints, bridgehead); - return (result.isPresent()) ? result : checkProjectRoleConstraints(roleConstraints, stateConstraints, projectCode, bridgehead); + public Optional checkRoleConstraints(Optional roleConstraints, Optional stateConstraints, Optional project, Optional bridgehead) { + Optional result = checkOrganisationRoleConstraints(roleConstraints); + return (result.isPresent()) ? result : checkProjectRoleConstraints(roleConstraints, stateConstraints, project, bridgehead); } - public Optional checkOrganisationRoleConstraints(Optional roleConstraints, Optional bridgehead) { + public Optional checkOrganisationRoleConstraints(Optional roleConstraints) { if (roleConstraints.isPresent() && roleConstraints.get().organisationRoles().length > 0) { boolean hasAnyOrganisationRole = false; for (OrganisationRole organisationRole : roleConstraints.get().organisationRoles()) { - if (sessionUser.getUserOrganisationRoles().containsRole(organisationRole, bridgehead)) { + if (sessionUser.getUserOrganisationRoles().containsAnyRole(organisationRole)) { hasAnyOrganisationRole = true; break; } @@ -75,19 +66,15 @@ public Optional checkOrganisationRoleConstraints(Optional checkProjectRoleConstraints(Optional roleConstraints, Optional stateConstraints, Optional projectCode, Optional bridgehead) { + public Optional checkProjectRoleConstraints(Optional roleConstraints, Optional stateConstraints, Optional project, Optional bridgehead) { if (roleConstraints.isPresent() && roleConstraints.get().projectRoles().length > 0) { - if (projectCode.isEmpty() || projectCode.get().length() == 0) { - return Optional.of(ResponseEntity.badRequest().body("Project code not provided")); - } - Optional project = AspectUtils.fetchProject(projectRepository, projectCode); if (project.isEmpty()) { return Optional.of(ResponseEntity.notFound().build()); } Optional userProjectRoles = organisationRoleToProjectRoleMapper.map(project.get()); boolean userHasProjectRoleInProject = false; for (ProjectRole projectRole : roleConstraints.get().projectRoles()) { - if (userHasProjectRoleInProject(userProjectRoles, project.get(), projectRole, bridgehead) && + if (userHasProjectRoleInProject(userProjectRoles, projectRole, bridgehead) && isProjectRoleInAuthorizedProjectState(projectRole, project.get(), stateConstraints)) { userHasProjectRoleInProject = true; break; @@ -108,16 +95,13 @@ private boolean isProjectRoleInAuthorizedProjectState(ProjectRole projectRole, P return project.getState() == projectState && Arrays.asList(stateConstraints.get().projectStates()).contains(projectState); } - private boolean userHasProjectRoleInProject(Optional userProjectRoles, Project project, ProjectRole projectRole, Optional bridgehead) { - return (userProjectRoles.isPresent()) ? userProjectRoles.get().containsRole(projectRole, bridgehead) : false; + private boolean userHasProjectRoleInProject(Optional userProjectRoles, ProjectRole projectRole, Optional bridgehead) { + Optional bridgeheadOptional = bridgehead.map(ProjectBridgehead::getBridgehead); + return userProjectRoles.isPresent() && userProjectRoles.get().containsRole(projectRole, bridgeheadOptional); } - public Optional checkStateConstraints(Optional stateConstraints, Optional projectCode, Optional bridgehead) { + public Optional checkStateConstraints(Optional stateConstraints, Optional project, Optional bridgehead) { if (stateConstraints.isPresent()) { - if (projectCode.isEmpty() || projectCode.get().length() == 0) { - return Optional.of(ResponseEntity.badRequest().body("Project code not provided")); - } - Optional project = AspectUtils.fetchProject(projectRepository, projectCode); if (project.isEmpty()) { return Optional.of(ResponseEntity.notFound().build()); } @@ -134,32 +118,27 @@ public Optional checkStateConstraints(Optional } } if (stateConstraints.get().projectBridgeheadStates().length > 0 || stateConstraints.get().queryStates().length > 0 || stateConstraints.get().userProjectStates().length > 0) { - Optional projectBridgehead = fetchProjectBridgehead(project.get(), bridgehead); - if (projectBridgehead.isEmpty()) { + if (bridgehead.isEmpty()) { return Optional.of(ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).build()); } boolean hasAnyProjectBridgeheadStateConstraint = true; if (stateConstraints.get().projectBridgeheadStates().length > 0) { hasAnyProjectBridgeheadStateConstraint = false; for (ProjectBridgeheadState projectBridgeheadState : stateConstraints.get().projectBridgeheadStates()) { - if (projectBridgehead.get().getState() == projectBridgeheadState) { + if (bridgehead.get().getState() == projectBridgeheadState) { hasAnyProjectBridgeheadStateConstraint = true; break; } } } if (hasAnyProjectBridgeheadStateConstraint && stateConstraints.get().queryStates().length > 0) { - hasAnyProjectBridgeheadStateConstraint = false; - for (QueryState queryState : stateConstraints.get().queryStates()) { - if (projectBridgehead.get().getQueryState() == queryState) { - hasAnyProjectBridgeheadStateConstraint = true; - break; - } - } + hasAnyProjectBridgeheadStateConstraint = bridgehead.get().getExecutions().stream() + .anyMatch(exec -> Arrays.asList(stateConstraints.get().queryStates()) + .contains(exec.getQueryState())); } if (hasAnyProjectBridgeheadStateConstraint && stateConstraints.get().userProjectStates().length > 0) { hasAnyProjectBridgeheadStateConstraint = false; - Optional projectBridgeheadUser = fetchProjectBridgeheadUser(projectBridgehead.get()); + Optional projectBridgeheadUser = fetchProjectBridgeheadUser(bridgehead.get()); if (projectBridgeheadUser.isPresent()) { for (UserProjectState userProjectState : stateConstraints.get().userProjectStates()) { if (projectBridgeheadUser.get().getProjectState() == userProjectState) { @@ -188,24 +167,16 @@ public Optional checkStateConstraints(Optional return Optional.empty(); } - public Optional checkProjectConstraints(Optional projectConstraints, Optional projectCode) { + public Optional checkProjectConstraints(Optional projectConstraints, Optional project) { //TODO if (projectConstraints.isPresent()) { - if (projectCode.isEmpty() || projectCode.get().length() == 0) { - return Optional.of(ResponseEntity.badRequest().body("Project code not provided")); - } - Optional project = AspectUtils.fetchProject(projectRepository, projectCode); if (project.isEmpty()) { return Optional.of(ResponseEntity.notFound().build()); } if (projectConstraints.get().projectTypes().length > 0) { - boolean hasAnyProjectTypeConstraint = false; - for (ProjectType projectType : projectConstraints.get().projectTypes()) { - if (project.get().getType() == projectType) { - hasAnyProjectTypeConstraint = true; - break; - } - } + boolean hasAnyProjectTypeConstraint = Arrays.stream(projectConstraints.get().projectTypes()) + .anyMatch(pc -> project.get().hasProjectType(pc)); + if (!hasAnyProjectTypeConstraint) { return Optional.of(ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).build()); } @@ -214,12 +185,8 @@ public Optional checkProjectConstraints(Optional fetchProjectBridgehead(Project project, Optional bridgehead) { - return (bridgehead.isEmpty()) ? Optional.empty() : this.projectBridgeheadRepository.findFirstByBridgeheadAndProject(bridgehead.get(), project); - } - private Optional fetchProjectBridgeheadUser(ProjectBridgehead projectBridgehead) { - return this.projectBridgeheadUserRepository.getFirstByEmailAndProjectBridgeheadOrderByModifiedAtDesc(sessionUser.getEmail(), projectBridgehead); + return projectBridgeheadUserService.fetchFirstUsersOrderByModifiedAtDesc(sessionUser.getEmail(), projectBridgehead); } } diff --git a/src/main/java/de/samply/aop/EmailSenderAspect.java b/src/main/java/de/samply/aop/EmailSenderAspect.java index aba995fd..e722d859 100644 --- a/src/main/java/de/samply/aop/EmailSenderAspect.java +++ b/src/main/java/de/samply/aop/EmailSenderAspect.java @@ -8,10 +8,12 @@ import de.samply.db.model.Project; import de.samply.db.model.ProjectBridgehead; import de.samply.db.model.ProjectBridgeheadUser; -import de.samply.db.repository.*; import de.samply.email.*; +import de.samply.project.ProjectBridgeheadService; +import de.samply.project.ProjectBridgeheadUserService; import de.samply.project.state.ProjectBridgeheadState; import de.samply.security.SessionUser; +import de.samply.user.UserService; import de.samply.user.roles.OrganisationRoleToProjectRoleMapper; import de.samply.user.roles.ProjectRole; import de.samply.user.roles.UserProjectRoles; @@ -26,7 +28,6 @@ import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; -import java.lang.annotation.Annotation; import java.util.*; import java.util.function.Supplier; @@ -34,48 +35,48 @@ @Aspect public class EmailSenderAspect { + // Services private final EmailService emailService; private final SessionUser sessionUser; - private final BridgeheadAdminUserRepository bridgeheadAdminUserRepository; - private final ProjectBridgeheadUserRepository projectBridgeheadUserRepository; - private final ProjectManagerAdminUserRepository projectManagerAdminUserRepository; + private final UserService userService; + private final ProjectBridgeheadService projectBridgeheadService; + private final ProjectBridgeheadUserService projectBridgeheadUserService; + private final OrganisationRoleToProjectRoleMapper organisationRoleToProjectRoleMapper; - private final ProjectBridgeheadRepository projectBridgeheadRepository; - private final ProjectRepository projectRepository; private final EmailKeyValuesFactory emailKeyValuesFactory; public EmailSenderAspect(EmailService emailService, SessionUser sessionUser, - BridgeheadAdminUserRepository bridgeheadAdminUserRepository, - ProjectBridgeheadUserRepository projectBridgeheadUserRepository, - ProjectManagerAdminUserRepository projectManagerAdminUserRepository, + UserService userService, + ProjectBridgeheadService projectBridgeheadService, + ProjectBridgeheadUserService projectBridgeheadUserService, OrganisationRoleToProjectRoleMapper organisationRoleToProjectRoleMapper, - ProjectBridgeheadRepository projectBridgeheadRepository, - ProjectRepository projectRepository, EmailKeyValuesFactory emailKeyValuesFactory) { this.emailService = emailService; this.sessionUser = sessionUser; - this.bridgeheadAdminUserRepository = bridgeheadAdminUserRepository; - this.projectBridgeheadUserRepository = projectBridgeheadUserRepository; - this.projectManagerAdminUserRepository = projectManagerAdminUserRepository; + this.userService = userService; + this.projectBridgeheadService = projectBridgeheadService; + this.projectBridgeheadUserService = projectBridgeheadUserService; this.organisationRoleToProjectRoleMapper = organisationRoleToProjectRoleMapper; - this.projectBridgeheadRepository = projectBridgeheadRepository; - this.projectRepository = projectRepository; this.emailKeyValuesFactory = emailKeyValuesFactory; } + @SuppressWarnings("EmptyMethod") @Pointcut("@annotation(de.samply.annotations.EmailSender)") public void emailSenderPointcut() { } + @SuppressWarnings("EmptyMethod") @Pointcut("@annotation(de.samply.annotations.EmailSenders)") public void emailSendersPointcut() { } + @SuppressWarnings("EmptyMethod") @Pointcut("@annotation(de.samply.annotations.EmailSenderIfError)") public void emailSenderIfErrorPointcut() { } + @SuppressWarnings("EmptyMethod") @Pointcut("@annotation(de.samply.annotations.EmailSendersIfError)") public void emailSendersIfErrorPointcut() { } @@ -101,16 +102,16 @@ public Object aroundEmailSendersIfError(ProceedingJoinPoint joinPoint) throws Th return aroundEmailSender(joinPoint, false, true); } - private Object aroundEmailSender(ProceedingJoinPoint joinPoint, boolean isSingleEmailSender, boolean ifError) throws Throwable { + private Object aroundEmailSender(ProceedingJoinPoint joinPoint, boolean isSingleEmailSender, boolean ifError) throws Throwable { try { - ResponseEntity responseEntity = (ResponseEntity) joinPoint.proceed(); + @SuppressWarnings("rawtypes") ResponseEntity responseEntity = (ResponseEntity) joinPoint.proceed(); if (responseEntity.getStatusCode().is2xxSuccessful() ^ ifError) { sendEmail(joinPoint, isSingleEmailSender, ifError); } return responseEntity; } catch (Exception e) { if (ifError) { - sendEmail(joinPoint, isSingleEmailSender, ifError); + sendEmail(joinPoint, isSingleEmailSender, true); } throw new RuntimeException(e); } @@ -138,8 +139,8 @@ private void sendEmailFromEmailSenders(ProceedingJoinPoint joinPoint, Optional emailSenderOptional) { - emailSenderOptional.ifPresent(emailSender -> fetchEmailRecipients(() -> emailSender.recipients(), joinPoint) - .forEach(emailRecipient -> sendEmail(emailRecipient, () -> emailSender.templateType()))); + emailSenderOptional.ifPresent(emailSender -> fetchEmailRecipients(emailSender::recipients, joinPoint) + .forEach(emailRecipient -> sendEmail(emailRecipient, emailSender::templateType))); } private void sendEmailFromEmailSendersIfError(ProceedingJoinPoint joinPoint, Optional emailSendersIfErrorOptional) { @@ -148,14 +149,14 @@ private void sendEmailFromEmailSendersIfError(ProceedingJoinPoint joinPoint, Opt } private void sendEmailFromEmailSenderIfError(ProceedingJoinPoint joinPoint, Optional emailSenderIfErrorOptional) { - emailSenderIfErrorOptional.ifPresent(emailSenderIfError -> fetchEmailRecipients(() -> emailSenderIfError.recipients(), joinPoint) - .forEach(emailRecipient -> sendEmail(emailRecipient, () -> emailSenderIfError.templateType()))); + emailSenderIfErrorOptional.ifPresent(emailSenderIfError -> fetchEmailRecipients(emailSenderIfError::recipients, joinPoint) + .forEach(emailRecipient -> sendEmail(emailRecipient, emailSenderIfError::templateType))); } @Async(ProjectManagerConst.ASYNC_EMAIL_SENDER_EXECUTOR) protected void sendEmail(EmailRecipient emailRecipient, Supplier emailTemplateTypeSupplier) { try { - emailService.sendEmail(emailRecipient.getEmail(), emailRecipient.getProjectCode(), emailRecipient.getBridgehead(), + emailService.sendEmail(emailRecipient.getEmail(), emailRecipient.getProject(), emailRecipient.getBridgehead(), emailRecipient.getRole(), emailTemplateTypeSupplier.get(), emailKeyValuesFactory.newInstance().add(emailRecipient)); } catch (EmailServiceException e) { throw new RuntimeException(e); @@ -180,24 +181,25 @@ private Optional fetchEmailSendersIfError(JoinPoint joinPoi private Set fetchEmailRecipients(Supplier emailRecipientTypesSupplier, ProceedingJoinPoint joinPoint) { Set result = new HashSet<>(); - Optional projectCode = AspectUtils.fetchProjectCode(joinPoint); - Optional bridgehead = AspectUtils.fetchBridgehead(joinPoint); + Optional project = AspectUtils.fetchProject(joinPoint); + Optional projectBridgehead = AspectUtils.fetchBridgehead(joinPoint); Optional email = AspectUtils.fetchEmail(joinPoint); Optional message = AspectUtils.fetchMessage(joinPoint); Arrays.stream(emailRecipientTypesSupplier.get()).forEach(emailRecipientType -> result.addAll(switch (emailRecipientType) { - case SESSION_USER -> fetchEmailRecipientsForSessionUser(projectCode, bridgehead); - case CREATOR -> fetchEmailRecipientsForCreator(projectCode, bridgehead); - case EMAIL_ANNOTATION -> fetchEmailRecipientsForEmailAnnotation(projectCode, bridgehead, email); - case ALL_BRIDGEHEAD_ADMINS -> fetchEmailRecipientsForAllBridgeheadAdminsOfTheProject(projectCode); - case ALL_DEVELOPERS -> fetchEmailRecipientsForAllDeveloperUsersOfTheProject(projectCode); - case ALL_PILOTS -> fetchEmailRecipientsForAllPilotUsersOfTheProject(projectCode); - case ALL_FINALS -> fetchEmailRecipientsForAllFinalUsersOfTheProject(projectCode); - case BRIDGEHEAD_ADMIN -> fetchEmailRecipientsForBridgeheadAdmin(projectCode, bridgehead); - case BRIDGHEAD_ADMINS_WHO_HAVE_NOT_ACCEPTED_NOR_REJECTED_THE_PROJECT -> - fetchEmailRecipientsForBridgeheadAdminsWhoHaveNotAcceptedNorRejectedTheProject(projectCode); - case PROJECT_MANAGER_ADMIN -> fetchEmailRecipientsForProjectManagerAdmin(projectCode, bridgehead); - case PROJECT_ALL -> fetchEmailRecipientsForAllProjectUsers(projectCode, bridgehead); + case SESSION_USER -> fetchEmailRecipientsForSessionUser(project, projectBridgehead); + case CREATOR -> fetchEmailRecipientsForCreator(project, projectBridgehead); + case EMAIL_ANNOTATION -> fetchEmailRecipientsForEmailAnnotation(project, projectBridgehead, email); + case ALL_BRIDGEHEAD_ADMINS -> fetchEmailRecipientsForAllBridgeheadAdminsOfTheProject(project); + case ALL_DEVELOPERS -> fetchEmailRecipientsForAllDeveloperUsersOfTheProject(project); + case ALL_PILOTS -> fetchEmailRecipientsForAllPilotUsersOfTheProject(project); + case ALL_FINALS -> fetchEmailRecipientsForAllFinalUsersOfTheProject(project); + case BRIDGEHEAD_ADMIN -> fetchEmailRecipientsForBridgeheadAdmin(project, projectBridgehead); + case BRIDGEHEAD_ADMINS_WHO_HAVE_NOT_ACCEPTED_NOR_REJECTED_THE_PROJECT -> + fetchEmailRecipientsForBridgeheadAdminsWhoHaveNotAcceptedNorRejectedTheProject(project); + case PROJECT_MANAGER_ADMIN -> + fetchEmailRecipientsForProjectManagerAdmin(project, projectBridgehead); + case PROJECT_ALL -> fetchEmailRecipientsForAllProjectUsers(project, projectBridgehead); })); if (message.isPresent()) { result.forEach(emailRecipient -> emailRecipient.setMessage(message)); @@ -205,170 +207,171 @@ private Set fetchEmailRecipients(Supplier return result; } - private Set fetchEmailRecipientsForCreator(Optional projectCode, Optional bridgehead) { + private Set fetchEmailRecipientsForCreator(Optional project, Optional bridgehead) { Set result = new HashSet<>(); - if (projectCode.isPresent()) { - Optional project = this.projectRepository.findByCode(projectCode.get()); - if (project.isPresent()) { - result.add(new EmailRecipient(project.get().getCreatorEmail(), projectCode, bridgehead, ProjectRole.CREATOR)); - } + if (project.isPresent()) { + project.ifPresent(value -> result.add(new EmailRecipient(value.getCreatorEmail(), project, bridgehead, ProjectRole.CREATOR))); } return result; } - private Set fetchEmailRecipientsForAllDeveloperUsersOfTheProject(Optional projectCode) { - return fetchEmailRecipientsForAllDeveloperUsersOfTheProject(projectCode, ProjectRole.DEVELOPER); + private Set fetchEmailRecipientsForAllDeveloperUsersOfTheProject(Optional project) { + return fetchEmailRecipientsForAllDeveloperUsersOfTheProject(project, ProjectRole.DEVELOPER); } - private Set fetchEmailRecipientsForAllPilotUsersOfTheProject(Optional projectCode) { - return fetchEmailRecipientsForAllDeveloperUsersOfTheProject(projectCode, ProjectRole.PILOT); + private Set fetchEmailRecipientsForAllPilotUsersOfTheProject(Optional project) { + return fetchEmailRecipientsForAllDeveloperUsersOfTheProject(project, ProjectRole.PILOT); } - private Set fetchEmailRecipientsForAllFinalUsersOfTheProject(Optional projectCode) { - return fetchEmailRecipientsForAllDeveloperUsersOfTheProject(projectCode, ProjectRole.FINAL); + private Set fetchEmailRecipientsForAllFinalUsersOfTheProject(Optional project) { + return fetchEmailRecipientsForAllDeveloperUsersOfTheProject(project, ProjectRole.FINAL); } - private Set fetchEmailRecipientsForAllDeveloperUsersOfTheProject(Optional projectCode, ProjectRole projectRole) { + private Set fetchEmailRecipientsForAllDeveloperUsersOfTheProject(Optional project, ProjectRole projectRole) { Set result = new HashSet<>(); - if (projectCode.isPresent()) { - projectBridgeheadUserRepository.getDistinctByProjectRoleAndProjectCode(projectRole, projectCode.get()).stream() - .filter(projectBridgeheadUser -> !projectBridgeheadUser.getEmail().equals(sessionUser.getEmail())).forEach(projectBridgeheadUser -> - result.add(new EmailRecipient(projectBridgeheadUser.getEmail(), projectCode, Optional.of(projectBridgeheadUser.getProjectBridgehead().getBridgehead()), projectRole))); + if (project.isPresent()) { + projectBridgeheadUserService + .fetchUsers(projectRole, project.get()) + .stream() + .filter(projectBridgeheadUser -> !projectBridgeheadUser.getEmail().equals(sessionUser.getEmail())) + .forEach(projectBridgeheadUser -> + result.add( + new EmailRecipient( + projectBridgeheadUser.getEmail(), + project, + Optional.of(projectBridgeheadUser.getProjectBridgehead()), + projectRole))); } return result; } - private Set fetchEmailRecipientsForAllBridgeheadAdminsOfTheProject(Optional projectCode) { + private Set fetchEmailRecipientsForAllBridgeheadAdminsOfTheProject(Optional project) { Set result = new HashSet<>(); - if (projectCode.isPresent()) { - Optional project = this.projectRepository.findByCode(projectCode.get()); - if (project.isPresent()) { - this.projectBridgeheadRepository.findByProject(project.get()).forEach(projectBridgehead -> - this.bridgeheadAdminUserRepository.findByBridgehead(projectBridgehead.getBridgehead()).forEach(bridgeheadAdminUser -> - result.add(new EmailRecipient(bridgeheadAdminUser.getEmail(), projectCode, - Optional.of(projectBridgehead.getBridgehead()), ProjectRole.BRIDGEHEAD_ADMIN)))); - } + if (project.isPresent()) { + project.ifPresent(value -> projectBridgeheadService + .fetchBridgeheads(value) + .forEach(projectBridgehead -> userService + .fetchBridgeheadAdmin(projectBridgehead) + .forEach(bridgeheadAdminUser -> result.add( + new EmailRecipient( + bridgeheadAdminUser.getEmail(), + project, + Optional.of(projectBridgehead), + ProjectRole.BRIDGEHEAD_ADMIN))))); } return result; } - private Set fetchEmailRecipientsForSessionUser(Optional projectCode, Optional bridgehead) { + private Set fetchEmailRecipientsForSessionUser(Optional project, Optional bridgehead) { Set result = new HashSet<>(); - Optional userProjectRolesOptional = fetchSessionUserProjectRoles(projectCode); + Optional userProjectRolesOptional = fetchSessionUserProjectRoles(project); userProjectRolesOptional.ifPresent(userProjectRoles -> { ProjectRole projectRole = null; if (bridgehead.isPresent()) { - List bridgeheadRolesOrderedInTimeDescendent = userProjectRoles.getBridgeheadRolesOrderedInDescendentTime(bridgehead.get()); + List bridgeheadRolesOrderedInTimeDescendent = userProjectRoles.getBridgeheadRolesOrderedInDescendentTime(bridgehead.get().getBridgehead()); if (!bridgeheadRolesOrderedInTimeDescendent.isEmpty()) { - projectRole = bridgeheadRolesOrderedInTimeDescendent.get(0); + projectRole = bridgeheadRolesOrderedInTimeDescendent.getFirst(); } } if (projectRole == null) { Set rolesNotDependentOnBridgeheads = userProjectRoles.getRolesNotDependentOnBridgeheads(); if (!rolesNotDependentOnBridgeheads.isEmpty()) { - projectRole = rolesNotDependentOnBridgeheads.stream().toList().get(0); + projectRole = rolesNotDependentOnBridgeheads.stream().toList().getFirst(); } } if (projectRole == null) { projectRole = ProjectRole.DEFAULT; } - result.add(new EmailRecipient(sessionUser.getEmail(), projectCode, bridgehead, projectRole)); + result.add(new EmailRecipient(sessionUser.getEmail(), project, bridgehead, projectRole)); }); return result; } - private Optional fetchSessionUserProjectRoles(Optional projectCode) { - return (projectCode.isPresent()) ? organisationRoleToProjectRoleMapper.map(projectCode.get()) : Optional.empty(); + private Optional fetchSessionUserProjectRoles(Optional project) { + return (project.isPresent()) ? organisationRoleToProjectRoleMapper.map(project.get()) : Optional.empty(); } - private Set fetchEmailRecipientsForEmailAnnotation(Optional projectCode, Optional bridgehead, Optional email) { + private Set fetchEmailRecipientsForEmailAnnotation(Optional project, Optional bridgehead, Optional email) { Set result = new HashSet<>(); if (email.isPresent()) { ProjectRole projectRole = ProjectRole.DEFAULT; - if (projectCode.isPresent() && bridgehead.isPresent()) { - Optional projectBridgeheadUser = fetchProjectBridgeheadUser(projectCode.get(), bridgehead.get(), email.get()); + if (project.isPresent() && bridgehead.isPresent()) { + Optional projectBridgeheadUser = fetchProjectBridgeheadUser(bridgehead.get(), email.get()); if (projectBridgeheadUser.isPresent()) { projectRole = projectBridgeheadUser.get().getProjectRole(); } } - result.add(new EmailRecipient(email.get(), projectCode, bridgehead, projectRole)); + result.add(new EmailRecipient(email.get(), project, bridgehead, projectRole)); } return result; } - private Optional fetchProjectBridgeheadUser(String projectCode, String bridgehead, String email) { - Optional project = projectRepository.findByCode(projectCode); - if (project.isPresent()) { - Optional projectBridgehead = projectBridgeheadRepository.findFirstByBridgeheadAndProject(bridgehead, project.get()); - if (projectBridgehead.isPresent()) { - List projectBridgeheadUserList = - ProjectRolesUtils.orderCollectionInDescendentTime( - projectBridgeheadUserRepository.getByEmailAndProjectBridgehead(email, projectBridgehead.get()), - projectBridgeheadUser -> projectBridgeheadUser.getProjectRole()); - if (!projectBridgeheadUserList.isEmpty()) { - return Optional.of(projectBridgeheadUserList.get(0)); - } - } + private Optional fetchProjectBridgeheadUser(ProjectBridgehead bridgehead, String email) { + List projectBridgeheadUserList = + ProjectRolesUtils.orderCollectionInDescendentTime( + projectBridgeheadUserService.fetchUsers(email, bridgehead), + ProjectBridgeheadUser::getProjectRole); + if (!projectBridgeheadUserList.isEmpty()) { + return Optional.of(projectBridgeheadUserList.getFirst()); + } else { + return Optional.empty(); } - return Optional.empty(); } - private Set fetchEmailRecipientsForBridgeheadAdmin(Optional projectCode, Optional bridgehead) { + private Set fetchEmailRecipientsForBridgeheadAdmin(Optional project, Optional bridgehead) { Set result = new HashSet<>(); - fetchProjectBridgeheads(projectCode, bridgehead).forEach(projectBridgehead -> - bridgeheadAdminUserRepository.findByBridgehead(projectBridgehead.getBridgehead()).forEach(bridgeheadAdminUser -> - result.add(new EmailRecipient(bridgeheadAdminUser.getEmail(), projectCode, Optional.of(projectBridgehead.getBridgehead()), ProjectRole.BRIDGEHEAD_ADMIN)))); + fetchProjectBridgeheads(project, bridgehead) + .forEach(projectBridgehead -> userService + .fetchBridgeheadAdmin(projectBridgehead) + .forEach(bridgeheadAdminUser -> result.add( + new EmailRecipient( + bridgeheadAdminUser.getEmail(), + project, + Optional.of(projectBridgehead), + ProjectRole.BRIDGEHEAD_ADMIN)))); return result; } - private Set fetchEmailRecipientsForBridgeheadAdminsWhoHaveNotAcceptedNorRejectedTheProject(Optional projectCode) { + private Set fetchEmailRecipientsForBridgeheadAdminsWhoHaveNotAcceptedNorRejectedTheProject(Optional project) { Set result = new HashSet<>(); - fetchProjectBridgeheadsNotAcceptedNorRejected(projectCode).forEach(projectBridgehead -> - bridgeheadAdminUserRepository.findByBridgehead(projectBridgehead.getBridgehead()).forEach(bridgeheadAdminUser -> - result.add(new EmailRecipient(bridgeheadAdminUser.getEmail(), projectCode, Optional.of(projectBridgehead.getBridgehead()), ProjectRole.BRIDGEHEAD_ADMIN)))); + fetchProjectBridgeheadsNotAcceptedNorRejected(project) + .forEach(projectBridgehead -> userService + .fetchBridgeheadAdmin(projectBridgehead) + .forEach(bridgeheadAdminUser -> result.add( + new EmailRecipient( + bridgeheadAdminUser.getEmail(), + project, + Optional.of(projectBridgehead), + ProjectRole.BRIDGEHEAD_ADMIN)))); return result; } - private Set fetchProjectBridgeheadsNotAcceptedNorRejected(Optional projectCode) { - if (projectCode.isPresent()) { - Optional project = projectRepository.findByCode(projectCode.get()); - if (project.isPresent()) { - return projectBridgeheadRepository.findByProjectAndState(project.get(), ProjectBridgeheadState.CREATED); - } - } - return new HashSet<>(); + private Set fetchProjectBridgeheadsNotAcceptedNorRejected(Optional project) { + return project + .map(p -> projectBridgeheadService.fetchBridgeheads(p, ProjectBridgeheadState.CREATED)) + .orElse(Collections.emptySet()); } - private Set fetchProjectBridgeheads(Optional projectCode, Optional bridgehead) { - if (projectCode.isPresent()) { - Optional project = projectRepository.findByCode(projectCode.get()); - if (project.isPresent()) { - if (bridgehead.isPresent()) { - Optional projectBridgehead = projectBridgeheadRepository.findFirstByBridgeheadAndProject(bridgehead.get(), project.get()); - if (projectBridgehead.isPresent()) { - return Set.of(projectBridgehead.get()); - } - } else { - return projectBridgeheadRepository.findByProject(project.get()); - } - } + private Set fetchProjectBridgeheads(Optional project, Optional bridgehead) { + if (project.isEmpty()) { + return new HashSet<>(); } - return new HashSet<>(); + return bridgehead.map(Set::of).orElseGet(() -> projectBridgeheadService.fetchBridgeheads(project.get())); } - private Set fetchEmailRecipientsForProjectManagerAdmin(Optional projectCode, Optional bridgehead) { + private Set fetchEmailRecipientsForProjectManagerAdmin(Optional project, Optional bridgehead) { Set result = new HashSet<>(); - projectManagerAdminUserRepository.findAll().forEach(projectManagerAdminUser -> - result.add(new EmailRecipient(projectManagerAdminUser.getEmail(), projectCode, bridgehead, ProjectRole.PROJECT_MANAGER_ADMIN))); + userService.fetchAllProjectManagerAdmins().forEach(projectManagerAdminUser -> + result.add(new EmailRecipient(projectManagerAdminUser.getEmail(), project, bridgehead, ProjectRole.PROJECT_MANAGER_ADMIN))); return result; } - private Set fetchEmailRecipientsForAllProjectUsers(Optional projectCode, Optional bridgehead) { + private Set fetchEmailRecipientsForAllProjectUsers(Optional project, Optional bridgehead) { Map userEmailRecipientMap = new HashMap<>(); - fetchProjectBridgeheads(projectCode, bridgehead).forEach(projectBridgehead -> { - projectBridgeheadUserRepository.getByProjectBridgehead(projectBridgehead).forEach(projectBridgeheadUser -> { + fetchProjectBridgeheads(project, bridgehead).forEach(projectBridgehead -> { + projectBridgeheadUserService.fetchUsers(projectBridgehead).forEach(projectBridgeheadUser -> { boolean addUser; EmailRecipient emailRecipient = userEmailRecipientMap.get(projectBridgeheadUser.getEmail()); if (emailRecipient != null) { @@ -378,18 +381,26 @@ private Set fetchEmailRecipientsForAllProjectUsers(Optional { - if (!userEmailRecipientMap.containsKey(bridgeheadAdminUser.getEmail())) { - userEmailRecipientMap.put(bridgeheadAdminUser.getEmail(), new EmailRecipient(bridgeheadAdminUser.getEmail(), projectCode, Optional.of(bridgeheadAdminUser.getBridgehead()), ProjectRole.BRIDGEHEAD_ADMIN)); + new EmailRecipient(projectBridgeheadUser.getEmail(), project, Optional.of(projectBridgehead), projectBridgeheadUser.getProjectRole())); } }); + userService + .fetchBridgeheadAdmin(projectBridgehead) + .forEach(bridgeheadAdminUser -> { + if (!userEmailRecipientMap.containsKey(bridgeheadAdminUser.getEmail())) { + userEmailRecipientMap.put( + bridgeheadAdminUser.getEmail(), + new EmailRecipient( + bridgeheadAdminUser.getEmail(), + project, + project.flatMap(p -> projectBridgeheadService.fetchBridgehead(p, bridgeheadAdminUser.getBridgehead())), + ProjectRole.BRIDGEHEAD_ADMIN)); + } + }); }); - projectManagerAdminUserRepository.findAll().forEach(projectManagerAdminUser -> { + userService.fetchAllProjectManagerAdmins().forEach(projectManagerAdminUser -> { if (!userEmailRecipientMap.containsKey(projectManagerAdminUser.getEmail())) { - userEmailRecipientMap.put(projectManagerAdminUser.getEmail(), new EmailRecipient(projectManagerAdminUser.getEmail(), projectCode, Optional.empty(), ProjectRole.PROJECT_MANAGER_ADMIN)); + userEmailRecipientMap.put(projectManagerAdminUser.getEmail(), new EmailRecipient(projectManagerAdminUser.getEmail(), project, Optional.empty(), ProjectRole.PROJECT_MANAGER_ADMIN)); } }); return new HashSet<>(userEmailRecipientMap.values()); diff --git a/src/main/java/de/samply/aop/NotEmptyAspect.java b/src/main/java/de/samply/aop/NotEmptyAspect.java index 2edaf23c..c3edd384 100644 --- a/src/main/java/de/samply/aop/NotEmptyAspect.java +++ b/src/main/java/de/samply/aop/NotEmptyAspect.java @@ -18,6 +18,7 @@ @Aspect public class NotEmptyAspect { + @SuppressWarnings("EmptyMethod") @Pointcut("execution(* *(.., @de.samply.annotations.NotEmpty (*), ..))") public void notEmptyPointcut() { } diff --git a/src/main/java/de/samply/aop/ProjectConstraintsAspect.java b/src/main/java/de/samply/aop/ProjectConstraintsAspect.java index bfe36239..c8aecc1d 100644 --- a/src/main/java/de/samply/aop/ProjectConstraintsAspect.java +++ b/src/main/java/de/samply/aop/ProjectConstraintsAspect.java @@ -1,6 +1,7 @@ package de.samply.aop; import de.samply.annotations.ProjectConstraints; +import de.samply.db.model.Project; import de.samply.utils.AspectUtils; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; @@ -22,6 +23,7 @@ public ProjectConstraintsAspect(ConstraintsService constraintsService) { this.constraintsService = constraintsService; } + @SuppressWarnings("EmptyMethod") @Pointcut("@annotation(de.samply.annotations.ProjectConstraints)") public void projectConstraintsPointcut() { } @@ -29,8 +31,8 @@ public void projectConstraintsPointcut() { @Around("projectConstraintsPointcut()") public Object aroundProjectConstraints(ProceedingJoinPoint joinPoint) throws Throwable { Optional projectConstraints = fetchProjectConstrains(joinPoint); - Optional projectCode = AspectUtils.fetchProjectCode(joinPoint); - Optional result = this.constraintsService.checkProjectConstraints(projectConstraints, projectCode); + Optional project = AspectUtils.fetchProject(joinPoint); + @SuppressWarnings("rawtypes") Optional result = this.constraintsService.checkProjectConstraints(projectConstraints, project); return (result.isEmpty()) ? joinPoint.proceed() : result.get(); } diff --git a/src/main/java/de/samply/aop/ProjectRoleConstraintsAspect.java b/src/main/java/de/samply/aop/RoleConstraintsAspect.java similarity index 71% rename from src/main/java/de/samply/aop/ProjectRoleConstraintsAspect.java rename to src/main/java/de/samply/aop/RoleConstraintsAspect.java index 3d15270f..7e499f6f 100644 --- a/src/main/java/de/samply/aop/ProjectRoleConstraintsAspect.java +++ b/src/main/java/de/samply/aop/RoleConstraintsAspect.java @@ -2,6 +2,8 @@ import de.samply.annotations.RoleConstraints; import de.samply.annotations.StateConstraints; +import de.samply.db.model.Project; +import de.samply.db.model.ProjectBridgehead; import de.samply.utils.AspectUtils; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; @@ -15,11 +17,11 @@ @Component @Aspect -public class ProjectRoleConstraintsAspect { +public class RoleConstraintsAspect { - private ConstraintsService constraintsService; + private final ConstraintsService constraintsService; - public ProjectRoleConstraintsAspect(ConstraintsService constraintsService) { + public RoleConstraintsAspect(ConstraintsService constraintsService) { this.constraintsService = constraintsService; } @@ -27,13 +29,14 @@ public ProjectRoleConstraintsAspect(ConstraintsService constraintsService) { public void roleConstraintsPointcut() { } + @SuppressWarnings("rawtypes") // For Optional. Otherwise, it would be too complex @Around("roleConstraintsPointcut()") public Object aroundRoleConstraints(ProceedingJoinPoint joinPoint) throws Throwable { Optional roleConstraints = fetchRoleConstrains(joinPoint); Optional stateConstraints = fetchStateConstrains(joinPoint); - Optional projectCode = AspectUtils.fetchProjectCode(joinPoint); - Optional bridgehead = AspectUtils.fetchBridgehead(joinPoint); - Optional result = this.constraintsService.checkProjectRoleConstraints(roleConstraints, stateConstraints, projectCode, bridgehead); + Optional project = AspectUtils.fetchProject(joinPoint); + Optional bridgehead = AspectUtils.fetchBridgehead(joinPoint); + Optional result = this.constraintsService.checkRoleConstraints(roleConstraints, stateConstraints, project, bridgehead); return (result.isEmpty()) ? joinPoint.proceed() : result.get(); } diff --git a/src/main/java/de/samply/aop/StateConstraintsAspect.java b/src/main/java/de/samply/aop/StateConstraintsAspect.java index adf45e6d..02b7f036 100644 --- a/src/main/java/de/samply/aop/StateConstraintsAspect.java +++ b/src/main/java/de/samply/aop/StateConstraintsAspect.java @@ -1,6 +1,8 @@ package de.samply.aop; import de.samply.annotations.StateConstraints; +import de.samply.db.model.Project; +import de.samply.db.model.ProjectBridgehead; import de.samply.utils.AspectUtils; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; @@ -22,6 +24,7 @@ public StateConstraintsAspect(ConstraintsService constraintsService) { this.constraintsService = constraintsService; } + @SuppressWarnings("EmptyMethod") @Pointcut("@annotation(de.samply.annotations.StateConstraints)") public void stateConstraintsPointcut() { } @@ -29,9 +32,9 @@ public void stateConstraintsPointcut() { @Around("stateConstraintsPointcut()") public Object aroundStateConstraints(ProceedingJoinPoint joinPoint) throws Throwable { Optional stateConstraints = fetchStateConstraints(joinPoint); - Optional projectCode = AspectUtils.fetchProjectCode(joinPoint); - Optional bridghead = AspectUtils.fetchBridgehead(joinPoint); - Optional result = this.constraintsService.checkStateConstraints(stateConstraints, projectCode, bridghead); + Optional project = AspectUtils.fetchProject(joinPoint); + Optional bridgehead = AspectUtils.fetchBridgehead(joinPoint); + @SuppressWarnings("rawtypes") Optional result = this.constraintsService.checkStateConstraints(stateConstraints, project, bridgehead); return (result.isEmpty()) ? joinPoint.proceed() : result.get(); } diff --git a/src/main/java/de/samply/aop/ValidEmailAspect.java b/src/main/java/de/samply/aop/ValidEmailAspect.java index b1a1e422..c443b987 100644 --- a/src/main/java/de/samply/aop/ValidEmailAspect.java +++ b/src/main/java/de/samply/aop/ValidEmailAspect.java @@ -1,7 +1,6 @@ package de.samply.aop; import de.samply.annotations.Email; -import de.samply.annotations.NotEmpty; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; @@ -13,12 +12,11 @@ import java.lang.annotation.Annotation; import java.util.Optional; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Pattern; import java.util.stream.IntStream; -// It checks if the parameters marked with the annotation @Email of a REST service, are valid emails. +// It checks if the parameters marked with the annotation @Email of a REST service are valid emails. // If not, it returns BAD REQUEST @Component @Aspect @@ -28,6 +26,7 @@ public class ValidEmailAspect { "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}$" ); + @SuppressWarnings("EmptyMethod") @Pointcut("execution(* *(.., @de.samply.annotations.Email (*), ..))") public void emailPointcut() { } diff --git a/src/main/java/de/samply/app/ProjectManagerApplication.java b/src/main/java/de/samply/app/ProjectManagerApplication.java index f012404e..b2169b7e 100644 --- a/src/main/java/de/samply/app/ProjectManagerApplication.java +++ b/src/main/java/de/samply/app/ProjectManagerApplication.java @@ -1,8 +1,9 @@ package de.samply.app; +import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.boot.persistence.autoconfigure.EntityScan; import org.springframework.context.annotation.ComponentScan; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.scheduling.annotation.EnableScheduling; @@ -12,9 +13,10 @@ @EntityScan(basePackages = {"de.samply.db.model"}) @EnableJpaRepositories("de.samply.db.repository") @EnableScheduling +@EnableSchedulerLock(defaultLockAtMostFor = ProjectManagerConst.SHED_LOCK_MAX_TIME_SV) public class ProjectManagerApplication { - public static void main(String[] args) { + static void main(String[] args) { SpringApplication.run(ProjectManagerApplication.class, args); } diff --git a/src/main/java/de/samply/app/ProjectManagerConst.java b/src/main/java/de/samply/app/ProjectManagerConst.java index 26151832..8ad07f84 100644 --- a/src/main/java/de/samply/app/ProjectManagerConst.java +++ b/src/main/java/de/samply/app/ProjectManagerConst.java @@ -2,25 +2,10 @@ public class ProjectManagerConst { - public final static String APP_NAME = "Project Manager"; - - // R-Studio Group Implementations - public final static String RSTUDIO_GROUP_KEYCLOAK_IMPLEMENTATION = "KEYCLOAK"; - - // Keycloak paths - public final static String FETCH_USER_ID_KEYCLOAK_PATH = "/admin/realms/{realm}/users?email={email}"; - public final static String FETCH_GROUP_ID_KEYCLOAK_PATH = "/admin/realms/{realm}/groups?search={group}"; - public final static String CHANGE_USER_GROUP_KEYCLOAK_PATH = "/admin/realms/{realm}/users/{user-id}/groups/{group-id}"; - public final static String FETCH_TOKEN_KEYCLOAK_PATH = "/realms/{realm}/protocol/openid-connect/token"; - - // Keycloak parameters - public final static String CLIENT_ID_KEYCLOAK_PARAM = "client_id"; - public final static String CLIENT_SECRET_KEYCLOAK_PARAM = "client_secret"; - public final static String GRANT_TYPE_KEYCLOAK_PARAM = "grant_type"; - public final static String CLIENT_CREDENTIALS_KEYCLOAK_CONST = "client_credentials"; - public final static String ACCES_TOKEN_KEYCLOAK_CONST = "access_token"; - public final static String ID_KEYCLOAK_CONST = "id"; + public final static String APP_NAME = "ProjectCode Manager"; + // Profiles + public final static String EXTERNAL_EXECUTION_NOT_ALLOWED = "external-execution-not-allowed"; // Sites public final static String PROJECT_DASHBOARD_SITE = "project-dashboard"; @@ -54,33 +39,42 @@ public class ProjectManagerConst { public final static String START_PILOT_STAGE_ACTION = "START_PILOT_STAGE"; public final static String START_FINAL_STAGE_ACTION = "START_FINAL_STAGE"; public final static String FINISH_PROJECT_ACTION = "FINISH_PROJECT"; - public final static String DOWNLOAD_APPLICATION_FORM_TEMPLATE_ACTION = "DOWNLOAD_APPLICATION_FORM_TEMPLATE"; public final static String SAVE_QUERY_IN_BRIDGEHEAD_ACTION = "SAVE_QUERY_IN_BRIDGEHEAD"; public final static String SAVE_AND_EXECUTE_QUERY_IN_BRIDGEHEAD_ACTION = "SAVE_AND_EXECUTE_QUERY_IN_BRIDGEHEAD"; public final static String DOWNLOAD_AUTHENTICATION_SCRIPT_ACTION = "DOWNLOAD_AUTHENTICATION_SCRIPT"; public final static String EDIT_PROJECT_ACTION = "EDIT_PROJECT"; + public final static String REMOVE_PROJECT_OUTPUT_ACTION = "REMOVE_PROJECT_OUTPUT"; public final static String FETCH_EXPORTER_TEMPLATES_ACTION = "EXPORTER_TEMPLATES"; public final static String FETCH_QUERY_FORMATS_ACTION = "FETCH_QUERY_FORMATS"; public final static String FETCH_OUTPUT_FORMATS_ACTION = "FETCH_OUTPUT_FORMATS"; public final static String UPLOAD_VOTUM_ACTION = "UPLOAD_VOTUM"; + public final static String UPLOAD_DESCRIPTION_ACTION = "UPLOAD_DESCRIPTION"; + public final static String UPLOAD_ETHICS_APPROVAL_ACTION = "UPLOAD_ETHICS_APPROVAL"; public final static String UPLOAD_VOTUM_FOR_ALL_BRIDGEHEADS_ACTION = "UPLOAD_VOTUM_FOR_ALL_BRIDGEHEADS"; - public final static String UPLOAD_APPLICATION_FORM_ACTION = "UPLOAD_APPLICATION_FORM"; public final static String UPLOAD_PUBLICATION_ACTION = "UPLOAD_PUBLICATION"; + public final static String UPLOAD_FINAL_REPORT_ACTION = "UPLOAD_FINAL_REPORT"; public final static String UPLOAD_SCRIPT_ACTION = "UPLOAD_SCRIPT"; public final static String UPLOAD_OTHER_DOCUMENT_ACTION = "UPLOAD_OTHER_DOCUMENT"; public final static String ADD_PUBLICATION_URL_ACTION = "ADD_PUBLICATION_URL"; + public final static String ADD_FINAL_REPORT_URL_ACTION = "ADD_FINAL_REPORT_URL"; public final static String ADD_OTHER_DOCUMENT_URL_ACTION = "ADD_OTHER_DOCUMENT_URL"; + public final static String DOWNLOAD_DESCRIPTION_ACTION = "DOWNLOAD_DESCRIPTION"; + public final static String DOWNLOAD_ETHICS_APPROVAL_ACTION = "DOWNLOAD_ETHICS_APPROVAL"; + public final static String EXISTS_ETHICS_APPROVAL_ACTION = "EXISTS_ETHICS_APPROVAL"; + public final static String FETCH_ETHICS_APPROVAL_DESCRIPTION_ACTION = "FETCH_ETHICS_APPROVAL_DESCRIPTION"; public final static String DOWNLOAD_VOTUM_ACTION = "DOWNLOAD_VOTUM"; public final static String DOWNLOAD_VOTUM_FOR_ALL_BRIDGEHEADS_ACTION = "DOWNLOAD_VOTUM_FOR_ALL_BRIDGEHEADS"; public final static String FETCH_VOTUM_DESCRIPTION_ACTION = "FETCH_VOTUM_DESCRIPTION"; + public final static String FETCH_DESCRIPTION_ACTION = "FETCH_DESCRIPTION"; public final static String FETCH_VOTUM_FOR_ALL_BRIDGEHEADS_DESCRIPTION_ACTION = "FETCH_VOTUM_FOR_ALL_BRIDGEHEADS_DESCRIPTION"; public final static String EXISTS_VOTUM_ACTION = "EXISTS_VOTUM"; + public final static String EXISTS_PUBLICATION_ACTION = "EXISTS_PUBLICATION"; + public final static String EXISTS_FINAL_REPORT_ACTION = "EXISTS_FINAL_REPORT"; + public final static String EXISTS_DESCRIPTION_ACTION = "EXISTS_DESCRIPTION"; public final static String EXISTS_VOTUM_FOR_ALL_BRIDGEHEADS_ACTION = "EXISTS_VOTUM_FOR_ALL_BRIDGEHEADS"; - public final static String DOWNLOAD_APPLICATION_FORM_ACTION = "DOWNLOAD_APPLICATION_FORM"; - public final static String FETCH_APPLICATION_FORM_DESCRIPTION_ACTION = "FETCH_APPLICATION_FORM_DESCRIPTION"; - public final static String EXISTS_APPLICATION_FORM_ACTION = "EXISTS_APPLICATION_FORM"; public final static String EXISTS_SCRIPT_ACTION = "EXISTS_SCRIPT"; public final static String DOWNLOAD_PUBLICATION_ACTION = "DOWNLOAD_PUBLICATION"; + public final static String DOWNLOAD_FINAL_REPORT_ACTION = "DOWNLOAD_FINAL_REPORT"; public final static String DOWNLOAD_SCRIPT_ACTION = "DOWNLOAD_SCRIPT"; public final static String FETCH_SCRIPT_DESCRIPTION_ACTION = "FETCH_SCRIPT_DESCRIPTION"; public final static String DOWNLOAD_OTHER_DOCUMENT_ACTION = "DOWNLOAD_OTHER_DOCUMENT"; @@ -93,6 +87,7 @@ public class ProjectManagerConst { public final static String FETCH_PROJECT_TYPES_ACTION = "FETCH_PROJECT_TYPES"; public final static String FETCH_PROJECTS_ACTION = "FETCH_PROJECTS"; public final static String FETCH_PUBLICATIONS_ACTION = "FETCH_PUBLICATIONS"; + public final static String FETCH_FINAL_REPORTS_ACTION = "FETCH_FINAL_REPORTS"; public final static String FETCH_OTHER_DOCUMENTS_ACTION = "FETCH_OTHER_DOCUMENTS"; public final static String ACCEPT_PROJECT_RESULTS_ACTION = "ACCEPT_PROJECT_RESULTS"; public final static String REJECT_PROJECT_RESULTS_ACTION = "REJECT_PROJECT_RESULTS"; @@ -122,7 +117,7 @@ public class ProjectManagerConst { public final static String REMOVE_USER_FROM_MAILING_BLACK_LIST_ACTION = "REMOVE_USER_FROM_MAILING_BLACK_LIST"; public final static String FETCH_MAILING_BLACK_LIST_ACTION = "FETCH_MAILING_BLACK_LIST"; public final static String FETCH_USERS_FOR_AUTOCOMPLETE_IN_MAILING_BLACK_LIST_ACTION = "FETCH_USERS_FOR_AUTOCOMPLETE_IN_MAILING_BLACK_LIST"; - public final static String ADD_PROJECT_BRIDGHEAD_RESULTS_URL_ACTION = "ADD_PROJECT_BRIDGHEAD_RESULTS_URL"; + public final static String ADD_PROJECT_BRIDGEHEAD_RESULTS_URL_ACTION = "ADD_PROJECT_BRIDGEHEAD_RESULTS_URL"; public final static String ADD_PROJECT_RESULTS_URL_ACTION = "ADD_PROJECT_RESULTS_URL"; public final static String ACCEPT_PROJECT_RESULTS_URL_ACTION = "ACCEPT_PROJECT_RESULTS_URL"; public final static String REJECT_PROJECT_RESULTS_URL_ACTION = "REJECT_PROJECT_RESULTS_URL"; @@ -137,6 +132,15 @@ public class ProjectManagerConst { public final static String IS_PROJECT_MANAGER_ADMIN_ACTION = "IS_PROJECT_MANAGER_ADMIN"; public final static String FETCH_RESEARCH_ENVIRONMENT_URL_ACTION = "FETCH_RESEARCH_ENVIRONMENT_URL"; public final static String EXISTS_RESEARCH_ENVIRONMENT_WORKSPACE_ACTION = "EXISTS_RESEARCH_ENVIRONMENT_WORKSPACE"; + public final static String FETCH_PROJECT_FORM_TITLES_ACTION = "FETCH_PROJECT_FORM_TITLES"; + public final static String FETCH_PROJECT_FORM_FIELDS_ACTION = "FETCH_PROJECT_FORM_FIELDS"; + public final static String FETCH_PROJECT_FORM_TEMPLATES_ACTION = "FETCH_FORM_TEMPLATES"; + public final static String FETCH_BEST_PROJECT_FORM_TEMPLATES_ACTION = "FETCH_BEST_FORM_TEMPLATES"; + public final static String EDIT_PROJECT_FORM_FIELDS_ACTION = "EDIT_PROJECT_FORM_FIELDS"; + public final static String DOWNLOAD_FORM_AS_PDF_ACTION = "DOWNLOAD_FORM_AS_PDF"; + public final static String FETCH_SELECTED_PROJECT_FORMS_ACTION = "FETCH_SELECTED_PROJECT_FORMS"; + public final static String ADD_SELECTED_PROJECT_FORM_ACTION = "ADD_SELECTED_PROJECT_FORM"; + public final static String REMOVE_SELECTED_PROJECT_FORM_ACTION = "REMOVE_SELECTED_PROJECT_FORM"; // REST Services @@ -145,102 +149,121 @@ public class ProjectManagerConst { public final static String ACTIONS = "/actions"; public final static String ALL_ACTIONS = "/all-actions"; public final static String FETCH_PROJECTS = "/projects"; - public final static String SET_DEVELOPER_USER = "/set-developer-user"; - public final static String SET_PILOT_USER = "/set-pilot-user"; - public final static String SET_FINAL_USER = "/set-final-user"; + public final static String SET_DEVELOPER_USER = "/developer"; + public final static String SET_PILOT_USER = "/pilot-user"; + public final static String SET_FINAL_USER = "/final-user"; public final static String CREATE_QUERY_AND_DESIGN_PROJECT = "/create-query-and-design-project"; - public final static String EDIT_PROJECT = "/edit-project"; - public final static String FETCH_EXPORTER_TEMPLATES = "/exporter-templates"; - public final static String FETCH_QUERY_FORMATS = "/query-formats"; - public final static String FETCH_PROJECT_ROLES = "/project-roles"; - public final static String FETCH_OUTPUT_FORMATS = "/output-formats"; - public final static String FETCH_PROJECT_CONFIGURATIONS = "/project-configurations"; - public final static String FETCH_CURRENT_PROJECT_CONFIGURATION = "/project-configuration"; - public final static String SET_PROJECT_CONFIGURATION = "/project-configuration"; - public final static String DESIGN_PROJECT = "/design-project"; - public final static String CREATE_PROJECT = "/create-project"; - public final static String ACCEPT_PROJECT = "/accept-project"; - public final static String REJECT_PROJECT = "/reject-project"; - public final static String ADD_USER_TO_MAILING_BLACK_LIST = "/add-mailing-black-list"; - public final static String REMOVE_USER_FROM_MAILING_BLACK_LIST = "/remove-mailing-black-list"; + public final static String EDIT_PROJECT = "/project/edit"; + public final static String REMOVE_PROJECT_OUTPUT = "/project/output"; + public final static String FETCH_EXPORTER_TEMPLATES = "/exporter/templates"; + public final static String FETCH_QUERY_FORMATS = "/query/formats"; + public final static String FETCH_PROJECT_ROLES = "/project/roles"; + public final static String FETCH_OUTPUT_FORMATS = "/output/formats"; + public final static String FETCH_PROJECT_CONFIGURATIONS = "/project/configurations"; + public final static String FETCH_CURRENT_PROJECT_CONFIGURATION = "/project/configuration"; + public final static String SET_PROJECT_CONFIGURATION = "/project/configuration"; + public final static String DESIGN_PROJECT = "/project/design"; + public final static String CREATE_PROJECT = "/project"; + public final static String ACCEPT_PROJECT = "/project/accept"; + public final static String REJECT_PROJECT = "/project/reject"; + public final static String ADD_USER_TO_MAILING_BLACK_LIST = "/mailing-black-list"; + public final static String REMOVE_USER_FROM_MAILING_BLACK_LIST = "/mailing-black-list"; public final static String FETCH_MAILING_BLACK_LIST = "/mailing-black-list"; - public final static String FETCH_USERS_FOR_AUTOCOMPLETE_IN_MAILING_BLACK_LIST = "/mailing-black-list-autocomplete"; - public final static String ACCEPT_BRIDGEHEAD_PROJECT = "/accept-bridgehead-project"; - public final static String REJECT_BRIDGEHEAD_PROJECT = "/reject-bridgehead-project"; - public final static String ACCEPT_SCRIPT = "/accept-script"; - public final static String REJECT_SCRIPT = "/reject-script"; - public final static String REQUEST_SCRIPT_CHANGES = "/request-script-changes"; - public final static String ACCEPT_PROJECT_RESULTS = "/accept-project-results"; - public final static String REJECT_PROJECT_RESULTS = "/reject-project-results"; - public final static String REQUEST_CHANGES_IN_PROJECT = "/request-changes-in-project"; - public final static String ACCEPT_PROJECT_ANALYSIS = "/accept-project-analysis"; - public final static String REJECT_PROJECT_ANALYSIS = "/reject-project-analysis"; - public final static String REQUEST_CHANGES_IN_PROJECT_ANALYSIS = "/request-changes-in-project-analysis"; - public final static String FETCH_PROJECT_BRIDGEHEADS = "/project-bridgeheads"; - public final static String FETCH_VISIBLE_PROJECT_BRIDGEHEADS = "/visible-project-bridgeheads"; + public final static String FETCH_USERS_FOR_AUTOCOMPLETE_IN_MAILING_BLACK_LIST = "/mailing-black-list/autocomplete"; + public final static String ACCEPT_BRIDGEHEAD_PROJECT = "/project/bridgehead/accept"; + public final static String REJECT_BRIDGEHEAD_PROJECT = "/project/bridgehead/reject"; + public final static String ACCEPT_SCRIPT = "/script/accept"; + public final static String REJECT_SCRIPT = "/script/reject"; + public final static String REQUEST_SCRIPT_CHANGES = "/script/request-changes"; + public final static String ACCEPT_PROJECT_RESULTS = "/project/results/accept"; + public final static String REJECT_PROJECT_RESULTS = "/project/results/reject"; + public final static String REQUEST_CHANGES_IN_PROJECT = "/project/request-changes"; + public final static String ACCEPT_PROJECT_ANALYSIS = "/project/analysis/accept"; + public final static String REJECT_PROJECT_ANALYSIS = "/project/analysis/reject"; + public final static String REQUEST_CHANGES_IN_PROJECT_ANALYSIS = "/project/analysis/request-changes"; + public final static String FETCH_PROJECT_BRIDGEHEADS = "/project/bridgeheads"; + public final static String FETCH_VISIBLE_PROJECT_BRIDGEHEADS = "/project/bridgeheads/visible"; public final static String FETCH_PROJECT = "/project"; - public final static String FETCH_PROJECT_STATES = "/project-states"; - public final static String ARCHIVE_PROJECT = "/archive-project"; - public final static String START_DEVELOP_STAGE = "/start-develop-project"; - public final static String START_PILOT_STAGE = "/start-pilot-project"; - public final static String START_FINAL_STAGE = "/start-final-project"; - public final static String FINISH_PROJECT = "/finish-project"; - public final static String CREATE_QUERY = "/create-query"; - public final static String FETCH_PROJECT_TYPES = "/project-types"; - public final static String UPLOAD_VOTUM = "/upload-votum"; - public final static String UPLOAD_VOTUM_FOR_ALL_BRIDGEHEADS = "/upload-votum-for-all-bridgeheads"; - public final static String UPLOAD_APPLICATION_FORM = "/upload-application-form"; - public final static String UPLOAD_PUBLICATION = "/upload-publication"; - public final static String UPLOAD_SCRIPT = "/upload-script"; - public final static String UPLOAD_OTHER_DOCUMENT = "/upload-other-document"; - public final static String ADD_PUBLICATION_URL = "/add-publication-url"; - public final static String ADD_OTHER_DOCUMENT_URL = "/add-other-document-url"; - public final static String DOWNLOAD_VOTUM = "/download-votum"; - public final static String DOWNLOAD_VOTUM_FOR_ALL_BRIDGEHEADS = "/download-votum-for-all-bridgeheads"; - public final static String FETCH_VOTUM_DESCRIPTION = "/votum-description"; - public final static String FETCH_VOTUM_FOR_ALL_BRIDGEHEADS_DESCRIPTION = "/votum-description-for-all-bridgeheads"; - public final static String EXISTS_VOTUM = "/exists-votum"; - public final static String EXISTS_VOTUM_FOR_ALL_BRIDGEHEADS = "/exists-votum-for-all-bridgeheads"; - public final static String DOWNLOAD_APPLICATION_FORM = "/download-application-form"; - public final static String FETCH_APPLICATION_FORM_DESCRIPTION = "/application-form-description"; - public final static String EXISTS_APPLICATION_FORM = "/exists-application-form"; - public final static String DOWNLOAD_PUBLICATION = "/download-publication"; - public final static String DOWNLOAD_SCRIPT = "/download-script"; - public final static String FETCH_SCRIPT_DESCRIPTION = "/script-description"; - public final static String EXISTS_SCRIPT = "/exists-script"; - public final static String DOWNLOAD_OTHER_DOCUMENT = "/download-other-document"; - public final static String DOWNLOAD_APPLICATION_FORM_TEMPLATE = "/download-application-form-template"; - public final static String SAVE_QUERY_IN_BRIDGEHEAD = "/save-query-in-bridgehead"; - public final static String SAVE_AND_EXECUTE_QUERY_IN_BRIDGEHEAD = "/save-and-execute-query-in-bridgehead"; - public final static String DOWNLOAD_AUTHENTICATION_SCRIPT = "/download-authentication-script"; - public final static String SEND_EXPORT_FILES_TO_RESEARCH_ENVIRONMENT = "/send-export-files-to-research-environment"; - public final static String ARE_EXPORT_FILES_TRANSFERRED_TO_RESEARCH_ENVIRONMENT = "/export-files-transferred-to-research-environment"; - public final static String EXISTS_AUTHENTICATION_SCRIPT = "/exists-authentication-script"; + public final static String FETCH_PROJECT_STATES = "/project/states"; + public final static String ARCHIVE_PROJECT = "/project/archive"; + public final static String START_DEVELOP_STAGE = "/project/start-develop"; + public final static String START_PILOT_STAGE = "/project/start-pilot"; + public final static String START_FINAL_STAGE = "/project/start-final"; + public final static String FINISH_PROJECT = "/project/finish"; + public final static String CREATE_QUERY = "/query"; + public final static String FETCH_PROJECT_TYPES = "/project/types"; + public final static String UPLOAD_VOTUM = "/votum/upload"; + public final static String UPLOAD_DESCRIPTION = "/project/description/upload"; + public final static String UPLOAD_ETHICS_APPROVAL = "/project/ethics-approval/upload"; + public final static String DOWNLOAD_ETHICS_APPROVAL = "/project/ethics-approval/download"; + public final static String EXISTS_ETHICS_APPROVAL = "/project/ethics-approval/exists"; + public final static String FETCH_ETHICS_APPROVAL_DESCRIPTION = "/project/ethics-approval/description"; + public final static String UPLOAD_VOTUM_FOR_ALL_BRIDGEHEADS = "/all-bridgeheads/votum/upload"; + public final static String UPLOAD_PUBLICATION = "/publication/upload"; + public final static String UPLOAD_FINAL_REPORT = "/report/upload"; + public final static String UPLOAD_SCRIPT = "/script/upload"; + public final static String UPLOAD_OTHER_DOCUMENT = "/other-document/upload"; + public final static String ADD_PUBLICATION_URL = "/publication/url"; + public final static String ADD_FINAL_REPORT_URL = "/report/url"; + public final static String ADD_OTHER_DOCUMENT_URL = "/document/url"; + public final static String DOWNLOAD_VOTUM = "/votum/download"; + public final static String DOWNLOAD_DESCRIPTION = "/project/description/download"; + public final static String DOWNLOAD_VOTUM_FOR_ALL_BRIDGEHEADS = "/all-bridgeheads/votum/download"; + public final static String FETCH_VOTUM_DESCRIPTION = "/votum/description"; + public final static String FETCH_DESCRIPTION = "/project/description"; + public final static String FETCH_VOTUM_FOR_ALL_BRIDGEHEADS_DESCRIPTION = "/all-bridgeheads/votum/description"; + public final static String EXISTS_VOTUM = "/votum/exists"; + public final static String EXISTS_PUBLICATION = "/publication/exists"; + public final static String EXISTS_FINAL_REPORT = "/report/exists"; + public final static String EXISTS_DESCRIPTION = "/project/description/exists"; + public final static String EXISTS_VOTUM_FOR_ALL_BRIDGEHEADS = "/all-bridgeheads/votum/exists"; + public final static String DOWNLOAD_PUBLICATION = "/publication/download"; + public final static String DOWNLOAD_FINAL_REPORT = "/report/download"; + public final static String DOWNLOAD_SCRIPT = "/script/download"; + public final static String FETCH_SCRIPT_DESCRIPTION = "/script/description"; + public final static String EXISTS_SCRIPT = "/script/exists"; + public final static String DOWNLOAD_OTHER_DOCUMENT = "/other-document/download"; + public final static String SAVE_QUERY_IN_BRIDGEHEAD = "/project/bridgehead/query/save"; + public final static String SAVE_AND_EXECUTE_QUERY_IN_BRIDGEHEAD = "/project/bridgehead/query/save-execute"; + public final static String DOWNLOAD_AUTHENTICATION_SCRIPT = "/authentication-script/download"; + public final static String SEND_EXPORT_FILES_TO_RESEARCH_ENVIRONMENT = "/project/export-files/send"; + public final static String ARE_EXPORT_FILES_TRANSFERRED_TO_RESEARCH_ENVIRONMENT = "/project/export-files/status"; + public final static String EXISTS_AUTHENTICATION_SCRIPT = "/authentication-script/exists"; public final static String FETCH_DATASHIELD_STATUS = "/datashield-status"; public final static String FETCH_PUBLICATIONS = "/publications"; + public final static String FETCH_FINAL_REPORTS = "/reports"; public final static String FETCH_OTHER_DOCUMENTS = "/other-documents"; public final static String FETCH_NOTIFICATIONS = "/notifications"; - public final static String SET_NOTIFICATION_AS_READ = "/read-notification"; + public final static String SET_NOTIFICATION_AS_READ = "/notification/read"; public final static String FETCH_ALL_REGISTERED_BRIDGEHEADS = "/bridgeheads"; public final static String FETCH_USERS_FOR_AUTOCOMPLETE = "/autocomplete-users"; - public final static String FETCH_PROJECT_USERS = "/project-users"; + public final static String FETCH_PROJECT_USERS = "/project/users"; public final static String FETCH_CURRENT_USER = "/current-user"; - public final static String EXIST_INVITED_USERS = "/exist-invited-users"; - public final static String ADD_PROJECT_BRIDGHEAD_RESULTS_URL = "/project-bridgehead-results-url"; - public final static String ADD_PROJECT_RESULTS_URL = "/project-results-url"; - public final static String ACCEPT_PROJECT_RESULTS_URL = "/accept-project-results-url"; - public final static String REJECT_PROJECT_RESULTS_URL = "/reject-project-results-url"; - public final static String FETCH_PROJECT_RESULTS = "/project-results"; - public final static String REQUEST_CHANGES_IN_PROJECT_RESULTS_URL = "/request-changes-project-results-url"; - public final static String ACCEPT_PROJECT_BRIDGEHEAD_RESULTS_URL = "/accept-project-bridgehead-results-url"; - public final static String REJECT_PROJECT_BRIDGEHEAD_RESULTS_URL = "/reject-project-bridgehead-results-url"; - public final static String REQUEST_CHANGES_IN_PROJECT_BRIDGEHEAD_RESULTS_URL = "/request-changes-project-bridgehead-results-url"; - public final static String FETCH_PROJECT_BRIDGEHEAD_RESULTS = "/project-bridgehead-results"; - public final static String FETCH_PROJECT_BRIDGEHEAD_RESULTS_FOR_OWN_BRIDGEHEAD = "/own-project-bridgehead-results"; + public final static String EXIST_INVITED_USERS = "/invited-users/exists"; + public final static String ADD_PROJECT_BRIDGEHEAD_RESULTS_URL = "/project/bridgehead/results/url"; + public final static String ADD_PROJECT_RESULTS_URL = "/project/results/url"; + public final static String ACCEPT_PROJECT_RESULTS_URL = "/project/results/accept-url"; + public final static String REJECT_PROJECT_RESULTS_URL = "/project/results/reject-url"; + public final static String FETCH_PROJECT_RESULTS = "/project/results"; + public final static String REQUEST_CHANGES_IN_PROJECT_RESULTS_URL = "/project/results/url/request-changes"; + public final static String ACCEPT_PROJECT_BRIDGEHEAD_RESULTS_URL = "/project/bridgehead/results/url/accept"; + public final static String REJECT_PROJECT_BRIDGEHEAD_RESULTS_URL = "/project/bridgehead/results/url/reject"; + public final static String REQUEST_CHANGES_IN_PROJECT_BRIDGEHEAD_RESULTS_URL = "/project/bridgehead/results/url/request-changes"; + public final static String FETCH_PROJECT_BRIDGEHEAD_RESULTS = "/project/bridgehead/results"; + public final static String FETCH_PROJECT_BRIDGEHEAD_RESULTS_FOR_OWN_BRIDGEHEAD = "/project/bridgeheads/own/results"; public final static String FETCH_EMAIL_MESSAGE_AND_SUBJECT = "/email-message-and-subject"; public final static String IS_PROJECT_MANAGER_ADMIN = "/is-project-manager-admin"; public final static String FETCH_RESEARCH_ENVIRONMENT_URL = "/research-environment-url"; - public final static String EXISTS_RESEARCH_ENVIRONMENT_WORKSPACE = "/exists-research-environment-workspace"; + public final static String EXISTS_RESEARCH_ENVIRONMENT_WORKSPACE = "/research-environment-workspace/exists"; + public final static String FETCH_PROJECT_FORM_TITLES = "/project/forms/titles"; + public final static String FETCH_PROJECT_FORM_FIELDS = "/project/forms/fields"; + public final static String FETCH_PROJECT_FORM_TEMPLATES = "/project/forms/templates"; + public final static String FETCH_BEST_PROJECT_FORM_TEMPLATES = "/project/forms/templates/best"; + public final static String EDIT_PROJECT_FORM_FIELDS = "/project/forms/fields"; + public final static String DOWNLOAD_FORM_AS_PDF = "/download-form-as-pdf"; + public final static String FETCH_SELECTED_PROJECT_FORMS = "/project/forms/selected"; + public final static String ADD_SELECTED_PROJECT_FORM = "/project/forms/selected"; + public final static String REMOVE_SELECTED_PROJECT_FORM = "/project/forms/selected"; // REST Parameters @@ -255,6 +278,7 @@ public class ProjectManagerConst { public final static String PARTIAL_EMAIL = "partial-email"; public final static String SITE = "site"; public final static String EMAIL = "email"; + public final static String QUERY = "query"; public final static String QUERY_FORMAT = "query-format"; public final static String PROJECT_TYPE = "project-type"; public final static String PROJECT_STATE = "project-state"; @@ -267,11 +291,16 @@ public class ProjectManagerConst { public final static String FILENAME = "filename"; public final static String QUERY_CODE = "query-code"; public final static String LABEL = "label"; + public final static String FORM_TITLE = "form-title"; + public final static String FORM_FIELDS = "form-fields"; + public final static String FORM_TEMPLATE = "form-template"; public final static String DESCRIPTION = "description"; + public final static String COHORT_DEFINITION = "cohort-definition"; public final static String OUTPUT_FORMAT = "output-format"; public final static String TEMPLATE_ID = "template-id"; public final static String HUMAN_READABLE = "human-readable"; public final static String REDIRECT_EXPLORER_URL = "explorer-url"; + public final static String QUERY_DETAILS = "query-details"; public final static String QUERY_CONTEXT = "query-context"; public final static String RESULTS_URL = "results-url"; public final static String PROJECT_ROLE = "project-role"; @@ -281,7 +310,6 @@ public class ProjectManagerConst { public final static String EMAIL_CONTEXT_VARIABLE_TAG_ATTRIBUTE_DEFAULT_VARIABLE = "default-var"; // Application Properties - public final static String JWKS_URI_PROPERTY = "spring.security.oauth2.client.provider.oidc.jwk-set-uri"; public final static String REGISTERED_BRIDGEHEADS = "bridgeheads"; public final static String FRONTEND_CONFIG = "frontend"; public final static String HTTP_PROXY_PREFIX = "http.proxy"; @@ -350,8 +378,6 @@ public class ProjectManagerConst { public final static String BK_ADMIN_GROUP_PREFIX = "BK_ADMIN_GROUP_PREFIX"; public final static String BK_ADMIN_GROUP_SUFFIX = "BK_ADMIN_GROUP_SUFFIX"; public final static String PROJECT_DOCUMENTS_DIRECTORY = "PROJECT_DOCUMENTS_DIRECTORY"; - public final static String PUBLIC_DOCUMENTS_DIRECTORY = "PUBLIC_DOCUMENTS_DIRECTORY"; - public final static String APPLICATION_FORM_FILENAME = "APPLICATION_FORM_FILENAME"; public final static String PROJECT_DOCUMENTS_DIRECTORY_TIMESTAMP_FORMAT = "PROJECT_DOCUMENTS_DIRECTORY_TIMESTAMP"; public final static String WEBCLIENT_BUFFER_SIZE_IN_BYTES = "WEBCLIENT_BUFFER_SIZE_IN_BYTES"; public final static String WEBCLIENT_REQUEST_TIMEOUT_IN_SECONDS = "WEBCLIENT_REQUEST_TIMEOUT_IN_SECONDS"; @@ -367,6 +393,7 @@ public class ProjectManagerConst { public final static String ACTION_EXPLANATION_CONFIG_PATH = "ACTION_EXPLANATION_CONFIG_PATH"; public final static String EMAIL_TEMPLATES_DIRECTORY = "EMAIL_TEMPLATES_DIRECTORY"; public final static String EXPORT_TEMPLATES = "EXPORT_TEMPLATES"; + public final static String SAMPLES_TEMPLATES = "SAMPLES_TEMPLATES"; public final static String DATASHIELD_TEMPLATES = "DATASHIELD_TEMPLATES"; public final static String RESEARCH_ENVIRONMENT_TEMPLATES = "RESEARCH_ENVIRONMENT_TEMPLATES"; public final static String BEAM_PROJECT_MANAGER_ID = "BEAM_PROJECT_MANAGER_ID"; @@ -385,21 +412,16 @@ public class ProjectManagerConst { public final static String ENABLE_TOKEN_MANAGER = "ENABLE_TOKEN_MANAGER"; public final static String EXPLORER_REDIRECT_URI_PARAMETER = "EXPLORER_REDIRECT_URI_PARAMETER"; public final static String FRONTEND_PROJECT_CONFIG_PATH = "FRONTEND_PROJECT_CONFIG_PATH"; - public final static String RSTUDIO_GROUP_IMPLEMENTATION = "RSTUDIO_GROUP_IMPLEMENTATION"; - public final static String KEYCLOAK_RSTUDIO_GROUP_CLIENT_ID = "KEYCLOAK_RSTUDIO_GROUP_CLIENT_ID"; - public final static String KEYCLOAK_RSTUDIO_GROUP_CLIENT_SECRET = "KEYCLOAK_RSTUDIO_GROUP_CLIENT_SECRET"; - public final static String KEYCLOAK_RSTUDIO_GROUP = "KEYCLOAK_RSTUDIO_GROUP"; - public final static String ENABLE_RSTUDIO_GROUP_MANAGER = "ENABLE_RSTUDIO_GROUP_MANAGER"; - public final static String OIDC_URL = "OIDC_URL"; - public final static String OIDC_REALM = "OIDC_REALM"; public final static String ENABLE_EXPORTER = "ENABLE_EXPORTER"; public final static String MAX_TIME_TO_WAIT_FOCUS_TASK_IN_MINUTES = "MAX_TIME_TO_WAIT_FOCUS_TASK_IN_MINUTES"; public final static String DEFAULT_LANGUAGE = "DEFAULT_LANGUAGE"; + public final static String REQUEST_CACHE_LIMIT = "REQUEST_CACHE_LIMIT"; public final static String JWT_GROUPS_CLAIM = "JWT_GROUPS_CLAIM"; public final static String JWT_EMAIL_CLAIM = "JWT_EMAIL_CLAIM"; public final static String JWT_FIRST_NAME_CLAIM = "JWT_FIRST_NAME_CLAIM"; public final static String JWT_LAST_NAME_CLAIM = "JWT_LAST_NAME_CLAIM"; + public final static String JWT_FULL_NAME_CLAIM = "JWT_FULL_NAME_CLAIM"; public final static String CODER_BASE_URL = "CODER_BASE_URL"; public final static String CODER_ORGANISATION_ID = "CODER_ORGANISATION_ID"; @@ -436,6 +458,17 @@ public class ProjectManagerConst { public final static String APP_REGISTER_AUTHORIZATION_FORMAT = "APP_REGISTER_AUTHORIZATION_FORMAT"; public final static String ENABLE_APP_REGISTER = "ENABLE_APP_REGISTER"; + public final static String FORM_RESOURCES_DIRECTORY = "FORM_RESOURCES_DIRECTORY"; + public final static String FORM_TEMPLATE_METADATA_DIRECTORY = "FORM_TEMPLATE_METADATA_DIRECTORY"; + public final static String FORM_TEMPLATE_DEFAULT_PDF_FILENAME = "FORM_TEMPLATE_DEFAULT_PDF_FILENAME"; + public final static String FORM_FIELDS_DIRECTORY = "FORM_FIELDS_DIRECTORY"; + public final static String FORM_TEMPLATE_DATE_PATTERN = "FORM_TEMPLATE_DATE_PATTERN"; + + public final static String SHED_LOCK_MAX_TIME = "SHED_LOCK_MAX_TIME"; + public final static String TIME_BETWEEN_CHECK_EXPORTS_IN_SECONDS = "TIME_BETWEEN_CHECK_EXPORTS_IN_SECONDS"; + public final static String MAX_NUMBER_OF_RETRIES_BETWEEN_CHECK_EXPORTS = "MAX_NUMBER_OF_RETRIES_BETWEEN_CHECK_EXPORTS"; + + // Spring Values (SV) public final static String HEAD_SV = "${"; public final static String BOTTOM_SV = "}"; @@ -444,14 +477,12 @@ public class ProjectManagerConst { public final static String JWT_EMAIL_CLAIM_SV = HEAD_SV + JWT_EMAIL_CLAIM + ":email" + BOTTOM_SV; public final static String JWT_FIRST_NAME_CLAIM_SV = HEAD_SV + JWT_FIRST_NAME_CLAIM + ":given_name" + BOTTOM_SV; public final static String JWT_LAST_NAME_CLAIM_SV = HEAD_SV + JWT_LAST_NAME_CLAIM + ":family_name" + BOTTOM_SV; - public final static String JWKS_URI_PROPERTY_SV = HEAD_SV + JWKS_URI_PROPERTY + BOTTOM_SV; + public final static String JWT_FULL_NAME_CLAIM_SV = HEAD_SV + JWT_FULL_NAME_CLAIM + ":name" + BOTTOM_SV; public final static String BK_USER_GROUP_PREFIX_SV = HEAD_SV + BK_USER_GROUP_PREFIX + BOTTOM_SV; public final static String BK_USER_GROUP_SUFFIX_SV = HEAD_SV + BK_USER_GROUP_SUFFIX + BOTTOM_SV; public final static String BK_ADMIN_GROUP_PREFIX_SV = HEAD_SV + BK_ADMIN_GROUP_PREFIX + BOTTOM_SV; public final static String BK_ADMIN_GROUP_SUFFIX_SV = HEAD_SV + BK_ADMIN_GROUP_SUFFIX + BOTTOM_SV; public final static String PROJECT_DOCUMENTS_DIRECTORY_SV = HEAD_SV + PROJECT_DOCUMENTS_DIRECTORY + BOTTOM_SV; - public final static String PUBLIC_DOCUMENTS_DIRECTORY_SV = HEAD_SV + PUBLIC_DOCUMENTS_DIRECTORY + BOTTOM_SV; - public final static String APPLICATION_FORM_FILENAME_SV = HEAD_SV + APPLICATION_FORM_FILENAME + BOTTOM_SV; public final static String PROJECT_DOCUMENTS_DIRECTORY_TIMESTAMP_FORMAT_SV = HEAD_SV + PROJECT_DOCUMENTS_DIRECTORY_TIMESTAMP_FORMAT + ":yyyyMMdd-HHmmss" + BOTTOM_SV; public final static String SECURITY_ENABLED_SV = HEAD_SV + SECURITY_ENABLED + ":true" + BOTTOM_SV; public final static String WEBCLIENT_BUFFER_SIZE_IN_BYTES_SV = @@ -480,6 +511,7 @@ public class ProjectManagerConst { public final static String ACTION_EXPLANATION_CONFIG_PATH_SV = HEAD_SV + ACTION_EXPLANATION_CONFIG_PATH + BOTTOM_SV; public final static String EMAIL_TEMPLATES_DIRECTORY_SV = HEAD_SV + EMAIL_TEMPLATES_DIRECTORY + BOTTOM_SV; public final static String EXPORT_TEMPLATES_SV = HEAD_SV + EXPORT_TEMPLATES + BOTTOM_SV; + public final static String SAMPLES_TEMPLATES_SV = HEAD_SV + SAMPLES_TEMPLATES + BOTTOM_SV; public final static String DATASHIELD_TEMPLATES_SV = HEAD_SV + DATASHIELD_TEMPLATES + BOTTOM_SV; public final static String RESEARCH_ENVIRONMENT_TEMPLATES_SV = HEAD_SV + RESEARCH_ENVIRONMENT_TEMPLATES + BOTTOM_SV; public final static String TOKEN_MANAGER_URL_SV = HEAD_SV + TOKEN_MANAGER_URL + BOTTOM_SV; @@ -503,16 +535,8 @@ public class ProjectManagerConst { public final static String EXPLORER_URL_SV = HEAD_SV + EXPLORER_URL + BOTTOM_SV; public final static String EXPLORER_REDIRECT_URI_PARAMETER_SV = HEAD_SV + EXPLORER_REDIRECT_URI_PARAMETER + BOTTOM_SV; public final static String FRONTEND_PROJECT_CONFIG_PATH_SV = HEAD_SV + FRONTEND_PROJECT_CONFIG_PATH + BOTTOM_SV; - public final static String RSTUDIO_GROUP_IMPLEMENTATION_SV = HEAD_SV + RSTUDIO_GROUP_IMPLEMENTATION + ":" + RSTUDIO_GROUP_KEYCLOAK_IMPLEMENTATION + BOTTOM_SV; - public final static String KEYCLOAK_RSTUDIO_GROUP_CLIENT_ID_SV = HEAD_SV + KEYCLOAK_RSTUDIO_GROUP_CLIENT_ID + BOTTOM_SV; - public final static String KEYCLOAK_RSTUDIO_GROUP_CLIENT_SECRET_SV = HEAD_SV + KEYCLOAK_RSTUDIO_GROUP_CLIENT_SECRET + BOTTOM_SV; - public final static String KEYCLOAK_RSTUDIO_GROUP_SV = HEAD_SV + KEYCLOAK_RSTUDIO_GROUP + BOTTOM_SV; - public final static String ENABLE_RSTUDIO_GROUP_MANAGER_SV = HEAD_SV + ENABLE_RSTUDIO_GROUP_MANAGER + ":true" + BOTTOM_SV; - public final static String OIDC_URL_SV = HEAD_SV + OIDC_URL + BOTTOM_SV; - public final static String OIDC_REALM_SV = HEAD_SV + OIDC_REALM + BOTTOM_SV; public final static String CODER_BASE_URL_SV = HEAD_SV + CODER_BASE_URL + BOTTOM_SV; public final static String CODER_ORGANISATION_ID_SV = HEAD_SV + CODER_ORGANISATION_ID + BOTTOM_SV; - public final static String CODER_MEMBER_ID_SV = HEAD_SV + CODER_MEMBER_ID + BOTTOM_SV; public final static String CODER_CREATE_PATH_SV = HEAD_SV + CODER_CREATE_PATH + BOTTOM_SV; public final static String CODER_DELETE_PATH_SV = HEAD_SV + CODER_DELETE_PATH + BOTTOM_SV; public final static String CODER_SESSION_TOKEN_SV = HEAD_SV + CODER_SESSION_TOKEN + BOTTOM_SV; @@ -520,6 +544,7 @@ public class ProjectManagerConst { public final static String CODER_TEST_FILE_BEAM_ID_SV = HEAD_SV + CODER_TEST_FILE_BEAM_ID + BOTTOM_SV; public final static String CODER_CRON_EXPRESSION_SV = HEAD_SV + CODER_CRON_EXPRESSION + BOTTOM_SV; public final static String CODER_WORKSPACE_NAME_MAX_LENGTH_SV = HEAD_SV + CODER_WORKSPACE_NAME_MAX_LENGTH + ":32" + BOTTOM_SV; + public final static String REQUEST_CACHE_LIMIT_SV = HEAD_SV + REQUEST_CACHE_LIMIT + ":#{2 * 1024 * 1024}" + BOTTOM_SV; public final static String ENABLE_CODER_SV = HEAD_SV + ENABLE_CODER + ":true" + BOTTOM_SV; @@ -535,7 +560,7 @@ public class ProjectManagerConst { public final static String EXPORTER_CORE_POOL_SIZE_SV = HEAD_SV + EXPORTER_CORE_POOL_SIZE + ":4" + BOTTOM_SV; public final static String EXPORTER_MAX_POOL_SIZE_SV = HEAD_SV + EXPORTER_MAX_POOL_SIZE + ":8" + BOTTOM_SV; public final static String EXPORTER_QUEUE_CAPACITY_SV = HEAD_SV + EXPORTER_QUEUE_CAPACITY + ":500" + BOTTOM_SV; - public final static String DEFAULT_LANGUAGE_SV = HEAD_SV + DEFAULT_LANGUAGE + ":EN" + BOTTOM_SV; + public final static String DEFAULT_LANGUAGE_SV = HEAD_SV + DEFAULT_LANGUAGE + ":en" + BOTTOM_SV; public final static String TEST_EMAIL_DOMAINS_SV = HEAD_SV + TEST_EMAIL_DOMAINS + ":" + BOTTOM_SV; @@ -547,6 +572,16 @@ public class ProjectManagerConst { public final static String APP_REGISTER_AUTHORIZATION_FORMAT_SV = HEAD_SV + APP_REGISTER_AUTHORIZATION_FORMAT + ":ApiKey {}" + BOTTOM_SV; public final static String ENABLE_APP_REGISTER_SV = HEAD_SV + ENABLE_APP_REGISTER + ":true" + BOTTOM_SV; + public final static String FORM_RESOURCES_DIRECTORY_SV = HEAD_SV + FORM_RESOURCES_DIRECTORY + BOTTOM_SV; + public final static String FORM_TEMPLATE_METADATA_DIRECTORY_SV = HEAD_SV + FORM_TEMPLATE_METADATA_DIRECTORY + BOTTOM_SV; + public final static String FORM_TEMPLATE_DEFAULT_PDF_FILENAME_SV = HEAD_SV + FORM_TEMPLATE_DEFAULT_PDF_FILENAME + ":form.pdf" + BOTTOM_SV; + public final static String FORM_FIELDS_DIRECTORY_SV = HEAD_SV + FORM_FIELDS_DIRECTORY + BOTTOM_SV; + public final static String FORM_TEMPLATE_DATE_PATTERN_SV = HEAD_SV + FORM_TEMPLATE_DATE_PATTERN + ":d. MMMM yyyy" + BOTTOM_SV; + public final static String SHED_LOCK_MAX_TIME_SV = HEAD_SV + SHED_LOCK_MAX_TIME + ":PT5M" + BOTTOM_SV; + public final static String TIME_BETWEEN_CHECK_EXPORTS_IN_SECONDS_SV = HEAD_SV + TIME_BETWEEN_CHECK_EXPORTS_IN_SECONDS + ":5" + BOTTOM_SV; + public final static String MAX_NUMBER_OF_RETRIES_BETWEEN_CHECK_EXPORTS_SV = HEAD_SV + MAX_NUMBER_OF_RETRIES_BETWEEN_CHECK_EXPORTS + ":3" + BOTTOM_SV; + + // Async Configuration public final static String ASYNC_EMAIL_SENDER_EXECUTOR = "email-sender"; public final static String ASYNC_NOTIFICATION_EXECUTOR = "notification"; @@ -555,7 +590,7 @@ public class ProjectManagerConst { // Thymeleaf public final static int THYMELEAF_PROCESSOR_PRECEDENCE = 1000; public final static int THYMELEAF_DIALECT_PRECEDENCE = 1000; - public final static String THYMELEAF_DIALECT_NAME = "Project Manager"; + public final static String THYMELEAF_DIALECT_NAME = "ProjectCode Manager"; public final static String THYMELEAF_DIALECT_PREFIX = "pm"; // Variable name placeholders @@ -564,7 +599,6 @@ public class ProjectManagerConst { // App Register public final static String REGISTER_PATH = "/beam-app"; - public final static String UNREGISTER_PATH = "/beam-app"; // Others @@ -583,6 +617,12 @@ public class ProjectManagerConst { public final static String PRIMARY_MAIL_SENDER = "primaryJavaMailSender"; public final static String TEST_MAIL_SENDER = "testJavaMailSender"; public final static String NOT_AUTHORIZED = "Not authorized yet"; + public final static String FORM_FILENAME_TIMESTAMP_FORMAT = "yyyyMMdd-HHmm"; + public final static String FORM_FILENAME_TIMESTAMP_ZONE = "UTC"; + public final static String FORM_PROJECT_FIELDS_TITLE_PREFIX = "_project_fields"; + public final static String NOT_SET = "__NOT_SET__"; + public final static String EXPORTER_JOB_NAME = "exporterJob"; + public final static String SHEDLOCK_TABLE_NAME = "samply.shedlock"; } diff --git a/src/main/java/de/samply/app/ProjectManagerController.java b/src/main/java/de/samply/app/ProjectManagerController.java index d3def9e2..53d5b78d 100644 --- a/src/main/java/de/samply/app/ProjectManagerController.java +++ b/src/main/java/de/samply/app/ProjectManagerController.java @@ -6,21 +6,28 @@ import de.samply.annotations.*; import de.samply.bridgehead.BridgeheadConfiguration; import de.samply.coder.CoderService; +import de.samply.datashield.DataShieldTokenManagerService; +import de.samply.db.model.Project; +import de.samply.db.model.ProjectBridgehead; import de.samply.db.model.ProjectDocument; import de.samply.document.DocumentService; import de.samply.document.DocumentServiceException; import de.samply.document.DocumentType; +import de.samply.document.DtoDocumentService; import de.samply.email.EmailRecipientType; import de.samply.email.EmailService; import de.samply.email.EmailTemplateType; import de.samply.exporter.ExporterService; +import de.samply.form.DtoFormService; +import de.samply.form.FormService; +import de.samply.form.template.FormTemplateService; import de.samply.frontend.FrontendService; import de.samply.frontend.dto.DtoFactory; +import de.samply.frontend.dto.FormField; import de.samply.frontend.dto.configuration.ProjectConfigurations; +import de.samply.notification.DtoNotificationService; import de.samply.notification.NotificationService; -import de.samply.project.ProjectBridgeheadService; -import de.samply.project.ProjectService; -import de.samply.project.ProjectType; +import de.samply.project.*; import de.samply.project.event.ProjectEventActionsException; import de.samply.project.event.ProjectEventService; import de.samply.project.state.ProjectBridgeheadState; @@ -30,15 +37,15 @@ import de.samply.query.QueryFormat; import de.samply.query.QueryService; import de.samply.query.QueryState; -import de.samply.datashield.DataShieldTokenManagerService; +import de.samply.user.DtoUserService; import de.samply.user.UserService; import de.samply.user.roles.OrganisationRole; import de.samply.user.roles.ProjectRole; import de.samply.utils.ProjectVersion; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; +import org.springframework.context.annotation.Profile; import org.springframework.core.io.ByteArrayResource; -import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -47,248 +54,308 @@ import org.springframework.web.multipart.MultipartFile; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; +// Suppress raw types warning: +// This controller intentionally returns raw ResponseEntity to avoid +// excessive generic duplication across many endpoints. + +// Suppress unused parameter warning: +// Parameters such as bridgehead and project are not always used +// directly in the method body but are required for validation and +// authorization in the AOP layer. +@SuppressWarnings({"rawtypes"}) @RestController @Slf4j public class ProjectManagerController { private final String projectVersion = ProjectVersion.getProjectVersion(); - private final ObjectMapper objectMapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT) - .registerModule(new JavaTimeModule()).configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + private final ObjectMapper objectMapper = new ObjectMapper().enable( + SerializationFeature.INDENT_OUTPUT) + .registerModule(new JavaTimeModule()) + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); private final ProjectEventService projectEventService; private final FrontendService frontendService; private final UserService userService; + private final DtoUserService dtoUserService; private final QueryService queryService; private final DocumentService documentService; + private final DtoDocumentService dtoDocumentService; private final ExporterService exporterService; private final DataShieldTokenManagerService dataShieldTokenManagerService; private final ProjectService projectService; + private final DtoProjectService dtoProjectService; private final ProjectBridgeheadService projectBridgeheadService; + private final DtoProjectBridgeheadService dtoProjectBridgeheadService; private final NotificationService notificationService; + private final DtoNotificationService dtoNotificationService; private final BridgeheadConfiguration bridgeheadConfiguration; private final ProjectConfigurations frontendProjectConfigurations; private final DtoFactory dtoFactory; private final EmailService emailService; private final CoderService coderService; + private final FormService formService; + private final DtoFormService dtoFormService; + private final FormTemplateService formTemplateService; public ProjectManagerController(ProjectEventService projectEventService, FrontendService frontendService, UserService userService, + DtoUserService dtoUserService, QueryService queryService, DocumentService documentService, + DtoDocumentService dtoDocumentService, ExporterService exporterService, DataShieldTokenManagerService dataShieldTokenManagerService, ProjectService projectService, + DtoProjectService dtoProjectService, ProjectBridgeheadService projectBridgeheadService, + DtoProjectBridgeheadService dtoProjectBridgeheadService, NotificationService notificationService, + DtoNotificationService dtoNotificationService, BridgeheadConfiguration bridgeheadConfiguration, ProjectConfigurations frontendProjectConfigurations, DtoFactory dtoFactory, EmailService emailService, - CoderService coderService) { + CoderService coderService, + FormService formService, + DtoFormService dtoFormService, + FormTemplateService formTemplateService) { this.projectEventService = projectEventService; this.frontendService = frontendService; this.userService = userService; + this.dtoUserService = dtoUserService; this.queryService = queryService; this.documentService = documentService; + this.dtoDocumentService = dtoDocumentService; this.exporterService = exporterService; this.dataShieldTokenManagerService = dataShieldTokenManagerService; this.projectService = projectService; + this.dtoProjectService = dtoProjectService; this.projectBridgeheadService = projectBridgeheadService; + this.dtoProjectBridgeheadService = dtoProjectBridgeheadService; this.notificationService = notificationService; + this.dtoNotificationService = dtoNotificationService; this.bridgeheadConfiguration = bridgeheadConfiguration; this.frontendProjectConfigurations = frontendProjectConfigurations; this.dtoFactory = dtoFactory; this.emailService = emailService; this.coderService = coderService; + this.formService = formService; + this.dtoFormService = dtoFormService; + this.formTemplateService = formTemplateService; } @GetMapping(value = ProjectManagerConst.INFO) - public ResponseEntity info() { + public ResponseEntity info() { return new ResponseEntity<>(projectVersion, HttpStatus.OK); } @GetMapping(value = ProjectManagerConst.TEST) - public ResponseEntity test() { + public ResponseEntity test() { return new ResponseEntity<>(ProjectManagerConst.THIS_IS_A_TEST, HttpStatus.OK); } @GetMapping(value = ProjectManagerConst.ACTIONS, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity fetchActions( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE, required = false) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD, required = false) String bridgehead, - @RequestParam(name = ProjectManagerConst.LANGUAGE, required = false) String language, - @RequestParam(name = ProjectManagerConst.SITE) String site + public ResponseEntity fetchActions( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE, required = false) Project project, + @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD, required = false) ProjectBridgehead bridgehead, + @Language String language, + @RequestParameter(name = ProjectManagerConst.SITE) String site ) { return convertToResponseEntity(() -> - this.frontendService.fetchModuleActionPackage(site, Optional.ofNullable(projectCode), Optional.ofNullable(bridgehead), Optional.ofNullable(language), true)); + this.frontendService.fetchModuleActionPackage(site, Optional.ofNullable(project), + Optional.ofNullable(bridgehead), Optional.ofNullable(language), true)); } @GetMapping(value = ProjectManagerConst.ALL_ACTIONS, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity fetchAllActions( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE, required = false) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD, required = false) String bridgehead, - @RequestParam(name = ProjectManagerConst.LANGUAGE, required = false) String language, - @RequestParam(name = ProjectManagerConst.SITE) String site + public ResponseEntity fetchAllActions( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE, required = false) Project project, + @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD, required = false) ProjectBridgehead bridgehead, + @Language String language, + @RequestParameter(name = ProjectManagerConst.SITE) String site ) { return convertToResponseEntity(() -> - this.frontendService.fetchModuleActionPackage(site, Optional.ofNullable(projectCode), Optional.ofNullable(bridgehead), Optional.ofNullable(language), false)); + this.frontendService.fetchModuleActionPackage(site, Optional.ofNullable(project), + Optional.ofNullable(bridgehead), Optional.ofNullable(language), false)); } @FrontendSiteModule(site = ProjectManagerConst.PROJECT_DASHBOARD_SITE, module = ProjectManagerConst.PROJECTS_MODULE) @FrontendAction(action = ProjectManagerConst.FETCH_PROJECTS_ACTION) @GetMapping(value = ProjectManagerConst.FETCH_PROJECTS, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity fetchProjects( - @RequestParam(name = ProjectManagerConst.PROJECT_STATE, required = false) ProjectState projectState, - @RequestParam(name = ProjectManagerConst.ARCHIVED, required = false) Boolean archived, - @RequestParam(name = ProjectManagerConst.LAST_MODIFIED_DESC, required = false, defaultValue = "true") boolean modifiedDescendant, - @RequestParam(name = ProjectManagerConst.PAGE) int page, - @RequestParam(name = ProjectManagerConst.PAGE_SIZE) int pageSize + public ResponseEntity fetchProjects( + @RequestParameter(name = ProjectManagerConst.PROJECT_STATE, required = false) ProjectState projectState, + @RequestParameter(name = ProjectManagerConst.ARCHIVED, required = false) Boolean archived, + @RequestParameter(name = ProjectManagerConst.LAST_MODIFIED_DESC, required = false, defaultValue = "true") boolean modifiedDescendant, + @RequestParameter(name = ProjectManagerConst.PAGE) int page, + @RequestParameter(name = ProjectManagerConst.PAGE_SIZE) int pageSize ) { - return convertToResponseEntity(() -> projectService.fetchUserVisibleProjects( - Optional.ofNullable(projectState), Optional.ofNullable(archived), page, pageSize, modifiedDescendant)); + return convertToResponseEntity(() -> dtoProjectService.fetchUserVisibleProjects( + Optional.ofNullable(projectState), Optional.ofNullable(archived), page, pageSize, + modifiedDescendant)); } - @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.DEVELOPER, ProjectRole.PILOT, ProjectRole.FINAL, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.DEVELOPER, ProjectRole.PILOT, + ProjectRole.FINAL, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_BRIDGEHEAD_MODULE) @FrontendAction(action = ProjectManagerConst.FETCH_PROJECT_ACTION) @GetMapping(value = ProjectManagerConst.FETCH_PROJECT, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity fetchProject( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, + public ResponseEntity fetchProject( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, // Bridgehead required for role constraints - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead + @SuppressWarnings("unused") @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead ) { - return convertToResponseEntity(() -> projectService.fetchProject(projectCode)); + return convertToResponseEntity(() -> dtoProjectService.fetchDtoProject(project.getCode())); } @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_BRIDGEHEAD_MODULE) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_DASHBOARD_SITE, module = ProjectManagerConst.PROJECT_BRIDGEHEAD_MODULE) @FrontendAction(action = ProjectManagerConst.FETCH_PROJECT_STATES_ACTION) @GetMapping(value = ProjectManagerConst.FETCH_PROJECT_STATES, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity fetchProjectStates( + public ResponseEntity fetchProjectStates( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE, required = false) Project project ) { - return convertToResponseEntity(() -> ProjectState.values()); + return convertToResponseEntity( + () -> projectEventService.fetchAllProjectEvents(Optional.ofNullable(project))); } @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_BRIDGEHEAD_MODULE) @FrontendAction(action = ProjectManagerConst.FETCH_VISIBLE_PROJECT_BRIDGEHEADS_ACTION) @GetMapping(value = ProjectManagerConst.FETCH_VISIBLE_PROJECT_BRIDGEHEADS, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity fetchVisibleProjectsBridgeheads( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode + public ResponseEntity fetchVisibleProjectsBridgeheads( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project ) { - return convertToResponseEntity(() -> projectBridgeheadService.fetchUserVisibleProjectBridgeheads(projectCode)); + return convertToResponseEntity( + () -> dtoProjectBridgeheadService.fetchUserVisibleProjectBridgeheads(project)); } @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_BRIDGEHEAD_MODULE) @FrontendAction(action = ProjectManagerConst.FETCH_PROJECT_BRIDGEHEADS_ACTION) @GetMapping(value = ProjectManagerConst.FETCH_PROJECT_BRIDGEHEADS, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity fetchProjectsBridgeheads( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode + public ResponseEntity fetchProjectsBridgeheads( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project ) { - return convertToResponseEntity(() -> projectBridgeheadService.fetchProjectBridgeheads(projectCode)); + return convertToResponseEntity( + () -> dtoProjectBridgeheadService.fetchProjectBridgeheads(project)); } - @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, + ProjectRole.PROJECT_MANAGER_ADMIN}) @StateConstraints(projectStates = {ProjectState.DEVELOP}) @ProjectConstraints(projectTypes = {ProjectType.DATASHIELD, ProjectType.RESEARCH_ENVIRONMENT}) - @EmailSender(templateType = EmailTemplateType.INVITATION, recipients = {EmailRecipientType.EMAIL_ANNOTATION}) - @EmailSender(templateType = EmailTemplateType.REQUEST_TECHNICAL_APPROVAL, recipients = {EmailRecipientType.BRIDGEHEAD_ADMIN}) + @EmailSender(templateType = EmailTemplateType.INVITATION, recipients = { + EmailRecipientType.EMAIL_ANNOTATION}) + @EmailSender(templateType = EmailTemplateType.REQUEST_TECHNICAL_APPROVAL, recipients = { + EmailRecipientType.BRIDGEHEAD_ADMIN}) //TODO: Send email to PM-ADMIN, that there was a problem with the operation @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.USER_MODULE) @FrontendAction(action = ProjectManagerConst.SET_DEVELOPER_USER_ACTION) @PostMapping(value = ProjectManagerConst.SET_DEVELOPER_USER) - public ResponseEntity setUserAsDeveloper( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead, - @Email @RequestParam(name = ProjectManagerConst.EMAIL) String email + public ResponseEntity setUserAsDeveloper( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestVariable(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead, + @Email @RequestVariable(name = ProjectManagerConst.EMAIL) String email ) { return convertToResponseEntity(() -> - this.userService.setProjectBridgheadUserWithRoleAndGenerateTokensIfDataShield(email, projectCode, bridgehead, ProjectRole.DEVELOPER)); + this.userService.setProjectBridgeheadUserWithRoleAndGenerateTokensIfDataShield(email, + project, bridgehead, ProjectRole.DEVELOPER)); } @RoleConstraints(projectRoles = {ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) @StateConstraints(projectStates = {ProjectState.PILOT}) @ProjectConstraints(projectTypes = {ProjectType.DATASHIELD, ProjectType.RESEARCH_ENVIRONMENT}) - @EmailSender(templateType = EmailTemplateType.INVITATION, recipients = {EmailRecipientType.EMAIL_ANNOTATION}) - @EmailSender(templateType = EmailTemplateType.REQUEST_TECHNICAL_APPROVAL, recipients = {EmailRecipientType.BRIDGEHEAD_ADMIN}) + @EmailSender(templateType = EmailTemplateType.INVITATION, recipients = { + EmailRecipientType.EMAIL_ANNOTATION}) + @EmailSender(templateType = EmailTemplateType.REQUEST_TECHNICAL_APPROVAL, recipients = { + EmailRecipientType.BRIDGEHEAD_ADMIN}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.USER_MODULE) @FrontendAction(action = ProjectManagerConst.SET_PILOT_USER_ACTION) @PostMapping(value = ProjectManagerConst.SET_PILOT_USER) - public ResponseEntity setUserAsPilot( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead, - @Email @RequestParam(name = ProjectManagerConst.EMAIL) String email + public ResponseEntity setUserAsPilot( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestVariable(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead, + @Email @RequestVariable(name = ProjectManagerConst.EMAIL) String email ) { return convertToResponseEntity(() -> - this.userService.setProjectBridgheadUserWithRoleAndGenerateTokensIfDataShield(email, projectCode, bridgehead, ProjectRole.PILOT)); + this.userService.setProjectBridgeheadUserWithRoleAndGenerateTokensIfDataShield(email, + project, bridgehead, ProjectRole.PILOT)); } @RoleConstraints(organisationRoles = {OrganisationRole.PROJECT_MANAGER_ADMIN}) @StateConstraints(projectStates = {ProjectState.FINAL}) - @EmailSender(templateType = EmailTemplateType.INVITATION, recipients = {EmailRecipientType.EMAIL_ANNOTATION}) + @EmailSender(templateType = EmailTemplateType.INVITATION, recipients = { + EmailRecipientType.EMAIL_ANNOTATION}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.USER_MODULE) @FrontendAction(action = ProjectManagerConst.SET_FINAL_USER_ACTION) @PostMapping(value = ProjectManagerConst.SET_FINAL_USER) - public ResponseEntity setUserAsFinal( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead, - @Email @RequestParam(name = ProjectManagerConst.EMAIL) String email + public ResponseEntity setUserAsFinal( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestVariable(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead, + @Email @RequestVariable(name = ProjectManagerConst.EMAIL) String email ) { return convertToResponseEntity(() -> - this.userService.setProjectBridgheadUserWithRoleAndGenerateTokensIfDataShield(email, projectCode, bridgehead, ProjectRole.FINAL)); + this.userService.setProjectBridgeheadUserWithRoleAndGenerateTokensIfDataShield(email, + project, bridgehead, ProjectRole.FINAL)); } @RoleConstraints(organisationRoles = {OrganisationRole.RESEARCHER}) @PostMapping(value = ProjectManagerConst.CREATE_QUERY) - public ResponseEntity createProjectQuery( - @NotEmpty @RequestBody() String query, - @NotEmpty @RequestParam(name = ProjectManagerConst.QUERY_FORMAT) QueryFormat queryFormat, - @RequestParam(name = ProjectManagerConst.LABEL, required = false) String label, - @RequestParam(name = ProjectManagerConst.DESCRIPTION, required = false) String description, - @RequestParam(name = ProjectManagerConst.OUTPUT_FORMAT, required = false) OutputFormat outputFormat, - @RequestParam(name = ProjectManagerConst.TEMPLATE_ID, required = false) String templateId, - @RequestParam(name = ProjectManagerConst.HUMAN_READABLE, required = false) String humanReadable, - @RequestParam(name = ProjectManagerConst.REDIRECT_EXPLORER_URL, required = false) String explorerUrl, - @RequestParam(name = ProjectManagerConst.QUERY_CONTEXT, required = false) String queryContext + public ResponseEntity createProjectQuery( + @RequestVariable(name = ProjectManagerConst.QUERY, notEmpty = true) String query, + @RequestVariable(name = ProjectManagerConst.QUERY_FORMAT, notEmpty = true) QueryFormat queryFormat, + @RequestVariable(name = ProjectManagerConst.LABEL, required = false) String label, + @RequestVariable(name = ProjectManagerConst.DESCRIPTION, required = false) String description, + @RequestVariable(name = ProjectManagerConst.OUTPUT_FORMAT, required = false) OutputFormat outputFormat, + @RequestVariable(name = ProjectManagerConst.TEMPLATE_ID, required = false) String templateId, + @RequestVariable(name = ProjectManagerConst.HUMAN_READABLE, required = false) String humanReadable, + @RequestVariable(name = ProjectManagerConst.REDIRECT_EXPLORER_URL, required = false) String explorerUrl, + @RequestVariable(name = ProjectManagerConst.QUERY_CONTEXT, required = false) String queryContext, + @RequestVariable(name = ProjectManagerConst.QUERY_DETAILS, required = false) String queryDetails, + @RequestVariable(name = ProjectManagerConst.PROJECT_TYPE, required = false) ProjectType projectType ) { return convertToResponseEntity(() -> - this.queryService.createQuery(query, queryFormat, label, description, outputFormat, templateId, humanReadable, explorerUrl, queryContext)); + this.queryService.createQuery(query, queryFormat, label, description, + outputFormat, templateId, projectType, humanReadable, explorerUrl, queryContext, + queryDetails)); } @RoleConstraints(organisationRoles = {OrganisationRole.RESEARCHER}) @PostMapping(value = ProjectManagerConst.CREATE_QUERY_AND_DESIGN_PROJECT) - public ResponseEntity createQueryAndDesignProject( - @NotEmpty @RequestBody() String query, - @NotEmpty @RequestParam(name = ProjectManagerConst.QUERY_FORMAT) QueryFormat queryFormat, - @RequestParam(name = ProjectManagerConst.BRIDGEHEADS, required = false) String[] bridgeheads, - @RequestParam(name = ProjectManagerConst.EXPLORER_IDS, required = false) String[] explorerIds, - @RequestParam(name = ProjectManagerConst.LABEL, required = false) String label, - @RequestParam(name = ProjectManagerConst.DESCRIPTION, required = false) String description, - @RequestParam(name = ProjectManagerConst.OUTPUT_FORMAT, required = false) OutputFormat outputFormat, - @RequestParam(name = ProjectManagerConst.TEMPLATE_ID, required = false) String templateId, - @RequestParam(name = ProjectManagerConst.HUMAN_READABLE, required = false) String humanReadable, - @RequestParam(name = ProjectManagerConst.REDIRECT_EXPLORER_URL, required = false) String explorerUrl, - @RequestParam(name = ProjectManagerConst.PROJECT_TYPE, required = false) ProjectType projectType, - @RequestParam(name = ProjectManagerConst.QUERY_CONTEXT, required = false) String queryContext + public ResponseEntity createQueryAndDesignProject( + @RequestVariable(name = ProjectManagerConst.QUERY, notEmpty = true) String query, + @RequestVariable(name = ProjectManagerConst.QUERY_FORMAT, notEmpty = true) QueryFormat queryFormat, + @RequestVariable(name = ProjectManagerConst.BRIDGEHEADS, required = false) String[] bridgeheads, + @RequestVariable(name = ProjectManagerConst.EXPLORER_IDS, required = false) String[] explorerIds, + @RequestVariable(name = ProjectManagerConst.LABEL, required = false) String label, + @RequestVariable(name = ProjectManagerConst.DESCRIPTION, required = false) String description, + @RequestVariable(name = ProjectManagerConst.OUTPUT_FORMAT, required = false) OutputFormat outputFormat, + @RequestVariable(name = ProjectManagerConst.TEMPLATE_ID, required = false) String templateId, + @RequestVariable(name = ProjectManagerConst.HUMAN_READABLE, required = false) String humanReadable, + @RequestVariable(name = ProjectManagerConst.REDIRECT_EXPLORER_URL, required = false) String explorerUrl, + @RequestVariable(name = ProjectManagerConst.PROJECT_TYPE, required = false) ProjectType projectType, + @RequestVariable(name = ProjectManagerConst.QUERY_DETAILS, required = false) String queryDetails, + @RequestVariable(name = ProjectManagerConst.QUERY_CONTEXT, required = false) String queryContext ) throws ProjectEventActionsException { if (areBridgeheadsOrExplorerIdsEmpty(bridgeheads, explorerIds)) { return ResponseEntity.badRequest().body("Bridgeheads or explorer ids cannot be empty"); } String[] tempBridgeheads = (explorerIds != null && explorerIds.length > 0) ? - Arrays.stream(explorerIds).map(explorerId -> - bridgeheadConfiguration.getBridgeheadForExplorerId(explorerId).get()).toArray(String[]::new) : bridgeheads; + Arrays.stream(explorerIds) + .map(bridgeheadConfiguration::getBridgeheadForExplorerId) + .flatMap(Optional::stream) + .toArray(String[]::new) : bridgeheads; String queryCode = this.queryService.createQuery( - query, queryFormat, label, description, outputFormat, templateId, humanReadable, explorerUrl, queryContext); - String projectCode = this.projectEventService.draft(tempBridgeheads, queryCode, projectType); + query, queryFormat, label, description, outputFormat, templateId, + projectType, humanReadable, explorerUrl, queryContext, queryDetails); + String projectCode = this.projectEventService.draft(tempBridgeheads, queryCode); this.queryService.addProjectCodeToExporterUrl(queryCode, projectCode); return convertToResponseEntity(() -> this.frontendService.fetchExplorerRedirectUri( ProjectManagerConst.PROJECT_VIEW_SITE, @@ -297,95 +364,271 @@ public ResponseEntity createQueryAndDesignProject( } private boolean areBridgeheadsOrExplorerIdsEmpty(String[] bridgeheads, String[] explorerIds) { - return (bridgeheads == null || bridgeheads.length == 0) && (explorerIds == null || explorerIds.length == 0); + return (bridgeheads == null || bridgeheads.length == 0) && (explorerIds == null + || explorerIds.length == 0); } - @RoleConstraints(projectRoles = {ProjectRole.CREATOR}) - @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.CREATED}) + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.PROJECT_MANAGER_ADMIN}) + @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.REVIEW}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_EDITION_MODULE) @FrontendAction(action = ProjectManagerConst.EDIT_PROJECT_ACTION) - @PostMapping(value = ProjectManagerConst.EDIT_PROJECT) - public ResponseEntity editProject( - @RequestBody() String query, - @RequestParam(name = ProjectManagerConst.QUERY_FORMAT, required = false) QueryFormat queryFormat, - @RequestParam(name = ProjectManagerConst.BRIDGEHEADS, required = false) String[] bridgeheads, - @RequestParam(name = ProjectManagerConst.EXPLORER_IDS, required = false) String[] explorerIds, - @RequestParam(name = ProjectManagerConst.LABEL, required = false) String label, - @RequestParam(name = ProjectManagerConst.DESCRIPTION, required = false) String description, - @RequestParam(name = ProjectManagerConst.OUTPUT_FORMAT, required = false) OutputFormat outputFormat, - @RequestParam(name = ProjectManagerConst.TEMPLATE_ID, required = false) String templateId, - @RequestParam(name = ProjectManagerConst.HUMAN_READABLE, required = false) String humanReadable, - @RequestParam(name = ProjectManagerConst.REDIRECT_EXPLORER_URL, required = false) String explorerUrl, - @RequestParam(name = ProjectManagerConst.PROJECT_TYPE, required = false) ProjectType projectType, - @RequestParam(name = ProjectManagerConst.QUERY_CONTEXT, required = false) String queryContext, - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode - ) { - String[] tempBridgeheads = (explorerIds != null && explorerIds.length > 0) ? - Arrays.stream(explorerIds).map(explorerId -> - bridgeheadConfiguration.getBridgeheadForExplorerId(explorerId).get()).toArray(String[]::new) : bridgeheads; - projectService.editProject(projectCode, projectType, tempBridgeheads); - queryService.editQuery(projectCode, (query != null && query.trim().length() > 0 && !query.equals("{}")) ? query : null, queryFormat, label, description, outputFormat, templateId, humanReadable, explorerUrl, queryContext); + @PutMapping(value = ProjectManagerConst.EDIT_PROJECT) + public ResponseEntity editProject( + @RequestVariable(name = ProjectManagerConst.QUERY, required = false) String query, + @RequestVariable(name = ProjectManagerConst.QUERY_FORMAT, required = false) QueryFormat queryFormat, + @RequestVariable(name = ProjectManagerConst.BRIDGEHEADS, required = false) String[] bridgeheads, + @RequestVariable(name = ProjectManagerConst.EXPLORER_IDS, required = false) String[] explorerIds, + @RequestVariable(name = ProjectManagerConst.LABEL, required = false) String label, + @RequestVariable(name = ProjectManagerConst.DESCRIPTION, required = false) String description, + @RequestVariable(name = ProjectManagerConst.COHORT_DEFINITION, required = false) String cohortDefinition, + @RequestVariable(name = ProjectManagerConst.OUTPUT_FORMAT, required = false) OutputFormat outputFormat, + @RequestVariable(name = ProjectManagerConst.TEMPLATE_ID, required = false) String templateId, + @RequestVariable(name = ProjectManagerConst.PROJECT_TYPE, required = false) ProjectType projectType, + @RequestVariable(name = ProjectManagerConst.HUMAN_READABLE, required = false) String humanReadable, + @RequestVariable(name = ProjectManagerConst.REDIRECT_EXPLORER_URL, required = false) String explorerUrl, + @RequestVariable(name = ProjectManagerConst.QUERY_CONTEXT, required = false) String queryContext, + @RequestVariable(name = ProjectManagerConst.QUERY_DETAILS, required = false) String queryDetails, + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project + ) { + if (bridgeheads != null) { + String[] tempBridgeheads = (explorerIds != null && explorerIds.length > 0) ? + Arrays.stream(explorerIds) + .map(bridgeheadConfiguration::getBridgeheadForExplorerId) + .flatMap(Optional::stream) + .toArray(String[]::new) : bridgeheads; + projectService.updateBridgeheads(project, tempBridgeheads); + } + queryService.editQuery(project, + (query != null && !query.trim().isEmpty() && !query.equals("{}")) ? query : + null, queryFormat, label, description, outputFormat, templateId, projectType, + humanReadable, + explorerUrl, queryContext, cohortDefinition, queryDetails); return convertToResponseEntity(() -> this.frontendService.fetchExplorerRedirectUri( ProjectManagerConst.PROJECT_VIEW_SITE, - Map.of(ProjectManagerConst.PROJECT_CODE, projectCode) + Map.of(ProjectManagerConst.PROJECT_CODE, project.getCode()) )); } + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.PROJECT_MANAGER_ADMIN}) + @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.REVIEW}) + @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_EDITION_MODULE) + @FrontendAction(action = ProjectManagerConst.REMOVE_PROJECT_OUTPUT_ACTION) + @DeleteMapping(value = ProjectManagerConst.REMOVE_PROJECT_OUTPUT) + public ResponseEntity removeOutput( + @RequestVariable(name = ProjectManagerConst.PROJECT_TYPE) ProjectType projectType, + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project + ) { + return convertToResponseEntity(() -> this.queryService.removeOutput(project, projectType)); + } + + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.PROJECT_MANAGER_ADMIN, + ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.DEVELOPER, ProjectRole.FINAL, ProjectRole.PILOT}) + @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.REVIEW, ProjectState.DEVELOP, + ProjectState.PILOT, ProjectState.FINAL, ProjectState.FINISHED}) + @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_EDITION_MODULE) + @FrontendAction(action = ProjectManagerConst.FETCH_PROJECT_FORM_TITLES_ACTION) + @GetMapping(value = ProjectManagerConst.FETCH_PROJECT_FORM_TITLES) + public ResponseEntity fetchProjectFormTitles( + // ProjectCode code needed for role constraints + @SuppressWarnings("unused") @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @SuppressWarnings("unused") @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD, required = false) ProjectBridgehead bridgehead, + @Language String language + ) { + return convertToResponseEntity( + () -> dtoFormService.fetchProjectFormTitles(Optional.ofNullable(language))); + } + + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.PROJECT_MANAGER_ADMIN, + ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.DEVELOPER, ProjectRole.FINAL, ProjectRole.PILOT}) + @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.REVIEW, ProjectState.DEVELOP, + ProjectState.PILOT, ProjectState.FINAL, ProjectState.FINISHED}) + @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_EDITION_MODULE) + @FrontendAction(action = ProjectManagerConst.FETCH_PROJECT_FORM_FIELDS_ACTION) + @GetMapping(value = ProjectManagerConst.FETCH_PROJECT_FORM_FIELDS) + public ResponseEntity fetchProjectFormFields( + // ProjectCode code needed for role constraints + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @SuppressWarnings("unused") @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD, required = false) ProjectBridgehead bridgehead, + @Language String language, + @RequestParameter(name = ProjectManagerConst.FORM_TITLE, required = false) String formTitle + ) { + return convertToResponseEntity( + () -> dtoFormService.fetchProjectFormFields(Optional.ofNullable(formTitle), project, + Optional.ofNullable(language))); + } + + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.PROJECT_MANAGER_ADMIN, + ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.DEVELOPER, ProjectRole.FINAL, ProjectRole.PILOT}) + @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.REVIEW, ProjectState.DEVELOP, + ProjectState.PILOT, ProjectState.FINAL, ProjectState.FINISHED}) + @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_EDITION_MODULE) + @FrontendAction(action = ProjectManagerConst.FETCH_SELECTED_PROJECT_FORMS_ACTION) + @GetMapping(value = ProjectManagerConst.FETCH_SELECTED_PROJECT_FORMS) + public ResponseEntity fetchSelectedProjectForms( + // ProjectCode code needed for role constraints + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @SuppressWarnings("unused") @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD, required = false) ProjectBridgehead bridgehead, + @Language String language + ) { + return convertToResponseEntity( + () -> dtoFormService.fetchSelectedForms(project, Optional.ofNullable(language))); + } + + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.PROJECT_MANAGER_ADMIN}) + @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.REVIEW}) + @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_EDITION_MODULE) + @FrontendAction(action = ProjectManagerConst.ADD_SELECTED_PROJECT_FORM_ACTION) + @PostMapping(value = ProjectManagerConst.ADD_SELECTED_PROJECT_FORM) + public ResponseEntity addSelectedProjectForm( + // ProjectCode code needed for role constraints + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + @RequestVariable(name = ProjectManagerConst.FORM_TITLE) String formTitle + ) { + return convertToResponseEntity(() -> formService.addSelectedForm(project, formTitle)); + } + + @RoleConstraints(projectRoles = {ProjectRole.CREATOR}) + @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.REVIEW}) + @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_EDITION_MODULE) + @FrontendAction(action = ProjectManagerConst.REMOVE_SELECTED_PROJECT_FORM_ACTION) + @DeleteMapping(value = ProjectManagerConst.REMOVE_SELECTED_PROJECT_FORM) + public ResponseEntity removeSelectedProjectForm( + // ProjectCode code needed for role constraints + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @RequestParameter(name = ProjectManagerConst.FORM_TITLE) String formTitle + ) { + return convertToResponseEntity(() -> formService.removeSelectedForm(project, formTitle)); + } + + + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.PROJECT_MANAGER_ADMIN}) + @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.REVIEW}) + @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_EDITION_MODULE) + @FrontendAction(action = ProjectManagerConst.EDIT_PROJECT_FORM_FIELDS_ACTION) + @PutMapping(value = ProjectManagerConst.EDIT_PROJECT_FORM_FIELDS) + public ResponseEntity editProjectFormValues( + // ProjectCode code needed for role constraints + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + @RequestVariable(name = ProjectManagerConst.FORM_FIELDS) FormField[] formFields + ) { + return convertToResponseEntity( + () -> formService.editProjectFormFieldValues(Optional.ofNullable(formFields), project)); + } + + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.PROJECT_MANAGER_ADMIN, + ProjectRole.BRIDGEHEAD_ADMIN}) + @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.REVIEW, ProjectState.DEVELOP, + ProjectState.PILOT, ProjectState.FINAL, ProjectState.FINISHED}) + @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_EDITION_MODULE) + @FrontendAction(action = ProjectManagerConst.DOWNLOAD_FORM_AS_PDF_ACTION) + @GetMapping(value = ProjectManagerConst.DOWNLOAD_FORM_AS_PDF) + public ResponseEntity downloadFormAsPdf( + // ProjectCode code and bridgehead needed for role constraints + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @SuppressWarnings("unused") @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD, required = false) ProjectBridgehead bridgehead, + @Language String language, + @RequestParameter(name = ProjectManagerConst.FORM_TEMPLATE) String formTemplate + ) { + return convertToResponseEntity(() -> + ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + + formTemplateService.fetchFormFilename(project, formTemplate)) + .contentType(MediaType.APPLICATION_PDF) + .body(formTemplateService.createFormAsPdf(project, formTemplate, + Optional.ofNullable(language))) + ); + } + + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.PROJECT_MANAGER_ADMIN, + ProjectRole.BRIDGEHEAD_ADMIN}) + @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.REVIEW, ProjectState.DEVELOP, + ProjectState.PILOT, ProjectState.FINAL, ProjectState.FINISHED}) + @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_EDITION_MODULE) + @FrontendAction(action = ProjectManagerConst.FETCH_PROJECT_FORM_TEMPLATES_ACTION) + @GetMapping(value = ProjectManagerConst.FETCH_PROJECT_FORM_TEMPLATES) + public ResponseEntity fetchProjectFormTemplates( + // ProjectCode code and bridgehead needed for role constraints + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @SuppressWarnings("unused") @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD, required = false) ProjectBridgehead bridgehead, + @Language String language + ) { + return convertToResponseEntity( + () -> formTemplateService.fetchTemplates(project, Optional.ofNullable(language))); + } + + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.PROJECT_MANAGER_ADMIN, + ProjectRole.BRIDGEHEAD_ADMIN}) + @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.REVIEW, ProjectState.DEVELOP, + ProjectState.PILOT, ProjectState.FINAL, ProjectState.FINISHED}) + @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_EDITION_MODULE) + @FrontendAction(action = ProjectManagerConst.FETCH_BEST_PROJECT_FORM_TEMPLATES_ACTION) + @GetMapping(value = ProjectManagerConst.FETCH_BEST_PROJECT_FORM_TEMPLATES) + public ResponseEntity fetchBestProjectFormTemplates( + // ProjectCode code and bridgehead needed for role constraints + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @SuppressWarnings("unused") @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD, required = false) ProjectBridgehead bridgehead, + @Language String language + ) { + return convertToResponseEntity( + () -> formTemplateService.fetchBestTemplates(project, Optional.ofNullable(language))); + } + @RoleConstraints(projectRoles = {ProjectRole.CREATOR}) - @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.CREATED}) + @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.REVIEW}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_EDITION_MODULE) @FrontendAction(action = ProjectManagerConst.FETCH_EXPORTER_TEMPLATES_ACTION) @GetMapping(value = ProjectManagerConst.FETCH_EXPORTER_TEMPLATES) - public ResponseEntity fetchExporterTemplates( - // Project code needed for role constraints - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @RequestParam(name = ProjectManagerConst.PROJECT_TYPE) ProjectType projectType + public ResponseEntity fetchExporterTemplates( + // ProjectCode code needed for role constraints + @SuppressWarnings("unused") @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project ) { - return convertToResponseEntity(() -> exporterService.getExporterTemplates(projectType)); + return convertToResponseEntity(exporterService::getExporterTemplates); } @RoleConstraints(projectRoles = {ProjectRole.CREATOR}) - @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.CREATED}) + @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.REVIEW}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_EDITION_MODULE) @FrontendAction(action = ProjectManagerConst.FETCH_QUERY_FORMATS_ACTION) @GetMapping(value = ProjectManagerConst.FETCH_QUERY_FORMATS) - public ResponseEntity fetchQueryFormats( - // Project code needed for role constraints - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode + public ResponseEntity fetchQueryFormats( + // ProjectCode code needed for role constraints + @SuppressWarnings("unused") @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project ) { - return convertToResponseEntity(() -> QueryFormat.values()); + return convertToResponseEntity(QueryFormat::values); } - @RoleConstraints(organisationRoles = {OrganisationRole.RESEARCHER, OrganisationRole.BRIDGEHEAD_ADMIN, OrganisationRole.PROJECT_MANAGER_ADMIN}) + @RoleConstraints(organisationRoles = {OrganisationRole.RESEARCHER, + OrganisationRole.BRIDGEHEAD_ADMIN, OrganisationRole.PROJECT_MANAGER_ADMIN}) @FrontendSiteModule(site = ProjectManagerConst.NAVIGATION_BAR_SITE, module = ProjectManagerConst.USER_MODULE) @FrontendAction(action = ProjectManagerConst.IS_PROJECT_MANAGER_ADMIN_ACTION) @GetMapping(value = ProjectManagerConst.IS_PROJECT_MANAGER_ADMIN) - public ResponseEntity isProjectManagerAdmin( - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD, required = false) String bridgehead + public ResponseEntity isProjectManagerAdmin( ) { - return convertToResponseEntity(() -> userService.isProjectManagerAdmin()); + return convertToResponseEntity(userService::isProjectManagerAdmin); } - @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.DEVELOPER, ProjectRole.PILOT, ProjectRole.FINAL, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.DEVELOPER, ProjectRole.PILOT, + ProjectRole.FINAL, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.USER_MODULE) @FrontendAction(action = ProjectManagerConst.FETCH_PROJECT_ROLES_ACTION) @GetMapping(value = ProjectManagerConst.FETCH_PROJECT_ROLES) - public ResponseEntity fetchProjectRoles( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD, required = false) String bridgehead + public ResponseEntity fetchProjectRoles( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD, required = false) ProjectBridgehead bridgehead ) { - return convertToResponseEntity(() -> userService.fetchProjectRoles(projectCode, Optional.ofNullable(bridgehead))); + return convertToResponseEntity( + () -> userService.fetchProjectRoles(project, Optional.ofNullable(bridgehead))); } @RoleConstraints(projectRoles = {ProjectRole.CREATOR}) - @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.CREATED}) + @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.REVIEW}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_EDITION_MODULE) @FrontendAction(action = ProjectManagerConst.FETCH_OUTPUT_FORMATS_ACTION) @GetMapping(value = ProjectManagerConst.FETCH_OUTPUT_FORMATS) - public ResponseEntity fetchOutputFormats( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode + public ResponseEntity fetchOutputFormats( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project ) { - return convertToResponseEntity(() -> projectService.fetchOutputFormats(projectCode)); + return convertToResponseEntity(() -> projectService.fetchOutputFormats(project)); } @RoleConstraints(projectRoles = {ProjectRole.DEVELOPER, ProjectRole.PILOT, ProjectRole.FINAL}) @@ -393,11 +636,11 @@ public ResponseEntity fetchOutputFormats( @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.USER_MODULE) @FrontendAction(action = ProjectManagerConst.FETCH_RESEARCH_ENVIRONMENT_URL_ACTION) @GetMapping(value = ProjectManagerConst.FETCH_RESEARCH_ENVIRONMENT_URL) - public ResponseEntity fetchResearchEnvironmentUrl( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead + public ResponseEntity fetchResearchEnvironmentUrl( + @SuppressWarnings("unused") @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @SuppressWarnings("unused") @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead ) { - return convertToResponseEntity(() -> coderService.getResearchEnvironmentUrl()); + return convertToResponseEntity(coderService::getResearchEnvironmentUrl); } @RoleConstraints(projectRoles = {ProjectRole.DEVELOPER, ProjectRole.PILOT, ProjectRole.FINAL}) @@ -405,91 +648,94 @@ public ResponseEntity fetchResearchEnvironmentUrl( @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.USER_MODULE) @FrontendAction(action = ProjectManagerConst.EXISTS_RESEARCH_ENVIRONMENT_WORKSPACE_ACTION) @GetMapping(value = ProjectManagerConst.EXISTS_RESEARCH_ENVIRONMENT_WORKSPACE) - public ResponseEntity existsUserResearchEnvironmentWorkspace( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead + public ResponseEntity existsUserResearchEnvironmentWorkspace( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead ) { - return convertToResponseEntity(() -> coderService.existsUserResearchEnvironmentWorkspace(projectCode, bridgehead)); + return convertToResponseEntity( + () -> coderService.existsUserResearchEnvironmentWorkspace(project, bridgehead)); } @RoleConstraints(projectRoles = {ProjectRole.CREATOR}) - @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.CREATED}) + @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.REVIEW}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_EDITION_MODULE) @FrontendAction(action = ProjectManagerConst.FETCH_PROJECT_CONFIGURATIONS_ACTION) @GetMapping(value = ProjectManagerConst.FETCH_PROJECT_CONFIGURATIONS) - public ResponseEntity fetchProjectConfigurations( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode + public ResponseEntity fetchProjectConfigurations( + @SuppressWarnings("unused") @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project ) { - return convertToResponseEntity(() -> this.frontendProjectConfigurations.getConfig()); + return convertToResponseEntity(this.frontendProjectConfigurations::getConfig); } @RoleConstraints(projectRoles = {ProjectRole.CREATOR}) - @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.CREATED}) + @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.REVIEW}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_EDITION_MODULE) @FrontendAction(action = ProjectManagerConst.FETCH_CURRENT_PROJECT_CONFIGURATION_ACTION) @GetMapping(value = ProjectManagerConst.FETCH_CURRENT_PROJECT_CONFIGURATION) - public ResponseEntity fetchCurrentProjectConfiguration( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode + public ResponseEntity fetchCurrentProjectConfiguration( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project ) { - return convertToResponseEntity(() -> this.projectService.fetchCurrentProjectConfiguration(projectCode)); + return convertToResponseEntity( + () -> this.dtoProjectService.fetchCurrentProjectConfiguration(project)); } - @RoleConstraints(projectRoles = {ProjectRole.CREATOR}) - @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.CREATED}) + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.PROJECT_MANAGER_ADMIN}) + @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.REVIEW}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_EDITION_MODULE) @FrontendAction(action = ProjectManagerConst.SET_PROJECT_CONFIGURATION_ACTION) - @PostMapping(value = ProjectManagerConst.SET_PROJECT_CONFIGURATION) - public ResponseEntity setProjectConfiguration( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @RequestParam(name = ProjectManagerConst.PROJECT_CONFIGURATION) String projectConfigurationName + @PutMapping(value = ProjectManagerConst.SET_PROJECT_CONFIGURATION) + public ResponseEntity setProjectConfiguration( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + @RequestVariable(name = ProjectManagerConst.PROJECT_CONFIGURATION) String projectConfigurationName ) { - return convertToResponseEntity(() -> this.projectService.setProjectConfiguration(projectCode, projectConfigurationName)); + return convertToResponseEntity( + () -> this.projectService.setProjectConfiguration(project, projectConfigurationName)); } @RoleConstraints(projectRoles = {ProjectRole.CREATOR}) - @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.CREATED}) + @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.REVIEW}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_EDITION_MODULE) @FrontendAction(action = ProjectManagerConst.FETCH_PROJECT_TYPES_ACTION) @GetMapping(value = ProjectManagerConst.FETCH_PROJECT_TYPES) - public ResponseEntity fetchProjectTypes( - // Project code needed for role constraints - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode + public ResponseEntity fetchProjectTypes( + // ProjectCode code needed for role constraints + @SuppressWarnings("unused") @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project ) { - return convertToResponseEntity(() -> ProjectType.values()); + return convertToResponseEntity(ProjectType::values); } @RoleConstraints(organisationRoles = {OrganisationRole.RESEARCHER}) @PostMapping(value = ProjectManagerConst.DESIGN_PROJECT) - public ResponseEntity designProject( - @RequestParam(name = ProjectManagerConst.BRIDGEHEADS) String[] bridgeheads, - @RequestParam(name = ProjectManagerConst.QUERY_CODE) String queryCode, - @RequestParam(name = ProjectManagerConst.PROJECT_TYPE) ProjectType projectType + public ResponseEntity designProject( + @RequestVariable(name = ProjectManagerConst.BRIDGEHEADS) String[] bridgeheads, + @RequestVariable(name = ProjectManagerConst.QUERY_CODE) String queryCode ) { - return convertToResponseEntity(() -> this.projectEventService.draft(bridgeheads, queryCode, projectType)); + return convertToResponseEntity(() -> this.projectEventService.draft(bridgeheads, queryCode)); } @RoleConstraints(projectRoles = {ProjectRole.CREATOR}) @StateConstraints(projectStates = {ProjectState.DRAFT}) - @EmailSender(templateType = EmailTemplateType.NEW_PROJECT, recipients = {EmailRecipientType.PROJECT_MANAGER_ADMIN}) + @EmailSender(templateType = EmailTemplateType.NEW_PROJECT, recipients = { + EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.CREATOR}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_STATE_MODULE) @FrontendAction(action = ProjectManagerConst.CREATE_PROJECT_ACTION) @PostMapping(value = ProjectManagerConst.CREATE_PROJECT) - public ResponseEntity createProject( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode + public ResponseEntity createProject( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project ) { - return convertToResponseEntity(() -> projectEventService.create(projectCode)); + return convertToResponseEntity(() -> projectEventService.create(project)); } @RoleConstraints(organisationRoles = {OrganisationRole.PROJECT_MANAGER_ADMIN}) - @StateConstraints(projectStates = {ProjectState.ACCEPTED}) + @StateConstraints(projectStates = {ProjectState.APPROVAL}) @ProjectConstraints(projectTypes = {ProjectType.DATASHIELD, ProjectType.RESEARCH_ENVIRONMENT}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_STATE_MODULE) @FrontendAction(action = ProjectManagerConst.START_DEVELOP_STAGE_ACTION) - @PostMapping(value = ProjectManagerConst.START_DEVELOP_STAGE) - public ResponseEntity startDevelopStage( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode + @PutMapping(value = ProjectManagerConst.START_DEVELOP_STAGE) + public ResponseEntity startDevelopStage( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project ) { - return convertToResponseEntity(() -> projectEventService.startDevelopStage(projectCode)); + return convertToResponseEntity(() -> projectEventService.startDevelopStage(project)); } @RoleConstraints(organisationRoles = {OrganisationRole.PROJECT_MANAGER_ADMIN}) @@ -497,58 +743,63 @@ public ResponseEntity startDevelopStage( @ProjectConstraints(projectTypes = {ProjectType.DATASHIELD, ProjectType.RESEARCH_ENVIRONMENT}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_STATE_MODULE) @FrontendAction(action = ProjectManagerConst.START_PILOT_STAGE_ACTION) - @PostMapping(value = ProjectManagerConst.START_PILOT_STAGE) - public ResponseEntity startPilotStage( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode + @PutMapping(value = ProjectManagerConst.START_PILOT_STAGE) + public ResponseEntity startPilotStage( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project ) { - return convertToResponseEntity(() -> projectEventService.startPilotStage(projectCode)); + return convertToResponseEntity(() -> projectEventService.startPilotStage(project)); } @RoleConstraints(organisationRoles = {OrganisationRole.PROJECT_MANAGER_ADMIN}) - @StateConstraints(projectStates = {ProjectState.PILOT, ProjectState.ACCEPTED}) - @EmailSender(templateType = EmailTemplateType.REQUEST_TECHNICAL_APPROVAL, recipients = {EmailRecipientType.BRIDGHEAD_ADMINS_WHO_HAVE_NOT_ACCEPTED_NOR_REJECTED_THE_PROJECT}) + @StateConstraints(projectStates = {ProjectState.PILOT, ProjectState.APPROVAL}) + @EmailSender(templateType = EmailTemplateType.REQUEST_TECHNICAL_APPROVAL, recipients = { + EmailRecipientType.BRIDGEHEAD_ADMINS_WHO_HAVE_NOT_ACCEPTED_NOR_REJECTED_THE_PROJECT}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_STATE_MODULE) @FrontendAction(action = ProjectManagerConst.START_FINAL_STAGE_ACTION) - @PostMapping(value = ProjectManagerConst.START_FINAL_STAGE) - public ResponseEntity startFinalStage( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode + @PutMapping(value = ProjectManagerConst.START_FINAL_STAGE) + public ResponseEntity startFinalStage( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project ) { - return convertToResponseEntity(() -> projectEventService.startFinalStage(projectCode)); + return convertToResponseEntity(() -> projectEventService.startFinalStage(project)); } @RoleConstraints(organisationRoles = {OrganisationRole.PROJECT_MANAGER_ADMIN}) - @StateConstraints(projectStates = {ProjectState.CREATED, ProjectState.ARCHIVED}) - @EmailSender(templateType = EmailTemplateType.NEW_PROJECT_ACCEPTED, recipients = {EmailRecipientType.CREATOR}) - @EmailSender(templateType = EmailTemplateType.NEW_PROJECT_ACCEPTED, recipients = {EmailRecipientType.ALL_BRIDGEHEAD_ADMINS}) + @StateConstraints(projectStates = {ProjectState.REVIEW, ProjectState.ARCHIVED}) + @EmailSender(templateType = EmailTemplateType.NEW_PROJECT_ACCEPTED, recipients = { + EmailRecipientType.CREATOR}) + @EmailSender(templateType = EmailTemplateType.NEW_PROJECT_ACCEPTED, recipients = { + EmailRecipientType.ALL_BRIDGEHEAD_ADMINS}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_STATE_MODULE) @FrontendAction(action = ProjectManagerConst.ACCEPT_PROJECT_ACTION) - @PostMapping(value = ProjectManagerConst.ACCEPT_PROJECT) - public ResponseEntity acceptProject( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode + @PutMapping(value = ProjectManagerConst.ACCEPT_PROJECT) + public ResponseEntity acceptProject( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project ) { - return convertToResponseEntity(() -> projectEventService.accept(projectCode)); + return convertToResponseEntity(() -> projectEventService.accept(project)); } @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.PROJECT_MANAGER_ADMIN}) - @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.CREATED, ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL}) - @EmailSender(templateType = EmailTemplateType.PROJECT_REJECTED, recipients = {EmailRecipientType.PROJECT_ALL}) + @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.REVIEW, ProjectState.DEVELOP, + ProjectState.PILOT, ProjectState.FINAL}) + @EmailSender(templateType = EmailTemplateType.PROJECT_REJECTED, recipients = { + EmailRecipientType.PROJECT_ALL}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_STATE_MODULE) @FrontendAction(action = ProjectManagerConst.REJECT_PROJECT_ACTION) - @PostMapping(value = ProjectManagerConst.REJECT_PROJECT) - public ResponseEntity rejectProject( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, + @PutMapping(value = ProjectManagerConst.REJECT_PROJECT) + public ResponseEntity rejectProject( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, // Message is sent per email - @Message @RequestParam(name = ProjectManagerConst.MESSAGE, required = false) String message + @SuppressWarnings("unused") @Message @RequestVariable(name = ProjectManagerConst.MESSAGE, required = false) String message ) { - return convertToResponseEntity(() -> projectEventService.reject(projectCode)); + return convertToResponseEntity(() -> projectEventService.reject(project)); } @RoleConstraints(organisationRoles = OrganisationRole.PROJECT_MANAGER_ADMIN) @FrontendSiteModule(site = ProjectManagerConst.CONFIGURATION_SITE, module = ProjectManagerConst.USER_MODULE) @FrontendAction(action = ProjectManagerConst.ADD_USER_TO_MAILING_BLACK_LIST_ACTION) @PostMapping(value = ProjectManagerConst.ADD_USER_TO_MAILING_BLACK_LIST) - public ResponseEntity addUserToMailingBlackList( - @Email @RequestParam(name = ProjectManagerConst.EMAIL) String email + public ResponseEntity addUserToMailingBlackList( + @Email @RequestVariable(name = ProjectManagerConst.EMAIL) String email ) { return convertToResponseEntity(() -> userService.updateUserInMailingBlackList(email, true)); } @@ -556,9 +807,9 @@ public ResponseEntity addUserToMailingBlackList( @RoleConstraints(organisationRoles = OrganisationRole.PROJECT_MANAGER_ADMIN) @FrontendSiteModule(site = ProjectManagerConst.CONFIGURATION_SITE, module = ProjectManagerConst.USER_MODULE) @FrontendAction(action = ProjectManagerConst.REMOVE_USER_FROM_MAILING_BLACK_LIST_ACTION) - @PostMapping(value = ProjectManagerConst.REMOVE_USER_FROM_MAILING_BLACK_LIST) - public ResponseEntity removeUserFromMailingBlackList( - @Email @RequestParam(name = ProjectManagerConst.EMAIL) String email + @DeleteMapping(value = ProjectManagerConst.REMOVE_USER_FROM_MAILING_BLACK_LIST) + public ResponseEntity removeUserFromMailingBlackList( + @Email @RequestParameter(name = ProjectManagerConst.EMAIL) String email ) { return convertToResponseEntity(() -> userService.updateUserInMailingBlackList(email, false)); } @@ -566,726 +817,983 @@ public ResponseEntity removeUserFromMailingBlackList( @RoleConstraints(organisationRoles = OrganisationRole.PROJECT_MANAGER_ADMIN) @FrontendSiteModule(site = ProjectManagerConst.CONFIGURATION_SITE, module = ProjectManagerConst.USER_MODULE) @FrontendAction(action = ProjectManagerConst.FETCH_MAILING_BLACK_LIST_ACTION) - @PostMapping(value = ProjectManagerConst.FETCH_MAILING_BLACK_LIST) - public ResponseEntity fetchMailingBlackList() { - return convertToResponseEntity(() -> userService.fetchMailingBlackList()); + @GetMapping(value = ProjectManagerConst.FETCH_MAILING_BLACK_LIST) + public ResponseEntity fetchMailingBlackList() { + return convertToResponseEntity(dtoUserService::fetchMailingBlackList); } @RoleConstraints(organisationRoles = OrganisationRole.PROJECT_MANAGER_ADMIN) @FrontendSiteModule(site = ProjectManagerConst.CONFIGURATION_SITE, module = ProjectManagerConst.USER_MODULE) @FrontendAction(action = ProjectManagerConst.FETCH_USERS_FOR_AUTOCOMPLETE_IN_MAILING_BLACK_LIST_ACTION) - @PostMapping(value = ProjectManagerConst.FETCH_USERS_FOR_AUTOCOMPLETE_IN_MAILING_BLACK_LIST) - public ResponseEntity fetchUsersForAutocompleteInMailingBlackList( - @RequestParam(name = ProjectManagerConst.EMAIL, required = false) String partialEmail + @GetMapping(value = ProjectManagerConst.FETCH_USERS_FOR_AUTOCOMPLETE_IN_MAILING_BLACK_LIST) + public ResponseEntity fetchUsersForAutocompleteInMailingBlackList( + @RequestParameter(name = ProjectManagerConst.EMAIL, required = false) String partialEmail ) { - return convertToResponseEntity(() -> userService.fetchUsersForAutocompleteInMailingBlackList(partialEmail)); + return convertToResponseEntity( + () -> dtoUserService.fetchUsersForAutocompleteInMailingBlackList(partialEmail)); } @RoleConstraints(projectRoles = {ProjectRole.BRIDGEHEAD_ADMIN}) - @StateConstraints(projectStates = {ProjectState.FINAL}, projectBridgeheadStates = {ProjectBridgeheadState.ACCEPTED}) - @EmailSender(templateType = EmailTemplateType.PROJECT_BRIDGHEAD_RESULTS_URL_ADDED, recipients = {EmailRecipientType.ALL_FINALS, EmailRecipientType.CREATOR}) + @StateConstraints(projectStates = {ProjectState.FINAL}, projectBridgeheadStates = { + ProjectBridgeheadState.ACCEPTED}) + @EmailSender(templateType = EmailTemplateType.PROJECT_BRIDGEHEAD_RESULTS_URL_ADDED, recipients = { + EmailRecipientType.ALL_FINALS, EmailRecipientType.CREATOR}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_RESULTS_MODULE) - @FrontendAction(action = ProjectManagerConst.ADD_PROJECT_BRIDGHEAD_RESULTS_URL_ACTION) - @PostMapping(value = ProjectManagerConst.ADD_PROJECT_BRIDGHEAD_RESULTS_URL) - public ResponseEntity addProjectBridgeheadResultsUrl( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead, - @RequestParam(name = ProjectManagerConst.RESULTS_URL) String resultsUrl + @FrontendAction(action = ProjectManagerConst.ADD_PROJECT_BRIDGEHEAD_RESULTS_URL_ACTION) + @PostMapping(value = ProjectManagerConst.ADD_PROJECT_BRIDGEHEAD_RESULTS_URL) + public ResponseEntity addProjectBridgeheadResultsUrl( + @SuppressWarnings("unused") @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestVariable(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead, + @RequestVariable(name = ProjectManagerConst.RESULTS_URL) String resultsUrl ) { - return convertToResponseEntity(() -> projectBridgeheadService.addResultsUrl(projectCode, bridgehead, resultsUrl)); + return convertToResponseEntity( + () -> projectBridgeheadService.addResultsUrl(bridgehead, resultsUrl)); } @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.FINAL}) @StateConstraints(projectStates = {ProjectState.FINAL}) + @ProjectConstraints(projectTypes = {ProjectType.DATASHIELD, ProjectType.RESEARCH_ENVIRONMENT, + ProjectType.EXPORT}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_RESULTS_MODULE) @FrontendAction(action = ProjectManagerConst.FETCH_PROJECT_RESULTS_ACTION) @GetMapping(value = ProjectManagerConst.FETCH_PROJECT_RESULTS) - public ResponseEntity fetchProjectResults( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead + public ResponseEntity fetchProjectResults( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @SuppressWarnings("unused") @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead ) { - return convertOptionalToResponseEntity(() -> projectService.fetchResults(projectCode)); + return convertOptionalToResponseEntity(() -> dtoProjectService.fetchResults(project)); } @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.FINAL}) @StateConstraints(projectStates = {ProjectState.FINAL}) + @ProjectConstraints(projectTypes = {ProjectType.DATASHIELD, ProjectType.RESEARCH_ENVIRONMENT, + ProjectType.EXPORT}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_RESULTS_MODULE) @FrontendAction(action = ProjectManagerConst.FETCH_PROJECT_BRIDGEHEAD_RESULTS_ACTION) @GetMapping(value = ProjectManagerConst.FETCH_PROJECT_BRIDGEHEAD_RESULTS) - public ResponseEntity fetchProjectBridgeheadResults( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead + public ResponseEntity fetchProjectBridgeheadResults( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @SuppressWarnings("unused") @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead ) { - return convertToResponseEntity(() -> projectBridgeheadService.fetchResults(projectCode)); + return convertToResponseEntity(() -> dtoProjectBridgeheadService.fetchResults(project)); } @RoleConstraints(projectRoles = {ProjectRole.BRIDGEHEAD_ADMIN}) @StateConstraints(projectStates = {ProjectState.FINAL}) + @ProjectConstraints(projectTypes = {ProjectType.DATASHIELD, ProjectType.RESEARCH_ENVIRONMENT, + ProjectType.EXPORT}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_RESULTS_MODULE) @FrontendAction(action = ProjectManagerConst.FETCH_PROJECT_BRIDGEHEAD_RESULTS_FOR_OWN_BRIDGEHEAD_ACTION) @GetMapping(value = ProjectManagerConst.FETCH_PROJECT_BRIDGEHEAD_RESULTS_FOR_OWN_BRIDGEHEAD) - public ResponseEntity fetchProjectBridgeheadResultsForOwnBridgehead( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead + public ResponseEntity fetchProjectBridgeheadResultsForOwnBridgehead( + @SuppressWarnings("unused") @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead ) { - return convertOptionalToResponseEntity(() -> projectBridgeheadService.fetchResultsOfOwnBridgehead(projectCode, bridgehead)); + return convertOptionalToResponseEntity(() -> Optional.of( + dtoProjectBridgeheadService.fetchResultsOfOwnBridgehead(bridgehead))); } @RoleConstraints(projectRoles = {ProjectRole.FINAL}) - @StateConstraints(projectStates = {ProjectState.FINAL}, userProjectStates = {UserProjectState.ACCEPTED}) - @EmailSender(templateType = EmailTemplateType.PROJECT_RESULTS_URL_ADDED, recipients = {EmailRecipientType.CREATOR}) + @StateConstraints(projectStates = {ProjectState.FINAL}, userProjectStates = { + UserProjectState.ACCEPTED}) + @ProjectConstraints(projectTypes = {ProjectType.DATASHIELD, ProjectType.RESEARCH_ENVIRONMENT, + ProjectType.EXPORT}) + @EmailSender(templateType = EmailTemplateType.PROJECT_RESULTS_URL_ADDED, recipients = { + EmailRecipientType.CREATOR}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_RESULTS_MODULE) @FrontendAction(action = ProjectManagerConst.ADD_PROJECT_RESULTS_URL_ACTION) @PostMapping(value = ProjectManagerConst.ADD_PROJECT_RESULTS_URL) - public ResponseEntity addProjectResultsUrl( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead, - @RequestParam(name = ProjectManagerConst.RESULTS_URL) String resultsUrl + public ResponseEntity addProjectResultsUrl( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + @SuppressWarnings("unused") @Bridgehead @RequestVariable(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead, + @RequestVariable(name = ProjectManagerConst.RESULTS_URL) String resultsUrl ) { - return convertToResponseEntity(() -> projectService.addProjectResultUrl(projectCode, resultsUrl)); + return convertToResponseEntity(() -> projectService.addProjectResultUrl(project, resultsUrl)); } @RoleConstraints(projectRoles = {ProjectRole.CREATOR}) @StateConstraints(projectStates = {ProjectState.FINAL}) - @EmailSender(templateType = EmailTemplateType.PROJECT_RESULTS_ACCEPTED_BY_CREATOR, recipients = {EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.ALL_FINALS}) + @ProjectConstraints(projectTypes = {ProjectType.DATASHIELD, ProjectType.RESEARCH_ENVIRONMENT, + ProjectType.EXPORT}) + @EmailSender(templateType = EmailTemplateType.PROJECT_RESULTS_ACCEPTED_BY_CREATOR, recipients = { + EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.ALL_FINALS}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_RESULTS_MODULE) @FrontendAction(action = ProjectManagerConst.ACCEPT_PROJECT_RESULTS_URL_ACTION) - @PostMapping(value = ProjectManagerConst.ACCEPT_PROJECT_RESULTS_URL) - public ResponseEntity acceptProjectResultsUrlByCreator( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead + @PutMapping(value = ProjectManagerConst.ACCEPT_PROJECT_RESULTS_URL) + public ResponseEntity acceptProjectResultsUrlByCreator( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + @SuppressWarnings("unused") @Bridgehead @RequestVariable(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead ) { - return convertToResponseEntity(() -> projectService.acceptResultsByCreator(projectCode)); + return convertToResponseEntity(() -> projectService.acceptResultsByCreator(project)); } @RoleConstraints(projectRoles = {ProjectRole.CREATOR}) @StateConstraints(projectStates = {ProjectState.FINAL}) - @EmailSender(templateType = EmailTemplateType.PROJECT_RESULTS_REJECTED_BY_CREATOR, recipients = {EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.ALL_FINALS}) + @ProjectConstraints(projectTypes = {ProjectType.DATASHIELD, ProjectType.RESEARCH_ENVIRONMENT, + ProjectType.EXPORT}) + @EmailSender(templateType = EmailTemplateType.PROJECT_RESULTS_REJECTED_BY_CREATOR, recipients = { + EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.ALL_FINALS}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_RESULTS_MODULE) @FrontendAction(action = ProjectManagerConst.REJECT_PROJECT_RESULTS_URL_ACTION) - @PostMapping(value = ProjectManagerConst.REJECT_PROJECT_RESULTS_URL) - public ResponseEntity rejectProjectResultsUrlByCreator( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead + @PutMapping(value = ProjectManagerConst.REJECT_PROJECT_RESULTS_URL) + public ResponseEntity rejectProjectResultsUrlByCreator( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + @SuppressWarnings("unused") @Bridgehead @RequestVariable(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead ) { - return convertToResponseEntity(() -> projectService.rejectResultsForCreator(projectCode)); + return convertToResponseEntity(() -> projectService.rejectResultsForCreator(project)); } @RoleConstraints(projectRoles = {ProjectRole.CREATOR}) @StateConstraints(projectStates = {ProjectState.FINAL}) - @EmailSender(templateType = EmailTemplateType.PROJECT_RESULTS_REQUESTED_CHANGES_BY_CREATOR, recipients = {EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.ALL_FINALS}) + @ProjectConstraints(projectTypes = {ProjectType.DATASHIELD, ProjectType.RESEARCH_ENVIRONMENT, + ProjectType.EXPORT}) + @EmailSender(templateType = EmailTemplateType.PROJECT_RESULTS_REQUESTED_CHANGES_BY_CREATOR, recipients = { + EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.ALL_FINALS}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_RESULTS_MODULE) @FrontendAction(action = ProjectManagerConst.REQUEST_CHANGES_IN_PROJECT_RESULTS_URL_ACTION) - @PostMapping(value = ProjectManagerConst.REQUEST_CHANGES_IN_PROJECT_RESULTS_URL) - public ResponseEntity requestChangesInProjectResultsUrlByCreator( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead + @PutMapping(value = ProjectManagerConst.REQUEST_CHANGES_IN_PROJECT_RESULTS_URL) + public ResponseEntity requestChangesInProjectResultsUrlByCreator( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + @SuppressWarnings("unused") @Bridgehead @RequestVariable(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead ) { - return convertToResponseEntity(() -> projectService.requestChangesInResultsForCreator(projectCode)); + return convertToResponseEntity(() -> projectService.requestChangesInResultsForCreator(project)); } @RoleConstraints(projectRoles = {ProjectRole.CREATOR}) @StateConstraints(projectStates = {ProjectState.FINAL}) - @EmailSender(templateType = EmailTemplateType.PROJECT_BRIDGEHEAD_RESULTS_ACCEPTED_BY_CREATOR, recipients = {EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.BRIDGEHEAD_ADMIN, EmailRecipientType.ALL_FINALS}) + @ProjectConstraints(projectTypes = {ProjectType.DATASHIELD, ProjectType.RESEARCH_ENVIRONMENT, + ProjectType.EXPORT}) + @EmailSender(templateType = EmailTemplateType.PROJECT_BRIDGEHEAD_RESULTS_ACCEPTED_BY_CREATOR, recipients = { + EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.BRIDGEHEAD_ADMIN, + EmailRecipientType.ALL_FINALS}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_RESULTS_MODULE) @FrontendAction(action = ProjectManagerConst.ACCEPT_PROJECT_BRIDGEHEAD_RESULTS_URL_ACTION) - @PostMapping(value = ProjectManagerConst.ACCEPT_PROJECT_BRIDGEHEAD_RESULTS_URL) - public ResponseEntity acceptProjectBridgeheadResultsUrlByCreator( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead + @PutMapping(value = ProjectManagerConst.ACCEPT_PROJECT_BRIDGEHEAD_RESULTS_URL) + public ResponseEntity acceptProjectBridgeheadResultsUrlByCreator( + @SuppressWarnings("unused") @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestVariable(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead ) { - return convertToResponseEntity(() -> projectBridgeheadService.acceptResultsForCreator(projectCode, bridgehead)); + return convertToResponseEntity( + () -> projectBridgeheadService.acceptResultsForCreator(bridgehead)); } @RoleConstraints(projectRoles = {ProjectRole.CREATOR}) @StateConstraints(projectStates = {ProjectState.FINAL}) - @EmailSender(templateType = EmailTemplateType.PROJECT_BRIDGEHEAD_RESULTS_REJECTED_BY_CREATOR, recipients = {EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.BRIDGEHEAD_ADMIN, EmailRecipientType.ALL_FINALS}) + @ProjectConstraints(projectTypes = {ProjectType.DATASHIELD, ProjectType.RESEARCH_ENVIRONMENT, + ProjectType.EXPORT}) + @EmailSender(templateType = EmailTemplateType.PROJECT_BRIDGEHEAD_RESULTS_REJECTED_BY_CREATOR, recipients = { + EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.BRIDGEHEAD_ADMIN, + EmailRecipientType.ALL_FINALS}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_RESULTS_MODULE) @FrontendAction(action = ProjectManagerConst.REJECT_PROJECT_BRIDGEHEAD_RESULTS_URL_ACTION) - @PostMapping(value = ProjectManagerConst.REJECT_PROJECT_BRIDGEHEAD_RESULTS_URL) - public ResponseEntity rejectProjectBridgeheadResultsUrlByCreator( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead + @PutMapping(value = ProjectManagerConst.REJECT_PROJECT_BRIDGEHEAD_RESULTS_URL) + public ResponseEntity rejectProjectBridgeheadResultsUrlByCreator( + @SuppressWarnings("unused") @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestVariable(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead ) { - return convertToResponseEntity(() -> projectBridgeheadService.rejectResultsForCreator(projectCode, bridgehead)); + return convertToResponseEntity( + () -> projectBridgeheadService.rejectResultsForCreator(bridgehead)); } @RoleConstraints(projectRoles = {ProjectRole.CREATOR}) @StateConstraints(projectStates = {ProjectState.FINAL}) - @EmailSender(templateType = EmailTemplateType.PROJECT_BRIDGEHEAD_RESULTS_REQUESTED_CHANGES_BY_CREATOR, recipients = {EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.BRIDGEHEAD_ADMIN, EmailRecipientType.ALL_FINALS}) + @ProjectConstraints(projectTypes = {ProjectType.DATASHIELD, ProjectType.RESEARCH_ENVIRONMENT, + ProjectType.EXPORT}) + @EmailSender(templateType = EmailTemplateType.PROJECT_BRIDGEHEAD_RESULTS_REQUESTED_CHANGES_BY_CREATOR, recipients = { + EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.BRIDGEHEAD_ADMIN, + EmailRecipientType.ALL_FINALS}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_RESULTS_MODULE) @FrontendAction(action = ProjectManagerConst.REQUEST_CHANGES_IN_PROJECT_BRIDGEHEAD_RESULTS_URL_ACTION) - @PostMapping(value = ProjectManagerConst.REQUEST_CHANGES_IN_PROJECT_BRIDGEHEAD_RESULTS_URL) - public ResponseEntity requestChangesProjectBridgeheadResultsUrlByCreator( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead + @PutMapping(value = ProjectManagerConst.REQUEST_CHANGES_IN_PROJECT_BRIDGEHEAD_RESULTS_URL) + public ResponseEntity requestChangesProjectBridgeheadResultsUrlByCreator( + @SuppressWarnings("unused") @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestVariable(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead ) { - return convertToResponseEntity(() -> projectBridgeheadService.requestChangesInResultsForCreator(projectCode, bridgehead)); + return convertToResponseEntity( + () -> projectBridgeheadService.requestChangesInResultsForCreator(bridgehead)); } - @RoleConstraints(organisationRoles = {OrganisationRole.RESEARCHER, OrganisationRole.BRIDGEHEAD_ADMIN, OrganisationRole.PROJECT_MANAGER_ADMIN}) + @RoleConstraints(organisationRoles = {OrganisationRole.RESEARCHER, + OrganisationRole.BRIDGEHEAD_ADMIN, OrganisationRole.PROJECT_MANAGER_ADMIN}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_RESULTS_MODULE) @FrontendAction(action = ProjectManagerConst.FETCH_EMAIL_MESSAGE_AND_SUBJECT_ACTION) - @PostMapping(value = ProjectManagerConst.FETCH_EMAIL_MESSAGE_AND_SUBJECT) - public ResponseEntity fetchEmailMessageAndSubject( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE, required = false) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD, required = false) String bridgehead, - @Email @RequestParam(name = ProjectManagerConst.EMAIL) String email, - @RequestParam(name = ProjectManagerConst.PROJECT_ROLE) ProjectRole projectRole, - @RequestParam(name = ProjectManagerConst.EMAIL_TEMPLATE_TYPE) EmailTemplateType emailTemplateType + @GetMapping(value = ProjectManagerConst.FETCH_EMAIL_MESSAGE_AND_SUBJECT) + public ResponseEntity fetchEmailMessageAndSubject( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE, required = false) Project project, + @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD, required = false) ProjectBridgehead bridgehead, + @Email @RequestParameter(name = ProjectManagerConst.EMAIL) String email, + @RequestParameter(name = ProjectManagerConst.PROJECT_ROLE) ProjectRole projectRole, + @RequestParameter(name = ProjectManagerConst.EMAIL_TEMPLATE_TYPE) EmailTemplateType emailTemplateType ) { - return convertOptionalToResponseEntity(() -> emailService.createEmailMessageAndSubject(email, Optional.of(projectCode), Optional.of(bridgehead), projectRole, emailTemplateType)); + return convertOptionalToResponseEntity( + () -> emailService.createEmailMessageAndSubject(email, Optional.ofNullable(project), + Optional.ofNullable(bridgehead), projectRole, emailTemplateType)); } @RoleConstraints(projectRoles = {ProjectRole.BRIDGEHEAD_ADMIN}) - @StateConstraints(projectStates = {ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL}, queryStates = {QueryState.FINISHED, QueryState.ERROR}) - @EmailSender(templateType = EmailTemplateType.PROJECT_BRIDGEHEAD_ACCEPTED, recipients = {EmailRecipientType.PROJECT_MANAGER_ADMIN}) + @StateConstraints(projectStates = {ProjectState.DEVELOP, ProjectState.PILOT, + ProjectState.FINAL}, queryStates = {QueryState.FINISHED, QueryState.ERROR}) + @EmailSender(templateType = EmailTemplateType.PROJECT_BRIDGEHEAD_ACCEPTED, recipients = { + EmailRecipientType.PROJECT_MANAGER_ADMIN}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_STATE_MODULE) @FrontendAction(action = ProjectManagerConst.ACCEPT_BRIDGEHEAD_PROJECT_ACTION) - @PostMapping(value = ProjectManagerConst.ACCEPT_BRIDGEHEAD_PROJECT) - public ResponseEntity acceptBridgeheadProject( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead + @PutMapping(value = ProjectManagerConst.ACCEPT_BRIDGEHEAD_PROJECT) + public ResponseEntity acceptBridgeheadProject( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestVariable(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead ) { - return convertToResponseEntity(() -> projectBridgeheadService.acceptProject(projectCode, bridgehead)); + return convertToResponseEntity( + () -> projectBridgeheadService.acceptProject(project, bridgehead)); } @RoleConstraints(projectRoles = {ProjectRole.BRIDGEHEAD_ADMIN}) - @StateConstraints(projectStates = {ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL}, queryStates = {QueryState.FINISHED, QueryState.ERROR}) - @EmailSender(templateType = EmailTemplateType.PROJECT_BRIDGEHEAD_REJECTED, recipients = {EmailRecipientType.PROJECT_MANAGER_ADMIN}) + @StateConstraints(projectStates = {ProjectState.DEVELOP, ProjectState.PILOT, + ProjectState.FINAL}, queryStates = {QueryState.FINISHED, QueryState.ERROR}) + @EmailSender(templateType = EmailTemplateType.PROJECT_BRIDGEHEAD_REJECTED, recipients = { + EmailRecipientType.PROJECT_MANAGER_ADMIN}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_STATE_MODULE) @FrontendAction(action = ProjectManagerConst.REJECT_BRIDGEHEAD_PROJECT_ACTION) - @PostMapping(value = ProjectManagerConst.REJECT_BRIDGEHEAD_PROJECT) - public ResponseEntity rejectBridgeheadProject( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead, + @PutMapping(value = ProjectManagerConst.REJECT_BRIDGEHEAD_PROJECT) + public ResponseEntity rejectBridgeheadProject( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestVariable(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead, // Message is sent per email - @Message @RequestParam(name = ProjectManagerConst.MESSAGE, required = false) String message + @SuppressWarnings("unused") @Message @RequestVariable(name = ProjectManagerConst.MESSAGE, required = false) String message ) { - return convertToResponseEntity(() -> projectBridgeheadService.rejectProject(projectCode, bridgehead)); + return convertToResponseEntity( + () -> projectBridgeheadService.rejectProject(project, bridgehead)); } @RoleConstraints(projectRoles = {ProjectRole.DEVELOPER, ProjectRole.PILOT}) @StateConstraints(projectStates = {ProjectState.DEVELOP, ProjectState.PILOT}) @ProjectConstraints(projectTypes = {ProjectType.DATASHIELD}) - @EmailSender(templateType = EmailTemplateType.SCRIPT_ACCEPTED, recipients = {EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.ALL_DEVELOPERS, EmailRecipientType.ALL_PILOTS}) + @EmailSender(templateType = EmailTemplateType.SCRIPT_ACCEPTED, recipients = { + EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.ALL_DEVELOPERS, + EmailRecipientType.ALL_PILOTS}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_STATE_MODULE) @FrontendAction(action = ProjectManagerConst.ACCEPT_SCRIPT_ACTION) - @PostMapping(value = ProjectManagerConst.ACCEPT_SCRIPT) - public ResponseEntity acceptScript( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead + @PutMapping(value = ProjectManagerConst.ACCEPT_SCRIPT) + public ResponseEntity acceptScript( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestVariable(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead ) { - return convertToResponseEntity(() -> userService.acceptProject(projectCode, bridgehead)); + return convertToResponseEntity(() -> userService.acceptProject(project, bridgehead)); } @RoleConstraints(projectRoles = {ProjectRole.DEVELOPER, ProjectRole.PILOT}) @StateConstraints(projectStates = {ProjectState.DEVELOP, ProjectState.PILOT}) @ProjectConstraints(projectTypes = {ProjectType.DATASHIELD}) - @EmailSender(templateType = EmailTemplateType.SCRIPT_REJECTED, recipients = {EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.ALL_DEVELOPERS, EmailRecipientType.ALL_PILOTS}) + @EmailSender(templateType = EmailTemplateType.SCRIPT_REJECTED, recipients = { + EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.ALL_DEVELOPERS, + EmailRecipientType.ALL_PILOTS}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_STATE_MODULE) @FrontendAction(action = ProjectManagerConst.REJECT_SCRIPT_ACTION) - @PostMapping(value = ProjectManagerConst.REJECT_SCRIPT) - public ResponseEntity rejectScript( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead, + @PutMapping(value = ProjectManagerConst.REJECT_SCRIPT) + public ResponseEntity rejectScript( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestVariable(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead, // Message is sent per email - @Message @RequestParam(name = ProjectManagerConst.MESSAGE, required = false) String message + @SuppressWarnings("unused") @Message @RequestVariable(name = ProjectManagerConst.MESSAGE, required = false) String message ) { - return convertToResponseEntity(() -> userService.rejectProject(projectCode, bridgehead)); + return convertToResponseEntity(() -> userService.rejectProject(project, bridgehead)); } @RoleConstraints(projectRoles = {ProjectRole.DEVELOPER, ProjectRole.PILOT}) @StateConstraints(projectStates = {ProjectState.DEVELOP, ProjectState.PILOT}) @ProjectConstraints(projectTypes = {ProjectType.DATASHIELD}) - @EmailSender(templateType = EmailTemplateType.REQUEST_CHANGES_IN_SCRIPT, recipients = {EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.ALL_DEVELOPERS, EmailRecipientType.ALL_PILOTS}) + @EmailSender(templateType = EmailTemplateType.REQUEST_CHANGES_IN_SCRIPT, recipients = { + EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.ALL_DEVELOPERS, + EmailRecipientType.ALL_PILOTS}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_STATE_MODULE) @FrontendAction(action = ProjectManagerConst.REQUEST_SCRIPT_CHANGES_ACTION) - @PostMapping(value = ProjectManagerConst.REQUEST_SCRIPT_CHANGES) - public ResponseEntity requestChangesInScript( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead, + @PutMapping(value = ProjectManagerConst.REQUEST_SCRIPT_CHANGES) + public ResponseEntity requestChangesInScript( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestVariable(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead, // Message is sent per email - @Message @RequestParam(name = ProjectManagerConst.MESSAGE, required = false) String message + @SuppressWarnings("unused") @Message @RequestVariable(name = ProjectManagerConst.MESSAGE, required = false) String message ) { - return convertToResponseEntity(() -> userService.requestChangesInProject(projectCode, bridgehead)); + return convertToResponseEntity(() -> userService.requestChangesInProject(project, bridgehead)); } @RoleConstraints(projectRoles = {ProjectRole.FINAL}) @StateConstraints(projectStates = {ProjectState.FINAL}) - @EmailSender(templateType = EmailTemplateType.RESULTS_ACCEPTED, recipients = {EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.ALL_FINALS}) + @ProjectConstraints(projectTypes = {ProjectType.DATASHIELD, ProjectType.RESEARCH_ENVIRONMENT, + ProjectType.EXPORT}) + @EmailSender(templateType = EmailTemplateType.RESULTS_ACCEPTED, recipients = { + EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.ALL_FINALS}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_STATE_MODULE) @FrontendAction(action = ProjectManagerConst.ACCEPT_PROJECT_RESULTS_ACTION) - @PostMapping(value = ProjectManagerConst.ACCEPT_PROJECT_RESULTS) - public ResponseEntity acceptProjectResults( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead + @PutMapping(value = ProjectManagerConst.ACCEPT_PROJECT_RESULTS) + public ResponseEntity acceptProjectResults( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestVariable(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead ) { - return convertToResponseEntity(() -> userService.acceptProject(projectCode, bridgehead)); + return convertToResponseEntity(() -> userService.acceptProject(project, bridgehead)); } @RoleConstraints(projectRoles = {ProjectRole.FINAL}) @StateConstraints(projectStates = {ProjectState.FINAL}) - @EmailSender(templateType = EmailTemplateType.RESULTS_REJECTED, recipients = {EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.ALL_FINALS}) + @ProjectConstraints(projectTypes = {ProjectType.DATASHIELD, ProjectType.RESEARCH_ENVIRONMENT, + ProjectType.EXPORT}) + @EmailSender(templateType = EmailTemplateType.RESULTS_REJECTED, recipients = { + EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.ALL_FINALS}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_STATE_MODULE) @FrontendAction(action = ProjectManagerConst.REJECT_PROJECT_RESULTS_ACTION) - @PostMapping(value = ProjectManagerConst.REJECT_PROJECT_RESULTS) - public ResponseEntity rejectProjectResults( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead, + @PutMapping(value = ProjectManagerConst.REJECT_PROJECT_RESULTS) + public ResponseEntity rejectProjectResults( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestVariable(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead, // Message is sent per email - @Message @RequestParam(name = ProjectManagerConst.MESSAGE, required = false) String message + @SuppressWarnings("unused") @Message @RequestVariable(name = ProjectManagerConst.MESSAGE, required = false) String message ) { - return convertToResponseEntity(() -> userService.rejectProject(projectCode, bridgehead)); + return convertToResponseEntity(() -> userService.rejectProject(project, bridgehead)); } @RoleConstraints(projectRoles = {ProjectRole.FINAL}) @StateConstraints(projectStates = {ProjectState.FINAL}) - @EmailSender(templateType = EmailTemplateType.REQUEST_CHANGES_IN_PROJECT, recipients = {EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.ALL_FINALS}) + @EmailSender(templateType = EmailTemplateType.REQUEST_CHANGES_IN_PROJECT, recipients = { + EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.ALL_FINALS}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_STATE_MODULE) @FrontendAction(action = ProjectManagerConst.REQUEST_CHANGES_IN_PROJECT_ACTION) - @PostMapping(value = ProjectManagerConst.REQUEST_CHANGES_IN_PROJECT) - public ResponseEntity requestChangesInProject( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead, + @PutMapping(value = ProjectManagerConst.REQUEST_CHANGES_IN_PROJECT) + public ResponseEntity requestChangesInProject( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestVariable(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead, // Message is sent per email - @Message @RequestParam(name = ProjectManagerConst.MESSAGE, required = false) String message + @SuppressWarnings("unused") @Message @RequestVariable(name = ProjectManagerConst.MESSAGE, required = false) String message ) { - return convertToResponseEntity(() -> userService.requestChangesInProject(projectCode, bridgehead)); + return convertToResponseEntity(() -> userService.requestChangesInProject(project, bridgehead)); } @RoleConstraints(projectRoles = {ProjectRole.DEVELOPER, ProjectRole.PILOT}) @StateConstraints(projectStates = {ProjectState.DEVELOP, ProjectState.PILOT}) @ProjectConstraints(projectTypes = {ProjectType.RESEARCH_ENVIRONMENT}) - @EmailSender(templateType = EmailTemplateType.ANALYSIS_ACCEPTED, recipients = {EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.ALL_DEVELOPERS, EmailRecipientType.ALL_PILOTS, EmailRecipientType.ALL_FINALS}) + @EmailSender(templateType = EmailTemplateType.ANALYSIS_ACCEPTED, recipients = { + EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.ALL_DEVELOPERS, + EmailRecipientType.ALL_PILOTS, EmailRecipientType.ALL_FINALS}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_STATE_MODULE) @FrontendAction(action = ProjectManagerConst.ACCEPT_PROJECT_ANALYSIS_ACTION) - @PostMapping(value = ProjectManagerConst.ACCEPT_PROJECT_ANALYSIS) - public ResponseEntity acceptProjectAnalysis( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead + @PutMapping(value = ProjectManagerConst.ACCEPT_PROJECT_ANALYSIS) + public ResponseEntity acceptProjectAnalysis( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestVariable(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead ) { - return convertToResponseEntity(() -> userService.acceptProject(projectCode, bridgehead)); + return convertToResponseEntity(() -> userService.acceptProject(project, bridgehead)); } @RoleConstraints(projectRoles = {ProjectRole.DEVELOPER, ProjectRole.PILOT}) @StateConstraints(projectStates = {ProjectState.DEVELOP, ProjectState.PILOT}) @ProjectConstraints(projectTypes = {ProjectType.RESEARCH_ENVIRONMENT}) - @EmailSender(templateType = EmailTemplateType.ANALYSIS_REJECTED, recipients = {EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.ALL_DEVELOPERS, EmailRecipientType.ALL_PILOTS, EmailRecipientType.ALL_FINALS}) + @EmailSender(templateType = EmailTemplateType.ANALYSIS_REJECTED, recipients = { + EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.ALL_DEVELOPERS, + EmailRecipientType.ALL_PILOTS, EmailRecipientType.ALL_FINALS}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_STATE_MODULE) @FrontendAction(action = ProjectManagerConst.REJECT_PROJECT_ANALYSIS_ACTION) - @PostMapping(value = ProjectManagerConst.REJECT_PROJECT_ANALYSIS) - public ResponseEntity rejectProjectAnalysis( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead, + @PutMapping(value = ProjectManagerConst.REJECT_PROJECT_ANALYSIS) + public ResponseEntity rejectProjectAnalysis( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestVariable(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead, // Message is sent per email - @Message @RequestParam(name = ProjectManagerConst.MESSAGE, required = false) String message + @SuppressWarnings("unused") @Message @RequestVariable(name = ProjectManagerConst.MESSAGE, required = false) String message ) { - return convertToResponseEntity(() -> userService.rejectProject(projectCode, bridgehead)); + return convertToResponseEntity(() -> userService.rejectProject(project, bridgehead)); } @RoleConstraints(projectRoles = {ProjectRole.DEVELOPER, ProjectRole.PILOT}) @StateConstraints(projectStates = {ProjectState.DEVELOP, ProjectState.PILOT}) @ProjectConstraints(projectTypes = {ProjectType.RESEARCH_ENVIRONMENT}) - @EmailSender(templateType = EmailTemplateType.REQUEST_CHANGES_IN_PROJECT_ANALYSIS, recipients = {EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.ALL_DEVELOPERS, EmailRecipientType.ALL_PILOTS, EmailRecipientType.ALL_FINALS}) + @EmailSender(templateType = EmailTemplateType.REQUEST_CHANGES_IN_PROJECT_ANALYSIS, recipients = { + EmailRecipientType.PROJECT_MANAGER_ADMIN, EmailRecipientType.ALL_DEVELOPERS, + EmailRecipientType.ALL_PILOTS, EmailRecipientType.ALL_FINALS}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_STATE_MODULE) @FrontendAction(action = ProjectManagerConst.REQUEST_CHANGES_IN_PROJECT_ANALYSIS_ACTION) - @PostMapping(value = ProjectManagerConst.REQUEST_CHANGES_IN_PROJECT_ANALYSIS) - public ResponseEntity requestChangesInProjectAnalysis( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead, + @PutMapping(value = ProjectManagerConst.REQUEST_CHANGES_IN_PROJECT_ANALYSIS) + public ResponseEntity requestChangesInProjectAnalysis( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestVariable(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead, // Message is sent per email - @Message @RequestParam(name = ProjectManagerConst.MESSAGE, required = false) String message + @SuppressWarnings("unused") @Message @RequestVariable(name = ProjectManagerConst.MESSAGE, required = false) String message ) { - return convertToResponseEntity(() -> userService.requestChangesInProject(projectCode, bridgehead)); + return convertToResponseEntity(() -> userService.requestChangesInProject(project, bridgehead)); } @RoleConstraints(organisationRoles = {OrganisationRole.PROJECT_MANAGER_ADMIN}) - @StateConstraints(projectStates = {ProjectState.CREATED, ProjectState.ACCEPTED, ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL}) + @StateConstraints(projectStates = {ProjectState.REVIEW, ProjectState.APPROVAL, + ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_STATE_MODULE) @FrontendAction(action = ProjectManagerConst.ARCHIVE_PROJECT_ACTION) - @PostMapping(value = ProjectManagerConst.ARCHIVE_PROJECT) - public ResponseEntity archiveProject( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode + @PutMapping(value = ProjectManagerConst.ARCHIVE_PROJECT) + public ResponseEntity archiveProject( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project ) { - return convertToResponseEntity(() -> projectEventService.archive(projectCode)); + return convertToResponseEntity(() -> projectEventService.archive(project)); } @RoleConstraints(organisationRoles = {OrganisationRole.PROJECT_MANAGER_ADMIN}) @StateConstraints(projectStates = {ProjectState.FINAL}) - @EmailSender(templateType = EmailTemplateType.FINISHED_PROJECT, recipients = {EmailRecipientType.PROJECT_ALL}) + @EmailSender(templateType = EmailTemplateType.FINISHED_PROJECT, recipients = { + EmailRecipientType.PROJECT_ALL}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_STATE_MODULE) @FrontendAction(action = ProjectManagerConst.FINISH_PROJECT_ACTION) - @PostMapping(value = ProjectManagerConst.FINISH_PROJECT) - public ResponseEntity finishProject( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode + @PutMapping(value = ProjectManagerConst.FINISH_PROJECT) + public ResponseEntity finishProject( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project ) { - return convertToResponseEntity(() -> projectEventService.finish(projectCode)); + return convertToResponseEntity(() -> projectEventService.finish(project)); } - @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.DEVELOPER, ProjectRole.PILOT, ProjectRole.FINAL, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.DEVELOPER, ProjectRole.PILOT, + ProjectRole.FINAL, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) @FrontendAction(action = ProjectManagerConst.UPLOAD_OTHER_DOCUMENT_ACTION) @PostMapping(value = ProjectManagerConst.UPLOAD_OTHER_DOCUMENT) - public ResponseEntity uploadOtherDocument( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, + public ResponseEntity uploadOtherDocument( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, // bridgehead required for identifying developer, pilot, final user or bridgehead admin in role constraints - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD, required = false) String bridgehead, - @RequestParam(name = ProjectManagerConst.LABEL, required = false) String label, - @RequestParam(name = ProjectManagerConst.DOCUMENT) MultipartFile document + @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD, required = false) ProjectBridgehead bridgehead, + @RequestParameter(name = ProjectManagerConst.LABEL, required = false) String label, + @RequestParameter(name = ProjectManagerConst.DOCUMENT) MultipartFile document ) { return convertToResponseEntity(() -> this.documentService.uploadDocument( - projectCode, Optional.of(bridgehead), document, DocumentType.OTHERS, Optional.ofNullable(label))); + project, Optional.ofNullable(bridgehead), document, DocumentType.OTHERS, + Optional.ofNullable(label))); } - @RoleConstraints(organisationRoles = {OrganisationRole.RESEARCHER, OrganisationRole.BRIDGEHEAD_ADMIN, OrganisationRole.PROJECT_MANAGER_ADMIN}) + @RoleConstraints(organisationRoles = {OrganisationRole.RESEARCHER, + OrganisationRole.BRIDGEHEAD_ADMIN, OrganisationRole.PROJECT_MANAGER_ADMIN}) @StateConstraints(projectStates = {ProjectState.FINISHED}) - @EmailSender(templateType = EmailTemplateType.NEW_PUBLICATION, recipients = {EmailRecipientType.PROJECT_ALL}) + @EmailSender(templateType = EmailTemplateType.NEW_PUBLICATION, recipients = { + EmailRecipientType.PROJECT_ALL}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) @FrontendAction(action = ProjectManagerConst.UPLOAD_PUBLICATION_ACTION) @PostMapping(value = ProjectManagerConst.UPLOAD_PUBLICATION) - public ResponseEntity uploadPublication( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @RequestParam(name = ProjectManagerConst.LABEL, required = false) String label, - @RequestParam(name = ProjectManagerConst.DOCUMENT) MultipartFile document + public ResponseEntity uploadPublication( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @RequestParameter(name = ProjectManagerConst.LABEL, required = false) String label, + @RequestParameter(name = ProjectManagerConst.DOCUMENT) MultipartFile document + ) { + return convertToResponseEntity(() -> this.documentService.uploadDocument( + project, Optional.empty(), document, DocumentType.PUBLICATION, Optional.ofNullable(label))); + } + + @RoleConstraints(organisationRoles = {OrganisationRole.RESEARCHER, + OrganisationRole.BRIDGEHEAD_ADMIN, OrganisationRole.PROJECT_MANAGER_ADMIN}) + @StateConstraints(projectStates = {ProjectState.FINISHED}) + @EmailSender(templateType = EmailTemplateType.NEW_FINAL_REPORT, recipients = { + EmailRecipientType.PROJECT_ALL}) + @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) + @FrontendAction(action = ProjectManagerConst.UPLOAD_FINAL_REPORT_ACTION) + @PostMapping(value = ProjectManagerConst.UPLOAD_FINAL_REPORT) + public ResponseEntity uploadFinalReport( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @RequestParameter(name = ProjectManagerConst.LABEL, required = false) String label, + @RequestParameter(name = ProjectManagerConst.DOCUMENT) MultipartFile document ) { return convertToResponseEntity(() -> this.documentService.uploadDocument( - projectCode, Optional.empty(), document, DocumentType.PUBLICATION, Optional.ofNullable(label))); + project, Optional.empty(), document, DocumentType.FINAL_REPORT, Optional.ofNullable(label))); } - @RoleConstraints(projectRoles = {ProjectRole.DEVELOPER, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) + @RoleConstraints(projectRoles = {ProjectRole.DEVELOPER, ProjectRole.BRIDGEHEAD_ADMIN, + ProjectRole.PROJECT_MANAGER_ADMIN}) @StateConstraints(projectStates = {ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL}) @ProjectConstraints(projectTypes = {ProjectType.DATASHIELD}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) @FrontendAction(action = ProjectManagerConst.UPLOAD_SCRIPT_ACTION) @PostMapping(value = ProjectManagerConst.UPLOAD_SCRIPT) - public ResponseEntity uploadScript( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, + public ResponseEntity uploadScript( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, // bridgehead required for identifying developer user or bridgehead admin in role constraints - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD, required = false) String bridgehead, - @RequestParam(name = ProjectManagerConst.LABEL, required = false) String label, - @RequestParam(name = ProjectManagerConst.DOCUMENT) MultipartFile document + @SuppressWarnings("unused") @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD, required = false) ProjectBridgehead bridgehead, + @RequestParameter(name = ProjectManagerConst.LABEL, required = false) String label, + @RequestParameter(name = ProjectManagerConst.DOCUMENT) MultipartFile document ) { return convertToResponseEntity(() -> this.documentService.uploadDocument( - projectCode, Optional.empty(), document, DocumentType.SCRIPT, Optional.ofNullable(label))); + project, Optional.empty(), document, DocumentType.SCRIPT, Optional.ofNullable(label))); } - @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) - @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.CREATED}) + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, + ProjectRole.PROJECT_MANAGER_ADMIN}) + @StateConstraints(projectStates = {ProjectState.REVIEW, ProjectState.APPROVAL, + ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL}) + @EmailSender(templateType = EmailTemplateType.NEW_VOTUM, recipients = { + EmailRecipientType.PROJECT_MANAGER_ADMIN}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) - @FrontendAction(action = ProjectManagerConst.UPLOAD_APPLICATION_FORM_ACTION) - @PostMapping(value = ProjectManagerConst.UPLOAD_APPLICATION_FORM) - public ResponseEntity uploadApplicationForm( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - // bridgehead required for identifying bridgehead admin in role constraints - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD, required = false) String bridgehead, - @RequestParam(name = ProjectManagerConst.LABEL, required = false) String label, - @RequestParam(name = ProjectManagerConst.DOCUMENT) MultipartFile document + @FrontendSiteModule(site = ProjectManagerConst.VOTUM_VIEW_SITE, module = ProjectManagerConst.VOTUM_ACTIONS_MODULE) + @FrontendAction(action = ProjectManagerConst.UPLOAD_VOTUM_ACTION) + @PostMapping(value = ProjectManagerConst.UPLOAD_VOTUM) + public ResponseEntity uploadVotum( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead, + @RequestParameter(name = ProjectManagerConst.LABEL, required = false) String label, + @RequestParameter(name = ProjectManagerConst.DOCUMENT) MultipartFile document ) { return convertToResponseEntity(() -> this.documentService.uploadDocument( - projectCode, Optional.empty(), document, DocumentType.APPLICATION_FORM, Optional.ofNullable(label))); + project, Optional.ofNullable(bridgehead), document, DocumentType.VOTUM, + Optional.ofNullable(label))); } - @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) - @StateConstraints(projectStates = {ProjectState.CREATED, ProjectState.ACCEPTED, ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL}) - @EmailSender(templateType = EmailTemplateType.NEW_VOTUM, recipients = {EmailRecipientType.PROJECT_MANAGER_ADMIN}) + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.PROJECT_MANAGER_ADMIN}) + @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.REVIEW}) + @EmailSender(templateType = EmailTemplateType.NEW_VOTUM, recipients = { + EmailRecipientType.PROJECT_MANAGER_ADMIN}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) - @FrontendSiteModule(site = ProjectManagerConst.VOTUM_VIEW_SITE, module = ProjectManagerConst.VOTUM_ACTIONS_MODULE) - @FrontendAction(action = ProjectManagerConst.UPLOAD_VOTUM_ACTION) - @PostMapping(value = ProjectManagerConst.UPLOAD_VOTUM) - public ResponseEntity uploadVotum( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead, - @RequestParam(name = ProjectManagerConst.LABEL, required = false) String label, - @RequestParam(name = ProjectManagerConst.DOCUMENT) MultipartFile document + @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_EDITION_MODULE) + @FrontendAction(action = ProjectManagerConst.UPLOAD_DESCRIPTION_ACTION) + @PostMapping(value = ProjectManagerConst.UPLOAD_DESCRIPTION) + public ResponseEntity uploadDescription( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @SuppressWarnings("unused") @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead, + @RequestParameter(name = ProjectManagerConst.LABEL, required = false) String label, + @RequestParameter(name = ProjectManagerConst.DOCUMENT) MultipartFile document ) { return convertToResponseEntity(() -> this.documentService.uploadDocument( - projectCode, Optional.ofNullable(bridgehead), document, DocumentType.VOTUM, Optional.ofNullable(label))); + project, Optional.empty(), document, DocumentType.DESCRIPTION, Optional.ofNullable(label))); + } + + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.PROJECT_MANAGER_ADMIN}) + @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.REVIEW}) + @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) + @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_EDITION_MODULE) + @FrontendAction(action = ProjectManagerConst.UPLOAD_ETHICS_APPROVAL_ACTION) + @PostMapping(value = ProjectManagerConst.UPLOAD_ETHICS_APPROVAL) + public ResponseEntity uploadEthicsApproval( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @SuppressWarnings("unused") @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead, + @RequestParameter(name = ProjectManagerConst.LABEL, required = false) String label, + @RequestParameter(name = ProjectManagerConst.DOCUMENT) MultipartFile document + ) { + return convertToResponseEntity(() -> this.documentService.uploadDocument( + project, Optional.empty(), document, DocumentType.VOTUM, Optional.ofNullable(label))); + } + + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) + @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.REVIEW}) + @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) + @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_EDITION_MODULE) + @FrontendAction(action = ProjectManagerConst.DOWNLOAD_ETHICS_APPROVAL_ACTION) + @GetMapping(value = ProjectManagerConst.DOWNLOAD_ETHICS_APPROVAL) + public ResponseEntity downloadEthicsApproval( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @SuppressWarnings("unused") @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead + ) throws DocumentServiceException { + return downloadProjectDocument(project, Optional.empty(), Optional.empty(), DocumentType.VOTUM); + } + + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) + @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.REVIEW}) + @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) + @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_EDITION_MODULE) + @FrontendAction(action = ProjectManagerConst.EXISTS_ETHICS_APPROVAL_ACTION) + @GetMapping(value = ProjectManagerConst.EXISTS_ETHICS_APPROVAL) + public ResponseEntity existsEthicsApproval( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @SuppressWarnings("unused") @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead + ) { + return existsProjectDocument(project, Optional.empty(), DocumentType.VOTUM); + } + + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) + @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.REVIEW}) + @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) + @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_EDITION_MODULE) + @FrontendAction(action = ProjectManagerConst.FETCH_ETHICS_APPROVAL_DESCRIPTION_ACTION) + @GetMapping(value = ProjectManagerConst.FETCH_ETHICS_APPROVAL_DESCRIPTION) + public ResponseEntity fetchEthicsApprovalDescription( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @SuppressWarnings("unused") @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead + ) { + return convertOptionalToResponseEntity( + () -> this.dtoDocumentService.fetchLastDocumentOfThisTypeForFrontend(project, + Optional.empty(), DocumentType.VOTUM)); } @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.PROJECT_MANAGER_ADMIN}) - @StateConstraints(projectStates = {ProjectState.CREATED, ProjectState.ACCEPTED, ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL}) - @EmailSender(templateType = EmailTemplateType.NEW_VOTUM_FOR_ALL_BRIDGEHEADS, recipients = {EmailRecipientType.ALL_BRIDGEHEAD_ADMINS, EmailRecipientType.CREATOR, EmailRecipientType.PROJECT_MANAGER_ADMIN}) + @StateConstraints(projectStates = {ProjectState.REVIEW, ProjectState.APPROVAL, + ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL}) + @EmailSender(templateType = EmailTemplateType.NEW_VOTUM_FOR_ALL_BRIDGEHEADS, recipients = { + EmailRecipientType.ALL_BRIDGEHEAD_ADMINS, EmailRecipientType.CREATOR, + EmailRecipientType.PROJECT_MANAGER_ADMIN}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) @FrontendSiteModule(site = ProjectManagerConst.VOTUM_VIEW_SITE, module = ProjectManagerConst.VOTUM_ACTIONS_MODULE) @FrontendAction(action = ProjectManagerConst.UPLOAD_VOTUM_FOR_ALL_BRIDGEHEADS_ACTION) @PostMapping(value = ProjectManagerConst.UPLOAD_VOTUM_FOR_ALL_BRIDGEHEADS) - public ResponseEntity uploadVotumForAllBridgheads( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead, - @RequestParam(name = ProjectManagerConst.LABEL, required = false) String label, - @RequestParam(name = ProjectManagerConst.DOCUMENT) MultipartFile document + public ResponseEntity uploadVotumForAllBridgeheads( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @SuppressWarnings("unused") @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead, + @RequestParameter(name = ProjectManagerConst.LABEL, required = false) String label, + @RequestParameter(name = ProjectManagerConst.DOCUMENT) MultipartFile document ) { return convertToResponseEntity(() -> this.documentService.uploadDocument( - projectCode, Optional.empty(), document, DocumentType.VOTUM, Optional.ofNullable(label))); + project, Optional.empty(), document, DocumentType.VOTUM, Optional.ofNullable(label))); } - @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, + ProjectRole.PROJECT_MANAGER_ADMIN}) @StateConstraints(projectStates = {ProjectState.FINISHED}) - @EmailSender(templateType = EmailTemplateType.NEW_PUBLICATION, recipients = {EmailRecipientType.PROJECT_ALL}) + @EmailSender(templateType = EmailTemplateType.NEW_PUBLICATION, recipients = { + EmailRecipientType.PROJECT_ALL}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) @FrontendAction(action = ProjectManagerConst.ADD_PUBLICATION_URL_ACTION) @PostMapping(value = ProjectManagerConst.ADD_PUBLICATION_URL) - public ResponseEntity addPublicationUrl( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, + public ResponseEntity addPublicationUrl( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, // bridgehead required for identifying bridgehead admin in role constraints - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD, required = false) String bridgehead, - @RequestParam(name = ProjectManagerConst.DOCUMENT_URL) String documentUrl, - @RequestParam(name = ProjectManagerConst.LABEL, required = false) String label + @SuppressWarnings("unused") @Bridgehead @RequestVariable(name = ProjectManagerConst.BRIDGEHEAD, required = false) ProjectBridgehead bridgehead, + @RequestVariable(name = ProjectManagerConst.DOCUMENT_URL) String documentUrl, + @RequestVariable(name = ProjectManagerConst.LABEL, required = false) String label ) { return convertToResponseEntity(() -> this.documentService.addDocumentUrl( - projectCode, Optional.empty(), documentUrl, DocumentType.PUBLICATION, Optional.ofNullable(label))); + project, Optional.empty(), documentUrl, DocumentType.PUBLICATION, + Optional.ofNullable(label))); } - @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, + ProjectRole.PROJECT_MANAGER_ADMIN}) + @StateConstraints(projectStates = {ProjectState.FINISHED}) + @EmailSender(templateType = EmailTemplateType.NEW_FINAL_REPORT, recipients = { + EmailRecipientType.PROJECT_ALL}) + @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) + @FrontendAction(action = ProjectManagerConst.ADD_FINAL_REPORT_URL_ACTION) + @PostMapping(value = ProjectManagerConst.ADD_FINAL_REPORT_URL) + public ResponseEntity addFinalReportUrl( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + // bridgehead required for identifying bridgehead admin in role constraints + @SuppressWarnings("unused") @Bridgehead @RequestVariable(name = ProjectManagerConst.BRIDGEHEAD, required = false) ProjectBridgehead bridgehead, + @RequestVariable(name = ProjectManagerConst.DOCUMENT_URL) String documentUrl, + @RequestVariable(name = ProjectManagerConst.LABEL, required = false) String label + ) { + return convertToResponseEntity(() -> this.documentService.addDocumentUrl( + project, Optional.empty(), documentUrl, DocumentType.FINAL_REPORT, + Optional.ofNullable(label))); + } + + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, + ProjectRole.PROJECT_MANAGER_ADMIN}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) @FrontendAction(action = ProjectManagerConst.ADD_OTHER_DOCUMENT_URL_ACTION) @PostMapping(value = ProjectManagerConst.ADD_OTHER_DOCUMENT_URL) - public ResponseEntity addOtherDocumentUrl( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD, required = false) String bridgehead, - @RequestParam(name = ProjectManagerConst.DOCUMENT_URL) String documentUrl, - @RequestParam(name = ProjectManagerConst.LABEL, required = false) String label + public ResponseEntity addOtherDocumentUrl( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestVariable(name = ProjectManagerConst.BRIDGEHEAD, required = false) ProjectBridgehead bridgehead, + @RequestVariable(name = ProjectManagerConst.DOCUMENT_URL) String documentUrl, + @RequestVariable(name = ProjectManagerConst.LABEL, required = false) String label ) { return convertToResponseEntity(() -> this.documentService.addDocumentUrl( - projectCode, Optional.ofNullable(bridgehead), documentUrl, DocumentType.OTHERS, Optional.ofNullable(label))); + project, Optional.ofNullable(bridgehead), documentUrl, DocumentType.OTHERS, + Optional.ofNullable(label))); } - @RoleConstraints(projectRoles = {ProjectRole.DEVELOPER, ProjectRole.PILOT, ProjectRole.FINAL, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) - @StateConstraints(projectStates = {ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL, ProjectState.FINISHED, ProjectState.ARCHIVED, ProjectState.REJECTED}) + @RoleConstraints(projectRoles = {ProjectRole.DEVELOPER, ProjectRole.PILOT, ProjectRole.FINAL, + ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) + @StateConstraints(projectStates = {ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL, + ProjectState.FINISHED, ProjectState.ARCHIVED, ProjectState.REJECTED}) @ProjectConstraints(projectTypes = {ProjectType.DATASHIELD}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) @FrontendAction(action = ProjectManagerConst.DOWNLOAD_SCRIPT_ACTION) @GetMapping(value = ProjectManagerConst.DOWNLOAD_SCRIPT) - public ResponseEntity downloadScript( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, + public ResponseEntity downloadScript( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, // bridgehead required for identifying developer user or bridgehead admin in role constraints - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD, required = false) String bridgehead + @SuppressWarnings("unused") @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD, required = false) ProjectBridgehead bridgehead ) throws DocumentServiceException { - return downloadProjectDocument(projectCode, Optional.empty(), DocumentType.SCRIPT); + return downloadProjectDocument(project, Optional.empty(), Optional.empty(), DocumentType.SCRIPT); } - @RoleConstraints(projectRoles = {ProjectRole.DEVELOPER, ProjectRole.PILOT, ProjectRole.FINAL, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) - @StateConstraints(projectStates = {ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL, ProjectState.FINISHED, ProjectState.ARCHIVED, ProjectState.REJECTED}) + @RoleConstraints(projectRoles = {ProjectRole.DEVELOPER, ProjectRole.PILOT, ProjectRole.FINAL, + ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) + @StateConstraints(projectStates = {ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL, + ProjectState.FINISHED, ProjectState.ARCHIVED, ProjectState.REJECTED}) @ProjectConstraints(projectTypes = {ProjectType.DATASHIELD}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) @FrontendAction(action = ProjectManagerConst.FETCH_SCRIPT_DESCRIPTION_ACTION) @GetMapping(value = ProjectManagerConst.FETCH_SCRIPT_DESCRIPTION) - public ResponseEntity fetchScriptDescription( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, + public ResponseEntity fetchScriptDescription( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, // bridgehead required for identifying developer user or bridgehead admin in role constraints - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD, required = false) String bridgehead + @SuppressWarnings("unused") @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD, required = false) ProjectBridgehead bridgehead ) { - return convertOptionalToResponseEntity(() -> this.documentService.fetchLastDocumentOfThisTypeForFrontend(projectCode, Optional.empty(), DocumentType.SCRIPT)); + return convertOptionalToResponseEntity( + () -> this.dtoDocumentService.fetchLastDocumentOfThisTypeForFrontend(project, + Optional.empty(), DocumentType.SCRIPT)); } - @RoleConstraints(projectRoles = {ProjectRole.DEVELOPER, ProjectRole.PILOT, ProjectRole.FINAL, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) - @StateConstraints(projectStates = {ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL, ProjectState.FINISHED, ProjectState.ARCHIVED, ProjectState.REJECTED}) + @RoleConstraints(projectRoles = {ProjectRole.DEVELOPER, ProjectRole.PILOT, ProjectRole.FINAL, + ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) + @StateConstraints(projectStates = {ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL, + ProjectState.FINISHED, ProjectState.ARCHIVED, ProjectState.REJECTED}) @ProjectConstraints(projectTypes = {ProjectType.DATASHIELD}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) @FrontendAction(action = ProjectManagerConst.EXISTS_SCRIPT_ACTION) @GetMapping(value = ProjectManagerConst.EXISTS_SCRIPT) - public ResponseEntity existsScript( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, + public ResponseEntity existsScript( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, // bridgehead required for identifying developer user or bridgehead admin in role constraints - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD, required = false) String bridgehead - ) throws DocumentServiceException { - return existsProjectDocument(projectCode, Optional.empty(), DocumentType.SCRIPT); + @SuppressWarnings("unused") @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD, required = false) ProjectBridgehead bridgehead + ) { + return existsProjectDocument(project, Optional.empty(), DocumentType.SCRIPT); } - @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) - @StateConstraints(projectStates = {ProjectState.CREATED, ProjectState.ACCEPTED, ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL}) + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, + ProjectRole.PROJECT_MANAGER_ADMIN}) + @StateConstraints(projectStates = {ProjectState.REVIEW, ProjectState.APPROVAL, + ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) @FrontendSiteModule(site = ProjectManagerConst.VOTUM_VIEW_SITE, module = ProjectManagerConst.VOTUM_ACTIONS_MODULE) @FrontendAction(action = ProjectManagerConst.DOWNLOAD_VOTUM_ACTION) @GetMapping(value = ProjectManagerConst.DOWNLOAD_VOTUM) - public ResponseEntity downloadVotum( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead + public ResponseEntity downloadVotum( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead ) throws DocumentServiceException { - return downloadProjectDocument(projectCode, Optional.of(bridgehead), DocumentType.VOTUM); + return downloadProjectDocument(project, Optional.of(bridgehead), Optional.empty(), DocumentType.VOTUM); } - @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) - @StateConstraints(projectStates = {ProjectState.CREATED, ProjectState.ACCEPTED, ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL}) + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, + ProjectRole.PROJECT_MANAGER_ADMIN}) + @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) + @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_EDITION_MODULE) + @FrontendAction(action = ProjectManagerConst.DOWNLOAD_DESCRIPTION_ACTION) + @GetMapping(value = ProjectManagerConst.DOWNLOAD_DESCRIPTION) + public ResponseEntity downloadDescription( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @SuppressWarnings("unused") @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead + ) throws DocumentServiceException { + return downloadProjectDocument(project, Optional.empty(), Optional.empty(), DocumentType.DESCRIPTION); + } + + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, + ProjectRole.PROJECT_MANAGER_ADMIN}) + @StateConstraints(projectStates = {ProjectState.REVIEW, ProjectState.APPROVAL, + ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) @FrontendSiteModule(site = ProjectManagerConst.VOTUM_VIEW_SITE, module = ProjectManagerConst.VOTUM_ACTIONS_MODULE) @FrontendAction(action = ProjectManagerConst.DOWNLOAD_VOTUM_FOR_ALL_BRIDGEHEADS_ACTION) @GetMapping(value = ProjectManagerConst.DOWNLOAD_VOTUM_FOR_ALL_BRIDGEHEADS) - public ResponseEntity downloadVotumForAllBridgeheads( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead + public ResponseEntity downloadVotumForAllBridgeheads( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @SuppressWarnings("unused") @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead ) throws DocumentServiceException { - return downloadProjectDocument(projectCode, Optional.empty(), DocumentType.VOTUM); + return downloadProjectDocument(project, Optional.empty(), Optional.empty(), DocumentType.VOTUM); } - @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) - @StateConstraints(projectStates = {ProjectState.CREATED, ProjectState.ACCEPTED, ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL}) + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, + ProjectRole.PROJECT_MANAGER_ADMIN}) + @StateConstraints(projectStates = {ProjectState.REVIEW, ProjectState.APPROVAL, + ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) @FrontendSiteModule(site = ProjectManagerConst.VOTUM_VIEW_SITE, module = ProjectManagerConst.VOTUM_ACTIONS_MODULE) @FrontendAction(action = ProjectManagerConst.FETCH_VOTUM_DESCRIPTION_ACTION) @GetMapping(value = ProjectManagerConst.FETCH_VOTUM_DESCRIPTION) - public ResponseEntity fetchVotumDescription( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead + public ResponseEntity fetchVotumDescription( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead ) { - return convertOptionalToResponseEntity(() -> this.documentService.fetchLastDocumentOfThisTypeForFrontend(projectCode, Optional.ofNullable(bridgehead), DocumentType.VOTUM)); + return convertOptionalToResponseEntity( + () -> this.dtoDocumentService.fetchLastDocumentOfThisTypeForFrontend(project, + Optional.ofNullable(bridgehead), DocumentType.VOTUM)); } - @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) - @StateConstraints(projectStates = {ProjectState.CREATED, ProjectState.ACCEPTED, ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL}) + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, + ProjectRole.PROJECT_MANAGER_ADMIN}) + @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.REVIEW, ProjectState.APPROVAL, + ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL}) + @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) + @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_EDITION_MODULE) + @FrontendAction(action = ProjectManagerConst.FETCH_DESCRIPTION_ACTION) + @GetMapping(value = ProjectManagerConst.FETCH_DESCRIPTION) + public ResponseEntity fetchProjectDescription( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @SuppressWarnings("unused") @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead + ) { + return convertOptionalToResponseEntity( + () -> this.dtoDocumentService.fetchLastDocumentOfThisTypeForFrontend(project, + Optional.empty(), DocumentType.DESCRIPTION)); + } + + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, + ProjectRole.PROJECT_MANAGER_ADMIN}) + @StateConstraints(projectStates = {ProjectState.REVIEW, ProjectState.APPROVAL, + ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) @FrontendSiteModule(site = ProjectManagerConst.VOTUM_VIEW_SITE, module = ProjectManagerConst.VOTUM_ACTIONS_MODULE) @FrontendAction(action = ProjectManagerConst.FETCH_VOTUM_FOR_ALL_BRIDGEHEADS_DESCRIPTION_ACTION) @GetMapping(value = ProjectManagerConst.FETCH_VOTUM_FOR_ALL_BRIDGEHEADS_DESCRIPTION) - public ResponseEntity fetchVotumDescriptionForAllBridgeheads( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead + public ResponseEntity fetchVotumDescriptionForAllBridgeheads( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @SuppressWarnings("unused") @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead ) { - return convertOptionalToResponseEntity(() -> this.documentService.fetchLastDocumentOfThisTypeForFrontend(projectCode, Optional.empty(), DocumentType.VOTUM)); + return convertOptionalToResponseEntity( + () -> this.dtoDocumentService.fetchLastDocumentOfThisTypeForFrontend(project, + Optional.empty(), DocumentType.VOTUM)); } - @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) - @StateConstraints(projectStates = {ProjectState.CREATED, ProjectState.ACCEPTED, ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL, ProjectState.FINISHED, ProjectState.ARCHIVED, ProjectState.REJECTED}) + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, + ProjectRole.PROJECT_MANAGER_ADMIN}) + @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.REVIEW, ProjectState.APPROVAL, + ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL, ProjectState.FINISHED, + ProjectState.ARCHIVED, ProjectState.REJECTED}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) @FrontendSiteModule(site = ProjectManagerConst.VOTUM_VIEW_SITE, module = ProjectManagerConst.VOTUM_ACTIONS_MODULE) @FrontendAction(action = ProjectManagerConst.EXISTS_VOTUM_ACTION) @GetMapping(value = ProjectManagerConst.EXISTS_VOTUM) - public ResponseEntity existsVotum( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead - ) throws DocumentServiceException { - return existsProjectDocument(projectCode, Optional.of(bridgehead), DocumentType.VOTUM); + public ResponseEntity existsVotum( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead + ) { + return existsProjectDocument(project, Optional.of(bridgehead), DocumentType.VOTUM); } - @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) - @StateConstraints(projectStates = {ProjectState.CREATED, ProjectState.ACCEPTED, ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL, ProjectState.FINISHED, ProjectState.ARCHIVED, ProjectState.REJECTED}) + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, + ProjectRole.PROJECT_MANAGER_ADMIN}) + @StateConstraints(projectStates = {ProjectState.FINISHED, ProjectState.ARCHIVED, + ProjectState.REJECTED}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) - @FrontendSiteModule(site = ProjectManagerConst.VOTUM_VIEW_SITE, module = ProjectManagerConst.VOTUM_ACTIONS_MODULE) - @FrontendAction(action = ProjectManagerConst.EXISTS_VOTUM_FOR_ALL_BRIDGEHEADS_ACTION) - @GetMapping(value = ProjectManagerConst.EXISTS_VOTUM_FOR_ALL_BRIDGEHEADS) - public ResponseEntity existsVotumForAllBridgeheads( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead - ) throws DocumentServiceException { - return existsProjectDocument(projectCode, Optional.empty(), DocumentType.VOTUM); + @FrontendAction(action = ProjectManagerConst.EXISTS_PUBLICATION_ACTION) + @GetMapping(value = ProjectManagerConst.EXISTS_PUBLICATION) + public ResponseEntity existsPublication( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @SuppressWarnings("unused") @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead + ) { + return existsProjectDocument(project, Optional.empty(), DocumentType.PUBLICATION); } - - @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) - @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.CREATED, ProjectState.ACCEPTED, ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL, ProjectState.FINISHED, ProjectState.ARCHIVED, ProjectState.REJECTED}) + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, + ProjectRole.PROJECT_MANAGER_ADMIN}) + @StateConstraints(projectStates = {ProjectState.FINISHED, ProjectState.ARCHIVED, + ProjectState.REJECTED}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) - @FrontendAction(action = ProjectManagerConst.DOWNLOAD_APPLICATION_FORM_ACTION) - @GetMapping(value = ProjectManagerConst.DOWNLOAD_APPLICATION_FORM) - public ResponseEntity downloadApplicationForm( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - // bridgehead required for identifying bridgehead admin in role constraints - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD, required = false) String bridgehead - ) throws DocumentServiceException { - return downloadProjectDocument(projectCode, Optional.empty(), DocumentType.APPLICATION_FORM); + @FrontendAction(action = ProjectManagerConst.EXISTS_FINAL_REPORT_ACTION) + @GetMapping(value = ProjectManagerConst.EXISTS_FINAL_REPORT) + public ResponseEntity existsFinalReport( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @SuppressWarnings("unused") @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead + ) { + return existsProjectDocument(project, Optional.empty(), DocumentType.FINAL_REPORT); } - @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) - @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.CREATED, ProjectState.ACCEPTED, ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL, ProjectState.FINISHED, ProjectState.ARCHIVED, ProjectState.REJECTED}) + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, + ProjectRole.PROJECT_MANAGER_ADMIN}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) - @FrontendAction(action = ProjectManagerConst.FETCH_APPLICATION_FORM_DESCRIPTION_ACTION) - @GetMapping(value = ProjectManagerConst.FETCH_APPLICATION_FORM_DESCRIPTION) - public ResponseEntity fetchApplicationFormDescription( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - // bridgehead required for identifying bridgehead admin in role constraints - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD, required = false) String bridgehead + @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_EDITION_MODULE) + @FrontendAction(action = ProjectManagerConst.EXISTS_DESCRIPTION_ACTION) + @GetMapping(value = ProjectManagerConst.EXISTS_DESCRIPTION) + public ResponseEntity existsDescription( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @SuppressWarnings("unused") @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead ) { - return convertOptionalToResponseEntity(() -> this.documentService.fetchLastDocumentOfThisTypeForFrontend(projectCode, Optional.empty(), DocumentType.APPLICATION_FORM)); + return existsProjectDocument(project, Optional.empty(), DocumentType.DESCRIPTION); } - @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) - @StateConstraints(projectStates = {ProjectState.DRAFT, ProjectState.CREATED, ProjectState.ACCEPTED, ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL, ProjectState.FINISHED, ProjectState.ARCHIVED, ProjectState.REJECTED}) + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, + ProjectRole.PROJECT_MANAGER_ADMIN}) + @StateConstraints(projectStates = {ProjectState.REVIEW, ProjectState.APPROVAL, + ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL, ProjectState.FINISHED, + ProjectState.ARCHIVED, ProjectState.REJECTED}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) - @FrontendAction(action = ProjectManagerConst.EXISTS_APPLICATION_FORM_ACTION) - @GetMapping(value = ProjectManagerConst.EXISTS_APPLICATION_FORM) - public ResponseEntity existsApplicationForm( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - // bridgehead required for identifying bridgehead admin in role constraints - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD, required = false) String bridgehead - ) throws DocumentServiceException { - return existsProjectDocument(projectCode, Optional.empty(), DocumentType.APPLICATION_FORM); + @FrontendSiteModule(site = ProjectManagerConst.VOTUM_VIEW_SITE, module = ProjectManagerConst.VOTUM_ACTIONS_MODULE) + @FrontendAction(action = ProjectManagerConst.EXISTS_VOTUM_FOR_ALL_BRIDGEHEADS_ACTION) + @GetMapping(value = ProjectManagerConst.EXISTS_VOTUM_FOR_ALL_BRIDGEHEADS) + public ResponseEntity existsVotumForAllBridgeheads( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @SuppressWarnings("unused") @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead + ) { + return existsProjectDocument(project, Optional.empty(), DocumentType.VOTUM); } - @RoleConstraints(organisationRoles = {OrganisationRole.RESEARCHER}) - @StateConstraints(projectStates = {ProjectState.DRAFT}) + @StateConstraints(projectStates = {ProjectState.FINISHED}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) - @FrontendAction(action = ProjectManagerConst.DOWNLOAD_APPLICATION_FORM_TEMPLATE_ACTION) - @GetMapping(value = ProjectManagerConst.DOWNLOAD_APPLICATION_FORM_TEMPLATE) - public ResponseEntity downloadApplicationFormTemplate( - // Project code is needed for the project constraint. - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode + @FrontendAction(action = ProjectManagerConst.DOWNLOAD_PUBLICATION_ACTION) + @GetMapping(value = ProjectManagerConst.DOWNLOAD_PUBLICATION) + public ResponseEntity downloadPublication( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @RequestParameter(name = ProjectManagerConst.FILENAME, required = false) String filename ) throws DocumentServiceException { - Optional filePath = documentService.fetchApplicationForm(); - return (filePath.isEmpty()) ? ResponseEntity.notFound().build() : - downloadDocument(filePath.get().getFileName().toString(), filePath.get()); + return downloadProjectDocument(project, Optional.empty(), Optional.ofNullable(filename), DocumentType.PUBLICATION); } + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) @StateConstraints(projectStates = {ProjectState.FINISHED}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) - @FrontendAction(action = ProjectManagerConst.DOWNLOAD_PUBLICATION_ACTION) - @GetMapping(value = ProjectManagerConst.DOWNLOAD_PUBLICATION) - public ResponseEntity downloadPublication( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @RequestParam(name = ProjectManagerConst.FILENAME) String filename + @FrontendAction(action = ProjectManagerConst.DOWNLOAD_FINAL_REPORT_ACTION) + @GetMapping(value = ProjectManagerConst.DOWNLOAD_FINAL_REPORT) + public ResponseEntity downloadFinalReport( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @RequestParameter(name = ProjectManagerConst.FILENAME, required = false) String filename ) throws DocumentServiceException { - return downloadProjectDocument(projectCode, null, filename, DocumentType.PUBLICATION); + return downloadProjectDocument(project, Optional.empty(), Optional.ofNullable(filename), DocumentType.FINAL_REPORT); } @StateConstraints(projectStates = {ProjectState.FINISHED}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) @FrontendAction(action = ProjectManagerConst.FETCH_PUBLICATIONS_ACTION) @GetMapping(value = ProjectManagerConst.FETCH_PUBLICATIONS, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity fetchPublications( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode + public ResponseEntity fetchPublications( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project + ) { + return convertToResponseEntity(() -> dtoDocumentService.fetchPublications(project)); + } + + @StateConstraints(projectStates = {ProjectState.FINISHED}) + @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) + @FrontendAction(action = ProjectManagerConst.FETCH_FINAL_REPORTS_ACTION) + @GetMapping(value = ProjectManagerConst.FETCH_FINAL_REPORTS, produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity fetchFinalReports( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project ) { - return convertToResponseEntity(() -> documentService.fetchPublications(projectCode)); + return convertToResponseEntity(() -> dtoDocumentService.fetchFinalReports(project)); } - @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.DEVELOPER, ProjectRole.PILOT, ProjectRole.FINAL, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.DEVELOPER, ProjectRole.PILOT, + ProjectRole.FINAL, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) @FrontendAction(action = ProjectManagerConst.DOWNLOAD_OTHER_DOCUMENT_ACTION) @GetMapping(value = ProjectManagerConst.DOWNLOAD_OTHER_DOCUMENT) - public ResponseEntity downloadOtherProjectDocument( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD, required = false) String bridgehead, - @RequestParam(name = ProjectManagerConst.FILENAME) String filename + public ResponseEntity downloadOtherProjectDocument( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD, required = false) ProjectBridgehead bridgehead, + @RequestParameter(name = ProjectManagerConst.FILENAME) String filename ) throws DocumentServiceException { - return downloadProjectDocument(projectCode, bridgehead, filename, DocumentType.OTHERS); + return downloadProjectDocument(project, Optional.ofNullable(bridgehead), Optional.of(filename), DocumentType.OTHERS); } - @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.DEVELOPER, ProjectRole.PILOT, ProjectRole.FINAL, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.DEVELOPER, ProjectRole.PILOT, + ProjectRole.FINAL, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_DOCUMENTS_MODULE) @FrontendAction(action = ProjectManagerConst.FETCH_OTHER_DOCUMENTS_ACTION) @GetMapping(value = ProjectManagerConst.FETCH_OTHER_DOCUMENTS, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity fetchOtherDocuments( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD, required = false) String bridgehead + public ResponseEntity fetchOtherDocuments( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD, required = false) ProjectBridgehead bridgehead ) { - return convertToResponseEntity(() -> documentService.fetchOtherDocuments(projectCode, Optional.ofNullable(bridgehead))); + return convertToResponseEntity( + () -> dtoDocumentService.fetchOtherDocuments(project, Optional.ofNullable(bridgehead))); } - private ResponseEntity downloadProjectDocument(String projectCode, Optional bridgehead, DocumentType documentType) throws DocumentServiceException { - return downloadProjectDocument(this.documentService.fetchLastDocumentOfThisType(projectCode, bridgehead, documentType)); - } + private ResponseEntity downloadProjectDocument(Project project, + Optional bridgehead, + Optional filename, + DocumentType documentType) throws DocumentServiceException { + Optional projectDocument = filename.isPresent() + ? this.documentService.fetchProjectDocument(project, bridgehead, filename.get()) + : this.documentService.fetchLastDocumentOfThisType(project, bridgehead, documentType); - private ResponseEntity existsProjectDocument(String projectCode, Optional bridgehead, DocumentType documentType) throws DocumentServiceException { - return convertToResponseEntity(() -> this.documentService.fetchLastDocumentOfThisType(projectCode, bridgehead, documentType).isPresent()); - } + if (filename.isPresent() && projectDocument.isPresent() + && projectDocument.get().getDocumentType() != documentType) { + return createMethodNotAllowedResponse("Requested document is not of allowed type: " + documentType); + } + return downloadProjectDocument(projectDocument); + } - private ResponseEntity downloadProjectDocument(String projectCode, String bridgehead, String filename, DocumentType allowedType) throws DocumentServiceException { - Optional projectDocument = this.documentService.fetchProjectDocument(projectCode, Optional.ofNullable(bridgehead), filename); - return (projectDocument.isPresent() && projectDocument.get().getDocumentType() != allowedType) ? - createMethodNotAllowedResponse("Requested document is not of allowed type: " + allowedType) : - downloadProjectDocument(projectDocument); + private ResponseEntity existsProjectDocument(Project project, + Optional bridgehead, DocumentType documentType) { + return convertToResponseEntity( + () -> this.documentService.fetchLastDocumentOfThisType(project, bridgehead, documentType) + .isPresent()); } - private ResponseEntity createMethodNotAllowedResponse(String errorMessage) { + private ResponseEntity createMethodNotAllowedResponse(String errorMessage) { ByteArrayResource errorResource = new ByteArrayResource(errorMessage.getBytes()); HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE); @@ -1294,12 +1802,15 @@ private ResponseEntity createMethodNotAllowedResponse(String errorMess .body(errorResource); } - private ResponseEntity downloadProjectDocument(Optional projectDocument) throws DocumentServiceException { + private ResponseEntity downloadProjectDocument(Optional projectDocument) + throws DocumentServiceException { return (projectDocument.isEmpty()) ? ResponseEntity.notFound().build() : - downloadDocument(encodeFilename(projectDocument.get().getOriginalFilename()), Path.of(projectDocument.get().getFilePath())); + downloadDocument(encodeFilename(projectDocument.get().getOriginalFilename()), + Path.of(projectDocument.get().getFilePath())); } - private ResponseEntity downloadDocument(String filename, Path filePath) throws DocumentServiceException { + private ResponseEntity downloadDocument(String filename, Path filePath) + throws DocumentServiceException { HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename); headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); @@ -1310,11 +1821,7 @@ private ResponseEntity downloadDocument(String filename, Path filePath } private String encodeFilename(String filename) { - try { - return URLEncoder.encode(filename, StandardCharsets.UTF_8.toString()); - } catch (UnsupportedEncodingException e) { - return filename; - } + return URLEncoder.encode(filename, StandardCharsets.UTF_8); } private ByteArrayResource fetchResource(Path filePath) throws DocumentServiceException { @@ -1331,28 +1838,45 @@ private ByteArrayResource fetchResource(Path filePath) throws DocumentServiceExc @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.EXPORT_MODULE) @FrontendAction(action = ProjectManagerConst.SAVE_QUERY_IN_BRIDGEHEAD_ACTION) @PostMapping(value = ProjectManagerConst.SAVE_QUERY_IN_BRIDGEHEAD) - public ResponseEntity saveQueryInBridgehead( - @NotEmpty @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @NotEmpty @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead + public ResponseEntity saveQueryInBridgehead( + @SuppressWarnings("unused") @NotEmpty @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + @NotEmpty @Bridgehead @RequestVariable(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead, + @RequestVariable(name = ProjectManagerConst.PROJECT_TYPE) List projectTypes ) { - return convertToResponseEntity(() -> this.projectBridgeheadService.scheduleSendQueryToBridgehead(projectCode, bridgehead)); + return convertToResponseEntity(() -> Optional + .ofNullable(projectTypes) + .ifPresent(types -> types + .forEach(type -> + this.projectBridgeheadService.scheduleSendQueryToBridgehead(bridgehead, type)))); + } - // For the moment, due to data protection, this feature is disabled: - /* + + // This endpoint is active only when external execution is allowed. + // By default, it is disabled via spring.profiles.active in application.yaml. + // To enable it, override the configuration so that the + // 'external-execution-not-allowed' profile is NOT active + // (e.g., set SPRING_PROFILES_ACTIVE to an empty value). + @Profile("!" + ProjectManagerConst.EXTERNAL_EXECUTION_NOT_ALLOWED) @RoleConstraints(projectRoles = {ProjectRole.BRIDGEHEAD_ADMIN}) @StateConstraints(projectStates = {ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL}, queryStates = {QueryState.CREATED, QueryState.ERROR, QueryState.FINISHED}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.EXPORT_MODULE) @FrontendAction(action = ProjectManagerConst.SAVE_AND_EXECUTE_QUERY_IN_BRIDGEHEAD_ACTION) @PostMapping(value = ProjectManagerConst.SAVE_AND_EXECUTE_QUERY_IN_BRIDGEHEAD) - public ResponseEntity saveAndExecuteQueryInBridgehead( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead + public ResponseEntity saveAndExecuteQueryInBridgehead( + @SuppressWarnings("unused") @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestVariable(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead, + @RequestVariable(name = ProjectManagerConst.PROJECT_TYPE) List projectTypes ) { - return convertToResponseEntity(() -> this.projectBridgeheadService.scheduleSendQueryToBridgeheadAndExecute(projectCode, bridgehead)); + return convertToResponseEntity(() -> Optional + .ofNullable(projectTypes) + .ifPresent(types -> types + .forEach(type -> + this.projectBridgeheadService.scheduleSendQueryToBridgeheadAndExecute(bridgehead, + type)))); } - */ + @RoleConstraints(projectRoles = {ProjectRole.DEVELOPER, ProjectRole.PILOT, ProjectRole.FINAL}) @StateConstraints(projectStates = {ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL}, @@ -1361,11 +1885,13 @@ public ResponseEntity saveAndExecuteQueryInBridgehead( @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.EXPORT_MODULE) @FrontendAction(action = ProjectManagerConst.SEND_EXPORT_FILES_TO_RESEARCH_ENVIRONMENT_ACTION) @PostMapping(value = ProjectManagerConst.SEND_EXPORT_FILES_TO_RESEARCH_ENVIRONMENT) - public ResponseEntity sendExportFilesToResearchEnvironment( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead + public ResponseEntity sendExportFilesToResearchEnvironment( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestVariable(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead ) { - return convertToResponseEntity(() -> exporterService.transferFileToResearchEnvironment(projectCode, bridgehead)); + return convertToResponseEntity( + () -> exporterService.transferFileToResearchEnvironment(project.getCode(), + bridgehead.getBridgehead())); } @RoleConstraints(projectRoles = {ProjectRole.DEVELOPER, ProjectRole.PILOT, ProjectRole.FINAL}) @@ -1374,12 +1900,14 @@ public ResponseEntity sendExportFilesToResearchEnvironment( @ProjectConstraints(projectTypes = {ProjectType.RESEARCH_ENVIRONMENT}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.EXPORT_MODULE) @FrontendAction(action = ProjectManagerConst.ARE_EXPORT_FILES_TRANSFERRED_TO_RESEARCH_ENVIRONMENT_ACTION) - @PostMapping(value = ProjectManagerConst.ARE_EXPORT_FILES_TRANSFERRED_TO_RESEARCH_ENVIRONMENT) - public ResponseEntity isExportFileTransferredToResearchEnvironment( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead + @GetMapping(value = ProjectManagerConst.ARE_EXPORT_FILES_TRANSFERRED_TO_RESEARCH_ENVIRONMENT) + public ResponseEntity isExportFileTransferredToResearchEnvironment( + @ProjectCode @RequestVariable(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestVariable(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead ) { - return convertToResponseEntity(() -> exporterService.isExportFileTransferredToResearchEnvironment(projectCode, bridgehead)); + return convertToResponseEntity( + () -> exporterService.isExportFileTransferredToResearchEnvironment(project.getCode(), + bridgehead.getBridgehead())); } @RoleConstraints(projectRoles = {ProjectRole.DEVELOPER, ProjectRole.PILOT, ProjectRole.FINAL}) @@ -1388,27 +1916,30 @@ public ResponseEntity isExportFileTransferredToResearchEnvironment( @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.TOKEN_MANAGER_MODULE) @FrontendAction(action = ProjectManagerConst.DOWNLOAD_AUTHENTICATION_SCRIPT_ACTION) @GetMapping(value = ProjectManagerConst.DOWNLOAD_AUTHENTICATION_SCRIPT) - public ResponseEntity fetchTokenScript( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead + public ResponseEntity fetchTokenScript( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead ) { return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + - ProjectManagerConst.AUTHENTICATION_SCRIPT_FILENAME_PREFIX + projectCode + ProjectManagerConst.AUTHENTICATION_SCRIPT_FILENAME_SUFFIX + "\"") - .body(this.dataShieldTokenManagerService.fetchAuthenticationScript(projectCode, bridgehead)); + ProjectManagerConst.AUTHENTICATION_SCRIPT_FILENAME_PREFIX + project.getCode() + + ProjectManagerConst.AUTHENTICATION_SCRIPT_FILENAME_SUFFIX + "\"") + .body(this.dataShieldTokenManagerService.fetchAuthenticationScript(project, bridgehead)); } - @RoleConstraints(projectRoles = {ProjectRole.DEVELOPER, ProjectRole.PILOT, ProjectRole.FINAL, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) + @RoleConstraints(projectRoles = {ProjectRole.DEVELOPER, ProjectRole.PILOT, ProjectRole.FINAL, + ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) @StateConstraints(projectStates = {ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL}) @ProjectConstraints(projectTypes = {ProjectType.DATASHIELD}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.TOKEN_MANAGER_MODULE) @FrontendAction(action = ProjectManagerConst.FETCH_DATASHIELD_STATUS_ACTION) @GetMapping(value = ProjectManagerConst.FETCH_DATASHIELD_STATUS) - public ResponseEntity fetchOpalStatus( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead + public ResponseEntity fetchDatashieldStatus( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead ) { - return convertToResponseEntity(() -> this.dataShieldTokenManagerService.fetchProjectStatus(projectCode, bridgehead).block()); + return convertToResponseEntity( + () -> this.dataShieldTokenManagerService.fetchProjectStatus(project, bridgehead).block()); } @RoleConstraints(projectRoles = {ProjectRole.DEVELOPER, ProjectRole.PILOT, ProjectRole.FINAL}) @@ -1417,65 +1948,79 @@ public ResponseEntity fetchOpalStatus( @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.TOKEN_MANAGER_MODULE) @FrontendAction(action = ProjectManagerConst.EXISTS_AUTHENTICATION_SCRIPT_ACTION) @GetMapping(value = ProjectManagerConst.EXISTS_AUTHENTICATION_SCRIPT) - public ResponseEntity existsTokenScript( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead + public ResponseEntity existsTokenScript( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead ) { - return convertToResponseEntity(() -> this.dataShieldTokenManagerService.existsAuthenticationScript(projectCode, bridgehead)); + return convertToResponseEntity( + () -> this.dataShieldTokenManagerService.existsAuthenticationScript(project, bridgehead)); } + @RoleConstraints(organisationRoles = {OrganisationRole.PROJECT_MANAGER_ADMIN}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.NOTIFICATIONS_MODULE) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_DASHBOARD_SITE, module = ProjectManagerConst.NOTIFICATIONS_MODULE) @FrontendAction(action = ProjectManagerConst.FETCH_NOTIFICATIONS_ACTION) @GetMapping(value = ProjectManagerConst.FETCH_NOTIFICATIONS) - public ResponseEntity fetchNotifications( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE, required = false) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD, required = false) String bridgehead + public ResponseEntity fetchNotifications( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE, required = false) Project project, + @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD, required = false) ProjectBridgehead bridgehead ) { - return convertToResponseEntity(() -> this.notificationService.fetchUserVisibleNotifications(Optional.ofNullable(projectCode), Optional.ofNullable(bridgehead), () -> projectService.fetchAllUserVisibleProjects())); + return convertToResponseEntity(() -> + this.dtoNotificationService.fetchUserVisibleNotifications( + Optional.ofNullable(project), + Optional.ofNullable(bridgehead), + projectService::fetchAllUserVisibleProjects)); } + @RoleConstraints(organisationRoles = {OrganisationRole.PROJECT_MANAGER_ADMIN}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.NOTIFICATIONS_MODULE) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_DASHBOARD_SITE, module = ProjectManagerConst.NOTIFICATIONS_MODULE) @FrontendAction(action = ProjectManagerConst.SET_NOTIFICATION_AS_READ_ACTION) - @PostMapping(value = ProjectManagerConst.SET_NOTIFICATION_AS_READ) - public ResponseEntity setNotificationAsRead( - @RequestParam(name = ProjectManagerConst.NOTIFICATION_ID) Long notificationId + @PutMapping(value = ProjectManagerConst.SET_NOTIFICATION_AS_READ) + public ResponseEntity setNotificationAsRead( + @RequestVariable(name = ProjectManagerConst.NOTIFICATION_ID) Long notificationId ) { - return convertToResponseEntity(() -> this.notificationService.setNotificationAsRead(notificationId)); + return convertToResponseEntity( + () -> this.notificationService.setNotificationAsRead(notificationId)); } - @RoleConstraints(organisationRoles = {OrganisationRole.RESEARCHER, OrganisationRole.BRIDGEHEAD_ADMIN, OrganisationRole.PROJECT_MANAGER_ADMIN}) + @RoleConstraints(organisationRoles = {OrganisationRole.RESEARCHER, + OrganisationRole.BRIDGEHEAD_ADMIN, OrganisationRole.PROJECT_MANAGER_ADMIN}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.PROJECT_BRIDGEHEAD_MODULE) @FrontendAction(action = ProjectManagerConst.FETCH_ALL_REGISTERED_BRIDGEHEADS_ACTION) @GetMapping(value = ProjectManagerConst.FETCH_ALL_REGISTERED_BRIDGEHEADS) - public ResponseEntity fetchAllRegisteredBridgeheads() { - return convertToResponseEntity(() -> bridgeheadConfiguration.getRegisteredBridgeheads().stream().map(dtoFactory::convertToBridgehead).collect(Collectors.toSet())); + public ResponseEntity fetchAllRegisteredBridgeheads() { + return convertToResponseEntity(() -> bridgeheadConfiguration + .getRegisteredBridgeheads().stream().map(dtoFactory::convertToBridgehead) + .collect(Collectors.toSet())); } - @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, + ProjectRole.PROJECT_MANAGER_ADMIN}) @StateConstraints(projectStates = {ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.USER_MODULE) @FrontendAction(action = ProjectManagerConst.FETCH_USERS_FOR_AUTOCOMPLETE_ACTION) @GetMapping(value = ProjectManagerConst.FETCH_USERS_FOR_AUTOCOMPLETE) - public ResponseEntity fetchUsersForAutocomplete( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead, - @RequestParam(name = ProjectManagerConst.PARTIAL_EMAIL) String partialEmail + public ResponseEntity fetchUsersForAutocomplete( + @SuppressWarnings("unused") @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead, + @RequestParameter(name = ProjectManagerConst.PARTIAL_EMAIL) String partialEmail ) { - return convertToResponseEntity(() -> this.userService.fetchUsersForAutocomplete(projectCode, partialEmail, bridgehead)); + return convertToResponseEntity( + () -> this.dtoUserService.fetchUsersForAutocomplete(partialEmail, bridgehead)); } - @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, + ProjectRole.PROJECT_MANAGER_ADMIN}) @StateConstraints(projectStates = {ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.USER_MODULE) @FrontendAction(action = ProjectManagerConst.FETCH_PROJECT_USERS_ACTION) @GetMapping(value = ProjectManagerConst.FETCH_PROJECT_USERS) - public ResponseEntity fetchProjectUsers( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead + public ResponseEntity fetchProjectUsers( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @SuppressWarnings("unused") @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead ) { - return convertToResponseEntity(() -> this.userService.fetchProjectUsers(projectCode)); + return convertToResponseEntity(() -> this.dtoUserService.fetchProjectUsers(project)); } @RoleConstraints(projectRoles = {ProjectRole.DEVELOPER, ProjectRole.PILOT, ProjectRole.FINAL}) @@ -1483,23 +2028,24 @@ public ResponseEntity fetchProjectUsers( @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.USER_MODULE) @FrontendAction(action = ProjectManagerConst.FETCH_CURRENT_USER_ACTION) @GetMapping(value = ProjectManagerConst.FETCH_CURRENT_USER) - public ResponseEntity fetchCurrentUser( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead + public ResponseEntity fetchCurrentUser( + @SuppressWarnings("unused") @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead ) { - return convertOptionalToResponseEntity(() -> this.userService.fetchCurrentUser(projectCode, bridgehead)); + return convertOptionalToResponseEntity(() -> this.dtoUserService.fetchCurrentUser(bridgehead)); } - @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN}) + @RoleConstraints(projectRoles = {ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, + ProjectRole.PROJECT_MANAGER_ADMIN}) @StateConstraints(projectStates = {ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL}) @FrontendSiteModule(site = ProjectManagerConst.PROJECT_VIEW_SITE, module = ProjectManagerConst.USER_MODULE) @FrontendAction(action = ProjectManagerConst.EXIST_INVITED_USERS_ACTION) @GetMapping(value = ProjectManagerConst.EXIST_INVITED_USERS) - public ResponseEntity existInvitedUsers( - @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, - @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead + public ResponseEntity existInvitedUsers( + @ProjectCode @RequestParameter(name = ProjectManagerConst.PROJECT_CODE) Project project, + @SuppressWarnings("unused") @Bridgehead @RequestParameter(name = ProjectManagerConst.BRIDGEHEAD) ProjectBridgehead bridgehead ) { - return convertToResponseEntity(() -> this.userService.existInvitedUsers(projectCode)); + return convertToResponseEntity(() -> this.userService.existInvitedUsers(project)); } @@ -1512,7 +2058,8 @@ private ResponseEntity convertToResponseEntity(RunnableWithException runnable) { } } - private ResponseEntity convertOptionalToResponseEntity(SupplierWithException> supplier) { + private ResponseEntity convertOptionalToResponseEntity( + SupplierWithException> supplier) { try { Optional result = supplier.get(); if (result.isEmpty()) { @@ -1545,10 +2092,12 @@ private ResponseEntity createInternalServerError(Exception e) { } private interface RunnableWithException { + void run() throws Exception; } private interface SupplierWithException { + T get() throws Exception; } diff --git a/src/main/java/de/samply/app/ProjectManagerWebConfig.java b/src/main/java/de/samply/app/ProjectManagerWebConfig.java new file mode 100644 index 00000000..5bba628c --- /dev/null +++ b/src/main/java/de/samply/app/ProjectManagerWebConfig.java @@ -0,0 +1,29 @@ +package de.samply.app; + +import de.samply.resolvers.LanguageArgumentResolver; +import de.samply.resolvers.RequestVariableAndParameterMethodArgumentResolver; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@Configuration +public class ProjectManagerWebConfig implements WebMvcConfigurer { + + private final RequestVariableAndParameterMethodArgumentResolver requestVariableResolver; + private final LanguageArgumentResolver languageArgumentResolver; + + public ProjectManagerWebConfig(RequestVariableAndParameterMethodArgumentResolver resolver, + LanguageArgumentResolver languageArgumentResolver) { + this.requestVariableResolver = resolver; + this.languageArgumentResolver = languageArgumentResolver; + } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(requestVariableResolver); + resolvers.add(languageArgumentResolver); + } + +} diff --git a/src/main/java/de/samply/app/ShedLockConfig.java b/src/main/java/de/samply/app/ShedLockConfig.java new file mode 100644 index 00000000..5fb279bf --- /dev/null +++ b/src/main/java/de/samply/app/ShedLockConfig.java @@ -0,0 +1,25 @@ +package de.samply.app; + +import net.javacrumbs.shedlock.core.LockProvider; +import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; + +import javax.sql.DataSource; + +@Configuration +public class ShedLockConfig { + + @Bean + public LockProvider lockProvider(DataSource dataSource) { + return new JdbcTemplateLockProvider( + JdbcTemplateLockProvider.Configuration.builder() + .withJdbcTemplate(new JdbcTemplate(dataSource)) + .withTableName(ProjectManagerConst.SHEDLOCK_TABLE_NAME) + .usingDbTime() + .build() + ); + } + +} diff --git a/src/main/java/de/samply/bridgehead/BridgeheadConfiguration.java b/src/main/java/de/samply/bridgehead/BridgeheadConfiguration.java index f4328b77..6331ddcd 100644 --- a/src/main/java/de/samply/bridgehead/BridgeheadConfiguration.java +++ b/src/main/java/de/samply/bridgehead/BridgeheadConfiguration.java @@ -34,6 +34,7 @@ public static class BridgeheadConfig { private String fileDispatcherBeamId; private String tokenManagerId; private String humanReadable; + private String affiliation; } @PostConstruct @@ -54,10 +55,8 @@ private void replaceHyphenInBridgeheads() { } private void logBridgeheads() { - log.info("Registered bridgeheads: (" + config.keySet().size() + ")"); - config.keySet().stream().sorted().forEach(bridgehead -> { - log.info("\t- " + bridgehead + " (" + getHumanReadable(bridgehead).get() + ")"); - }); + log.info("Registered bridgeheads: ({})", config.size()); + config.keySet().stream().sorted().forEach(bridgehead -> log.info("\t- {} ({})", bridgehead, getHumanReadable(bridgehead).orElse(""))); } private String replaceHyphen(String var) { @@ -79,19 +78,23 @@ private void addBridgeheadId(String bridgehead, String id, Map i } public boolean isRegisteredBridgehead(String bridgehead) { - return config.keySet().contains(bridgehead); + return config.containsKey(bridgehead); } public Optional getFocusBeamId(String bridgehead) { - return getProperty(bridgehead, bridgeheadConfig -> bridgeheadConfig.getFocusBeamId()); + return getProperty(bridgehead, BridgeheadConfig::getFocusBeamId); } public Optional getHumanReadable(String bridgehead) { - return getProperty(bridgehead, bridgeheadConfig -> bridgeheadConfig.getHumanReadable()); + return getProperty(bridgehead, BridgeheadConfig::getHumanReadable); + } + + public Optional getAffiliation(String bridgehead) { + return getProperty(bridgehead, BridgeheadConfig::getAffiliation); } public Optional getTokenManagerId(String bridgehead) { - return getProperty(bridgehead, bridgeheadConfig -> bridgeheadConfig.getTokenManagerId()); + return getProperty(bridgehead, BridgeheadConfig::getTokenManagerId); } Optional getProperty(@NotNull String bridgehead, Function propertyGetter) { @@ -107,14 +110,17 @@ public Optional getBridgeheadForExplorerId(String explorerId) { return fetchBridgeheadOptional(explorerId, explorerIdBridgeheadMap); } + @SuppressWarnings("unused") public String fetchBridgeheadForExplorerId(String explorerId) { return fetchBridgehead(explorerId, explorerIdBridgeheadMap); } + @SuppressWarnings("unused") public Optional getBridgeheadForFocusId(String focusId) { return fetchBridgeheadOptional(focusId, focusIdBridgeheadMap); } + @SuppressWarnings("unused") public String fetchBridgeheadForFocusId(String focusId) { return fetchBridgehead(focusId, focusIdBridgeheadMap); } @@ -123,6 +129,7 @@ public String getFileDispatcherBeamId(String bridgehead) { return config.get(bridgehead).getFileDispatcherBeamId(); } + @SuppressWarnings("unused") public Optional getBridgeheadForTokenManagerId(String tokenManagerId) { return fetchBridgeheadOptional(tokenManagerId, tokenManagerIdBridgeheadMap); } diff --git a/src/main/java/de/samply/bridgehead/BridgeheadOperationType.java b/src/main/java/de/samply/bridgehead/BridgeheadOperationType.java index baaeaf9e..3396853e 100644 --- a/src/main/java/de/samply/bridgehead/BridgeheadOperationType.java +++ b/src/main/java/de/samply/bridgehead/BridgeheadOperationType.java @@ -1,5 +1,6 @@ package de.samply.bridgehead; +@SuppressWarnings("unused") public enum BridgeheadOperationType { SEND_QUERY_TO_BRIDGEHEAD_AND_EXECUTE, SEND_QUERY_TO_BRIDGEHEAD, diff --git a/src/main/java/de/samply/coder/CoderJob.java b/src/main/java/de/samply/coder/CoderJob.java index c9f39939..3d742132 100644 --- a/src/main/java/de/samply/coder/CoderJob.java +++ b/src/main/java/de/samply/coder/CoderJob.java @@ -2,9 +2,8 @@ import de.samply.app.ProjectManagerConst; import de.samply.db.model.ProjectBridgeheadUser; -import de.samply.db.repository.ProjectBridgeheadUserRepository; -import de.samply.db.repository.ProjectCoderRepository; import de.samply.exporter.ExporterService; +import de.samply.project.ProjectBridgeheadUserService; import de.samply.project.ProjectType; import de.samply.project.state.ProjectBridgeheadState; import de.samply.query.QueryState; @@ -21,20 +20,18 @@ @Component public class CoderJob { - private final ProjectBridgeheadUserRepository projectBridgeheadUserRepository; - private final ProjectCoderRepository projectCoderRepository; + // Services private final CoderService coderService; private final ExporterService exporterService; private final AppRegisterService appRegisterService; + private final ProjectBridgeheadUserService projectBridgeheadUserService; - public CoderJob(ProjectBridgeheadUserRepository projectBridgeheadUserRepository, - ProjectCoderRepository projectCoderRepository, - CoderService coderService, + public CoderJob(CoderService coderService, ExporterService exporterService, - AppRegisterService appRegisterService + AppRegisterService appRegisterService, + ProjectBridgeheadUserService projectBridgeheadUserService ) { - this.projectBridgeheadUserRepository = projectBridgeheadUserRepository; - this.projectCoderRepository = projectCoderRepository; + this.projectBridgeheadUserService = projectBridgeheadUserService; this.coderService = coderService; this.exporterService = exporterService; this.appRegisterService = appRegisterService; @@ -60,7 +57,7 @@ public Mono manageCoderActiveUsers() { } private List fetchActiveUsers() { - return projectBridgeheadUserRepository.getDistinctInValidaProjectStateByProjectTypeAndQueryStateAndProjectBridgeheadState(ProjectType.RESEARCH_ENVIRONMENT, QueryState.FINISHED, ProjectBridgeheadState.ACCEPTED); + return projectBridgeheadUserService.fetchUsersInValidProjectState(ProjectType.RESEARCH_ENVIRONMENT, QueryState.FINISHED, ProjectBridgeheadState.ACCEPTED); } public Mono manageCoderInactiveUsers() { @@ -72,7 +69,7 @@ public Mono manageCoderInactiveUsers() { } private List fetchInactiveUsers() { - return projectBridgeheadUserRepository.getDistinctInInvalidProjectStateByProjectType(ProjectType.RESEARCH_ENVIRONMENT); + return projectBridgeheadUserService.fetchUsersInInvalidProjectState(ProjectType.RESEARCH_ENVIRONMENT); } } diff --git a/src/main/java/de/samply/coder/CoderService.java b/src/main/java/de/samply/coder/CoderService.java index 20e92dcb..6e70ff03 100644 --- a/src/main/java/de/samply/coder/CoderService.java +++ b/src/main/java/de/samply/coder/CoderService.java @@ -4,15 +4,17 @@ import de.samply.coder.request.CreateRequestBody; import de.samply.coder.request.Response; import de.samply.coder.request.TransitionRequestBody; +import de.samply.db.model.Project; +import de.samply.db.model.ProjectBridgehead; import de.samply.db.model.ProjectBridgeheadUser; import de.samply.db.model.ProjectCoder; -import de.samply.db.repository.ProjectBridgeheadUserRepository; import de.samply.db.repository.ProjectCoderRepository; import de.samply.notification.NotificationService; import de.samply.notification.OperationType; -import de.samply.security.SessionUser; +import de.samply.project.ProjectType; import de.samply.utils.WebClientFactory; import jakarta.validation.constraints.NotNull; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; @@ -32,16 +34,15 @@ public class CoderService { private final boolean coderEnabled; - private final ProjectBridgeheadUserRepository projectBridgeheadUserRepository; private final ProjectCoderRepository projectCoderRepository; private final NotificationService notificationService; - private final SessionUser sessionUser; private final CoderConfiguration coderConfiguration; private final WebClient webClient; private final String coderCreatePath; private final String coderDeletePath; private final String coderSessionToken; + @Getter private final String researchEnvironmentUrl; private final int coderWorkspaceMaxLength; @@ -50,8 +51,6 @@ public class CoderService { public CoderService( ProjectCoderRepository projectCoderRepository, NotificationService notificationService, - ProjectBridgeheadUserRepository projectBridgeheadUserRepository, - SessionUser sessionUser, CoderConfiguration coderConfiguration, WebClientFactory webClientFactory, @Value(ProjectManagerConst.ENABLE_CODER_SV) boolean coderEnabled, @@ -64,8 +63,6 @@ public CoderService( this.coderEnabled = coderEnabled; this.projectCoderRepository = projectCoderRepository; this.notificationService = notificationService; - this.projectBridgeheadUserRepository = projectBridgeheadUserRepository; - this.sessionUser = sessionUser; this.coderConfiguration = coderConfiguration; this.coderSessionToken = coderSessionToken; this.coderWorkspaceMaxLength = coderWorkspaceMaxLength; @@ -81,25 +78,16 @@ private String replaceVariablesInPath(String path, Map pathVaria AtomicReference result = new AtomicReference<>(path); if (path != null) { pathVariables.entrySet().stream().filter(entry -> entry.getKey() != null && entry.getValue() != null) - .forEach(entry -> result.set(result.get().replace(fetchVariableExpresion(entry.getKey()), entry.getValue()))); + .forEach(entry -> result.set(result.get().replace(fetchVariableExpression(entry.getKey()), entry.getValue()))); } return result.get(); } - private String fetchVariableExpresion(String variable) { + private String fetchVariableExpression(String variable) { return "{" + variable + "}"; } - public Mono createWorkspace(String email, String projectCode) throws CoderServiceException { - Optional user = projectBridgeheadUserRepository.getFirstByEmailAndProjectBridgehead_ProjectCodeOrderByModifiedAtDesc(email, projectCode); - if (user.isEmpty()) { - log.error("User " + email + " for project " + projectCode + " not found"); - return Mono.empty(); - } - return createWorkspace(user.get()); - } - public Mono createWorkspace(@NotNull ProjectBridgeheadUser projectBridgeheadUser) { if (coderEnabled) { if (projectCoderRepository.findFirstByProjectBridgeheadUserAndDeletedAtIsNullOrderByCreatedAtDesc(projectBridgeheadUser).isEmpty()) { @@ -108,7 +96,7 @@ public Mono createWorkspace(@NotNull ProjectBridgeheadUser project return createWorkspace(projectCoder, createRequestBody).flatMap(response -> { projectCoder.setWorkspaceId(response.getLatestBuild().getWorkspaceId()); projectCoderRepository.save(projectCoder); - notificationService.createNotification(projectBridgeheadUser.getProjectBridgehead().getProject().getCode(), + notificationService.createNotification(projectBridgeheadUser.getProjectBridgehead().getProject(), projectBridgeheadUser.getProjectBridgehead().getBridgehead(), projectBridgeheadUser.getEmail(), OperationType.CREATE_CODER_WORKSPACE, "Created workspace " + projectCoder.getWorkspaceId(), null, null); @@ -133,17 +121,19 @@ private Mono createWorkspace(ProjectCoder projectCoder, CreateRequestB log.info("Coder workspace created"); return clientResponse.bodyToMono(Response.class); } else { - log.error("Http error " + clientResponse.statusCode() + " creating workspace in Coder for user " - + projectCoder.getProjectBridgeheadUser().getEmail() + " in project " + - projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getProject().getCode()); + log.error("Http error {} creating workspace in Coder for user {} in project {}", clientResponse.statusCode(), projectCoder.getProjectBridgeheadUser().getEmail(), projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getProject().getCode()); return clientResponse.bodyToMono(String.class).flatMap(errorBody -> { - log.error("Error: {}", errorBody); + logError(errorBody); return Mono.error(new RuntimeException(errorBody)); }); } }); } + private void logError(String error) { + log.error("Error: {}", error); + } + private ProjectCoder generateProjectCoder(ProjectBridgeheadUser projectBridgeheadUser) { ProjectCoder projectCoder = new ProjectCoder(); projectCoder.setProjectBridgeheadUser(projectBridgeheadUser); @@ -154,7 +144,7 @@ private ProjectCoder generateProjectCoder(ProjectBridgeheadUser projectBridgehea public Flux deleteAllWorkspaces(@NotNull String projectCode, @NotNull String bridgehead) { return Flux.fromIterable(projectCoderRepository.findDistinctByProjectCodeAndBridgeheadIfNotDeleted(projectCode, bridgehead)) - .flatMap(projectCoder -> deleteWorkspace(projectCoder)); + .flatMap(this::deleteWorkspace); } public Mono deleteWorkspace(@NotNull ProjectBridgeheadUser user) { @@ -169,13 +159,13 @@ public Mono deleteWorkspace(@NotNull ProjectBridgeheadUser user) { public Mono deleteWorkspace(@NotNull ProjectCoder projectCoder) { if (coderEnabled) { - return deleteWorkspaceInCoder(projectCoder).doOnSuccess(response -> { + return deleteWorkspaceInCoder(projectCoder).doOnSuccess(_ -> { projectCoder.setDeletedAt(Instant.now()); projectCoderRepository.save(projectCoder); - notificationService.createNotification(projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getProject().getCode(), + notificationService.createNotification(projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getProject(), projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getBridgehead(), projectCoder.getProjectBridgeheadUser().getEmail(), OperationType.DELETE_CODER_WORKSPACE, "Deleted workspace " + projectCoder.getWorkspaceId(), null, null); - }).flatMap(response -> Mono.just(projectCoder)); + }).flatMap(_ -> Mono.just(projectCoder)); } return Mono.empty(); } @@ -193,11 +183,9 @@ private Mono deleteWorkspaceInCoder(ProjectCoder projectCoder) { log.info("Coder workspace deleted"); return clientResponse.bodyToMono(Response.class); } else { - log.error("Http error " + clientResponse.statusCode() + " deleting workspace in Coder for user " - + projectCoder.getProjectBridgeheadUser().getEmail() + " in project " + - projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getProject().getCode()); + log.error("Http error {} deleting workspace in Coder for user {} in project {}", clientResponse.statusCode(), projectCoder.getProjectBridgeheadUser().getEmail(), projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getProject().getCode()); return clientResponse.bodyToMono(String.class).flatMap(errorBody -> { - log.error("Error: {}", errorBody); + logError(errorBody); return Mono.error(new RuntimeException(errorBody)); }); } @@ -205,7 +193,7 @@ private Mono deleteWorkspaceInCoder(ProjectCoder projectCoder) { } private CreateRequestBody generateCreateRequestBody(ProjectCoder projectCoder) { - CreateRequestBody createRequestBody = coderConfiguration.cloneCreateRequestBody(projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getProject().getType()); + CreateRequestBody createRequestBody = coderConfiguration.cloneCreateRequestBody(ProjectType.RESEARCH_ENVIRONMENT); createRequestBody.setName(projectCoder.getAppId()); replaceParameterValues(createRequestBody, projectCoder); return createRequestBody; @@ -255,19 +243,24 @@ private String fetchCoderMemberId(@NotNull String email) { return email.substring(0, email.indexOf("@")).replace(".", ""); } - public String getResearchEnvironmentUrl() { - return researchEnvironmentUrl; - } - - public boolean existsUserResearchEnvironmentWorkspace(@NotNull String projectCode, @NotNull String bridgehead) { - List projectCoder = this.projectCoderRepository.findByBridgeheadAndProjectCodeOrderedByCreatedAtDesc(bridgehead, projectCode); - return !projectCoder.isEmpty() && projectCoder.get(0).getDeletedAt() == null; + public boolean existsUserResearchEnvironmentWorkspace(@NotNull Project project, @NotNull ProjectBridgehead bridgehead) { + List projectCoder = this.projectCoderRepository + .findByBridgeheadAndProjectCodeOrderedByCreatedAtDesc(bridgehead.getBridgehead(), project.getCode()); + return !projectCoder.isEmpty() && projectCoder.getFirst().getDeletedAt() == null; } public boolean existsUserResearchEnvironmentWorkspace(@NotNull ProjectBridgeheadUser projectBridgeheadUser) { List projectCoders = this.projectCoderRepository.findByProjectBridgeheadUserOrderByCreatedAtDesc(projectBridgeheadUser); - return !projectCoders.isEmpty() && projectCoders.get(0).getDeletedAt() == null; + return !projectCoders.isEmpty() && projectCoders.getFirst().getDeletedAt() == null; + } + + public List fetchCoderOrderedByCreatedAtDesc(String projectCode, String bridgehead, String email) { + return projectCoderRepository.findByBridgeheadAndProjectCodeAndEmailOrderedByCreatedAtDesc(bridgehead, projectCode, email); + } + + public void saveCoder(ProjectCoder projectCoder) { + projectCoderRepository.save(projectCoder); } } diff --git a/src/main/java/de/samply/coder/CoderServiceException.java b/src/main/java/de/samply/coder/CoderServiceException.java deleted file mode 100644 index b5f887d2..00000000 --- a/src/main/java/de/samply/coder/CoderServiceException.java +++ /dev/null @@ -1,7 +0,0 @@ -package de.samply.coder; - -public class CoderServiceException extends RuntimeException{ - public CoderServiceException(String message) { - super(message); - } -} diff --git a/src/main/java/de/samply/coder/request/Build.java b/src/main/java/de/samply/coder/request/Build.java index e68da82b..1864ad9f 100644 --- a/src/main/java/de/samply/coder/request/Build.java +++ b/src/main/java/de/samply/coder/request/Build.java @@ -3,6 +3,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; +// This class can be instantiated with CoderConfiguration with ObjectMapper +@SuppressWarnings("unused") @Data public class Build { diff --git a/src/main/java/de/samply/coder/request/TransitionRequestBody.java b/src/main/java/de/samply/coder/request/TransitionRequestBody.java index 090b84bc..84bf822f 100644 --- a/src/main/java/de/samply/coder/request/TransitionRequestBody.java +++ b/src/main/java/de/samply/coder/request/TransitionRequestBody.java @@ -12,6 +12,7 @@ public class TransitionRequestBody { @JsonProperty("orphan") private boolean orphan = false; + @SuppressWarnings("unused") public TransitionRequestBody() { } diff --git a/src/main/java/de/samply/datashield/DataShieldTokenManagerJob.java b/src/main/java/de/samply/datashield/DataShieldTokenManagerJob.java index 3fdb419a..b336f06c 100644 --- a/src/main/java/de/samply/datashield/DataShieldTokenManagerJob.java +++ b/src/main/java/de/samply/datashield/DataShieldTokenManagerJob.java @@ -6,20 +6,19 @@ import de.samply.datashield.dto.DataShieldProjectStatus; import de.samply.datashield.dto.DataShieldTokenManagerProjectStatus; import de.samply.datashield.dto.DataShieldTokenStatus; +import de.samply.db.model.Project; import de.samply.db.model.ProjectBridgehead; import de.samply.db.model.ProjectBridgeheadDataShield; import de.samply.db.model.ProjectBridgeheadUser; -import de.samply.db.repository.ProjectBridgeheadDataShieldRepository; -import de.samply.db.repository.ProjectBridgeheadRepository; -import de.samply.db.repository.ProjectBridgeheadUserRepository; import de.samply.email.EmailService; import de.samply.email.EmailServiceException; import de.samply.email.EmailTemplateType; +import de.samply.project.ProjectBridgeheadService; +import de.samply.project.ProjectBridgeheadUserService; import de.samply.project.ProjectType; import de.samply.project.state.ProjectBridgeheadState; import de.samply.project.state.ProjectState; import de.samply.register.AppRegisterService; -import de.samply.rstudio.group.RstudioGroupService; import de.samply.user.roles.ProjectRole; import lombok.Data; import lombok.extern.slf4j.Slf4j; @@ -39,33 +38,31 @@ @Component public class DataShieldTokenManagerJob { - private final RstudioGroupService rstudioGroupService; + // Services private final CoderService coderService; + private final ProjectBridgeheadService projectBridgeheadService; + private final ProjectBridgeheadUserService projectBridgeheadUserService; private final DataShieldTokenManagerService tokenManagerService; - private final ProjectBridgeheadUserRepository projectBridgeheadUserRepository; - private final ProjectBridgeheadRepository projectBridgeheadRepository; - private final ProjectBridgeheadDataShieldRepository projectBridgeheadDataShieldRepository; private final EmailService emailService; - private final BridgeheadConfiguration bridgeheadConfiguration; private final AppRegisterService appRegisterService; + + private final BridgeheadConfiguration bridgeheadConfiguration; + private final boolean isTokenManagerActive; - public DataShieldTokenManagerJob(RstudioGroupService rstudioGroupService, - CoderService coderService, + public DataShieldTokenManagerJob(CoderService coderService, + ProjectBridgeheadService projectBridgeheadService, + ProjectBridgeheadUserService projectBridgeheadUserService, DataShieldTokenManagerService tokenManagerService, - ProjectBridgeheadUserRepository projectBridgeheadUserRepository, - ProjectBridgeheadRepository projectBridgeheadRepository, - ProjectBridgeheadDataShieldRepository projectBridgeheadDataShieldRepository, - EmailService emailService, BridgeheadConfiguration bridgeheadConfiguration, + EmailService emailService, + BridgeheadConfiguration bridgeheadConfiguration, AppRegisterService appRegisterService, @Value(ProjectManagerConst.ENABLE_TOKEN_MANAGER_SV) boolean isTokenManagerActive ) { - this.rstudioGroupService = rstudioGroupService; this.coderService = coderService; + this.projectBridgeheadService = projectBridgeheadService; + this.projectBridgeheadUserService = projectBridgeheadUserService; this.tokenManagerService = tokenManagerService; - this.projectBridgeheadUserRepository = projectBridgeheadUserRepository; - this.projectBridgeheadRepository = projectBridgeheadRepository; - this.projectBridgeheadDataShieldRepository = projectBridgeheadDataShieldRepository; this.emailService = emailService; this.bridgeheadConfiguration = bridgeheadConfiguration; this.appRegisterService = appRegisterService; @@ -91,15 +88,15 @@ private Mono manageActiveUsers() { fetchActiveUsersOfDataShieldProjectsInDevelopPilotAndFinalState().stream() .flatMap(user -> tokenManagerService.fetchProjectBridgeheads( - user.getProjectBridgehead().getProject().getCode(), - user.getProjectBridgehead().getBridgehead(), + user.getProjectBridgehead().getProject(), + user.getProjectBridgehead(), user.getEmail(), projectBridgehead -> projectBridgehead.getState() == ProjectBridgeheadState.ACCEPTED).stream() .filter(this::isBridgeheadConfiguredForTokenManager) .map(bridgehead -> new UserBridgehead(user, bridgehead))) .toList()) .flatMap(userBridgehead -> - tokenManagerService.fetchTokenStatus(userBridgehead.user().getProjectBridgehead().getProject().getCode(), + tokenManagerService.fetchTokenStatus(userBridgehead.user().getProjectBridgehead().getProject(), userBridgehead.bridgehead, userBridgehead.user().getEmail()) .filter(status -> status.projectStatus() == DataShieldProjectStatus.WITH_DATA) .flatMap(status -> { @@ -107,11 +104,11 @@ private Mono manageActiveUsers() { sendNewTokenEmailAndCreateWorkspaceIfNotExists(userBridgehead.user(), usersToSendAnEmail); if (status.tokenStatus() == DataShieldTokenStatus.NOT_FOUND) { // If user token not found: Create token return tokenManagerService.generateTokensInOpal( - userBridgehead.user().getProjectBridgehead().getProject().getCode(), + userBridgehead.user().getProjectBridgehead().getProject(), userBridgehead.bridgehead(), userBridgehead.user().getEmail(), ifSuccessMonoSupplier); } else if (status.tokenStatus() == DataShieldTokenStatus.EXPIRED) { // If user token expired: Refresh Token return tokenManagerService.refreshToken( - userBridgehead.user().getProjectBridgehead().getProject().getCode(), + userBridgehead.user().getProjectBridgehead().getProject(), userBridgehead.bridgehead(), userBridgehead.user().getEmail(), ifSuccessMonoSupplier); } else if (status.tokenStatus() == DataShieldTokenStatus.CREATED) { @@ -123,15 +120,14 @@ private Mono manageActiveUsers() { .then(); } - private record UserBridgehead(ProjectBridgeheadUser user, String bridgehead) { + private record UserBridgehead(ProjectBridgeheadUser user, ProjectBridgehead bridgehead) { } private Mono sendNewTokenEmailAndCreateWorkspaceIfNotExists(ProjectBridgeheadUser user, Set usersToSendAnEmail) { ProjectEmail projectEmail = new ProjectEmail(user.getProjectBridgehead().getProject().getCode(), user.getProjectBridgehead().getBridgehead()); if (!usersToSendAnEmail.contains(projectEmail)) { usersToSendAnEmail.add(projectEmail); - sendEmail(user.getEmail(), user.getProjectBridgehead().getProject().getCode(), user.getProjectBridgehead().getBridgehead(), EmailTemplateType.NEW_TOKEN_FOR_AUTHENTICATION_SCRIPT, user.getProjectRole()); - this.rstudioGroupService.addUserToRstudioGroup(user.getEmail()); + sendEmail(user.getEmail(), user.getProjectBridgehead().getProject(), user.getProjectBridgehead(), EmailTemplateType.NEW_TOKEN_FOR_AUTHENTICATION_SCRIPT, user.getProjectRole()); return createWorkspaceIfNotExists(user); } return Mono.empty(); @@ -143,15 +139,15 @@ private Mono createWorkspaceIfNotExists(ProjectBridgeheadUser user) { } private Set fetchActiveUsersOfDataShieldProjectsInDevelopPilotAndFinalState() { - Set activeUsers = this.projectBridgeheadUserRepository.getByProjectTypeAndProjectStateAndProjectRole(ProjectType.DATASHIELD, ProjectState.DEVELOP, ProjectRole.DEVELOPER); - activeUsers.addAll(this.projectBridgeheadUserRepository.getByProjectTypeAndProjectStateAndProjectRole(ProjectType.DATASHIELD, ProjectState.PILOT, ProjectRole.PILOT)); - activeUsers.addAll(this.projectBridgeheadUserRepository.getByProjectTypeAndProjectStateAndProjectRole(ProjectType.DATASHIELD, ProjectState.FINAL, ProjectRole.FINAL)); + Set activeUsers = projectBridgeheadUserService.fetchUsers(ProjectType.DATASHIELD, ProjectState.DEVELOP, ProjectRole.DEVELOPER); + activeUsers.addAll(projectBridgeheadUserService.fetchUsers(ProjectType.DATASHIELD, ProjectState.PILOT, ProjectRole.PILOT)); + activeUsers.addAll(projectBridgeheadUserService.fetchUsers(ProjectType.DATASHIELD, ProjectState.FINAL, ProjectRole.FINAL)); return activeUsers; } - private void sendEmail(String email, String projectCode, String bridgehead, EmailTemplateType type, ProjectRole projectRole) { + private void sendEmail(String email, Project project, ProjectBridgehead bridgehead, EmailTemplateType type, ProjectRole projectRole) { try { - emailService.sendEmail(email, Optional.ofNullable(projectCode), Optional.ofNullable(bridgehead), projectRole, type); + emailService.sendEmail(email, Optional.ofNullable(project), Optional.ofNullable(bridgehead), projectRole, type); } catch (EmailServiceException e) { throw new RuntimeException(e); } @@ -161,17 +157,16 @@ private Mono manageInactiveUsers() { log.debug("Manage DataSHIELD inactive users..."); Set usersToSendAnEmail = new HashSet<>(); return Flux.fromIterable(Stream.concat( - // Manage users that are not developers in develop, pilot in pilot or final in final + // Manage users that are not developers in development, pilot in pilot or final in final Stream.of( - this.projectBridgeheadUserRepository.getByProjectTypeAndProjectStateAndNotProjectRole(ProjectType.DATASHIELD, ProjectState.DEVELOP, ProjectRole.DEVELOPER), - this.projectBridgeheadUserRepository.getByProjectTypeAndProjectStateAndNotProjectRole(ProjectType.DATASHIELD, ProjectState.PILOT, ProjectRole.PILOT), - this.projectBridgeheadUserRepository.getByProjectTypeAndProjectStateAndNotProjectRole(ProjectType.DATASHIELD, ProjectState.FINAL, ProjectRole.FINAL)) + projectBridgeheadUserService.fetchUsersWithNotProjectRole(ProjectType.DATASHIELD, ProjectState.DEVELOP, ProjectRole.DEVELOPER), + projectBridgeheadUserService.fetchUsersWithNotProjectRole(ProjectType.DATASHIELD, ProjectState.PILOT, ProjectRole.PILOT), + projectBridgeheadUserService.fetchUsersWithNotProjectRole(ProjectType.DATASHIELD, ProjectState.FINAL, ProjectRole.FINAL)) .flatMap(Set::stream) - .filter(user -> user.getProjectRole() != ProjectRole.CREATOR) .flatMap(user -> this.tokenManagerService.fetchProjectBridgeheads( - user.getProjectBridgehead().getProject().getCode(), - user.getProjectBridgehead().getBridgehead(), + user.getProjectBridgehead().getProject(), + user.getProjectBridgehead(), user.getEmail()).stream() .filter(this::isBridgeheadConfiguredForTokenManager) .map(bridgehead -> new UserBridgehead(user, bridgehead))), @@ -179,8 +174,8 @@ private Mono manageInactiveUsers() { // Manage active users that are not accepted in any of the bridgeheads fetchActiveUsersOfDataShieldProjectsInDevelopPilotAndFinalState().stream() .flatMap(user -> this.tokenManagerService.fetchProjectBridgeheads( - user.getProjectBridgehead().getProject().getCode(), - user.getProjectBridgehead().getBridgehead(), + user.getProjectBridgehead().getProject(), + user.getProjectBridgehead(), user.getEmail(), projectBridgehead -> projectBridgehead.getState() != ProjectBridgeheadState.ACCEPTED).stream() .filter(this::isBridgeheadConfiguredForTokenManager) @@ -189,13 +184,13 @@ private Mono manageInactiveUsers() { .then(); } - private Mono manageInactiveUsers(ProjectBridgeheadUser user, String bridgehead, Set usersToSendAnEmail) { - return tokenManagerService.fetchTokenStatus(user.getProjectBridgehead().getProject().getCode(), bridgehead, user.getEmail()) // Check user status + private Mono manageInactiveUsers(ProjectBridgeheadUser user, ProjectBridgehead bridgehead, Set usersToSendAnEmail) { + return tokenManagerService.fetchTokenStatus(user.getProjectBridgehead().getProject(), bridgehead, user.getEmail()) // Check user status .filter(status -> status.tokenStatus() != DataShieldTokenStatus.NOT_FOUND && status.tokenStatus() != DataShieldTokenStatus.INACTIVE) - .flatMap(status -> { - // If user token created or expired: Remove token - if (this.projectBridgeheadUserRepository.getFirstValidByEmailAndProjectBridgehead(user.getEmail(), user.getProjectBridgehead()).isEmpty()) { // Check that is not valid again (e.g. user that is developer and final user in final state - return tokenManagerService.removeTokens(user.getProjectBridgehead().getProject().getCode(), bridgehead, user.getEmail(), + .flatMap(_ -> { + // If a user token created or expired: Remove token + if (projectBridgeheadUserService.fetchFirstValidUser(user.getEmail(), user.getProjectBridgehead()).isEmpty()) { // Check that is not valid again (e.g., user that is a developer and final user in the final state + return tokenManagerService.removeTokens(user.getProjectBridgehead().getProject(), bridgehead, user.getEmail(), () -> sendEmailAndDeleteWorkspace(user, usersToSendAnEmail)); } else { return deleteWorkspace(user); @@ -204,12 +199,22 @@ private Mono manageInactiveUsers(ProjectBridgeheadUser user, String bridge } private Mono sendEmailAndDeleteWorkspace(ProjectBridgeheadUser user, Set usersToSendAnEmail) { - ProjectEmail projectEmail = new ProjectEmail(user.getEmail(), user.getProjectBridgehead().getProject().getCode()); + ProjectEmail projectEmail = new ProjectEmail( + user.getEmail(), + user.getProjectBridgehead().getProject().getCode()); if (!usersToSendAnEmail.contains(projectEmail)) { usersToSendAnEmail.add(projectEmail); - sendEmail(user.getEmail(), user.getProjectBridgehead().getProject().getCode(), user.getProjectBridgehead().getBridgehead(), EmailTemplateType.INVALID_AUTHENTICATION_SCRIPT, user.getProjectRole()); - if (user.getProjectRole() != ProjectRole.FINAL || this.projectBridgeheadRepository.findByProjectAndState(user.getProjectBridgehead().getProject(), ProjectBridgeheadState.ACCEPTED).isEmpty()) { - this.rstudioGroupService.removeUserFromRstudioGroup(user.getEmail()); + sendEmail( + user.getEmail(), + user.getProjectBridgehead().getProject(), + user.getProjectBridgehead(), + EmailTemplateType.INVALID_AUTHENTICATION_SCRIPT, + user.getProjectRole()); + if (user.getProjectRole() != ProjectRole.FINAL || + projectBridgeheadService.fetchBridgeheads( + user.getProjectBridgehead().getProject(), + ProjectBridgeheadState.ACCEPTED) + .isEmpty()) { return deleteWorkspace(user); } } @@ -232,13 +237,19 @@ private boolean isBridgeheadConfiguredForTokenManager(String bridgehead) { private Mono manageInactiveProjects() { log.debug("Manage DataSHIELD active projects..."); // Get users of DataSHIELD inactive states projects - return Flux.fromIterable(this.projectBridgeheadRepository.getByProjectTypeAndNotProjectState(ProjectType.DATASHIELD, Set.of(ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL)).stream() - .filter(this::isBridgeheadConfiguredForTokenManager) - .map(projectBridgehead -> new InactiveProject(projectBridgeheadDataShieldRepository.findByProjectBridgehead(projectBridgehead), projectBridgehead)) - .filter(inactiveProject -> inactiveProject.getProjectBridgeheadDataShield().isEmpty() || !inactiveProject.getProjectBridgeheadDataShield().get().isRemoved()) - .toList()) + return Flux.fromIterable( + projectBridgeheadService + .fetchByProjectTypeAndNotProjectState( + ProjectType.DATASHIELD, + Set.of(ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL)) + .stream() + .filter(this::isBridgeheadConfiguredForTokenManager) + .map(projectBridgehead -> + new InactiveProject(projectBridgeheadService.fetchDataShield(projectBridgehead), projectBridgehead)) + .filter(inactiveProject -> inactiveProject.getProjectBridgeheadDataShield().isEmpty() || !inactiveProject.getProjectBridgeheadDataShield().get().isRemoved()) + .toList()) .flatMap(inactiveProject -> - tokenManagerService.fetchProjectStatus(inactiveProject.getProjectBridgehead().getProject().getCode(), inactiveProject.getProjectBridgehead().getBridgehead()) + tokenManagerService.fetchProjectStatus(inactiveProject.getProjectBridgehead().getProject(), inactiveProject.getProjectBridgehead()) .flatMap(status -> { inactiveProject.setStatus(status); return Mono.just(inactiveProject); @@ -247,20 +258,20 @@ private Mono manageInactiveProjects() { .flatMap(inactiveProject -> Mono.when( Mono.just(inactiveProject) - .filter(unused -> + .filter(_ -> inactiveProject.getStatus().projectStatus() != DataShieldProjectStatus.NOT_FOUND && inactiveProject.getStatus().projectStatus() != DataShieldProjectStatus.INACTIVE) - .flatMap(unused -> tokenManagerService.removeProjectAndTokens(inactiveProject.getProjectBridgehead().getProject().getCode(), inactiveProject.getProjectBridgehead().getBridgehead())), + .flatMap(_ -> tokenManagerService.removeProjectAndTokens(inactiveProject.getProjectBridgehead().getProject(), inactiveProject.getProjectBridgehead())), coderService.deleteAllWorkspaces(inactiveProject.getProjectBridgehead().getProject().getCode(), inactiveProject.getProjectBridgehead().getBridgehead()) .flatMap(appRegisterService::unregister).then()) - .doOnSuccess(unused -> setAsRemovedIfConditions(inactiveProject)) + .doOnSuccess(_ -> setAsRemovedIfConditions(inactiveProject)) ) .then(); } @Data - private class InactiveProject { - private Optional projectBridgeheadDataShield = Optional.empty(); + private static class InactiveProject { + private Optional projectBridgeheadDataShield; private ProjectBridgehead projectBridgehead; private DataShieldTokenManagerProjectStatus status; @@ -277,8 +288,8 @@ private void setAsRemovedIfConditions(InactiveProject inactiveProject) { if ((inactiveProject.getStatus().projectStatus() == DataShieldProjectStatus.NOT_FOUND || inactiveProject.getStatus().projectStatus() == DataShieldProjectStatus.INACTIVE) && coderService.existsUserResearchEnvironmentWorkspace( - inactiveProject.getProjectBridgehead().getProject().getCode(), - inactiveProject.getProjectBridgehead().getBridgehead()) + inactiveProject.getProjectBridgehead().getProject(), + inactiveProject.getProjectBridgehead()) ) { result = inactiveProject.getProjectBridgeheadDataShield().get(); result.setRemoved(true); @@ -288,7 +299,7 @@ private void setAsRemovedIfConditions(InactiveProject inactiveProject) { result.setProjectBridgehead(inactiveProject.getProjectBridgehead()); } if (result != null) { - projectBridgeheadDataShieldRepository.save(result); + projectBridgeheadService.saveDataShield(result); } } diff --git a/src/main/java/de/samply/datashield/DataShieldTokenManagerService.java b/src/main/java/de/samply/datashield/DataShieldTokenManagerService.java index bd2596f6..990e4aa0 100644 --- a/src/main/java/de/samply/datashield/DataShieldTokenManagerService.java +++ b/src/main/java/de/samply/datashield/DataShieldTokenManagerService.java @@ -9,11 +9,10 @@ import de.samply.db.model.Project; import de.samply.db.model.ProjectBridgehead; import de.samply.db.model.ProjectBridgeheadUser; -import de.samply.db.repository.ProjectBridgeheadRepository; -import de.samply.db.repository.ProjectBridgeheadUserRepository; -import de.samply.db.repository.ProjectRepository; import de.samply.notification.NotificationService; import de.samply.notification.OperationType; +import de.samply.project.ProjectBridgeheadService; +import de.samply.project.ProjectBridgeheadUserService; import de.samply.security.SessionUser; import de.samply.user.roles.ProjectRole; import de.samply.utils.WebClientFactory; @@ -49,57 +48,60 @@ @Slf4j public class DataShieldTokenManagerService { + + // Services + private final ProjectBridgeheadService projectBridgeheadService; + private final ProjectBridgeheadUserService projectBridgeheadUserService; + private final NotificationService notificationService; + private final SessionUser sessionUser; private final WebClientFactory webClientFactory; private final WebClient webClient; - private final ProjectRepository projectRepository; - private final ProjectBridgeheadRepository projectBridgeheadRepository; - private final ProjectBridgeheadUserRepository projectBridgeheadUserRepository; - private final NotificationService notificationService; + private final BridgeheadConfiguration bridgeheadConfiguration; + private final boolean isTokenManagerActive; + private final ObjectMapper objectMapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT); public DataShieldTokenManagerService(SessionUser sessionUser, WebClientFactory webClientFactory, @Value(ProjectManagerConst.TOKEN_MANAGER_URL_SV) String tokenManagerUrl, - ProjectRepository projectRepository, - ProjectBridgeheadRepository projectBridgeheadRepository, - ProjectBridgeheadUserRepository projectBridgeheadUserRepository, + ProjectBridgeheadService projectBridgeheadService, + ProjectBridgeheadUserService projectBridgeheadUserService, NotificationService notificationService, BridgeheadConfiguration bridgeheadConfiguration, @Value(ProjectManagerConst.ENABLE_TOKEN_MANAGER_SV) boolean isTokenManagerActive) { this.sessionUser = sessionUser; this.webClientFactory = webClientFactory; - this.projectRepository = projectRepository; - this.projectBridgeheadRepository = projectBridgeheadRepository; - this.projectBridgeheadUserRepository = projectBridgeheadUserRepository; + this.projectBridgeheadService = projectBridgeheadService; + this.projectBridgeheadUserService = projectBridgeheadUserService; this.notificationService = notificationService; this.bridgeheadConfiguration = bridgeheadConfiguration; this.isTokenManagerActive = isTokenManagerActive; this.webClient = webClientFactory.createWebClient(tokenManagerUrl); } - public Mono generateTokensInOpal(@NotNull String projectCode, @NotNull String bridgehead, @NotNull String email, Supplier ifSuccessMonoSupplier) throws DataShieldTokenManagerServiceException { + public Mono generateTokensInOpal(@NotNull Project project, @NotNull ProjectBridgehead bridgehead, @NotNull String email, Supplier ifSuccessMonoSupplier) throws DataShieldTokenManagerServiceException { if (!isTokenManagerActive) { - log.error("Token manager is not active. It couldn't generate token in opal for project " + projectCode + " and bridgehead " + bridgehead + " and user " + email); + log.error("Token manager is not active. It couldn't generate token in opal for project {} and bridgehead {} and user {}", project.getCode(), bridgehead.getBridgehead(), email); return Mono.empty(); } - List bridgeheads = List.of(bridgehead); + List bridgeheads = List.of(bridgehead); List tokenManagerIds = fetchTokenManagerIds(bridgeheads); - if (tokenManagerIds.size() > 0) { - log.info("Generating token in Opal for project {}, bridgehead {} and user {}...", projectCode, bridgehead, email); + if (!tokenManagerIds.isEmpty()) { + log.info("Generating token in Opal for project {}, bridgehead {} and user {}...", project, bridgehead, email); AtomicInteger retryCount = new AtomicInteger(0); return webClient.post().uri(uriBuilder -> uriBuilder.path(ProjectManagerConst.TOKEN_MANAGER_ROOT + ProjectManagerConst.TOKEN_MANAGER_TOKENS).build()) .contentType(MediaType.APPLICATION_JSON) - .bodyValue(new TokenParams(email, projectCode, tokenManagerIds)) + .bodyValue(new TokenParams(email, project.getCode(), tokenManagerIds)) .retrieve() .bodyToMono(String.class) .retryWhen( Retry.fixedDelay(webClientFactory.getWebClientMaxNumberOfRetries(), Duration.ofSeconds(webClientFactory.getWebClientTimeInSecondsAfterRetryWithFailure())) .filter(error -> error instanceof WebClientResponseException) - .doBeforeRetry(s -> retryCount.incrementAndGet()) + .doBeforeRetry(_ -> retryCount.incrementAndGet()) ) .doOnError(WebClientResponseException.class, ex -> { HttpStatusCode statusCode = ex.getStatusCode(); @@ -108,19 +110,19 @@ public Mono generateTokensInOpal(@NotNull String projectCode, @NotNull Str if (statusCode.equals(HttpStatus.BAD_REQUEST)) { log.error("Received 400 Bad Request"); } else if (statusCode.is5xxServerError()) { - log.error("Received Server Error: " + statusCode); + log.error("Received Server Error: {}", statusCode); } else { - log.error("Received HTTP Status Code: " + statusCode); + log.error("Received HTTP Status Code: {}", statusCode); } if (retryCount.get() >= webClientFactory.getWebClientMaxNumberOfRetries()) { bridgeheads.forEach(tempBridgehead -> - notificationService.createNotification(projectCode, tempBridgehead, email, + notificationService.createNotification(project, tempBridgehead.getBridgehead(), email, OperationType.CREATE_DATASHIELD_TOKEN, "Error generating token", error, (HttpStatus) statusCode)); } }) - .doOnSuccess(response -> bridgeheads.forEach(tempBridgehead -> { + .doOnSuccess(_ -> bridgeheads.forEach(tempBridgehead -> { log.info("Token generated successfully for bridgehead {}", tempBridgehead); - notificationService.createNotification(projectCode, tempBridgehead, email, + notificationService.createNotification(project, tempBridgehead.getBridgehead(), email, OperationType.CREATE_DATASHIELD_TOKEN, "Token generated successfully in Token Manager", null, null); })) .then((ifSuccessMonoSupplier != null) ? ifSuccessMonoSupplier.get() : Mono.empty()); @@ -128,46 +130,37 @@ public Mono generateTokensInOpal(@NotNull String projectCode, @NotNull Str return Mono.empty(); } - public List fetchProjectBridgeheads(String projectCode, String bridgehead, String email) throws DataShieldTokenManagerServiceException { - return fetchProjectBridgeheads(projectCode, bridgehead, email, projectBridgehead -> true); + public List fetchProjectBridgeheads(Project project, ProjectBridgehead bridgehead, String email) throws DataShieldTokenManagerServiceException { + return fetchProjectBridgeheads(project, bridgehead, email, _ -> true); } - public List fetchProjectBridgeheads(String projectCode, String bridgehead, String email, Function filter) throws DataShieldTokenManagerServiceException { - Project project = fetchProject(projectCode); - Optional projectBridgeheadUser = projectBridgeheadUserRepository.getFirstByEmailAndProjectBridgehead_ProjectAndProjectBridgehead_BridgeheadOrderByModifiedAtDesc(email, project, bridgehead); + public List fetchProjectBridgeheads(Project project, ProjectBridgehead bridgehead, String email, Function filter) throws DataShieldTokenManagerServiceException { + Optional projectBridgeheadUser = projectBridgeheadUserService.fetchFirstUsersOrderByModifiedAtDesc(email, bridgehead); if (projectBridgeheadUser.isEmpty()) { - throw new DataShieldTokenManagerServiceException("User " + email + " with token manager rights not found for project " + projectCode); + throw new DataShieldTokenManagerServiceException("User " + email + " with token manager rights not found for project " + project); } ProjectRole userProjectRole = projectBridgeheadUser.get().getProjectRole(); if (userProjectRole == ProjectRole.DEVELOPER || userProjectRole == ProjectRole.PILOT) { return (filter.apply(projectBridgeheadUser.get().getProjectBridgehead())) ? List.of(bridgehead) : List.of(); } else if (userProjectRole == ProjectRole.FINAL) { - return projectBridgeheadRepository.findByProject(project).stream().filter(filter::apply).map(projectBridgehead -> projectBridgehead.getBridgehead()).toList(); + return projectBridgeheadService.fetchBridgeheads(project).stream().filter(filter::apply).toList(); } else { throw new DataShieldTokenManagerServiceException("Role " + userProjectRole + " of user " + email + " not supported"); } } - private Project fetchProject(String projectCode) throws DataShieldTokenManagerServiceException { - Optional project = projectRepository.findByCode(projectCode); - if (project.isEmpty()) { - throw new DataShieldTokenManagerServiceException("Project " + projectCode + " not found"); - } - return project.get(); - } - - public Mono fetchTokenStatus(@NotNull String projectCode, @NotNull String bridgehead, @NotNull String email) { + public Mono fetchTokenStatus(@NotNull Project project, @NotNull ProjectBridgehead bridgehead, @NotNull String email) { if (!isTokenManagerActive) { - return Mono.just(new DataShieldTokenManagerTokenStatus(projectCode, bridgehead, email, null, DataShieldProjectStatus.INACTIVE, DataShieldTokenStatus.INACTIVE)); + return Mono.just(new DataShieldTokenManagerTokenStatus(project.getCode(), bridgehead.getBridgehead(), email, null, DataShieldProjectStatus.INACTIVE, DataShieldTokenStatus.INACTIVE)); } Optional tokenManagerId = fetchTokenManagerId(bridgehead); if (tokenManagerId.isPresent()) { String uri = UriComponentsBuilder.fromPath(ProjectManagerConst.TOKEN_MANAGER_ROOT + ProjectManagerConst.TOKEN_MANAGER_TOKEN_STATUS) .queryParam(ProjectManagerConst.TOKEN_MANAGER_PARAMETER_BRIDGEHEAD, tokenManagerId.get()) - .queryParam(ProjectManagerConst.TOKEN_MANAGER_PARAMETER_PROJECT_CODE, projectCode) + .queryParam(ProjectManagerConst.TOKEN_MANAGER_PARAMETER_PROJECT_CODE, project) .queryParam(ProjectManagerConst.TOKEN_MANAGER_PARAMETER_EMAIL, email) .toUriString(); - log.debug("Fetching DataSHIELD Token status for project {}, bridgehead {} and user {}...", projectCode, bridgehead, email); + log.debug("Fetching DataSHIELD Token status for project {}, bridgehead {} and user {}...", project, bridgehead, email); return webClient.get() .uri(uri) .accept(MediaType.APPLICATION_JSON) @@ -175,10 +168,10 @@ public Mono fetchTokenStatus(@NotNull String .bodyToMono(DataShieldTokenManagerTokenStatus.class) .onErrorResume(exception -> { log.debug(ExceptionUtils.getStackTrace(exception)); - return Mono.just(new DataShieldTokenManagerTokenStatus(projectCode, bridgehead, email, Instant.now().toString(), DataShieldProjectStatus.ERROR, DataShieldTokenStatus.ERROR)); + return Mono.just(new DataShieldTokenManagerTokenStatus(project.getCode(), bridgehead.getBridgehead(), email, Instant.now().toString(), DataShieldProjectStatus.ERROR, DataShieldTokenStatus.ERROR)); }) .doOnSuccess(dataShieldTokenManagerTokenStatus -> - log.debug("Token status received: " + printJsonObject(dataShieldTokenManagerTokenStatus)) + log.debug("Token status received: {}", printJsonObject(dataShieldTokenManagerTokenStatus)) ) .map(this::replaceTokenManagerId); @@ -196,16 +189,16 @@ private String printJsonObject(Object object) { } } - public Mono fetchProjectStatus(@NotNull String projectCode, @NotNull String bridgehead) { + public Mono fetchProjectStatus(@NotNull Project project, @NotNull ProjectBridgehead bridgehead) { if (!isTokenManagerActive) { - return Mono.just(new DataShieldTokenManagerProjectStatus(projectCode, bridgehead, DataShieldProjectStatus.INACTIVE)); + return Mono.just(new DataShieldTokenManagerProjectStatus(project.getCode(), bridgehead.getBridgehead(), DataShieldProjectStatus.INACTIVE)); } Optional tokenManagerId = fetchTokenManagerId(bridgehead); if (tokenManagerId.isPresent()) { - log.debug("Fetching DataSHIELD project status for project {} and bridgehead {}", projectCode, bridgehead); + log.debug("Fetching DataSHIELD project status for project {} and bridgehead {}", project, bridgehead); String uri = UriComponentsBuilder.fromPath(ProjectManagerConst.TOKEN_MANAGER_ROOT + ProjectManagerConst.TOKEN_MANAGER_PROJECT_STATUS) .queryParam(ProjectManagerConst.TOKEN_MANAGER_PARAMETER_BRIDGEHEAD, tokenManagerId.get()) - .queryParam(ProjectManagerConst.TOKEN_MANAGER_PARAMETER_PROJECT_CODE, projectCode) + .queryParam(ProjectManagerConst.TOKEN_MANAGER_PARAMETER_PROJECT_CODE, project) .toUriString(); return webClient.get() .uri(uri) @@ -214,52 +207,50 @@ public Mono fetchProjectStatus(@NotNull Str .bodyToMono(DataShieldTokenManagerProjectStatus.class) .onErrorResume(exception -> { log.debug(ExceptionUtils.getStackTrace(exception)); - return Mono.just(new DataShieldTokenManagerProjectStatus(projectCode, bridgehead, DataShieldProjectStatus.ERROR)); + return Mono.just(new DataShieldTokenManagerProjectStatus(project.getCode(), bridgehead.getBridgehead(), DataShieldProjectStatus.ERROR)); }) - .doOnSuccess(dataShieldTokenManagerProjectStatus -> log.debug("DataSHIELD Project Status fetched: " + printJsonObject(dataShieldTokenManagerProjectStatus))) + .doOnSuccess(dataShieldTokenManagerProjectStatus -> log.debug("DataSHIELD ProjectCode Status fetched: {}", printJsonObject(dataShieldTokenManagerProjectStatus))) .map(this::replaceTokenManagerId); } else { throw new DataShieldTokenManagerServiceException("Bridgehead " + bridgehead + " not configured for token manager"); } } - public Resource fetchAuthenticationScript(String projectCode, String bridgehead) throws DataShieldTokenManagerServiceException { + public Resource fetchAuthenticationScript(Project project, ProjectBridgehead bridgehead) throws DataShieldTokenManagerServiceException { if (!isTokenManagerActive) { return new ByteArrayResource("Token Manager inactive".getBytes()); } - List tokenManagerIds = fetchTokenManagerIds(fetchProjectBridgeheads(projectCode, bridgehead, sessionUser.getEmail())); - if (tokenManagerIds.size() > 0) { - log.debug("Fetching authentication script for project {} and bridgehead {}", projectCode, bridgehead); + List tokenManagerIds = fetchTokenManagerIds(fetchProjectBridgeheads(project, bridgehead, sessionUser.getEmail())); + if (!tokenManagerIds.isEmpty()) { + log.debug("Fetching authentication script for project {} and bridgehead {}", project, bridgehead); String authenticationScript = webClient.post().uri(uriBuilder -> uriBuilder.path(ProjectManagerConst.TOKEN_MANAGER_ROOT + ProjectManagerConst.TOKEN_MANAGER_SCRIPTS).build()) .contentType(MediaType.APPLICATION_JSON) - .bodyValue(new TokenParams(sessionUser.getEmail(), projectCode, tokenManagerIds)) + .bodyValue(new TokenParams(sessionUser.getEmail(), project.getCode(), tokenManagerIds)) .accept(MediaType.TEXT_PLAIN).retrieve().bodyToMono(String.class).block(); log.debug("Authentication script fetched"); if (!StringUtils.hasText(authenticationScript)) { - throw new DataShieldTokenManagerServiceException("Script could not be generated for project " + projectCode + " and user " + sessionUser.getEmail()); + throw new DataShieldTokenManagerServiceException("Script could not be generated for project " + project + " and user " + sessionUser.getEmail()); } return new ByteArrayResource(authenticationScript.getBytes()); } - throw new DataShieldTokenManagerServiceException("Script could not be generated for project " + projectCode + " and user " + sessionUser.getEmail()); + throw new DataShieldTokenManagerServiceException("Script could not be generated for project " + project + " and user " + sessionUser.getEmail()); } - public Boolean existsAuthenticationScript(String projectCode, String bridgehead) { + public Boolean existsAuthenticationScript(Project project, ProjectBridgehead bridgehead) { if (!isTokenManagerActive) { return false; } - List tokenManagerIds = fetchTokenManagerIds(fetchProjectBridgeheads(projectCode, bridgehead, sessionUser.getEmail())); - log.debug("Checking if authentication script exists for project {} and bridgehead {}", projectCode, bridgehead); + List tokenManagerIds = fetchTokenManagerIds(fetchProjectBridgeheads(project, bridgehead, sessionUser.getEmail())); + log.debug("Checking if authentication script exists for project {} and bridgehead {}", project, bridgehead); return Boolean.valueOf(webClient.post() .uri(uriBuilder -> uriBuilder.path(ProjectManagerConst.TOKEN_MANAGER_ROOT + ProjectManagerConst.AUTHENTICATION_SCRIPT_STATUS).build()) .contentType(MediaType.APPLICATION_JSON) - .bodyValue(new TokenParams(sessionUser.getEmail(), projectCode, tokenManagerIds)) + .bodyValue(new TokenParams(sessionUser.getEmail(), project.getCode(), tokenManagerIds)) .retrieve() .bodyToMono(String.class) - .doOnSuccess(result -> { - log.debug("Exists authentication script: " + Boolean.valueOf(result)); - }) + .doOnSuccess(result -> log.debug("Exists authentication script: {}", Boolean.valueOf(result))) .onErrorResume(exception -> { log.debug(ExceptionUtils.getStackTrace(exception)); return Mono.just("false"); @@ -268,25 +259,25 @@ public Boolean existsAuthenticationScript(String projectCode, String bridgehead) } - public Mono refreshToken(@NotNull String projectCode, @NotNull String bridgehead, @NotNull String email, Supplier ifSuccessMonoSupplier) throws DataShieldTokenManagerServiceException { + public Mono refreshToken(@NotNull Project project, @NotNull ProjectBridgehead bridgehead, @NotNull String email, Supplier ifSuccessMonoSupplier) throws DataShieldTokenManagerServiceException { if (!isTokenManagerActive) { - log.error("Token Manager inactive in project manager. It coudn#t refresh token for project " + projectCode + " and bridgehead " + bridgehead + " and user " + email); + log.error("Token Manager inactive in project manager. It couldn't refresh token for project {} and bridgehead {} and user {}", project, bridgehead, email); return Mono.empty(); } - List tokenManagerIds = fetchTokenManagerIds(fetchProjectBridgeheads(projectCode, bridgehead, email)); - if (tokenManagerIds.size() > 0) { - TokenParams tokenParams = new TokenParams(email, projectCode, tokenManagerIds); + List tokenManagerIds = fetchTokenManagerIds(fetchProjectBridgeheads(project, bridgehead, email)); + if (!tokenManagerIds.isEmpty()) { + TokenParams tokenParams = new TokenParams(email, project.getCode(), tokenManagerIds); String uri = ProjectManagerConst.TOKEN_MANAGER_ROOT + ProjectManagerConst.TOKEN_MANAGER_REFRESH_TOKEN; - log.info("Refreshing DataSHIELD Token for project {}, bridgehead {} and user {}...", projectCode, bridgehead, email); + log.info("Refreshing DataSHIELD Token for project {}, bridgehead {} and user {}...", project, bridgehead, email); return webClient.put() .uri(uri) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .body(BodyInserters.fromValue(tokenParams)) .retrieve() .bodyToMono(String.class) - .doOnSuccess(response -> { + .doOnSuccess(_ -> { log.info("DataSHIELD Token refreshed"); - notificationService.createNotification(projectCode, bridgehead, email, + notificationService.createNotification(project, bridgehead.getBridgehead(), email, OperationType.REFRESH_DATASHIELD_TOKEN, "Token refreshed", null, null); }) .then((ifSuccessMonoSupplier != null) ? ifSuccessMonoSupplier.get() : Mono.empty()); @@ -294,72 +285,72 @@ public Mono refreshToken(@NotNull String projectCode, @NotNull String brid return Mono.empty(); } - public Mono removeTokens(@NotNull String projectCode, @NotNull String bridgehead, @NotNull String email, Supplier ifSuccessMonoSupplier) { + public Mono removeTokens(@NotNull Project project, @NotNull ProjectBridgehead bridgehead, @NotNull String email, Supplier ifSuccessMonoSupplier) { if (!isTokenManagerActive) { - log.error("Token Manager inactive in project manager. It couln't remove tokens for project " + projectCode + " and bridgehead " + bridgehead + " and user " + email); + log.error("Token Manager inactive in project manager. It couldn't remove tokens for project {} and bridgehead {} and user {}", project, bridgehead, email); return Mono.empty(); } Optional tokenManagerId = fetchTokenManagerId(bridgehead); if (tokenManagerId.isPresent()) { String uri = UriComponentsBuilder.fromPath(ProjectManagerConst.TOKEN_MANAGER_ROOT + ProjectManagerConst.TOKEN_MANAGER_TOKENS) .queryParam(ProjectManagerConst.TOKEN_MANAGER_PARAMETER_BRIDGEHEAD, tokenManagerId.get()) - .queryParam(ProjectManagerConst.TOKEN_MANAGER_PARAMETER_PROJECT_CODE, projectCode) + .queryParam(ProjectManagerConst.TOKEN_MANAGER_PARAMETER_PROJECT_CODE, project) .queryParam(ProjectManagerConst.TOKEN_MANAGER_PARAMETER_EMAIL, email) .toUriString(); - log.info("Removing token for project {}, bridgehead {} and user {}...", projectCode, bridgehead, email); + log.info("Removing token for project {}, bridgehead {} and user {}...", project, bridgehead, email); return webClient.delete() .uri(uri) .retrieve() .bodyToMono(Void.class) - .doOnSuccess(response -> { + .doOnSuccess(_ -> { log.info("DataSHIELD Token removed"); - notificationService.createNotification(projectCode, bridgehead, email, + notificationService.createNotification(project, bridgehead.getBridgehead(), email, OperationType.REMOVE_DATASHIELD_TOKEN, "Token removed", null, null); }) .then((ifSuccessMonoSupplier != null) ? ifSuccessMonoSupplier.get() : Mono.empty()); } else { - log.error("Bridgehead " + bridgehead + " not configured for token manager"); + log.error("Bridgehead {} not configured for token manager", bridgehead); return Mono.empty(); } } - public Mono removeProjectAndTokens(@NotNull String projectCode, @NotNull String bridgehead) { + public Mono removeProjectAndTokens(@NotNull Project project, @NotNull ProjectBridgehead bridgehead) { if (!isTokenManagerActive) { - log.error("Token Manager inactive in project manager. It cannot remove tokens for project " + projectCode + " and bridgehead " + bridgehead); + log.error("Token Manager inactive in project manager. It cannot remove tokens for project {} and bridgehead {}", project, bridgehead); return Mono.empty(); } Optional tokenManagerId = fetchTokenManagerId(bridgehead); if (tokenManagerId.isPresent()) { String uri = UriComponentsBuilder.fromPath(ProjectManagerConst.TOKEN_MANAGER_ROOT + ProjectManagerConst.TOKEN_MANAGER_PROJECT) .queryParam(ProjectManagerConst.TOKEN_MANAGER_PARAMETER_BRIDGEHEAD, tokenManagerId.get()) - .queryParam(ProjectManagerConst.TOKEN_MANAGER_PARAMETER_PROJECT_CODE, projectCode) + .queryParam(ProjectManagerConst.TOKEN_MANAGER_PARAMETER_PROJECT_CODE, project) .toUriString(); - log.info("Removing token for project {}, bridgehead {}", projectCode, bridgehead); + log.info("Removing token for project {}, bridgehead {}", project, bridgehead); return webClient.delete() .uri(uri) .retrieve() .bodyToMono(Void.class) - .doOnSuccess(response -> { - log.debug("Project and token removed"); - notificationService.createNotification(projectCode, bridgehead, null, + .doOnSuccess(_ -> { + log.debug("ProjectCode and token removed"); + notificationService.createNotification(project, bridgehead.getBridgehead(), null, OperationType.REMOVE_DATASHIELD_TOKEN, "Token removed", null, null); }) .then(); } else { - log.info("Bridgehead " + bridgehead + " not configured for token manager"); + log.info("Bridgehead {} not configured for token manager", bridgehead); return Mono.empty(); } } - private List fetchTokenManagerIds(List bridgeheads) { + private List fetchTokenManagerIds(List bridgeheads) { return bridgeheads.stream().map(bridgehead -> { Optional tokenManagerId = fetchTokenManagerId(bridgehead); - return (tokenManagerId.isPresent()) ? tokenManagerId.get() : null; + return tokenManagerId.orElse(null); }).filter(ObjectUtils::isNotEmpty).toList(); } - private Optional fetchTokenManagerId(String bridgehead) { - return bridgeheadConfiguration.getTokenManagerId(bridgehead); + private Optional fetchTokenManagerId(ProjectBridgehead bridgehead) { + return bridgeheadConfiguration.getTokenManagerId(bridgehead.getBridgehead()); } private DataShieldTokenManagerProjectStatus replaceTokenManagerId(DataShieldTokenManagerProjectStatus dataShieldTokenManagerProjectStatus) { @@ -369,7 +360,7 @@ private DataShieldTokenManagerProjectStatus replaceTokenManagerId(DataShieldToke bridgeheadConfiguration.fetchBridgeheadForTokenManagerId(dataShieldTokenManagerProjectStatus.bridgehead()), dataShieldTokenManagerProjectStatus.projectStatus() ) - : dataShieldTokenManagerProjectStatus; + : null; } private DataShieldTokenManagerTokenStatus replaceTokenManagerId(DataShieldTokenManagerTokenStatus dataShieldTokenManagerTokenStatus) { @@ -382,7 +373,7 @@ private DataShieldTokenManagerTokenStatus replaceTokenManagerId(DataShieldTokenM dataShieldTokenManagerTokenStatus.projectStatus(), dataShieldTokenManagerTokenStatus.tokenStatus() ) - : dataShieldTokenManagerTokenStatus; + : null; } } diff --git a/src/main/java/de/samply/datashield/DataShieldTokenManagerServiceException.java b/src/main/java/de/samply/datashield/DataShieldTokenManagerServiceException.java index 687bb53e..2718ea7f 100644 --- a/src/main/java/de/samply/datashield/DataShieldTokenManagerServiceException.java +++ b/src/main/java/de/samply/datashield/DataShieldTokenManagerServiceException.java @@ -1,11 +1,9 @@ package de.samply.datashield; public class DataShieldTokenManagerServiceException extends RuntimeException { - public DataShieldTokenManagerServiceException(Throwable cause) { - super(cause); - } public DataShieldTokenManagerServiceException(String message) { super(message); } + } diff --git a/src/main/java/de/samply/datashield/ProjectEmail.java b/src/main/java/de/samply/datashield/ProjectEmail.java index d12c5b05..5a5a830d 100644 --- a/src/main/java/de/samply/datashield/ProjectEmail.java +++ b/src/main/java/de/samply/datashield/ProjectEmail.java @@ -1,18 +1,6 @@ package de.samply.datashield; -import lombok.EqualsAndHashCode; -import lombok.Getter; - -@EqualsAndHashCode(of = {"email", "projectCode"}) -@Getter -public class ProjectEmail { - private String email; - private String projectCode; - - public ProjectEmail(String email, String projectCode) { - this.email = email; - this.projectCode = projectCode; - } +public record ProjectEmail(String email, String projectCode) { } diff --git a/src/main/java/de/samply/datashield/dto/DataShieldProjectStatus.java b/src/main/java/de/samply/datashield/dto/DataShieldProjectStatus.java index 84aae204..a4ff78dd 100644 --- a/src/main/java/de/samply/datashield/dto/DataShieldProjectStatus.java +++ b/src/main/java/de/samply/datashield/dto/DataShieldProjectStatus.java @@ -1,9 +1,12 @@ package de.samply.datashield.dto; + +// Status comes from Token Manager +@SuppressWarnings("unused") public enum DataShieldProjectStatus { CREATED, // The project exists in the bridgehead opal - WITH_DATA, // The project has already data in the bridgehead opal - NOT_FOUND, // The project wasn't found in the bridghead opal - INACTIVE, // The token manager is not enabled in project manager + WITH_DATA, // The project already has data in the bridgehead opal + NOT_FOUND, // The project wasn't found in the bridgehead opal + INACTIVE, // The token manager is not enabled in the project manager ERROR // There was an error while fetching the status } diff --git a/src/main/java/de/samply/db/model/CreatorUser.java b/src/main/java/de/samply/db/model/CreatorUser.java new file mode 100644 index 00000000..9974e0fd --- /dev/null +++ b/src/main/java/de/samply/db/model/CreatorUser.java @@ -0,0 +1,26 @@ +package de.samply.db.model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "creator_user", schema = "samply") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CreatorUser { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + @Column(name = "email", nullable = false) + private String email; + + @Column(name = "bridgehead", nullable = false) + private String bridgehead; + +} diff --git a/src/main/java/de/samply/db/model/Project.java b/src/main/java/de/samply/db/model/Project.java index 0e2a5e82..02c9968d 100644 --- a/src/main/java/de/samply/db/model/Project.java +++ b/src/main/java/de/samply/db/model/Project.java @@ -10,6 +10,8 @@ import java.time.Instant; import java.time.LocalDate; +import java.util.Optional; +import java.util.Set; @Entity @Table(name = "project", schema = "samply") @@ -52,12 +54,8 @@ public class Project { @JoinColumn(name = "query_id") private Query query; - @Column(name = "type") - @Enumerated(EnumType.STRING) - private ProjectType type; - - @Column(name = "is_custom_config", nullable = false) - private boolean isCustomConfig = true; + @Column(name = "is_custom_config_selected") + private Boolean isCustomConfigSelected; @Column(name = "results_url") @Convert(converter = EncryptionConverter.class) @@ -67,4 +65,24 @@ public class Project { @Enumerated(EnumType.STRING) private UserProjectState creatorResultsState = UserProjectState.CREATED; + @Transient + public Set fetchProjectTypes() { + return query.fetchProjectTypes(); + } + + @Transient + public boolean hasProjectType(ProjectType projectType) { + return query.hasProjectType(projectType); + } + + @Transient + public Optional fetchOutput(ProjectType projectType) { + return query.fetchOutput(projectType); + } + + @Transient + public void addOutput(QueryOutput output) { + query.addOutput(output); + } + } diff --git a/src/main/java/de/samply/db/model/ProjectBridgehead.java b/src/main/java/de/samply/db/model/ProjectBridgehead.java index 3725f062..57d4d635 100644 --- a/src/main/java/de/samply/db/model/ProjectBridgehead.java +++ b/src/main/java/de/samply/db/model/ProjectBridgehead.java @@ -1,26 +1,30 @@ package de.samply.db.model; +import de.samply.project.ProjectType; import de.samply.project.state.ProjectBridgeheadState; import de.samply.project.state.UserProjectState; import de.samply.query.QueryState; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; +import jakarta.validation.constraints.NotNull; +import lombok.*; import java.time.Instant; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; @Entity @Table(name = "project_bridgehead", schema = "samply") @Data @NoArgsConstructor @AllArgsConstructor +@EqualsAndHashCode(onlyExplicitlyIncluded = true) public class ProjectBridgehead { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "id", nullable = false) + @EqualsAndHashCode.Include private Long id; @ManyToOne @@ -37,22 +41,6 @@ public class ProjectBridgehead { @Column(name = "modified_at", nullable = false) private Instant modifiedAt = Instant.now(); - @Column(name = "query_state", nullable = false) - @Enumerated(EnumType.STRING) - private QueryState queryState = QueryState.CREATED; - - @Column(name = "exporter_response") - private String exporterResponse; - - @Column(name = "exporter_user") - private String exporterUser; - - @Column(name = "exporter_execution_id") - private String exporterExecutionId; - - @Column(name = "exporter_dispatch_counter") - private int exporterDispatchCounter = 0; - @Column(name = "results_url") @Convert(converter = EncryptionConverter.class) private String resultsUrl; @@ -61,4 +49,61 @@ public class ProjectBridgehead { @Enumerated(EnumType.STRING) private UserProjectState creatorResultsState = UserProjectState.CREATED; + @OneToMany(mappedBy = "projectBridgehead", + cascade = CascadeType.ALL, + orphanRemoval = true, + fetch = FetchType.EAGER) + @ToString.Exclude + private Set executions = new HashSet<>(); + + @Transient + public void addOrUpdateExecution(@NotNull ProjectType projectType, + QueryState queryState, + String exporterResponse, + String exporterUser, + String exporterExecutionId, + Integer exporterDispatchCounter) { + + this.project.fetchOutput(projectType).ifPresentOrElse( + queryOutput -> { + ProjectBridgeheadExecution execution = executions.stream() + .filter(e -> e.getQueryOutput().equals(queryOutput)) + .findFirst() + .orElseGet(() -> { + ProjectBridgeheadExecution newExec = new ProjectBridgeheadExecution(); + newExec.setQueryOutput(queryOutput); + newExec.setProjectBridgehead(this); + executions.add(newExec); + return newExec; + }); + + // Update all provided fields + Optional.ofNullable(queryState).ifPresent(execution::setQueryState); + Optional.ofNullable(exporterResponse).ifPresent(execution::setExporterResponse); + Optional.ofNullable(exporterUser).ifPresent(execution::setExporterUser); + Optional.ofNullable(exporterExecutionId).ifPresent(execution::setExporterExecutionId); + Optional.ofNullable(exporterDispatchCounter).ifPresent(execution::setExporterDispatchCounter); + execution.setModifiedAt(Instant.now()); + }, + () -> { + throw new IllegalArgumentException("ProjectCode does not have output for project type " + projectType); + } + ); + + } + + @Transient + public Optional fetchExecution(ProjectType projectType) { + return executions.stream() + .filter(e -> e.getQueryOutput().getProjectType().equals(projectType)) + .findFirst(); + } + + @Transient + public void removeExecution(ProjectType projectType) { + executions.removeIf(e -> + e.getQueryOutput().getProjectType().equals(projectType) + ); + } + } diff --git a/src/main/java/de/samply/db/model/ProjectBridgeheadExecution.java b/src/main/java/de/samply/db/model/ProjectBridgeheadExecution.java new file mode 100644 index 00000000..06f20461 --- /dev/null +++ b/src/main/java/de/samply/db/model/ProjectBridgeheadExecution.java @@ -0,0 +1,55 @@ +package de.samply.db.model; + +import de.samply.query.QueryState; +import jakarta.persistence.*; +import lombok.*; + +import java.time.Instant; + +@Entity +@Table(name = "project_bridgehead_execution", + schema = "samply", + uniqueConstraints = @UniqueConstraint( + columnNames = {"project_bridgehead_id", "query_output_id"})) +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +public class ProjectBridgeheadExecution { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "project_bridgehead_id", nullable = false) + @ToString.Exclude + @EqualsAndHashCode.Include + private ProjectBridgehead projectBridgehead; + + @ManyToOne + @JoinColumn(name = "query_output_id", nullable = false) + @ToString.Exclude + @EqualsAndHashCode.Include + private QueryOutput queryOutput; + + @Column(name = "query_state", nullable = false) + @Enumerated(EnumType.STRING) + private QueryState queryState = QueryState.CREATED; + + @Column(name = "exporter_response") + private String exporterResponse; + + @Column(name = "exporter_user") + private String exporterUser; + + @Column(name = "exporter_execution_id") + private String exporterExecutionId; + + @Column(name = "exporter_dispatch_counter") + private int exporterDispatchCounter = 0; + + @Column(name = "modified_at", nullable = false) + private Instant modifiedAt = Instant.now(); + +} \ No newline at end of file diff --git a/src/main/java/de/samply/db/model/ProjectForm.java b/src/main/java/de/samply/db/model/ProjectForm.java new file mode 100644 index 00000000..36f647f4 --- /dev/null +++ b/src/main/java/de/samply/db/model/ProjectForm.java @@ -0,0 +1,35 @@ +package de.samply.db.model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; + +@Entity +@Table(name = "project_form", schema = "samply") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ProjectForm { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + @ManyToOne + @JoinColumn(name = "project_id", nullable = false) + private Project project; + + @Column(name = "form_title", nullable = false) + private String formTitle; + + @Column(name = "created_at", nullable = false) + private Instant createdAt = Instant.now(); + + @Column(name = "modified_at") + private Instant modifiedAt; + +} diff --git a/src/main/java/de/samply/db/model/ProjectFormField.java b/src/main/java/de/samply/db/model/ProjectFormField.java new file mode 100644 index 00000000..30806faa --- /dev/null +++ b/src/main/java/de/samply/db/model/ProjectFormField.java @@ -0,0 +1,42 @@ +package de.samply.db.model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; + +@Entity +@Table(name = "project_form_field", schema = "samply") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ProjectFormField { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + @ManyToOne + @JoinColumn(name = "project_id", nullable = false) + private Project project; + + @Column(name = "label", nullable = false) + private String label; + + @Column(name = "form_title", nullable = false) + private String formTitle; + + @Column(name = "value") + private String value; + + @Column(name = "created_at", nullable = false) + private Instant createdAt = Instant.now(); + + @Column(name = "modified_at") + private Instant modifiedAt; + + +} diff --git a/src/main/java/de/samply/db/model/Query.java b/src/main/java/de/samply/db/model/Query.java index 6b959cbf..f6e65755 100644 --- a/src/main/java/de/samply/db/model/Query.java +++ b/src/main/java/de/samply/db/model/Query.java @@ -1,24 +1,27 @@ package de.samply.db.model; -import de.samply.query.OutputFormat; +import de.samply.project.ProjectType; import de.samply.query.QueryFormat; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.*; import java.time.Instant; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; @Entity @Table(name = "query", schema = "samply") @Data @NoArgsConstructor @AllArgsConstructor +@EqualsAndHashCode(onlyExplicitlyIncluded = true) public class Query { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "id", nullable = false) + @EqualsAndHashCode.Include private Long id; @Column(name = "code", nullable = false) @@ -37,12 +40,8 @@ public class Query { @Enumerated(EnumType.STRING) private QueryFormat queryFormat; - @Column(name = "output_format") - @Enumerated(EnumType.STRING) - private OutputFormat outputFormat; - - @Column(name = "template_id") - private String templateId; + @Column(name = "cohort_definition") + private String cohortDefinition; @Column(name = "label") private String label; @@ -50,10 +49,46 @@ public class Query { @Column(name = "description") private String description; + @Column(name = "details") + private String details; + @Column(name = "explorer_url") private String explorerUrl; @Column(name = "context") private String context; -} + @OneToMany(mappedBy = "query", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) + @ToString.Exclude // prevent infinite recursion in toString + private Set outputs = new HashSet<>(); + + @Transient + public Set fetchProjectTypes() { + return outputs.stream().map(QueryOutput::getProjectType).collect(Collectors.toSet()); + } + + @Transient + public boolean hasProjectType(ProjectType projectType) { + return outputs.stream().anyMatch(queryOutput -> queryOutput.getProjectType() == projectType); + } + + @Transient + public Optional fetchOutput(ProjectType projectType) { + return outputs.stream() + .filter(queryOutput -> queryOutput.getProjectType() == projectType) + .findFirst(); + } + + @Transient + public void addOutput(QueryOutput output) { + outputs.add(output); + output.setQuery(this); + } + + @Transient + public void removeOutput(ProjectType projectType) { + outputs.removeIf(queryOutput -> queryOutput.getProjectType() == projectType); + } + + +} \ No newline at end of file diff --git a/src/main/java/de/samply/db/model/QueryOutput.java b/src/main/java/de/samply/db/model/QueryOutput.java new file mode 100644 index 00000000..82197427 --- /dev/null +++ b/src/main/java/de/samply/db/model/QueryOutput.java @@ -0,0 +1,41 @@ +package de.samply.db.model; + +import de.samply.project.ProjectType; +import de.samply.query.OutputFormat; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name = "query_output", schema = "samply", + uniqueConstraints = @UniqueConstraint(columnNames = {"query_id", "project_type"})) +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +public class QueryOutput { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "query_id", nullable = false) + @ToString.Exclude + @EqualsAndHashCode.Exclude // prevents recursion + private Query query; + + @Column(name = "project_type", nullable = false) + @Enumerated(EnumType.STRING) + @EqualsAndHashCode.Include + private ProjectType projectType; + + @Column(name = "template_id") + @EqualsAndHashCode.Include + private String templateId; + + @Column(name = "output_format") + @Enumerated(EnumType.STRING) + @EqualsAndHashCode.Include + private OutputFormat outputFormat; + +} \ No newline at end of file diff --git a/src/main/java/de/samply/db/model/User.java b/src/main/java/de/samply/db/model/User.java index 202152a5..b10c8539 100644 --- a/src/main/java/de/samply/db/model/User.java +++ b/src/main/java/de/samply/db/model/User.java @@ -20,10 +20,10 @@ public class User { @Column(name = "email", nullable = false, unique = true) private String email; - @Column(name = "first_name", nullable = false) + @Column(name = "first_name") private String firstName; - @Column(name = "last_name", nullable = false) + @Column(name = "last_name") private String lastName; @Column(name = "mailing_black_list", nullable = false) diff --git a/src/main/java/de/samply/db/repository/CreatorUserRepository.java b/src/main/java/de/samply/db/repository/CreatorUserRepository.java new file mode 100644 index 00000000..1d5d4e33 --- /dev/null +++ b/src/main/java/de/samply/db/repository/CreatorUserRepository.java @@ -0,0 +1,17 @@ +package de.samply.db.repository; + +import de.samply.db.model.CreatorUser; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.Set; + +@Repository +public interface CreatorUserRepository extends JpaRepository { + + Set findByEmail(String email); + + Optional findByEmailAndBridgehead(String email, String bridgehead); + +} diff --git a/src/main/java/de/samply/db/repository/ProjectBridgeheadRepository.java b/src/main/java/de/samply/db/repository/ProjectBridgeheadRepository.java index a1ad68eb..48e613a3 100644 --- a/src/main/java/de/samply/db/repository/ProjectBridgeheadRepository.java +++ b/src/main/java/de/samply/db/repository/ProjectBridgeheadRepository.java @@ -6,8 +6,6 @@ import de.samply.project.state.ProjectBridgeheadState; import de.samply.project.state.ProjectState; import de.samply.query.QueryState; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @@ -19,20 +17,41 @@ @Repository public interface ProjectBridgeheadRepository extends JpaRepository { + Optional findFirstByBridgeheadAndProject_Code(String bridgehead, String projectCode); + Optional findFirstByBridgeheadAndProject(String bridgehead, Project project); Set findByProject(Project project); - Set findByProjectCodeAndState(String projectCode, ProjectBridgeheadState state); - Set findByProjectAndState(Project project, ProjectBridgeheadState state); - Page findAll(Pageable pageable); - - @Query("SELECT DISTINCT pb FROM ProjectBridgehead pb WHERE pb.project.type = :projectType AND pb.project.state NOT IN :projectStates") - List getByProjectTypeAndNotProjectState(ProjectType projectType, Set projectStates); - - @Query("SELECT DISTINCT pb FROM ProjectBridgehead pb WHERE pb.queryState = :queryState AND pb.project.state IN :projectStates") - Set getByQueryStateAndProjectState(QueryState queryState, Set projectStates); + @Query(""" + SELECT DISTINCT pb + FROM ProjectBridgehead pb + JOIN pb.project p + JOIN p.query q + JOIN q.outputs qo + WHERE qo.projectType = :projectType + AND p.state NOT IN :projectStates + """) + List getByProjectTypeAndNotProjectState( + ProjectType projectType, + Set projectStates + ); + + + @Query(""" + SELECT DISTINCT pb + FROM ProjectBridgehead pb + JOIN pb.executions exec + WHERE exec.queryState = :queryState + AND pb.project.state IN :projectStates + """) + Set getByQueryStateAndProjectState( + QueryState queryState, + Set projectStates + ); + + List findByProject_Query_Id(Long queryId); } diff --git a/src/main/java/de/samply/db/repository/ProjectBridgeheadUserRepository.java b/src/main/java/de/samply/db/repository/ProjectBridgeheadUserRepository.java index 99295319..47d6f3a1 100644 --- a/src/main/java/de/samply/db/repository/ProjectBridgeheadUserRepository.java +++ b/src/main/java/de/samply/db/repository/ProjectBridgeheadUserRepository.java @@ -25,8 +25,6 @@ public interface ProjectBridgeheadUserRepository extends JpaRepository getFirstByEmailAndProjectBridgeheadOrderByModifiedAtDesc(String email, ProjectBridgehead projectBridgehead); - Optional getFirstByEmailAndProjectBridgehead_ProjectCodeOrderByModifiedAtDesc(String email, String projectCode); - Optional getFirstByEmailAndProjectBridgehead_ProjectAndProjectBridgehead_BridgeheadOrderByModifiedAtDesc(String email, Project project, String bridgehead); List getByProjectBridgehead(ProjectBridgehead projectBridgehead); @@ -34,11 +32,39 @@ public interface ProjectBridgeheadUserRepository extends JpaRepository findProjectsByEmail(String email); - @Query("SELECT DISTINCT pbu FROM ProjectBridgeheadUser pbu WHERE pbu.projectBridgehead.project.type = :projectType AND pbu.projectBridgehead.project.state = :projectState AND pbu.projectRole = :projectRole") - Set getByProjectTypeAndProjectStateAndProjectRole(ProjectType projectType, ProjectState projectState, ProjectRole projectRole); - - @Query("SELECT DISTINCT pbu FROM ProjectBridgeheadUser pbu WHERE pbu.projectBridgehead.project.type = :projectType AND pbu.projectBridgehead.project.state = :projectState AND pbu.projectRole != :projectRole") - Set getByProjectTypeAndProjectStateAndNotProjectRole(ProjectType projectType, ProjectState projectState, ProjectRole projectRole); + @Query(""" + SELECT DISTINCT pbu + FROM ProjectBridgeheadUser pbu + JOIN pbu.projectBridgehead pb + JOIN pb.project p + JOIN p.query q + JOIN q.outputs qo + WHERE qo.projectType = :projectType + AND p.state = :projectState + AND pbu.projectRole = :projectRole + """) + Set getByProjectTypeAndProjectStateAndProjectRole( + ProjectType projectType, + ProjectState projectState, + ProjectRole projectRole + ); + + @Query(""" + SELECT DISTINCT pbu + FROM ProjectBridgeheadUser pbu + JOIN pbu.projectBridgehead pb + JOIN pb.project p + JOIN p.query q + JOIN q.outputs qo + WHERE qo.projectType = :projectType + AND p.state = :projectState + AND pbu.projectRole <> :projectRole + """) + Set getByProjectTypeAndProjectStateAndNotProjectRole( + ProjectType projectType, + ProjectState projectState, + ProjectRole projectRole + ); @Query("SELECT DISTINCT pbu FROM ProjectBridgeheadUser pbu WHERE pbu.email LIKE %:email% AND pbu.projectBridgehead.bridgehead = :bridgehead AND (" + "(pbu.projectBridgehead.project.code = :projectCode AND pbu.projectBridgehead.project.state = 'DEVELOP' AND pbu.projectRole = 'DEVELOPER') OR " + @@ -46,7 +72,7 @@ public interface ProjectBridgeheadUserRepository extends JpaRepository getDistinctByEmailContainingAndProjectBridgehead_BridgeheadAndUserAlreadySetForThisProjectInThisRole(String email, String bridgehead, String projectCode); - List getDistinctByEmailContainingAndProjectBridgehead_Bridgehead(String email, String bridgehead); + List getDistinctByEmailContainingAndProjectBridgehead(String email, ProjectBridgehead projectBridgehead); @Query("SELECT pbu FROM ProjectBridgeheadUser pbu WHERE pbu.projectBridgehead = :projectBridgehead AND pbu.email = :email AND (" + "(pbu.projectBridgehead.project.state = 'DEVELOP' AND pbu.projectRole = 'DEVELOPER') OR " + @@ -64,18 +90,44 @@ public interface ProjectBridgeheadUserRepository extends JpaRepository getDistinctByProjectRoleAndProjectCode(ProjectRole projectRole, String projectCode); - @Query("SELECT DISTINCT pbu FROM ProjectBridgeheadUser pbu WHERE pbu.projectBridgehead.project.type = :projectType AND " + - "pbu.projectBridgehead.state = :projectBridgeheadState AND pbu.projectBridgehead.queryState = :queryState AND (" + - "(pbu.projectBridgehead.project.state = 'DEVELOP' AND pbu.projectRole = 'DEVELOPER') OR " + - "(pbu.projectBridgehead.project.state = 'PILOT' AND pbu.projectRole = 'PILOT') OR" + - "(pbu.projectBridgehead.project.state = 'FINAL' AND pbu.projectRole = 'FINAL'))") - List getDistinctInValidaProjectStateByProjectTypeAndQueryStateAndProjectBridgeheadState(ProjectType projectType, QueryState queryState, ProjectBridgeheadState projectBridgeheadState); - - @Query("SELECT DISTINCT pbu FROM ProjectBridgeheadUser pbu WHERE pbu.projectBridgehead.project.type = :projectType AND (" + - "(pbu.projectRole = 'DEVELOPER' AND pbu.projectBridgehead.project.state != 'DEVELOP') OR " + - "(pbu.projectRole = 'PILOT' AND pbu.projectBridgehead.project.state != 'PILOT') OR" + - "(pbu.projectRole = 'FINAL' AND pbu.projectBridgehead.project.state != 'FINAL'))") - List getDistinctInInvalidProjectStateByProjectType(ProjectType projectType); - + @Query(""" + SELECT DISTINCT pbu + FROM ProjectBridgeheadUser pbu + JOIN pbu.projectBridgehead pb + JOIN pb.project p + JOIN pb.executions exec + JOIN exec.queryOutput qo + WHERE qo.projectType = :projectType + AND pb.state = :projectBridgeheadState + AND exec.queryState = :queryState + AND ( + (p.state = 'DEVELOP' AND pbu.projectRole = 'DEVELOPER') OR + (p.state = 'PILOT' AND pbu.projectRole = 'PILOT') OR + (p.state = 'FINAL' AND pbu.projectRole = 'FINAL') + ) + """) + List + getDistinctInValidProjectStateByProjectTypeAndQueryStateAndProjectBridgeheadState( + ProjectType projectType, + QueryState queryState, + ProjectBridgeheadState projectBridgeheadState + ); + + @Query(""" + SELECT DISTINCT pbu + FROM ProjectBridgeheadUser pbu + JOIN pbu.projectBridgehead pb + JOIN pb.project p + JOIN p.query q + JOIN q.outputs qo + WHERE qo.projectType = :projectType + AND ( + (pbu.projectRole = 'DEVELOPER' AND p.state <> 'DEVELOP') OR + (pbu.projectRole = 'PILOT' AND p.state <> 'PILOT') OR + (pbu.projectRole = 'FINAL' AND p.state <> 'FINAL') + ) + """) + List + getDistinctInInvalidProjectStateByProjectType(ProjectType projectType); } diff --git a/src/main/java/de/samply/db/repository/ProjectFormFieldRepository.java b/src/main/java/de/samply/db/repository/ProjectFormFieldRepository.java new file mode 100644 index 00000000..ece84a9c --- /dev/null +++ b/src/main/java/de/samply/db/repository/ProjectFormFieldRepository.java @@ -0,0 +1,17 @@ +package de.samply.db.repository; + +import de.samply.db.model.Project; +import de.samply.db.model.ProjectFormField; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface ProjectFormFieldRepository extends JpaRepository { + + List findByProject(Project project); + + List findByProjectAndFormTitle(Project project, String formTitle); + +} diff --git a/src/main/java/de/samply/db/repository/ProjectFormRepository.java b/src/main/java/de/samply/db/repository/ProjectFormRepository.java new file mode 100644 index 00000000..07317be6 --- /dev/null +++ b/src/main/java/de/samply/db/repository/ProjectFormRepository.java @@ -0,0 +1,18 @@ +package de.samply.db.repository; + +import de.samply.db.model.Project; +import de.samply.db.model.ProjectForm; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface ProjectFormRepository extends JpaRepository { + + List findByProject(Project project); + + Optional findByProjectAndFormTitle(Project project, String formTitle); + +} diff --git a/src/main/java/de/samply/db/repository/ProjectRepository.java b/src/main/java/de/samply/db/repository/ProjectRepository.java index f840328f..ae3214e7 100644 --- a/src/main/java/de/samply/db/repository/ProjectRepository.java +++ b/src/main/java/de/samply/db/repository/ProjectRepository.java @@ -22,8 +22,7 @@ public interface ProjectRepository extends JpaRepository { @Query("SELECT p FROM Project p WHERE p.expiresAt < :expirationTime AND p.state IN :states") List findByExpiresAtBeforeAndStateIn(LocalDate expirationTime, Set states); - ////////// Project Manager Admins: - List findAll(); + /// /////// ProjectCode Manager Admins: @Query("SELECT p FROM Project p WHERE p.state != 'DRAFT' ORDER BY p.modifiedAt DESC") Page findAllByOrderByModifiedAtDesc(Pageable pageable); @@ -61,142 +60,478 @@ public interface ProjectRepository extends JpaRepository { Page findNotArchivedProjectsByStateModifiedAtAsc(ProjectState state, Pageable pageable); - ////////// Bridgehead Admins: - - @Query("SELECT DISTINCT p FROM Project p INNER JOIN ProjectBridgehead pb ON pb.project = p " + - "WHERE pb.bridgehead IN :bridgeheads ORDER BY p.modifiedAt DESC") - List findByBridgeheads(Set bridgeheads); - - @Query("SELECT DISTINCT p FROM Project p INNER JOIN ProjectBridgehead pb ON pb.project = p " + - "WHERE pb.bridgehead IN :bridgeheads ORDER BY p.modifiedAt DESC") - Page findByBridgeheadsModifiedAtDesc(Set bridgeheads, Pageable pageable); - - @Query("SELECT DISTINCT p FROM Project p INNER JOIN ProjectBridgehead pb ON pb.project = p " + - "WHERE pb.bridgehead IN :bridgeheads ORDER BY p.modifiedAt ASC") - Page findByBridgeheadsModifiedAtAsc(Set bridgeheads, Pageable pageable); - - @Query("SELECT DISTINCT p FROM Project p INNER JOIN ProjectBridgehead pb ON pb.project = p " + - "WHERE p.state = :state AND pb.bridgehead IN :bridgeheads ORDER BY p.modifiedAt DESC") - Page findByStateAndBridgeheadsModifiedAtDesc(ProjectState state, Set bridgeheads, Pageable pageable); - - @Query("SELECT DISTINCT p FROM Project p INNER JOIN ProjectBridgehead pb ON pb.project = p " + - "WHERE p.state = :state AND pb.bridgehead IN :bridgeheads ORDER BY p.modifiedAt ASC") - Page findByStateAndBridgeheadsModifiedAtAsc(ProjectState state, Set bridgeheads, Pageable pageable); - - @Query("SELECT DISTINCT p FROM Project p INNER JOIN ProjectBridgehead pb ON pb.project = p " + - "WHERE p.state = :state AND pb.bridgehead IN :bridgeheads AND p.archivedAt IS NULL ORDER BY p.modifiedAt DESC") - Page findNotArchivedProjectsByStateAndBridgeheadsModifiedAtDesc(ProjectState state, Set bridgeheads, Pageable pageable); - - @Query("SELECT DISTINCT p FROM Project p INNER JOIN ProjectBridgehead pb ON pb.project = p " + - "WHERE p.state = :state AND pb.bridgehead IN :bridgeheads AND p.archivedAt IS NULL ORDER BY p.modifiedAt ASC") - Page findNotArchivedProjectsByStateAndBridgeheadsModifiedAtAsc(ProjectState state, Set bridgeheads, Pageable pageable); - - @Query("SELECT DISTINCT p FROM Project p INNER JOIN ProjectBridgehead pb ON pb.project = p " + - "WHERE p.state = :state AND pb.bridgehead IN :bridgeheads AND p.archivedAt IS NOT NULL ORDER BY p.modifiedAt DESC") - Page findArchivedProjectsByStateAndBridgeheadsModifiedAtDesc(ProjectState state, Set bridgeheads, Pageable pageable); - - @Query("SELECT DISTINCT p FROM Project p INNER JOIN ProjectBridgehead pb ON pb.project = p " + - "WHERE p.state = :state AND pb.bridgehead IN :bridgeheads AND p.archivedAt IS NOT NULL ORDER BY p.modifiedAt ASC") - Page findArchivedProjectsByStateAndBridgeheadsModifiedAtAsc(ProjectState state, Set bridgeheads, Pageable pageable); - - @Query("SELECT DISTINCT p FROM Project p INNER JOIN ProjectBridgehead pb ON pb.project = p " + - "WHERE pb.bridgehead IN :bridgeheads AND p.archivedAt IS NULL ORDER BY p.modifiedAt DESC") - Page findNotArchivedProjectsByBridgeheadsModifiedAtDesc(Set bridgeheads, Pageable pageable); - - @Query("SELECT DISTINCT p FROM Project p INNER JOIN ProjectBridgehead pb ON pb.project = p " + - "WHERE pb.bridgehead IN :bridgeheads AND p.archivedAt IS NULL ORDER BY p.modifiedAt ASC") - Page findNotArchivedProjectsByBridgeheadsModifiedAtAsc(Set bridgeheads, Pageable pageable); - - @Query("SELECT DISTINCT p FROM Project p INNER JOIN ProjectBridgehead pb ON pb.project = p " + - "WHERE pb.bridgehead IN :bridgeheads AND p.archivedAt IS NOT NULL ORDER BY p.modifiedAt DESC") - Page findArchivedProjectsByBridgeheadsModifiedAtDesc(Set bridgeheads, Pageable pageable); - - @Query("SELECT DISTINCT p FROM Project p INNER JOIN ProjectBridgehead pb ON pb.project = p " + - "WHERE pb.bridgehead IN :bridgeheads AND p.archivedAt IS NOT NULL ORDER BY p.modifiedAt ASC") - Page findArchivedProjectsByBridgeheadsModifiedAtAsc(Set bridgeheads, Pageable pageable); - - - ////////// Researchers: - @Query("SELECT DISTINCT p FROM Project p INNER JOIN ProjectBridgehead pb ON pb.project = p " + - "LEFT JOIN ProjectBridgeheadUser pbu ON pbu.projectBridgehead = pb " + - "WHERE pb.bridgehead IN :bridgeheads " + - "AND (pbu.email = :email OR p.creatorEmail = :email) ORDER BY p.modifiedAt DESC") - Page findByEmailAndBridgeheadsModifiedAtDesc(String email, Set bridgeheads, Pageable pageable); - - @Query("SELECT DISTINCT p FROM Project p INNER JOIN ProjectBridgehead pb ON pb.project = p " + - "LEFT JOIN ProjectBridgeheadUser pbu ON pbu.projectBridgehead = pb " + - "WHERE pb.bridgehead IN :bridgeheads " + - "AND (pbu.email = :email OR p.creatorEmail = :email) ORDER BY p.modifiedAt ASC") - Page findByEmailAndBridgeheadsModifiedAtAsc(String email, Set bridgeheads, Pageable pageable); - - @Query("SELECT DISTINCT p FROM Project p INNER JOIN ProjectBridgehead pb ON pb.project = p " + - "LEFT JOIN ProjectBridgeheadUser pbu ON pbu.projectBridgehead = pb " + - "WHERE p.state = :state AND pb.bridgehead IN :bridgeheads " + - "AND (pbu.email = :email OR p.creatorEmail = :email) ORDER BY p.modifiedAt DESC") - Page findByEmailAndStateAndBridgeheadsModifiedAtDesc( + /// /////// Bridgehead Admins: + + @Query(""" + SELECT p + FROM Project p + WHERE + p.creatorEmail = :email + OR EXISTS ( + SELECT 1 + FROM ProjectBridgehead pb + WHERE pb.project = p + AND pb.bridgehead IN :bridgeheads + ) + ORDER BY p.modifiedAt DESC + """) + List findByBridgeheadsOrCreator(String email, Set bridgeheads); + + + @Query(""" + SELECT p + FROM Project p + WHERE + p.creatorEmail = :email + OR EXISTS ( + SELECT 1 + FROM ProjectBridgehead pb + WHERE pb.project = p + AND pb.bridgehead IN :bridgeheads + ) + ORDER BY p.modifiedAt DESC + """) + Page findByBridgeheadsOrCreatorModifiedAtDesc(String email, Set bridgeheads, Pageable pageable); + + @Query(""" + SELECT p + FROM Project p + WHERE + p.creatorEmail = :email + OR EXISTS ( + SELECT 1 + FROM ProjectBridgehead pb + WHERE pb.project = p + AND pb.bridgehead IN :bridgeheads + ) + ORDER BY p.modifiedAt ASC + """) + Page findByBridgeheadsOrCreatorModifiedAtAsc(String email, Set bridgeheads, Pageable pageable); + + @Query(""" + SELECT p + FROM Project p + WHERE p.state = :state + AND ( + p.creatorEmail = :email + OR EXISTS ( + SELECT 1 + FROM ProjectBridgehead pb + WHERE pb.project = p + AND pb.bridgehead IN :bridgeheads + ) + ) + ORDER BY p.modifiedAt DESC + """) + Page findByStateAndBridgeheadsOrCreatorModifiedAtDesc(String email, ProjectState state, Set bridgeheads, Pageable pageable); + + @Query(""" + SELECT p + FROM Project p + WHERE p.state = :state + AND ( + p.creatorEmail = :email + OR EXISTS ( + SELECT 1 + FROM ProjectBridgehead pb + WHERE pb.project = p + AND pb.bridgehead IN :bridgeheads + ) + ) + ORDER BY p.modifiedAt ASC + """) + Page findByStateAndBridgeheadsOrCreatorModifiedAtAsc(String email, ProjectState state, Set bridgeheads, Pageable pageable); + + @Query(""" + SELECT p + FROM Project p + WHERE p.state = :state + AND p.archivedAt IS NULL + AND ( + p.creatorEmail = :email + OR EXISTS ( + SELECT 1 + FROM ProjectBridgehead pb + WHERE pb.project = p + AND pb.bridgehead IN :bridgeheads + ) + ) + ORDER BY p.modifiedAt DESC + """) + Page findNotArchivedProjectsByStateAndBridgeheadsOrCreatorModifiedAtDesc(String email, ProjectState state, Set bridgeheads, Pageable pageable); + + @Query(""" + SELECT p + FROM Project p + WHERE p.state = :state + AND p.archivedAt IS NULL + AND ( + p.creatorEmail = :email + OR EXISTS ( + SELECT 1 + FROM ProjectBridgehead pb + WHERE pb.project = p + AND pb.bridgehead IN :bridgeheads + ) + ) + ORDER BY p.modifiedAt ASC + """) + Page findNotArchivedProjectsByStateAndBridgeheadsOrCreatorModifiedAtAsc(String email, ProjectState state, Set bridgeheads, Pageable pageable); + + @Query(""" + SELECT p + FROM Project p + WHERE p.state = :state + AND p.archivedAt IS NOT NULL + AND ( + p.creatorEmail = :email + OR EXISTS ( + SELECT 1 + FROM ProjectBridgehead pb + WHERE pb.project = p + AND pb.bridgehead IN :bridgeheads + ) + ) + ORDER BY p.modifiedAt DESC + """) + Page findArchivedProjectsByStateAndBridgeheadsOrCreatorModifiedAtDesc(String email, ProjectState state, Set bridgeheads, Pageable pageable); + + @Query(""" + SELECT p + FROM Project p + WHERE p.state = :state + AND p.archivedAt IS NOT NULL + AND ( + p.creatorEmail = :email + OR EXISTS ( + SELECT 1 + FROM ProjectBridgehead pb + WHERE pb.project = p + AND pb.bridgehead IN :bridgeheads + ) + ) + ORDER BY p.modifiedAt ASC + """) + Page findArchivedProjectsByStateAndBridgeheadsOrCreatorModifiedAtAsc(String email, ProjectState state, Set bridgeheads, Pageable pageable); + + @Query(""" + SELECT p + FROM Project p + WHERE p.archivedAt IS NULL + AND ( + p.creatorEmail = :email + OR EXISTS ( + SELECT 1 + FROM ProjectBridgehead pb + WHERE pb.project = p + AND pb.bridgehead IN :bridgeheads + ) + ) + ORDER BY p.modifiedAt DESC + """) + Page findNotArchivedProjectsByBridgeheadsOrCreatorModifiedAtDesc(String email, Set bridgeheads, Pageable pageable); + + @Query(""" + SELECT p + FROM Project p + WHERE p.archivedAt IS NULL + AND ( + p.creatorEmail = :email + OR EXISTS ( + SELECT 1 + FROM ProjectBridgehead pb + WHERE pb.project = p + AND pb.bridgehead IN :bridgeheads + ) + ) + ORDER BY p.modifiedAt ASC + """) + Page findNotArchivedProjectsByBridgeheadsOrCreatorModifiedAtAsc(String email, Set bridgeheads, Pageable pageable); + + @Query(""" + SELECT p + FROM Project p + WHERE p.archivedAt IS NOT NULL + AND ( + p.creatorEmail = :email + OR EXISTS ( + SELECT 1 + FROM ProjectBridgehead pb + WHERE pb.project = p + AND pb.bridgehead IN :bridgeheads + ) + ) + ORDER BY p.modifiedAt DESC + """) + Page findArchivedProjectsByBridgeheadsOrCreatorModifiedAtDesc(String email, Set bridgeheads, Pageable pageable); + + @Query(""" + SELECT p + FROM Project p + WHERE p.archivedAt IS NOT NULL + AND ( + p.creatorEmail = :email + OR EXISTS ( + SELECT 1 + FROM ProjectBridgehead pb + WHERE pb.project = p + AND pb.bridgehead IN :bridgeheads + ) + ) + ORDER BY p.modifiedAt ASC + """) + Page findArchivedProjectsByBridgeheadsOrCreatorModifiedAtAsc(String email, Set bridgeheads, Pageable pageable); + + + /// /////// Researchers: + @Query(""" + SELECT p + FROM Project p + WHERE + p.creatorEmail = :email + OR EXISTS ( + SELECT 1 + FROM ProjectBridgehead pb + JOIN ProjectBridgeheadUser pbu + ON pbu.projectBridgehead = pb + WHERE pb.project = p + AND pb.bridgehead IN :bridgeheads + AND pbu.email = :email + ) + ORDER BY p.modifiedAt DESC + """) + Page findByEmailAndBridgeheadsOrCreatorModifiedAtDesc(String email, Set bridgeheads, Pageable pageable); + + @Query(""" + SELECT p + FROM Project p + WHERE + p.creatorEmail = :email + OR EXISTS ( + SELECT 1 + FROM ProjectBridgehead pb + JOIN ProjectBridgeheadUser pbu + ON pbu.projectBridgehead = pb + WHERE pb.project = p + AND pb.bridgehead IN :bridgeheads + AND pbu.email = :email + ) + ORDER BY p.modifiedAt ASC + """) + Page findByEmailAndBridgeheadsOrCreatorModifiedAtAsc(String email, Set bridgeheads, Pageable pageable); + + @Query(""" + SELECT p + FROM Project p + WHERE p.state = :state + AND ( + p.creatorEmail = :email + OR EXISTS ( + SELECT 1 + FROM ProjectBridgehead pb + JOIN ProjectBridgeheadUser pbu + ON pbu.projectBridgehead = pb + WHERE pb.project = p + AND pb.bridgehead IN :bridgeheads + AND pbu.email = :email + ) + ) + ORDER BY p.modifiedAt DESC + """) + Page findByEmailAndStateAndBridgeheadsOrCreatorModifiedAtDesc( String email, ProjectState state, Set bridgeheads, Pageable pageable); - @Query("SELECT DISTINCT p FROM Project p INNER JOIN ProjectBridgehead pb ON pb.project = p " + - "LEFT JOIN ProjectBridgeheadUser pbu ON pbu.projectBridgehead = pb " + - "WHERE p.state = :state AND pb.bridgehead IN :bridgeheads " + - "AND (pbu.email = :email OR p.creatorEmail = :email) ORDER BY p.modifiedAt ASC") - Page findByEmailAndStateAndBridgeheadsModifiedAtAsc( + @Query(""" + SELECT p + FROM Project p + WHERE p.state = :state + AND ( + p.creatorEmail = :email + OR EXISTS ( + SELECT 1 + FROM ProjectBridgehead pb + JOIN ProjectBridgeheadUser pbu + ON pbu.projectBridgehead = pb + WHERE pb.project = p + AND pb.bridgehead IN :bridgeheads + AND pbu.email = :email + ) + ) + ORDER BY p.modifiedAt ASC + """) + Page findByEmailAndStateAndBridgeheadsOrCreatorModifiedAtAsc( String email, ProjectState state, Set bridgeheads, Pageable pageable); - @Query("SELECT DISTINCT p FROM Project p INNER JOIN ProjectBridgehead pb ON pb.project = p " + - "LEFT JOIN ProjectBridgeheadUser pbu ON pbu.projectBridgehead = pb " + - "WHERE (pbu.email = :email OR p.creatorEmail = :email) AND p.state = :state " + - "AND pb.bridgehead IN :bridgeheads AND p.archivedAt IS NULL ORDER BY p.modifiedAt DESC") - Page findNotArchivedProjectsByEmailAndStateAndBridgeheadsModifiedAtDesc( + @Query(""" + SELECT p + FROM Project p + WHERE p.state = :state + AND p.archivedAt IS NULL + AND ( + p.creatorEmail = :email + OR EXISTS ( + SELECT 1 + FROM ProjectBridgehead pb + JOIN ProjectBridgeheadUser pbu + ON pbu.projectBridgehead = pb + WHERE pb.project = p + AND pb.bridgehead IN :bridgeheads + AND pbu.email = :email + ) + ) + ORDER BY p.modifiedAt DESC + """) + Page findNotArchivedProjectsByEmailAndStateAndBridgeheadsOrCreatorModifiedAtDesc( String email, ProjectState state, Set bridgeheads, Pageable pageable); - @Query("SELECT DISTINCT p FROM Project p INNER JOIN ProjectBridgehead pb ON pb.project = p " + - "LEFT JOIN ProjectBridgeheadUser pbu ON pbu.projectBridgehead = pb " + - "WHERE (pbu.email = :email OR p.creatorEmail = :email) AND p.state = :state " + - "AND pb.bridgehead IN :bridgeheads AND p.archivedAt IS NULL ORDER BY p.modifiedAt ASC") - Page findNotArchivedProjectsByEmailAndStateAndBridgeheadsModifiedAtAsc( + @Query(""" + SELECT p + FROM Project p + WHERE p.state = :state + AND p.archivedAt IS NULL + AND ( + p.creatorEmail = :email + OR EXISTS ( + SELECT 1 + FROM ProjectBridgehead pb + JOIN ProjectBridgeheadUser pbu + ON pbu.projectBridgehead = pb + WHERE pb.project = p + AND pb.bridgehead IN :bridgeheads + AND pbu.email = :email + ) + ) + ORDER BY p.modifiedAt ASC + """) + Page findNotArchivedProjectsByEmailAndStateAndBridgeheadsOrCreatorModifiedAtAsc( String email, ProjectState state, Set bridgeheads, Pageable pageable); - @Query("SELECT DISTINCT p FROM Project p INNER JOIN ProjectBridgehead pb ON pb.project = p " + - "LEFT JOIN ProjectBridgeheadUser pbu ON pbu.projectBridgehead = pb " + - "WHERE (pbu.email = :email OR p.creatorEmail = :email) AND p.state = :state " + - "AND pb.bridgehead IN :bridgeheads AND p.archivedAt IS NOT NULL ORDER BY p.modifiedAt DESC") - Page findArchivedProjectsByEmailAndStateAndBridgeheadsModifiedAtDesc( + @Query(""" + SELECT p + FROM Project p + WHERE p.state = :state + AND p.archivedAt IS NOT NULL + AND ( + p.creatorEmail = :email + OR EXISTS ( + SELECT 1 + FROM ProjectBridgehead pb + JOIN ProjectBridgeheadUser pbu + ON pbu.projectBridgehead = pb + WHERE pb.project = p + AND pb.bridgehead IN :bridgeheads + AND pbu.email = :email + ) + ) + ORDER BY p.modifiedAt DESC + """) + Page findArchivedProjectsByEmailAndStateAndBridgeheadsOrCreatorModifiedAtDesc( String email, ProjectState state, Set bridgeheads, Pageable pageable); - @Query("SELECT DISTINCT p FROM Project p INNER JOIN ProjectBridgehead pb ON pb.project = p " + - "LEFT JOIN ProjectBridgeheadUser pbu ON pbu.projectBridgehead = pb " + - "WHERE (pbu.email = :email OR p.creatorEmail = :email) AND p.state = :state " + - "AND pb.bridgehead IN :bridgeheads AND p.archivedAt IS NOT NULL ORDER BY p.modifiedAt ASC") - Page findArchivedProjectsByEmailAndStateAndBridgeheadsModifiedAtAsc( + @Query(""" + SELECT p + FROM Project p + WHERE p.state = :state + AND p.archivedAt IS NOT NULL + AND ( + p.creatorEmail = :email + OR EXISTS ( + SELECT 1 + FROM ProjectBridgehead pb + JOIN ProjectBridgeheadUser pbu + ON pbu.projectBridgehead = pb + WHERE pb.project = p + AND pb.bridgehead IN :bridgeheads + AND pbu.email = :email + ) + ) + ORDER BY p.modifiedAt ASC + """) + Page findArchivedProjectsByEmailAndStateAndBridgeheadsOrCreatorModifiedAtAsc( String email, ProjectState state, Set bridgeheads, Pageable pageable); - @Query("SELECT DISTINCT p FROM Project p INNER JOIN ProjectBridgehead pb ON pb.project = p " + - "LEFT JOIN ProjectBridgeheadUser pbu ON pbu.projectBridgehead = pb " + - "WHERE (pbu.email = :email OR p.creatorEmail = :email) AND pb.bridgehead IN :bridgeheads " + - "AND p.archivedAt IS NULL ORDER BY p.modifiedAt DESC") - Page findNotArchivedProjectsByEmailAndBridgeheadsModifiedAtDesc( + @Query(""" + SELECT p + FROM Project p + WHERE p.archivedAt IS NULL + AND ( + p.creatorEmail = :email + OR EXISTS ( + SELECT 1 + FROM ProjectBridgehead pb + JOIN ProjectBridgeheadUser pbu + ON pbu.projectBridgehead = pb + WHERE pb.project = p + AND pb.bridgehead IN :bridgeheads + AND pbu.email = :email + ) + ) + ORDER BY p.modifiedAt DESC + """) + Page findNotArchivedProjectsByEmailAndBridgeheadsOrCreatorModifiedAtDesc( String email, Set bridgeheads, Pageable pageable); - @Query("SELECT DISTINCT p FROM Project p INNER JOIN ProjectBridgehead pb ON pb.project = p " + - "LEFT JOIN ProjectBridgeheadUser pbu ON pbu.projectBridgehead = pb " + - "WHERE (pbu.email = :email OR p.creatorEmail = :email) AND pb.bridgehead IN :bridgeheads " + - "AND p.archivedAt IS NULL ORDER BY p.modifiedAt ASC") - Page findNotArchivedProjectsByEmailAndBridgeheadsModifiedAtAsc( + @Query(""" + SELECT p + FROM Project p + WHERE p.archivedAt IS NULL + AND ( + p.creatorEmail = :email + OR EXISTS ( + SELECT 1 + FROM ProjectBridgehead pb + JOIN ProjectBridgeheadUser pbu + ON pbu.projectBridgehead = pb + WHERE pb.project = p + AND pb.bridgehead IN :bridgeheads + AND pbu.email = :email + ) + ) + ORDER BY p.modifiedAt ASC + """) + Page findNotArchivedProjectsByEmailAndBridgeheadsOrCreatorModifiedAtAsc( String email, Set bridgeheads, Pageable pageable); - @Query("SELECT DISTINCT p FROM Project p INNER JOIN ProjectBridgehead pb ON pb.project = p " + - "LEFT JOIN ProjectBridgeheadUser pbu ON pbu.projectBridgehead = pb " + - "WHERE (pbu.email = :email OR p.creatorEmail = :email) AND pb.bridgehead IN :bridgeheads " + - "AND p.archivedAt IS NOT NULL ORDER BY p.modifiedAt DESC") - Page findArchivedProjectsByEmailAndBridgeheadsModifiedAtDesc( + @Query(""" + SELECT p + FROM Project p + WHERE p.archivedAt IS NOT NULL + AND ( + p.creatorEmail = :email + OR EXISTS ( + SELECT 1 + FROM ProjectBridgehead pb + JOIN ProjectBridgeheadUser pbu + ON pbu.projectBridgehead = pb + WHERE pb.project = p + AND pb.bridgehead IN :bridgeheads + AND pbu.email = :email + ) + ) + ORDER BY p.modifiedAt DESC + """) + Page findArchivedProjectsByEmailAndBridgeheadsOrCreatorModifiedAtDesc( String email, Set bridgeheads, Pageable pageable); - @Query("SELECT DISTINCT p FROM Project p INNER JOIN ProjectBridgehead pb ON pb.project = p " + - "LEFT JOIN ProjectBridgeheadUser pbu ON pbu.projectBridgehead = pb " + - "WHERE (pbu.email = :email OR p.creatorEmail = :email) AND pb.bridgehead IN :bridgeheads " + - "AND p.archivedAt IS NOT NULL ORDER BY p.modifiedAt ASC") - Page findArchivedProjectsByEmailAndBridgeheadsModifiedAtAsc( + @Query(""" + SELECT p + FROM Project p + WHERE p.archivedAt IS NOT NULL + AND ( + p.creatorEmail = :email + OR EXISTS ( + SELECT 1 + FROM ProjectBridgehead pb + JOIN ProjectBridgeheadUser pbu + ON pbu.projectBridgehead = pb + WHERE pb.project = p + AND pb.bridgehead IN :bridgeheads + AND pbu.email = :email + ) + ) + ORDER BY p.modifiedAt ASC + """) + Page findArchivedProjectsByEmailAndBridgeheadsOrCreatorModifiedAtAsc( String email, Set bridgeheads, Pageable pageable); } diff --git a/src/main/java/de/samply/db/utils/QueryMapper.java b/src/main/java/de/samply/db/utils/QueryMapper.java new file mode 100644 index 00000000..04d9ea02 --- /dev/null +++ b/src/main/java/de/samply/db/utils/QueryMapper.java @@ -0,0 +1,15 @@ +package de.samply.db.utils; + +import de.samply.db.model.Query; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingConstants; +import org.mapstruct.MappingTarget; + +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING) +public interface QueryMapper { + + @Mapping(target = "outputs", ignore = true) + void updateQuery(Query source, @MappingTarget Query target); + +} diff --git a/src/main/java/de/samply/db/utils/QueryOutputMapper.java b/src/main/java/de/samply/db/utils/QueryOutputMapper.java new file mode 100644 index 00000000..c77af1b5 --- /dev/null +++ b/src/main/java/de/samply/db/utils/QueryOutputMapper.java @@ -0,0 +1,15 @@ +package de.samply.db.utils; + +import de.samply.db.model.QueryOutput; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingConstants; + +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING) +public interface QueryOutputMapper { + + @Mapping(target = "id", ignore = true) + @Mapping(target = "query", ignore = true) + QueryOutput toEntity(QueryOutput source); + +} diff --git a/src/main/java/de/samply/document/DocumentService.java b/src/main/java/de/samply/document/DocumentService.java index c6ac6f6f..c9cba90a 100644 --- a/src/main/java/de/samply/document/DocumentService.java +++ b/src/main/java/de/samply/document/DocumentService.java @@ -2,10 +2,9 @@ import de.samply.app.ProjectManagerConst; import de.samply.db.model.Project; +import de.samply.db.model.ProjectBridgehead; import de.samply.db.model.ProjectDocument; import de.samply.db.repository.ProjectDocumentRepository; -import de.samply.db.repository.ProjectRepository; -import de.samply.frontend.dto.DtoFactory; import de.samply.notification.NotificationService; import de.samply.notification.OperationType; import de.samply.security.SessionUser; @@ -20,8 +19,8 @@ import java.time.Instant; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; -import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.UUID; @@ -31,35 +30,23 @@ public class DocumentService { private final NotificationService notificationService; private final ProjectDocumentRepository projectDocumentRepository; - private final ProjectRepository projectRepository; private final Path documentsDirectory; - private final Path publicDocumentsDirectory; - private final String applicationFormFile; private final String timestampFormat; private final SessionUser sessionUser; - private final DtoFactory dtoFactory; public DocumentService(NotificationService notificationService, ProjectDocumentRepository projectDocumentRepository, - ProjectRepository projectRepository, @Value(ProjectManagerConst.PROJECT_DOCUMENTS_DIRECTORY_SV) String documentsDirectory, - @Value(ProjectManagerConst.PUBLIC_DOCUMENTS_DIRECTORY_SV) String publicDocumentsDirectory, - @Value(ProjectManagerConst.APPLICATION_FORM_FILENAME_SV) String applicationFormFile, @Value(ProjectManagerConst.PROJECT_DOCUMENTS_DIRECTORY_TIMESTAMP_FORMAT_SV) String timestampFormat, - SessionUser sessionUser, - DtoFactory dtoFactory) throws IOException { + SessionUser sessionUser) throws IOException { this.notificationService = notificationService; this.projectDocumentRepository = projectDocumentRepository; - this.projectRepository = projectRepository; this.documentsDirectory = fetchPathDirectory(documentsDirectory); - this.publicDocumentsDirectory = Path.of(publicDocumentsDirectory); - this.applicationFormFile = applicationFormFile; this.timestampFormat = timestampFormat; this.sessionUser = sessionUser; - this.dtoFactory = dtoFactory; } - public void uploadDocument(String projectCode, Optional bridgeheadOptional, MultipartFile document, DocumentType documentType, Optional labelOptional) throws DocumentServiceException { + public void uploadDocument(Project projectCode, Optional bridgeheadOptional, MultipartFile document, DocumentType documentType, Optional labelOptional) throws DocumentServiceException { String bridgehead = fetchBridgeheadForSearch(bridgeheadOptional); FunctionWithException> documentInitializer = project -> { String originalFilename = document.getOriginalFilename(); @@ -74,55 +61,47 @@ public void uploadDocument(String projectCode, Optional bridgeheadOption return Optional.empty(); }; ConsumerWithException documentSetter = projectDocument -> { - projectDocument.setOriginalFilename(document.getOriginalFilename().trim()); + projectDocument.setOriginalFilename(Objects.requireNonNull(document.getOriginalFilename()).trim()); Path documentPath = writeDocumentInDirectory(document); projectDocument.setFilePath(documentPath.toAbsolutePath().toString()); }; addDocument(projectCode, bridgehead, documentType, labelOptional, documentInitializer, documentSetter); } - public void addDocumentUrl(String projectCode, Optional bridgeheadOptional, String url, DocumentType documentType, Optional labelOptional) throws DocumentServiceException { + public void addDocumentUrl(Project project, Optional bridgeheadOptional, String url, DocumentType documentType, Optional labelOptional) throws DocumentServiceException { String bridgehead = fetchBridgeheadForSearch(bridgeheadOptional); - FunctionWithException> documentInitializer = project -> { - return this.projectDocumentRepository.findFirstByProjectAndBridgeheadAndOriginalFilename(project, bridgehead, url); - }; - ConsumerWithException documentSetter = projectDocument -> { - projectDocument.setUrl(url); - }; - addDocument(projectCode, bridgehead, documentType, labelOptional, documentInitializer, documentSetter); + FunctionWithException> documentInitializer = testProject -> this.projectDocumentRepository.findFirstByProjectAndBridgeheadAndOriginalFilename(testProject, bridgehead, url); + ConsumerWithException documentSetter = projectDocument -> projectDocument.setUrl(url); + addDocument(project, bridgehead, documentType, labelOptional, documentInitializer, documentSetter); } - private String fetchBridgeheadForSearch(Optional bridgehead) { - return (bridgehead.isPresent()) ? bridgehead.get() : ProjectManagerConst.NO_BRIDGEHEAD; + private String fetchBridgeheadForSearch(Optional bridgehead) { + return bridgehead.map(ProjectBridgehead::getBridgehead).orElse(ProjectManagerConst.NO_BRIDGEHEAD); } - private void addDocument(String projectCode, + private void addDocument(Project project, String bridgehead, DocumentType documentType, Optional labelOptional, FunctionWithException> documentInitializer, ConsumerWithException documentSetter) throws DocumentServiceException { - Optional project = projectRepository.findByCode(projectCode); - if (project.isEmpty()) { - throw new DocumentServiceException("Project not found"); - } ProjectDocument projectDocument; - Optional projectDocumentOptional = documentInitializer.apply(project.get()); + Optional projectDocumentOptional = documentInitializer.apply(project); if (projectDocumentOptional.isPresent()) { projectDocument = projectDocumentOptional.get(); } else { projectDocument = new ProjectDocument(); - projectDocument.setProject(project.get()); + projectDocument.setProject(project); projectDocument.setBridgehead(bridgehead); } - labelOptional.ifPresent(label -> projectDocument.setLabel(label)); + labelOptional.ifPresent(projectDocument::setLabel); projectDocument.setDocumentType(documentType); projectDocument.setCreatedAt(Instant.now()); projectDocument.setCreatorEmail(sessionUser.getEmail()); documentSetter.accept(projectDocument); this.projectDocumentRepository.save(projectDocument); - this.notificationService.createNotification(projectCode, bridgehead, sessionUser.getEmail(), OperationType.ADD_DOCUMENT, + this.notificationService.createNotification(project, bridgehead, sessionUser.getEmail(), OperationType.ADD_DOCUMENT, "Add document of type " + documentType + ": " + projectDocument.getOriginalFilename(), null, null); } @@ -175,56 +154,19 @@ private void deleteFileWithoutExceptionHandling(ProjectDocument projectDocument) } } - public Optional fetchProjectDocument(String projectCode, Optional bridgeheadOptional, String filename) { - Optional project = projectRepository.findByCode(projectCode); - if (project.isEmpty()) { - return Optional.empty(); - } + public Optional fetchProjectDocument(Project project, Optional bridgeheadOptional, String filename) { String bridgehead = fetchBridgeheadForSearch(bridgeheadOptional); - Optional projectDocument = projectDocumentRepository.findFirstByProjectAndBridgeheadAndOriginalFilename(project.get(), bridgehead, filename); - if (projectDocument.isEmpty()) { - projectDocument = projectDocumentRepository.findFirstByProjectAndOriginalFilename(project.get(), filename); - } + Optional projectDocument = projectDocumentRepository.findFirstByProjectAndBridgeheadAndOriginalFilename(project, bridgehead, filename); if (projectDocument.isEmpty()) { - return Optional.empty(); + projectDocument = projectDocumentRepository.findFirstByProjectAndOriginalFilename(project, filename); } return projectDocument; } - public Optional fetchPublicDocument(String documentFilename) { - if (documentFilename == null) { - return Optional.empty(); - } - Path path = publicDocumentsDirectory.resolve(documentFilename); - return (Files.exists(path)) ? Optional.of(path) : Optional.empty(); - } - - public Optional fetchApplicationForm() { - return fetchPublicDocument(applicationFormFile); - } - - public List fetchPublications(String projectCode) { - return convertToDto(fetchDocuments(projectCode, Optional.empty(), DocumentType.PUBLICATION)); - - } - - private List convertToDto(List projectDocumentList) { - return projectDocumentList.stream().map(dtoFactory::convert).toList(); - } - - public List fetchOtherDocuments(String projectCode, Optional bridgehead) { - return convertToDto(fetchDocuments(projectCode, bridgehead, DocumentType.OTHERS)); - } - - - private List fetchDocuments(String projectCode, Optional bridgehead, DocumentType documentType) { - Optional project = projectRepository.findByCode(projectCode); - if (project.isEmpty()) { - return new ArrayList<>(); - } + public List fetchDocuments(Project project, Optional bridgehead, DocumentType documentType) { return (bridgehead.isPresent()) ? - projectDocumentRepository.findAllByBridgeheadAndProjectAndDocumentTypeOrderByLabelAsc(bridgehead.get(), project.get(), documentType) : - projectDocumentRepository.findAllByProjectAndDocumentTypeOrderByLabelAsc(project.get(), documentType); + projectDocumentRepository.findAllByBridgeheadAndProjectAndDocumentTypeOrderByLabelAsc(bridgehead.get().getBridgehead(), project, documentType) : + projectDocumentRepository.findAllByProjectAndDocumentTypeOrderByLabelAsc(project, documentType); } @@ -236,19 +178,13 @@ private interface FunctionWithException { R apply(T t) throws DocumentServiceException; } - public Optional fetchLastDocumentOfThisTypeForFrontend(String projectCode, Optional bridgeheadOptional, DocumentType type) { - Optional projectDocument = fetchLastDocumentOfThisType(projectCode, bridgeheadOptional, type); - return (projectDocument.isEmpty()) ? Optional.empty() : Optional.of(dtoFactory.convert(projectDocument.get())); - + public Optional fetchLastDocumentOfThisType(Project project, Optional bridgeheadOptional, DocumentType type) { + return projectDocumentRepository.findFirstByProjectAndDocumentTypeAndBridgeheadOrderByCreatedAtDesc( + project, type, fetchBridgeheadForSearch(bridgeheadOptional)); } - public Optional fetchLastDocumentOfThisType(String projectCode, Optional bridgeheadOptional, DocumentType type) { - Optional projectOptional = projectRepository.findByCode(projectCode); - if (projectOptional.isPresent()) { - return projectDocumentRepository.findFirstByProjectAndDocumentTypeAndBridgeheadOrderByCreatedAtDesc( - projectOptional.get(), type, fetchBridgeheadForSearch(bridgeheadOptional)); - } - return Optional.empty(); + public Optional fetchDocumentOrderByCreatedAtDesc(Project project) { + return projectDocumentRepository.findTopByProjectOrderByCreatedAtDesc(project); } } diff --git a/src/main/java/de/samply/document/DocumentServiceException.java b/src/main/java/de/samply/document/DocumentServiceException.java index 9fb7ee15..157cf8a7 100644 --- a/src/main/java/de/samply/document/DocumentServiceException.java +++ b/src/main/java/de/samply/document/DocumentServiceException.java @@ -2,23 +2,12 @@ public class DocumentServiceException extends Exception { - public DocumentServiceException() { - } - public DocumentServiceException(String message) { super(message); } - public DocumentServiceException(String message, Throwable cause) { - super(message, cause); - } - public DocumentServiceException(Throwable cause) { super(cause); } - public DocumentServiceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } - } diff --git a/src/main/java/de/samply/document/DocumentType.java b/src/main/java/de/samply/document/DocumentType.java index bc65780d..6f1d90e3 100644 --- a/src/main/java/de/samply/document/DocumentType.java +++ b/src/main/java/de/samply/document/DocumentType.java @@ -1,9 +1,10 @@ package de.samply.document; public enum DocumentType { - APPLICATION_FORM, VOTUM, SCRIPT, PUBLICATION, + DESCRIPTION, + FINAL_REPORT, OTHERS } diff --git a/src/main/java/de/samply/document/DtoDocumentService.java b/src/main/java/de/samply/document/DtoDocumentService.java new file mode 100644 index 00000000..f3784cf9 --- /dev/null +++ b/src/main/java/de/samply/document/DtoDocumentService.java @@ -0,0 +1,46 @@ +package de.samply.document; + +import de.samply.db.model.Project; +import de.samply.db.model.ProjectBridgehead; +import de.samply.frontend.dto.DtoFactory; +import de.samply.frontend.dto.ProjectDocument; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +public class DtoDocumentService { + + private final DocumentService documentService; + private final DtoFactory dtoFactory; + + public DtoDocumentService(DocumentService documentService, DtoFactory dtoFactory) { + this.documentService = documentService; + this.dtoFactory = dtoFactory; + } + + public List fetchPublications(Project project) { + return convertToDto(documentService.fetchDocuments(project, Optional.empty(), DocumentType.PUBLICATION)); + } + + public List fetchFinalReports(Project project) { + return convertToDto(documentService.fetchDocuments(project, Optional.empty(), DocumentType.FINAL_REPORT)); + } + + public List fetchOtherDocuments(Project project, Optional bridgehead) { + return convertToDto(documentService.fetchDocuments(project, bridgehead, DocumentType.OTHERS)); + } + + public Optional fetchLastDocumentOfThisTypeForFrontend(Project project, Optional bridgeheadOptional, DocumentType type) { + return documentService + .fetchLastDocumentOfThisType(project, bridgeheadOptional, type) + .map(dtoFactory::convert); + } + + private List convertToDto(List projectDocumentList) { + return projectDocumentList.stream().map(dtoFactory::convert).toList(); + } + + +} diff --git a/src/main/java/de/samply/email/EmailContext.java b/src/main/java/de/samply/email/EmailContext.java index 1c406c05..4dedc8e6 100644 --- a/src/main/java/de/samply/email/EmailContext.java +++ b/src/main/java/de/samply/email/EmailContext.java @@ -25,14 +25,14 @@ public class EmailContext { // If a key is Base64-encoded, it should start with the prefix "B64" to indicate its encoding. @PostConstruct public void init() { - log.debug("Email Context initialized: ({})", context.entrySet().size()); + log.debug("Email Context initialized: ({})", context.size()); Map tempContext = new HashMap<>(); - context.entrySet().stream().forEach(keyValue -> { - String key = replaceHyphen(keyValue.getKey()); - AtomicReference value = new AtomicReference<>(keyValue.getValue()); + context.forEach((key1, value1) -> { + String key = replaceHyphen(key1); + AtomicReference value = new AtomicReference<>(value1); if (key.startsWith(ProjectManagerConst.BASE_64)) { key = key.replaceFirst(ProjectManagerConst.BASE_64, ""); - Base64Utils.decodeIfNecessary(keyValue.getValue()).ifPresent(value::set); + Base64Utils.decodeIfNecessary(value1).ifPresent(value::set); } log.debug("\t-\t{}: {}", key, value.get()); tempContext.put(key, value.get()); diff --git a/src/main/java/de/samply/email/EmailContextKey.java b/src/main/java/de/samply/email/EmailContextKey.java index 2efcfe1a..59afde5a 100644 --- a/src/main/java/de/samply/email/EmailContextKey.java +++ b/src/main/java/de/samply/email/EmailContextKey.java @@ -1,9 +1,12 @@ package de.samply.email; +import lombok.Getter; + import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; +@Getter public enum EmailContextKey { BRIDGEHEAD("bridgehead"), @@ -43,19 +46,12 @@ public enum EmailContextKey { BRIDGEHEAD_ADMIN_EMAIL("bridgehead-admin-email"), RESEARCH_ENVIRONMENT_URL("research-environment-url"); - private String value; - private static Set allValues = Stream.of(values()).map(EmailContextKey::getValue).collect(Collectors.toSet()); + private final String value; + @Getter + private static final Set allValues = Stream.of(values()).map(EmailContextKey::getValue).collect(Collectors.toSet()); EmailContextKey(String value) { this.value = value; } - public String getValue() { - return value; - } - - public static Set getAllValues() { - return allValues; - } - } diff --git a/src/main/java/de/samply/email/EmailKeyValues.java b/src/main/java/de/samply/email/EmailKeyValues.java index 3abc7e08..459380b4 100644 --- a/src/main/java/de/samply/email/EmailKeyValues.java +++ b/src/main/java/de/samply/email/EmailKeyValues.java @@ -6,48 +6,48 @@ import de.samply.db.model.ProjectBridgehead; import de.samply.db.model.ProjectBridgeheadUser; import de.samply.db.model.Query; -import de.samply.db.repository.*; +import de.samply.document.DocumentService; import de.samply.frontend.FrontendService; +import de.samply.project.ProjectBridgeheadService; +import de.samply.user.UserService; import de.samply.user.roles.ProjectRole; +import de.samply.utils.ProjectUtils; +import de.samply.utils.UserUtils; import jakarta.validation.constraints.NotNull; +import lombok.Getter; import java.util.HashMap; import java.util.Map; import java.util.Optional; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; import java.util.regex.Pattern; import java.util.stream.Collectors; public class EmailKeyValues { - private Map keyValues = new HashMap<>(); + @Getter + private final Map keyValues = new HashMap<>(); private final FrontendService frontendService; - private final ProjectBridgeheadRepository projectBridgeheadRepository; - private final ProjectRepository projectRepository; - private final UserRepository userRepository; + private final DocumentService documentService; + private final UserService userService; + private final ProjectBridgeheadService projectBridgeheadService; + private final BridgeheadConfiguration bridgeheadConfiguration; - private final ProjectDocumentRepository projectDocumentRepository; - private final BridgeheadAdminUserRepository bridgeheadAdminUserRepository; public EmailKeyValues(FrontendService frontendService, EmailContext emailContext, - ProjectBridgeheadRepository projectBridgeheadRepository, - ProjectRepository projectRepository, - UserRepository userRepository, + DocumentService documentService, + UserService userService, BridgeheadConfiguration bridgeheadConfiguration, - ProjectDocumentRepository projectDocumentRepository, - BridgeheadAdminUserRepository bridgeheadAdminUserRepository, - String researchEnvironmentUrl + String researchEnvironmentUrl, + ProjectBridgeheadService projectBridgeheadService ) { this.frontendService = frontendService; - this.projectBridgeheadRepository = projectBridgeheadRepository; - this.projectRepository = projectRepository; - this.userRepository = userRepository; + this.documentService = documentService; + this.userService = userService; this.bridgeheadConfiguration = bridgeheadConfiguration; - this.projectDocumentRepository = projectDocumentRepository; - this.bridgeheadAdminUserRepository = bridgeheadAdminUserRepository; + this.projectBridgeheadService = projectBridgeheadService; keyValues.putAll(emailContext.getContext()); addKeyValue(EmailContextKey.RESEARCH_ENVIRONMENT_URL, researchEnvironmentUrl); } @@ -67,25 +67,17 @@ public EmailKeyValues add(EmailRecipient emailRecipient) { } private void addProjectBridgeheadOrProject(EmailRecipient emailRecipient) { - AtomicReference> projectBridgeheadOptional = new AtomicReference<>(Optional.empty()); - AtomicReference> projectOptional = new AtomicReference<>(Optional.empty()); - emailRecipient.getProjectCode().ifPresent(projectCode -> - projectRepository.findByCode(projectCode).ifPresent(project -> - emailRecipient.getBridgehead().ifPresentOrElse(bridgehead -> - projectBridgeheadRepository.findFirstByBridgeheadAndProject(bridgehead, project).ifPresentOrElse(projectBridgehead -> - projectBridgeheadOptional.set(Optional.of(projectBridgehead)), - () -> projectOptional.set(Optional.of(project))), - () -> projectOptional.set(Optional.of(project))))); - if (projectBridgeheadOptional.get().isPresent()) { - add(projectBridgeheadOptional.get().get()); - } else if (projectOptional.get().isPresent()) { - add(projectOptional.get().get()); - } else { - emailRecipient.getProjectCode().ifPresent(this::addProjectCode); - emailRecipient.getBridgehead().ifPresent(this::addBridgehead); - } + emailRecipient.getBridgehead() + .map(pb -> () -> add(pb)) + .or(() -> emailRecipient.getProject().map(p -> () -> add(p))) + .orElse(() -> { + emailRecipient.getProject().ifPresent(this::addProject); + emailRecipient.getBridgehead().ifPresent(this::addBridgehead); + }) + .run(); } + @SuppressWarnings("unused") public EmailKeyValues add(ProjectBridgeheadUser projectBridgeheadUser) { if (projectBridgeheadUser != null) { addEmailData(projectBridgeheadUser.getEmail(), @@ -104,55 +96,62 @@ public EmailKeyValues add(ProjectRole projectRole) { return this; } + @SuppressWarnings("UnusedReturnValue") public EmailKeyValues addMessage(String message) { addKeyValue(EmailContextKey.MESSAGE, message); return this; } - public EmailKeyValues addProjectCode(String projectCode) { - addKeyValue(EmailContextKey.PROJECT_CODE, projectCode); + @SuppressWarnings("UnusedReturnValue") + public EmailKeyValues addProject(Project project) { + addKeyValue(EmailContextKey.PROJECT_CODE, project.getCode()); addKeyValue(EmailContextKey.PROJECT_VIEW_URL, this.frontendService.fetchUrl(ProjectManagerConst.PROJECT_VIEW_SITE, - Map.of(ProjectManagerConst.PROJECT_CODE, projectCode))); + Map.of(ProjectManagerConst.PROJECT_CODE, project.getCode()))); return this; } public EmailKeyValues add(ProjectBridgehead projectBridgehead) { if (projectBridgehead != null) { - addBridgehead(projectBridgehead.getBridgehead()); + addBridgehead(projectBridgehead); add(projectBridgehead.getProject()); addKeyValue(EmailContextKey.PROJECT_BRIDGEHEAD_RESULTS_URL, projectBridgehead.getResultsUrl()); - addBridgeheadAdmin(projectBridgehead.getBridgehead()); + addBridgeheadAdmin(projectBridgehead); } return this; } - public EmailKeyValues addBridgeheadAdmin(String bridgehead) { + @SuppressWarnings("UnusedReturnValue") + public EmailKeyValues addBridgeheadAdmin(ProjectBridgehead bridgehead) { if (bridgehead != null) { - this.bridgeheadAdminUserRepository.findFirstByBridgehead(bridgehead).ifPresent(bridgeheadAdminUser -> - addEmailData(bridgeheadAdminUser.getEmail(), - EmailContextKey.BRIDGEHEAD_ADMIN_EMAIL, - EmailContextKey.BRIDGEHEAD_ADMIN_FIRST_NAME, - EmailContextKey.BRIDGEHEAD_ADMIN_LAST_NAME, - EmailContextKey.BRIDGEHEAD_ADMIN_NAME)); + userService + .fetchFirstBridgeheadAdmin(bridgehead) + .ifPresent(bridgeheadAdminUser -> + addEmailData( + bridgeheadAdminUser.getEmail(), + EmailContextKey.BRIDGEHEAD_ADMIN_EMAIL, + EmailContextKey.BRIDGEHEAD_ADMIN_FIRST_NAME, + EmailContextKey.BRIDGEHEAD_ADMIN_LAST_NAME, + EmailContextKey.BRIDGEHEAD_ADMIN_NAME)); } return this; } - public EmailKeyValues addBridgehead(String bridgehead) { + @SuppressWarnings("UnusedReturnValue") + public EmailKeyValues addBridgehead(ProjectBridgehead bridgehead) { addKeyValue(EmailContextKey.BRIDGEHEAD, fetchHumanReadableBridgehead(bridgehead)); addBridgeheadAdmin(bridgehead); return this; } - private String fetchHumanReadableBridgehead(String bridgehead) { - Optional humanReadable = bridgeheadConfiguration.getHumanReadable(bridgehead); - return humanReadable.isPresent() ? humanReadable.get() : bridgehead; + private String fetchHumanReadableBridgehead(ProjectBridgehead bridgehead) { + Optional humanReadable = bridgeheadConfiguration.getHumanReadable(bridgehead.getBridgehead()); + return humanReadable.orElse(bridgehead.getBridgehead()); } public EmailKeyValues add(Project project) { if (project != null) { - addProjectCode(project.getCode()); + addProject(project); addEmailData(project.getCreatorEmail(), EmailContextKey.PROJECT_CREATOR_EMAIL, EmailContextKey.PROJECT_CREATOR_FIRST_NAME, @@ -161,7 +160,7 @@ public EmailKeyValues add(Project project) { addKeyValue(EmailContextKey.QUERY, (project.getQuery().getHumanReadable()) != null ? project.getQuery().getHumanReadable() : project.getQuery().getQuery()); - addKeyValue(EmailContextKey.PROJECT_TYPE, () -> project.getType().toString()); + addKeyValue(EmailContextKey.PROJECT_TYPE, () -> ProjectUtils.formatWithCommasAndAnd(project.fetchProjectTypes())); addKeyValue(EmailContextKey.PROJECT_RESULTS_URL, project.getResultsUrl()); add(project.getQuery()); addBridgeheads(project); @@ -172,13 +171,15 @@ public EmailKeyValues add(Project project) { private void addBridgeheads(Project project) { addKeyValue(EmailContextKey.PROJECT_BRIDGEHEADS, - projectBridgeheadRepository.findByProject(project).stream() - .map(projectBridgehead -> fetchHumanReadableBridgehead(projectBridgehead.getBridgehead())) + projectBridgeheadService + .fetchBridgeheads(project) + .stream() + .map(this::fetchHumanReadableBridgehead) .collect(Collectors.joining(", "))); } private void addLastDocument(Project project) { - projectDocumentRepository.findTopByProjectOrderByCreatedAtDesc(project).ifPresent(projectDocument -> { + documentService.fetchDocumentOrderByCreatedAtDesc(project).ifPresent(projectDocument -> { addKeyValue(EmailContextKey.LAST_DOCUMENT_LABEL, projectDocument::getLabel); addKeyValue(EmailContextKey.LAST_DOCUMENT_FILENAME, projectDocument::getOriginalFilename); addKeyValue(EmailContextKey.LAST_DOCUMENT_URL, projectDocument::getUrl); @@ -219,17 +220,13 @@ public void addKeyValue(@NotNull String key, @NotNull String value) { keyValues.put(key, value); } - public Map getKeyValues() { - return keyValues; - } - private void addEmailData(String email, @NotNull EmailContextKey emailKey, @NotNull EmailContextKey emailFirstNameKey, @NotNull EmailContextKey emailLastNameKey, @NotNull EmailContextKey emailNameKey) { if (email != null) { addKeyValue(emailKey, email); - userRepository.findByEmail(email).ifPresent(user -> { + userService.fetchUser(email).ifPresent(user -> { addKeyValue(emailFirstNameKey, user::getFirstName); addKeyValue(emailLastNameKey, user::getLastName); - addKeyValue(emailNameKey, () -> user.getFirstName() + " " + user.getLastName()); + addKeyValue(emailNameKey, () -> UserUtils.extractFullName(Optional.of(user))); }); } } @@ -241,7 +238,7 @@ public String replaceHtmlVariables(String htmlText) { public static String replaceHtmlVariables(String htmlText, Map keyValues) { if (htmlText != null) { // Regular expression to match the variable pattern - // e.g. It is like a HTML tag + // e.g. It is like an HTML tag String regex = "<\\s*([a-zA-Z0-9_-]+)\\s*/>"; // Use StringBuilder to build the result efficiently @@ -258,7 +255,7 @@ public static String replaceHtmlVariables(String htmlText, Map k // Extract the variable name (group 1 in the regex) String variableName = matcher.group(1); - // Replace the variable with its value, or keep it as-is if not found + // Replace the variable with its value or keep it as-is if not found result.append(keyValues.getOrDefault(variableName, matcher.group())); // Update lastMatchEnd to the end of this match diff --git a/src/main/java/de/samply/email/EmailKeyValuesFactory.java b/src/main/java/de/samply/email/EmailKeyValuesFactory.java index 6ea2ce0b..d4de8c9b 100644 --- a/src/main/java/de/samply/email/EmailKeyValuesFactory.java +++ b/src/main/java/de/samply/email/EmailKeyValuesFactory.java @@ -2,50 +2,47 @@ import de.samply.app.ProjectManagerConst; import de.samply.bridgehead.BridgeheadConfiguration; -import de.samply.db.repository.*; +import de.samply.document.DocumentService; import de.samply.frontend.FrontendService; +import de.samply.project.ProjectBridgeheadService; +import de.samply.user.UserService; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component public class EmailKeyValuesFactory { + // Services private final FrontendService frontendService; + private final DocumentService documentService; + private final UserService userService; + private final ProjectBridgeheadService projectBridgeheadService; + private final EmailContext emailContext; - private final ProjectBridgeheadRepository projectBridgeheadRepository; - private final ProjectRepository projectRepository; - private final UserRepository userRepository; private final BridgeheadConfiguration bridgeheadConfiguration; - private final ProjectDocumentRepository projectDocumentRepository; - private final BridgeheadAdminUserRepository bridgeheadAdminUserRepository; private final String researchEnvironmentUrl; - public EmailKeyValuesFactory(FrontendService frontendService, + public EmailKeyValuesFactory(FrontendService frontendService, DocumentService documentService, EmailContext emailContext, - ProjectBridgeheadRepository projectBridgeheadRepository, - ProjectRepository projectRepository, - UserRepository userRepository, + UserService userService, ProjectBridgeheadService projectBridgeheadService, BridgeheadConfiguration bridgeheadConfiguration, - ProjectDocumentRepository projectDocumentRepository, - BridgeheadAdminUserRepository bridgeheadAdminUserRepository, @Value(ProjectManagerConst.CODER_BASE_URL_SV) String researchEnvironmentUrl) { this.frontendService = frontendService; + this.documentService = documentService; this.emailContext = emailContext; - this.projectBridgeheadRepository = projectBridgeheadRepository; - this.projectRepository = projectRepository; - this.userRepository = userRepository; + this.userService = userService; + this.projectBridgeheadService = projectBridgeheadService; this.bridgeheadConfiguration = bridgeheadConfiguration; - this.projectDocumentRepository = projectDocumentRepository; - this.bridgeheadAdminUserRepository = bridgeheadAdminUserRepository; this.researchEnvironmentUrl = researchEnvironmentUrl; } public EmailKeyValues newInstance() { return new EmailKeyValues( - frontendService, emailContext, projectBridgeheadRepository, projectRepository, - userRepository, bridgeheadConfiguration, projectDocumentRepository, - bridgeheadAdminUserRepository, researchEnvironmentUrl); + frontendService, emailContext, documentService, + userService, bridgeheadConfiguration, + researchEnvironmentUrl, + projectBridgeheadService); } } diff --git a/src/main/java/de/samply/email/EmailMetaData.java b/src/main/java/de/samply/email/EmailMetaData.java index 96e45539..0871e7a9 100644 --- a/src/main/java/de/samply/email/EmailMetaData.java +++ b/src/main/java/de/samply/email/EmailMetaData.java @@ -1,15 +1,20 @@ package de.samply.email; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; import de.samply.user.roles.ProjectRole; import lombok.Data; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; @Data public class EmailMetaData { private String subject; + @JsonProperty("attachment_files") + private List attachmentFiles = new ArrayList<>(); private Map files = new HashMap<>(); @JsonIgnore diff --git a/src/main/java/de/samply/email/EmailRecipient.java b/src/main/java/de/samply/email/EmailRecipient.java index 087de08c..76bf30c7 100644 --- a/src/main/java/de/samply/email/EmailRecipient.java +++ b/src/main/java/de/samply/email/EmailRecipient.java @@ -1,28 +1,28 @@ package de.samply.email; +import de.samply.db.model.Project; +import de.samply.db.model.ProjectBridgehead; import de.samply.user.roles.ProjectRole; import lombok.Getter; +import lombok.Setter; import java.util.Optional; @Getter public class EmailRecipient { - private String email; - private Optional projectCode; - private Optional bridgehead; - private ProjectRole role; + private final String email; + private final Optional project; + private final Optional bridgehead; + private final ProjectRole role; + @Setter private Optional message = Optional.empty(); - public EmailRecipient(String email, Optional projectCode, Optional bridgehead, ProjectRole role) { + public EmailRecipient(String email, Optional project, Optional bridgehead, ProjectRole role) { this.email = email; - this.projectCode = projectCode; + this.project = project; this.bridgehead = bridgehead; this.role = role; } - public void setMessage(Optional message) { - this.message = message; - } - } diff --git a/src/main/java/de/samply/email/EmailRecipientType.java b/src/main/java/de/samply/email/EmailRecipientType.java index ea5624d0..a2647d19 100644 --- a/src/main/java/de/samply/email/EmailRecipientType.java +++ b/src/main/java/de/samply/email/EmailRecipientType.java @@ -10,6 +10,6 @@ public enum EmailRecipientType { ALL_FINALS, ALL_BRIDGEHEAD_ADMINS, BRIDGEHEAD_ADMIN, - BRIDGHEAD_ADMINS_WHO_HAVE_NOT_ACCEPTED_NOR_REJECTED_THE_PROJECT, + BRIDGEHEAD_ADMINS_WHO_HAVE_NOT_ACCEPTED_NOR_REJECTED_THE_PROJECT, PROJECT_ALL } diff --git a/src/main/java/de/samply/email/EmailService.java b/src/main/java/de/samply/email/EmailService.java index f972437b..8c950181 100644 --- a/src/main/java/de/samply/email/EmailService.java +++ b/src/main/java/de/samply/email/EmailService.java @@ -1,6 +1,10 @@ package de.samply.email; import de.samply.app.ProjectManagerConst; +import de.samply.db.model.Project; +import de.samply.db.model.ProjectBridgehead; +import de.samply.email.attachment.AttachmentFileService; +import de.samply.email.attachment.FilenameAndFileContent; import de.samply.notification.NotificationService; import de.samply.notification.OperationType; import de.samply.user.UserService; @@ -13,6 +17,7 @@ import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.ByteArrayResource; import org.springframework.mail.MailException; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; @@ -35,10 +40,13 @@ public class EmailService { private final Optional testMailSender; private final TemplateEngine templateEngine; private final EmailTemplates emailTemplates; - private final NotificationService notificationService; private final EmailKeyValuesFactory emailKeyValuesFactory; private final List testMailDomains; + + // Services private final UserService userService; + private final NotificationService notificationService; + private final AttachmentFileService attachmentFileService; public EmailService( @@ -51,7 +59,8 @@ public EmailService( NotificationService notificationService, EmailKeyValuesFactory emailKeyValuesFactory, @Value(ProjectManagerConst.TEST_EMAIL_DOMAINS_SV) List testMailDomains, - UserService userService) { + UserService userService, + AttachmentFileService attachmentFileService) { this.emailFrom = emailFrom; this.mailSender = mailSender; this.testMailSender = testMailSender; @@ -62,28 +71,32 @@ public EmailService( this.emailKeyValuesFactory = emailKeyValuesFactory; this.testMailDomains = testMailDomains; this.userService = userService; + this.attachmentFileService = attachmentFileService; } @Async(ProjectManagerConst.ASYNC_EMAIL_SENDER_EXECUTOR) - public void sendEmail(@NotNull String emailTo, Optional project, Optional bridgehead, @NotNull ProjectRole role, @NotNull EmailTemplateType type) throws EmailServiceException { + public void sendEmail(@NotNull String emailTo, Optional project, Optional bridgehead, @NotNull ProjectRole role, @NotNull EmailTemplateType type) throws EmailServiceException { sendEmail(emailTo, project, bridgehead, role, type, this.emailKeyValuesFactory.newInstance()); } @Async(ProjectManagerConst.ASYNC_EMAIL_SENDER_EXECUTOR) - public void sendEmail(@NotNull String emailTo, Optional project, Optional bridgehead, @NotNull ProjectRole role, @NotNull EmailTemplateType type, EmailKeyValues keyValues) throws EmailServiceException { + public void sendEmail(@NotNull String emailTo, Optional project, + Optional bridgehead, @NotNull ProjectRole role, + @NotNull EmailTemplateType type, EmailKeyValues keyValues) throws EmailServiceException { if (enableEmails && !userService.isUserInMailingBlackList(emailTo)) { - project.ifPresent(keyValues::addProjectCode); + project.ifPresent(keyValues::addProject); bridgehead.ifPresent(keyValues::addBridgehead); Optional messageSubject = createEmailMessageAndSubject(role, type, keyValues); if (messageSubject.isPresent()) { - sendEmail(emailTo, messageSubject.get()); + List attachments = fetchAttachments(project, type); + sendEmail(emailTo, messageSubject.get(), attachments); if (project.isPresent()) { String details = "Email to " + emailTo + " (" + role + ") of type " + type.toString(); String message = keyValues.getKeyValues().get(EmailContextKey.MESSAGE.getValue()); if (message != null) { details += " : " + message; } - notificationService.createNotification(project.get(), bridgehead.isPresent() ? bridgehead.get() : null, + notificationService.createNotification(project.get(), bridgehead.map(ProjectBridgehead::getBridgehead).orElse(null), ProjectManagerConst.EMAIL_SERVICE, OperationType.SEND_EMAIL, details, null, null); } } else { @@ -91,37 +104,84 @@ public void sendEmail(@NotNull String emailTo, Optional project, Optiona } } else { log.info(enableEmails ? "SMTP Server not enabled." : "User Email in mailing blacklist"); - log.info("Email to " + emailTo + " with role " + role + " for bridgehead " + - (bridgehead.isPresent() ? bridgehead.get() : "NONE") + " and type " + type + " could not be sent"); + log.info("Email to {} with role {} for bridgehead {} and type {} could not be sent", emailTo, role, + bridgehead.map(ProjectBridgehead::getBridgehead).orElse("NONE"), type); } } - private void sendEmail(String emailTo, MessageSubject messageSubject) { + private List fetchAttachments(Optional project, EmailTemplateType type) { + return project + .map(tempProject -> emailTemplates + .getAttachmentFiles(type) + .stream() + .flatMap(attachmentFile -> attachmentFileService + .fetchAttachmentFilenameAndContent( + tempProject, + attachmentFile, + Optional.empty() + ) + .stream() + ) + .toList() + ) + .orElseGet(List::of); + } + + private void sendEmail(String emailTo, MessageSubject messageSubject, List attachments) { try { - fetchMailSender(emailTo).send(createMimeMessage(emailTo, emailFrom, messageSubject)); + fetchMailSender(emailTo).send(createMimeMessage(emailTo, emailFrom, messageSubject, attachments)); } catch (MailException | EmailServiceException e) { log.error("Failed to send email"); log.error(ExceptionUtils.getStackTrace(e)); } } - private MimeMessage createMimeMessage(String emailTo, String emailFrom, MessageSubject messageSubject) throws EmailServiceException { + private MimeMessage createMimeMessage( + String emailTo, + String emailFrom, + MessageSubject messageSubject, + List attachments) throws EmailServiceException { try { - return createMimeMessageWithoutHandlingException(emailTo, emailFrom, messageSubject); + return createMimeMessageWithoutHandlingException(emailTo, emailFrom, messageSubject, attachments); } catch (MessagingException e) { throw new EmailServiceException(e); } } - private MimeMessage createMimeMessageWithoutHandlingException(String emailTo, String emailFrom, MessageSubject messageSubject) throws MessagingException { + private MimeMessage createMimeMessageWithoutHandlingException( + String emailTo, + String emailFrom, + MessageSubject messageSubject, + List attachments + ) throws MessagingException { + MimeMessage message = fetchMailSender(emailTo).createMimeMessage(); - MimeMessageHelper messageHelper = new MimeMessageHelper(message, true, StandardCharsets.UTF_8.name()); - message.setHeader("Content-Type", "text/html; charset=UTF-8"); - messageHelper.setTo(emailTo); - message.setFrom(emailFrom); - message.setSubject(messageSubject.subject(), StandardCharsets.UTF_8.name()); - message.setContent(messageSubject.message(), "text/html; charset=UTF-8"); + + MimeMessageHelper helper = new MimeMessageHelper( + message, + !attachments.isEmpty(), // multipart only if needed + StandardCharsets.UTF_8.name() + ); + + helper.setTo(emailTo); + helper.setFrom(emailFrom); + helper.setSubject(messageSubject.subject()); + + // Use helper, not message.setContent(...) + helper.setText( + messageSubject.message(), + true // HTML + ); + + for (FilenameAndFileContent attachment : attachments) { + helper.addAttachment( + attachment.filename(), + new ByteArrayResource(attachment.fileContent()) + ); + } + return message; + } public Optional createEmailMessageAndSubject(ProjectRole role, EmailTemplateType type, EmailKeyValues keyValues) { @@ -133,19 +193,24 @@ public Optional createEmailMessageAndSubject(ProjectRole role, E return Optional.empty(); } - public Optional createEmailMessageAndSubject(String emailTo, Optional projectCode, Optional bridgehead, ProjectRole projectRole, EmailTemplateType emailTemplateType) { - return createEmailMessageAndSubject(projectRole, emailTemplateType, emailKeyValuesFactory.newInstance().add(new EmailRecipient(emailTo, projectCode, bridgehead, projectRole))); + public Optional createEmailMessageAndSubject(String emailTo, Optional project, Optional bridgehead, ProjectRole projectRole, EmailTemplateType emailTemplateType) { + return createEmailMessageAndSubject( + projectRole, + emailTemplateType, + emailKeyValuesFactory + .newInstance() + .add(new EmailRecipient(emailTo, project, bridgehead, projectRole))); } private Context createContext(EmailKeyValues keyValues) { Context context = new Context(); - keyValues.getKeyValues().forEach((key, value) -> context.setVariable(key, value)); + keyValues.getKeyValues().forEach(context::setVariable); // Remove hyphens ("-") and convert keys to camel case to ensure Thymeleaf can process variables correctly. - // For example: "my-variable" -> "myVariable". + // For example, "my-variable" -> "myVariable". // In Thymeleaf templates, we can use to reference the variable directly. // However, when using the standard Thymeleaf processor, we need to use // because Thymeleaf does not support hyphens ("-") in variable names (e.g., ${my-variable} is invalid). - KeyTransformer.transformMapKeys(keyValues.getKeyValues()).forEach((key, value) -> context.setVariable(key, value)); + KeyTransformer.transformMapKeys(keyValues.getKeyValues()).forEach(context::setVariable); return context; } diff --git a/src/main/java/de/samply/email/EmailTemplateType.java b/src/main/java/de/samply/email/EmailTemplateType.java index dc0e7266..7a5fdfcb 100644 --- a/src/main/java/de/samply/email/EmailTemplateType.java +++ b/src/main/java/de/samply/email/EmailTemplateType.java @@ -23,10 +23,11 @@ public enum EmailTemplateType { NEW_VOTUM_FOR_ALL_BRIDGEHEADS, REQUEST_TECHNICAL_APPROVAL, NEW_PUBLICATION, + NEW_FINAL_REPORT, QUERY_SAVED_IN_EXPORTER, QUERY_SAVED_IN_EXPORTER_AND_EXECUTED, ERROR_WHILE_SAVING_QUERY_IN_EXPORTER, - PROJECT_BRIDGHEAD_RESULTS_URL_ADDED, + PROJECT_BRIDGEHEAD_RESULTS_URL_ADDED, PROJECT_RESULTS_URL_ADDED, PROJECT_RESULTS_ACCEPTED_BY_CREATOR, PROJECT_RESULTS_REJECTED_BY_CREATOR, diff --git a/src/main/java/de/samply/email/EmailTemplates.java b/src/main/java/de/samply/email/EmailTemplates.java index ff633fd1..25322632 100644 --- a/src/main/java/de/samply/email/EmailTemplates.java +++ b/src/main/java/de/samply/email/EmailTemplates.java @@ -6,22 +6,32 @@ import lombok.Data; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; @Data public class EmailTemplates { - private Map templates = new HashMap<>(); @JsonIgnore - public Optional getTemplateAndSubject(@NotNull EmailTemplateType type, @NotNull ProjectRole role) { - EmailMetaData emailMetaData = templates.get(type); - if (emailMetaData != null) { - return Optional.ofNullable(emailMetaData.getTemplateAndSubject(role)); - } - return Optional.empty(); + public Optional getTemplateAndSubject( + @NotNull EmailTemplateType type, + @NotNull ProjectRole role) { + + return Optional + .ofNullable(templates.get(type)) + .map(meta -> meta.getTemplateAndSubject(role)); + } + + public List getAttachmentFiles( + @NotNull EmailTemplateType type) { + + return Optional + .ofNullable(templates.get(type)) + .map(EmailMetaData::getAttachmentFiles) + .orElseGet(List::of); } } diff --git a/src/main/java/de/samply/email/attachment/AttachmentExtra.java b/src/main/java/de/samply/email/attachment/AttachmentExtra.java new file mode 100644 index 00000000..814b2b26 --- /dev/null +++ b/src/main/java/de/samply/email/attachment/AttachmentExtra.java @@ -0,0 +1,5 @@ +package de.samply.email.attachment; + +public sealed interface AttachmentExtra + permits FormExtra { +} diff --git a/src/main/java/de/samply/email/attachment/AttachmentFile.java b/src/main/java/de/samply/email/attachment/AttachmentFile.java new file mode 100644 index 00000000..c35ddc9c --- /dev/null +++ b/src/main/java/de/samply/email/attachment/AttachmentFile.java @@ -0,0 +1,47 @@ +package de.samply.email.attachment; + +import java.util.Optional; + +public enum AttachmentFile { + + FORM { + @Override + public Optional parseExtra(String extra) { + return Optional.of(new FormExtra(extra)); + } + }; + + public Optional parseExtra(String extra) { + return Optional.empty(); + } + + public static Optional parse(String value) { + + if (value == null || value.isBlank()) { + return Optional.empty(); + } + + String[] parts = value.split("-", 2); + + return fromString(parts[0]) + .map(type -> { + Optional extra = + parts.length > 1 + ? type.parseExtra(parts[1]) + : Optional.empty(); + + return new ParsedAttachment(type, extra); + }); + } + + private static Optional fromString(String value) { + try { + return Optional.of( + AttachmentFile.valueOf(value) + ); + } catch (IllegalArgumentException e) { + return Optional.empty(); + } + } + +} \ No newline at end of file diff --git a/src/main/java/de/samply/email/attachment/AttachmentFileService.java b/src/main/java/de/samply/email/attachment/AttachmentFileService.java new file mode 100644 index 00000000..cb8b7b19 --- /dev/null +++ b/src/main/java/de/samply/email/attachment/AttachmentFileService.java @@ -0,0 +1,52 @@ +package de.samply.email.attachment; + +import de.samply.db.model.Project; +import de.samply.form.template.FormTemplateService; +import de.samply.frontend.dto.FormTemplate; +import jakarta.validation.constraints.NotNull; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class AttachmentFileService { + + private final FormTemplateService formTemplateService; + + public AttachmentFileService(FormTemplateService formTemplateService) { + this.formTemplateService = formTemplateService; + } + + public Optional fetchAttachmentFilenameAndContent( + @NotNull Project project, + @NotNull String attachmentFile, + Optional language) { + return AttachmentFile + .parse(attachmentFile) + .flatMap(parsedAttachment -> switch (parsedAttachment.type()) { + case FORM -> fetchFormTemplateFilenameAndContent(project, parsedAttachment.extraInfo(), language); + }); + } + + private Optional fetchFormTemplateFilenameAndContent( + @NotNull Project project, + Optional extra, + Optional language) { + + return extra + .filter(FormExtra.class::isInstance) + .map(FormExtra.class::cast) + .map(FormExtra::formTemplate) + .or(() -> formTemplateService + .fetchTemplates(project, language) + .stream() + .findFirst() + .map(FormTemplate::template)) + .map(formTemplate -> new FilenameAndFileContent( + formTemplateService.fetchFormFilename(project, formTemplate), + formTemplateService.createFormAsPdf(project, formTemplate, language) + )); + } + + +} diff --git a/src/main/java/de/samply/email/attachment/FilenameAndFileContent.java b/src/main/java/de/samply/email/attachment/FilenameAndFileContent.java new file mode 100644 index 00000000..7adabec6 --- /dev/null +++ b/src/main/java/de/samply/email/attachment/FilenameAndFileContent.java @@ -0,0 +1,7 @@ +package de.samply.email.attachment; + +public record FilenameAndFileContent( + String filename, + byte[] fileContent +) { +} diff --git a/src/main/java/de/samply/email/attachment/FormExtra.java b/src/main/java/de/samply/email/attachment/FormExtra.java new file mode 100644 index 00000000..1741c1ac --- /dev/null +++ b/src/main/java/de/samply/email/attachment/FormExtra.java @@ -0,0 +1,6 @@ +package de.samply.email.attachment; + +public record FormExtra( + String formTemplate +) implements AttachmentExtra { +} diff --git a/src/main/java/de/samply/email/attachment/ParsedAttachment.java b/src/main/java/de/samply/email/attachment/ParsedAttachment.java new file mode 100644 index 00000000..8c5186c4 --- /dev/null +++ b/src/main/java/de/samply/email/attachment/ParsedAttachment.java @@ -0,0 +1,9 @@ +package de.samply.email.attachment; + +import java.util.Optional; + +public record ParsedAttachment( + AttachmentFile type, + Optional extraInfo +) { +} \ No newline at end of file diff --git a/src/main/java/de/samply/email/thymeleaf/EmailContextProcessor.java b/src/main/java/de/samply/email/thymeleaf/EmailContextProcessor.java index 445701e6..235169eb 100644 --- a/src/main/java/de/samply/email/thymeleaf/EmailContextProcessor.java +++ b/src/main/java/de/samply/email/thymeleaf/EmailContextProcessor.java @@ -31,7 +31,7 @@ protected void doProcess(ITemplateContext context, IProcessableElementTag tag, I // Retrieve the desired variable from the context Object variableValue = context.getVariable(variableName); - // Retrieve the "default" and "defaultVar" attributes, if they exist + // Retrieve the "default" and "defaultVar" attributes if they exist String defaultValue = tag.getAttributeValue(ProjectManagerConst.EMAIL_CONTEXT_VARIABLE_TAG_ATTRIBUTE_DEFAULT_VALUE); String defaultVarName = tag.getAttributeValue(ProjectManagerConst.EMAIL_CONTEXT_VARIABLE_TAG_ATTRIBUTE_DEFAULT_VARIABLE); @@ -60,7 +60,7 @@ protected void doProcess(ITemplateContext context, IProcessableElementTag tag, I } } - private boolean isEmailContextVariable(ITemplateContext context, String variable){ + private boolean isEmailContextVariable(ITemplateContext context, String variable) { return variable != null && (EmailContextKey.getAllValues().contains(variable) || context.getVariable(variable) != null); } @@ -77,14 +77,14 @@ private String resolveVariables(String content, ITemplateContext context) { ); Matcher combinedMatcher = combinedPattern.matcher(content); - StringBuffer resolvedContent = new StringBuffer(); + StringBuilder resolvedContent = new StringBuilder(); // Process the combined matches while (combinedMatcher.find()) { if (combinedMatcher.group(1) != null) { // Match found for ${variable} - String variableName = combinedMatcher.group(2); // Extract variable name inside ${} - if (isEmailContextVariable(context, variableName)){ + String variableName = combinedMatcher.group(2); // Extract the variable name inside ${} + if (isEmailContextVariable(context, variableName)) { Object variableValue = context.getVariable(variableName); // Resolve variable String replacement = (variableValue != null) ? variableValue.toString() : ""; // Use empty string if not found combinedMatcher.appendReplacement(resolvedContent, Matcher.quoteReplacement(replacement)); @@ -92,7 +92,7 @@ private String resolveVariables(String content, ITemplateContext context) { } else if (combinedMatcher.group(3) != null) { // Match found for String tagName = combinedMatcher.group(4); // Extract the tag name - if (isEmailContextVariable(context, tagName)){ + if (isEmailContextVariable(context, tagName)) { Object tagValue = context.getVariable(tagName); // Resolve the value from context String replacement = (tagValue != null) ? tagValue.toString() : ""; // Use empty string if not found combinedMatcher.appendReplacement(resolvedContent, Matcher.quoteReplacement(replacement)); diff --git a/src/main/java/de/samply/email/thymeleaf/ThymeleafConfiguration.java b/src/main/java/de/samply/email/thymeleaf/ThymeleafConfiguration.java index 4cacc461..aec3953a 100644 --- a/src/main/java/de/samply/email/thymeleaf/ThymeleafConfiguration.java +++ b/src/main/java/de/samply/email/thymeleaf/ThymeleafConfiguration.java @@ -1,16 +1,16 @@ package de.samply.email.thymeleaf; import de.samply.app.ProjectManagerConst; +import de.samply.utils.DirectoryUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.util.StringUtils; import org.thymeleaf.spring6.SpringTemplateEngine; +import org.thymeleaf.templatemode.TemplateMode; import org.thymeleaf.templateresolver.FileTemplateResolver; import java.io.FileNotFoundException; -import java.nio.file.Files; -import java.nio.file.Path; +import java.nio.charset.StandardCharsets; @Configuration public class ThymeleafConfiguration { @@ -19,16 +19,7 @@ public class ThymeleafConfiguration { public ThymeleafConfiguration( @Value(ProjectManagerConst.EMAIL_TEMPLATES_DIRECTORY_SV) String externalTemplateDirectory) throws FileNotFoundException { - this.externalTemplateDirectory = fetchExternalTemplateDirectory(externalTemplateDirectory); - } - - private String fetchExternalTemplateDirectory(String environmentVariable) throws FileNotFoundException { - if (StringUtils.hasText(environmentVariable) && Files.isDirectory(Path.of(environmentVariable))) { - return (environmentVariable.endsWith("/") || environmentVariable.endsWith("\\")) ? - environmentVariable : environmentVariable + "/"; - } else { - throw new FileNotFoundException("Email Templates Directory not set or set to incorrect directory: " + environmentVariable); - } + this.externalTemplateDirectory = DirectoryUtils.fetchExternalTemplateDirectory(externalTemplateDirectory); } @Bean @@ -36,8 +27,8 @@ public FileTemplateResolver externalTemplateResolver() { FileTemplateResolver resolver = new FileTemplateResolver(); resolver.setPrefix(externalTemplateDirectory); resolver.setSuffix(".html"); - resolver.setTemplateMode("HTML"); - resolver.setCharacterEncoding("UTF-8"); + resolver.setTemplateMode(TemplateMode.HTML); + resolver.setCharacterEncoding(StandardCharsets.UTF_8.name()); resolver.setCacheable(true); return resolver; } diff --git a/src/main/java/de/samply/exporter/ExporterJob.java b/src/main/java/de/samply/exporter/ExporterJob.java index f7784969..d129bff4 100644 --- a/src/main/java/de/samply/exporter/ExporterJob.java +++ b/src/main/java/de/samply/exporter/ExporterJob.java @@ -2,22 +2,23 @@ import de.samply.app.ProjectManagerConst; import de.samply.db.model.ProjectBridgehead; -import de.samply.db.repository.BridgeheadAdminUserRepository; -import de.samply.db.repository.ProjectBridgeheadRepository; +import de.samply.db.model.ProjectBridgeheadExecution; import de.samply.email.EmailKeyValuesFactory; import de.samply.email.EmailService; import de.samply.email.EmailTemplateType; +import de.samply.project.ProjectBridgeheadService; import de.samply.project.state.ProjectState; import de.samply.query.QueryState; +import de.samply.user.UserService; import de.samply.user.roles.ProjectRole; import lombok.extern.slf4j.Slf4j; +import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.time.Instant; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; @@ -28,29 +29,34 @@ public class ExporterJob { private final boolean enabled; - private final ExporterService exporterService; - private final ProjectBridgeheadRepository projectBridgeheadRepository; private final Set activeStates = Set.of(ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL); + + // Services + private final ExporterService exporterService; private final EmailService emailService; - private final BridgeheadAdminUserRepository bridgeheadAdminUserRepository; + private final UserService userService; + private final ProjectBridgeheadService projectBridgeheadService; + private final EmailKeyValuesFactory emailKeyValuesFactory; + public ExporterJob( @Value(ProjectManagerConst.ENABLE_EXPORTER_SV) boolean enabled, ExporterService exporterService, - ProjectBridgeheadRepository projectBridgeheadRepository, EmailService emailService, - BridgeheadAdminUserRepository bridgeheadAdminUserRepository, + UserService userService, + ProjectBridgeheadService projectBridgeheadService, EmailKeyValuesFactory emailKeyValuesFactory) { this.enabled = enabled; this.exporterService = exporterService; - this.projectBridgeheadRepository = projectBridgeheadRepository; + this.projectBridgeheadService = projectBridgeheadService; this.emailService = emailService; - this.bridgeheadAdminUserRepository = bridgeheadAdminUserRepository; + this.userService = userService; this.emailKeyValuesFactory = emailKeyValuesFactory; } @Scheduled(cron = ProjectManagerConst.EXPORTER_CRON_EXPRESSION_SV) + @SchedulerLock(name = ProjectManagerConst.EXPORTER_JOB_NAME) public void checkExports() { if (enabled) { log.debug("Exporter Job started"); @@ -77,7 +83,7 @@ private Mono checkQueriesToSendAndExecute() { private Mono checkQueriesAlreadySent() { log.debug("Checking queries already sent..."); - return checkQueries(QueryState.SENDING, QueryState.FINISHED, exporterService::checkIfQueryIsAlreadySentOrExecuted, EmailTemplateType.QUERY_SAVED_IN_EXPORTER); + return checkQueries(QueryState.SENDING, exporterService::checkIfQueryIsAlreadySentOrExecuted, EmailTemplateType.QUERY_SAVED_IN_EXPORTER); } private Mono checkQueriesAlreadyExecutingStep1() { @@ -87,7 +93,7 @@ private Mono checkQueriesAlreadyExecutingStep1() { private Mono checkQueriesAlreadyExecutingStep2() { log.debug("Checking queries already executing (step 2)..."); - return checkQueries(QueryState.EXPORT_RUNNING_2, QueryState.FINISHED, exporterService::checkIfQueryIsAlreadySentOrExecuted, EmailTemplateType.QUERY_SAVED_IN_EXPORTER_AND_EXECUTED); + return checkQueries(QueryState.EXPORT_RUNNING_2, exporterService::checkIfQueryIsAlreadySentOrExecuted, EmailTemplateType.QUERY_SAVED_IN_EXPORTER_AND_EXECUTED); } private Mono checkQueriesAlreadySentToBeExecuted() { @@ -95,44 +101,65 @@ private Mono checkQueriesAlreadySentToBeExecuted() { return checkQueries(QueryState.SENDING_AND_EXECUTING, QueryState.EXPORT_RUNNING_1, exporterService::checkIfQueryIsAlreadySentOrExecuted, Optional.of( exporterServiceResult -> exporterService.fetchExporterExecutionIdFromExporterResponse(exporterServiceResult.result()).ifPresent(exportExecutionId -> - exporterServiceResult.projectBridgehead().setExporterExecutionId(exportExecutionId))), Optional.empty()); + exporterServiceResult + .projectBridgeheadAndType() + .projectBridgehead() + .fetchExecution(exporterServiceResult.projectBridgeheadAndType().projectType()) + .orElseThrow(() -> new IllegalStateException("Missing execution for " + exporterServiceResult.projectBridgeheadAndType().projectType())) + .setExporterExecutionId(exportExecutionId))), Optional.empty()); } private Mono checkQueries(QueryState initialQueryState, QueryState finalQueryState, - Function> exporterServiceFunction) { + Function> exporterServiceFunction) { return checkQueries(initialQueryState, finalQueryState, exporterServiceFunction, Optional.empty(), Optional.empty()); } - private Mono checkQueries(QueryState initialQueryState, QueryState finalQueryState, - Function> exporterServiceFunction, EmailTemplateType emailTemplateType) { - return checkQueries(initialQueryState, finalQueryState, exporterServiceFunction, Optional.empty(), Optional.of(emailTemplateType)); + private Mono checkQueries(QueryState initialQueryState, + Function> exporterServiceFunction, EmailTemplateType emailTemplateType) { + return checkQueries(initialQueryState, QueryState.FINISHED, exporterServiceFunction, Optional.empty(), Optional.of(emailTemplateType)); } - private Mono checkQueries(QueryState initialQueryState, QueryState finalQueryState, - Function> exporterServiceFunction, - Optional> exporterServiceResultConsumer, Optional emailTemplateType) { - return Flux.fromIterable(projectBridgeheadRepository.getByQueryStateAndProjectState(initialQueryState, activeStates)) + private Mono checkQueries(QueryState initialQueryState, + QueryState finalQueryState, + Function> exporterServiceFunction, + Optional> exporterServiceResultConsumer, + Optional emailTemplateType) { + return Flux.fromStream( + projectBridgeheadService + .fetchBridgeheads(initialQueryState, activeStates) + .stream() + .flatMap(ProjectBridgeheadAndType::from) + ) .flatMap(exporterServiceFunction) .doOnNext(exporterServiceResult -> { exporterServiceResultConsumer.ifPresent(consumer -> consumer.accept(exporterServiceResult)); - ProjectBridgehead projectBridgehead = exporterServiceResult.projectBridgehead(); + ProjectBridgehead projectBridgehead = exporterServiceResult.projectBridgeheadAndType().projectBridgehead(); + ProjectBridgeheadExecution execution = projectBridgehead + .fetchExecution(exporterServiceResult.projectBridgeheadAndType().projectType()) + .orElseThrow(() -> new IllegalStateException("Missing execution for " + exporterServiceResult.projectBridgeheadAndType().projectType())); log.debug("Setting final query state and updating exporter response for project {} and bridgehead {}", projectBridgehead.getProject().getCode(), projectBridgehead.getBridgehead()); - projectBridgehead.setQueryState(finalQueryState); - projectBridgehead.setExporterResponse(exporterServiceResult.result()); - projectBridgehead.setModifiedAt(Instant.now()); + execution.setQueryState(finalQueryState); + execution.setExporterResponse(exporterServiceResult.result()); if (finalQueryState == QueryState.FINISHED) { - projectBridgehead.setExporterDispatchCounter(projectBridgehead.getExporterDispatchCounter() + 1); + execution.setExporterDispatchCounter(execution.getExporterDispatchCounter() + 1); } - projectBridgeheadRepository.save(projectBridgehead); + projectBridgeheadService.saveBridgehead(projectBridgehead); emailTemplateType.ifPresent(type -> sendEmail(projectBridgehead, type)); - }).then(); + }) + .then(); } private void sendEmail(ProjectBridgehead projectBridgehead, EmailTemplateType templateType) { - bridgeheadAdminUserRepository.findByBridgehead(projectBridgehead.getBridgehead()).forEach(bridgeheadAdmin -> { - emailService.sendEmail(bridgeheadAdmin.getEmail(), Optional.of(projectBridgehead.getProject().getCode()), Optional.of(projectBridgehead.getBridgehead()), ProjectRole.BRIDGEHEAD_ADMIN, templateType, emailKeyValuesFactory.newInstance().add(projectBridgehead)); - }); + userService.fetchBridgeheadAdmin(projectBridgehead) + .forEach(bridgeheadAdmin -> + emailService.sendEmail( + bridgeheadAdmin.getEmail(), + Optional.of(projectBridgehead.getProject()), + Optional.of(projectBridgehead), + ProjectRole.BRIDGEHEAD_ADMIN, + templateType, + emailKeyValuesFactory.newInstance().add(projectBridgehead))); } } diff --git a/src/main/java/de/samply/exporter/ExporterService.java b/src/main/java/de/samply/exporter/ExporterService.java index ed1f2248..3880101e 100644 --- a/src/main/java/de/samply/exporter/ExporterService.java +++ b/src/main/java/de/samply/exporter/ExporterService.java @@ -5,14 +5,8 @@ import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import de.samply.app.ProjectManagerConst; -import de.samply.db.model.ProjectBridgehead; -import de.samply.db.model.ProjectBridgeheadDataShield; -import de.samply.db.model.ProjectCoder; -import de.samply.db.model.Query; -import de.samply.db.repository.BridgeheadAdminUserRepository; -import de.samply.db.repository.ProjectBridgeheadDataShieldRepository; -import de.samply.db.repository.ProjectBridgeheadRepository; -import de.samply.db.repository.ProjectCoderRepository; +import de.samply.coder.CoderService; +import de.samply.db.model.*; import de.samply.email.EmailKeyValuesFactory; import de.samply.email.EmailService; import de.samply.email.EmailTemplateType; @@ -22,14 +16,17 @@ import de.samply.exporter.focus.TaskType; import de.samply.notification.NotificationService; import de.samply.notification.OperationType; +import de.samply.project.ProjectBridgeheadService; import de.samply.project.ProjectType; import de.samply.query.QueryState; import de.samply.security.SessionUser; +import de.samply.user.UserService; import de.samply.user.roles.ProjectRole; import de.samply.utils.Base64Utils; import de.samply.utils.MessageStatus; import de.samply.utils.WebClientFactory; import jakarta.validation.constraints.NotNull; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.beans.factory.annotation.Value; @@ -40,16 +37,14 @@ import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.time.Duration; import java.time.Instant; import java.time.LocalDate; import java.time.format.DateTimeFormatter; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -58,16 +53,21 @@ public class ExporterService { - private final BeamService beamService; private final WebClient webClient; private final SessionUser sessionUser; - private final ProjectBridgeheadDataShieldRepository projectBridgeheadDataShieldRepository; - private final ProjectBridgeheadRepository projectBridgeheadRepository; - private final ProjectCoderRepository projectCoderRepository; + private final EmailKeyValuesFactory emailKeyValuesFactory; + + // Services + private final BeamService beamService; + private final CoderService coderService; private final NotificationService notificationService; - private final Set exportTemplates; - private final Set datashieldTemplates; - private final Set researchEnvironmentTemplates; + private final ProjectBridgeheadService projectBridgeheadService; + private final EmailService emailService; + private final UserService userService; + + @Getter + private final Map> exporterTemplates = new HashMap<>(); + private final String focusProjectManagerId; private final String exporterApiKey; private final String coderBeamIdSuffix; @@ -76,10 +76,8 @@ public class ExporterService { private final String beamWaitTime; private final String beamWaitCount; private final int maxTimeToWaitFocusTaskInMinutes; - private final EmailService emailService; - private final BridgeheadAdminUserRepository bridgeheadAdminUserRepository; - private final EmailKeyValuesFactory emailKeyValuesFactory; - private ObjectMapper objectMapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT) + + private final ObjectMapper objectMapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT) .registerModule(new JavaTimeModule()).configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); public ExporterService( @@ -89,6 +87,7 @@ public ExporterService( @Value(ProjectManagerConst.EXPORT_TEMPLATES_SV) Set exportTemplates, @Value(ProjectManagerConst.DATASHIELD_TEMPLATES_SV) Set datashieldTemplates, @Value(ProjectManagerConst.RESEARCH_ENVIRONMENT_TEMPLATES_SV) Set researchEnvironmentTemplates, + @Value(ProjectManagerConst.SAMPLES_TEMPLATES_SV) Set samplesTemplates, CoderService coderService, ProjectBridgeheadService projectBridgeheadService, @Value(ProjectManagerConst.BEAM_TTL_SV) String beamWaitTime, @Value(ProjectManagerConst.BEAM_WAIT_COUNT_SV) String beamWaitCount, @Value(ProjectManagerConst.MAX_TIME_TO_WAIT_FOCUS_TASK_IN_MINUTES_SV) int maxTimeToWaitFocusTaskInMinutes, @@ -96,124 +95,145 @@ public ExporterService( @Value(ProjectManagerConst.CODER_TEST_FILE_BEAM_ID_SV) String testCoderFileBeamId, SessionUser sessionUser, BeamService beamService, - ProjectBridgeheadDataShieldRepository projectBridgeheadDataShieldRepository, NotificationService notificationService, WebClientFactory webClientFactory, - ProjectBridgeheadRepository projectBridgeheadRepository, - ProjectCoderRepository projectCoderRepository, EmailService emailService, - BridgeheadAdminUserRepository bridgeheadAdminUserRepository, - EmailKeyValuesFactory emailKeyValuesFactory) { + EmailKeyValuesFactory emailKeyValuesFactory, + UserService userService) { + this.coderService = coderService; + this.projectBridgeheadService = projectBridgeheadService; + this.userService = userService; + + this.exporterTemplates.put(ProjectType.EXPORT, exportTemplates); + this.exporterTemplates.put(ProjectType.DATASHIELD, datashieldTemplates); + this.exporterTemplates.put(ProjectType.RESEARCH_ENVIRONMENT, researchEnvironmentTemplates); + this.exporterTemplates.put(ProjectType.SAMPLES, samplesTemplates); + this.sessionUser = sessionUser; this.beamService = beamService; - this.projectBridgeheadDataShieldRepository = projectBridgeheadDataShieldRepository; this.notificationService = notificationService; - this.exportTemplates = exportTemplates; - this.datashieldTemplates = datashieldTemplates; this.focusProjectManagerId = focusProjectManagerId; this.beamWaitTime = beamWaitTime; this.beamWaitCount = beamWaitCount; this.maxTimeToWaitFocusTaskInMinutes = maxTimeToWaitFocusTaskInMinutes; - this.researchEnvironmentTemplates = researchEnvironmentTemplates; this.coderBeamIdSuffix = coderBeamIdSuffix; this.testCoderFileBeamId = testCoderFileBeamId; - this.projectCoderRepository = projectCoderRepository; this.emailService = emailService; - this.bridgeheadAdminUserRepository = bridgeheadAdminUserRepository; this.emailKeyValuesFactory = emailKeyValuesFactory; this.webClient = webClientFactory.createWebClient(focusUrl); this.exporterApiKey = exporterApiKey; - this.projectBridgeheadRepository = projectBridgeheadRepository; } - public Mono sendQueryToBridgehead(ProjectBridgehead projectBridgehead) throws ExporterServiceException { - String description = "Sending query of project " + projectBridgehead.getProject().getCode() + " to bridgehead " + projectBridgehead.getBridgehead() + " ..."; + public Mono sendQueryToBridgehead(ProjectBridgeheadAndType projectBridgeheadAndType) throws ExporterServiceException { + String description = "Sending query of project " + projectBridgeheadAndType.projectBridgehead().getProject().getCode() + " to bridgehead " + projectBridgeheadAndType.projectBridgehead().getBridgehead() + " ..."; log.info(description); TaskType taskType = TaskType.CREATE; - return postRequest(projectBridgehead, generateFocusBody(projectBridgehead, taskType), taskType, description) - .doOnSuccess(exporterServiceResult -> log.info("Query sent")); + return postRequest(projectBridgeheadAndType, generateFocusBody(projectBridgeheadAndType, taskType), taskType, description) + .doOnSuccess(_ -> logQuerySent()); } - public Mono sendQueryToBridgeheadAndExecute(ProjectBridgehead projectBridgehead) throws ExporterServiceException { - String description = "Sending query of project " + projectBridgehead.getProject().getCode() + " to bridgehead " + projectBridgehead.getBridgehead() + " to be executed..."; + private void logQuerySent() { + log.info("Query sent"); + } + + public Mono sendQueryToBridgeheadAndExecute(ProjectBridgeheadAndType projectBridgeheadAndType) throws ExporterServiceException { + String description = "Sending query of project " + projectBridgeheadAndType.projectBridgehead().getProject().getCode() + " to bridgehead " + projectBridgeheadAndType.projectBridgehead().getBridgehead() + " to be executed..."; log.info(description); TaskType taskType = TaskType.EXECUTE; - return postRequest(projectBridgehead, generateFocusBody(projectBridgehead, taskType), taskType, description) - .doOnSuccess(exporterServiceResult -> log.info("Query sent")); + return postRequest(projectBridgeheadAndType, generateFocusBody(projectBridgeheadAndType, taskType), taskType, description) + .doOnSuccess(_ -> logQuerySent()); } - public Mono checkExecutionStatus(ProjectBridgehead projectBridgehead) { - String description = "Checking export execution status of project " + projectBridgehead.getProject().getCode() + " in bridgehead " + projectBridgehead.getBridgehead() + "..."; + public Mono checkExecutionStatus(ProjectBridgeheadAndType projectBridgeheadAndType) throws ExporterServiceException { + String description = "Checking export execution status of project " + projectBridgeheadAndType.projectBridgehead().getProject().getCode() + " in bridgehead " + projectBridgeheadAndType.projectBridgehead().getBridgehead() + "..."; log.info(description); TaskType taskType = TaskType.STATUS; - return postRequest(projectBridgehead, generateFocusBody(projectBridgehead, taskType), taskType, description) - .doOnSuccess(exporterServiceResult -> log.info("Status checked")); + return postRequest(projectBridgeheadAndType, generateFocusBody(projectBridgeheadAndType, taskType), taskType, description) + .doOnSuccess(_ -> log.info("Status checked")); } @Async() public void transferFileToResearchEnvironment(@NotNull String projectCode, @NotNull String bridgehead) { - List projectCoder = this.projectCoderRepository.findByBridgeheadAndProjectCodeAndEmailOrderedByCreatedAtDesc(bridgehead, projectCode, sessionUser.getEmail()); + List projectCoder = coderService.fetchCoderOrderedByCreatedAtDesc(projectCode, bridgehead, sessionUser.getEmail()); if (projectCoder.isEmpty()) { - throw new ExporterServiceException("Project " + projectCode + " for bridgehead " + bridgehead + " for user " + sessionUser.getEmail() + " not found"); + throw new ExporterServiceException("ProjectCode " + projectCode + " for bridgehead " + bridgehead + " for user " + sessionUser.getEmail() + " not found"); } - transferFileToResearchEnvironment(projectCoder.get(0)).block(); + transferFileToResearchEnvironment(projectCoder.getFirst()).block(); } public boolean isExportFileTransferredToResearchEnvironment(@NotNull String projectCode, @NotNull String bridgehead) { - List projectCoder = this.projectCoderRepository.findByBridgeheadAndProjectCodeAndEmailOrderedByCreatedAtDesc(bridgehead, projectCode, sessionUser.getEmail()); + List projectCoder = coderService.fetchCoderOrderedByCreatedAtDesc(projectCode, bridgehead, sessionUser.getEmail()); if (projectCoder.isEmpty()) { - throw new ExporterServiceException("Project " + projectCode + " for bridgehead " + bridgehead + " for user " + sessionUser.getEmail() + " not found"); + throw new ExporterServiceException("ProjectCode " + projectCode + " for bridgehead " + bridgehead + " for user " + sessionUser.getEmail() + " not found"); } - return projectCoder.get(0).isExportTransferred(); + return projectCoder.getFirst().isExportTransferred(); } @Async public Mono transferFileToResearchEnvironment(ProjectCoder projectCoder) { String description = "Transferring file to Research Environment for project " + projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getProject().getCode() + " in bridgehead " + projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getBridgehead(); log.info(description); - return postRequest(projectCoder.getProjectBridgeheadUser().getProjectBridgehead(), generateTransferFileBeamRequest(projectCoder), TaskType.FILE_TRANSFER, description) - .doOnError(throwable -> { - MessageStatus messageStatus = MessageStatus.newInstance(throwable, "Error transferring file to Research Environment"); - notificationService.createNotification( - projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getProject().getCode(), - projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getBridgehead(), - projectCoder.getProjectBridgeheadUser().getEmail(), - OperationType.TRANSFER_FILES_TO_RESEARCH_ENVIRONMENT, - messageStatus.message(), - ExceptionUtils.getStackTrace(throwable), - messageStatus.status() - ); - log.error(ExceptionUtils.getStackTrace(throwable)); - }) - .doOnSuccess(result -> { - log.info("Files transferred correctly"); - projectCoder.setExportTransferred(true); - this.projectCoderRepository.save(projectCoder); - notificationService.createNotification( - projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getProject().getCode(), - projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getBridgehead(), - projectCoder.getProjectBridgeheadUser().getEmail(), - OperationType.TRANSFER_FILES_TO_RESEARCH_ENVIRONMENT, - "File transferred to Research Environment", - null, - HttpStatus.OK - ); - }).then(); - } - - private BeamRequest generateTransferFileBeamRequest(ProjectCoder projectCoder) { - return beamService.generateExporterFileTransferBeamRequest(projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getBridgehead(), - projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getExporterExecutionId(), fetchCoderFileBeamId(projectCoder)); + + return Flux.fromStream( + projectCoder + .getProjectBridgeheadUser() + .getProjectBridgehead() + .getProject() + .fetchProjectTypes() + .stream() + .map(projectType -> new ProjectBridgeheadAndType(projectCoder.getProjectBridgeheadUser().getProjectBridgehead(), projectType)) + ).flatMap(projectBridgeheadAndType -> + postRequest(projectBridgeheadAndType, + generateTransferFileBeamRequest(projectCoder, projectBridgeheadAndType.projectType()), + TaskType.FILE_TRANSFER, description) + .doOnError(throwable -> { + MessageStatus messageStatus = MessageStatus.newInstance(throwable, "Error transferring file to Research Environment"); + notificationService.createNotification( + projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getProject(), + projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getBridgehead(), + projectCoder.getProjectBridgeheadUser().getEmail(), + OperationType.TRANSFER_FILES_TO_RESEARCH_ENVIRONMENT, + messageStatus.message(), + ExceptionUtils.getStackTrace(throwable), + messageStatus.status() + ); + log.error(ExceptionUtils.getStackTrace(throwable)); + }) + .doOnSuccess(_ -> { + log.info("Files transferred correctly"); + projectCoder.setExportTransferred(true); + coderService.saveCoder(projectCoder); + notificationService.createNotification( + projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getProject(), + projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getBridgehead(), + projectCoder.getProjectBridgeheadUser().getEmail(), + OperationType.TRANSFER_FILES_TO_RESEARCH_ENVIRONMENT, + "File transferred to Research Environment", + null, + HttpStatus.OK + ); + })) + .then(); + } + + private BeamRequest generateTransferFileBeamRequest(ProjectCoder projectCoder, ProjectType projectType) { + return beamService.generateExporterFileTransferBeamRequest( + projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getBridgehead(), + projectCoder.getProjectBridgeheadUser().getProjectBridgehead() + .fetchExecution(projectType) + .orElseThrow(() -> new IllegalStateException("Missing execution for " + projectType)) + .getExporterExecutionId(), + fetchCoderFileBeamId(projectCoder) + ); } private String fetchCoderFileBeamId(ProjectCoder projectCoder) { - if (testCoderFileBeamId != null) { - return testCoderFileBeamId; - } - return projectCoder.getAppId() + ((coderBeamIdSuffix.startsWith(".")) ? "" : ".") + coderBeamIdSuffix; + return Objects.requireNonNullElseGet(testCoderFileBeamId, () -> projectCoder.getAppId() + ((coderBeamIdSuffix.startsWith(".")) ? "" : ".") + coderBeamIdSuffix); } - private Mono postRequest(ProjectBridgehead projectBridgehead, BeamRequest beamRequest, TaskType taskType, String description) { + private Mono postRequest(ProjectBridgeheadAndType projectBridgeheadAndType, BeamRequest beamRequest, TaskType taskType, String description) { + ProjectBridgehead projectBridgehead = projectBridgeheadAndType.projectBridgehead(); + ProjectType projectType = projectBridgeheadAndType.projectType(); return webClient.post() .uri(uriBuilder -> uriBuilder.path(ProjectManagerConst.BEAM_TASK_PATH).build()) .header(HttpHeaders.AUTHORIZATION, fetchAuthorization()) @@ -222,30 +242,42 @@ private Mono postRequest(ProjectBridgehead projectBridgeh .exchangeToMono(clientResponse -> { log.debug("Received HTTP POST response with status {}", clientResponse.statusCode()); if (clientResponse.statusCode().equals(HttpStatus.OK) || clientResponse.statusCode().equals(HttpStatus.CREATED)) { - fetchBridgeheadOperationType(taskType).ifPresent(operationType -> - createBridgeheadNotification((HttpStatus) clientResponse.statusCode(), null, projectBridgehead, projectBridgehead.getExporterUser(), operationType, description)); - resetProjectBridgeheadDataShield(projectBridgehead); - return Mono.just(new ExporterServiceResult(projectBridgehead, beamService.serializeFocusQuery(beamRequest))); + createBridgeheadNotification( + (HttpStatus) clientResponse.statusCode(), + projectBridgehead, + projectBridgehead + .fetchExecution(projectType) + .orElseThrow(() -> new IllegalStateException("Missing execution for " + projectType)) + .getExporterUser(), + operationType, + description)); + resetProjectBridgeheadDataShield(projectBridgeheadAndType); + return Mono.just(new ExporterServiceResult(projectBridgeheadAndType, beamService.serializeFocusQuery(beamRequest))); } else { - log.error("Http Error " + clientResponse.statusCode() + " posting task " + beamRequest.getId() + " : " + beamRequest.getBody() + - " for project " + projectBridgehead.getProject().getCode() + " and bridgehead " + projectBridgehead.getBridgehead()); + log.error("Http Error {} posting task {} : {} for project {} and bridgehead {}", + clientResponse.statusCode(), beamRequest.getId(), beamRequest.getBody(), + projectBridgehead.getProject().getCode(), projectBridgehead.getBridgehead()); return clientResponse.bodyToMono(String.class).flatMap(errorBody -> { - log.error("Error: {}", errorBody); + logError(errorBody); return Mono.error(new RuntimeException(errorBody)); }); } }); } + private void logError(String error) { + log.error("Error: {}", error); + } + private String fetchAuthorization() { return ProjectManagerConst.API_KEY + ' ' + focusProjectManagerId + ' ' + exporterApiKey; } private void createBridgeheadNotification( - HttpStatus status, String error, ProjectBridgehead projectBridgehead, String email, OperationType operationType, String description) { + HttpStatus status, ProjectBridgehead projectBridgehead, String email, OperationType operationType, String description) { notificationService.createNotification( - projectBridgehead.getProject().getCode(), projectBridgehead.getBridgehead(), email, operationType, description, error, status); + projectBridgehead.getProject(), projectBridgehead.getBridgehead(), email, operationType, description, null, status); } private Optional fetchBridgeheadOperationType(TaskType taskType) { @@ -265,20 +297,31 @@ private String convertToBase64String(Object jsonObject) { } } - private String generateExportQueryInBase64ForExporterRequest(ProjectBridgehead projectBridgehead) + private String generateExportQueryInBase64ForExporterRequest(ProjectBridgeheadAndType projectBridgeheadAndType) throws ExporterServiceException { + ProjectBridgehead projectBridgehead = projectBridgeheadAndType.projectBridgehead(); + ProjectType projectType = projectBridgeheadAndType.projectType(); Query query = projectBridgehead.getProject().getQuery(); Map result = Stream.of( new String[][]{ {ProjectManagerConst.EXPORTER_PARAM_QUERY, query.getQuery()}, {ProjectManagerConst.EXPORTER_PARAM_QUERY_FORMAT, query.getQueryFormat().name()}, - {ProjectManagerConst.EXPORTER_PARAM_QUERY_LABEL, fetchLabel(projectBridgehead)}, + {ProjectManagerConst.EXPORTER_PARAM_QUERY_LABEL, fetchLabel(projectBridgeheadAndType)}, {ProjectManagerConst.EXPORTER_PARAM_QUERY_DESCRIPTION, query.getDescription()}, {ProjectManagerConst.EXPORTER_PARAM_QUERY_CONTEXT, generateQueryContextForExporter(query.getContext(), projectBridgehead.getProject().getCode())}, {ProjectManagerConst.EXPORTER_PARAM_QUERY_CONTACT_ID, projectBridgehead.getProject().getCreatorEmail()}, - {ProjectManagerConst.EXPORTER_PARAM_QUERY_EXECUTION_CONTACT_ID, projectBridgehead.getExporterUser()}, - {ProjectManagerConst.EXPORTER_PARAM_OUTPUT_FORMAT, query.getOutputFormat().name()}, - {ProjectManagerConst.EXPORTER_PARAM_TEMPLATE_ID, query.getTemplateId()}, + {ProjectManagerConst.EXPORTER_PARAM_QUERY_EXECUTION_CONTACT_ID, projectBridgehead + .fetchExecution(projectType) + .orElseThrow(() -> new IllegalStateException("Missing execution for " + projectType)) + .getExporterUser()}, + {ProjectManagerConst.EXPORTER_PARAM_OUTPUT_FORMAT, query + .fetchOutput(projectType) + .orElseThrow(() -> new IllegalStateException("Missing output for " + projectType)) + .getOutputFormat().name()}, + {ProjectManagerConst.EXPORTER_PARAM_TEMPLATE_ID, query + .fetchOutput(projectType) + .orElseThrow(() -> new IllegalStateException("Missing output for " + projectType)) + .getTemplateId()}, {ProjectManagerConst.EXPORTER_PARAM_QUERY_EXPIRATION_DATE, convertToString(projectBridgehead.getProject().getExpiresAt())} } ) @@ -287,26 +330,37 @@ private String generateExportQueryInBase64ForExporterRequest(ProjectBridgehead p return convertToBase64String(result); } - private String generateExportStatusInBase64ForExporterRequest(ProjectBridgehead projectBridgehead) { + private String generateExportStatusInBase64ForExporterRequest(ProjectBridgeheadAndType projectBridgeheadAndType) { Map result = Map.of( - ProjectManagerConst.EXPORTER_PARAM_QUERY_EXECUTION_ID, projectBridgehead.getExporterExecutionId() + ProjectManagerConst.EXPORTER_PARAM_QUERY_EXECUTION_ID, projectBridgeheadAndType.projectBridgehead() + .fetchExecution(projectBridgeheadAndType.projectType()) + .orElseThrow(() -> new IllegalStateException("Missing execution for " + projectBridgeheadAndType.projectType())) + .getExporterExecutionId() ); return convertToBase64String(result); } - private String generateExporterQueryInBase64ForExporterCreateQuery(ProjectBridgehead projectBridgehead) + private String generateExporterQueryInBase64ForExporterCreateQuery(ProjectBridgeheadAndType projectBridgeheadAndType) throws ExporterServiceException { + ProjectBridgehead projectBridgehead = projectBridgeheadAndType.projectBridgehead(); + ProjectType projectType = projectBridgeheadAndType.projectType(); Query query = projectBridgehead.getProject().getQuery(); Map result = Stream.of( new String[][]{ {ProjectManagerConst.EXPORTER_PARAM_QUERY, query.getQuery()}, {ProjectManagerConst.EXPORTER_PARAM_QUERY_FORMAT, query.getQueryFormat().name()}, - {ProjectManagerConst.EXPORTER_PARAM_QUERY_LABEL, fetchLabel(projectBridgehead)}, + {ProjectManagerConst.EXPORTER_PARAM_QUERY_LABEL, fetchLabel(projectBridgeheadAndType)}, {ProjectManagerConst.EXPORTER_PARAM_QUERY_DESCRIPTION, query.getDescription()}, {ProjectManagerConst.EXPORTER_PARAM_QUERY_CONTEXT, generateQueryContextForExporter(query.getContext(), projectBridgehead.getProject().getCode())}, {ProjectManagerConst.EXPORTER_PARAM_QUERY_CONTACT_ID, projectBridgehead.getProject().getCreatorEmail()}, - {ProjectManagerConst.EXPORTER_PARAM_DEFAULT_OUTPUT_FORMAT, query.getOutputFormat().name()}, - {ProjectManagerConst.EXPORTER_PARAM_DEFAULT_TEMPLATE_ID, query.getTemplateId()}, + {ProjectManagerConst.EXPORTER_PARAM_DEFAULT_OUTPUT_FORMAT, query + .fetchOutput(projectType) + .orElseThrow(() -> new IllegalStateException("Missing output for " + projectType)) + .getOutputFormat().name()}, + {ProjectManagerConst.EXPORTER_PARAM_DEFAULT_TEMPLATE_ID, query + .fetchOutput(projectType) + .orElseThrow(() -> new IllegalStateException("Missing output for " + projectType)) + .getTemplateId()}, {ProjectManagerConst.EXPORTER_PARAM_QUERY_EXPIRATION_DATE, convertToString(projectBridgehead.getProject().getExpiresAt())} } ) @@ -315,27 +369,39 @@ private String generateExporterQueryInBase64ForExporterCreateQuery(ProjectBridge return convertToBase64String(result); } - private String fetchLabel(@NotNull ProjectBridgehead projectBridgehead) { - String label = "[DRN: " + projectBridgehead.getProject().getCode() + "] " + projectBridgehead.getProject().getQuery().getLabel(); - return (projectBridgehead.getExporterDispatchCounter() == 0) ? label : label + " (Attempt: " + (projectBridgehead.getExporterDispatchCounter() + 1) + ")"; - } - - private BeamRequest generateFocusBody(ProjectBridgehead projectBridgehead, TaskType taskType) throws ExporterServiceException { + private String fetchLabel(@NotNull ProjectBridgeheadAndType projectBridgeheadAndType) { + ProjectBridgehead projectBridgehead = projectBridgeheadAndType.projectBridgehead(); + ProjectType projectType = projectBridgeheadAndType.projectType(); + String label = "[" + projectType.name() + "-" + + projectBridgehead.getProject().getCode().substring(0, 5) + "] " + + projectBridgehead.getProject().getQuery().getLabel(); + return (projectBridgehead + .fetchExecution(projectType) + .orElseThrow(() -> new IllegalStateException("Missing execution for " + projectType)) + .getExporterDispatchCounter() == 0) ? + label : + label + " (Attempt: " + (projectBridgehead + .fetchExecution(projectType) + .orElseThrow(() -> new IllegalStateException("Missing execution for " + projectType)) + .getExporterDispatchCounter() + 1) + ")"; + } + + private BeamRequest generateFocusBody(ProjectBridgeheadAndType projectBridgeheadAndType, TaskType taskType) throws ExporterServiceException { try { - return generateFocusQueryWithoutExceptionHandling(projectBridgehead, taskType); + return generateFocusQueryWithoutExceptionHandling(projectBridgeheadAndType, taskType); } catch (BeamServiceException e) { throw new ExporterServiceException(e); } } - private BeamRequest generateFocusQueryWithoutExceptionHandling(ProjectBridgehead projectBridgehead, TaskType taskType) throws BeamServiceException { + private BeamRequest generateFocusQueryWithoutExceptionHandling(ProjectBridgeheadAndType projectBridgeheadAndType, TaskType taskType) throws BeamServiceException { String exporterQueryInBase64 = switch (taskType) { - case CREATE -> generateExporterQueryInBase64ForExporterCreateQuery(projectBridgehead); - case EXECUTE -> generateExportQueryInBase64ForExporterRequest(projectBridgehead); - case STATUS -> generateExportStatusInBase64ForExporterRequest(projectBridgehead); + case CREATE -> generateExporterQueryInBase64ForExporterCreateQuery(projectBridgeheadAndType); + case EXECUTE -> generateExportQueryInBase64ForExporterRequest(projectBridgeheadAndType); + case STATUS -> generateExportStatusInBase64ForExporterRequest(projectBridgeheadAndType); default -> null; }; - return beamService.generateFocusBeamRequest(exporterQueryInBase64, taskType, projectBridgehead.getBridgehead()); + return beamService.generateFocusBeamRequest(exporterQueryInBase64, taskType, projectBridgeheadAndType.projectBridgehead().getBridgehead()); } private String convertToString(LocalDate date) { @@ -350,17 +416,15 @@ private String generateQueryContextForExporter(String queryContext, String proje return Base64Utils.encode(context); } - public Set getExporterTemplates(@NotNull ProjectType projectType) { - return switch (projectType) { - case EXPORT -> exportTemplates; - case DATASHIELD -> datashieldTemplates; - case RESEARCH_ENVIRONMENT -> researchEnvironmentTemplates; - }; - } - - private void resetProjectBridgeheadDataShield(ProjectBridgehead projectBridgehead) { - if (projectBridgehead.getProject().getType() == ProjectType.DATASHIELD) { - Optional projectBridgeheadInDataSHIELD = this.projectBridgeheadDataShieldRepository.findByProjectBridgehead(projectBridgehead); + private void resetProjectBridgeheadDataShield(ProjectBridgeheadAndType projectBridgeheadAndType) { + ProjectBridgehead projectBridgehead = projectBridgeheadAndType.projectBridgehead(); + ProjectType projectType = projectBridgeheadAndType.projectType(); + if (projectBridgehead.getProject() + .fetchOutput(projectType) + .orElseThrow(() -> new IllegalStateException("Missing output for " + projectType)) + .getProjectType() == ProjectType.DATASHIELD) { + Optional projectBridgeheadInDataSHIELD = + projectBridgeheadService.fetchDataShield(projectBridgehead); ProjectBridgeheadDataShield result; if (projectBridgeheadInDataSHIELD.isEmpty()) { result = new ProjectBridgeheadDataShield(); @@ -369,12 +433,14 @@ private void resetProjectBridgeheadDataShield(ProjectBridgehead projectBridgehea result = projectBridgeheadInDataSHIELD.get(); } result.setRemoved(false); - this.projectBridgeheadDataShieldRepository.save(result); + projectBridgeheadService.saveDataShield(result); } } - public Mono checkIfQueryIsAlreadySentOrExecuted(ProjectBridgehead projectBridgehead) { - Optional focusQuery = extractFocusQuery(projectBridgehead); + public Mono checkIfQueryIsAlreadySentOrExecuted(ProjectBridgeheadAndType projectBridgeheadAndType) { + Optional focusQuery = extractFocusQuery(projectBridgeheadAndType); + ProjectBridgehead projectBridgehead = projectBridgeheadAndType.projectBridgehead(); + ProjectType projectType = projectBridgeheadAndType.projectType(); if (focusQuery.isEmpty()) { throw new RuntimeException("Focus Query not found for project " + projectBridgehead.getProject().getCode() + " and bridgehead " + projectBridgehead.getBridgehead()); } @@ -388,73 +454,90 @@ public Mono checkIfQueryIsAlreadySentOrExecuted(ProjectBr .exchangeToMono(clientResponse -> { log.debug("Received HTTP GET response with status {}", clientResponse.statusCode()); if (clientResponse.statusCode().equals(HttpStatus.OK) || clientResponse.statusCode().equals(HttpStatus.PARTIAL_CONTENT)) { - Optional operationType = switch (projectBridgehead.getQueryState()) { + Optional operationType = switch (projectBridgehead + .fetchExecution(projectType) + .orElseThrow(() -> new IllegalStateException("Missing execution for " + projectType)) + .getQueryState()) { case SENDING -> Optional.of(OperationType.CHECK_SEND_QUERY); case SENDING_AND_EXECUTING -> Optional.of(OperationType.CHECK_SEND_AND_EXECUTE_QUERY); default -> Optional.empty(); }; operationType.ifPresent(type -> createBridgeheadNotification( - HttpStatus.OK, null, projectBridgehead, projectBridgehead.getExporterUser(), + HttpStatus.OK, projectBridgehead, projectBridgehead + .fetchExecution(projectType) + .orElseThrow(() -> new IllegalStateException("Missing execution for " + projectType)) + .getExporterUser(), type, "Checking if query is already sent or executed")); - return clientResponse.bodyToMono(BeamRequest[].class).filter(focusQueries -> focusQueries != null && focusQueries.length > 0).flatMap(newBeamRequest -> { - if (projectBridgehead.getQueryState() == QueryState.EXPORT_RUNNING_2) { + return clientResponse.bodyToMono(BeamRequest[].class).filter(focusQueries -> focusQueries.length > 0).flatMap(newBeamRequest -> { + if (projectBridgehead + .fetchExecution(projectType) + .orElseThrow(() -> new IllegalStateException("Missing execution for " + projectType)) + .getQueryState() == QueryState.EXPORT_RUNNING_2) { if (newBeamRequest[0].getBody() == null) { return Mono.empty(); } Optional decodedBody = Base64Utils.decodeIfNecessary(newBeamRequest[0].getBody()); if (decodedBody.isEmpty() || !decodedBody.get().contains("OK")) { if (decodedBody.isEmpty() || decodedBody.get().contains("ERROR")) { - modifyProjectBridgeheadState(projectBridgehead, QueryState.ERROR); + modifyProjectBridgeheadState(projectBridgeheadAndType, QueryState.ERROR); } else { - modifyProjectBridgeheadState(projectBridgehead, QueryState.EXPORT_RUNNING_1); + modifyProjectBridgeheadState(projectBridgeheadAndType, QueryState.EXPORT_RUNNING_1); } return Mono.empty(); } } - return Mono.just(new ExporterServiceResult(projectBridgehead, beamService.serializeFocusQuery(newBeamRequest[0]))); + return Mono.just(new ExporterServiceResult(projectBridgeheadAndType, beamService.serializeFocusQuery(newBeamRequest[0]))); }); } else { - log.error("Http Error " + clientResponse.statusCode() + " checking task " + extractTaskId(focusQuery.get()) + - " for project " + projectBridgehead.getProject().getCode() + " and bridgehead " + projectBridgehead.getBridgehead()); + log.error("Http Error {} checking task {} for project {} and bridgehead {}", + clientResponse.statusCode(), extractTaskId(focusQuery.get()), + projectBridgehead.getProject().getCode(), projectBridgehead.getBridgehead()); if (isQueryStateToBeChangedToError((HttpStatus) clientResponse.statusCode(), projectBridgehead)) { - modifyProjectBridgeheadState(projectBridgehead, QueryState.ERROR); + modifyProjectBridgeheadState(projectBridgeheadAndType, QueryState.ERROR); } return clientResponse.bodyToMono(String.class).flatMap(errorBody -> { - log.error("Error: {}", errorBody); + logError(errorBody); return Mono.error(new RuntimeException(errorBody)); }); } }); } - private void modifyProjectBridgeheadState(ProjectBridgehead projectBridgehead, QueryState newState) { - projectBridgehead.setQueryState(newState); - projectBridgehead.setModifiedAt(Instant.now()); - if (newState == QueryState.ERROR) { - projectBridgehead.setExporterDispatchCounter(projectBridgehead.getExporterDispatchCounter() + 1); - sendEmail(projectBridgehead, EmailTemplateType.ERROR_WHILE_SAVING_QUERY_IN_EXPORTER); - } - projectBridgeheadRepository.save(projectBridgehead); + private void modifyProjectBridgeheadState(ProjectBridgeheadAndType projectBridgeheadAndType, QueryState newState) { + ProjectBridgehead projectBridgehead = projectBridgeheadAndType.projectBridgehead(); + projectBridgehead.fetchExecution(projectBridgeheadAndType.projectType()).ifPresent(execution -> { + execution.setQueryState(newState); + execution.setModifiedAt(Instant.now()); + if (newState == QueryState.ERROR) { + execution.setExporterDispatchCounter(execution.getExporterDispatchCounter() + 1); + sendEmail(projectBridgehead, EmailTemplateType.ERROR_WHILE_SAVING_QUERY_IN_EXPORTER); + } + projectBridgeheadService.saveBridgehead(projectBridgehead); + }); } - private void sendEmail(ProjectBridgehead projectBridgehead, EmailTemplateType templateType) { - bridgeheadAdminUserRepository.findByBridgehead(projectBridgehead.getBridgehead()).forEach(bridgeheadAdmin -> { - emailService.sendEmail(bridgeheadAdmin.getEmail(), Optional.of(projectBridgehead.getProject().getCode()), Optional.of(projectBridgehead.getBridgehead()), ProjectRole.BRIDGEHEAD_ADMIN, templateType, emailKeyValuesFactory.newInstance().add(projectBridgehead)); - }); + private void sendEmail(ProjectBridgehead projectBridgehead, @SuppressWarnings("SameParameterValue") EmailTemplateType templateType) { + userService + .fetchBridgeheadAdmin(projectBridgehead) + .forEach(bridgeheadAdmin -> emailService.sendEmail( + bridgeheadAdmin.getEmail(), + Optional.of(projectBridgehead.getProject()), + Optional.of(projectBridgehead), + ProjectRole.BRIDGEHEAD_ADMIN, + templateType, + emailKeyValuesFactory.newInstance().add(projectBridgehead))); } private String extractTaskId(BeamRequest beamRequest) { return (beamRequest.getId() != null) ? beamRequest.getId() : beamRequest.getTask(); } - private Optional extractFocusQuery(ProjectBridgehead projectBridgehead) { - if (projectBridgehead.getExporterResponse() != null) { - BeamRequest[] focusQueries = beamService.deserializeFocusResponse(projectBridgehead.getExporterResponse()); - if (focusQueries != null && focusQueries.length > 0) { - return Optional.of(focusQueries[0]); - } - } - return Optional.empty(); + private Optional extractFocusQuery(ProjectBridgeheadAndType projectBridgeheadAndType) { + return projectBridgeheadAndType.projectBridgehead().fetchExecution(projectBridgeheadAndType.projectType()) + .map(ProjectBridgeheadExecution::getExporterResponse) + .map(beamService::deserializeFocusResponse) + .filter(focusQueries -> focusQueries.length > 0) + .map(focusQueries -> focusQueries[0]); } private boolean isQueryStateToBeChangedToError(HttpStatus httpStatus, ProjectBridgehead projectBridgehead) { diff --git a/src/main/java/de/samply/exporter/ExporterServiceResult.java b/src/main/java/de/samply/exporter/ExporterServiceResult.java index 2b28faba..f5416064 100644 --- a/src/main/java/de/samply/exporter/ExporterServiceResult.java +++ b/src/main/java/de/samply/exporter/ExporterServiceResult.java @@ -1,6 +1,4 @@ package de.samply.exporter; -import de.samply.db.model.ProjectBridgehead; - -public record ExporterServiceResult(ProjectBridgehead projectBridgehead, String result) { +public record ExporterServiceResult(ProjectBridgeheadAndType projectBridgeheadAndType, String result) { } diff --git a/src/main/java/de/samply/exporter/ProjectBridgeheadAndType.java b/src/main/java/de/samply/exporter/ProjectBridgeheadAndType.java new file mode 100644 index 00000000..d7b5e6f6 --- /dev/null +++ b/src/main/java/de/samply/exporter/ProjectBridgeheadAndType.java @@ -0,0 +1,14 @@ +package de.samply.exporter; + +import de.samply.db.model.ProjectBridgehead; +import de.samply.project.ProjectType; + +import java.util.stream.Stream; + +public record ProjectBridgeheadAndType(ProjectBridgehead projectBridgehead, ProjectType projectType) { + + public static Stream from(ProjectBridgehead projectBridgehead) { + return projectBridgehead.getExecutions().stream().map(execution -> + new ProjectBridgeheadAndType(projectBridgehead, execution.getQueryOutput().getProjectType())); + } +} diff --git a/src/main/java/de/samply/exporter/focus/RetryStrategy.java b/src/main/java/de/samply/exporter/focus/RetryStrategy.java index 163b7957..df491763 100644 --- a/src/main/java/de/samply/exporter/focus/RetryStrategy.java +++ b/src/main/java/de/samply/exporter/focus/RetryStrategy.java @@ -6,6 +6,7 @@ @Data public class RetryStrategy { + @SuppressWarnings("SpellCheckingInspection") @JsonProperty("backoff_millisecs") private Integer backoffInMilliseconds; @JsonProperty("max_tries") diff --git a/src/main/java/de/samply/form/DataType.java b/src/main/java/de/samply/form/DataType.java new file mode 100644 index 00000000..43501f13 --- /dev/null +++ b/src/main/java/de/samply/form/DataType.java @@ -0,0 +1,45 @@ +package de.samply.form; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; + +import java.time.Instant; +import java.time.LocalDate; + +@Getter +public enum DataType { + + INTEGER(Integer.class), + DOUBLE(Double.class), + BOOLEAN(Boolean.class), + STRING(String.class), + ENUM(String.class), + DATE(LocalDate.class), + TIMESTAMP(Instant.class); + + private static final ObjectMapper objectMapper = new ObjectMapper(); + private final Class clazz; + + DataType(Class clazz) { + this.clazz = clazz; + } + + public static DataType fromString(String type) { + for (DataType dataType : values()) { + if (dataType.name().equalsIgnoreCase(type)) { + return dataType; + } + } + throw new IllegalArgumentException("Unsupported dataType: " + type); + } + + public static Object convert(String dataType, String value) throws Exception { + DataType type = fromString(dataType); + return objectMapper.readValue(value, type.getClazz()); + } + + public Object convert(String value) throws Exception { + return objectMapper.readValue(value, this.getClazz()); + } + +} diff --git a/src/main/java/de/samply/form/DisplayMetadata.java b/src/main/java/de/samply/form/DisplayMetadata.java new file mode 100644 index 00000000..4f92b9b5 --- /dev/null +++ b/src/main/java/de/samply/form/DisplayMetadata.java @@ -0,0 +1,71 @@ +package de.samply.form; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.samply.utils.LanguageUtils; +import jakarta.annotation.PostConstruct; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Represents metadata that provides localized display names and descriptions + * for various entities, such as forms or groups. + * + *

This class ensures that all language codes are stored in lowercase, + * even after deserialization.

+ * + * @author Your Name + * @version 1.0 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@SuperBuilder(toBuilder = true) +public class DisplayMetadata { + + @JsonProperty("display_name") + private Map displayName = new HashMap<>(); + + @JsonProperty("description") + private Map description = new HashMap<>(); + + /** + * Ensures that language keys in displayName and description are stored in lowercase. + * This is executed automatically after object creation or deserialization. + */ + @PostConstruct + private void normalizeLanguageKeys() { + if (displayName != null) { + displayName = displayName.entrySet().stream() + .collect(Collectors.toMap( + entry -> LanguageUtils.normalize(entry.getKey()), + Map.Entry::getValue, + (existing, _) -> existing, + HashMap::new + )); + } + + if (description != null) { + description = description.entrySet().stream() + .collect(Collectors.toMap( + entry -> LanguageUtils.normalize(entry.getKey()), + Map.Entry::getValue, + (existing, _) -> existing, + HashMap::new + )); + } + } + + public DisplayMetadata fetchDisplayMetadata() { + DisplayMetadata result = new DisplayMetadata(); + result.setDisplayName(displayName); + result.setDescription(description); + return result; + } + +} diff --git a/src/main/java/de/samply/form/DtoFormService.java b/src/main/java/de/samply/form/DtoFormService.java new file mode 100644 index 00000000..02a13263 --- /dev/null +++ b/src/main/java/de/samply/form/DtoFormService.java @@ -0,0 +1,129 @@ +package de.samply.form; + +import de.samply.db.model.Project; +import de.samply.frontend.dto.DtoFactory; +import de.samply.frontend.dto.Form; +import de.samply.frontend.dto.FormField; +import de.samply.project.DtoProjectService; +import de.samply.utils.FormFieldUtils; +import jakarta.validation.constraints.NotNull; +import org.springframework.stereotype.Service; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Service +public class DtoFormService { + + // Services + private final FormService formService; + private final DtoProjectService dtoProjectService; + + private final DtoFactory dtoFactory; + private final FormConfig formConfig; + + + public DtoFormService( + FormService formService, + DtoFactory dtoFactory, + FormConfig formConfig, + DtoProjectService dtoProjectService) { + this.formService = formService; + this.dtoFactory = dtoFactory; + this.formConfig = formConfig; + this.dtoProjectService = dtoProjectService; + } + + public List fetchProjectFormTitles(Optional language) { + return formConfig + .getFormTitleLabelFieldMap() + .keySet() + .stream() + .map(title -> dtoFactory.convert( + title, + Optional.empty(), + Optional.empty(), + language + )) + .collect(Collectors.toList()); + } + + public List fetchProjectFormFieldsDefinedInConfigWithoutValues(@NotNull String formTitle, Optional language) { + return formConfig + .getFormTitleLabelFieldMap() + .getOrDefault(formTitle, Map.of()) + .values() + .stream() + .filter(field -> !field.isArchived()) + .map(field -> + dtoFactory.convert(formTitle, field, Optional.empty(), language)) + .toList(); + } + + public List fetchProjectFormFieldsWithValues(@NotNull String formTitle, @NotNull Project project, Optional language) { + return formService + .fetchProjectFormFieldsWithValues(formTitle, project) + .stream() + .map(projectFormField -> dtoFactory.convert(projectFormField, language)) + .toList(); + } + + public List
fetchSelectedForms(@NotNull Project project, Optional language) { + return Stream.concat( + // Fetch forms selected explicitly + formService.fetchSelectedForms(project) + .stream() + .map(projectForm -> dtoFactory.convert(projectForm, language)), + // Fetch forms that should be selected according to the current configuration. + // This is particularly important if the current configuration is CUSTOM + dtoProjectService.fetchCurrentProjectConfiguration(project) + .values() + .stream() + .flatMap(projectAndForms -> Arrays.stream(projectAndForms.forms())) + ) + // Remove duplicates + .collect(Collectors.toMap( + Form::title, + Function.identity(), + (existing, _) -> existing + )) + .values() + .stream() + .toList(); + } + + public Stream fetchBaseAndOverrideFormFields(@NotNull String formTitle, @NotNull Project project, Optional language + ) { + return Stream.concat( + fetchProjectFormFieldsDefinedInConfigWithoutValues(formTitle, language).stream(), + fetchProjectFormFieldsWithValues(formTitle, project, language).stream() + ); + } + + public List fetchProjectFormFields(Optional formTitle, @NotNull Project project, Optional language) { + return formTitle + .map(Stream::of) + .orElseGet(() -> formConfig.getFormTitleLabelFieldMap().keySet().stream()) + .flatMap(title -> fetchBaseAndOverrideFormFields(title, project, language)) + .sorted(FormFieldUtils.FORM_FIELD_COMPARATOR) + .collect(FormFieldUtils.formFieldMapCollector()) + .values() + .stream() + .filter(field -> !isArchivedInConfig(field.title(), field.label())) + .toList(); + } + + private boolean isArchivedInConfig(String title, String label) { + return Optional.ofNullable(formConfig.getFormTitleLabelFieldMap().get(title)) + .map(m -> m.get(label)) + .map(FormFieldConfig::isArchived) + .orElse(false); + } + + +} diff --git a/src/main/java/de/samply/form/FormConfig.java b/src/main/java/de/samply/form/FormConfig.java new file mode 100644 index 00000000..51732177 --- /dev/null +++ b/src/main/java/de/samply/form/FormConfig.java @@ -0,0 +1,94 @@ +package de.samply.form; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.samply.app.ProjectManagerConst; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; + +@Slf4j +@Configuration +@Getter +public class FormConfig { + + private final Map formTitleDisplaMetadataMap = new HashMap<>(); + private final Map groupsDisplayMetadataMap = new HashMap<>(); + private final Map> formTitleLabelFieldMap = new HashMap<>(); + private final Map> formTitleLabelOrderMap = new HashMap<>(); + + public FormConfig(@Value(ProjectManagerConst.FORM_FIELDS_DIRECTORY_SV) Path configDir + ) { + ObjectMapper objectMapper = new ObjectMapper(); + + if (!Files.isDirectory(configDir)) { + log.warn("Form config directory does not exist or is not a directory: {}", configDir); + return; + } + + try (Stream files = Files.list(configDir)) { + files + .filter(path -> path.toString().endsWith(".json")) + .sorted() // deterministic order + .forEach(path -> loadConfigFile(path, objectMapper)); + } catch (IOException e) { + log.error("Failed to read form config directory {}", configDir, e); + } + } + + private void loadConfigFile(Path configFile, ObjectMapper objectMapper) { + try { + FormMetadataConfig formMetadataConfig = + objectMapper.readValue(configFile.toFile(), FormMetadataConfig.class); + + // Title metadata + formTitleDisplaMetadataMap.put( + formMetadataConfig.getTitle(), + formMetadataConfig.fetchDisplayMetadata() + ); + + // Group metadata + if (formMetadataConfig.getGroups() != null) { + groupsDisplayMetadataMap.putAll(formMetadataConfig.getGroups()); + } + + // Fields + order + Map fieldMap = + formTitleLabelFieldMap.computeIfAbsent( + formMetadataConfig.getTitle(), + _ -> new HashMap<>() + ); + + Map orderMap = + formTitleLabelOrderMap.computeIfAbsent( + formMetadataConfig.getTitle(), + _ -> new HashMap<>() + ); + + AtomicInteger counter = new AtomicInteger(1); + Arrays.stream(formMetadataConfig.getFields()).forEach(field -> { + fieldMap.put(field.getLabel(), field); + orderMap.put(field.getLabel(), counter.getAndIncrement()); + }); + + log.info( + "Loaded {} form fields from {}", + formMetadataConfig.getFields().length, + configFile.getFileName() + ); + + } catch (IOException e) { + log.error("Failed to read form config file {}", configFile, e); + } + } + +} diff --git a/src/main/java/de/samply/form/FormFieldConfig.java b/src/main/java/de/samply/form/FormFieldConfig.java new file mode 100644 index 00000000..a77a6876 --- /dev/null +++ b/src/main/java/de/samply/form/FormFieldConfig.java @@ -0,0 +1,39 @@ +package de.samply.form; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@EqualsAndHashCode(callSuper = true) +@Data +@NoArgsConstructor +@AllArgsConstructor +@SuperBuilder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class FormFieldConfig extends DisplayMetadata { + + private String label; + + @JsonProperty("data_type") + private DataType dataType; + + @JsonProperty("allowed_values") + private FormFieldValue[] allowedValues; + + private boolean mandatory; + + private String[] groups; + + // This field is intended for project values to be displayed as form fields in a form. + // e.g. "${project-code}": This will be replaced with the value of the project code. + @JsonProperty("value") + private String projectValue; + + // When true, the field is hidden from the UI even if a value exists in the database. + private boolean archived; + +} diff --git a/src/main/java/de/samply/form/FormFieldValue.java b/src/main/java/de/samply/form/FormFieldValue.java new file mode 100644 index 00000000..c108b516 --- /dev/null +++ b/src/main/java/de/samply/form/FormFieldValue.java @@ -0,0 +1,20 @@ +package de.samply.form; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@EqualsAndHashCode(callSuper = true) +@Data +@NoArgsConstructor +@AllArgsConstructor +@SuperBuilder(toBuilder = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class FormFieldValue extends DisplayMetadata { + + private String label; + +} diff --git a/src/main/java/de/samply/form/FormMetadataConfig.java b/src/main/java/de/samply/form/FormMetadataConfig.java new file mode 100644 index 00000000..5b5227b5 --- /dev/null +++ b/src/main/java/de/samply/form/FormMetadataConfig.java @@ -0,0 +1,21 @@ +package de.samply.form; + + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.util.Map; + +@EqualsAndHashCode(callSuper = true) +@Data +@NoArgsConstructor +@AllArgsConstructor +public class FormMetadataConfig extends DisplayMetadata{ + + private String title; + private Map groups; + private FormFieldConfig[] fields; + +} diff --git a/src/main/java/de/samply/form/FormService.java b/src/main/java/de/samply/form/FormService.java new file mode 100644 index 00000000..b89db595 --- /dev/null +++ b/src/main/java/de/samply/form/FormService.java @@ -0,0 +1,185 @@ +package de.samply.form; + +import de.samply.db.model.Project; +import de.samply.db.model.ProjectForm; +import de.samply.db.model.ProjectFormField; +import de.samply.db.repository.ProjectFormFieldRepository; +import de.samply.db.repository.ProjectFormRepository; +import de.samply.frontend.dto.FormField; +import de.samply.notification.NotificationService; +import de.samply.notification.OperationType; +import de.samply.security.SessionUser; +import jakarta.validation.constraints.NotNull; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Instant; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Service class responsible for handling operations related to forms and their associated configurations, + * labels, and values within a project context. + *

+ * This class provides methods to fetch form titles, retrieve form field configurations, and manage label-value + * pairs for specific project forms. It uses repositories and services to interact with underlying + * storage and notification systems. + */ +@Service +public class FormService { + + private final FormConfig formConfig; + private final ProjectFormFieldRepository projectFormFieldRepository; + private final ProjectFormRepository projectFormRepository; + private final NotificationService notificationService; + private final SessionUser sessionUser; + + + public FormService(FormConfig formConfig, + ProjectFormFieldRepository projectFormFieldRepository, + ProjectFormRepository projectFormRepository, + NotificationService notificationService, + SessionUser sessionUser) { + this.formConfig = formConfig; + this.projectFormFieldRepository = projectFormFieldRepository; + this.projectFormRepository = projectFormRepository; + this.notificationService = notificationService; + this.sessionUser = sessionUser; + } + + protected List fetchProjectFormFieldsWithValues(@NotNull String formTitle, @NotNull Project project) { + return projectFormFieldRepository.findByProjectAndFormTitle(project, formTitle); + } + + @Transactional + public void editProjectFormFieldValues(Optional formFields, @NotNull Project project) { + if (formFields.isEmpty() || formFields.get().length == 0) { + return; + } + Map labelFormMap = projectFormFieldRepository.findByProject(project).stream() + .collect(Collectors.toMap(ProjectFormField::getLabel, Function.identity())); + Arrays.stream(formFields.get()).forEach(formField -> { + ProjectFormField projectFormField = labelFormMap.get(formField.label()); + boolean isModified = false; + String details = "title: " + formField.title() + "label: " + formField.label() + " - value: " + formField.value(); + if (projectFormField == null) { + projectFormField = new ProjectFormField(); + projectFormField.setLabel(formField.label()); + projectFormField.setFormTitle(formField.title()); + projectFormField.setValue(formField.value()); + projectFormField.setProject(project); + isModified = true; + notificationService.createNotification(project, null, + sessionUser.getEmail(), OperationType.ADD_PROJECT_FORM_LABEL, + details, null, null); + } else { + if (!projectFormField.getValue().equals(formField.value())) { + projectFormField.setValue(formField.value()); + notificationService.createNotification(project, null, + sessionUser.getEmail(), OperationType.EDIT_PROJECT_FORM_LABEL, + details, null, null); + isModified = true; + } + } + if (isModified) { + projectFormField.setModifiedAt(Instant.now()); + projectFormFieldRepository.save(projectFormField); + } + }); + } + + protected List fetchSelectedForms(@NotNull Project project) { + return projectFormRepository.findByProject(project); + } + + @Transactional + public void addSelectedForm(@NotNull Project project, @NotNull String formTitle) { + projectFormRepository + .findByProjectAndFormTitle(project, formTitle) + .ifPresentOrElse( + _ -> { /* already exists → do nothing */ }, + () -> { + if (!formConfig.getFormTitleLabelFieldMap().containsKey(formTitle)) { + throw new IllegalArgumentException("Form title not found: " + formTitle); + } + + ProjectForm projectForm = new ProjectForm(); + projectForm.setFormTitle(formTitle); + projectForm.setProject(project); + projectForm.setCreatedAt(Instant.now()); + + projectFormRepository.save(projectForm); + } + ); + } + + @Transactional + public void removeSelectedForm(@NotNull Project project, @NotNull String formTitle) { + projectFormRepository + .findByProjectAndFormTitle(project, formTitle) + .ifPresent(projectForm -> { + if (!formConfig.getFormTitleLabelFieldMap().containsKey(formTitle)) { + throw new IllegalArgumentException("Form title not found: " + formTitle); + } + this.projectFormRepository.delete(projectForm); + } + ); + } + + @Transactional + public void syncSelectedForms(@NotNull Project project, + @NotNull Collection formTitles) { + + // Validate input titles first + Set allowedTitles = formConfig.getFormTitleLabelFieldMap().keySet(); + + for (String title : formTitles) { + if (!allowedTitles.contains(title)) { + throw new IllegalArgumentException("Form title not found: " + title); + } + } + + // Load existing forms from DB + List existingForms = + projectFormRepository.findByProject(project); + + Set existingTitles = existingForms.stream() + .map(ProjectForm::getFormTitle) + .collect(Collectors.toSet()); + + Set newTitles = new HashSet<>(formTitles); + + // Determine what to ADD + Set toAdd = new HashSet<>(newTitles); + toAdd.removeAll(existingTitles); + + // Determine what to REMOVE + Set toRemove = new HashSet<>(existingTitles); + toRemove.removeAll(newTitles); + + // Add missing forms + for (String title : toAdd) { + ProjectForm projectForm = new ProjectForm(); + projectForm.setProject(project); + projectForm.setFormTitle(title); + projectForm.setCreatedAt(Instant.now()); + projectFormRepository.save(projectForm); + } + + // Remove obsolete forms + existingForms.stream() + .filter(pf -> toRemove.contains(pf.getFormTitle())) + .forEach(projectFormRepository::delete); + } + + public List fetchProjectForms(Project project) { + return projectFormRepository.findByProject(project); + } + + public List fetchProjectFormFields(Project project) { + return projectFormFieldRepository.findByProject(project); + } + + +} diff --git a/src/main/java/de/samply/form/FormServiceException.java b/src/main/java/de/samply/form/FormServiceException.java new file mode 100644 index 00000000..4fa6651b --- /dev/null +++ b/src/main/java/de/samply/form/FormServiceException.java @@ -0,0 +1,9 @@ +package de.samply.form; + +public class FormServiceException extends RuntimeException { + + public FormServiceException(String message) { + super(message); + } + +} diff --git a/src/main/java/de/samply/form/pdf/FormPdfConverter.java b/src/main/java/de/samply/form/pdf/FormPdfConverter.java new file mode 100644 index 00000000..e6223bf7 --- /dev/null +++ b/src/main/java/de/samply/form/pdf/FormPdfConverter.java @@ -0,0 +1,46 @@ +package de.samply.form.pdf; + +import com.openhtmltopdf.pdfboxout.PdfRendererBuilder; +import de.samply.app.ProjectManagerConst; +import de.samply.pdf.PdfConverter; +import de.samply.pdf.PdfConverterException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Paths; + +@Component +public class FormPdfConverter implements PdfConverter { + + private final String resourcesBaseUri; + + public FormPdfConverter(@Value(ProjectManagerConst.FORM_RESOURCES_DIRECTORY_SV) String resourcesPath) { + this.resourcesBaseUri = Paths.get(resourcesPath).toUri().toString(); + } + + @Override + public byte[] convert(String content) throws PdfConverterException { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + convertContentToPdfAndRenderInOutputStream(content, outputStream); + return outputStream.toByteArray(); + } catch (IOException e) { + throw new PdfConverterException(e); + } + } + + private void convertContentToPdfAndRenderInOutputStream(String content, OutputStream outputStream) throws PdfConverterException { + try { + new PdfRendererBuilder() + .withHtmlContent(content, resourcesBaseUri) + .toStream(outputStream) + .run(); + } catch (IOException e) { + throw new PdfConverterException(e); + } + + } + +} diff --git a/src/main/java/de/samply/form/pdf/FormPdfGeneratorFactory.java b/src/main/java/de/samply/form/pdf/FormPdfGeneratorFactory.java new file mode 100644 index 00000000..5d2765b8 --- /dev/null +++ b/src/main/java/de/samply/form/pdf/FormPdfGeneratorFactory.java @@ -0,0 +1,51 @@ +package de.samply.form.pdf; + +import de.samply.app.ProjectManagerConst; +import de.samply.pdf.PdfGenerator; +import de.samply.utils.DirectoryUtils; +import de.samply.utils.FileExtension; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.spring6.SpringTemplateEngine; +import org.thymeleaf.templatemode.TemplateMode; +import org.thymeleaf.templateresolver.FileTemplateResolver; + +import java.io.FileNotFoundException; +import java.nio.charset.StandardCharsets; + +@Component +public class FormPdfGeneratorFactory { + + private final FormPdfConverter formPdfConverter; + private final TemplateEngine templateEngine; + + public FormPdfGeneratorFactory(FormPdfConverter formPdfConverter, + @Value(ProjectManagerConst.FORM_RESOURCES_DIRECTORY_SV) String externalTemplateDirectory) throws FileNotFoundException { + this.formPdfConverter = formPdfConverter; + this.templateEngine = createTemplateEngine( + DirectoryUtils.fetchExternalTemplateDirectory(externalTemplateDirectory)); + } + + public PdfGenerator createPdfGenerator() { + return new PdfGenerator(templateEngine, formPdfConverter); + } + + private TemplateEngine createTemplateEngine(String externalTemplateDirectory) { + SpringTemplateEngine templateEngine = new SpringTemplateEngine(); + templateEngine.addTemplateResolver(createFileTemplateResolver(externalTemplateDirectory)); + return templateEngine; + } + + private FileTemplateResolver createFileTemplateResolver(String externalTemplateDirectory) { + FileTemplateResolver resolver = new FileTemplateResolver(); + resolver.setPrefix(externalTemplateDirectory); + resolver.setSuffix("." + FileExtension.HTML.value()); + resolver.setTemplateMode(TemplateMode.HTML); + resolver.setCharacterEncoding(StandardCharsets.UTF_8.name()); + resolver.setCacheable(true); + return resolver; + } + + +} diff --git a/src/main/java/de/samply/form/pdf/FormTemplateServiceException.java b/src/main/java/de/samply/form/pdf/FormTemplateServiceException.java new file mode 100644 index 00000000..e45339c7 --- /dev/null +++ b/src/main/java/de/samply/form/pdf/FormTemplateServiceException.java @@ -0,0 +1,9 @@ +package de.samply.form.pdf; + +public class FormTemplateServiceException extends RuntimeException { + + public FormTemplateServiceException(Throwable cause) { + super(cause); + } + +} diff --git a/src/main/java/de/samply/form/template/FormContextKey.java b/src/main/java/de/samply/form/template/FormContextKey.java new file mode 100644 index 00000000..b8591432 --- /dev/null +++ b/src/main/java/de/samply/form/template/FormContextKey.java @@ -0,0 +1,18 @@ +package de.samply.form.template; + +import lombok.Getter; + +@Getter +public enum FormContextKey { + + FIELDS("fields"), + DATA_TYPE_CLASS("DataType"), + CURRENT_DATE("currentDate"); + + private final String text; + + FormContextKey(String text) { + this.text = text; + } + +} diff --git a/src/main/java/de/samply/form/template/FormFieldConfigDeserializer.java b/src/main/java/de/samply/form/template/FormFieldConfigDeserializer.java new file mode 100644 index 00000000..be8e6dee --- /dev/null +++ b/src/main/java/de/samply/form/template/FormFieldConfigDeserializer.java @@ -0,0 +1,42 @@ +package de.samply.form.template; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import de.samply.form.FormFieldConfig; + +import java.io.IOException; +import java.util.UUID; + +public class FormFieldConfigDeserializer extends JsonDeserializer { + + @Override + public FormFieldConfig[] deserialize(JsonParser p, DeserializationContext context) throws IOException { + + JsonNode node = p.getCodec().readTree(p); + + if (!node.isArray()) { + throw new IOException("Expected an array for projectFields"); + } + + FormFieldConfig[] fields = new FormFieldConfig[node.size()]; + + for (int i = 0; i < node.size(); i++) { + JsonNode fieldNode = node.get(i); + + // Use ObjectMapper to deserialize the individual object + FormFieldConfig field = p.getCodec().treeToValue(fieldNode, FormFieldConfig.class); + + // Generate a UUID if the label is missing or empty + if (field.getLabel() == null || field.getLabel().isBlank()) { + field.setLabel(UUID.randomUUID().toString()); + } + + fields[i] = field; + } + + return fields; + } + +} diff --git a/src/main/java/de/samply/form/template/FormFilenameKey.java b/src/main/java/de/samply/form/template/FormFilenameKey.java new file mode 100644 index 00000000..3c21ec8a --- /dev/null +++ b/src/main/java/de/samply/form/template/FormFilenameKey.java @@ -0,0 +1,50 @@ +package de.samply.form.template; + +import de.samply.app.ProjectManagerConst; +import de.samply.utils.DateUtils; +import lombok.Getter; + +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Getter +public enum FormFilenameKey { + + PROJECT_CODE("project-code"), + TIMESTAMP("timestamp") { + @Override + public Optional fetchValue(Map context) { + return Optional.of(DateUtils.fetchCurrentDate(ProjectManagerConst.FORM_FILENAME_TIMESTAMP_FORMAT)); + } + }; + + private final String text; + + FormFilenameKey(String text) { + this.text = text; + } + + private static final Map BY_TEXT = + Arrays.stream(values()) + .collect(Collectors.toUnmodifiableMap( + FormFilenameKey::getText, + Function.identity() + )); + + public static Optional fromText(String text) { + return Optional.ofNullable(BY_TEXT.get(text)); + } + + public Optional fetchValue(Map context) { + return Optional.ofNullable(context != null ? context.get(text) : null); + } + + public static Optional fetchValue(String key, Map context) { + return fromText(key).flatMap(k -> k.fetchValue(context)); + } + + +} diff --git a/src/main/java/de/samply/form/template/FormFilenameResolver.java b/src/main/java/de/samply/form/template/FormFilenameResolver.java new file mode 100644 index 00000000..9b944922 --- /dev/null +++ b/src/main/java/de/samply/form/template/FormFilenameResolver.java @@ -0,0 +1,45 @@ +package de.samply.form.template; + +import jakarta.validation.constraints.NotNull; + +import java.util.Map; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class FormFilenameResolver { + + private static final Pattern VARIABLE_PATTERN = + Pattern.compile("\\$\\{([^}]+)}"); + + /** + * Resolves a filename template using the given context. + * Only variables defined in FormFilenameKey are allowed. + * Missing variables are replaced with an empty string. + * + * @param template The filename template, e.g. "samples-${project-code}-${timestamp}.pdf" + * @param context The variable context map, e.g. Map.of("project-code", "ABC123") + * @return The resolved filename + */ + public static String resolve(@NotNull String template, @NotNull Map context) { + Objects.requireNonNull(template, "template must not be null"); + Objects.requireNonNull(context, "context must not be null"); + + Matcher matcher = VARIABLE_PATTERN.matcher(template); + StringBuilder result = new StringBuilder(); + + while (matcher.find()) { + String key = matcher.group(1); + + // Fetch the value from the enum + context, or "" if missing + String replacement = FormFilenameKey.fetchValue(key, context) + .orElse(""); + + matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); + } + + matcher.appendTail(result); + return result.toString(); + } + +} diff --git a/src/main/java/de/samply/form/template/FormTemplateConfig.java b/src/main/java/de/samply/form/template/FormTemplateConfig.java new file mode 100644 index 00000000..0ffb6300 --- /dev/null +++ b/src/main/java/de/samply/form/template/FormTemplateConfig.java @@ -0,0 +1,166 @@ +package de.samply.form.template; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.samply.app.ProjectManagerConst; +import de.samply.form.FormFieldConfig; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Configuration +public class FormTemplateConfig { + + @Getter + private final Map templateMetadataMap; + private final Map> templateLabelOrderMap; + private final String defaultLanguage; + + public FormTemplateConfig( + @Value(ProjectManagerConst.FORM_TEMPLATE_METADATA_DIRECTORY_SV) Path templatesDir, + @Value(ProjectManagerConst.DEFAULT_LANGUAGE_SV) String defaultLanguage + ) { + this.defaultLanguage = defaultLanguage; + this.templateMetadataMap = loadTemplates(new ObjectMapper(), templatesDir); + this.templateLabelOrderMap = loadTemplateLabelOrderMap(templateMetadataMap); + } + + public Optional getTemplate(String key) { + return Optional.ofNullable(templateMetadataMap.get(key)); + } + + public boolean isProjectFormFieldTitle(String formFieldTitle) { + return formFieldTitle.startsWith(ProjectManagerConst.FORM_PROJECT_FIELDS_TITLE_PREFIX); + } + + public String fetchProjectFormFieldTitle(String formTemplate) { + return ProjectManagerConst.FORM_PROJECT_FIELDS_TITLE_PREFIX + formTemplate; + } + + public int fetchProjectFormFieldOrder(String projectFormFieldTitle, String formFieldLabel) { + String prefix = ProjectManagerConst.FORM_PROJECT_FIELDS_TITLE_PREFIX; + + if (!isProjectFormFieldTitle(projectFormFieldTitle)) { + throw new IllegalArgumentException( + "Invalid formFieldTitle: must start with " + prefix + ", got: " + projectFormFieldTitle); + } + + String template = projectFormFieldTitle.substring(prefix.length()); + + Map labelOrder = templateLabelOrderMap.get(template); + if (labelOrder == null) { + throw new IllegalArgumentException("No template found for name: " + template); + } + + Integer priority = labelOrder.get(formFieldLabel); + if (priority == null) { + throw new IllegalArgumentException( + "Label '" + formFieldLabel + "' not found in template '" + template + "'"); + } + + return priority; + } + + private Map loadTemplates( + ObjectMapper objectMapper, + Path templatesDir + ) { + if (!Files.isDirectory(templatesDir)) { + throw new IllegalStateException( + "Form templates directory does not exist or is not a directory: " + templatesDir + ); + } + try (Stream paths = Files.list(templatesDir)) { + return loadTemplates(objectMapper, paths); + + } catch (IOException e) { + throw new IllegalStateException( + "Failed to load form templates from directory: " + templatesDir, e + ); + } + } + + private Map> loadTemplateLabelOrderMap(Map templateMetadataMap) { + return templateMetadataMap.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, // template name + entry -> { + FormFieldConfig[] fields = entry.getValue().getProjectFields(); + if (fields == null || fields.length == 0) { + return Map.of(); // empty map if no fields + } + Map labelOrder = new HashMap<>(); + for (int i = 0; i < fields.length; i++) { + // last element = -1, first element = length + labelOrder.put(fields[i].getLabel(), i - fields.length); + } + return labelOrder; + } + )); + } + + private Map loadTemplates(ObjectMapper objectMapper, Stream paths) { + Map result = new HashMap<>(); + paths + .filter(p -> p.toString().endsWith(".json")) + .forEach(path -> loadTemplates(objectMapper, result, path)); + return result; + } + + private void loadTemplates(ObjectMapper objectMapper, Map result, Path path) { + try (InputStream is = Files.newInputStream(path)) { + FormTemplateMetadata metadata = + objectMapper.readValue(is, FormTemplateMetadata.class); + result.put(metadata.getTemplate(), metadata); + + } catch (IOException e) { + throw new UncheckedIOException( + "Failed to read form template: " + path, e + ); + } + } + + public Optional fetchFormVariable(String template, String formVariable, String language) { + return Optional.ofNullable(templateMetadataMap.get(template)) + .map(FormTemplateMetadata::getVariableLanguageValueMap) + .map(m -> m.get(formVariable)) + .map(translations -> + translations.getOrDefault(language, translations.get(defaultLanguage)) + ); + } + + public Map fetchAllFormVariables(@NotNull String template, @NotNull String language) { + return Optional.ofNullable(templateMetadataMap.get(template)) + .stream() + .flatMap(metadata -> + metadata.getVariableLanguageValueMap().keySet().stream() + .flatMap(key -> + fetchFormVariable(template, key, language) + .stream() + .map(value -> Map.entry(key, value)) + ) + ) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue + )); + } + + public Optional fetchTemplateFile(String formTemplate){ + return Optional.ofNullable(templateMetadataMap.get(formTemplate)) + .map(FormTemplateMetadata::getTemplateFile); + } + + +} \ No newline at end of file diff --git a/src/main/java/de/samply/form/template/FormTemplateMetadata.java b/src/main/java/de/samply/form/template/FormTemplateMetadata.java new file mode 100644 index 00000000..1aa281cc --- /dev/null +++ b/src/main/java/de/samply/form/template/FormTemplateMetadata.java @@ -0,0 +1,40 @@ +package de.samply.form.template; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import de.samply.form.FormFieldConfig; +import de.samply.utils.FileExtension; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class FormTemplateMetadata { + + private String template; + @JsonProperty("template_file") + // Input template file + // Without extension: Currently, only HTML files are supported + private String templateFile; + @JsonProperty("form_titles") + private String[] formTitles; + // Output filenames + @JsonProperty("filename_templates") + private Map extensionFilenameTemplateMap; + @JsonProperty("variables") + @JsonDeserialize(using = VariableLanguageValueMapDeserializer.class) + private Map> variableLanguageValueMap; + @JsonProperty("display_name") + private Map languageDisplayNameMap; + @JsonProperty("project_fields") + @JsonDeserialize(using = FormFieldConfigDeserializer.class) + private FormFieldConfig[] projectFields; + + +} diff --git a/src/main/java/de/samply/form/template/FormTemplateService.java b/src/main/java/de/samply/form/template/FormTemplateService.java new file mode 100644 index 00000000..8194297f --- /dev/null +++ b/src/main/java/de/samply/form/template/FormTemplateService.java @@ -0,0 +1,179 @@ +package de.samply.form.template; + +import de.samply.app.ProjectManagerConst; +import de.samply.db.model.Project; +import de.samply.form.DataType; +import de.samply.form.DtoFormService; +import de.samply.form.pdf.FormPdfGeneratorFactory; +import de.samply.form.pdf.FormTemplateServiceException; +import de.samply.frontend.dto.DtoFactory; +import de.samply.frontend.dto.Form; +import de.samply.frontend.dto.FormField; +import de.samply.frontend.dto.FormTemplate; +import de.samply.pdf.PdfGenerator; +import de.samply.pdf.PdfGeneratorException; +import de.samply.utils.DateUtils; +import de.samply.utils.FileExtension; +import de.samply.utils.FormFieldUtils; +import de.samply.utils.LanguageUtils; +import jakarta.validation.constraints.NotNull; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Service +public class FormTemplateService { + + private final DtoFormService dtoFormService; + private final PdfGenerator pdfGenerator; + private final String defaultLanguage; + private final FormTemplateConfig formTemplateConfig; + private final String defaultPdfFilename; + private final DtoFactory dtoFactory; + private final String datePattern; + private final ProjectContextFactory projectContextFactory; + + + public FormTemplateService(DtoFormService dtoFormService, + FormPdfGeneratorFactory pdfGeneratorFactory, + @Value(ProjectManagerConst.DEFAULT_LANGUAGE_SV) String defaultLanguage, + @Value(ProjectManagerConst.FORM_TEMPLATE_DEFAULT_PDF_FILENAME_SV) String defaultPdfFilename, + FormTemplateConfig formTemplateConfig, + DtoFactory dtoFactory, + @Value(ProjectManagerConst.FORM_TEMPLATE_DATE_PATTERN_SV) String datePattern, + ProjectContextFactory projectContextFactory) { + this.dtoFormService = dtoFormService; + this.pdfGenerator = pdfGeneratorFactory.createPdfGenerator(); + this.defaultLanguage = defaultLanguage; + this.formTemplateConfig = formTemplateConfig; + this.defaultPdfFilename = defaultPdfFilename; + this.dtoFactory = dtoFactory; + this.datePattern = datePattern; + this.projectContextFactory = projectContextFactory; + } + + public String fetchFormFilename(@NotNull Project project, String formTemplate) { + return FormFilenameResolver.resolve( + formTemplateConfig.getTemplate(formTemplate) + .map(metadata -> metadata.getExtensionFilenameTemplateMap().get(FileExtension.PDF)) + .orElse(defaultPdfFilename), + Map.of(FormFilenameKey.PROJECT_CODE.getText(), project.getCode()) + ); + } + + public byte[] createFormAsPdf(@NotNull Project project, @NotNull String formTemplate, Optional language) throws FormTemplateServiceException { + try { + String templateFile = formTemplateConfig + .fetchTemplateFile(formTemplate) + .orElseThrow(() -> new RuntimeException("Template not found: " + formTemplate)); + return pdfGenerator.generatePdf( + templateFile, + createContext(project, formTemplate, LanguageUtils.normalize(language.orElse(defaultLanguage)))); + } catch (PdfGeneratorException e) { + throw new FormTemplateServiceException(e); + } + } + + private Map createContext(Project project, String formTemplate, String language) { + ProjectContext projectContext = projectContextFactory.createProjectContext(project, language); + //Add project context + Map result = new HashMap<>(projectContext.fetchContext()); + // Add form fields + result.put(FormContextKey.FIELDS.getText(), fetchFormFields(project, formTemplate, language, projectContext)); + // Add form variables + result.putAll(formTemplateConfig.fetchAllFormVariables(formTemplate, language)); + result.put(FormContextKey.DATA_TYPE_CLASS.getText(), DataType.class); + result.put(FormContextKey.CURRENT_DATE.getText(), DateUtils.fetchCurrentDate(datePattern, language)); + + return result; + } + + public Map fetchFormFields( + @NotNull Project project, + @NotNull String formTemplate, + @NotNull String language, + @NotNull ProjectContext projectContext + ) { + FormTemplateMetadata template = resolveTemplateMetadata(formTemplate); + + return Stream.concat( + // 1️⃣ ProjectCode fields + Stream.ofNullable(template.getProjectFields()) + .flatMap(Arrays::stream) + .map(projectContext::resolveProjectContext) + .map(field -> dtoFactory.convert( + formTemplateConfig.fetchProjectFormFieldTitle(formTemplate), + field, + Optional.ofNullable(field.getProjectValue()), + Optional.of(language) + )), + + // 2️⃣ Form fields from formService (raw, base + override) + Arrays.stream(template.getFormTitles()) + .flatMap(formTitle -> dtoFormService.fetchBaseAndOverrideFormFields( + formTitle, project, Optional.of(language))) + ) + .sorted(FormFieldUtils.FORM_FIELD_COMPARATOR) + .collect(FormFieldUtils.formFieldMapCollector()); + } + + private List fetchValidMetadata(Project project, Optional language) { + + // Fetch all selected forms for the project and extract their titles into a Set + // A Set is used for O(1) lookup when checking if a title is contained + Set selectedFormTitles = dtoFormService.fetchSelectedForms(project, language).stream() + .map(Form::title) + .collect(Collectors.toSet()); + + // From all available template metadata: + // Keep only those templates where *all* required form titles + // are present in the selected forms of the project + return formTemplateConfig.getTemplateMetadataMap().values().stream() + .filter(metadata -> Arrays.stream(metadata.getFormTitles()) + .allMatch(selectedFormTitles::contains) + ) + .toList(); + } + + public List fetchTemplates(@NotNull Project project, Optional language) { + + // Convert all valid metadata objects into DTOs (FormTemplate) + // This method returns ALL matching templates without ranking + return fetchValidMetadata(project, language).stream() + .map(metadata -> dtoFactory.convert(metadata, language)) + .toList(); + } + + public List fetchBestTemplates(@NotNull Project project, Optional language) { + + // Step 1: Get all templates that fully match the selected forms + List validTemplates = fetchValidMetadata(project, language); + + // Step 2: Compute the "score" of each template + // Here, the score is simply the number of form titles the template requires + // (this works because we already ensured all titles are present via allMatch) + int maxScore = validTemplates.stream() + .mapToInt(metadata -> metadata.getFormTitles().length) + .max() + .orElse(0); // fallback if list is empty + + // Step 3: Keep only templates that have the maximum score + // → i.e., the most specific / most complete templates + return validTemplates.stream() + .filter(metadata -> metadata.getFormTitles().length == maxScore) + // Step 4: Convert the remaining metadata into DTOs for output + .map(metadata -> dtoFactory.convert(metadata, language)) + .toList(); + } + + private FormTemplateMetadata resolveTemplateMetadata(String formTemplate) { + return formTemplateConfig.getTemplate(formTemplate) + .orElseThrow(() -> + new IllegalArgumentException("Template not found: " + formTemplate)); + } + + +} diff --git a/src/main/java/de/samply/form/template/ProjectContext.java b/src/main/java/de/samply/form/template/ProjectContext.java new file mode 100644 index 00000000..ff950ed8 --- /dev/null +++ b/src/main/java/de/samply/form/template/ProjectContext.java @@ -0,0 +1,58 @@ +package de.samply.form.template; + +import de.samply.form.FormFieldConfig; + +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class ProjectContext { + + private static final Pattern PLACEHOLDER = Pattern.compile("\\$\\{([^}]+)}"); + + private final Map context; + + public ProjectContext(Map context) { + this.context = context; + } + + public FormFieldConfig resolveProjectContext(FormFieldConfig original) { + if (original == null || original.getProjectValue() == null) { + return original; + } + return original.toBuilder().projectValue(resolvePlaceholders(original.getProjectValue())).build(); + } + + public Map fetchContext() { + return context + .entrySet() + .stream() + .collect(Collectors.toMap( + e -> e.getKey().getText(), + Map.Entry::getValue + )); + } + + private String resolvePlaceholders(String input) { + if (input == null) return null; + + Matcher matcher = PLACEHOLDER.matcher(input); + StringBuilder result = new StringBuilder(); + + while (matcher.find()) { + String keyText = matcher.group(1); + ProjectContextKey key = ProjectContextKey.fromText(keyText); + + String replacement = key != null ? context.get(key) : null; + + matcher.appendReplacement(result, + replacement != null ? Matcher.quoteReplacement(replacement) + : matcher.group(0)); // keep placeholder if missing + } + + matcher.appendTail(result); + return result.toString(); + } + +} diff --git a/src/main/java/de/samply/form/template/ProjectContextFactory.java b/src/main/java/de/samply/form/template/ProjectContextFactory.java new file mode 100644 index 00000000..05699bdd --- /dev/null +++ b/src/main/java/de/samply/form/template/ProjectContextFactory.java @@ -0,0 +1,85 @@ +package de.samply.form.template; + +import de.samply.app.ProjectManagerConst; +import de.samply.bridgehead.BridgeheadConfiguration; +import de.samply.db.model.CreatorUser; +import de.samply.db.model.Project; +import de.samply.db.model.User; +import de.samply.document.DocumentService; +import de.samply.document.DocumentType; +import de.samply.user.UserService; +import de.samply.utils.DateUtils; +import de.samply.utils.UserUtils; +import jakarta.validation.constraints.NotNull; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +@Component +public class ProjectContextFactory { + + private final UserService userService; + private final BridgeheadConfiguration bridgeheadConfiguration; + private final String datePattern; + private final DocumentService documentService; + + public ProjectContextFactory( + UserService userService, + BridgeheadConfiguration bridgeheadConfiguration, + @Value(ProjectManagerConst.FORM_TEMPLATE_DATE_PATTERN_SV) String datePattern, + DocumentService documentService) { + this.userService = userService; + this.bridgeheadConfiguration = bridgeheadConfiguration; + this.datePattern = datePattern; + this.documentService = documentService; + } + + public ProjectContext createProjectContext(@NotNull Project project, String language) { + Map result = new LinkedHashMap<>(); + result.put(ProjectContextKey.PROJECT_CODE, project.getCode()); + result.put(ProjectContextKey.PROJECT_TITLE, project.getQuery().getLabel()); + result.put(ProjectContextKey.PROJECT_DESCRIPTION, project.getQuery().getDescription()); + result.put(ProjectContextKey.PROJECT_CREATION_DATE, DateUtils.fetchDate(project.getCreatedAt(), datePattern, language)); + result.put(ProjectContextKey.ETHICAL_APPROVAL, existsVotum(project).toString()); + fetchCreator(project).ifPresent(user -> { + result.put(ProjectContextKey.CREATOR_NAME, UserUtils.extractFullName(Optional.of(user))); + result.put(ProjectContextKey.CREATOR_EMAIL, project.getCreatorEmail()); + Set users = userService.fetchCreatorUser(user.getEmail()); + String bridgeheads = users.stream() + .map(CreatorUser::getBridgehead) + .map(bridgeheadConfiguration::getHumanReadable) + .flatMap(Optional::stream) + .collect(Collectors.joining(",")).trim(); + if (!bridgeheads.isEmpty()) { + result.put(ProjectContextKey.CREATOR_BRIDGEHEADS, bridgeheads); + } + String affiliations = users.stream() + .map(CreatorUser::getBridgehead) + .map(bridgeheadConfiguration::getAffiliation) + .flatMap(Optional::stream) + .collect(Collectors.joining(",")).trim(); + if (!affiliations.isEmpty()) { + result.put(ProjectContextKey.CREATOR_AFFILIATIONS, affiliations); + } + }); + + return new ProjectContext(result); + } + + private Boolean existsVotum(Project project){ + return documentService + .fetchLastDocumentOfThisType(project, Optional.empty(), DocumentType.VOTUM) + .isPresent(); + } + + private Optional fetchCreator(@NotNull Project project) { + return userService.fetchUser(project.getCreatorEmail()); + } + + +} diff --git a/src/main/java/de/samply/form/template/ProjectContextKey.java b/src/main/java/de/samply/form/template/ProjectContextKey.java new file mode 100644 index 00000000..c733a3c6 --- /dev/null +++ b/src/main/java/de/samply/form/template/ProjectContextKey.java @@ -0,0 +1,40 @@ +package de.samply.form.template; + +import lombok.Getter; + +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Getter +public enum ProjectContextKey { + + PROJECT_CODE("project-code"), + CREATOR_BRIDGEHEADS("creator-bridgeheads"), + CREATOR_AFFILIATIONS("creator-affiliations"), + CREATOR_NAME("creator-name"), + CREATOR_EMAIL("creator-email"), + PROJECT_TITLE("project-title"), + PROJECT_DESCRIPTION("project-description"), + PROJECT_CREATION_DATE("project-creation-date"), + ETHICAL_APPROVAL("ethical-approval"); + + private final String text; + + private static final Map BY_TEXT = + Arrays.stream(values()) + .collect(Collectors.toMap( + ProjectContextKey::getText, + Function.identity() + )); + + ProjectContextKey(String text) { + this.text = text; + } + + public static ProjectContextKey fromText(String text) { + return BY_TEXT.get(text); + } + +} diff --git a/src/main/java/de/samply/form/template/VariableLanguageValueMapDeserializer.java b/src/main/java/de/samply/form/template/VariableLanguageValueMapDeserializer.java new file mode 100644 index 00000000..f397340a --- /dev/null +++ b/src/main/java/de/samply/form/template/VariableLanguageValueMapDeserializer.java @@ -0,0 +1,51 @@ +package de.samply.form.template; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import de.samply.utils.LanguageUtils; + +import java.io.IOException; +import java.util.Map; +import java.util.stream.Collectors; + +public class VariableLanguageValueMapDeserializer + extends StdDeserializer>> { + + public VariableLanguageValueMapDeserializer() { + super((Class) null); + } + + @Override + public Map> deserialize( + JsonParser p, + DeserializationContext context) throws IOException { + + ObjectMapper mapper = (ObjectMapper) p.getCodec(); + JsonNode node = mapper.readTree(p); + + Map> raw = + mapper.convertValue( + node, + new com.fasterxml.jackson.core.type.TypeReference<>() { + } + ); + + if (raw == null) { + return Map.of(); + } + + return raw.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + e -> e.getValue().entrySet().stream() + .collect(Collectors.toMap( + entry -> LanguageUtils.normalize(entry.getKey()), + Map.Entry::getValue + )) + )); + } + +} diff --git a/src/main/java/de/samply/frontend/ActionExplanation.java b/src/main/java/de/samply/frontend/ActionExplanation.java index 59ac5e98..1b917f70 100644 --- a/src/main/java/de/samply/frontend/ActionExplanation.java +++ b/src/main/java/de/samply/frontend/ActionExplanation.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import de.samply.project.ProjectType; import de.samply.project.state.ProjectBridgeheadState; import de.samply.project.state.ProjectState; @@ -42,6 +43,7 @@ public class ActionExplanation { @JsonProperty(value = "priority") private Integer priority; @JsonProperty(value = "messages", required = true) + @JsonDeserialize(using = LanguageMessageMapDeserializer.class) private Map languageMessageMap; } diff --git a/src/main/java/de/samply/frontend/ActionExplanations.java b/src/main/java/de/samply/frontend/ActionExplanations.java index 7bf22e81..46ede4ea 100644 --- a/src/main/java/de/samply/frontend/ActionExplanations.java +++ b/src/main/java/de/samply/frontend/ActionExplanations.java @@ -26,8 +26,8 @@ public Optional> getActionExplanation(String action) { } public Optional> fetchExplanationAndPriority(@NotNull String action, @NotNull String module, @NotNull String language, - Optional project, Optional projectBridgehead, - Optional projectBridgeheadUser, SessionUser sessionUser) { + Optional project, Optional projectBridgehead, + Optional projectBridgeheadUser, SessionUser sessionUser) { Optional> actionExplanations = getActionExplanation(action); if (actionExplanations.isPresent()) { for (ActionExplanation explanation : actionExplanations.get()) { @@ -47,7 +47,7 @@ private boolean isRequiredExplanation(String module, Optional project, return false; } if (project.isPresent()) { - if (explanation.getProjectType() != null && project.get().getType() != explanation.getProjectType()) { + if (explanation.getProjectType() != null && !project.get().hasProjectType(explanation.getProjectType())) { return false; } if (explanation.getProjectState() != null && project.get().getState() != explanation.getProjectState()) { @@ -55,7 +55,9 @@ private boolean isRequiredExplanation(String module, Optional project, } } if (projectBridgehead.isPresent()) { - if (explanation.getQueryState() != null && projectBridgehead.get().getQueryState() != explanation.getQueryState()) { + if (explanation.getQueryState() != null && + projectBridgehead.get().getExecutions().stream() + .noneMatch(exec -> exec.getQueryState() == explanation.getQueryState())) { return false; } if (explanation.getProjectBridgeheadState() != null && projectBridgehead.get().getState() != explanation.getProjectBridgeheadState()) { @@ -70,10 +72,7 @@ private boolean isRequiredExplanation(String module, Optional project, return false; } } - if (explanation.getOrganisationRole() != null && !sessionUser.getUserOrganisationRoles().containsRole(explanation.getOrganisationRole())) { - return false; - } - return true; + return explanation.getOrganisationRole() == null || sessionUser.getUserOrganisationRoles().containsRole(explanation.getOrganisationRole()); } } diff --git a/src/main/java/de/samply/frontend/FrontendConfiguration.java b/src/main/java/de/samply/frontend/FrontendConfiguration.java index 35e941a9..161028d9 100644 --- a/src/main/java/de/samply/frontend/FrontendConfiguration.java +++ b/src/main/java/de/samply/frontend/FrontendConfiguration.java @@ -3,6 +3,7 @@ import de.samply.app.ProjectManagerConst; import jakarta.annotation.PostConstruct; import lombok.Data; +import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; @@ -10,6 +11,7 @@ import java.util.Map; import java.util.Optional; +@Slf4j @Configuration @ConfigurationProperties(prefix = ProjectManagerConst.FRONTEND_CONFIG) @Data @@ -23,6 +25,14 @@ private void postConstruct() { Map tempSites = new HashMap<>(); sites.forEach((siteId, sitePath) -> tempSites.put(replaceHyphen(siteId), sitePath)); sites = tempSites; + log.info("----------------------------------------"); + log.info("Frontend Configuration:"); + log.info("Base URL: {}", baseUrl); + log.info("Sites:"); + sites.forEach((key, value) -> + log.info("- {} : {}", key, value) + ); + log.info("----------------------------------------"); } private String replaceHyphen(String var) { diff --git a/src/main/java/de/samply/frontend/FrontendService.java b/src/main/java/de/samply/frontend/FrontendService.java index c4341e28..d4e0d68b 100644 --- a/src/main/java/de/samply/frontend/FrontendService.java +++ b/src/main/java/de/samply/frontend/FrontendService.java @@ -7,12 +7,11 @@ import de.samply.db.model.Project; import de.samply.db.model.ProjectBridgehead; import de.samply.db.model.ProjectBridgeheadUser; -import de.samply.db.repository.ProjectBridgeheadRepository; -import de.samply.db.repository.ProjectBridgeheadUserRepository; -import de.samply.db.repository.ProjectRepository; +import de.samply.project.ProjectBridgeheadUserService; import de.samply.security.SessionUser; import de.samply.user.roles.RolesExtractor; import de.samply.utils.AspectUtils; +import de.samply.utils.LanguageUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.util.Pair; import org.springframework.http.ResponseEntity; @@ -25,45 +24,40 @@ @Service public class FrontendService { + // Services private final ConstraintsService constraintsService; + private final ProjectBridgeheadUserService projectBridgeheadUserService; + private final FrontendConfiguration frontendConfiguration; - private final String explorerUrlRedirectUriParameter; private final ActionExplanations actionExplanations; - private final String defaultLanguage; private final SessionUser sessionUser; - private final ProjectRepository projectRepository; - private final ProjectBridgeheadRepository projectBridgeheadRepository; - private final ProjectBridgeheadUserRepository projectBridgeheadUserRepository; + + private final String explorerUrlRedirectUriParameter; + private final String defaultLanguage; + public FrontendService( - ConstraintsService constraintsService, + ConstraintsService constraintsService, ProjectBridgeheadUserService projectBridgeheadUserService, FrontendConfiguration frontendConfiguration, ActionExplanations actionExplanations, SessionUser sessionUser, - ProjectBridgeheadUserRepository projectBridgeheadUserRepository, - ProjectRepository projectRepository, - ProjectBridgeheadRepository projectBridgeheadRepository, @Value(ProjectManagerConst.EXPLORER_REDIRECT_URI_PARAMETER_SV) String explorerUrlRedirectUriParameter, @Value(ProjectManagerConst.DEFAULT_LANGUAGE_SV) String defaultLanguage) { this.constraintsService = constraintsService; + this.projectBridgeheadUserService = projectBridgeheadUserService; this.frontendConfiguration = frontendConfiguration; this.explorerUrlRedirectUriParameter = explorerUrlRedirectUriParameter; this.actionExplanations = actionExplanations; - this.defaultLanguage = defaultLanguage; + this.defaultLanguage = LanguageUtils.normalize(defaultLanguage); this.sessionUser = sessionUser; - this.projectBridgeheadUserRepository = projectBridgeheadUserRepository; - this.projectRepository = projectRepository; - this.projectBridgeheadRepository = projectBridgeheadRepository; } - public Map> fetchModuleActionPackage(String site, Optional projectCode, - Optional bridgehead, Optional language, boolean withConstraints) { + public Map> fetchModuleActionPackage(String site, Optional project, + Optional bridgehead, Optional language, boolean withConstraints) { Map> moduleActionMap = new HashMap<>(); String rootPath = RolesExtractor.getRootPath(); - String tempLanguage = (language.isPresent()) ? language.get() : defaultLanguage; - Optional project = fetchProject(projectCode); - Optional projectBridgehead = fetchProjectBridgehead(project, bridgehead); - Optional projectBridgeheadUser = fetchProjectBridgeheadUser(projectBridgehead); + String tempLanguage = language.orElse(defaultLanguage); + Optional projectBridgeheadUser = fetchProjectBridgeheadUser(bridgehead); Arrays.stream(ProjectManagerController.class.getDeclaredMethods()).forEach(method -> { FrontendSiteModules frontendSiteModules = method.getAnnotation(FrontendSiteModules.class); FrontendSiteModule frontendSiteModule = method.getAnnotation(FrontendSiteModule.class); @@ -78,34 +72,38 @@ public Map> fetchModuleActionPackage(String site, Op } frontendSiteModuleList.forEach(tempFrontendSiteModule -> fetchModuleActionsPackages(moduleActionMap, rootPath, path, tempFrontendSiteModule, frontendAction, - site, projectCode, project, projectBridgehead, projectBridgeheadUser, tempLanguage, bridgehead, method, withConstraints)); + site, project, bridgehead, projectBridgeheadUser, tempLanguage, method, withConstraints)); }); return moduleActionMap; } + @SuppressWarnings("rawtypes") // For Optional. Otherwise, it would be too complex private void fetchModuleActionsPackages(Map> moduleActionsMap, - String rootPath, Optional path, - FrontendSiteModule frontendSiteModule, FrontendAction frontendAction, - String site, Optional projectCode, + String rootPath, + Optional path, + FrontendSiteModule frontendSiteModule, + FrontendAction frontendAction, + String site, Optional project, - Optional projectBridgehead, + Optional bridgehead, Optional projectBridgeheadUser, - String language, Optional bridgehead, Method method, + String language, + Method method, boolean withConstraints) { if (frontendSiteModule != null && site.equals(frontendSiteModule.site()) && frontendAction != null && path.isPresent()) { Optional roleConstraints = Optional.ofNullable(method.getAnnotation(RoleConstraints.class)); Optional stateConstraints = Optional.ofNullable(method.getAnnotation(StateConstraints.class)); - Optional responseEntity = this.constraintsService.checkRoleConstraints(roleConstraints, stateConstraints, projectCode, bridgehead); + Optional responseEntity = this.constraintsService.checkRoleConstraints(roleConstraints, stateConstraints, project, bridgehead); if (responseEntity.isEmpty()) { - responseEntity = this.constraintsService.checkStateConstraints(stateConstraints, projectCode, bridgehead); + responseEntity = this.constraintsService.checkStateConstraints(stateConstraints, project, bridgehead); } if (responseEntity.isEmpty()) { Optional projectConstraints = Optional.ofNullable(method.getAnnotation(ProjectConstraints.class)); - responseEntity = this.constraintsService.checkProjectConstraints(projectConstraints, projectCode); + responseEntity = this.constraintsService.checkProjectConstraints(projectConstraints, project); } if (responseEntity.isEmpty() || !withConstraints) { // If there are no restrictions addAction(moduleActionsMap, frontendSiteModule, frontendAction, rootPath, path, method, - project, projectBridgehead, projectBridgeheadUser, language); + project, bridgehead, projectBridgeheadUser, language); } } } @@ -115,22 +113,21 @@ private void addAction(Map> moduleActionsMap, String rootPath, Optional path, Method method, Optional project, Optional projectBridgehead, Optional projectBridgeheadUser, String language) { - Map actionNameActionsMap = moduleActionsMap.get(frontendSiteModule.module()); - if (actionNameActionsMap == null) { - actionNameActionsMap = new HashMap<>(); - moduleActionsMap.put(frontendSiteModule.module(), actionNameActionsMap); - } + Map actionNameActionsMap = moduleActionsMap.computeIfAbsent(frontendSiteModule.module(), _ -> new HashMap<>()); Optional> explanationPriority = actionExplanations.fetchExplanationAndPriority(frontendAction.action(), frontendSiteModule.module(), language, project, projectBridgehead, projectBridgeheadUser, sessionUser); - String explanation = explanationPriority.isPresent() ? explanationPriority.get().getFirst() : null; - Integer priority = explanationPriority.isPresent() ? explanationPriority.get().getSecond() : null; + String explanation = explanationPriority.map(Pair::getFirst).orElse(null); + Integer priority = explanationPriority.map(Pair::getSecond).orElse(null); + String resolvedPath = path.orElseThrow( + () -> new IllegalStateException("Path must be present for action " + frontendAction.action()) + ); actionNameActionsMap.put(frontendAction.action(), - new Action(rootPath + path.get(), fetchHttpMethod(method), fetchHttpParams(method), explanation, priority)); + new Action(rootPath + resolvedPath, fetchHttpMethod(method), fetchHttpParams(method), explanation, priority)); } private String fetchHttpMethod(Method method) { Optional result = AspectUtils.fetchHttpMethod(method); - return result.isPresent() ? result.get() : null; + return result.orElse(null); } private String[] fetchHttpParams(Method method) { @@ -138,12 +135,10 @@ private String[] fetchHttpParams(Method method) { } public String fetchUrl(String site, Map parameters) { - UriComponentsBuilder result = UriComponentsBuilder.fromHttpUrl(frontendConfiguration.getBaseUrl()); + UriComponentsBuilder result = UriComponentsBuilder.fromUriString(frontendConfiguration.getBaseUrl()); if (site != null) { Optional sitePath = frontendConfiguration.getSitePath(site); - if (sitePath.isPresent()) { - result.path(sitePath.get()); - } + sitePath.ifPresent(result::path); } if (parameters != null && !parameters.isEmpty()) { parameters.keySet().forEach(parameter -> @@ -158,21 +153,10 @@ public Map fetchExplorerRedirectUri(String site, Map fetchProject(Optional projectCode) { - return (projectCode.isPresent()) ? projectRepository.findByCode(projectCode.get()) : Optional.empty(); - } - - private Optional fetchProjectBridgehead(Optional project, Optional bridgehead) { - return (project.isPresent() && bridgehead.isPresent()) ? - projectBridgeheadRepository.findFirstByBridgeheadAndProject(bridgehead.get(), project.get()) : - Optional.empty(); - } - private Optional fetchProjectBridgeheadUser(Optional projectBridgehead) { return (projectBridgehead.isPresent()) ? - projectBridgeheadUserRepository.getFirstByEmailAndProjectBridgeheadOrderByModifiedAtDesc(sessionUser.getEmail(), projectBridgehead.get()) : + projectBridgeheadUserService.fetchFirstUsersOrderByModifiedAtDesc(sessionUser.getEmail(), projectBridgehead.get()) : Optional.empty(); } - } diff --git a/src/main/java/de/samply/frontend/LanguageMessageMapDeserializer.java b/src/main/java/de/samply/frontend/LanguageMessageMapDeserializer.java new file mode 100644 index 00000000..3491c33c --- /dev/null +++ b/src/main/java/de/samply/frontend/LanguageMessageMapDeserializer.java @@ -0,0 +1,33 @@ +package de.samply.frontend; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import de.samply.utils.LanguageUtils; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class LanguageMessageMapDeserializer + extends JsonDeserializer> { + + @Override + public Map deserialize(JsonParser p, DeserializationContext context) + throws IOException { + + Map result = new HashMap<>(); + JsonNode node = p.getCodec().readTree(p); + + node.properties().forEach(entry -> + result.put( + LanguageUtils.normalize(entry.getKey()), + entry.getValue().textValue() + ) + ); + + return result; + } +} + diff --git a/src/main/java/de/samply/frontend/dto/DtoFactory.java b/src/main/java/de/samply/frontend/dto/DtoFactory.java index fd5fac34..1de9fbe7 100644 --- a/src/main/java/de/samply/frontend/dto/DtoFactory.java +++ b/src/main/java/de/samply/frontend/dto/DtoFactory.java @@ -2,40 +2,61 @@ import de.samply.app.ProjectManagerConst; import de.samply.bridgehead.BridgeheadConfiguration; -import de.samply.db.model.BridgeheadAdminUser; -import de.samply.db.model.NotificationUserAction; -import de.samply.db.model.ProjectBridgeheadUser; -import de.samply.db.repository.BridgeheadAdminUserRepository; -import de.samply.db.repository.ProjectBridgeheadUserRepository; -import de.samply.db.repository.UserRepository; +import de.samply.db.model.*; +import de.samply.form.DisplayMetadata; +import de.samply.form.FormConfig; +import de.samply.form.FormFieldConfig; +import de.samply.form.FormService; +import de.samply.form.template.FormTemplateConfig; +import de.samply.form.template.FormTemplateMetadata; +import de.samply.project.ProjectBridgeheadUserService; +import de.samply.project.ProjectType; import de.samply.project.state.ProjectBridgeheadState; import de.samply.project.state.UserProjectState; +import de.samply.user.UserService; import de.samply.user.roles.ProjectRole; +import de.samply.utils.LanguageUtils; +import de.samply.utils.UserUtils; import jakarta.validation.constraints.NotNull; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import java.util.Arrays; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.Collectors; @Component public class DtoFactory { private final BridgeheadConfiguration bridgeheadConfiguration; - private final UserRepository userRepository; - private final ProjectBridgeheadUserRepository projectBridgeheadUserRepository; - private final BridgeheadAdminUserRepository bridgeheadAdminUserRepository; + private final FormConfig formConfig; + private final FormTemplateConfig formTemplateConfig; + private final String defaultLanguage; + + // Services + private final FormService formService; + private final UserService userService; + private final ProjectBridgeheadUserService projectBridgeheadUserService; + public DtoFactory(BridgeheadConfiguration bridgeheadConfiguration, - UserRepository userRepository, - ProjectBridgeheadUserRepository projectBridgeheadUserRepository, - BridgeheadAdminUserRepository bridgeheadAdminUserRepository) { + FormService formService, UserService userService, + FormConfig formConfig, + FormTemplateConfig formTemplateConfig, + @Value(ProjectManagerConst.DEFAULT_LANGUAGE_SV) String defaultLanguage, + ProjectBridgeheadUserService projectBridgeheadUserService) { this.bridgeheadConfiguration = bridgeheadConfiguration; - this.userRepository = userRepository; - this.projectBridgeheadUserRepository = projectBridgeheadUserRepository; - this.bridgeheadAdminUserRepository = bridgeheadAdminUserRepository; + this.userService = userService; + this.projectBridgeheadUserService = projectBridgeheadUserService; + this.formService = formService; + this.formConfig = formConfig; + this.formTemplateConfig = formTemplateConfig; + this.defaultLanguage = LanguageUtils.normalize(defaultLanguage); } public Project convert(@NotNull de.samply.db.model.Project project) { @@ -48,55 +69,75 @@ public Project convert(@NotNull de.samply.db.model.Project project) { result.setArchivedAt(project.getArchivedAt()); result.setModifiedAt(project.getModifiedAt()); result.setState(project.getState()); - result.setCustomConfig(project.isCustomConfig()); - result.setType(project.getType()); + result.setIsCustomConfigSelected(project.getIsCustomConfigSelected()); result.setQuery(project.getQuery().getQuery()); result.setHumanReadable(project.getQuery().getHumanReadable()); result.setQueryFormat(project.getQuery().getQueryFormat()); - result.setOutputFormat(project.getQuery().getOutputFormat()); - result.setTemplateId(project.getQuery().getTemplateId()); result.setLabel(project.getQuery().getLabel()); + result.setQueryDetails(project.getQuery().getDetails()); result.setDescription(project.getQuery().getDescription()); result.setExplorerUrl(project.getQuery().getExplorerUrl()); result.setQueryContext(project.getQuery().getContext()); result.setCreatorState(project.getCreatorResultsState()); result.setResultsUrl(project.getResultsUrl()); + result.setCohortDefinition(project.getQuery().getCohortDefinition()); + result.setOutputs(project.getQuery().getOutputs().stream().map(queryOutput -> + new ProjectOutput(queryOutput.getProjectType(), queryOutput.getOutputFormat(), queryOutput.getTemplateId()) + ).toArray(ProjectOutput[]::new)); return result; } private String fetchEmailUserName(String email) { - Optional user = userRepository.findByEmail(email); - return (user.isPresent()) ? - user.get().getFirstName() + " " + user.get().getLastName() : - null; + return UserUtils.extractFullName(userService.fetchUser(email)); } - public static de.samply.db.model.Project convert(@NotNull Project projectConfiguration, @NotNull de.samply.db.model.Project project) { - if (projectConfiguration.getExpiresAt() != null) { - project.setExpiresAt(projectConfiguration.getExpiresAt()); - } - if (projectConfiguration.getType() != null) { - project.setType(projectConfiguration.getType()); - } - if (projectConfiguration.getQuery() != null) { - project.getQuery().setQuery(projectConfiguration.getQuery()); + + public static de.samply.db.model.Project merge(@NotNull Project dtoProject, @NotNull de.samply.db.model.Project dbProject) { + if (dtoProject.getExpiresAt() != null) { + dbProject.setExpiresAt(dtoProject.getExpiresAt()); } - if (projectConfiguration.getHumanReadable() != null) { - project.getQuery().setHumanReadable(projectConfiguration.getHumanReadable()); + if (dtoProject.getQuery() != null) { + dbProject.getQuery().setQuery(dtoProject.getQuery()); } - if (projectConfiguration.getQueryFormat() != null) { - project.getQuery().setQueryFormat(projectConfiguration.getQueryFormat()); + if (dtoProject.getHumanReadable() != null) { + dbProject.getQuery().setHumanReadable(dtoProject.getHumanReadable()); } - if (projectConfiguration.getOutputFormat() != null) { - project.getQuery().setOutputFormat(projectConfiguration.getOutputFormat()); + if (dtoProject.getQueryFormat() != null) { + dbProject.getQuery().setQueryFormat(dtoProject.getQueryFormat()); } - if (projectConfiguration.getTemplateId() != null) { - project.getQuery().setTemplateId(projectConfiguration.getTemplateId()); + if (dtoProject.getQueryContext() != null) { + dbProject.getQuery().setContext(dtoProject.getQueryContext()); } - if (projectConfiguration.getQueryContext() != null) { - project.getQuery().setContext(projectConfiguration.getQueryContext()); + if (dtoProject.getOutputs() != null) { + merge(dtoProject.getOutputs(), dbProject); } - return project; + return dbProject; + } + + private static void merge(@NotNull ProjectOutput[] dtoOutputs, @NotNull de.samply.db.model.Project project) { + Map outputMap = project.getQuery().getOutputs().stream().collect(Collectors.toMap(QueryOutput::getProjectType, Function.identity())); + Arrays.stream(dtoOutputs).forEach(dtoOutput -> + outputMap.compute(dtoOutput.projectType(), (projectType, existingQueryOutput) -> { + if (existingQueryOutput == null) { + QueryOutput created = new QueryOutput(); + created.setProjectType(projectType); + created.setOutputFormat(dtoOutput.outputFormat()); + created.setTemplateId(dtoOutput.templateId()); + project.addOutput(created); + return created; + } + Optional.ofNullable(dtoOutput.outputFormat()).ifPresent(existingQueryOutput::setOutputFormat); + Optional.ofNullable(dtoOutput.templateId()).ifPresent(existingQueryOutput::setTemplateId); + return existingQueryOutput; + }) + ); + + var dtoTypes = Arrays.stream(dtoOutputs) + .map(ProjectOutput::projectType) + .collect(Collectors.toSet()); + + project.getQuery().getOutputs() + .removeIf(o -> !dtoTypes.contains(o.getProjectType())); } public Notification convert(@NotNull de.samply.db.model.Notification notification, Supplier userActionSupplier) { @@ -137,7 +178,11 @@ public ProjectBridgehead convert(@NotNull de.samply.db.model.ProjectBridgehead p fetchHumanReadableBridgehead(projectBridgehead), projectBridgehead.getState(), projectBridgehead.getModifiedAt(), - projectBridgehead.getQueryState(), + projectBridgehead.getExecutions().stream() + .map(execution -> new ProjectBridgeheadExecution( + execution.getQueryOutput().getProjectType(), + execution.getQueryState())) + .collect(Collectors.toSet()).toArray(ProjectBridgeheadExecution[]::new), projectBridgehead.getCreatorResultsState() ); } @@ -148,15 +193,15 @@ public String fetchHumanReadableBridgehead(@NotNull de.samply.db.model.ProjectBr public String fetchHumanReadableBridgehead(@NotNull String bridgehead) { Optional humanReadable = bridgeheadConfiguration.getHumanReadable(bridgehead); - return (humanReadable.isPresent()) ? humanReadable.get() : bridgehead; + return humanReadable.orElse(bridgehead); } public User convert(@NotNull de.samply.db.model.ProjectBridgeheadUser projectBridgeheadUser) { - Optional user = userRepository.findByEmail(projectBridgeheadUser.getEmail()); + Optional user = userService.fetchUser(projectBridgeheadUser.getEmail()); return new User( projectBridgeheadUser.getEmail(), - user.isPresent() ? user.get().getFirstName() : null, - user.isPresent() ? user.get().getLastName() : null, + user.map(de.samply.db.model.User::getFirstName).orElse(null), + user.map(de.samply.db.model.User::getLastName).orElse(null), projectBridgeheadUser.getProjectBridgehead().getBridgehead(), fetchHumanReadableBridgehead(projectBridgeheadUser.getProjectBridgehead()), projectBridgeheadUser.getProjectRole(), @@ -165,11 +210,11 @@ public User convert(@NotNull de.samply.db.model.ProjectBridgeheadUser projectBri } public User convertFilteringProjectRoleAndState(@NotNull de.samply.db.model.ProjectBridgeheadUser projectBridgeheadUser) { - Optional user = userRepository.findByEmail(projectBridgeheadUser.getEmail()); + Optional user = userService.fetchUser(projectBridgeheadUser.getEmail()); return new User( projectBridgeheadUser.getEmail(), - user.isPresent() ? user.get().getFirstName() : null, - user.isPresent() ? user.get().getLastName() : null, + user.map(de.samply.db.model.User::getFirstName).orElse(null), + user.map(de.samply.db.model.User::getLastName).orElse(null), projectBridgeheadUser.getProjectBridgehead().getBridgehead(), fetchHumanReadableBridgehead(projectBridgeheadUser.getProjectBridgehead()), null, @@ -179,37 +224,189 @@ public User convertFilteringProjectRoleAndState(@NotNull de.samply.db.model.Proj public Bridgehead convertToBridgehead(@NotNull String bridgehead) { Optional humanReadable = bridgeheadConfiguration.getHumanReadable(bridgehead); - return humanReadable.isPresent() ? new Bridgehead(bridgehead, humanReadable.get()) : new Bridgehead(bridgehead, null); + return humanReadable.map(s -> new Bridgehead(bridgehead, s)).orElseGet(() -> new Bridgehead(bridgehead, null)); } public static User convert(de.samply.db.model.User user) { return new User(user.getEmail(), user.getFirstName(), user.getLastName(), null, null, null, null); } + public FormField convert(@NotNull String title, Optional label, Optional value, Optional language) { + return new FormField( + title, + Optional.ofNullable(formConfig.getFormTitleDisplaMetadataMap().get(title)) + .map(DisplayMetadata::getDisplayName) + .map(m -> fetchValue(m, language)) + .orElse(null), + Optional.ofNullable(formConfig.getFormTitleDisplaMetadataMap().get(title)) + .map(DisplayMetadata::getDescription) + .map(m -> fetchValue(m, language)) + .orElse(null), + label.orElse(null), + label.map(_ -> formConfig.getFormTitleLabelFieldMap().get(title)) + .map(tm -> tm.get(label.get())) + .map(DisplayMetadata::getDisplayName) + .map(m -> fetchValue(m, language)) + .orElse(null), + label.map(_ -> formConfig.getFormTitleLabelFieldMap().get(title)) + .map(tm -> tm.get(label.get())) + .map(DisplayMetadata::getDescription) + .map(m -> fetchValue(m, language)) + .orElse(null), + label.map(_ -> formConfig.getFormTitleLabelFieldMap().get(title)) + .map(tm -> tm.get(label.get())) + .map(FormFieldConfig::getGroups) + .map(groups -> convertGroups(groups, language)) + .orElse(null), + label.map(_ -> formConfig.getFormTitleLabelFieldMap().get(title)) + .map(tm -> tm.get(label.get())) + .map(FormFieldConfig::getDataType) + .orElse(null), + label.map(_ -> formConfig.getFormTitleLabelFieldMap().get(title)) + .map(tm -> tm.get(label.get())) + .map(FormFieldConfig::getAllowedValues) + .map(values -> convert(values, language)) + .orElse(null), + label.map(_ -> formConfig.getFormTitleLabelFieldMap().get(title)) + .map(tm -> tm.get(label.get())) + .map(FormFieldConfig::isMandatory) + .orElse(null), + label.map(l -> fetchFormFieldOrder(title, l)).orElse(null), + value.orElse(null) + ); + } + + private FormFieldValue[] convert(de.samply.form.FormFieldValue[] values, Optional language) { + return Optional.ofNullable(values) + .map(Arrays::stream) + .map(stream -> stream + .map(value -> convert(value, language)) + .toArray(FormFieldValue[]::new)) + .orElse(null); + } + + private FormFieldValue convert(de.samply.form.FormFieldValue value, Optional language) { + return Optional.ofNullable(value) + .map(v -> new FormFieldValue( + v.getLabel(), + fetchValue(v.getDisplayName(), language), + fetchValue(v.getDescription(), language) + )) + .orElse(null); + } + + private FormFieldGroup[] convertGroups(String[] groups, Optional language) { + if (groups == null) { + return null; + } + return Arrays.stream(groups) + .map(group -> Map.entry( + group, + formConfig.getGroupsDisplayMetadataMap().get(group) + )) + .filter(entry -> entry.getValue() != null) + .map(entry -> { + var group = entry.getKey(); + var metadata = entry.getValue(); + + return new FormFieldGroup( + group, + fetchValue(metadata.getDisplayName(), language), + fetchValue(metadata.getDescription(), language) + ); + }) + .toArray(FormFieldGroup[]::new); + } + + private T fetchValue(Map languageMapper, Optional languageCode) { + return languageCode + .map(languageMapper::get) // returns the value or null + .orElse(languageMapper.get(defaultLanguage)); + } + + public FormField convert(@NotNull String title, @NotNull FormFieldConfig formFieldConfig, Optional value, Optional language) { + return new FormField( + title, + Optional.ofNullable(formConfig.getFormTitleDisplaMetadataMap().get(title)) + .map(DisplayMetadata::getDisplayName) + .map(m -> fetchValue(m, language)) + .orElse(null), + Optional.ofNullable(formConfig.getFormTitleDisplaMetadataMap().get(title)) + .map(DisplayMetadata::getDescription) + .map(m -> fetchValue(m, language)) + .orElse(null), + formFieldConfig.getLabel(), + fetchValue(formFieldConfig.getDisplayName(), language), + fetchValue(formFieldConfig.getDescription(), language), + convertGroups(formFieldConfig.getGroups(), language), + formFieldConfig.getDataType(), + convert(formFieldConfig.getAllowedValues(), language), + formFieldConfig.isMandatory(), + fetchFormFieldOrder(title, formFieldConfig.getLabel()), + value.orElse(null) + ); + } + + private int fetchFormFieldOrder(String title, String label) { + if (formTemplateConfig.isProjectFormFieldTitle(title)) { + return formTemplateConfig.fetchProjectFormFieldOrder(title, label); + } + Map orderMap = formConfig.getFormTitleLabelOrderMap().get(title); + if (orderMap == null) { + return Integer.MAX_VALUE; + } + Integer order = orderMap.get(label); + return order != null ? order : Integer.MAX_VALUE; + } + + public FormField convert(@NotNull ProjectFormField projectFormField, Optional language) { + return convert( + projectFormField.getFormTitle(), + Optional.of(projectFormField.getLabel()), + Optional.ofNullable(projectFormField.getValue()), + language); + } + + public Form convert(@NotNull ProjectForm projectForm, Optional language) { + return convertForm(projectForm.getFormTitle(), language); + } + + public Form convertForm(@NotNull String formTitle, Optional language) { + return new Form( + formTitle, + Optional.ofNullable(formConfig.getFormTitleDisplaMetadataMap().get(formTitle)) + .map(DisplayMetadata::getDisplayName) + .map(m -> fetchValue(m, language)) + .orElse(null), + Optional.ofNullable(formConfig.getFormTitleDisplaMetadataMap().get(formTitle)) + .map(DisplayMetadata::getDescription) + .map(m -> fetchValue(m, language)) + .orElse(null) + ); + } + public Optional fetchResults(@NotNull de.samply.db.model.Project project) { - Set finalUsers = projectBridgeheadUserRepository.getDistinctByProjectRoleAndProjectCode(ProjectRole.FINAL, project.getCode()); + Set finalUsers = projectBridgeheadUserService.fetchUsers(ProjectRole.FINAL, project); Optional finalUser = finalUsers.stream().filter(user -> user.getProjectState() == UserProjectState.ACCEPTED).findAny(); AtomicReference> email = new AtomicReference<>(Optional.empty()); AtomicReference> firstName = new AtomicReference<>(Optional.empty()); AtomicReference> lastName = new AtomicReference<>(Optional.empty()); if (finalUser.isEmpty()) { - if (finalUsers.isEmpty()){ + if (finalUsers.isEmpty()) { return Optional.empty(); } finalUser = finalUsers.stream().findAny(); } finalUser.ifPresent(user -> email.set(Optional.of(user.getEmail()))); - email.get().ifPresent(tempEmail -> { - userRepository.findByEmail(tempEmail).ifPresent(tempUser -> { - firstName.set(Optional.of(tempUser.getFirstName())); - lastName.set(Optional.of(tempUser.getLastName())); - }); + email.get().flatMap(userService::fetchUser).ifPresent(tempUser -> { + firstName.set(Optional.of(tempUser.getFirstName())); + lastName.set(Optional.of(tempUser.getLastName())); }); return Optional.of(new Results(null, null, fetchValue(email), fetchValue(firstName), fetchValue(lastName), fetchProjectResultsUrl(project, finalUser), project.getCreatorResultsState(), null, - fetchValue(new AtomicReference<>(finalUser), user -> user.getProjectState()))); + fetchValue(new AtomicReference<>(finalUser), ProjectBridgeheadUser::getProjectState))); } private String fetchProjectResultsUrl(@NotNull de.samply.db.model.Project project, Optional finalUser) { @@ -217,15 +414,15 @@ private String fetchProjectResultsUrl(@NotNull de.samply.db.model.Project projec } public Results fetchResults(@NotNull de.samply.db.model.ProjectBridgehead projectBridgehead) { - Optional bridgeheadAdmin = bridgeheadAdminUserRepository.findByBridgehead(projectBridgehead.getBridgehead()).stream().findAny(); + Optional bridgeheadAdmin = userService.fetchBridgeheadAdmin(projectBridgehead).stream().findAny(); AtomicReference> user = new AtomicReference<>(Optional.empty()); - bridgeheadAdmin.ifPresent(tempUser -> user.set(userRepository.findByEmail(tempUser.getEmail()))); - AtomicReference> humanReadableBridghead = new AtomicReference<>(bridgeheadConfiguration.getHumanReadable(projectBridgehead.getBridgehead())); + bridgeheadAdmin.ifPresent(tempUser -> user.set(userService.fetchUser(tempUser.getEmail()))); + AtomicReference> humanReadableBridgehead = new AtomicReference<>(bridgeheadConfiguration.getHumanReadable(projectBridgehead.getBridgehead())); return new Results(projectBridgehead.getBridgehead(), - fetchValue(humanReadableBridghead), - fetchValue(user, u -> u.getEmail()), - fetchValue(user, u -> u.getFirstName()), - fetchValue(user, u -> u.getLastName()), + fetchValue(humanReadableBridgehead), + fetchValue(user, de.samply.db.model.User::getEmail), + fetchValue(user, de.samply.db.model.User::getFirstName), + fetchValue(user, de.samply.db.model.User::getLastName), fetchProjectBridgeheadResults(projectBridgehead), projectBridgehead.getCreatorResultsState(), projectBridgehead.getState(), @@ -233,6 +430,29 @@ public Results fetchResults(@NotNull de.samply.db.model.ProjectBridgehead projec ); } + public ProjectAndForms convertToProjectAndForms(@NotNull de.samply.db.model.Project project, Optional language) { + return new ProjectAndForms( + convert(project), + formService + .fetchProjectForms(project) + .stream() + .map(f -> convert(f, language)) + .toArray(Form[]::new), + formService + .fetchProjectFormFields(project) + .stream() + .map(f -> convert(f, language)) + .toArray(FormField[]::new) + ); + } + + public FormTemplate convert(@NotNull FormTemplateMetadata formTemplateMetadata, Optional language) { + return new FormTemplate( + formTemplateMetadata.getTemplate(), + Optional.ofNullable(formTemplateMetadata.getLanguageDisplayNameMap().get(language.orElse(defaultLanguage))) + .orElse(formTemplateMetadata.getLanguageDisplayNameMap().get(defaultLanguage))); + } + private String fetchProjectBridgeheadResults(@NotNull de.samply.db.model.ProjectBridgehead projectBridgehead) { return (projectBridgehead.getState() == ProjectBridgeheadState.ACCEPTED) ? projectBridgehead.getResultsUrl() : ProjectManagerConst.NOT_AUTHORIZED; } @@ -241,7 +461,7 @@ private O fetchValue(AtomicReference> value) { return value.get().isPresent() ? value.get().get() : null; } - private O fetchValue(AtomicReference> value, Function function) { + private O fetchValue(AtomicReference> value, Function function) { return value.get().isPresent() ? function.apply(value.get().get()) : null; } diff --git a/src/main/java/de/samply/frontend/dto/Form.java b/src/main/java/de/samply/frontend/dto/Form.java new file mode 100644 index 00000000..5cd948fe --- /dev/null +++ b/src/main/java/de/samply/frontend/dto/Form.java @@ -0,0 +1,11 @@ +package de.samply.frontend.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record Form( + String title, + String titleDisplayName, + String titleDescription +) { +} diff --git a/src/main/java/de/samply/frontend/dto/FormField.java b/src/main/java/de/samply/frontend/dto/FormField.java new file mode 100644 index 00000000..b18cbff5 --- /dev/null +++ b/src/main/java/de/samply/frontend/dto/FormField.java @@ -0,0 +1,41 @@ +package de.samply.frontend.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import de.samply.form.DataType; + +import java.util.Arrays; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record FormField( + String title, + String titleDisplayName, + String titleDescription, + String label, + String labelDisplayName, + String labelDescription, + FormFieldGroup[] groups, + DataType type, + FormFieldValue[] allowedValues, + Boolean mandatory, + Integer order, + String value +) { + + // It can be used in the thymeleaf templates for the forms. + @JsonIgnore + @SuppressWarnings("unused") + public String fetchDisplayValue() { + return Optional + .ofNullable(allowedValues) + .stream() + .flatMap(Arrays::stream) + .filter(v -> Objects.equals(v.label(), value)) + .map(FormFieldValue::displayName) + .findFirst() + .orElse(value); + } + +} diff --git a/src/main/java/de/samply/frontend/dto/FormFieldGroup.java b/src/main/java/de/samply/frontend/dto/FormFieldGroup.java new file mode 100644 index 00000000..2f354c13 --- /dev/null +++ b/src/main/java/de/samply/frontend/dto/FormFieldGroup.java @@ -0,0 +1,11 @@ +package de.samply.frontend.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record FormFieldGroup ( + String group, + String displayName, + String description +){ +} diff --git a/src/main/java/de/samply/frontend/dto/FormFieldValue.java b/src/main/java/de/samply/frontend/dto/FormFieldValue.java new file mode 100644 index 00000000..491cfb9d --- /dev/null +++ b/src/main/java/de/samply/frontend/dto/FormFieldValue.java @@ -0,0 +1,10 @@ +package de.samply.frontend.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record FormFieldValue( + String label, + String displayName, + String description) { +} diff --git a/src/main/java/de/samply/frontend/dto/FormTemplate.java b/src/main/java/de/samply/frontend/dto/FormTemplate.java new file mode 100644 index 00000000..0982e14d --- /dev/null +++ b/src/main/java/de/samply/frontend/dto/FormTemplate.java @@ -0,0 +1,10 @@ +package de.samply.frontend.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record FormTemplate( + String template, + String displayName +) { +} diff --git a/src/main/java/de/samply/frontend/dto/Project.java b/src/main/java/de/samply/frontend/dto/Project.java index 3f791207..092ac751 100644 --- a/src/main/java/de/samply/frontend/dto/Project.java +++ b/src/main/java/de/samply/frontend/dto/Project.java @@ -2,10 +2,8 @@ import com.fasterxml.jackson.annotation.JsonInclude; import de.samply.annotations.IgnoreProjectConfigurationMatch; -import de.samply.project.ProjectType; import de.samply.project.state.ProjectState; import de.samply.project.state.UserProjectState; -import de.samply.query.OutputFormat; import de.samply.query.QueryFormat; import lombok.Data; @@ -30,23 +28,25 @@ public class Project { @IgnoreProjectConfigurationMatch private Instant modifiedAt; private ProjectState state; - private ProjectType type; private String query; private String humanReadable; private QueryFormat queryFormat; - private OutputFormat outputFormat; - private String templateId; + @IgnoreProjectConfigurationMatch + private String queryDetails; @IgnoreProjectConfigurationMatch private String label; @IgnoreProjectConfigurationMatch private String description; @IgnoreProjectConfigurationMatch + private String cohortDefinition; + @IgnoreProjectConfigurationMatch private String explorerUrl; private String queryContext; - private boolean isCustomConfig; + private Boolean isCustomConfigSelected; @IgnoreProjectConfigurationMatch private UserProjectState creatorState; @IgnoreProjectConfigurationMatch private String resultsUrl; + private ProjectOutput[] outputs; } diff --git a/src/main/java/de/samply/frontend/dto/ProjectAndForms.java b/src/main/java/de/samply/frontend/dto/ProjectAndForms.java new file mode 100644 index 00000000..69409bf8 --- /dev/null +++ b/src/main/java/de/samply/frontend/dto/ProjectAndForms.java @@ -0,0 +1,11 @@ +package de.samply.frontend.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record ProjectAndForms( + Project project, + Form[] forms, + FormField[] formFields +) { +} diff --git a/src/main/java/de/samply/frontend/dto/ProjectBridgehead.java b/src/main/java/de/samply/frontend/dto/ProjectBridgehead.java index 9fb21b7d..6c7c935c 100644 --- a/src/main/java/de/samply/frontend/dto/ProjectBridgehead.java +++ b/src/main/java/de/samply/frontend/dto/ProjectBridgehead.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.annotation.JsonInclude; import de.samply.project.state.ProjectBridgeheadState; import de.samply.project.state.UserProjectState; -import de.samply.query.QueryState; import java.time.Instant; @@ -14,7 +13,7 @@ public record ProjectBridgehead( String humanReadable, ProjectBridgeheadState state, Instant modifiedAt, - QueryState queryState, + ProjectBridgeheadExecution[] executions, UserProjectState creatorState ) { } diff --git a/src/main/java/de/samply/frontend/dto/ProjectBridgeheadExecution.java b/src/main/java/de/samply/frontend/dto/ProjectBridgeheadExecution.java new file mode 100644 index 00000000..7b32acda --- /dev/null +++ b/src/main/java/de/samply/frontend/dto/ProjectBridgeheadExecution.java @@ -0,0 +1,12 @@ +package de.samply.frontend.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import de.samply.project.ProjectType; +import de.samply.query.QueryState; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record ProjectBridgeheadExecution( + ProjectType projectType, + QueryState queryState) { + +} diff --git a/src/main/java/de/samply/frontend/dto/ProjectOutput.java b/src/main/java/de/samply/frontend/dto/ProjectOutput.java new file mode 100644 index 00000000..d4276c5d --- /dev/null +++ b/src/main/java/de/samply/frontend/dto/ProjectOutput.java @@ -0,0 +1,12 @@ +package de.samply.frontend.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import de.samply.project.ProjectType; +import de.samply.query.OutputFormat; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record ProjectOutput( + ProjectType projectType, + OutputFormat outputFormat, + String templateId) { +} diff --git a/src/main/java/de/samply/frontend/dto/configuration/ProjectConfiguration.java b/src/main/java/de/samply/frontend/dto/configuration/ProjectConfiguration.java deleted file mode 100644 index 5cb1adfa..00000000 --- a/src/main/java/de/samply/frontend/dto/configuration/ProjectConfiguration.java +++ /dev/null @@ -1,11 +0,0 @@ -package de.samply.frontend.dto.configuration; - -import lombok.Data; - -import java.util.HashMap; -import java.util.Map; - -@Data -public class ProjectConfiguration { - private Map fieldValues = new HashMap<>(); -} diff --git a/src/main/java/de/samply/frontend/dto/configuration/ProjectConfigurationMatcher.java b/src/main/java/de/samply/frontend/dto/configuration/ProjectConfigurationMatcher.java index c714387e..30e94c76 100644 --- a/src/main/java/de/samply/frontend/dto/configuration/ProjectConfigurationMatcher.java +++ b/src/main/java/de/samply/frontend/dto/configuration/ProjectConfigurationMatcher.java @@ -1,69 +1,192 @@ package de.samply.frontend.dto.configuration; -import de.samply.annotations.IgnoreProjectConfigurationMatch; import de.samply.app.ProjectManagerConst; -import de.samply.frontend.dto.Project; +import de.samply.frontend.dto.*; -import java.lang.reflect.Field; -import java.util.Comparator; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; public class ProjectConfigurationMatcher { - private static Map CUSTOM_CONFIGURATION = Map.of(ProjectManagerConst.CUSTOM_PROJECT_CONFIGURATION, new Project() {{ - setCustomConfig(true); - }}); + private static final String CUSTOM_KEY = ProjectManagerConst.CUSTOM_PROJECT_CONFIGURATION; - public static Map fetchMatchProjectConfiguration(Project project, Map projectConfigurations) throws ProjectConfigurationMatcherException { - if (project.isCustomConfig()) { - return fetchCustomConfiguration(projectConfigurations); + public static Map fetchMatchProjectConfiguration(ProjectAndForms runtime, Map config) { + + if (runtime.project() != null && isTrue(runtime.project().getIsCustomConfigSelected())) { + return fetchCustomConfiguration(config); } - Map, Integer> matchedFieldCounts = new HashMap<>(); - for (Map.Entry entry : projectConfigurations.entrySet()) { - if (!entry.getKey().equals(ProjectManagerConst.CUSTOM_PROJECT_CONFIGURATION)) { - int matchedFields = countMatchedFields(project, entry.getValue()); - if (matchedFields > 0) { - matchedFieldCounts.put(entry, matchedFields); - } + + Map, Integer> matchScores = new HashMap<>(); + + for (Map.Entry templateEntry : config.entrySet()) { + + if (templateEntry.getKey().equals(CUSTOM_KEY)) { + continue; + } + + int score = calculateMatchScore(runtime, templateEntry.getValue()); + + if (score >= 0) { + matchScores.put(templateEntry, score); } } - Optional> nameProjectConfig = matchedFieldCounts.entrySet().stream() - .max(Comparator.comparingInt(Map.Entry::getValue)) - .map(Map.Entry::getKey); - return (nameProjectConfig.isPresent()) ? Map.ofEntries(nameProjectConfig.get()) : fetchCustomConfiguration(projectConfigurations); + + if (matchScores.isEmpty()) { + return fetchCustomConfiguration(config); + } + + Map result = new LinkedHashMap<>(); + matchScores.entrySet().stream() + .sorted(Map.Entry., Integer>comparingByValue().reversed()) + .forEach(e -> result.put(e.getKey().getKey(), e.getKey().getValue())); + return result; } - private static Map fetchCustomConfiguration(Map projectConfigurations) { - Project result = projectConfigurations.get(ProjectManagerConst.CUSTOM_PROJECT_CONFIGURATION); - return (result != null) ? Map.of(ProjectManagerConst.CUSTOM_PROJECT_CONFIGURATION, result) : CUSTOM_CONFIGURATION; + private static boolean isTrue(Boolean value) { + return Boolean.TRUE.equals(value); } - private static int countMatchedFields(Project project, Project projectConfiguration) { - try { - return countMatchedFieldsWithoutHandlingException(project, projectConfiguration); - } catch (IllegalAccessException e) { - throw new ProjectConfigurationMatcherException(e); + private static Map fetchCustomConfiguration( + Map config) { + + ProjectAndForms custom = config.get(CUSTOM_KEY); + + if (custom != null) { + return Map.of(CUSTOM_KEY, custom); } + + // fallback minimal custom + Project customProject = new Project(); + customProject.setIsCustomConfigSelected(true); + + return Map.of(CUSTOM_KEY, + new ProjectAndForms(customProject, new Form[0], new FormField[0])); + } + + private static int calculateMatchScore( + ProjectAndForms runtime, + ProjectAndForms template) { + + int projectScore = matchProject(runtime.project(), template.project()); + if (projectScore < 0) return -1; + + int formScore = matchForms(runtime.forms(), template.forms()); + if (formScore < 0) return -1; + + int formFieldScore = matchFormFields(runtime.formFields(), template.formFields()); + if (formFieldScore < 0) return -1; + + return projectScore + formScore + formFieldScore; } - private static int countMatchedFieldsWithoutHandlingException(Project project, Project projectConfiguration) throws IllegalAccessException { - AtomicInteger matchedFields = new AtomicInteger(0); - for (Field field : project.getClass().getDeclaredFields()) { - field.setAccessible(true); - Object projectValue = field.get(project); - Object templateValue = field.get(projectConfiguration); - if (templateValue != null) { - if (field.isAnnotationPresent(IgnoreProjectConfigurationMatch.class) || templateValue.equals(projectValue)) { - matchedFields.getAndIncrement(); - } else { - return -1; - } + private static int matchProject(Project runtime, Project template) { + if (runtime == null || template == null) return -1; + + // Check the project-level field + if (isTrue(template.getIsCustomConfigSelected()) && !isTrue(runtime.getIsCustomConfigSelected())) { + return -1; + } + + ProjectOutput[] templateOutputs = template.getOutputs(); + ProjectOutput[] runtimeOutputs = runtime.getOutputs(); + + if (templateOutputs == null || templateOutputs.length == 0) return 0; + if (runtimeOutputs == null || runtimeOutputs.length == 0) return -1; + + // Subset matching: every template output must be present in the runtime outputs + int score = 0; + for (ProjectOutput templateOutput : templateOutputs) { + boolean found = false; + for (ProjectOutput runtimeOutput : runtimeOutputs) { + int s = matchOutput(runtimeOutput, templateOutput); + if (s >= 0) { score += s; found = true; break; } } + if (!found) return -1; } - return matchedFields.get(); + + return (isTrue(template.getIsCustomConfigSelected()) ? 1 : 0) + score; } + private static int matchOutput(ProjectOutput runtime, ProjectOutput template) { + if (template.projectType() != null && !Objects.equals(template.projectType(), runtime.projectType())) return -1; + if (template.outputFormat() != null && !Objects.equals(template.outputFormat(), runtime.outputFormat())) + return -1; + if (template.templateId() != null && !Objects.equals(template.templateId(), runtime.templateId())) return -1; + + // Count matched fields + return Stream.of(template.projectType(), template.outputFormat(), template.templateId()) + .filter(Objects::nonNull) + .mapToInt(_ -> 1) + .sum(); + } + + private static int matchForms(Form[] runtimeForms, Form[] templateForms) { + + if (templateForms == null || templateForms.length == 0) { + return 0; + } + + if (runtimeForms == null || runtimeForms.length == 0) { + return -1; + } + + Set runtimeTitles = Arrays.stream(runtimeForms) + .map(Form::title) + .collect(Collectors.toSet()); + + int score = 0; + + for (Form templateForm : templateForms) { + if (!runtimeTitles.contains(templateForm.title())) { + return -1; + } + score++; + } + + return score; + } + + private static int matchFormFields(FormField[] runtimeFields, FormField[] templateFields) { + + if (templateFields == null || templateFields.length == 0) { + return 0; + } + + if (runtimeFields == null || runtimeFields.length == 0) { + return -1; + } + + Map runtimeLookup = Arrays.stream(runtimeFields) + .collect(Collectors.toMap( + f -> buildKey(f.title(), f.label()), + f -> f, + (a, _) -> a + )); + + int score = 0; + + for (FormField templateField : templateFields) { + + String key = buildKey(templateField.title(), templateField.label()); + FormField runtimeField = runtimeLookup.get(key); + + if (runtimeField == null) { + return -1; + } + + if (templateField.value() != null && + !Objects.equals(templateField.value(), runtimeField.value())) { + return -1; + } + + score++; + } + + return score; + } + + private static String buildKey(String title, String label) { + return (title == null ? "" : title) + "|" + (label == null ? "" : label); + } } diff --git a/src/main/java/de/samply/frontend/dto/configuration/ProjectConfigurationMatcherException.java b/src/main/java/de/samply/frontend/dto/configuration/ProjectConfigurationMatcherException.java deleted file mode 100644 index a0cda74c..00000000 --- a/src/main/java/de/samply/frontend/dto/configuration/ProjectConfigurationMatcherException.java +++ /dev/null @@ -1,7 +0,0 @@ -package de.samply.frontend.dto.configuration; - -public class ProjectConfigurationMatcherException extends RuntimeException { - public ProjectConfigurationMatcherException(Throwable cause) { - super(cause); - } -} diff --git a/src/main/java/de/samply/frontend/dto/configuration/ProjectConfigurations.java b/src/main/java/de/samply/frontend/dto/configuration/ProjectConfigurations.java index 7e5dccf5..f239fa5c 100644 --- a/src/main/java/de/samply/frontend/dto/configuration/ProjectConfigurations.java +++ b/src/main/java/de/samply/frontend/dto/configuration/ProjectConfigurations.java @@ -1,8 +1,7 @@ package de.samply.frontend.dto.configuration; import com.fasterxml.jackson.annotation.JsonIgnore; -import de.samply.app.ProjectManagerConst; -import de.samply.frontend.dto.Project; +import de.samply.frontend.dto.ProjectAndForms; import lombok.Data; import java.util.HashMap; @@ -11,11 +10,12 @@ @Data public class ProjectConfigurations { - private Map config = new HashMap<>(); + private Map config = new HashMap<>(); @JsonIgnore - public Map fetchCurrentProjectConfiguration(Project project) { - return ProjectConfigurationMatcher.fetchMatchProjectConfiguration(project, config); + public Map fetchCurrentProjectConfiguration(ProjectAndForms projectAndForms) { + return ProjectConfigurationMatcher.fetchMatchProjectConfiguration(projectAndForms, config); } } + diff --git a/src/main/java/de/samply/frontend/dto/configuration/ProjectConfigurationsFactory.java b/src/main/java/de/samply/frontend/dto/configuration/ProjectConfigurationsFactory.java index ab18f80a..57552576 100644 --- a/src/main/java/de/samply/frontend/dto/configuration/ProjectConfigurationsFactory.java +++ b/src/main/java/de/samply/frontend/dto/configuration/ProjectConfigurationsFactory.java @@ -19,10 +19,9 @@ public class ProjectConfigurationsFactory { @Bean public ProjectConfigurations createProjectConfigurations(@Value(ProjectManagerConst.FRONTEND_PROJECT_CONFIG_PATH_SV) String frontendProjectConfigurationPath) { try { - ProjectConfigurations projectConfigurations = objectMapper.readValue(new File(frontendProjectConfigurationPath), ProjectConfigurations.class); - return projectConfigurations; + return objectMapper.readValue(new File(frontendProjectConfigurationPath), ProjectConfigurations.class); } catch (IOException e) { - log.error("Project configuration file not found: {}", frontendProjectConfigurationPath); + log.error("ProjectCode configuration file not found: {}", frontendProjectConfigurationPath); throw new RuntimeException(e); } } diff --git a/src/main/java/de/samply/notification/DtoNotificationService.java b/src/main/java/de/samply/notification/DtoNotificationService.java new file mode 100644 index 00000000..2e96c0db --- /dev/null +++ b/src/main/java/de/samply/notification/DtoNotificationService.java @@ -0,0 +1,36 @@ +package de.samply.notification; + +import de.samply.frontend.dto.DtoFactory; +import de.samply.frontend.dto.Notification; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; + +@Service +public class DtoNotificationService { + + private final NotificationService notificationService; + private final DtoFactory dtoFactory; + + public DtoNotificationService(NotificationService notificationService, + DtoFactory dtoFactory) { + this.notificationService = notificationService; + this.dtoFactory = dtoFactory; + } + + public List fetchUserVisibleNotifications( + Optional project, + Optional bridgehead, + Supplier> allUserVisibleProjectFetcher) throws NotificationServiceException { + return notificationService + .fetchUserVisibleNotifications(project, bridgehead, allUserVisibleProjectFetcher) + .stream() + .map(notification -> dtoFactory + .convert(notification, () -> + notificationService.fetchNotificationUserAction(notification))) + .toList(); + } + +} diff --git a/src/main/java/de/samply/notification/NotificationService.java b/src/main/java/de/samply/notification/NotificationService.java index 84798853..4da64a2d 100644 --- a/src/main/java/de/samply/notification/NotificationService.java +++ b/src/main/java/de/samply/notification/NotificationService.java @@ -4,10 +4,9 @@ import de.samply.db.model.Notification; import de.samply.db.model.NotificationUserAction; import de.samply.db.model.Project; +import de.samply.db.model.ProjectBridgehead; import de.samply.db.repository.NotificationRepository; import de.samply.db.repository.NotificationUserActionRepository; -import de.samply.db.repository.ProjectRepository; -import de.samply.frontend.dto.DtoFactory; import de.samply.security.SessionUser; import de.samply.user.roles.OrganisationRole; import jakarta.validation.constraints.NotNull; @@ -26,28 +25,22 @@ public class NotificationService { private final NotificationRepository notificationRepository; private final NotificationUserActionRepository notificationUserActionRepository; - private final ProjectRepository projectRepository; private final SessionUser sessionUser; - private final DtoFactory dtoFactory; + public NotificationService(NotificationRepository notificationRepository, NotificationUserActionRepository notificationUserActionRepository, - ProjectRepository projectRepository, - SessionUser sessionUser, - DtoFactory dtoFactory) { + SessionUser sessionUser) { this.notificationRepository = notificationRepository; this.notificationUserActionRepository = notificationUserActionRepository; - this.projectRepository = projectRepository; this.sessionUser = sessionUser; - this.dtoFactory = dtoFactory; } @Async(ProjectManagerConst.ASYNC_NOTIFICATION_EXECUTOR) - public void createNotification(@NotNull String projectCode, String bridgehead, String email, + public void createNotification(@NotNull Project project, String bridgehead, String email, @NotNull OperationType operationType, @NotNull String details, String error, HttpStatus httpStatus ) throws NotificationServiceException { - Project project = fetchProject(projectCode); Notification notification = new Notification(); notification.setProject(project); notification.setBridgehead(bridgehead); @@ -59,45 +52,31 @@ public void createNotification(@NotNull String projectCode, String bridgehead, S notificationRepository.save(notification); } - private Project fetchProject(String projectCode) throws NotificationServiceException { - Optional project = projectRepository.findByCode(projectCode); - if (project.isEmpty()) { - throw new NotificationServiceException("Project " + projectCode + " not found"); - } - return project.get(); - } - - // We use a supplier of ProjectService.fetchAllUserVisibleProjects in order to remove interdependence + // We use a supplier of ProjectService.fetchAllUserVisibleProjects to remove interdependence // between the notification service and the project service. - public List fetchUserVisibleNotifications( - Optional projectCodeOptional, Optional bridgheadOptional, + protected List fetchUserVisibleNotifications( + Optional project, Optional bridgehead, Supplier> allUserVisibleProjectFetcher) throws NotificationServiceException { List result = new ArrayList<>(); - List projects = (projectCodeOptional.isEmpty()) ? - allUserVisibleProjectFetcher.get() : List.of(fetchProject(projectCodeOptional.get())); - List bridgeheads = fetchUserVisibleBridgeheads(bridgheadOptional); - projects.forEach(project -> { + List projects = project.map(List::of).orElseGet(allUserVisibleProjectFetcher); + List bridgeheads = fetchUserVisibleBridgeheads(bridgehead.map(ProjectBridgehead::getBridgehead)); + projects.forEach(tempProject -> { if (bridgeheads.isEmpty() && sessionUser.getUserOrganisationRoles().containsRole(OrganisationRole.PROJECT_MANAGER_ADMIN)) { - result.addAll(notificationRepository.findByProjectOrderByTimestampDesc(project)); + result.addAll(notificationRepository.findByProjectOrderByTimestampDesc(tempProject)); } else { - bridgeheads.forEach(bridgehead -> result.addAll( - notificationRepository.findByProjectAndBridgeheadOrBridgeheadIsNullOrderByTimestampDesc(project, bridgehead))); + bridgeheads.forEach(tempBridgehead -> result.addAll( + notificationRepository.findByProjectAndBridgeheadOrBridgeheadIsNullOrderByTimestampDesc(tempProject, tempBridgehead))); } }); - return result.stream().map(notification -> - dtoFactory.convert(notification, () -> fetchNotificationUserAction(notification))).toList(); + return result; } private List fetchUserVisibleBridgeheads(Optional requestedBridgehead) { if (sessionUser.getUserOrganisationRoles().containsRole(OrganisationRole.PROJECT_MANAGER_ADMIN)) { - return (requestedBridgehead.isEmpty()) ? new ArrayList<>() : List.of(requestedBridgehead.get()); + return requestedBridgehead.map(List::of).orElseGet(ArrayList::new); } else { - if (requestedBridgehead.isEmpty()) { - return sessionUser.getBridgeheads().stream().toList(); - } else { - return (sessionUser.getBridgeheads().contains(requestedBridgehead.get())) ? - List.of(requestedBridgehead.get()) : new ArrayList<>(); - } + return requestedBridgehead.>map(s -> (sessionUser.getBridgeheads().contains(s)) ? + List.of(s) : new ArrayList<>()).orElseGet(() -> sessionUser.getBridgeheads().stream().toList()); } } diff --git a/src/main/java/de/samply/notification/OperationType.java b/src/main/java/de/samply/notification/OperationType.java index aa5ffd90..709228c3 100644 --- a/src/main/java/de/samply/notification/OperationType.java +++ b/src/main/java/de/samply/notification/OperationType.java @@ -8,11 +8,9 @@ public enum OperationType { CREATE_DATASHIELD_TOKEN, REFRESH_DATASHIELD_TOKEN, REMOVE_DATASHIELD_TOKEN, - REMOVE_DATASHIELD_TOKENS_AND_PROJECT, CHANGE_PROJECT_STATE, ASSIGN_USER_TO_PROJECT, CHANGE_PROJECT_BRIDGEHEAD_USER_EVALUATION, - CHANGE_PROJECT_BRIDGEHEAD_STATE, EDIT_QUERY, ADD_DOCUMENT, EDIT_PROJECT, @@ -22,5 +20,7 @@ public enum OperationType { TRANSFER_FILE_TO_CODER, REGISTER_IN_APP_REGISTER, UNREGISTER_IN_APP_REGISTER, - TRANSFER_FILES_TO_RESEARCH_ENVIRONMENT + TRANSFER_FILES_TO_RESEARCH_ENVIRONMENT, + ADD_PROJECT_FORM_LABEL, + EDIT_PROJECT_FORM_LABEL } diff --git a/src/main/java/de/samply/pdf/PdfConverter.java b/src/main/java/de/samply/pdf/PdfConverter.java new file mode 100644 index 00000000..f15e0fe2 --- /dev/null +++ b/src/main/java/de/samply/pdf/PdfConverter.java @@ -0,0 +1,7 @@ +package de.samply.pdf; + +public interface PdfConverter { + + byte[] convert(String content) throws PdfConverterException; + +} diff --git a/src/main/java/de/samply/pdf/PdfConverterException.java b/src/main/java/de/samply/pdf/PdfConverterException.java new file mode 100644 index 00000000..bf40b828 --- /dev/null +++ b/src/main/java/de/samply/pdf/PdfConverterException.java @@ -0,0 +1,9 @@ +package de.samply.pdf; + +public class PdfConverterException extends Exception { + + public PdfConverterException(Throwable cause) { + super(cause); + } + +} diff --git a/src/main/java/de/samply/pdf/PdfGenerator.java b/src/main/java/de/samply/pdf/PdfGenerator.java new file mode 100644 index 00000000..8caf67fe --- /dev/null +++ b/src/main/java/de/samply/pdf/PdfGenerator.java @@ -0,0 +1,34 @@ +package de.samply.pdf; + + +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; + +import java.util.Map; + + +public class PdfGenerator { + + private final TemplateEngine templateEngine; + private final PdfConverter pdfConverter; + + public PdfGenerator(TemplateEngine templateEngine, PdfConverter pdfConverter) { + this.templateEngine = templateEngine; + this.pdfConverter = pdfConverter; + } + + public byte[] generatePdf(String templateFilename, Map context) throws PdfGeneratorException { + try { + return pdfConverter.convert(templateEngine.process(templateFilename, createContext(context))); + } catch (PdfConverterException e) { + throw new PdfGeneratorException(e); + } + } + + private Context createContext(Map context) { + Context result = new Context(); + result.setVariables(context); + return result; + } + +} diff --git a/src/main/java/de/samply/pdf/PdfGeneratorException.java b/src/main/java/de/samply/pdf/PdfGeneratorException.java new file mode 100644 index 00000000..cd9184d8 --- /dev/null +++ b/src/main/java/de/samply/pdf/PdfGeneratorException.java @@ -0,0 +1,9 @@ +package de.samply.pdf; + +public class PdfGeneratorException extends Exception { + + public PdfGeneratorException(Throwable cause) { + super(cause); + } + +} diff --git a/src/main/java/de/samply/project/CheckExpiredActiveProjectsJob.java b/src/main/java/de/samply/project/CheckExpiredActiveProjectsJob.java index a975c41e..678729b2 100644 --- a/src/main/java/de/samply/project/CheckExpiredActiveProjectsJob.java +++ b/src/main/java/de/samply/project/CheckExpiredActiveProjectsJob.java @@ -1,7 +1,6 @@ package de.samply.project; import de.samply.app.ProjectManagerConst; -import de.samply.db.repository.ProjectRepository; import de.samply.project.event.ProjectEventService; import de.samply.project.state.ProjectState; import org.springframework.scheduling.annotation.Scheduled; @@ -13,21 +12,20 @@ @Component public class CheckExpiredActiveProjectsJob { - private final ProjectRepository projectRepository; + private final ProjectService projectService; private final ProjectEventService projectEventService; - public CheckExpiredActiveProjectsJob(ProjectRepository projectRepository, + public CheckExpiredActiveProjectsJob(ProjectService projectService, ProjectEventService projectEventService) { - this.projectRepository = projectRepository; + this.projectService = projectService; this.projectEventService = projectEventService; } @Scheduled(cron = ProjectManagerConst.CHECK_EXPIRED_ACTIVE_PROJECTS_CRON_EXPRESSION_SV) public void checkExpiredActiveProjects() { - projectRepository.findByExpiresAtBeforeAndStateIn(LocalDate.now(), - Set.of(ProjectState.CREATED, ProjectState.ACCEPTED, ProjectState.DEVELOP, - ProjectState.PILOT, ProjectState.FINAL)).forEach(expiredActiveProject -> - projectEventService.archive(expiredActiveProject.getCode())); + projectService.findProjectByExpiresAtBeforeAndStateIn(LocalDate.now(), + Set.of(ProjectState.REVIEW, ProjectState.APPROVAL, ProjectState.DEVELOP, + ProjectState.PILOT, ProjectState.FINAL)).forEach(projectEventService::archive); } } diff --git a/src/main/java/de/samply/project/DtoProjectBridgeheadService.java b/src/main/java/de/samply/project/DtoProjectBridgeheadService.java new file mode 100644 index 00000000..dcd6fc6d --- /dev/null +++ b/src/main/java/de/samply/project/DtoProjectBridgeheadService.java @@ -0,0 +1,63 @@ +package de.samply.project; + +import de.samply.db.model.Project; +import de.samply.db.model.ProjectBridgehead; +import de.samply.frontend.dto.DtoFactory; +import de.samply.frontend.dto.Results; +import jakarta.validation.constraints.NotNull; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Service +public class DtoProjectBridgeheadService { + + private final ProjectBridgeheadService projectBridgeheadService; + private final DtoFactory dtoFactory; + + public DtoProjectBridgeheadService(ProjectBridgeheadService projectBridgeheadService, DtoFactory dtoFactory) { + this.projectBridgeheadService = projectBridgeheadService; + this.dtoFactory = dtoFactory; + } + + public List fetchUserVisibleProjectBridgeheads(@NotNull Project project) throws ProjectBridgeheadServiceException { + Set projectBridgeheads = projectBridgeheadService.fetchBridgeheads(project); + if (projectBridgeheadService.isProjectManagerAdmin()) { + return new ArrayList<>(projectBridgeheads).stream().map(dtoFactory::convert).toList(); + } + Set tempProjectBridgeheads = new HashSet<>(); + projectBridgeheads.forEach(projectBridgehead -> { + if (projectBridgeheadService.isBridgeheadAdminOfProjectBridgehead(projectBridgehead) || + projectBridgeheadService.isUserOfProjectBridgehead(projectBridgehead) || + projectBridgeheadService.isUserCreatorOfProject(project) + ) { + tempProjectBridgeheads.add(projectBridgehead); + } + }); + return new ArrayList<>(tempProjectBridgeheads).stream().map(dtoFactory::convert).toList(); + } + + public List fetchResults(@NotNull Project project) throws ProjectBridgeheadServiceException { + return projectBridgeheadService + .fetchBridgeheads(project) + .stream() + .map(dtoFactory::fetchResults) + .toList(); // Uses the modern `toList()` instead of `Collectors.toList()` + } + + public List fetchProjectBridgeheads(@NotNull Project project) throws ProjectBridgeheadServiceException { + return projectBridgeheadService + .fetchBridgeheads(project) + .stream() + .map(dtoFactory::convert) + .toList(); + } + + public Results fetchResultsOfOwnBridgehead(@NotNull ProjectBridgehead bridgehead) throws ProjectBridgeheadServiceException { + return dtoFactory.fetchResults(bridgehead); + } + +} diff --git a/src/main/java/de/samply/project/DtoProjectService.java b/src/main/java/de/samply/project/DtoProjectService.java new file mode 100644 index 00000000..0fcdd7cf --- /dev/null +++ b/src/main/java/de/samply/project/DtoProjectService.java @@ -0,0 +1,66 @@ +package de.samply.project; + +import de.samply.frontend.dto.DtoFactory; +import de.samply.frontend.dto.Project; +import de.samply.frontend.dto.ProjectAndForms; +import de.samply.frontend.dto.Results; +import de.samply.frontend.dto.configuration.ProjectConfigurations; +import de.samply.project.state.ProjectState; +import de.samply.security.SessionUser; +import jakarta.validation.constraints.NotNull; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; + +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +@Service +public class DtoProjectService { + + private final ProjectService projectService; + private final DtoFactory dtoFactory; + private final SessionUser sessionUser; + private final ProjectConfigurations projectConfigurations; + + public DtoProjectService( + ProjectService projectService, + DtoFactory dtoFactory, + SessionUser sessionUser, + ProjectConfigurations projectConfigurations) { + this.projectService = projectService; + this.dtoFactory = dtoFactory; + this.sessionUser = sessionUser; + this.projectConfigurations = projectConfigurations; + } + + public Project fetchDtoProject(@NotNull String projectCode) throws ProjectServiceException { + return dtoFactory.convert(projectService.fetchProject(projectCode)); + } + + public Page fetchUserVisibleProjects( + Optional projectState, Optional archived, int page, int pageSize, + boolean modifiedDescendant) { + PageRequest pageRequest = PageRequest.of(page, pageSize); + if (projectService.isProjectManagerAdmin()) { + return projectService.fetchProjectManagerAdminProjects(projectState, archived, pageRequest, modifiedDescendant).map(dtoFactory::convert); + } + Set bridgeheads = sessionUser.getBridgeheads(); + // We make an assumption: A bridgehead admin is bridgehead admin in all of their bridgeheads. + if (projectService.isBridgeheadAdmin()) { + return projectService.fetchBridgeheadAdminProjects(bridgeheads, projectState, archived, pageRequest, modifiedDescendant).map(dtoFactory::convert); + } + return projectService.fetchResearcherProjects(sessionUser.getEmail(), bridgeheads, projectState, archived, pageRequest, modifiedDescendant).map(dtoFactory::convert); + } + + public Map fetchCurrentProjectConfiguration(@NotNull de.samply.db.model.Project project) throws ProjectServiceException { + return this.projectConfigurations.fetchCurrentProjectConfiguration(dtoFactory.convertToProjectAndForms(project, Optional.empty())); + } + + public Optional fetchResults(@NotNull de.samply.db.model.Project project) throws ProjectServiceException { + return dtoFactory.fetchResults(project); + } + + +} diff --git a/src/main/java/de/samply/project/ProjectBridgeheadConverter.java b/src/main/java/de/samply/project/ProjectBridgeheadConverter.java new file mode 100644 index 00000000..6da6de35 --- /dev/null +++ b/src/main/java/de/samply/project/ProjectBridgeheadConverter.java @@ -0,0 +1,45 @@ +package de.samply.project; + +import de.samply.annotations.ProjectCode; +import de.samply.db.model.Project; +import de.samply.db.model.ProjectBridgehead; +import de.samply.resolvers.AnnotatedParametersWrapper; +import org.jspecify.annotations.NonNull; +import org.springframework.core.convert.converter.Converter; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.util.Optional; + +@Component +public class ProjectBridgeheadConverter implements Converter { + + private final AnnotatedParametersWrapper annotatedParametersWrapper; + private final ProjectBridgeheadService projectBridgeheadService; + + public ProjectBridgeheadConverter( + AnnotatedParametersWrapper annotatedParametersWrapper, + ProjectBridgeheadService projectBridgeheadService) { + this.annotatedParametersWrapper = annotatedParametersWrapper; + this.projectBridgeheadService = projectBridgeheadService; + } + + @Override + public ProjectBridgehead convert(@NonNull String bridgehead) { + + if (!StringUtils.hasText(bridgehead)) return null; + + // Get the resolved ProjectCode if available + Optional projectOpt = annotatedParametersWrapper.getResolved(ProjectCode.class, Project.class); + + // Get the raw project code if no ProjectCode is resolved + String projectCode = projectOpt.map(Project::getCode) + .or(() -> annotatedParametersWrapper.getRaw(ProjectCode.class, String.class)) + .orElseThrow(() -> new IllegalArgumentException("ProjectCode code not found")); + + return projectBridgeheadService + .fetchProjectBridgehead(projectCode, bridgehead) + .orElseThrow(() -> new IllegalArgumentException("Bridgehead " + bridgehead + " not found for project " + projectCode)); + } + +} diff --git a/src/main/java/de/samply/project/ProjectBridgeheadService.java b/src/main/java/de/samply/project/ProjectBridgeheadService.java index 18c04355..4858b65f 100644 --- a/src/main/java/de/samply/project/ProjectBridgeheadService.java +++ b/src/main/java/de/samply/project/ProjectBridgeheadService.java @@ -2,106 +2,85 @@ import de.samply.db.model.Project; import de.samply.db.model.ProjectBridgehead; +import de.samply.db.model.ProjectBridgeheadDataShield; +import de.samply.db.repository.ProjectBridgeheadDataShieldRepository; import de.samply.db.repository.ProjectBridgeheadRepository; import de.samply.db.repository.ProjectBridgeheadUserRepository; -import de.samply.db.repository.ProjectRepository; -import de.samply.frontend.dto.DtoFactory; -import de.samply.frontend.dto.Results; import de.samply.notification.NotificationService; import de.samply.notification.OperationType; import de.samply.project.state.ProjectBridgeheadState; +import de.samply.project.state.ProjectState; import de.samply.project.state.UserProjectState; import de.samply.query.QueryState; import de.samply.security.SessionUser; import de.samply.user.roles.OrganisationRole; import jakarta.validation.constraints.NotNull; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; import java.time.Instant; -import java.util.*; +import java.util.List; +import java.util.Optional; +import java.util.Set; @Service public class ProjectBridgeheadService { + // Services private final NotificationService notificationService; - private final ProjectRepository projectRepository; + + // Repositories private final ProjectBridgeheadRepository projectBridgeheadRepository; private final ProjectBridgeheadUserRepository projectBridgeheadUserRepository; + private final ProjectBridgeheadDataShieldRepository projectBridgeheadDataShieldRepository; + private final SessionUser sessionUser; - private final DtoFactory dtoFactory; + private final ApplicationEventPublisher applicationEventPublisher; public ProjectBridgeheadService(NotificationService notificationService, - ProjectRepository projectRepository, ProjectBridgeheadRepository projectBridgeheadRepository, ProjectBridgeheadUserRepository projectBridgeheadUserRepository, + ProjectBridgeheadDataShieldRepository projectBridgeheadDataShieldRepository, SessionUser sessionUser, - DtoFactory dtoFactory) { + ApplicationEventPublisher applicationEventPublisher) { this.notificationService = notificationService; - this.projectRepository = projectRepository; this.projectBridgeheadRepository = projectBridgeheadRepository; this.projectBridgeheadUserRepository = projectBridgeheadUserRepository; + this.projectBridgeheadDataShieldRepository = projectBridgeheadDataShieldRepository; this.sessionUser = sessionUser; - this.dtoFactory = dtoFactory; + this.applicationEventPublisher = applicationEventPublisher; } - public void acceptProject(@NotNull String projectCode, @NotNull String bridgehead) throws ProjectBridgeheadServiceException { - changeProjectBridgeheadState(projectCode, bridgehead, ProjectBridgeheadState.ACCEPTED); + public void acceptProject(@NotNull Project project, @NotNull ProjectBridgehead bridgehead) throws ProjectBridgeheadServiceException { + changeProjectBridgeheadState(project, bridgehead, ProjectBridgeheadState.ACCEPTED); } - public void rejectProject(@NotNull String projectCode, @NotNull String bridgehead) throws ProjectBridgeheadServiceException { - changeProjectBridgeheadState(projectCode, bridgehead, ProjectBridgeheadState.REJECTED); + public void rejectProject(@NotNull Project project, @NotNull ProjectBridgehead bridgehead) throws ProjectBridgeheadServiceException { + changeProjectBridgeheadState(project, bridgehead, ProjectBridgeheadState.REJECTED); } - private void changeProjectBridgeheadState(@NotNull String projectCode, @NotNull String bridgehead, @NotNull ProjectBridgeheadState state) throws ProjectBridgeheadServiceException { - Optional project = projectRepository.findByCode(projectCode); - if (project.isEmpty()) { - throw new ProjectBridgeheadServiceException("Project not found: " + projectCode); - } - Optional projectBridgehead = projectBridgeheadRepository.findFirstByBridgeheadAndProject(bridgehead, project.get()); - if (projectBridgehead.isEmpty()) { - throw new ProjectBridgeheadServiceException("Bridghead " + bridgehead + " in project " + projectCode + " not found"); - } - projectBridgehead.get().setState(state); - projectBridgeheadRepository.save(projectBridgehead.get()); - this.notificationService.createNotification(projectCode, bridgehead, sessionUser.getEmail(), OperationType.CHANGE_PROJECT_STATE, + private void changeProjectBridgeheadState(@NotNull Project project, @NotNull ProjectBridgehead bridgehead, @NotNull ProjectBridgeheadState state) throws ProjectBridgeheadServiceException { + bridgehead.setState(state); + setModifiedAtAndSaveBridgehead(bridgehead); + this.notificationService.createNotification(project, bridgehead.getBridgehead(), sessionUser.getEmail(), OperationType.CHANGE_PROJECT_STATE, "Set project bridgehead state to " + state, null, null); } - public List fetchUserVisibleProjectBridgeheads(@NotNull String projectCode) throws ProjectBridgeheadServiceException { - Optional project = projectRepository.findByCode(projectCode); - if (project.isEmpty()) { - throw new ProjectBridgeheadServiceException("Project " + projectCode + " not found"); - } - Set projectBridgeheads = projectBridgeheadRepository.findByProject(project.get()); - if (isProjectManagerAdmin()) { - return new ArrayList<>(projectBridgeheads).stream().map(dtoFactory::convert).toList(); - } - Set tempProjectBridgeheads = new HashSet<>(); - projectBridgeheads.forEach(projectBridgehead -> { - if (isBridgeheadAdminOfProjectBridgehead(projectBridgehead) || isUserOfProjectBridgehead(projectBridgehead)) { - tempProjectBridgeheads.add(projectBridgehead); - } - }); - return new ArrayList<>(tempProjectBridgeheads).stream().map(dtoFactory::convert).toList(); - } - - public List fetchProjectBridgeheads(@NotNull String projectCode) throws ProjectBridgeheadServiceException { - Optional project = projectRepository.findByCode(projectCode); - if (project.isEmpty()) { - throw new ProjectBridgeheadServiceException("Project " + projectCode + " not found"); - } - return projectBridgeheadRepository.findByProject(project.get()).stream().map(dtoFactory::convert).toList(); + public Optional fetchProjectBridgehead(String projectCode, String bridgehead) throws ProjectBridgeheadServiceException { + return projectBridgeheadRepository.findFirstByBridgeheadAndProject_Code(bridgehead, projectCode); } - private boolean isProjectManagerAdmin() { + protected boolean isProjectManagerAdmin() { return sessionUser.getUserOrganisationRoles().containsRole(OrganisationRole.PROJECT_MANAGER_ADMIN); } - private boolean isBridgeheadAdminOfProjectBridgehead(ProjectBridgehead projectBridgehead) { + protected boolean isBridgeheadAdminOfProjectBridgehead(ProjectBridgehead projectBridgehead) { return sessionUser.getUserOrganisationRoles().containsRole(OrganisationRole.BRIDGEHEAD_ADMIN, Optional.of(projectBridgehead.getBridgehead())); } - private boolean isUserOfProjectBridgehead(ProjectBridgehead projectBridgehead) { + protected boolean isUserOfProjectBridgehead(ProjectBridgehead projectBridgehead) { if (projectBridgehead.getProject().getCreatorEmail().equals(sessionUser.getEmail())) { for (String bridgehead : sessionUser.getBridgeheads()) { if (projectBridgehead.getBridgehead().equals(bridgehead)) { @@ -112,87 +91,133 @@ private boolean isUserOfProjectBridgehead(ProjectBridgehead projectBridgehead) { return !projectBridgeheadUserRepository.getByEmailAndProjectBridgehead(sessionUser.getEmail(), projectBridgehead).isEmpty(); } - public void scheduleSendQueryToBridgehead(@NotNull String projectCode, @NotNull String bridgehead) throws ProjectBridgeheadServiceException { - changeQueryState(projectCode, bridgehead, QueryState.TO_BE_SENT); + protected boolean isUserCreatorOfProject(Project project) { + return project.getCreatorEmail().equals(sessionUser.getEmail()); } - public void scheduleSendQueryToBridgeheadAndExecute(@NotNull String projectCode, @NotNull String bridgehead) throws ProjectBridgeheadServiceException { - changeQueryState(projectCode, bridgehead, QueryState.TO_BE_SENT_AND_EXECUTED); + public void scheduleSendQueryToBridgehead(@NotNull ProjectBridgehead bridgehead, @NotNull ProjectType projectType) throws ProjectBridgeheadServiceException { + changeQueryState(bridgehead, QueryState.TO_BE_SENT, projectType); } - private void changeQueryState(String projectCode, String bridgehead, QueryState queryState) throws ProjectBridgeheadServiceException { - Optional project = projectRepository.findByCode(projectCode); - if (project.isEmpty()) { - throw new ProjectBridgeheadServiceException("Project not found: " + projectCode); - } - Optional projectBridgehead = projectBridgeheadRepository.findFirstByBridgeheadAndProject(bridgehead, project.get()); - if (projectBridgehead.isEmpty()) { - throw new ProjectBridgeheadServiceException("Bridghead " + bridgehead + " in project " + projectCode + " not found"); - } - projectBridgehead.get().setQueryState(queryState); - projectBridgehead.get().setModifiedAt(Instant.now()); - projectBridgehead.get().setExporterUser(sessionUser.getEmail()); - projectBridgehead.get().setExporterExecutionId(null); - projectBridgehead.get().setExporterResponse(null); - projectBridgeheadRepository.save(projectBridgehead.get()); - } - - public void addResultsUrl(@NotNull String projectCode, @NotNull String bridgehead, @NotNull String resultsUrl) throws ProjectBridgeheadServiceException { - Optional project = projectRepository.findByCode(projectCode); - if (project.isEmpty()) { - throw new ProjectBridgeheadServiceException("Project not found: " + projectCode); - } - Optional projectBridgehead = projectBridgeheadRepository.findFirstByBridgeheadAndProject(bridgehead, project.get()); - if (projectBridgehead.isEmpty()) { - throw new ProjectBridgeheadServiceException("Bridghead " + bridgehead + " in project " + projectCode + " not found"); - } - projectBridgehead.get().setResultsUrl(resultsUrl); - projectBridgehead.get().setCreatorResultsState(UserProjectState.CREATED); // The creator should accept the results again - projectBridgeheadRepository.save(projectBridgehead.get()); + public void scheduleSendQueryToBridgeheadAndExecute(@NotNull ProjectBridgehead bridgehead, @NotNull ProjectType projectType) throws ProjectBridgeheadServiceException { + changeQueryState(bridgehead, QueryState.TO_BE_SENT_AND_EXECUTED, projectType); } - public void acceptResultsForCreator(@NotNull String projectCode, @NotNull String bridgehead) throws ProjectBridgeheadServiceException { - changeCreatorResultsState(projectCode, bridgehead, UserProjectState.ACCEPTED); + private void changeQueryState(ProjectBridgehead bridgehead, QueryState queryState, ProjectType projectType) throws ProjectBridgeheadServiceException { + bridgehead.addOrUpdateExecution(projectType, queryState, null, sessionUser.getEmail(), null, null); + setModifiedAtAndSaveBridgehead(bridgehead); + applicationEventPublisher.publishEvent(new SendQueryToBridgeheadEvent()); } - public void rejectResultsForCreator(@NotNull String projectCode, @NotNull String bridgehead) throws ProjectBridgeheadServiceException { - changeCreatorResultsState(projectCode, bridgehead, UserProjectState.REJECTED); + public void addResultsUrl(@NotNull ProjectBridgehead bridgehead, @NotNull String resultsUrl) throws ProjectBridgeheadServiceException { + bridgehead.setResultsUrl(resultsUrl); + bridgehead.setCreatorResultsState(UserProjectState.CREATED); // The creator should accept the results again + setModifiedAtAndSaveBridgehead(bridgehead); } - public void requestChangesInResultsForCreator(@NotNull String projectCode, @NotNull String bridgehead) throws ProjectBridgeheadServiceException { - changeCreatorResultsState(projectCode, bridgehead, UserProjectState.REQUEST_CHANGES); + public void acceptResultsForCreator(@NotNull ProjectBridgehead bridgehead) throws ProjectBridgeheadServiceException { + changeCreatorResultsState(bridgehead, UserProjectState.ACCEPTED); } - private void changeCreatorResultsState(@NotNull String projectCode, @NotNull String bridgehead, @NotNull UserProjectState state) throws ProjectBridgeheadServiceException { - Optional project = projectRepository.findByCode(projectCode); - if (project.isEmpty()) { - throw new ProjectBridgeheadServiceException("Project not found: " + projectCode); - } - Optional projectBridgehead = projectBridgeheadRepository.findFirstByBridgeheadAndProject(bridgehead, project.get()); - if (projectBridgehead.isEmpty()) { - throw new ProjectBridgeheadServiceException("Bridghead " + bridgehead + " in project " + projectCode + " not found"); - } - projectBridgehead.get().setCreatorResultsState(state); - projectBridgeheadRepository.save(projectBridgehead.get()); + public void rejectResultsForCreator(@NotNull ProjectBridgehead bridgehead) throws ProjectBridgeheadServiceException { + changeCreatorResultsState(bridgehead, UserProjectState.REJECTED); } - public List fetchResults(@NotNull String projectCode) throws ProjectBridgeheadServiceException { - Optional project = projectRepository.findByCode(projectCode); - if (project.isEmpty()) { - throw new ProjectBridgeheadServiceException("Project not found: " + projectCode); - } - return projectBridgeheadRepository.findByProject(project.orElseThrow()) - .stream() - .map(dtoFactory::fetchResults) - .toList(); // Uses the modern `toList()` instead of `Collectors.toList()` + public void requestChangesInResultsForCreator(@NotNull ProjectBridgehead bridgehead) throws ProjectBridgeheadServiceException { + changeCreatorResultsState(bridgehead, UserProjectState.REQUEST_CHANGES); } - public Optional fetchResultsOfOwnBridgehead(@NotNull String projectCode, @NotNull String bridgehead) throws ProjectBridgeheadServiceException { - Optional project = projectRepository.findByCode(projectCode); - if (project.isEmpty()) { - throw new ProjectBridgeheadServiceException("Project not found: " + projectCode); - } - return projectBridgeheadRepository.findFirstByBridgeheadAndProject(bridgehead, project.get()).map(dtoFactory::fetchResults); + private void changeCreatorResultsState(@NotNull ProjectBridgehead bridgehead, @NotNull UserProjectState state) throws ProjectBridgeheadServiceException { + bridgehead.setCreatorResultsState(state); + setModifiedAtAndSaveBridgehead(bridgehead); + } + + public void saveBridgehead(@NotNull ProjectBridgehead bridgehead) throws ProjectBridgeheadServiceException { + addAllExecutions(bridgehead); + setModifiedAtAndSaveBridgehead(bridgehead); + } + + private void setModifiedAtAndSaveBridgehead(@NotNull ProjectBridgehead bridgehead) throws ProjectBridgeheadServiceException { + bridgehead.setModifiedAt(Instant.now()); + projectBridgeheadRepository.save(bridgehead); + } + + public Set fetchBridgeheads(Project project) { + return projectBridgeheadRepository.findByProject(project); } + public Set fetchBridgeheads(Project project, ProjectBridgeheadState projectBridgeheadState) { + return projectBridgeheadRepository.findByProjectAndState(project, projectBridgeheadState); + } + + public Optional fetchBridgehead(Project project, String bridgehead) { + return projectBridgeheadRepository.findFirstByBridgeheadAndProject(bridgehead, project); + } + + public Optional fetchDataShield(ProjectBridgehead bridgehead) { + return projectBridgeheadDataShieldRepository.findByProjectBridgehead(bridgehead); + } + + public void saveDataShield(ProjectBridgeheadDataShield dataShield) { + projectBridgeheadDataShieldRepository.save(dataShield); + } + + public Set fetchBridgeheads(QueryState queryState, Set projectStates) { + return projectBridgeheadRepository.getByQueryStateAndProjectState(queryState, projectStates); + } + + public void deleteBridgehead(ProjectBridgehead bridgehead) { + projectBridgeheadRepository.delete(bridgehead); + } + + public List fetchByProjectTypeAndNotProjectState(ProjectType projectType, Set projectStates) { + return projectBridgeheadRepository.getByProjectTypeAndNotProjectState(projectType, projectStates); + } + + private void addAllExecutions(ProjectBridgehead bridgehead) { + bridgehead + .getProject() + .getQuery() + .fetchProjectTypes() + .forEach(projectType -> addOrUpdateExecution(bridgehead, projectType)); + } + + private void addOrUpdateExecution(ProjectBridgehead bridgehead, ProjectType projectType) { + bridgehead.addOrUpdateExecution( + projectType, + null, + null, + null, + null, + null + ); + } + + /** + * REQUIRES_NEW is mandatory because this method is called after the original transaction + * has already committed — there is no active transaction to join, so Spring must open a + * fresh one to allow JPA dirty-checking and orphanRemoval to work correctly. + */ + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void handleChange(Long queryId) { + projectBridgeheadRepository + .findByProject_Query_Id(queryId) + .forEach(this::updateQueryInBridgehead); + } + + private void updateQueryInBridgehead(ProjectBridgehead bridgehead) { + Set expectedTypes = bridgehead.getProject().fetchProjectTypes(); + + // ADD missing executions + expectedTypes.forEach(type -> addOrUpdateExecution(bridgehead, type)); + // REMOVE obsolete executions + bridgehead.getExecutions().removeIf(exec -> + !expectedTypes.contains( + exec.getQueryOutput().getProjectType() + ) + ); + bridgehead.setModifiedAt(Instant.now()); + } + + } diff --git a/src/main/java/de/samply/project/ProjectBridgeheadServiceException.java b/src/main/java/de/samply/project/ProjectBridgeheadServiceException.java index f5db946f..c5af66ea 100644 --- a/src/main/java/de/samply/project/ProjectBridgeheadServiceException.java +++ b/src/main/java/de/samply/project/ProjectBridgeheadServiceException.java @@ -1,6 +1,6 @@ package de.samply.project; -public class ProjectBridgeheadServiceException extends Exception{ +public class ProjectBridgeheadServiceException extends RuntimeException { public ProjectBridgeheadServiceException(String message) { super(message); diff --git a/src/main/java/de/samply/project/ProjectBridgeheadUserService.java b/src/main/java/de/samply/project/ProjectBridgeheadUserService.java new file mode 100644 index 00000000..cd015396 --- /dev/null +++ b/src/main/java/de/samply/project/ProjectBridgeheadUserService.java @@ -0,0 +1,89 @@ +package de.samply.project; + +import de.samply.db.model.Project; +import de.samply.db.model.ProjectBridgehead; +import de.samply.db.model.ProjectBridgeheadUser; +import de.samply.db.repository.ProjectBridgeheadUserRepository; +import de.samply.project.state.ProjectBridgeheadState; +import de.samply.project.state.ProjectState; +import de.samply.query.QueryState; +import de.samply.user.roles.ProjectRole; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +@Service +public class ProjectBridgeheadUserService { + + private final ProjectBridgeheadUserRepository projectBridgeheadUserRepository; + + public ProjectBridgeheadUserService(ProjectBridgeheadUserRepository projectBridgeheadUserRepository) { + this.projectBridgeheadUserRepository = projectBridgeheadUserRepository; + } + + public List fetchProjects(String email) { + return projectBridgeheadUserRepository.findProjectsByEmail(email); + } + + public List fetchUsers(String email, ProjectBridgehead projectBridgehead) { + return projectBridgeheadUserRepository.getByEmailAndProjectBridgehead(email, projectBridgehead); + } + + public Optional fetchFirstUsersOrderByModifiedAtDesc(String email, ProjectBridgehead projectBridgehead) { + return projectBridgeheadUserRepository.getFirstByEmailAndProjectBridgeheadOrderByModifiedAtDesc(email, projectBridgehead); + } + + public Set fetchUsers(ProjectRole projectRole, Project project) { + return projectBridgeheadUserRepository.getDistinctByProjectRoleAndProjectCode(projectRole, project.getCode()); + } + + public Optional fetchFirstValidUser(String email, ProjectBridgehead projectBridgehead) { + return projectBridgeheadUserRepository.getFirstValidByEmailAndProjectBridgehead(email, projectBridgehead); + } + + public Set fetchUsersWithNotProjectRole(ProjectType projectType, ProjectState projectState, ProjectRole projectRole) { + return projectBridgeheadUserRepository.getByProjectTypeAndProjectStateAndNotProjectRole(projectType, projectState, projectRole); + } + + public Set fetchUsers(ProjectType projectType, ProjectState projectState, ProjectRole projectRole) { + return projectBridgeheadUserRepository.getByProjectTypeAndProjectStateAndProjectRole(projectType, projectState, projectRole); + } + + public List fetchUsersInValidProjectState(ProjectType projectType, QueryState queryState, ProjectBridgeheadState projectBridgeheadState) { + return projectBridgeheadUserRepository.getDistinctInValidProjectStateByProjectTypeAndQueryStateAndProjectBridgeheadState(projectType, queryState, projectBridgeheadState); + } + + public List fetchUsersInInvalidProjectState(ProjectType projectType) { + return projectBridgeheadUserRepository.getDistinctInInvalidProjectStateByProjectType(projectType); + } + + public List fetchUsers(ProjectBridgehead bridgehead) { + return projectBridgeheadUserRepository.getByProjectBridgehead(bridgehead); + } + + public Optional fetchFirstUser(String email, ProjectBridgehead projectBridgehead, ProjectRole projectRole) { + return projectBridgeheadUserRepository.findFirstByEmailAndProjectBridgeheadAndProjectRole(email, projectBridgehead, projectRole); + } + + public List fetchDistinctUsers(String email, ProjectBridgehead projectBridgehead){ + return projectBridgeheadUserRepository.getDistinctByEmailContainingAndProjectBridgehead(email, projectBridgehead); + } + + public List fetchUsers(ProjectRole role, ProjectBridgehead projectBridgehead){ + return projectBridgeheadUserRepository.getDistinctByProjectRoleAndProjectBridgehead(role, projectBridgehead); + } + + public List fetchUsersAlreadySetInThisProjectForThisRole(String email, ProjectBridgehead bridgehead){ + return projectBridgeheadUserRepository.getDistinctByEmailContainingAndProjectBridgehead_BridgeheadAndUserAlreadySetForThisProjectInThisRole(email, bridgehead.getBridgehead(), bridgehead.getProject().getCode()); + } + + public void saveUser(ProjectBridgeheadUser user) { + user.setModifiedAt(Instant.now()); + projectBridgeheadUserRepository.save(user); + } + + +} diff --git a/src/main/java/de/samply/project/ProjectConverter.java b/src/main/java/de/samply/project/ProjectConverter.java new file mode 100644 index 00000000..153bc29b --- /dev/null +++ b/src/main/java/de/samply/project/ProjectConverter.java @@ -0,0 +1,37 @@ +package de.samply.project; + +import de.samply.annotations.Bridgehead; +import de.samply.db.model.Project; +import de.samply.db.model.ProjectBridgehead; +import de.samply.resolvers.AnnotatedParametersWrapper; +import org.jspecify.annotations.NonNull; +import org.springframework.core.convert.converter.Converter; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.util.Optional; + +@Component +public class ProjectConverter implements Converter { + + private final ProjectService projectService; + private final AnnotatedParametersWrapper annotatedParametersWrapper; + + public ProjectConverter( + ProjectService projectService, + AnnotatedParametersWrapper annotatedParametersWrapper) { + this.projectService = projectService; + this.annotatedParametersWrapper = annotatedParametersWrapper; + } + + @Override + public Project convert(@NonNull String projectCode) { + if (!StringUtils.hasText(projectCode)) return null; + Optional projectBridgeheadOptional = + annotatedParametersWrapper.getResolved(Bridgehead.class, ProjectBridgehead.class); + return projectBridgeheadOptional + .map(ProjectBridgehead::getProject) + .orElse(projectService.fetchProject(projectCode)); + } + +} diff --git a/src/main/java/de/samply/project/ProjectService.java b/src/main/java/de/samply/project/ProjectService.java index 13ff671c..9847fb7e 100644 --- a/src/main/java/de/samply/project/ProjectService.java +++ b/src/main/java/de/samply/project/ProjectService.java @@ -3,12 +3,10 @@ import de.samply.app.ProjectManagerConst; import de.samply.db.model.Project; import de.samply.db.model.ProjectBridgehead; -import de.samply.db.repository.ProjectBridgeheadRepository; -import de.samply.db.repository.ProjectBridgeheadUserRepository; import de.samply.db.repository.ProjectRepository; -import de.samply.db.repository.QueryRepository; +import de.samply.form.FormService; import de.samply.frontend.dto.DtoFactory; -import de.samply.frontend.dto.Results; +import de.samply.frontend.dto.Form; import de.samply.frontend.dto.configuration.ProjectConfigurations; import de.samply.notification.NotificationService; import de.samply.notification.OperationType; @@ -16,6 +14,7 @@ import de.samply.project.state.ProjectState; import de.samply.project.state.UserProjectState; import de.samply.query.OutputFormat; +import de.samply.query.QueryPersistenceService; import de.samply.security.SessionUser; import de.samply.user.roles.OrganisationRole; import jakarta.validation.constraints.NotNull; @@ -24,83 +23,83 @@ import org.springframework.stereotype.Service; import java.time.Instant; +import java.time.LocalDate; import java.util.*; @Service public class ProjectService { - private final NotificationService notificationService; - private final ProjectRepository projectRepository; - private final QueryRepository queryRepository; - private final ProjectBridgeheadRepository projectBridgeheadRepository; + private final SessionUser sessionUser; - private final ProjectBridgeheadUserRepository projectBridgeheadUserRepository; private final ProjectConfigurations projectConfigurations; - private final DtoFactory dtoFactory; + + // Services + private final NotificationService notificationService; + private final FormService formService; + private final QueryPersistenceService queryPersistenceService; + private final ProjectBridgeheadService projectBridgeheadService; + private final ProjectBridgeheadUserService projectBridgeheadUserService; + + // Repositories + private final ProjectRepository projectRepository; + public ProjectService(NotificationService notificationService, ProjectRepository projectRepository, - QueryRepository queryRepository, - ProjectBridgeheadRepository projectBridgeheadRepository, SessionUser sessionUser, - ProjectBridgeheadUserRepository projectBridgeheadUserRepository, ProjectConfigurations projectConfigurations, - DtoFactory dtoFactory) { + FormService formService, + QueryPersistenceService queryPersistenceService, + ProjectBridgeheadService projectBridgeheadService, + ProjectBridgeheadUserService projectBridgeheadUserService) { this.notificationService = notificationService; this.projectRepository = projectRepository; - this.queryRepository = queryRepository; - this.projectBridgeheadRepository = projectBridgeheadRepository; + this.queryPersistenceService = queryPersistenceService; + this.projectBridgeheadService = projectBridgeheadService; this.sessionUser = sessionUser; - this.projectBridgeheadUserRepository = projectBridgeheadUserRepository; this.projectConfigurations = projectConfigurations; - this.dtoFactory = dtoFactory; - } - - public de.samply.frontend.dto.Project fetchProject(@NotNull String projectCode) throws ProjectServiceException { - Optional projectOptional = this.projectRepository.findByCode(projectCode); - if (projectOptional.isEmpty()) { - throw new ProjectServiceException("Project " + projectCode + " not found"); - } - return dtoFactory.convert(projectOptional.get()); + this.formService = formService; + this.projectBridgeheadUserService = projectBridgeheadUserService; } - public void editProject(@NotNull String projectCode, ProjectType type, String[] bridgeheads) { - Optional projectOptional = this.projectRepository.findByCode(projectCode); - boolean hasChanged = false; - if (projectOptional.isPresent()) { - if (type != null) { - projectOptional.get().setType(type); - this.notificationService.createNotification(projectCode, null, sessionUser.getEmail(), - OperationType.EDIT_PROJECT, "Changed project type to " + type, null, null); - hasChanged = true; - } - if (hasChanged) { - saveProject(projectOptional.get()); - } - if (bridgeheads != null && bridgeheads.length > 0) { - updateBridgeheads(projectOptional.get(), bridgeheads); - } + public Project fetchProject(@NotNull String projectCode) throws ProjectServiceException { + Optional project = projectRepository.findByCode(projectCode); + if (project.isEmpty()) { + throw new ProjectServiceException("ProjectCode " + projectCode + " not found"); } + return project.get(); } - private void saveProject(@NotNull Project project){ + public void saveProject(@NotNull Project project) { project.setModifiedAt(Instant.now()); projectRepository.save(project); } - private void updateBridgeheads(Project project, String[] bridgeheads) { + public void updateBridgeheads(Project project, String[] bridgeheads) { Set editionBridgeheads = Set.of(bridgeheads); + // Remove bridgeheads that are no longer present - projectBridgeheadRepository.findByProject(project).stream().filter(projectBridgehead -> - !editionBridgeheads.contains(projectBridgehead.getBridgehead())).forEach(projectBridgehead -> - projectBridgeheadRepository.delete(projectBridgehead)); + projectBridgeheadService + .fetchBridgeheads(project) + .stream() + .filter(projectBridgehead -> + !editionBridgeheads.contains(projectBridgehead.getBridgehead())) + .forEach(projectBridgeheadService::deleteBridgehead); + // Add new bridgeheads - Set oldBridgeheads = new HashSet<>(projectBridgeheadRepository.findByProject(project).stream(). - map(projectBridgehead -> projectBridgehead.getBridgehead()).toList()); - editionBridgeheads.stream().filter(bridgehead -> !oldBridgeheads.contains(bridgehead)).forEach(bridgehead -> - createProjectBridgehead(project, bridgehead)); - this.notificationService.createNotification(project.getCode(), null, sessionUser.getEmail(), - OperationType.EDIT_PROJECT, "Changed bridgeheads: " + String.join("," + bridgeheads), null, null); + Set oldBridgeheads = new HashSet<>(projectBridgeheadService + .fetchBridgeheads(project) + .stream() + .map(ProjectBridgehead::getBridgehead) + .toList()); + + editionBridgeheads + .stream() + .filter(bridgehead -> !oldBridgeheads.contains(bridgehead)) + .forEach(bridgehead -> createProjectBridgehead(project, bridgehead)); + + this.notificationService.createNotification(project, null, sessionUser.getEmail(), + OperationType.EDIT_PROJECT, "Changed bridgeheads: " + String.join("," + Arrays.toString(bridgeheads)), null, null); } @@ -109,12 +108,11 @@ private void createProjectBridgehead(Project project, String bridgehead) { projectBridgehead.setBridgehead(bridgehead); projectBridgehead.setProject(project); projectBridgehead.setState(ProjectBridgeheadState.CREATED); - projectBridgehead.setModifiedAt(Instant.now()); - projectBridgeheadRepository.save(projectBridgehead); + projectBridgeheadService.saveBridgehead(projectBridgehead); } public List fetchAllUserVisibleProjects() { - // Fetch projects as project manager + // Fetch projects as a project manager if (isProjectManagerAdmin()) { return projectRepository.findAll(); } @@ -122,32 +120,17 @@ public List fetchAllUserVisibleProjects() { // Fetch projects as bridgehead admin // We make an assumption: A bridgehead admin is bridgehead admin in all of their bridgeheads. if (isBridgeheadAdmin()) { - return projectRepository.findByBridgeheads(bridgeheads); + return projectRepository.findByBridgeheadsOrCreator(sessionUser.getEmail(), bridgeheads); } - // Fetch projects as researcher - return projectBridgeheadUserRepository.findProjectsByEmail(sessionUser.getEmail()); + // Fetch projects as a researcher + return projectBridgeheadUserService.fetchProjects(sessionUser.getEmail()); } - public Page fetchUserVisibleProjects( - Optional projectState, Optional archived, int page, int pageSize, - boolean modifiedDescendant) { - PageRequest pageRequest = PageRequest.of(page, pageSize); - if (isProjectManagerAdmin()) { - return fetchProjectManagerAdminProjects(projectState, archived, pageRequest, modifiedDescendant).map(dtoFactory::convert); - } - Set bridgeheads = sessionUser.getBridgeheads(); - // We make an assumption: A bridgehead admin is bridgehead admin in all of their bridgeheads. - if (isBridgeheadAdmin()) { - return fetchBridgeheadAdminProjects(bridgeheads, projectState, archived, pageRequest, modifiedDescendant).map(dtoFactory::convert); - } - return fetchResearcherProjects(sessionUser.getEmail(), bridgeheads, projectState, archived, pageRequest, modifiedDescendant).map(dtoFactory::convert); - } - - private boolean isProjectManagerAdmin() { + protected boolean isProjectManagerAdmin() { return sessionUser.getUserOrganisationRoles().containsRole(OrganisationRole.PROJECT_MANAGER_ADMIN); } - private boolean isBridgeheadAdmin() { + protected boolean isBridgeheadAdmin() { for (String bridgehead : sessionUser.getBridgeheads()) { if (sessionUser.getUserOrganisationRoles() .getBridgeheadRoles(bridgehead).contains(OrganisationRole.BRIDGEHEAD_ADMIN)) { @@ -157,7 +140,7 @@ private boolean isBridgeheadAdmin() { return false; } - private Page fetchProjectManagerAdminProjects( + protected Page fetchProjectManagerAdminProjects( Optional projectState, Optional archived, PageRequest pageRequest, boolean modifiedDescendant) { if (projectState.isEmpty()) { @@ -207,186 +190,190 @@ private Page fetchProjectManagerAdminProjects( } } - private Page fetchBridgeheadAdminProjects( + protected Page fetchBridgeheadAdminProjects( Set bridgeheads, Optional projectState, Optional archived, PageRequest pageRequest, boolean modifiedDescendant) { if (projectState.isEmpty()) { if (archived.isEmpty()) { if (modifiedDescendant) { - return projectRepository.findByBridgeheadsModifiedAtDesc(bridgeheads, pageRequest); + return projectRepository.findByBridgeheadsOrCreatorModifiedAtDesc(sessionUser.getEmail(), bridgeheads, pageRequest); } else { - return projectRepository.findByBridgeheadsModifiedAtAsc(bridgeheads, pageRequest); + return projectRepository.findByBridgeheadsOrCreatorModifiedAtAsc(sessionUser.getEmail(), bridgeheads, pageRequest); } } else { if (archived.get()) { if (modifiedDescendant) { - return projectRepository.findArchivedProjectsByBridgeheadsModifiedAtDesc(bridgeheads, pageRequest); + return projectRepository.findArchivedProjectsByBridgeheadsOrCreatorModifiedAtDesc(sessionUser.getEmail(), bridgeheads, pageRequest); } else { - return projectRepository.findArchivedProjectsByBridgeheadsModifiedAtAsc(bridgeheads, pageRequest); + return projectRepository.findArchivedProjectsByBridgeheadsOrCreatorModifiedAtAsc(sessionUser.getEmail(), bridgeheads, pageRequest); } } else { if (modifiedDescendant) { - return projectRepository.findNotArchivedProjectsByBridgeheadsModifiedAtDesc(bridgeheads, pageRequest); + return projectRepository.findNotArchivedProjectsByBridgeheadsOrCreatorModifiedAtDesc(sessionUser.getEmail(), bridgeheads, pageRequest); } else { - return projectRepository.findNotArchivedProjectsByBridgeheadsModifiedAtAsc(bridgeheads, pageRequest); + return projectRepository.findNotArchivedProjectsByBridgeheadsOrCreatorModifiedAtAsc(sessionUser.getEmail(), bridgeheads, pageRequest); } } } } else { if (archived.isEmpty()) { if (modifiedDescendant) { - return projectRepository.findByStateAndBridgeheadsModifiedAtDesc(projectState.get(), bridgeheads, pageRequest); + return projectRepository.findByStateAndBridgeheadsOrCreatorModifiedAtDesc(sessionUser.getEmail(), projectState.get(), bridgeheads, pageRequest); } else { - return projectRepository.findByStateAndBridgeheadsModifiedAtAsc(projectState.get(), bridgeheads, pageRequest); + return projectRepository.findByStateAndBridgeheadsOrCreatorModifiedAtAsc(sessionUser.getEmail(), projectState.get(), bridgeheads, pageRequest); } } else { if (archived.get()) { if (modifiedDescendant) { - return projectRepository.findArchivedProjectsByStateAndBridgeheadsModifiedAtDesc(projectState.get(), bridgeheads, pageRequest); + return projectRepository.findArchivedProjectsByStateAndBridgeheadsOrCreatorModifiedAtDesc(sessionUser.getEmail(), projectState.get(), bridgeheads, pageRequest); } else { - return projectRepository.findArchivedProjectsByStateAndBridgeheadsModifiedAtAsc(projectState.get(), bridgeheads, pageRequest); + return projectRepository.findArchivedProjectsByStateAndBridgeheadsOrCreatorModifiedAtAsc(sessionUser.getEmail(), projectState.get(), bridgeheads, pageRequest); } } else { if (modifiedDescendant) { - return projectRepository.findNotArchivedProjectsByStateAndBridgeheadsModifiedAtDesc(projectState.get(), bridgeheads, pageRequest); + return projectRepository.findNotArchivedProjectsByStateAndBridgeheadsOrCreatorModifiedAtDesc(sessionUser.getEmail(), projectState.get(), bridgeheads, pageRequest); } else { - return projectRepository.findNotArchivedProjectsByStateAndBridgeheadsModifiedAtAsc(projectState.get(), bridgeheads, pageRequest); + return projectRepository.findNotArchivedProjectsByStateAndBridgeheadsOrCreatorModifiedAtAsc(sessionUser.getEmail(), projectState.get(), bridgeheads, pageRequest); } } } } } - private Page fetchResearcherProjects(String - email, Set bridgeheads, Optional projectState, - Optional archived, PageRequest pageRequest, boolean modifiedDescendant) { + protected Page fetchResearcherProjects(String + email, Set bridgeheads, Optional projectState, + Optional archived, PageRequest pageRequest, boolean modifiedDescendant) { if (projectState.isEmpty()) { if (archived.isEmpty()) { if (modifiedDescendant) { - return projectRepository.findByEmailAndBridgeheadsModifiedAtDesc(email, bridgeheads, pageRequest); + return projectRepository.findByEmailAndBridgeheadsOrCreatorModifiedAtDesc(email, bridgeheads, pageRequest); } else { - return projectRepository.findByEmailAndBridgeheadsModifiedAtAsc(email, bridgeheads, pageRequest); + return projectRepository.findByEmailAndBridgeheadsOrCreatorModifiedAtAsc(email, bridgeheads, pageRequest); } } else { if (archived.get()) { if (modifiedDescendant) { - return projectRepository.findArchivedProjectsByEmailAndBridgeheadsModifiedAtDesc(email, bridgeheads, pageRequest); + return projectRepository.findArchivedProjectsByEmailAndBridgeheadsOrCreatorModifiedAtDesc(email, bridgeheads, pageRequest); } else { - return projectRepository.findArchivedProjectsByEmailAndBridgeheadsModifiedAtAsc(email, bridgeheads, pageRequest); + return projectRepository.findArchivedProjectsByEmailAndBridgeheadsOrCreatorModifiedAtAsc(email, bridgeheads, pageRequest); } } else { if (modifiedDescendant) { - return projectRepository.findNotArchivedProjectsByEmailAndBridgeheadsModifiedAtDesc(email, bridgeheads, pageRequest); + return projectRepository.findNotArchivedProjectsByEmailAndBridgeheadsOrCreatorModifiedAtDesc(email, bridgeheads, pageRequest); } else { - return projectRepository.findNotArchivedProjectsByEmailAndBridgeheadsModifiedAtAsc(email, bridgeheads, pageRequest); + return projectRepository.findNotArchivedProjectsByEmailAndBridgeheadsOrCreatorModifiedAtAsc(email, bridgeheads, pageRequest); } } } } else { if (archived.isEmpty()) { if (modifiedDescendant) { - return projectRepository.findByEmailAndStateAndBridgeheadsModifiedAtDesc(email, projectState.get(), bridgeheads, pageRequest); + return projectRepository.findByEmailAndStateAndBridgeheadsOrCreatorModifiedAtDesc(email, projectState.get(), bridgeheads, pageRequest); } else { - return projectRepository.findByEmailAndStateAndBridgeheadsModifiedAtAsc(email, projectState.get(), bridgeheads, pageRequest); + return projectRepository.findByEmailAndStateAndBridgeheadsOrCreatorModifiedAtAsc(email, projectState.get(), bridgeheads, pageRequest); } } else { if (archived.get()) { if (modifiedDescendant) { - return projectRepository.findArchivedProjectsByEmailAndStateAndBridgeheadsModifiedAtDesc(email, projectState.get(), bridgeheads, pageRequest); + return projectRepository.findArchivedProjectsByEmailAndStateAndBridgeheadsOrCreatorModifiedAtDesc(email, projectState.get(), bridgeheads, pageRequest); } else { - return projectRepository.findArchivedProjectsByEmailAndStateAndBridgeheadsModifiedAtAsc(email, projectState.get(), bridgeheads, pageRequest); + return projectRepository.findArchivedProjectsByEmailAndStateAndBridgeheadsOrCreatorModifiedAtAsc(email, projectState.get(), bridgeheads, pageRequest); } } else { if (modifiedDescendant) { - return projectRepository.findNotArchivedProjectsByEmailAndStateAndBridgeheadsModifiedAtDesc(email, projectState.get(), bridgeheads, pageRequest); + return projectRepository.findNotArchivedProjectsByEmailAndStateAndBridgeheadsOrCreatorModifiedAtDesc(email, projectState.get(), bridgeheads, pageRequest); } else { - return projectRepository.findNotArchivedProjectsByEmailAndStateAndBridgeheadsModifiedAtAsc(email, projectState.get(), bridgeheads, pageRequest); + return projectRepository.findNotArchivedProjectsByEmailAndStateAndBridgeheadsOrCreatorModifiedAtAsc(email, projectState.get(), bridgeheads, pageRequest); } } } } } - public OutputFormat[] fetchOutputFormats(@NotNull String projectCode) throws ProjectServiceException { - Optional projectOptional = this.projectRepository.findByCode(projectCode); - if (projectOptional.isEmpty()) { - throw new ProjectServiceException("Project " + projectCode + " not found"); - } - if (projectOptional.get().getType() == null){ - return OutputFormat.values(); - } - return switch (projectOptional.get().getType()) { - case DATASHIELD -> new OutputFormat[]{OutputFormat.OPAL}; - default -> - Arrays.stream(OutputFormat.values()).filter(outputFormat -> outputFormat != OutputFormat.OPAL).toArray(OutputFormat[]::new); - }; + public Map> fetchOutputFormats(@NotNull Project project) throws ProjectServiceException { + Map> result = new HashMap<>(); + project.fetchProjectTypes().forEach(projectType -> + result.put(projectType, fetchOutputFormats(projectType))); + return result; } - public Map fetchCurrentProjectConfiguration(@NotNull String projectCode) throws ProjectServiceException { - Optional projectOptional = this.projectRepository.findByCode(projectCode); - if (projectOptional.isEmpty()) { - throw new ProjectServiceException("Project " + projectCode + " not found"); - } - return this.projectConfigurations.fetchCurrentProjectConfiguration(dtoFactory.convert(projectOptional.get())); + private List fetchOutputFormats(ProjectType projectType) { + return (projectType == ProjectType.DATASHIELD) ? + List.of(OutputFormat.OPAL) : + Arrays.stream(OutputFormat.values()).filter(outputFormat -> outputFormat != OutputFormat.OPAL).toList(); } - public void setProjectConfiguration(@NotNull String projectCode, @NotNull String projectConfigurationName) throws ProjectServiceException { - Optional projectOptional = this.projectRepository.findByCode(projectCode); - if (projectOptional.isEmpty()) { - throw new ProjectServiceException("Project " + projectCode + " not found"); - } - if (!projectConfigurationName.equals(ProjectManagerConst.CUSTOM_PROJECT_CONFIGURATION)) { - de.samply.frontend.dto.Project projectConfiguration = this.projectConfigurations.getConfig().get(projectConfigurationName); - if (projectConfiguration == null) { - throw new ProjectServiceException("Project configuration " + projectConfigurationName + " not found"); + public void setProjectConfiguration(@NotNull Project project, @NotNull String projectConfigurationNames) throws ProjectServiceException { + if (!projectConfigurationNames.equals(ProjectManagerConst.CUSTOM_PROJECT_CONFIGURATION)) { + String[] names = projectConfigurationNames.split(","); + List allOutputs = new ArrayList<>(); + List allFormTitles = new ArrayList<>(); + + for (String rawName : names) { + String name = rawName.trim(); + de.samply.frontend.dto.ProjectAndForms paf = this.projectConfigurations.getConfig().get(name); + if (paf == null) { + throw new ProjectServiceException("ProjectCode configuration " + name + " not found"); + } + if (paf.project() != null && paf.project().getOutputs() != null) { + for (de.samply.frontend.dto.ProjectOutput output : paf.project().getOutputs()) { + if (allOutputs.stream().noneMatch(o -> o.projectType() == output.projectType())) { + allOutputs.add(output); + } + } + } + if (paf.forms() != null) { + Arrays.stream(paf.forms()).map(Form::title) + .filter(t -> !allFormTitles.contains(t)) + .forEach(allFormTitles::add); + } } - Project project = DtoFactory.convert(projectConfiguration, projectOptional.get()); - project.setCustomConfig(false); + + de.samply.frontend.dto.Project combinedDtoProject = new de.samply.frontend.dto.Project(); + combinedDtoProject.setOutputs(allOutputs.toArray(new de.samply.frontend.dto.ProjectOutput[0])); + + Project mergedProject = DtoFactory.merge(combinedDtoProject, project); + mergedProject.setIsCustomConfigSelected(false); + saveProject(mergedProject); + queryPersistenceService.saveQuery(mergedProject.getQuery()); + + if (!allFormTitles.isEmpty()) { + this.formService.syncSelectedForms(project, allFormTitles); + } + + this.formService.editProjectFormFieldValues(Optional.empty(), project); + + } else if (!Boolean.TRUE.equals(project.getIsCustomConfigSelected())) { + project.setIsCustomConfigSelected(true); saveProject(project); - this.queryRepository.save(project.getQuery()); - } else if (!projectOptional.get().isCustomConfig()) { - projectOptional.get().setCustomConfig(true); - saveProject(projectOptional.get()); } } - public void addProjectResultUrl(@NotNull String projectCode, @NotNull String resultUrl) throws ProjectServiceException { - Optional project = this.projectRepository.findByCode(projectCode); - if (project.isEmpty()) { - throw new ProjectServiceException("Project " + projectCode + " not found"); - } - project.get().setResultsUrl(resultUrl); - project.get().setCreatorResultsState(UserProjectState.CREATED); // The creator should accept the new results again - saveProject(project.get()); + public void addProjectResultUrl(@NotNull Project project, @NotNull String resultUrl) throws ProjectServiceException { + project.setResultsUrl(resultUrl); + project.setCreatorResultsState(UserProjectState.CREATED); // The creator should accept the new results again + saveProject(project); } - public void acceptResultsByCreator(@NotNull String projectCode) throws ProjectServiceException { - changeCreatorResultsState(projectCode, UserProjectState.ACCEPTED); + public void acceptResultsByCreator(@NotNull Project project) throws ProjectServiceException { + changeCreatorResultsState(project, UserProjectState.ACCEPTED); } - public void rejectResultsForCreator(@NotNull String projectCode) throws ProjectServiceException { - changeCreatorResultsState(projectCode, UserProjectState.REJECTED); + public void rejectResultsForCreator(@NotNull Project project) throws ProjectServiceException { + changeCreatorResultsState(project, UserProjectState.REJECTED); } - public void requestChangesInResultsForCreator(@NotNull String projectCode) throws ProjectServiceException { - changeCreatorResultsState(projectCode, UserProjectState.REQUEST_CHANGES); + public void requestChangesInResultsForCreator(@NotNull Project project) throws ProjectServiceException { + changeCreatorResultsState(project, UserProjectState.REQUEST_CHANGES); } - private void changeCreatorResultsState(@NotNull String projectCode, UserProjectState state) throws ProjectServiceException { - Optional project = this.projectRepository.findByCode(projectCode); - if (project.isEmpty()) { - throw new ProjectServiceException("Project " + projectCode + " not found"); - } - project.get().setCreatorResultsState(state); - saveProject(project.get()); + private void changeCreatorResultsState(@NotNull Project project, UserProjectState state) throws ProjectServiceException { + project.setCreatorResultsState(state); + saveProject(project); } - public Optional fetchResults(@NotNull String projectCode) throws ProjectServiceException { - Optional project = this.projectRepository.findByCode(projectCode); - if (project.isEmpty()) { - throw new ProjectServiceException("Project " + projectCode + " not found"); - } - return dtoFactory.fetchResults(project.get()); + public List findProjectByExpiresAtBeforeAndStateIn(LocalDate expirationTime, Set states) { + return projectRepository.findByExpiresAtBeforeAndStateIn(expirationTime, states); } } diff --git a/src/main/java/de/samply/project/ProjectServiceException.java b/src/main/java/de/samply/project/ProjectServiceException.java index 26c83bb6..11381fe0 100644 --- a/src/main/java/de/samply/project/ProjectServiceException.java +++ b/src/main/java/de/samply/project/ProjectServiceException.java @@ -1,6 +1,6 @@ package de.samply.project; -public class ProjectServiceException extends Exception { +public class ProjectServiceException extends RuntimeException { public ProjectServiceException(String message) { super(message); diff --git a/src/main/java/de/samply/project/ProjectType.java b/src/main/java/de/samply/project/ProjectType.java index 6b9371cd..e484d3c9 100644 --- a/src/main/java/de/samply/project/ProjectType.java +++ b/src/main/java/de/samply/project/ProjectType.java @@ -2,6 +2,8 @@ public enum ProjectType { EXPORT, + SAMPLES, // Interacts with Negotiator DATASHIELD, - RESEARCH_ENVIRONMENT + RESEARCH_ENVIRONMENT, + SEQUENCING } diff --git a/src/main/java/de/samply/project/SendQueryToBridgeheadEvent.java b/src/main/java/de/samply/project/SendQueryToBridgeheadEvent.java new file mode 100644 index 00000000..78c64baa --- /dev/null +++ b/src/main/java/de/samply/project/SendQueryToBridgeheadEvent.java @@ -0,0 +1,4 @@ +package de.samply.project; + +public record SendQueryToBridgeheadEvent() { +} diff --git a/src/main/java/de/samply/project/event/ProjectEventActions.java b/src/main/java/de/samply/project/event/ProjectEventActions.java index 049dc6f3..b911b74c 100644 --- a/src/main/java/de/samply/project/event/ProjectEventActions.java +++ b/src/main/java/de/samply/project/event/ProjectEventActions.java @@ -1,25 +1,26 @@ package de.samply.project.event; -import de.samply.project.ProjectType; +import de.samply.db.model.Project; +@SuppressWarnings("unused") public interface ProjectEventActions { - String draft(String[] bridgeheads, String queryCode, ProjectType projectType) throws ProjectEventActionsException; + String draft(String[] bridgeheads, String queryCode) throws ProjectEventActionsException; - void create(String projectCode) throws ProjectEventActionsException; + void create(Project project) throws ProjectEventActionsException; - void accept(String projectCode) throws ProjectEventActionsException; + void accept(Project project) throws ProjectEventActionsException; - void reject(String projectCode) throws ProjectEventActionsException; + void reject(Project project) throws ProjectEventActionsException; - void archive(String projectCode) throws ProjectEventActionsException; + void archive(Project project) throws ProjectEventActionsException; - void startDevelopStage(String projectCode) throws ProjectEventActionsException; + void startDevelopStage(Project project) throws ProjectEventActionsException; - void startPilotStage(String projectCode) throws ProjectEventActionsException; + void startPilotStage(Project project) throws ProjectEventActionsException; - void startFinalStage(String projectCode) throws ProjectEventActionsException; + void startFinalStage(Project project) throws ProjectEventActionsException; - void finish(String projectCode) throws ProjectEventActionsException; + void finish(Project project) throws ProjectEventActionsException; } diff --git a/src/main/java/de/samply/project/event/ProjectEventActionsException.java b/src/main/java/de/samply/project/event/ProjectEventActionsException.java index 1d61db0b..f02df5e3 100644 --- a/src/main/java/de/samply/project/event/ProjectEventActionsException.java +++ b/src/main/java/de/samply/project/event/ProjectEventActionsException.java @@ -2,22 +2,12 @@ public class ProjectEventActionsException extends RuntimeException { - public ProjectEventActionsException() { } - public ProjectEventActionsException(String message) { super(message); } - public ProjectEventActionsException(String message, Throwable cause) { - super(message, cause); - } - public ProjectEventActionsException(Throwable cause) { super(cause); } - public ProjectEventActionsException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } - } diff --git a/src/main/java/de/samply/project/event/ProjectEventService.java b/src/main/java/de/samply/project/event/ProjectEventService.java index 09462aa7..e0bf2466 100644 --- a/src/main/java/de/samply/project/event/ProjectEventService.java +++ b/src/main/java/de/samply/project/event/ProjectEventService.java @@ -4,17 +4,15 @@ import de.samply.db.model.Project; import de.samply.db.model.ProjectBridgehead; import de.samply.db.model.Query; -import de.samply.db.repository.ProjectBridgeheadRepository; -import de.samply.db.repository.ProjectRepository; -import de.samply.db.repository.QueryRepository; import de.samply.notification.NotificationService; import de.samply.notification.OperationType; +import de.samply.project.ProjectBridgeheadService; +import de.samply.project.ProjectService; import de.samply.project.ProjectType; -import de.samply.project.state.ProjectBridgeheadState; import de.samply.project.state.ProjectState; +import de.samply.query.QueryService; import de.samply.security.SessionUser; import de.samply.user.UserService; -import de.samply.user.roles.ProjectRole; import de.samply.utils.LogUtils; import jakarta.validation.constraints.NotNull; import org.springframework.beans.factory.annotation.Value; @@ -32,99 +30,93 @@ import java.time.Instant; import java.time.LocalDate; -import java.util.Arrays; -import java.util.Optional; -import java.util.UUID; +import java.util.*; import java.util.function.Consumer; @Service public class ProjectEventService implements ProjectEventActions { + // Services private final NotificationService notificationService; - private final ProjectRepository projectRepository; - private final QueryRepository queryRepository; + private final ProjectService projectService; + private final UserService userService; + private final QueryService queryService; + private final ProjectBridgeheadService projectBridgeheadService; + + private final StateMachineFactory projectStateMachineFactory; private final LogUtils logUtils; - private final ProjectBridgeheadRepository projectBridgeheadRepository; - private final UserService userService; private final SessionUser sessionUser; + private final int projectExpirationTimeInDays; public ProjectEventService(NotificationService notificationService, - ProjectRepository projectRepository, - QueryRepository queryRepository, + ProjectService projectService, StateMachineFactory projectStateMachineFactory, LogUtils logUtils, - ProjectBridgeheadRepository projectBridgeheadRepository, SessionUser sessionUser, + @Value(ProjectManagerConst.PROJECT_DEFAULT_EXPIRATION_TIME_IN_DAYS_SV) int projectExpirationTimeInDays, UserService userService, - @Value(ProjectManagerConst.PROJECT_DEFAULT_EXPIRATION_TIME_IN_DAYS_SV) int projectExpirationTimeInDays) { + QueryService queryService, + ProjectBridgeheadService projectBridgeheadService) { this.notificationService = notificationService; - this.projectRepository = projectRepository; - this.queryRepository = queryRepository; + this.projectService = projectService; + this.queryService = queryService; this.projectStateMachineFactory = projectStateMachineFactory; this.logUtils = logUtils; - this.projectBridgeheadRepository = projectBridgeheadRepository; - this.userService = userService; + this.projectBridgeheadService = projectBridgeheadService; this.sessionUser = sessionUser; this.projectExpirationTimeInDays = projectExpirationTimeInDays; + this.userService = userService; } - public void loadProject(String projectCode, Consumer> stateMachineConsumer) { - Optional project = this.projectRepository.findByCode(projectCode); - if (project.isPresent()) { - StateMachine stateMachine = this.projectStateMachineFactory.getStateMachine(project.get().getStateMachineKey()); - stateMachine.stopReactively().subscribe(null, logUtils::logError, - () -> stateMachine.getStateMachineAccessor().doWithAllRegions(stateMachineAccess -> { - stateMachineAccess.addStateMachineInterceptor(new StateMachineInterceptorAdapter<>() { - @Override - public void postStateChange(State state, Message message, Transition transition, StateMachine stateMachine, StateMachine rootStateMachine) { - project.get().setState(state.getId()); - saveProject(project.get()); - } - }); - stateMachineAccess.resetStateMachineReactively(new DefaultStateMachineContext<>(project.get().getState(), null, null, null)) - .subscribe(null, logUtils::logError, - () -> stateMachine.startReactively().subscribe(null, logUtils::logError, - () -> stateMachineConsumer.accept(stateMachine))); - })); - } + public void loadProject(Project project, Consumer> stateMachineConsumer) { + StateMachine stateMachine = this.projectStateMachineFactory.getStateMachine(project.getStateMachineKey()); + stateMachine.stopReactively().subscribe(null, logUtils::logError, + () -> stateMachine.getStateMachineAccessor().doWithAllRegions(stateMachineAccess -> { + stateMachineAccess.addStateMachineInterceptor(new StateMachineInterceptorAdapter<>() { + @Override + public void postStateChange(State state, Message message, Transition transition, StateMachine stateMachine, StateMachine rootStateMachine) { + project.setState(state.getId()); + saveProject(project); + } + }); + stateMachineAccess.resetStateMachineReactively(new DefaultStateMachineContext<>(project.getState(), null, null, null)) + .subscribe(null, logUtils::logError, + () -> stateMachine.startReactively().subscribe(null, logUtils::logError, + () -> stateMachineConsumer.accept(stateMachine))); + })); } - private Project saveProject(@NotNull Project project){ - project.setModifiedAt(Instant.now()); - projectRepository.save(project); + private Project saveProject(@NotNull Project project) { + projectService.saveProject(project); return project; } - private void changeEvent(String projectCode, ProjectEvent projectEvent) throws ProjectEventActionsException { - changeEvent(projectCode, projectEvent, Optional.empty()); + private void changeEvent(Project project, ProjectEvent projectEvent) throws ProjectEventActionsException { + changeEvent(project, projectEvent, Optional.empty()); } - private void changeEvent(String projectCode, ProjectEvent projectEvent, Optional> consumerAfterSuccesfulChangeEvent) throws ProjectEventActionsException { + private void changeEvent(Project project, ProjectEvent projectEvent, Optional> consumerAfterSuccessfulChangeEvent) throws ProjectEventActionsException { try { - changeEventWithoutExceptionHandling(projectCode, projectEvent, consumerAfterSuccesfulChangeEvent); + changeEventWithoutExceptionHandling(project, projectEvent, consumerAfterSuccessfulChangeEvent); } catch (Exception e) { throw new ProjectEventActionsException(e); } } - private void changeEventWithoutExceptionHandling(String projectCode, ProjectEvent projectEvent, Optional> consumerAfterSuccesfulChangeEvent) { - loadProject(projectCode, stateMachine -> { + private void changeEventWithoutExceptionHandling(Project project, ProjectEvent projectEvent, Optional> consumerAfterSuccessfulChangeEvent) { + loadProject(project, stateMachine -> { Message createEventMessage = MessageBuilder.withPayload(projectEvent).build(); stateMachine.sendEvent(Mono.just(createEventMessage)).subscribe(null, logUtils::logError, () -> { - Optional project = this.projectRepository.findByCode(projectCode); - if (project.isPresent()) { - project.get().setState(stateMachine.getState().getId()); - project.get().setModifiedAt(Instant.now()); - saveProject(project.get()); - this.notificationService.createNotification(projectCode, null, fetchSessionUserEmailIfSessionIsActive(), - OperationType.CHANGE_PROJECT_STATE, projectEvent + " project", null, null); - if (consumerAfterSuccesfulChangeEvent.isPresent()) { - consumerAfterSuccesfulChangeEvent.get().accept(project.get()); - } - } + project.setState(stateMachine.getState().getId()); + project.setModifiedAt(Instant.now()); + saveProject(project); + this.notificationService.createNotification(project, null, fetchSessionUserEmailIfSessionIsActive(), + OperationType.CHANGE_PROJECT_STATE, projectEvent + " project", null, null); + consumerAfterSuccessfulChangeEvent.ifPresent(projectConsumer -> + projectConsumer.accept(project)); }); }); } @@ -138,16 +130,16 @@ private String fetchSessionUserEmailIfSessionIsActive() { } @Override - public String draft(String[] bridgeheads, String queryCode, ProjectType projectType) throws ProjectEventActionsException { + public String draft(String[] bridgeheads, String queryCode) throws ProjectEventActionsException { try { - return draftWithoutExceptionHandling(bridgeheads, queryCode, projectType); + return draftWithoutExceptionHandling(bridgeheads, queryCode); } catch (Exception e) { throw new ProjectEventActionsException(e); } } - private String draftWithoutExceptionHandling(@NotNull String[] bridgeheads, @NotNull String queryCode, ProjectType projectType) throws ProjectEventActionsException { - Optional queryOptional = this.queryRepository.findByCode(queryCode); + private String draftWithoutExceptionHandling(@NotNull String[] bridgeheads, @NotNull String queryCode) throws ProjectEventActionsException { + Optional queryOptional = queryService.fetchQuery(queryCode); if (queryOptional.isEmpty()) { throw new ProjectEventActionsException("Query not found"); } @@ -155,8 +147,7 @@ private String draftWithoutExceptionHandling(@NotNull String[] bridgeheads, @Not createProjectAsDraft( projectCode, project -> Arrays.stream(bridgeheads).forEach(bridgehead -> createProjectBridgehead(bridgehead, project)), - queryOptional.get(), - projectType); + queryOptional.get()); return projectCode; } @@ -165,7 +156,7 @@ private String generateProjectCode() { } - private void createProjectAsDraft(String projectCode, Consumer projectConsumer, Query query, ProjectType projectType) { + private void createProjectAsDraft(String projectCode, Consumer projectConsumer, Query query) { Project project = new Project(); project.setCode(projectCode); project.setCreatorEmail(sessionUser.getEmail()); @@ -174,13 +165,13 @@ private void createProjectAsDraft(String projectCode, Consumer projectC project.setModifiedAt(Instant.now()); project.setStateMachineKey(UUID.randomUUID().toString().replace("-", "")); project.setQuery(query); - project.setType(projectType); StateMachine stateMachine = this.projectStateMachineFactory.getStateMachine(project.getStateMachineKey()); stateMachine.startReactively().subscribe(null, logUtils::logError, () -> { project.setState(stateMachine.getState().getId()); projectConsumer.accept(saveProject(project)); - this.notificationService.createNotification(projectCode, null, sessionUser.getEmail(), + userService.addCreatorIfNotExists(); + this.notificationService.createNotification(project, null, sessionUser.getEmail(), OperationType.CHANGE_PROJECT_STATE, "Design project", null, null); }); } @@ -189,75 +180,108 @@ private LocalDate createExpirationDate() { return LocalDate.now().plusDays(projectExpirationTimeInDays); } - private ProjectBridgehead createProjectBridgehead(String bridgehead, Project project) { + private void createProjectBridgehead(String bridgehead, Project project) { ProjectBridgehead projectBridgehead = new ProjectBridgehead(); projectBridgehead.setBridgehead(bridgehead.toLowerCase()); projectBridgehead.setProject(project); - projectBridgehead.setState(ProjectBridgeheadState.CREATED); // TODO: Replace with state machine - projectBridgehead.setModifiedAt(Instant.now()); - return this.projectBridgeheadRepository.save(projectBridgehead); + projectBridgeheadService.saveBridgehead(projectBridgehead); } - private void createProjectBridgeheadUser(String projectCode) throws ProjectEventActionsException { - Project project = fetchProject(projectCode); - sessionUser.getBridgeheads().stream().forEach(bridgehead -> { - Optional projectBridgehead = this.projectBridgeheadRepository.findFirstByBridgeheadAndProject(bridgehead, project); - if (projectBridgehead.isPresent()) { - this.userService.createProjectBridgeheadUserIfNotExists(sessionUser.getEmail(), projectBridgehead.get(), ProjectRole.CREATOR); - } - }); + @Override + public void create(Project project) throws ProjectEventActionsException { + changeEvent(project, ProjectEvent.CREATE); } - private Project fetchProject(String projectCode) throws ProjectEventActionsException { - Optional project = this.projectRepository.findByCode(projectCode); - if (project.isEmpty()) { - throw new ProjectEventActionsException("Project not found"); - } - return project.get(); + @Override + public void accept(Project project) throws ProjectEventActionsException { + changeEvent(project, ProjectEvent.ACCEPT); } @Override - public void create(String projectCode) throws ProjectEventActionsException { - changeEvent(projectCode, ProjectEvent.CREATE); - createProjectBridgeheadUser(projectCode); + public void reject(Project project) throws ProjectEventActionsException { + changeEvent(project, ProjectEvent.REJECT); } @Override - public void accept(String projectCode) throws ProjectEventActionsException { - changeEvent(projectCode, ProjectEvent.ACCEPT); + public void archive(Project project) throws ProjectEventActionsException { + changeEvent(project, ProjectEvent.ARCHIVE, Optional.of(tempProject -> { + tempProject.setArchivedAt(tempProject.getModifiedAt()); + saveProject(tempProject); + })); } @Override - public void reject(String projectCode) throws ProjectEventActionsException { - changeEvent(projectCode, ProjectEvent.REJECT); + public void startDevelopStage(Project project) throws ProjectEventActionsException { + changeEvent(project, ProjectEvent.START_DEVELOP); } @Override - public void archive(String projectCode) throws ProjectEventActionsException { - changeEvent(projectCode, ProjectEvent.ARCHIVE, Optional.of(project -> { - project.setArchivedAt(project.getModifiedAt()); - saveProject(project); - })); + public void startPilotStage(Project project) throws ProjectEventActionsException { + changeEvent(project, ProjectEvent.START_PILOT); } @Override - public void startDevelopStage(String projectCode) throws ProjectEventActionsException { - changeEvent(projectCode, ProjectEvent.START_DEVELOP); + public void startFinalStage(Project project) throws ProjectEventActionsException { + changeEvent(project, ProjectEvent.START_FINAL); } @Override - public void startPilotStage(String projectCode) throws ProjectEventActionsException { - changeEvent(projectCode, ProjectEvent.START_PILOT); + public void finish(Project project) throws ProjectEventActionsException { + changeEvent(project, ProjectEvent.FINISH); } - @Override - public void startFinalStage(String projectCode) throws ProjectEventActionsException { - changeEvent(projectCode, ProjectEvent.START_FINAL); + public ProjectState[] fetchAllProjectEvents(Optional project) { + return project + .map(Project::fetchProjectTypes) + .map(this::statesFor) + .orElseGet(ProjectState::values); } - @Override - public void finish(String projectCode) throws ProjectEventActionsException { - changeEvent(projectCode, ProjectEvent.FINISH); + private ProjectState[] statesFor(Set types) { + + if (types == null || types.isEmpty()) { + return ProjectState.values(); + } + + // Convert each type to a Set for intersection + Set intersection = types.stream() + .map(this::statesForSingleType) + .map(Set::of) // convert array to Set + .reduce((set1, set2) -> { + Set result = new HashSet<>(set1); + result.retainAll(set2); + return result; + }) + .orElse(Set.of()); + + // Preserve enum declaration order + return Arrays.stream(ProjectState.values()) + .filter(intersection::contains) + .toArray(ProjectState[]::new); + } + + private ProjectState[] statesForSingleType(ProjectType type) { + return switch (type) { + case EXPORT -> EXPORT_PROJECT_STATES; + case SAMPLES -> SAMPLES_PROJECT_STATES; + default -> ProjectState.values(); + }; } + private static final ProjectState[] EXPORT_PROJECT_STATES = { + ProjectState.DRAFT, + ProjectState.REVIEW, + ProjectState.APPROVAL, + ProjectState.FINAL, + ProjectState.FINISHED + }; + + private static final ProjectState[] SAMPLES_PROJECT_STATES = { + ProjectState.DRAFT, + ProjectState.REVIEW, + ProjectState.APPROVAL, + ProjectState.FINAL, + ProjectState.FINISHED + }; + } diff --git a/src/main/java/de/samply/project/state/ProjectState.java b/src/main/java/de/samply/project/state/ProjectState.java index bf05726a..9282581f 100644 --- a/src/main/java/de/samply/project/state/ProjectState.java +++ b/src/main/java/de/samply/project/state/ProjectState.java @@ -2,8 +2,8 @@ public enum ProjectState { DRAFT, - CREATED, - ACCEPTED, + REVIEW, + APPROVAL, DEVELOP, PILOT, FINAL, diff --git a/src/main/java/de/samply/project/state/ProjectStatemachineConfiguration.java b/src/main/java/de/samply/project/state/ProjectStatemachineConfiguration.java index a34c7cd2..2d5f2e84 100644 --- a/src/main/java/de/samply/project/state/ProjectStatemachineConfiguration.java +++ b/src/main/java/de/samply/project/state/ProjectStatemachineConfiguration.java @@ -26,7 +26,7 @@ public void configure(StateMachineConfigurationConfigurer from, State to) { super.stateChanged(from, to); - log.info("State changed from " + from + " to " + to); + log.info("State changed from {} to {}", from, to); } }); } @@ -35,8 +35,8 @@ public void stateChanged(State from, State states) throws Exception { states.withStates() .initial(ProjectState.DRAFT) - .state(ProjectState.CREATED) - .state(ProjectState.ACCEPTED) + .state(ProjectState.REVIEW) + .state(ProjectState.APPROVAL) .state(ProjectState.DEVELOP) .state(ProjectState.PILOT) .state(ProjectState.FINAL) @@ -50,24 +50,24 @@ public void configure(StateMachineTransitionConfigurer transitions) throws Exception { - ProjectState[] statesToBeRejected = {ProjectState.DRAFT, ProjectState.CREATED, ProjectState.ACCEPTED, + ProjectState[] statesToBeRejected = {ProjectState.DRAFT, ProjectState.REVIEW, ProjectState.APPROVAL, ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL, ProjectState.ARCHIVED}; createTransitions(statesToBeRejected, state -> rejectState(transitions, state)); } private void addArchiveTransitions(StateMachineTransitionConfigurer transitions) throws Exception { - ProjectState[] statesToBeRejected = {ProjectState.CREATED, ProjectState.ACCEPTED, ProjectState.DEVELOP, + ProjectState[] statesToBeRejected = {ProjectState.REVIEW, ProjectState.APPROVAL, ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL, ProjectState.FINISHED}; createTransitions(statesToBeRejected, state -> archiveState(transitions, state)); } diff --git a/src/main/java/de/samply/proxy/ProxyConfiguration.java b/src/main/java/de/samply/proxy/ProxyConfiguration.java index d58970f4..12f635e0 100644 --- a/src/main/java/de/samply/proxy/ProxyConfiguration.java +++ b/src/main/java/de/samply/proxy/ProxyConfiguration.java @@ -6,6 +6,7 @@ import org.springframework.util.StringUtils; import java.net.MalformedURLException; +import java.net.URI; import java.net.URL; import java.util.Arrays; import java.util.List; @@ -27,6 +28,7 @@ public class ProxyConfiguration { private String noProxyPattern; // Convenience method to get noProxy as a List + @SuppressWarnings("unused") public List getNoProxyList() { return noProxy != null ? Arrays.asList(noProxy.split(",")) : List.of(); } @@ -34,7 +36,7 @@ public List getNoProxyList() { @PostConstruct public void init() throws MalformedURLException { if (StringUtils.hasText(this.url)) { - URL url = new URL(this.url); + URL url = URI.create(this.url).toURL(); this.schema = url.getProtocol(); this.host = url.getHost(); this.port = url.getPort(); @@ -45,14 +47,14 @@ public void init() throws MalformedURLException { } if (isConfigured()) { String schema = (this.schema != null) ? this.schema : ""; - log.info(schema + " proxy configured:"); - log.info("\t-Host: " + this.host); - log.info("\t-Port: " + this.port); + log.info("{} proxy configured:", schema); + log.info("\t-Host: {}", this.host); + log.info("\t-Port: {}", this.port); if (username != null && password != null) { - log.info("\t-Username: " + username); + log.info("\t-Username: {}", username); } if (noProxy != null) { - log.info("\t-NoProxy: " + noProxy); + log.info("\t-NoProxy: {}", noProxy); } } } diff --git a/src/main/java/de/samply/query/OutputFormat.java b/src/main/java/de/samply/query/OutputFormat.java index d0bf5b21..2b2486e6 100644 --- a/src/main/java/de/samply/query/OutputFormat.java +++ b/src/main/java/de/samply/query/OutputFormat.java @@ -5,6 +5,6 @@ public enum OutputFormat { EXCEL, OPAL, JSON, - XML; + XML } diff --git a/src/main/java/de/samply/query/QueryChangedEvent.java b/src/main/java/de/samply/query/QueryChangedEvent.java new file mode 100644 index 00000000..2ff12bc3 --- /dev/null +++ b/src/main/java/de/samply/query/QueryChangedEvent.java @@ -0,0 +1,4 @@ +package de.samply.query; + +public record QueryChangedEvent(Long queryId) { +} diff --git a/src/main/java/de/samply/query/QueryEventService.java b/src/main/java/de/samply/query/QueryEventService.java new file mode 100644 index 00000000..ebbc1ce9 --- /dev/null +++ b/src/main/java/de/samply/query/QueryEventService.java @@ -0,0 +1,64 @@ +package de.samply.query; + +import de.samply.app.ProjectManagerConst; +import de.samply.exporter.ExporterJob; +import de.samply.project.ProjectBridgeheadService; +import de.samply.project.SendQueryToBridgeheadEvent; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Service; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +import java.time.Instant; + +@Service +@Slf4j +public class QueryEventService { + + // Services + private final ProjectBridgeheadService projectBridgeheadService; + + private final ExporterJob exporterJob; + private final TaskScheduler taskScheduler; + + private final int sleepTime; + private final int maxNumberOfRetries; + + + public QueryEventService( + ProjectBridgeheadService projectBridgeheadService, + ExporterJob exporterJob, + TaskScheduler taskScheduler, + @Value(ProjectManagerConst.TIME_BETWEEN_CHECK_EXPORTS_IN_SECONDS_SV) int sleepTime, + @Value(ProjectManagerConst.MAX_NUMBER_OF_RETRIES_BETWEEN_CHECK_EXPORTS_SV) int maxNumberOfRetries) { + this.projectBridgeheadService = projectBridgeheadService; + this.exporterJob = exporterJob; + this.taskScheduler = taskScheduler; + this.sleepTime = sleepTime; + this.maxNumberOfRetries = maxNumberOfRetries; + } + + /** + * Listens for query changes after the modifying transaction has successfully committed, + * ensuring we only react to the persisted state. AFTER_COMMIT (not AFTER_COMPLETION) guarantees + * we don't run on rollback. No @Transactional here — handleChange opens its own transaction. + */ + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void onQueryChanged(QueryChangedEvent event) { + projectBridgeheadService.handleChange(event.queryId()); + } + + @EventListener + public void onQuerySentToBridgehead(@SuppressWarnings("unused") SendQueryToBridgeheadEvent event) { + log.info("onQuerySentToBridgehead called!"); + for (int i = 0; i < maxNumberOfRetries; i++) { + Instant when = Instant.now().plusSeconds((long) sleepTime * i); + taskScheduler.schedule(exporterJob::checkExports, when); + } + } + + +} diff --git a/src/main/java/de/samply/query/QueryFormat.java b/src/main/java/de/samply/query/QueryFormat.java index bf4030c6..ab455872 100644 --- a/src/main/java/de/samply/query/QueryFormat.java +++ b/src/main/java/de/samply/query/QueryFormat.java @@ -3,6 +3,7 @@ public enum QueryFormat { FHIR_SEARCH, CQL, - CQL_DATA - + CQL_DATA, + AST, + AST_DATA } diff --git a/src/main/java/de/samply/query/QueryPersistenceService.java b/src/main/java/de/samply/query/QueryPersistenceService.java new file mode 100644 index 00000000..e8c193fd --- /dev/null +++ b/src/main/java/de/samply/query/QueryPersistenceService.java @@ -0,0 +1,120 @@ +package de.samply.query; + +import de.samply.db.model.Query; +import de.samply.db.model.QueryOutput; +import de.samply.db.repository.QueryRepository; +import de.samply.db.utils.QueryMapper; +import de.samply.db.utils.QueryOutputMapper; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +public class QueryPersistenceService { + + // Query Repository only used for saving queries within a transaction + private final QueryRepository queryRepository; + + private final QueryMapper queryMapper; + private final QueryOutputMapper queryOutputMapper; + + private final ApplicationEventPublisher applicationEventPublisher; + + public QueryPersistenceService( + QueryRepository queryRepository, + QueryMapper queryMapper, + QueryOutputMapper queryOutputMapper, + ApplicationEventPublisher applicationEventPublisher) { + this.queryRepository = queryRepository; + this.queryMapper = queryMapper; + this.queryOutputMapper = queryOutputMapper; + this.applicationEventPublisher = applicationEventPublisher; + } + + /** + * Saves or updates a Query entity, including its associated QueryOutputs. + * + *

Important: For updates, the QueryOutputs collection is NOT replaced via + * clear()+addAll(). Instead, it is merged (diff-based) to comply with JPA/Hibernate + * lifecycle rules and database constraints.

+ * + *

Why this is necessary: + *

    + *
  • The QueryOutput table has a unique constraint on (query_id, project_type).
  • + *
  • A naive replacement (clear and re-insert) may cause INSERTs to be executed + * before DELETEs during a flush, leading to constraint violations.
  • + *
  • Hibernate tracks changes on managed entities; replacing collections breaks + * this mechanism and can lead to detached entity or duplicate key issues.
  • + *
+ * + *

What this method does instead: + *

    + *
  • Updates scalar fields using MapStruct.
  • + *
  • Synchronizes QueryOutputs: + *
      + *
    • Adds new outputs
    • + *
    • Updates existing ones (by projectType)
    • + *
    • Removes obsolete ones (triggering orphanRemoval)
    • + *
    + *
  • + *
+ * + *

All changes are applied within a transaction. Hibernate dirty checking ensures + * that only necessary SQL operations are executed on commit.

+ * + *

After persistence, a QueryChangedEvent is published to trigger downstream updates.

+ */ + @Transactional + public Query saveQuery(Query query) { + + Query result = (query.getId() == null) + ? queryRepository.save(query) + : queryRepository.findByCode(query.getCode()) + .map(managed -> { + + // update scalar fields + queryMapper.updateQuery(query, managed); + + // --- merge outputs (no clear!) --- + var existing = managed.getOutputs().stream() + .collect(Collectors.toMap(QueryOutput::getProjectType, Function.identity())); + + var incoming = Optional.ofNullable(query.getOutputs()) + .orElseGet(Set::of); + + var incomingTypes = incoming.stream() + .map(QueryOutput::getProjectType) + .collect(Collectors.toSet()); + + // add or update + incoming.forEach(in -> { + var current = existing.get(in.getProjectType()); + + if (current == null) { + managed.addOutput(queryOutputMapper.toEntity(in)); + } else { + current.setOutputFormat(in.getOutputFormat()); + current.setTemplateId(in.getTemplateId()); + } + }); + + // remove obsolete + managed.getOutputs().removeIf(o -> !incomingTypes.contains(o.getProjectType())); + + return managed; + }) + .orElseThrow(() -> + new RuntimeException("Query with code " + query.getCode() + " does not exist") + ); + + applicationEventPublisher.publishEvent(new QueryChangedEvent(result.getId())); + + return result; + } + +} diff --git a/src/main/java/de/samply/query/QueryService.java b/src/main/java/de/samply/query/QueryService.java index 4249d87a..8cdc5652 100644 --- a/src/main/java/de/samply/query/QueryService.java +++ b/src/main/java/de/samply/query/QueryService.java @@ -5,18 +5,14 @@ import de.samply.app.ProjectManagerConst; import de.samply.db.model.Project; import de.samply.db.model.Query; -import de.samply.db.repository.ProjectRepository; +import de.samply.db.model.QueryOutput; import de.samply.db.repository.QueryRepository; import de.samply.notification.NotificationService; import de.samply.notification.OperationType; +import de.samply.project.ProjectType; import de.samply.security.SessionUser; import de.samply.utils.Base64Utils; import jakarta.validation.constraints.NotNull; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.exception.ExceptionUtils; -import org.springframework.stereotype.Service; -import org.springframework.web.util.UriComponentsBuilder; - import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.time.Instant; @@ -24,132 +20,190 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.springframework.stereotype.Service; +import org.springframework.web.util.UriComponentsBuilder; @Service @Slf4j public class QueryService { - private final NotificationService notificationService; - private final SessionUser sessionUser; - private final QueryRepository queryRepository; - private final ProjectRepository projectRepository; - private final ObjectMapper objectMapper = new ObjectMapper(); - - public QueryService(NotificationService notificationService, - SessionUser sessionUser, - QueryRepository queryRepository, - ProjectRepository projectRepository) { - this.notificationService = notificationService; - this.sessionUser = sessionUser; - this.queryRepository = queryRepository; - this.projectRepository = projectRepository; - } + private final SessionUser sessionUser; - public String createQuery( - String query, QueryFormat queryFormat, String label, String description, - OutputFormat outputFormat, String templateId, String humanReadable, String explorerUrl, String queryContext) { - Query tempQuery = new Query(); - tempQuery.setCode(generateQueryCode()); - tempQuery.setQuery(query); - tempQuery.setQueryFormat(queryFormat); - tempQuery.setCreatedAt(Instant.now()); - tempQuery.setLabel(label); - tempQuery.setDescription(description); - tempQuery.setOutputFormat(outputFormat); - tempQuery.setTemplateId(templateId); - Base64Utils.decodeIfNecessary(humanReadable).ifPresent(tempQuery::setHumanReadable); - tempQuery.setExplorerUrl(decodeUrlIfNecessary(explorerUrl)); - tempQuery.setContext(queryContext); - tempQuery = this.queryRepository.save(tempQuery); - return tempQuery.getCode(); - } + // Services + private final NotificationService notificationService; + private final QueryPersistenceService queryPersistenceService; - public void addProjectCodeToExporterUrl(@NotNull String queryCode, @NotNull String projectCode) { - queryRepository.findByCode(queryCode).ifPresent(query -> { - if (query.getExplorerUrl() != null) { - query.setExplorerUrl(addProjectCodeToUrl(query.getExplorerUrl(), projectCode)); - this.queryRepository.save(query); - } - }); - } + // Repositories + private final QueryRepository queryRepository; - private String addProjectCodeToUrl(@NotNull String url, @NotNull String projectCode) { - return (url.contains(ProjectManagerConst.PROJECT_CODE)) ? url : - UriComponentsBuilder.fromHttpUrl(url).queryParam(ProjectManagerConst.PROJECT_CODE, projectCode).toUriString(); - } + private final ObjectMapper objectMapper = new ObjectMapper(); - private String generateQueryCode() { - return UUID.randomUUID().toString().replace("-", "").substring(0, ProjectManagerConst.QUERY_CODE_SIZE); + + public QueryService( + NotificationService notificationService, + SessionUser sessionUser, + QueryPersistenceService queryPersistenceService, + QueryRepository queryRepository) { + this.notificationService = notificationService; + this.sessionUser = sessionUser; + this.queryPersistenceService = queryPersistenceService; + this.queryRepository = queryRepository; + } + + public String createQuery( + String query, QueryFormat queryFormat, String label, String description, + OutputFormat outputFormat, String templateId, ProjectType projectType, + String humanReadable, String explorerUrl, String queryContext, String queryDetails) { + Query tempQuery = new Query(); + tempQuery.setCode(generateQueryCode()); + tempQuery.setQuery(query); + tempQuery.setQueryFormat(queryFormat); + tempQuery.setCreatedAt(Instant.now()); + tempQuery.setLabel(label); + tempQuery.setDescription(description); + tempQuery.setDetails(queryDetails); + Base64Utils.decodeIfNecessary(humanReadable).ifPresent(tempQuery::setHumanReadable); + tempQuery.setExplorerUrl(decodeUrlIfNecessary(explorerUrl)); + tempQuery.setContext(queryContext); + // Every Query should have at least one output: + addOutputToQuery(tempQuery, Optional.ofNullable(outputFormat), Optional.ofNullable(templateId), + projectType); + tempQuery = queryPersistenceService.saveQuery(tempQuery); + return tempQuery.getCode(); + } + + private void addOutputToQuery(Query query, + Optional outputFormat, + Optional templateId, + @NotNull ProjectType projectType) { + if (projectType != null) { + QueryOutput output = query.fetchOutput(projectType) + .orElseGet(() -> { + QueryOutput newOutput = new QueryOutput(); + newOutput.setProjectType(projectType); + query.addOutput(newOutput); + return newOutput; + }); + + outputFormat.ifPresent(output::setOutputFormat); + templateId.ifPresent(output::setTemplateId); } + } + + public void removeOutput(@NotNull Project project, @NotNull ProjectType projectType) { + project.getQuery().removeOutput(projectType); + queryPersistenceService.saveQuery(project.getQuery()); + this.notificationService.createNotification(project, null, sessionUser.getEmail(), + OperationType.EDIT_PROJECT, "Removed output of type " + projectType.name(), null, null); + } + - public void editQuery(@NotNull String projectCode, - String query, QueryFormat queryFormat, String label, String description, - OutputFormat outputFormat, String templateId, String humanReadable, String explorerUrl, String queryContext) { - Optional projectOptional = projectRepository.findByCode(projectCode); - if (projectOptional.isPresent()) { - Query projectQuery = projectOptional.get().getQuery(); - if (projectQuery != null) { - Map changedKeyValueMap = new HashMap<>(); - if (query != null) { - projectQuery.setQuery(query); - changedKeyValueMap.put("query", query); - } - if (queryFormat != null) { - projectQuery.setQueryFormat(queryFormat); - changedKeyValueMap.put("query format", queryFormat.toString()); - } - if (label != null) { - projectQuery.setLabel(label); - changedKeyValueMap.put("label", label); - } - if (description != null) { - projectQuery.setDescription(description); - changedKeyValueMap.put("description", description); - } - if (outputFormat != null) { - projectQuery.setOutputFormat(outputFormat); - changedKeyValueMap.put("output format", outputFormat.toString()); - } - if (templateId != null) { - projectQuery.setTemplateId(templateId); - changedKeyValueMap.put("template id", templateId); - } - if (humanReadable != null) { - Base64Utils.decodeIfNecessary(humanReadable).ifPresent(projectQuery::setHumanReadable); - changedKeyValueMap.put("human readable", humanReadable); - } - if (explorerUrl != null) { - projectQuery.setExplorerUrl(addProjectCodeToUrl(decodeUrlIfNecessary(explorerUrl), projectCode)); - changedKeyValueMap.put("explorer url", explorerUrl); - } - if (queryContext != null) { - projectQuery.setContext(queryContext); - changedKeyValueMap.put("query context", queryContext); - } - if (!changedKeyValueMap.isEmpty()) { - queryRepository.save(projectQuery); - this.notificationService.createNotification(projectCode, null, sessionUser.getEmail(), - OperationType.EDIT_QUERY, printInOneLine(changedKeyValueMap), null, null); - } - } - } + public void addProjectCodeToExporterUrl(@NotNull String queryCode, @NotNull String projectCode) { + queryRepository.findByCode(queryCode).ifPresent(query -> { + if (query.getExplorerUrl() != null) { + query.setExplorerUrl(addProjectCodeToUrl(query.getExplorerUrl(), projectCode)); + queryPersistenceService.saveQuery(query); + } + }); + } + + private String addProjectCodeToUrl(@NotNull String url, @NotNull String projectCode) { + return (url.contains(ProjectManagerConst.PROJECT_CODE)) ? url : + UriComponentsBuilder.fromUriString(url) + .queryParam(ProjectManagerConst.PROJECT_CODE, projectCode).toUriString(); + } + + private String generateQueryCode() { + return UUID.randomUUID().toString().replace("-", "") + .substring(0, ProjectManagerConst.QUERY_CODE_SIZE); + } + + public void editQuery(@NotNull Project project, + String query, QueryFormat queryFormat, String label, String description, + OutputFormat outputFormat, String templateId, ProjectType projectType, + String humanReadable, String explorerUrl, String queryContext, String cohortDefinition, + String queryDetails) { + + Query projectQuery = project.getQuery(); + if (projectQuery != null) { + Map changedKeyValueMap = new HashMap<>(); + if (query != null) { + projectQuery.setQuery(query); + changedKeyValueMap.put("query", query); + } + if (queryFormat != null) { + projectQuery.setQueryFormat(queryFormat); + changedKeyValueMap.put("query format", queryFormat.toString()); + } + if (label != null) { + projectQuery.setLabel(label); + changedKeyValueMap.put("label", label); + } + if (description != null) { + projectQuery.setDescription(description); + changedKeyValueMap.put("description", description); + } + if (queryDetails != null) { + projectQuery.setDetails(queryDetails); + changedKeyValueMap.put("query details", queryDetails); + } + if (cohortDefinition != null) { + projectQuery.setCohortDefinition(cohortDefinition); + changedKeyValueMap.put("cohort definition", cohortDefinition); + } + if (humanReadable != null) { + Base64Utils.decodeIfNecessary(humanReadable).ifPresent(projectQuery::setHumanReadable); + changedKeyValueMap.put("human readable", humanReadable); + } + if (explorerUrl != null) { + projectQuery.setExplorerUrl( + addProjectCodeToUrl(decodeUrlIfNecessary(explorerUrl), project.getCode())); + changedKeyValueMap.put("explorer url", explorerUrl); + } + if (queryContext != null) { + projectQuery.setContext(queryContext); + changedKeyValueMap.put("query context", queryContext); + } + if (projectType != null && (outputFormat != null || templateId != null)) { + Optional outputFormatOptional = Optional.ofNullable(outputFormat); + Optional templateIdOptional = Optional.ofNullable(templateId); + addOutputToQuery(projectQuery, outputFormatOptional, templateIdOptional, projectType); + outputFormatOptional.ifPresent( + of -> changedKeyValueMap.put("output format for project type " + projectType, + of.toString())); + templateIdOptional.ifPresent( + tid -> changedKeyValueMap.put("template id for project type " + projectType, tid)); + } + if (!changedKeyValueMap.isEmpty()) { + queryPersistenceService.saveQuery(projectQuery); + this.notificationService.createNotification(project, null, sessionUser.getEmail(), + OperationType.EDIT_QUERY, printInOneLine(changedKeyValueMap), null, null); + } } - private String decodeUrlIfNecessary(String encodedUrl) { - try { - return URLDecoder.decode(encodedUrl, StandardCharsets.UTF_8); - } catch (Exception e) { - return encodedUrl; - } + } + + private String decodeUrlIfNecessary(String encodedUrl) { + try { + return URLDecoder.decode(encodedUrl, StandardCharsets.UTF_8); + } catch (Exception e) { + return encodedUrl; } + } - private String printInOneLine(Map changedKeyValuesMaps) { - try { - return "Query edited: " + objectMapper.writeValueAsString(changedKeyValuesMaps); - } catch (JsonProcessingException e) { - log.error(ExceptionUtils.getStackTrace(e)); - return null; - } + private String printInOneLine(Map changedKeyValuesMaps) { + try { + return "Query edited: " + objectMapper.writeValueAsString(changedKeyValuesMaps); + } catch (JsonProcessingException e) { + log.error(ExceptionUtils.getStackTrace(e)); + return null; } + } + + public Optional fetchQuery(String queryCode) { + return this.queryRepository.findByCode(queryCode); + } } diff --git a/src/main/java/de/samply/register/AppRegisterService.java b/src/main/java/de/samply/register/AppRegisterService.java index 7a35d6d0..f4f880ba 100644 --- a/src/main/java/de/samply/register/AppRegisterService.java +++ b/src/main/java/de/samply/register/AppRegisterService.java @@ -1,8 +1,8 @@ package de.samply.register; import de.samply.app.ProjectManagerConst; +import de.samply.coder.CoderService; import de.samply.db.model.ProjectCoder; -import de.samply.db.repository.ProjectCoderRepository; import de.samply.notification.NotificationService; import de.samply.notification.OperationType; import de.samply.utils.MessageStatus; @@ -18,13 +18,15 @@ import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; +import java.util.Optional; + @Slf4j @Service public class AppRegisterService { private WebClient webClient = null; private final NotificationService notificationService; - private final ProjectCoderRepository projectCoderRepository; + private final CoderService coderService; private String authorizationHeader = null; private final boolean appRegisterEnabled; @@ -34,11 +36,11 @@ public AppRegisterService( @Value(ProjectManagerConst.APP_REGISTER_AUTHORIZATION_FORMAT_SV) String authorizationFormat, @Value(ProjectManagerConst.ENABLE_APP_REGISTER_SV) boolean appRegisterEnabled, WebClientFactory webClientFactory, - NotificationService notificationService, - ProjectCoderRepository projectCoderRepository) { + CoderService coderService, + NotificationService notificationService) { + this.coderService = coderService; this.appRegisterEnabled = appRegisterEnabled; this.notificationService = notificationService; - this.projectCoderRepository = projectCoderRepository; if (appRegisterEnabled) { this.webClient = webClientFactory.createWebClient(appRegisterBaseUrl); this.authorizationHeader = fetchAuthorizationHeader(authorizationFormat, appRegisterApiKey); @@ -58,7 +60,7 @@ public Mono register(ProjectCoder projectCoder) { projectCoder.getProjectBridgeheadUser().getEmail(), projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getProject().getCode(), projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getBridgehead() - ); + ); return webClient.post() .uri(ProjectManagerConst.REGISTER_PATH) .header(HttpHeaders.AUTHORIZATION, authorizationHeader) @@ -69,7 +71,7 @@ public Mono register(ProjectCoder projectCoder) { .doOnError(throwable -> { MessageStatus messageStatus = MessageStatus.newInstance(throwable, "User app could not be registered in Beam"); notificationService.createNotification( - projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getProject().getCode(), + projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getProject(), projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getBridgehead(), projectCoder.getProjectBridgeheadUser().getEmail(), OperationType.REGISTER_IN_APP_REGISTER, @@ -81,16 +83,21 @@ public Mono register(ProjectCoder projectCoder) { .doOnSuccess(response -> { log.info("App registered"); projectCoder.setInAppRegister(true); - projectCoderRepository.save(projectCoder); + coderService.saveCoder(projectCoder); + String message = Optional.ofNullable(response) + .filter(r -> !r.isEmpty()) + .map(r -> " (" + r + ")") + .orElse(""); notificationService.createNotification( - projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getProject().getCode(), + projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getProject(), projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getBridgehead(), projectCoder.getProjectBridgeheadUser().getEmail(), OperationType.REGISTER_IN_APP_REGISTER, - "User app registered in Beam " + (response != null && response.length() > 0 ? " (" + response + ")" : ""), + "User app registered in Beam" + message, null, HttpStatus.OK ); + }).then(); } @@ -117,7 +124,7 @@ public Mono unregister(ProjectCoder projectCoder) { .doOnError(throwable -> { MessageStatus messageStatus = MessageStatus.newInstance(throwable, "User app could not be unregistered in Beam"); notificationService.createNotification( - projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getProject().getCode(), + projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getProject(), projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getBridgehead(), projectCoder.getProjectBridgeheadUser().getEmail(), OperationType.UNREGISTER_IN_APP_REGISTER, @@ -129,13 +136,17 @@ public Mono unregister(ProjectCoder projectCoder) { .doOnSuccess(response -> { log.info("App unregistered"); projectCoder.setInAppRegister(false); - projectCoderRepository.save(projectCoder); + coderService.saveCoder(projectCoder); + String message = Optional.ofNullable(response) + .filter(r -> !r.isEmpty()) + .map(r -> " (" + r + ")") + .orElse(""); notificationService.createNotification( - projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getProject().getCode(), + projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getProject(), projectCoder.getProjectBridgeheadUser().getProjectBridgehead().getBridgehead(), projectCoder.getProjectBridgeheadUser().getEmail(), OperationType.UNREGISTER_IN_APP_REGISTER, - "User app unregistered in Beam" + (response != null && response.length() > 0 ? response : ""), + "User app unregistered in Beam" + message, null, HttpStatus.OK ); diff --git a/src/main/java/de/samply/resolvers/AnnotatedParametersWrapper.java b/src/main/java/de/samply/resolvers/AnnotatedParametersWrapper.java new file mode 100644 index 00000000..e25b9c88 --- /dev/null +++ b/src/main/java/de/samply/resolvers/AnnotatedParametersWrapper.java @@ -0,0 +1,130 @@ +package de.samply.resolvers; + +import de.samply.annotations.RequestParameter; +import de.samply.annotations.RequestVariable; +import de.samply.app.ProjectManagerConst; +import de.samply.utils.ParamMetaUtils; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.request.NativeWebRequest; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +@Component +@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) +public class AnnotatedParametersWrapper { + + private final Map, Object> resolved = new HashMap<>(); + private final Map, Object> rawValues = new HashMap<>(); + + private final RequestBodyCache requestBodyCache; + private boolean initialized = false; + + public AnnotatedParametersWrapper(RequestBodyCache requestBodyCache) { + this.requestBodyCache = requestBodyCache; + } + + // ========================================================= + // 🔹 INITIALIZATION (called once per request) + // ========================================================= + + public void initializeIfNeeded(MethodParameter parameter, NativeWebRequest webRequest) { + if (initialized) return; + + Method method = parameter.getMethod(); + if (method == null) return; + + MethodParameter[] parameters = IntStream + .range(0, method.getParameterCount()) + .mapToObj(i -> new MethodParameter(method, i)) + .toArray(MethodParameter[]::new); + + initialize(parameters, webRequest); + initialized = true; + } + + private void initialize(MethodParameter[] parameters, NativeWebRequest webRequest) { + var request = webRequest.getNativeRequest(HttpServletRequest.class); + var bodyRef = new AtomicReference>(); + + Arrays.stream(parameters) + .map(ParamMetaUtils::extractParamMeta) + .filter(Objects::nonNull) + .forEach(meta -> { + // Lazy-load body only if needed + if (meta.bodyLookup() && bodyRef.get() == null) { + var body = Optional.ofNullable(request) + .map(req -> { + try { return requestBodyCache.getJsonBody(req); } + catch (IOException e) { throw new RuntimeException(e); } + }) + .orElse(Collections.emptyMap()); + bodyRef.set(body); + } + + processParameter(meta, webRequest, bodyRef.get()); + }); + } + + private void processParameter(ParamMeta meta, NativeWebRequest webRequest, Map body) { + var initialValue = (Object) webRequest.getParameter(meta.name()); + if (initialValue == null && meta.bodyLookup()) { + initialValue = body.get(meta.name()); + } + + var finalValue = initialValue != null ? initialValue + : (!ProjectManagerConst.NOT_SET.equals(meta.defaultValue()) ? meta.defaultValue() : null); + + if (finalValue == null) return; + + extractRelevantAnnotations(meta.parameter()) + .forEach(annotation -> rawValues.put(annotation, finalValue)); + } + + // ========================================================= + // 🔹 RESOLVED VALUES + // ========================================================= + + public void putResolved(MethodParameter parameter, T value) { + if (value == null) return; + + extractRelevantAnnotations(parameter) + .forEach(annotation -> resolved.put(annotation, value)); + } + + public Optional getResolved(Class annotation, Class type) { + Object value = resolved.get(annotation); + return type.isInstance(value) ? Optional.of(type.cast(value)) : Optional.empty(); + } + + // ========================================================= + // 🔹 RAW VALUES + // ========================================================= + + public Optional getRaw(Class annotation, Class type) { + Object value = rawValues.get(annotation); + return type.isInstance(value) ? Optional.of(type.cast(value)) : Optional.empty(); + } + + // ========================================================= + // 🔹 INTERNAL HELPERS + // ========================================================= + + private List> extractRelevantAnnotations(MethodParameter parameter) { + return Arrays.stream(parameter.getParameterAnnotations()) + .map(a -> (Class) a.annotationType()) + .filter(annotation -> !annotation.equals(RequestVariable.class) && !annotation.equals(RequestParameter.class)) + .collect(Collectors.toList()); + } + +} \ No newline at end of file diff --git a/src/main/java/de/samply/resolvers/LanguageArgumentResolver.java b/src/main/java/de/samply/resolvers/LanguageArgumentResolver.java new file mode 100644 index 00000000..482e25ce --- /dev/null +++ b/src/main/java/de/samply/resolvers/LanguageArgumentResolver.java @@ -0,0 +1,38 @@ +package de.samply.resolvers; + +import de.samply.annotations.Language; +import de.samply.app.ProjectManagerConst; +import de.samply.utils.LanguageUtils; +import jakarta.servlet.http.HttpServletRequest; +import org.jspecify.annotations.NonNull; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import java.util.Optional; + +@Component +public class LanguageArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(Language.class); + } + + @Override + public Object resolveArgument( + @NonNull MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory + ) { + return Optional.ofNullable(webRequest.getNativeRequest(HttpServletRequest.class)) + .map(r -> r.getParameter(ProjectManagerConst.LANGUAGE)) + .map(LanguageUtils::normalize) + .orElse(null); + } + +} diff --git a/src/main/java/de/samply/resolvers/ParamMeta.java b/src/main/java/de/samply/resolvers/ParamMeta.java new file mode 100644 index 00000000..42923c7a --- /dev/null +++ b/src/main/java/de/samply/resolvers/ParamMeta.java @@ -0,0 +1,7 @@ +package de.samply.resolvers; + +import org.springframework.core.MethodParameter; + +public record ParamMeta(MethodParameter parameter, String name, boolean required, boolean notEmpty, + String defaultValue, boolean bodyLookup) { +} \ No newline at end of file diff --git a/src/main/java/de/samply/resolvers/RequestBodyCache.java b/src/main/java/de/samply/resolvers/RequestBodyCache.java new file mode 100644 index 00000000..bca73ffa --- /dev/null +++ b/src/main/java/de/samply/resolvers/RequestBodyCache.java @@ -0,0 +1,37 @@ +package de.samply.resolvers; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.stereotype.Component; +import org.springframework.web.context.WebApplicationContext; + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; + +@Component +@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) +public class RequestBodyCache { + + private final static ObjectMapper objectMapper = new ObjectMapper(); + private Map jsonBody; + + public Map getJsonBody(HttpServletRequest request) throws IOException { + if (jsonBody == null) { + try { + jsonBody = objectMapper.readValue( + request.getInputStream(), + new TypeReference<>() { + } + ); + } catch (com.fasterxml.jackson.databind.exc.MismatchedInputException e) { + jsonBody = Collections.emptyMap(); + } + } + return jsonBody; + } + +} diff --git a/src/main/java/de/samply/resolvers/RequestBodyCachingFilter.java b/src/main/java/de/samply/resolvers/RequestBodyCachingFilter.java new file mode 100644 index 00000000..55b952bf --- /dev/null +++ b/src/main/java/de/samply/resolvers/RequestBodyCachingFilter.java @@ -0,0 +1,38 @@ +package de.samply.resolvers; + +import de.samply.app.ProjectManagerConst; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.jspecify.annotations.NonNull; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingRequestWrapper; + +import java.io.IOException; + +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +public class RequestBodyCachingFilter extends OncePerRequestFilter { + + private final int cacheLimit; + + public RequestBodyCachingFilter( + @Value(ProjectManagerConst.REQUEST_CACHE_LIMIT_SV) int cacheLimit) { + this.cacheLimit = cacheLimit; + } + + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + ContentCachingRequestWrapper wrapped = new ContentCachingRequestWrapper(request, cacheLimit); + filterChain.doFilter(wrapped, response); + } +} \ No newline at end of file diff --git a/src/main/java/de/samply/resolvers/RequestVariableAndParameterMethodArgumentResolver.java b/src/main/java/de/samply/resolvers/RequestVariableAndParameterMethodArgumentResolver.java new file mode 100644 index 00000000..f6bbd55d --- /dev/null +++ b/src/main/java/de/samply/resolvers/RequestVariableAndParameterMethodArgumentResolver.java @@ -0,0 +1,197 @@ +package de.samply.resolvers; + +import de.samply.annotations.RequestParameter; +import de.samply.annotations.RequestVariable; +import de.samply.app.ProjectManagerConst; +import de.samply.utils.ParamMetaUtils; +import jakarta.servlet.http.HttpServletRequest; +import org.jspecify.annotations.NonNull; +import org.springframework.context.annotation.Lazy; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.ServletRequestBindingException; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; +import tools.jackson.databind.JavaType; +import tools.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.lang.reflect.ParameterizedType; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * This class resolves method arguments annotated with either {@link RequestVariable} or {@link RequestParameter}. + * + *

+ * It handles the extraction of parameter values from HTTP request parameters (query or form parameters) + * and, for {@link RequestVariable}, optionally from the request body (JSON). + *

+ * + *

+ * For {@link RequestVariable}, the resolver supports the 'required' and 'notEmpty' constraints: + *

    + *
  • If a required parameter is missing, a {@link ServletRequestBindingException} is thrown.
  • + *
  • If a parameter marked 'notEmpty' is empty, a {@link ServletRequestBindingException} is thrown.
  • + *
+ * For {@link RequestParameter}, only query/form parameters are considered; the JSON body is ignored. + *

+ * + *

+ * After extraction, the value is converted to the target method argument type using + * {@link org.springframework.core.convert.ConversionService} and, if necessary, Jackson's {@link com.fasterxml.jackson.databind.ObjectMapper}. + *

+ * + *

+ * Additionally, resolved and raw parameter values are stored in the request-scoped + * {@link AnnotatedParametersWrapper}, which can be used by other components (e.g., converters) + * to access previously resolved objects or their raw values. + *

+ * + *

Usage examples:

+ * + *
+ * // Extracted from query parameters or JSON body
+ * @RequestVariable(name = "username", required = true, notEmpty = true)
+ * String username;
+ *
+ * // Extracted only from query/form parameters
+ * @RequestParameter(name = "page")
+ * Integer page;
+ * 
+ * + * Important: For {@link RequestVariable}, the value will be extracted from the JSON body + * if not present as a request parameter. For {@link RequestParameter}, the JSON body is ignored. + */ +@Component +public class RequestVariableAndParameterMethodArgumentResolver implements HandlerMethodArgumentResolver { + + /** + * Lazily inject the global ConversionService to avoid a circular dependency. + *

+ * Context: + * When this resolver is a @Component and we try to inject ConversionService eagerly, + * Spring attempts to instantiate beans in the following cycle: + *

+ * projectManagerWebConfig -> requestVariableMethodArgumentResolver -> + * WebMvcAutoConfiguration.EnableWebMvcConfiguration -> ConversionService + *

+ * This creates a circular reference and prevents application startup. By marking + * ConversionService as @Lazy, Spring delays its injection until the first time it is + * actually necessary during argument resolution, breaking the cycle. + *

+ * Functionally, this has no effect on the resolver itself: argument resolution only + * happens at request time, long after all beans are initialized. + */ + private final ConversionService conversionService; + private final RequestBodyCache requestBodyCache; // Injecting request-scoped bean + private final AnnotatedParametersWrapper annotatedParametersWrapper; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + public RequestVariableAndParameterMethodArgumentResolver( + @Lazy ConversionService conversionService, + RequestBodyCache requestBodyCache, + AnnotatedParametersWrapper annotatedParametersWrapper) { + this.conversionService = conversionService; + this.requestBodyCache = requestBodyCache; + this.annotatedParametersWrapper = annotatedParametersWrapper; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(RequestVariable.class) + || parameter.hasParameterAnnotation(RequestParameter.class); + } + + @Override + public Object resolveArgument(@NonNull MethodParameter parameter, + ModelAndViewContainer mavContainer, + @NonNull NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) throws Exception { + + annotatedParametersWrapper.initializeIfNeeded(parameter, webRequest); + + ParamMeta paramMeta = ParamMetaUtils.extractParamMeta(parameter); + assert paramMeta != null; + + HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); + assert request != null; + + Object value = request.getParameter(paramMeta.name()); + + // Only for @RequestVariable, try the JSON body + if (value == null && paramMeta.bodyLookup()) { + value = extractFromJsonBody(request, paramMeta.name()); + } + + // Apply defaultValue if still null + if (value == null && !ProjectManagerConst.NOT_SET.equals(paramMeta.defaultValue())) { + value = paramMeta.defaultValue(); + } + + // Required / notEmpty checks + if (paramMeta.required() && value == null) { + throw new ServletRequestBindingException("Missing required parameter: " + paramMeta.name()); + } + if (paramMeta.notEmpty() && value instanceof String s && !StringUtils.hasText(s)) { + throw new ServletRequestBindingException("Parameter '" + paramMeta.name() + "' must not be empty."); + } + + // Convert and cache + Object result = convertValue(value, parameter); + annotatedParametersWrapper.putResolved(parameter, result); + + return result; + } + + private Object extractFromJsonBody(HttpServletRequest request, String key) throws IOException { + String contentType = request.getContentType(); + if (contentType == null || !contentType.contains("application/json")) { + return null; + } + Map jsonBody = requestBodyCache.getJsonBody(request); // Retrieve from the cache + return (jsonBody != null) ? jsonBody.get(key) : null; + } + + private Object convertValue(Object value, MethodParameter parameter) { + if (value == null) return null; + + Class targetType = parameter.getParameterType(); + + // If the value is a String, try Spring's ConversionService first + if (value instanceof String s) { + + // Check if the element is a list (e.g., param=val1,val2) + if (List.class.isAssignableFrom(targetType)) { + Class elementType = getListElementType(parameter); + return Arrays.stream(s.split(",")) + .map(String::trim) + .map(v -> conversionService.convert(v, elementType)) + .toList(); + } + + if (conversionService.canConvert(String.class, targetType)) { + return conversionService.convert(s, targetType); + } + // fallback to ObjectMapper if ConversionService cannot convert + return objectMapper.convertValue(s, targetType); + } + + // For non-String values (e.g., JSON Maps, Lists, Numbers), delegate to ObjectMapper + JavaType javaType = objectMapper.getTypeFactory() + .constructType(parameter.getGenericParameterType()); + return objectMapper.convertValue(value, javaType); + } + + private Class getListElementType(MethodParameter parameter) { + ParameterizedType type = (ParameterizedType) parameter.getGenericParameterType(); + return (Class) type.getActualTypeArguments()[0]; + } + +} \ No newline at end of file diff --git a/src/main/java/de/samply/rstudio/group/RstudioGroupService.java b/src/main/java/de/samply/rstudio/group/RstudioGroupService.java deleted file mode 100644 index ff029d0d..00000000 --- a/src/main/java/de/samply/rstudio/group/RstudioGroupService.java +++ /dev/null @@ -1,9 +0,0 @@ -package de.samply.rstudio.group; - -public interface RstudioGroupService { - - void addUserToRstudioGroup(String email) throws RstudioGroupServiceException; - - void removeUserFromRstudioGroup(String email) throws RstudioGroupServiceException; - -} diff --git a/src/main/java/de/samply/rstudio/group/RstudioGroupServiceException.java b/src/main/java/de/samply/rstudio/group/RstudioGroupServiceException.java deleted file mode 100644 index 754e75b2..00000000 --- a/src/main/java/de/samply/rstudio/group/RstudioGroupServiceException.java +++ /dev/null @@ -1,7 +0,0 @@ -package de.samply.rstudio.group; - -public class RstudioGroupServiceException extends RuntimeException{ - public RstudioGroupServiceException(Throwable cause) { - super(cause); - } -} diff --git a/src/main/java/de/samply/rstudio/group/RstudioKeycloakGroupService.java b/src/main/java/de/samply/rstudio/group/RstudioKeycloakGroupService.java deleted file mode 100644 index ceac44b3..00000000 --- a/src/main/java/de/samply/rstudio/group/RstudioKeycloakGroupService.java +++ /dev/null @@ -1,158 +0,0 @@ -package de.samply.rstudio.group; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import de.samply.app.ProjectManagerConst; -import de.samply.utils.WebClientFactory; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.exception.ExceptionUtils; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; -import org.springframework.stereotype.Service; -import org.springframework.web.reactive.function.client.WebClient; - -import java.util.Optional; -import java.util.function.Function; - -@Slf4j -@Service -@ConditionalOnProperty(name = ProjectManagerConst.RSTUDIO_GROUP_IMPLEMENTATION, havingValue = ProjectManagerConst.RSTUDIO_GROUP_KEYCLOAK_IMPLEMENTATION) -public class RstudioKeycloakGroupService implements RstudioGroupService { - - private final ObjectMapper objectMapper = new ObjectMapper(); - private final String tokenRequestBody; - private final String rstudioGroup; - private final String realm; - private final WebClient webClient; - private final boolean enabled; - - public RstudioKeycloakGroupService( - @Value(ProjectManagerConst.OIDC_URL_SV) String keycloakUrl, - @Value(ProjectManagerConst.KEYCLOAK_RSTUDIO_GROUP_CLIENT_ID_SV) String clientId, - @Value(ProjectManagerConst.KEYCLOAK_RSTUDIO_GROUP_CLIENT_SECRET_SV) String clientSecret, - @Value(ProjectManagerConst.KEYCLOAK_RSTUDIO_GROUP_SV) String rstudioGroup, - @Value(ProjectManagerConst.OIDC_REALM_SV) String realm, - @Value(ProjectManagerConst.ENABLE_RSTUDIO_GROUP_MANAGER_SV) boolean enabled, - WebClientFactory webClientFactory) { - this.tokenRequestBody = createTokenRequestBody(clientId, clientSecret); - this.rstudioGroup = rstudioGroup; - this.realm = realm; - this.enabled = enabled; - this.webClient = webClientFactory.createWebClient(keycloakUrl); - } - - private String createTokenRequestBody(String clientId, String clientSecret) { - StringBuilder stringBuilder = new StringBuilder(); - append(stringBuilder, ProjectManagerConst.GRANT_TYPE_KEYCLOAK_PARAM, ProjectManagerConst.CLIENT_CREDENTIALS_KEYCLOAK_CONST); - append(stringBuilder, ProjectManagerConst.CLIENT_ID_KEYCLOAK_PARAM, clientId); - append(stringBuilder, ProjectManagerConst.CLIENT_SECRET_KEYCLOAK_PARAM, clientSecret); - stringBuilder.deleteCharAt(stringBuilder.length() - 1); - return stringBuilder.toString(); - } - - private void append(StringBuilder stringBuilder, String key, String value) { - stringBuilder.append(key + "=" + value + "&"); - } - - - @Override - public void addUserToRstudioGroup(String email) throws RstudioGroupServiceException { - if (enabled) { - log.info("Adding user " + email + " to Rstudio-Group"); - addUserToGroup(email); - } - } - - @Override - public void removeUserFromRstudioGroup(String email) throws RstudioGroupServiceException { - if (enabled) { - log.info("Removing user " + email + " from Rstudio-Group"); - removeUserFromGroup(email); - } - } - - private void addUserToGroup(String email) { - modifyUserInGroup(email, HttpMethod.PUT); - } - - private void removeUserFromGroup(String email) { - modifyUserInGroup(email, HttpMethod.DELETE); - } - - private void modifyUserInGroup(String email, HttpMethod method) { - String authToken = fetchToken(); - Optional userId = fetchUserId(email, authToken); - if (userId.isPresent()) { - Optional groupId = fetchGroupId(rstudioGroup, authToken); - if (groupId.isPresent()) { - this.webClient.method(method) - .uri(ProjectManagerConst.CHANGE_USER_GROUP_KEYCLOAK_PATH, realm, userId.get(), groupId.get()) - .headers(httpHeaders -> addAuthenticationHeaders(httpHeaders, authToken)) - .retrieve().bodyToMono(String.class).block(); - } - } - } - - private Optional fetchUserId(String email, String authToken) { - try { - return Optional.ofNullable(fetchUserIdWithoutHandlingExeception(email, authToken)); - } catch (Exception e) { - log.error(ExceptionUtils.getStackTrace(e)); - return Optional.empty(); - } - } - - private String fetchUserIdWithoutHandlingExeception(String email, String authToken) { - return this.webClient.get() - .uri(ProjectManagerConst.FETCH_USER_ID_KEYCLOAK_PATH, realm, email) - .headers(httpHeaders -> addAuthenticationHeaders(httpHeaders, authToken)) - .retrieve().bodyToMono(String.class) - .map(response -> extractAttribute(response, jsonNode -> jsonNode.get(0).get(ProjectManagerConst.ID_KEYCLOAK_CONST))) - .block(); - } - - private Optional fetchGroupId(String group, String authToken) { - try { - return Optional.ofNullable(fetchGroupIdWithoutHandlingException(group, authToken)); - } catch (Exception e) { - log.error(ExceptionUtils.getStackTrace(e)); - return Optional.empty(); - } - } - - private String fetchGroupIdWithoutHandlingException(String group, String authToken) { - return this.webClient.get() - .uri(ProjectManagerConst.FETCH_GROUP_ID_KEYCLOAK_PATH, realm, group) - .headers(httpHeaders -> addAuthenticationHeaders(httpHeaders, authToken)) - .retrieve().bodyToMono(String.class) - .map(response -> extractAttribute(response, jsonNode -> jsonNode.get(0).get(ProjectManagerConst.ID_KEYCLOAK_CONST))) - .block(); - } - - private void addAuthenticationHeaders(HttpHeaders httpHeaders, String authToken) { - httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE); - httpHeaders.setBearerAuth(authToken); - } - - private String fetchToken() { - return this.webClient.post() - .uri(ProjectManagerConst.FETCH_TOKEN_KEYCLOAK_PATH, realm) - .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .bodyValue(tokenRequestBody) - .retrieve().bodyToMono(String.class) - .map(response -> extractAttribute(response, jsonNode -> jsonNode.get(ProjectManagerConst.ACCES_TOKEN_KEYCLOAK_CONST))) - .block(); - } - - private String extractAttribute(String jsonObject, Function attributeExtractor) { - try { - return attributeExtractor.apply(objectMapper.readTree(jsonObject)).asText(); - } catch (Exception e) { - throw new RstudioGroupServiceException(e); - } - } - -} diff --git a/src/main/java/de/samply/security/GrantedAuthoritiesExtractor.java b/src/main/java/de/samply/security/GrantedAuthoritiesExtractor.java index abfc78bf..b17ced49 100644 --- a/src/main/java/de/samply/security/GrantedAuthoritiesExtractor.java +++ b/src/main/java/de/samply/security/GrantedAuthoritiesExtractor.java @@ -6,6 +6,7 @@ import org.springframework.stereotype.Component; import java.util.Collection; +import java.util.Objects; import java.util.stream.Collectors; @Component @@ -22,7 +23,7 @@ public GrantedAuthoritiesExtractor(GroupToRoleMapper groupToRoleMapper) { public Collection extractAuthoritiesFromGroups(Collection groups) throws OAuth2AuthenticationException { return groups.stream() .map(groupToRoleMapper::getRoleFromGroup) - .filter(role -> role != null) + .filter(Objects::nonNull) .map(role -> new SimpleGrantedAuthority(role.name()))// change me .collect(Collectors.toList()); } diff --git a/src/main/java/de/samply/security/GroupToRoleMapper.java b/src/main/java/de/samply/security/GroupToRoleMapper.java index 98a589d3..13970531 100644 --- a/src/main/java/de/samply/security/GroupToRoleMapper.java +++ b/src/main/java/de/samply/security/GroupToRoleMapper.java @@ -3,7 +3,6 @@ import de.samply.app.ProjectManagerConst; import de.samply.bridgehead.BridgeheadConfiguration; import de.samply.user.roles.OrganisationRole; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -25,20 +24,26 @@ public class GroupToRoleMapper { @Value(ProjectManagerConst.BK_ADMIN_GROUP_SUFFIX_SV) private String bridgeheadAdminGroupSuffix; - @Autowired - private ProjectManagerAdminGroups projectManagerAdminGroups; + private final ProjectManagerAdminGroups projectManagerAdminGroups; - @Autowired - private SessionUser sessionUser; + private final SessionUser sessionUser; - @Autowired - private BridgeheadConfiguration bridgeheadConfiguration; + private final BridgeheadConfiguration bridgeheadConfiguration; private Boolean adminOverUser; private final Map groupToRoleMapCache = new HashMap<>(); + public GroupToRoleMapper( + ProjectManagerAdminGroups projectManagerAdminGroups, + SessionUser sessionUser, + BridgeheadConfiguration bridgeheadConfiguration) { + this.projectManagerAdminGroups = projectManagerAdminGroups; + this.sessionUser = sessionUser; + this.bridgeheadConfiguration = bridgeheadConfiguration; + } + public OrganisationRole getRoleFromGroup(String group) { group = removeSlashFromStart(group); OrganisationRole organisationRole = groupToRoleMapCache.get(group); @@ -79,7 +84,7 @@ private OrganisationRole addBridgheadToUserInfoAndFilterOrganisationRole(String } private String extractBridgehead(String prefix, String suffix, String group) { - return ((suffix.length() > 0) ? group.substring(prefix.length(), group.indexOf(suffix)) : group.substring(prefix.length())).toLowerCase(); + return ((!suffix.isEmpty()) ? group.substring(prefix.length(), group.indexOf(suffix)) : group.substring(prefix.length())).toLowerCase(); } // Important for Keycloak configuration of the groups diff --git a/src/main/java/de/samply/security/JwtDecoderConfig.java b/src/main/java/de/samply/security/JwtDecoderConfig.java new file mode 100644 index 00000000..ef5386ba --- /dev/null +++ b/src/main/java/de/samply/security/JwtDecoderConfig.java @@ -0,0 +1,104 @@ +package de.samply.security; + +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; +import de.samply.utils.WebClientFactory; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtDecoderFactory; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.web.reactive.function.client.WebClient; +import tools.jackson.databind.JsonNode; + +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + + +@Configuration +@EnableScheduling +@Slf4j +public class JwtDecoderConfig { + + private final WebClient webClient; + private final AtomicReference jwkSetRef = new AtomicReference<>(); + private final String jwksUri; + + public JwtDecoderConfig( + @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") String issuerUri, + WebClientFactory webClientFactory + ) { + if (issuerUri.endsWith("/")) { + issuerUri = issuerUri.substring(0, issuerUri.length() - 1); + } + + this.webClient = webClientFactory.createWebClient(issuerUri); + + JsonNode config = webClient.get() + .uri("/.well-known/openid-configuration") + .retrieve() + .bodyToMono(JsonNode.class) + .block(Duration.ofSeconds(5)); + + this.jwksUri = Objects.requireNonNull(config).required("jwks_uri").asString(); + log.info("JWKS URI: {}", jwksUri); + + refreshJwks(); + } + + /** + * Shared JWKSource for both Resource Server and OIDC Login + */ + @Bean + public JWKSource jwkSource() { + return (selector, _) -> selector.select(jwkSetRef.get()); + } + + /** + * Resource-server JwtDecoder + */ + @Bean + public JwtDecoder jwtDecoder(JWKSource jwkSource) { + return NimbusJwtDecoder.withJwkSource(jwkSource).build(); + } + + /** + * IMPORTANT: + * Override the decoder used for ID Token validation (OIDC login) + * This forces Spring Security to use YOUR cached JWKS instead of + * fetching it again over the network (which causes the timeout). + */ + @Bean + public JwtDecoderFactory idTokenDecoderFactory(JWKSource jwkSource) { + return _ -> NimbusJwtDecoder.withJwkSource(jwkSource).build(); + } + + /** + * Refresh JWKS every X ms (default 5 min) + */ + @Scheduled(fixedDelayString = "${security.jwt.jwks-refresh-ms:300000}") + public void refreshJwks() { + try { + String json = webClient.get() + .uri(jwksUri) + .retrieve() + .bodyToMono(String.class) + .block(Duration.ofSeconds(5)); + + if (json != null && !json.isBlank()) { + JWKSet jwkSet = JWKSet.parse(json); + jwkSetRef.set(jwkSet); + log.debug("JWKS refreshed, keys: {}", jwkSet.getKeys().size()); + } + } catch (Exception e) { + log.error("Failed to refresh JWKS", e); + } + } +} diff --git a/src/main/java/de/samply/security/OidcProjectUserService.java b/src/main/java/de/samply/security/OidcProjectUserService.java index e0313343..fdda3dc3 100644 --- a/src/main/java/de/samply/security/OidcProjectUserService.java +++ b/src/main/java/de/samply/security/OidcProjectUserService.java @@ -43,6 +43,12 @@ public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2Authenticatio OidcIdToken idToken = oidcUser.getIdToken(); OidcUserInfo userInfo = oidcUser.getUserInfo(); sessionUser.setEmail(userInfo.getEmail()); + sessionUser.setFirstName(userInfo.getGivenName()); + sessionUser.setLastName(userInfo.getFamilyName()); + if (sessionUser.getFirstName() == null && sessionUser.getLastName() == null) { + sessionUser.setLastName(userInfo.getFullName()); + } + sessionUser.resetUserOrganisationRoles(); Collection mappedAuthorities = extractAuthoritiesFromGroups(userInfo); newUsersImporter.importNewUsers(); @@ -50,14 +56,20 @@ public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2Authenticatio return new DefaultOidcUser(mappedAuthorities, idToken, userInfo); } - public Collection extractAuthoritiesFromGroups(OidcUserInfo userInfo) throws OAuth2AuthenticationException { + public Collection extractAuthoritiesFromGroups(OidcUserInfo userInfo) + throws OAuth2AuthenticationException { + Map claims = userInfo.getClaims(); - if (claims.containsKey(groupClaim)) { - return grantedAuthoritiesExtractor.extractAuthoritiesFromGroups((Collection) claims.get(groupClaim)); + Object groupsObj = claims.get(groupClaim); + + if (groupsObj instanceof Collection collection) { + return grantedAuthoritiesExtractor.extractAuthoritiesFromGroups(collection.stream() + .map(Object::toString) + .toList()); } + throw new OAuth2AuthenticationException("No groups found"); } - } diff --git a/src/main/java/de/samply/security/ProjectManagerAdminGroups.java b/src/main/java/de/samply/security/ProjectManagerAdminGroups.java index e0e6696f..9d903eb8 100644 --- a/src/main/java/de/samply/security/ProjectManagerAdminGroups.java +++ b/src/main/java/de/samply/security/ProjectManagerAdminGroups.java @@ -9,7 +9,7 @@ @Configuration public class ProjectManagerAdminGroups { - private List groups; + private final List groups; public ProjectManagerAdminGroups(@Value(ProjectManagerConst.PM_ADMIN_GROUPS_SV) List groups) { this.groups = groups; diff --git a/src/main/java/de/samply/security/ProjectUserJwtGrantedAuthoritiesConverter.java b/src/main/java/de/samply/security/ProjectUserJwtGrantedAuthoritiesConverter.java index f97ef821..5bc534c6 100644 --- a/src/main/java/de/samply/security/ProjectUserJwtGrantedAuthoritiesConverter.java +++ b/src/main/java/de/samply/security/ProjectUserJwtGrantedAuthoritiesConverter.java @@ -16,6 +16,7 @@ public class ProjectUserJwtGrantedAuthoritiesConverter implements Converter convert(Jwt jwt) { sessionUser.setEmail(jwt.getClaim(emailClaim)); sessionUser.setFirstName(jwt.getClaim(firstNameClaim)); sessionUser.setLastName(jwt.getClaim(lastNameClaim)); + if (sessionUser.getFirstName() == null && sessionUser.getLastName() == null) { + sessionUser.setLastName(jwt.getClaimAsString(fullNameClaim)); + } + sessionUser.resetUserOrganisationRoles(); Collection grantedAuthorities = grantedAuthoritiesExtractor.extractAuthoritiesFromGroups( jwt.getClaimAsStringList(groupClaim)); newUsersImporter.importNewUsers(); diff --git a/src/main/java/de/samply/security/RequestCacheFilter.java b/src/main/java/de/samply/security/RequestCacheFilter.java index 244302a8..ca22041d 100644 --- a/src/main/java/de/samply/security/RequestCacheFilter.java +++ b/src/main/java/de/samply/security/RequestCacheFilter.java @@ -4,6 +4,7 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.jspecify.annotations.NonNull; import org.springframework.security.web.savedrequest.HttpSessionRequestCache; import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.savedrequest.SavedRequest; @@ -15,13 +16,13 @@ @Component public class RequestCacheFilter extends OncePerRequestFilter { - private RequestCache requestCache = new HttpSessionRequestCache(); + private final RequestCache requestCache = new HttpSessionRequestCache(); @Override protected void doFilterInternal( - HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain ) throws ServletException, IOException { SavedRequest savedRequest = requestCache.getRequest(request, response); if (savedRequest == null && !isAuthenticationRequest(request)) { @@ -31,7 +32,7 @@ protected void doFilterInternal( } private boolean isAuthenticationRequest(HttpServletRequest request) { - // Add conditions here to identify your authentication-related endpoints + // Add conditions here to identify your authentication-related endpoints, // For example: return request.getRequestURI().equals("/oauth2/authorization/oidc"); } diff --git a/src/main/java/de/samply/security/SecurityConfiguration.java b/src/main/java/de/samply/security/SecurityConfiguration.java index 2113b8e7..9305e02e 100644 --- a/src/main/java/de/samply/security/SecurityConfiguration.java +++ b/src/main/java/de/samply/security/SecurityConfiguration.java @@ -7,15 +7,17 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.springframework.beans.factory.annotation.Autowired; +import org.jspecify.annotations.NonNull; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter; @@ -26,7 +28,6 @@ import org.springframework.security.web.savedrequest.HttpSessionRequestCache; import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.savedrequest.SavedRequest; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; @@ -39,8 +40,7 @@ @EnableWebSecurity public class SecurityConfiguration { - @Autowired - private RequestCacheFilter requestCacheFilter; + private final RequestCacheFilter requestCacheFilter; @Value(ProjectManagerConst.SECURITY_ENABLED_SV) private boolean isSecurityEnabled; @@ -48,56 +48,60 @@ public class SecurityConfiguration { @Value(ProjectManagerConst.EXPLORER_URL_SV) private String explorerUrl; - @Value(ProjectManagerConst.JWKS_URI_PROPERTY_SV) - private String jwksUri; + private final FrontendConfiguration frontendConfiguration; - @Autowired - private FrontendConfiguration frontendConfiguration; + private final ProjectUserJwtGrantedAuthoritiesConverter projectUserJwtGrantedAuthoritiesConverter; - @Autowired - private ProjectUserJwtGrantedAuthoritiesConverter projectUserJwtGrantedAuthoritiesConverter; + private final OidcProjectUserService oidcProjectUserService; - @Autowired - private OidcProjectUserService oidcProjectUserService; + public SecurityConfiguration(RequestCacheFilter requestCacheFilter, FrontendConfiguration frontendConfiguration, ProjectUserJwtGrantedAuthoritiesConverter projectUserJwtGrantedAuthoritiesConverter, OidcProjectUserService oidcProjectUserService) { + this.requestCacheFilter = requestCacheFilter; + this.frontendConfiguration = frontendConfiguration; + this.projectUserJwtGrantedAuthoritiesConverter = projectUserJwtGrantedAuthoritiesConverter; + this.oidcProjectUserService = oidcProjectUserService; + } @Order(1) @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain filterChain(HttpSecurity http) { return (isSecurityEnabled) ? http.addFilterBefore(requestCacheFilter, OAuth2LoginAuthenticationFilter.class) .authorizeHttpRequests(this::addAuthorityMapping) .cors(Customizer.withDefaults()) - .csrf(csrf -> csrf.disable()) + .csrf(AbstractHttpConfigurer::disable) .oauth2ResourceServer(resourceServerConfigurer -> - resourceServerConfigurer.jwt(jwtConfigurer -> { - jwtConfigurer.jwkSetUri(jwksUri); - jwtConfigurer.jwtAuthenticationConverter(jwtAuthenticationConverter()); - })) + resourceServerConfigurer.jwt(jwtConfigurer -> jwtConfigurer.jwtAuthenticationConverter(jwtAuthenticationConverter()))) .oauth2Login(oauth2 -> oauth2 .userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig.oidcUserService(oidcProjectUserService)) .successHandler(successHandler())) .build() : - http.authorizeRequests(authorize -> authorize.requestMatchers("/**").permitAll()).build(); + http.authorizeHttpRequests(authorize -> authorize.requestMatchers("/**").permitAll()).build(); } - private AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry addAuthorityMapping(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry authorization) { + private void addAuthorityMapping( + AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry authorization) { + // Services without authentication required - authorization.requestMatchers(new AntPathRequestMatcher(ProjectManagerConst.INFO, "GET")).permitAll(); + authorization.requestMatchers(HttpMethod.GET, ProjectManagerConst.INFO).permitAll(); + // Services with authentication required Map pathRolesMap = RolesExtractor.extractPathRolesMap(); - pathRolesMap.keySet().forEach(path -> authorization - .requestMatchers(new AntPathRequestMatcher(path, pathRolesMap.get(path).httpMethod())) - .hasAnyAuthority(pathRolesMap.get(path).roles())); - return authorization.anyRequest().authenticated(); + pathRolesMap.forEach((path, methodRoles) -> + authorization.requestMatchers(HttpMethod.valueOf(methodRoles.httpMethod()), path) + .hasAnyAuthority(methodRoles.roles()) + ); + + authorization.anyRequest().authenticated(); } + private AuthenticationSuccessHandler successHandler() { return new SimpleUrlAuthenticationSuccessHandler() { - private RequestCache requestCache = new HttpSessionRequestCache(); + private final RequestCache requestCache = new HttpSessionRequestCache(); @Override public void onAuthenticationSuccess( - HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + @NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Authentication authentication) throws IOException, ServletException { SavedRequest savedRequest = requestCache.getRequest(request, response); setUseReferer(true); if (savedRequest != null) { @@ -122,7 +126,8 @@ public JwtAuthenticationConverter jwtAuthenticationConverter() { CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(Arrays.asList(frontendConfiguration.getBaseUrl(), explorerUrl)); - configuration.setAllowedMethods(Arrays.asList("GET", "POST", "OPTIONS")); + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "OPTIONS", "PUT", "DELETE")); + //noinspection SpellCheckingInspection configuration.setAllowedHeaders(Arrays.asList(HttpHeaders.ORIGIN, HttpHeaders.CONTENT_TYPE, HttpHeaders.ACCEPT, HttpHeaders.AUTHORIZATION, "Returnaccept")); // Allow required headers configuration.setAllowCredentials(true); // Allow credentials configuration.setExposedHeaders(Arrays.asList(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, HttpHeaders.CONTENT_DISPOSITION)); // Expose required headers diff --git a/src/main/java/de/samply/security/SessionUser.java b/src/main/java/de/samply/security/SessionUser.java index 05e9f975..03771344 100644 --- a/src/main/java/de/samply/security/SessionUser.java +++ b/src/main/java/de/samply/security/SessionUser.java @@ -35,4 +35,8 @@ public Set getBridgeheads() { return getUserOrganisationRoles().getBridgeheads(); } + public void resetUserOrganisationRoles() { + userOrganisationRoles = new UserOrganisationRoles(); + } + } diff --git a/src/main/java/de/samply/user/DtoUserService.java b/src/main/java/de/samply/user/DtoUserService.java new file mode 100644 index 00000000..24fcb7ea --- /dev/null +++ b/src/main/java/de/samply/user/DtoUserService.java @@ -0,0 +1,71 @@ +package de.samply.user; + +import de.samply.db.model.Project; +import de.samply.db.model.ProjectBridgehead; +import de.samply.db.model.ProjectBridgeheadUser; +import de.samply.frontend.dto.DtoFactory; +import de.samply.frontend.dto.User; +import jakarta.validation.constraints.NotNull; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +@Service +public class DtoUserService { + + private final UserService userService; + private final DtoFactory dtoFactory; + + public DtoUserService(UserService userService, DtoFactory dtoFactory) { + this.userService = userService; + this.dtoFactory = dtoFactory; + } + + public Set fetchUsersForAutocomplete(@NotNull String partialEmail, @NotNull ProjectBridgehead bridgehead) { + Set allUsers = userService + .fetchAllUsers(partialEmail, bridgehead) + .stream() + .map(dtoFactory::convertFilteringProjectRoleAndState) + .collect(Collectors.toSet()); + Set alreadySetUsers = userService + .fetchAlreadySetUsers(partialEmail, bridgehead) + .stream() + .map(dtoFactory::convertFilteringProjectRoleAndState) + .collect(Collectors.toSet()); + allUsers.removeAll(alreadySetUsers); + return allUsers; + } + + public Optional fetchCurrentUser(@NotNull ProjectBridgehead bridgehead) { + return userService + .fetchCurrentUser(bridgehead) + .map(dtoFactory::convert); + } + + public Set fetchProjectUsers(@NotNull Project project) throws UserServiceException { + return convert(userService.fetchProjectUsers(project)); + } + + public List fetchMailingBlackList() { + return userService + .fetchMailingBlackList() + .stream() + .map(DtoFactory::convert) + .sorted(Comparator.comparing(User::firstName).thenComparing(User::lastName)) + .collect(Collectors.toList()); + } + + public List fetchUsersForAutocompleteInMailingBlackList(String email) { + return userService + .fetchUsersForAutocompleteInMailingBlackList(email) + .stream() + .map(DtoFactory::convert) + .collect(Collectors.toList()); + } + + private Set convert(Collection users) { + return users.stream().map(dtoFactory::convert).collect(Collectors.toSet()); + } + +} diff --git a/src/main/java/de/samply/user/UserService.java b/src/main/java/de/samply/user/UserService.java index 3c2ff461..92be9b12 100644 --- a/src/main/java/de/samply/user/UserService.java +++ b/src/main/java/de/samply/user/UserService.java @@ -1,11 +1,14 @@ package de.samply.user; import de.samply.db.model.*; -import de.samply.db.repository.*; -import de.samply.frontend.dto.DtoFactory; -import de.samply.frontend.dto.User; +import de.samply.db.repository.BridgeheadAdminUserRepository; +import de.samply.db.repository.CreatorUserRepository; +import de.samply.db.repository.ProjectManagerAdminUserRepository; +import de.samply.db.repository.UserRepository; import de.samply.notification.NotificationService; import de.samply.notification.OperationType; +import de.samply.project.ProjectBridgeheadService; +import de.samply.project.ProjectBridgeheadUserService; import de.samply.project.state.UserProjectState; import de.samply.security.SessionUser; import de.samply.user.roles.OrganisationRole; @@ -17,7 +20,6 @@ import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; -import java.time.Instant; import java.util.*; import java.util.stream.Collectors; @@ -25,189 +27,141 @@ @Service public class UserService { + private final SessionUser sessionUser; + private final OrganisationRoleToProjectRoleMapper organisationRoleToProjectRoleMapper; + + // Services private final NotificationService notificationService; + private final ProjectBridgeheadService projectBridgeheadService; + private final ProjectBridgeheadUserService projectBridgeheadUserService; + + // Repositories private final BridgeheadAdminUserRepository bridgeheadAdminUserRepository; private final ProjectManagerAdminUserRepository projectManagerAdminUserRepository; - private final ProjectBridgeheadUserRepository projectBridgeheadUserRepository; private final UserRepository userRepository; - private final ProjectRepository projectRepository; - private final ProjectBridgeheadRepository projectBridgeheadRepository; - private final SessionUser sessionUser; - private final OrganisationRoleToProjectRoleMapper organisationRoleToProjectRoleMapper; - private final DtoFactory dtoFactory; + private final CreatorUserRepository creatorUserRepository; + public UserService(NotificationService notificationService, BridgeheadAdminUserRepository bridgeheadAdminUserRepository, ProjectManagerAdminUserRepository projectManagerAdminUserRepository, - ProjectBridgeheadUserRepository projectBridgeheadUserRepository, UserRepository userRepository, - ProjectRepository projectRepository, - ProjectBridgeheadRepository projectBridgeheadRepository, + CreatorUserRepository creatorUserRepository, SessionUser sessionUser, OrganisationRoleToProjectRoleMapper organisationRoleToProjectRoleMapper, - DtoFactory dtoFactory) { + ProjectBridgeheadService projectBridgeheadService, + ProjectBridgeheadUserService projectBridgeheadUserService) { this.notificationService = notificationService; this.bridgeheadAdminUserRepository = bridgeheadAdminUserRepository; this.projectManagerAdminUserRepository = projectManagerAdminUserRepository; - this.projectBridgeheadUserRepository = projectBridgeheadUserRepository; this.userRepository = userRepository; - this.projectRepository = projectRepository; - this.projectBridgeheadRepository = projectBridgeheadRepository; + this.creatorUserRepository = creatorUserRepository; this.sessionUser = sessionUser; this.organisationRoleToProjectRoleMapper = organisationRoleToProjectRoleMapper; - this.dtoFactory = dtoFactory; + this.projectBridgeheadService = projectBridgeheadService; + this.projectBridgeheadUserService = projectBridgeheadUserService; } - public BridgeheadAdminUser createBridgeheadAdminUserIfNotExists(@NotNull String email, @NotNull String bridgehead) { + public void createBridgeheadAdminUserIfNotExists(@NotNull String email, @NotNull String bridgehead) { Optional bridgeheadAdminUserOptional = this.bridgeheadAdminUserRepository.findFirstByEmailAndBridgehead(email, bridgehead); BridgeheadAdminUser result; if (bridgeheadAdminUserOptional.isEmpty()) { result = new BridgeheadAdminUser(); result.setBridgehead(bridgehead); result.setEmail(email); - result = this.bridgeheadAdminUserRepository.save(result); - } else { - result = bridgeheadAdminUserOptional.get(); + this.bridgeheadAdminUserRepository.save(result); } - return result; } - public ProjectManagerAdminUser createProjectManagerAdminUserIfNotExists(@NotNull String email) { + public void createProjectManagerAdminUserIfNotExists(@NotNull String email) { Optional projectManagerAdminUserOptional = this.projectManagerAdminUserRepository.findFirstByEmail(email); ProjectManagerAdminUser result; if (projectManagerAdminUserOptional.isEmpty()) { result = new ProjectManagerAdminUser(); result.setEmail(email); - result = this.projectManagerAdminUserRepository.save(result); - } else { - result = projectManagerAdminUserOptional.get(); - } - return result; - } - - public ProjectBridgeheadUser createProjectBridgeheadUserIfNotExists(@NotNull String email, @NotNull ProjectBridgehead projectBridgehead, @NotNull ProjectRole projectRole) { - Optional projectBridgeheadUserOptional = this.projectBridgeheadUserRepository.findFirstByEmailAndProjectBridgeheadAndProjectRole(email, projectBridgehead, projectRole); - ProjectBridgeheadUser result; - if (projectBridgeheadUserOptional.isEmpty()) { - result = new ProjectBridgeheadUser(); - result.setEmail(email); - result.setProjectBridgehead(projectBridgehead); - result.setProjectRole(projectRole); - result = this.projectBridgeheadUserRepository.save(result); - this.notificationService.createNotification(projectBridgehead.getProject().getCode(), projectBridgehead.getBridgehead(), email, OperationType.ASSIGN_USER_TO_PROJECT, - "Set role " + projectRole + " to user", null, null); - } else { - result = projectBridgeheadUserOptional.get(); + this.projectManagerAdminUserRepository.save(result); } - return result; } - public void setProjectBridgheadUserWithRoleAndGenerateTokensIfDataShield(@NotNull String email, @NotNull String projectCode, @NotNull String bridgehead, @NotNull ProjectRole projectRole) throws UserServiceException { - Optional project = this.projectRepository.findByCode(projectCode); - if (project.isEmpty()) { - throw new UserServiceException("Project " + projectCode + " not found"); - } - Optional projectBridgehead = this.projectBridgeheadRepository.findFirstByBridgeheadAndProject(bridgehead, project.get()); - if (projectBridgehead.isEmpty()) { - throw new UserServiceException("Bridgehead " + bridgehead + " not involved in project " + projectCode); - } - Optional projectBridgeheadUserOptional = this.projectBridgeheadUserRepository.findFirstByEmailAndProjectBridgeheadAndProjectRole(email, projectBridgehead.get(), projectRole); + public void setProjectBridgeheadUserWithRoleAndGenerateTokensIfDataShield(@NotNull String email, @NotNull Project project, @NotNull ProjectBridgehead bridgehead, @NotNull ProjectRole projectRole) throws UserServiceException { + Optional projectBridgeheadUserOptional = projectBridgeheadUserService.fetchFirstUser(email, bridgehead, projectRole); if (projectBridgeheadUserOptional.isEmpty()) { ProjectBridgeheadUser projectBridgeheadUser = new ProjectBridgeheadUser(); projectBridgeheadUser.setEmail(email); projectBridgeheadUser.setProjectRole(projectRole); - projectBridgeheadUser.setProjectBridgehead(projectBridgehead.get()); + projectBridgeheadUser.setProjectBridgehead(bridgehead); projectBridgeheadUser.setProjectState(UserProjectState.CREATED); - projectBridgeheadUser.setModifiedAt(Instant.now()); - this.projectBridgeheadUserRepository.save(projectBridgeheadUser); - this.notificationService.createNotification(projectCode, bridgehead, email, OperationType.ASSIGN_USER_TO_PROJECT, + projectBridgeheadUserService.saveUser(projectBridgeheadUser); + this.notificationService.createNotification(project, bridgehead.getBridgehead(), email, OperationType.ASSIGN_USER_TO_PROJECT, "Set role " + projectRole + " to user", null, null); } } - public void acceptProject(@NotNull String projectCode, @NotNull String bridgehead) throws UserServiceException { - changeProjectState(projectCode, bridgehead, UserProjectState.ACCEPTED); + public void acceptProject(@NotNull Project project, @NotNull ProjectBridgehead bridgehead) throws UserServiceException { + changeProjectState(project, bridgehead, UserProjectState.ACCEPTED); } - public void rejectProject(@NotNull String projectCode, @NotNull String bridgehead) throws UserServiceException { - changeProjectState(projectCode, bridgehead, UserProjectState.REJECTED); + public void rejectProject(@NotNull Project project, @NotNull ProjectBridgehead bridgehead) throws UserServiceException { + changeProjectState(project, bridgehead, UserProjectState.REJECTED); } - public void requestChangesInProject(@NotNull String projectCode, @NotNull String bridgehead) throws UserServiceException { - changeProjectState(projectCode, bridgehead, UserProjectState.REQUEST_CHANGES); + public void requestChangesInProject(@NotNull Project project, @NotNull ProjectBridgehead bridgehead) throws UserServiceException { + changeProjectState(project, bridgehead, UserProjectState.REQUEST_CHANGES); } - private void changeProjectState(@NotNull String projectCode, @NotNull String bridgehead, @NotNull UserProjectState state) throws UserServiceException { + private void changeProjectState(@NotNull Project project, @NotNull ProjectBridgehead bridgehead, @NotNull UserProjectState state) throws UserServiceException { Optional projectBridgeheadUser = - projectBridgeheadUserRepository.getFirstByEmailAndProjectBridgeheadOrderByModifiedAtDesc(sessionUser.getEmail(), - fetchProjectBridgehead(projectCode, bridgehead)); + projectBridgeheadUserService.fetchFirstUsersOrderByModifiedAtDesc(sessionUser.getEmail(), bridgehead); if (projectBridgeheadUser.isEmpty()) { - throw new UserServiceException("Project " + projectCode + " for bridgehead " + bridgehead + " and user " + sessionUser.getEmail()); + throw new UserServiceException("ProjectCode " + project.getCode() + " for bridgehead " + bridgehead.getBridgehead() + " and user " + sessionUser.getEmail()); } projectBridgeheadUser.get().setProjectState(state); - projectBridgeheadUser.get().setModifiedAt(Instant.now()); - projectBridgeheadUserRepository.save(projectBridgeheadUser.get()); - this.notificationService.createNotification(projectCode, bridgehead, sessionUser.getEmail(), OperationType.CHANGE_PROJECT_BRIDGEHEAD_USER_EVALUATION, + projectBridgeheadUserService.saveUser(projectBridgeheadUser.get()); + this.notificationService.createNotification(project, bridgehead.getBridgehead(), sessionUser.getEmail(), OperationType.CHANGE_PROJECT_BRIDGEHEAD_USER_EVALUATION, "Set project bridgehead user evaluation to " + state, null, null); } - public Set fetchUsersForAutocomplete(@NotNull String projectCode, @NotNull String partialEmail, @NotNull String bridgehead) { - Set allUsers = projectBridgeheadUserRepository.getDistinctByEmailContainingAndProjectBridgehead_Bridgehead(partialEmail, bridgehead).stream().map(dtoFactory::convertFilteringProjectRoleAndState).collect(Collectors.toSet()); - Set alreadySetUsers = projectBridgeheadUserRepository.getDistinctByEmailContainingAndProjectBridgehead_BridgeheadAndUserAlreadySetForThisProjectInThisRole(partialEmail, bridgehead, projectCode).stream().map(dtoFactory::convertFilteringProjectRoleAndState).collect(Collectors.toSet()); - allUsers.removeAll(alreadySetUsers); - return allUsers; + protected List fetchAllUsers(@NotNull String partialEmail, @NotNull ProjectBridgehead bridgehead) { + return projectBridgeheadUserService.fetchDistinctUsers(partialEmail, bridgehead); } - public Optional fetchCurrentUser(@NotNull String projectCode, @NotNull String bridgehead) { - Optional user = this.projectBridgeheadUserRepository.getFirstValidByEmailAndProjectBridgehead(sessionUser.getEmail(), fetchProjectBridgehead(projectCode, bridgehead)); - return (user.isEmpty()) ? Optional.empty() : Optional.ofNullable(dtoFactory.convert(user.get())); + protected List fetchAlreadySetUsers(@NotNull String partialEmail, @NotNull ProjectBridgehead bridgehead) { + return projectBridgeheadUserService.fetchUsersAlreadySetInThisProjectForThisRole(partialEmail, bridgehead); } - public Set fetchProjectUsers(@NotNull String projectCode) throws UserServiceException { - return projectBridgeheadRepository.findByProject(fetchProject(projectCode)).stream().flatMap(projectBridgehead -> fetchProjectUsers(projectBridgehead).stream()).collect(Collectors.toSet()); + protected Optional fetchCurrentUser(@NotNull ProjectBridgehead bridgehead) { + return projectBridgeheadUserService.fetchFirstValidUser(sessionUser.getEmail(), bridgehead); } - public Set fetchProjectUsers(ProjectBridgehead projectBridgehead) throws UserServiceException { + public List fetchProjectUsers(ProjectBridgehead projectBridgehead) throws UserServiceException { return (switch (projectBridgehead.getProject().getState()) { - case DEVELOP -> - this.projectBridgeheadUserRepository.getDistinctByProjectRoleAndProjectBridgehead(ProjectRole.DEVELOPER, projectBridgehead); - case PILOT -> - this.projectBridgeheadUserRepository.getDistinctByProjectRoleAndProjectBridgehead(ProjectRole.PILOT, projectBridgehead); - case FINAL -> - this.projectBridgeheadUserRepository.getDistinctByProjectRoleAndProjectBridgehead(ProjectRole.FINAL, projectBridgehead); - default -> new ArrayList(); - }).stream().map(dtoFactory::convert).collect(Collectors.toSet()); + case DEVELOP -> projectBridgeheadUserService.fetchUsers(ProjectRole.DEVELOPER, projectBridgehead); + case PILOT -> projectBridgeheadUserService.fetchUsers(ProjectRole.PILOT, projectBridgehead); + case FINAL -> projectBridgeheadUserService.fetchUsers(ProjectRole.FINAL, projectBridgehead); + default -> new ArrayList<>(); + }); } - public boolean existInvitedUsers(@NotNull String projectCode) throws UserServiceException { - return !fetchProjectUsers(projectCode).isEmpty(); + protected Set fetchProjectUsers(@NotNull Project project) throws UserServiceException { + return projectBridgeheadService + .fetchBridgeheads(project) + .stream() + .flatMap(projectBridgehead -> + fetchProjectUsers(projectBridgehead).stream()) + .collect(Collectors.toSet()); } - private ProjectBridgehead fetchProjectBridgehead(String projectCode, String bridgehead) throws UserServiceException { - Optional projectBridgehead = projectBridgeheadRepository.findFirstByBridgeheadAndProject(bridgehead, fetchProject(projectCode)); - if (projectBridgehead.isEmpty()) { - throw new UserServiceException("Project " + projectCode + " for bridgehead " + bridgehead + " not found"); - } - return projectBridgehead.get(); + public boolean existInvitedUsers(@NotNull Project project) throws UserServiceException { + return !fetchProjectUsers(project).isEmpty(); } - private Project fetchProject(@NotNull String projectCode) throws UserServiceException { - Optional project = projectRepository.findByCode(projectCode); - if (project.isEmpty()) { - throw new UserServiceException("Project " + projectCode + " not found"); - } - return project.get(); - } - - public Set fetchProjectRoles(@NotNull String projectCode, Optional bridgehead) throws UserServiceException { - Optional userProjectRoles = organisationRoleToProjectRoleMapper.map(projectCode); + public Set fetchProjectRoles(@NotNull Project project, Optional bridgehead) throws UserServiceException { + Optional userProjectRoles = organisationRoleToProjectRoleMapper.map(project); if (userProjectRoles.isEmpty()) { return new HashSet<>(); } Set result = userProjectRoles.get().getRolesNotDependentOnBridgeheads(); - if (bridgehead.isPresent()) { - result.addAll(userProjectRoles.get().getBridgeheadRoles(bridgehead.get())); - } + bridgehead.ifPresent(s -> result.addAll(userProjectRoles.get().getBridgeheadRoles(s.getBridgehead()))); return result; } @@ -216,7 +170,7 @@ public Boolean isProjectManagerAdmin() { } public synchronized void addUserInformationIfNotExists(String email, String firstName, String lastName) { - if (StringUtils.hasText(email) && StringUtils.hasText(firstName) && StringUtils.hasText(lastName)) { + if (StringUtils.hasText(email) && (StringUtils.hasText(firstName) || StringUtils.hasText(lastName))) { Optional userOptional = userRepository.findByEmail(email); if (userOptional.isEmpty()) { de.samply.db.model.User user = new de.samply.db.model.User(); @@ -224,7 +178,7 @@ public synchronized void addUserInformationIfNotExists(String email, String firs user.setFirstName(firstName); user.setLastName(lastName); userRepository.save(user); - log.info("Added user information for " + email); + log.info("Added user information for {}", email); } } } @@ -247,14 +201,43 @@ public boolean isUserInMailingBlackList(@NotNull String email) throws UserServic return false; } - public List fetchMailingBlackList() { - return userRepository.findByIsInMailingBlackListIsTrue().stream().map(DtoFactory::convert) - .sorted(Comparator.comparing(User::firstName).thenComparing(User::lastName)) - .collect(Collectors.toList()); + protected List fetchMailingBlackList() { + return userRepository.findByIsInMailingBlackListIsTrue(); + } + + protected List fetchUsersForAutocompleteInMailingBlackList(String email) { + return userRepository.findByEmailContainingIgnoreCaseAndIsInMailingBlackListIsFalse(email); + } + + public void addCreatorIfNotExists() { + sessionUser.getBridgeheads().forEach(bridgehead -> + creatorUserRepository.findByEmailAndBridgehead(sessionUser.getEmail(), bridgehead) + .orElseGet(() -> { + CreatorUser creatorUser = new CreatorUser(); + creatorUser.setBridgehead(bridgehead); + creatorUser.setEmail(sessionUser.getEmail()); + return creatorUserRepository.save(creatorUser); + })); + } + + public List fetchAllProjectManagerAdmins() { + return projectManagerAdminUserRepository.findAll(); + } + + public Set fetchCreatorUser(String email) { + return creatorUserRepository.findByEmail(email); + } + + public Optional fetchUser(String user) { + return userRepository.findByEmail(user); + } + + public Set fetchBridgeheadAdmin(ProjectBridgehead bridgehead) { + return bridgeheadAdminUserRepository.findByBridgehead(bridgehead.getBridgehead()); } - public List fetchUsersForAutocompleteInMailingBlackList(String email) { - return userRepository.findByEmailContainingIgnoreCaseAndIsInMailingBlackListIsFalse(email).stream().map(DtoFactory::convert).collect(Collectors.toList()); + public Optional fetchFirstBridgeheadAdmin(ProjectBridgehead bridgehead) { + return bridgeheadAdminUserRepository.findFirstByBridgehead(bridgehead.getBridgehead()); } } diff --git a/src/main/java/de/samply/user/UserServiceException.java b/src/main/java/de/samply/user/UserServiceException.java index acabcf86..6ad936c5 100644 --- a/src/main/java/de/samply/user/UserServiceException.java +++ b/src/main/java/de/samply/user/UserServiceException.java @@ -1,24 +1,9 @@ package de.samply.user; -public class UserServiceException extends RuntimeException{ - - public UserServiceException() { - } +public class UserServiceException extends RuntimeException { public UserServiceException(String message) { super(message); } - - public UserServiceException(String message, Throwable cause) { - super(message, cause); - } - - public UserServiceException(Throwable cause) { - super(cause); - } - - public UserServiceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } - + } diff --git a/src/main/java/de/samply/user/roles/OrganisationRole.java b/src/main/java/de/samply/user/roles/OrganisationRole.java index adbb33f6..5e21e55e 100644 --- a/src/main/java/de/samply/user/roles/OrganisationRole.java +++ b/src/main/java/de/samply/user/roles/OrganisationRole.java @@ -3,6 +3,5 @@ public enum OrganisationRole { RESEARCHER, BRIDGEHEAD_ADMIN, - PROJECT_MANAGER_ADMIN, - REMOVED + PROJECT_MANAGER_ADMIN } diff --git a/src/main/java/de/samply/user/roles/OrganisationRoleToProjectRoleMapper.java b/src/main/java/de/samply/user/roles/OrganisationRoleToProjectRoleMapper.java index ce0efb32..18f6eb7b 100644 --- a/src/main/java/de/samply/user/roles/OrganisationRoleToProjectRoleMapper.java +++ b/src/main/java/de/samply/user/roles/OrganisationRoleToProjectRoleMapper.java @@ -1,9 +1,8 @@ package de.samply.user.roles; import de.samply.db.model.Project; -import de.samply.db.repository.ProjectBridgeheadRepository; -import de.samply.db.repository.ProjectBridgeheadUserRepository; -import de.samply.db.repository.ProjectRepository; +import de.samply.project.ProjectBridgeheadService; +import de.samply.project.ProjectBridgeheadUserService; import de.samply.security.SessionUser; import org.springframework.stereotype.Component; @@ -14,30 +13,22 @@ public class OrganisationRoleToProjectRoleMapper { private final SessionUser sessionUser; - private final ProjectRepository projectRepository; - private final ProjectBridgeheadRepository projectBridgeheadRepository; - private final ProjectBridgeheadUserRepository projectBridgeheadUserRepository; - private Map organisationToProjectRoleMap = Map.of( + + // Services + private final ProjectBridgeheadService projectBridgeheadService; + private final ProjectBridgeheadUserService projectBridgeheadUserService; + + private final Map organisationToProjectRoleMap = Map.of( OrganisationRole.PROJECT_MANAGER_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN, OrganisationRole.BRIDGEHEAD_ADMIN, ProjectRole.BRIDGEHEAD_ADMIN ); public OrganisationRoleToProjectRoleMapper(SessionUser sessionUser, - ProjectRepository projectRepository, - ProjectBridgeheadRepository projectBridgeheadRepository, - ProjectBridgeheadUserRepository projectBridgeheadUserRepository) { + ProjectBridgeheadService projectBridgeheadService, + ProjectBridgeheadUserService projectBridgeheadUserService) { this.sessionUser = sessionUser; - this.projectRepository = projectRepository; - this.projectBridgeheadRepository = projectBridgeheadRepository; - this.projectBridgeheadUserRepository = projectBridgeheadUserRepository; - } - - public Optional map(String projectCode) { - if (projectCode == null) { - return Optional.empty(); - } - Optional project = projectRepository.findByCode(projectCode); - return (project.isPresent()) ? map(project.get()) : Optional.empty(); + this.projectBridgeheadService = projectBridgeheadService; + this.projectBridgeheadUserService = projectBridgeheadUserService; } public Optional map(Project project) { @@ -60,9 +51,16 @@ public Optional map(Project project) { if (projectRole != null) { result.addBridgeheadRole(bridgehead, projectRole); } else { - projectBridgeheadRepository.findFirstByBridgeheadAndProject(bridgehead, project).ifPresent(projectBridgehead -> - projectBridgeheadUserRepository.getByEmailAndProjectBridgehead(sessionUser.getEmail(), projectBridgehead).forEach(projectBridgeheadUser -> - result.addBridgeheadRole(bridgehead, projectBridgeheadUser.getProjectRole()))); + projectBridgeheadService + .fetchBridgehead(project, bridgehead) + .ifPresent(projectBridgehead -> + projectBridgeheadUserService.fetchUsers( + sessionUser.getEmail(), + projectBridgehead) + .forEach(projectBridgeheadUser -> + result.addBridgeheadRole( + bridgehead, + projectBridgeheadUser.getProjectRole()))); } })); return Optional.of(result); diff --git a/src/main/java/de/samply/user/roles/ProjectRole.java b/src/main/java/de/samply/user/roles/ProjectRole.java index c1df8357..404108a0 100644 --- a/src/main/java/de/samply/user/roles/ProjectRole.java +++ b/src/main/java/de/samply/user/roles/ProjectRole.java @@ -1,7 +1,7 @@ package de.samply.user.roles; public enum ProjectRole { - CREATOR, + CREATOR, // This role is intended for restrictions. It shouldn't appear in a project bridgehead user, as the creator is not associated with a specific bridgehead but to the whole project. DEVELOPER, PILOT, FINAL, diff --git a/src/main/java/de/samply/user/roles/RolesExtractor.java b/src/main/java/de/samply/user/roles/RolesExtractor.java index 544fbe66..43c04f37 100644 --- a/src/main/java/de/samply/user/roles/RolesExtractor.java +++ b/src/main/java/de/samply/user/roles/RolesExtractor.java @@ -22,7 +22,7 @@ public static Map extractPathRolesMap() { RoleConstraints roleConstraints = method.getAnnotation(RoleConstraints.class); if (roleConstraints != null && roleConstraints.organisationRoles().length > 0) { fetchPath(method).ifPresent(path -> AspectUtils.fetchHttpMethod(method).ifPresent(httpMethod -> result.put(rootPath + path, - new MethodRoles(httpMethod, Arrays.stream(roleConstraints.organisationRoles()).map(role -> role.name()).toArray(String[]::new))))); + new MethodRoles(httpMethod, Arrays.stream(roleConstraints.organisationRoles()).map(Enum::name).toArray(String[]::new))))); } }); return result; @@ -61,7 +61,7 @@ private static Optional fetchPathWithoutExceptionHandling(Annotation ann public static String getRootPath() { Optional path = fetchPath(ProjectManagerController.class.getAnnotation(RequestMapping.class)); - return (path.isPresent()) ? path.get() : ""; + return path.orElse(""); } } diff --git a/src/main/java/de/samply/user/roles/UserRoles.java b/src/main/java/de/samply/user/roles/UserRoles.java index d629a9e1..258e1d9c 100644 --- a/src/main/java/de/samply/user/roles/UserRoles.java +++ b/src/main/java/de/samply/user/roles/UserRoles.java @@ -1,20 +1,26 @@ package de.samply.user.roles; -import java.util.*; +import lombok.Getter; + +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; public class UserRoles { - private final Map> bridgheadRolesMap = new HashMap<>(); - private final Set rolesNotDependentOnBridgeheads = new HashSet<>(); + private final Map> bridgheadRolesMap = new ConcurrentHashMap<>(); + + @Getter + private final Set rolesNotDependentOnBridgeheads = ConcurrentHashMap.newKeySet(); public void addBridgeheadRole(String bridgehead, T role) { if (bridgehead != null && role != null) { - Set bridgeheadRoles = bridgheadRolesMap.get(bridgehead); - if (bridgeheadRoles == null) { - bridgeheadRoles = new HashSet<>(); - bridgheadRolesMap.put(bridgehead, bridgeheadRoles); - } - bridgeheadRoles.add(role); + bridgheadRolesMap + .computeIfAbsent(bridgehead, _ -> ConcurrentHashMap.newKeySet()) + .add(role); } } @@ -24,10 +30,6 @@ public void addRoleNotDependentOnBridgehead(T role) { } } - public Set getRolesNotDependentOnBridgeheads() { - return rolesNotDependentOnBridgeheads; - } - public Set getBridgeheadRoles(String bridgehead) { Set result = bridgheadRolesMap.get(bridgehead); return (result != null) ? result : new HashSet<>(); @@ -41,6 +43,7 @@ public boolean containsRole(T role) { return containsRole(role, Optional.empty()); } + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") // bridghead as optional public boolean containsRole(T role, Optional bridgehead) { if (rolesNotDependentOnBridgeheads.contains(role)) { return true; @@ -53,4 +56,15 @@ public boolean containsRole(T role, Optional bridgehead) { return false; } + // Checks the role independently of the bridgehead + public boolean containsAnyRole(T role) { + if (rolesNotDependentOnBridgeheads.contains(role)) { + return true; + } else { + return bridgheadRolesMap.values().stream() + .flatMap(Set::stream) + .collect(Collectors.toSet()).contains(role); + } + } + } diff --git a/src/main/java/de/samply/utils/AspectUtils.java b/src/main/java/de/samply/utils/AspectUtils.java index 5dc8661d..dba7e1c6 100644 --- a/src/main/java/de/samply/utils/AspectUtils.java +++ b/src/main/java/de/samply/utils/AspectUtils.java @@ -1,49 +1,49 @@ package de.samply.utils; -import de.samply.annotations.Bridgehead; -import de.samply.annotations.Email; -import de.samply.annotations.Message; -import de.samply.annotations.ProjectCode; +import de.samply.annotations.*; import de.samply.db.model.Project; -import de.samply.db.repository.ProjectRepository; +import de.samply.db.model.ProjectBridgehead; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.Arrays; -import java.util.HashSet; +import java.util.Objects; import java.util.Optional; -import java.util.Set; public class AspectUtils { - public static Optional fetchBridgehead(JoinPoint joinPoint) { - return fetchStringParameterAnnotation(joinPoint, Bridgehead.class); + public static Optional fetchBridgehead(JoinPoint joinPoint) { + return fetchParameterAnnotation(joinPoint, Bridgehead.class, ProjectBridgehead.class); } - public static Optional fetchProjectCode(JoinPoint joinPoint) { - return fetchStringParameterAnnotation(joinPoint, ProjectCode.class); + public static Optional fetchProject(JoinPoint joinPoint) { + return fetchParameterAnnotation(joinPoint, ProjectCode.class, Project.class); } public static Optional fetchEmail(JoinPoint joinPoint) { - return fetchStringParameterAnnotation(joinPoint, Email.class); + return fetchParameterAnnotation(joinPoint, Email.class, String.class); } public static Optional fetchMessage(JoinPoint joinPoint) { - return fetchStringParameterAnnotation(joinPoint, Message.class); + return fetchParameterAnnotation(joinPoint, Message.class, String.class); } - private static Optional fetchStringParameterAnnotation(JoinPoint joinPoint, Class annotationClass) { + public static Optional fetchParameterAnnotation( + JoinPoint joinPoint, + Class annotationClass, + Class targetClass + ) { Annotation[][] parameterAnnotations = fetchMethod(joinPoint).getParameterAnnotations(); Object[] args = joinPoint.getArgs(); + for (int i = 0; i < parameterAnnotations.length; i++) { for (Annotation annotation : parameterAnnotations[i]) { if (annotation.annotationType() == annotationClass) { - if (args[i] instanceof String) { - return Optional.ofNullable((String) args[i]); + if (targetClass.isInstance(args[i])) { + return Optional.of(targetClass.cast(args[i])); } } } @@ -59,17 +59,21 @@ public static Optional fetchT(JoinPoint joinPoint, Cla return Optional.ofNullable(fetchMethod(joinPoint).getAnnotation(clazz)); } - public static Optional fetchProject(ProjectRepository projectRepository, Optional projectCode) { - return (projectCode.isPresent()) ? projectRepository.findByCode(projectCode.get()) : Optional.empty(); - } - public static String[] fetchRequestParamNames(Method method) { - Set result = new HashSet<>(); - Arrays.stream(method.getParameters()).forEach(parameter -> Arrays.stream(parameter.getAnnotations()) - .filter(annotation -> annotation.annotationType() == RequestParam.class) - .map(annotation -> (RequestParam) annotation) - .forEach(requestParam -> result.add(requestParam.name()))); - return result.toArray(String[]::new); + return Arrays.stream(method.getParameters()) + .flatMap(parameter -> Arrays.stream(parameter.getAnnotations()) + .filter( + annotation -> + annotation instanceof RequestVariable || + annotation instanceof RequestParameter) + .map(annotation -> switch (annotation) { + case RequestParameter requestParameter -> requestParameter.name(); + case RequestVariable requestVariable -> requestVariable.name(); + default -> null; + }) + .filter(Objects::nonNull)) + .distinct() + .toArray(String[]::new); } public static Optional fetchHttpMethod(Method method) { @@ -78,15 +82,16 @@ public static Optional fetchHttpMethod(Method method) { for (Annotation methodAnnotation : method.getDeclaredAnnotations()) { for (Annotation methodAnnotationAnnotation : methodAnnotation.annotationType().getDeclaredAnnotations()) { if (methodAnnotationAnnotation.annotationType() == RequestMapping.class) { - return fetchHttpMetod((RequestMapping) methodAnnotationAnnotation); + return fetchHttpMethod((RequestMapping) methodAnnotationAnnotation); } } } } - return fetchHttpMetod(annotation); + assert annotation != null; + return fetchHttpMethod(annotation); } - private static Optional fetchHttpMetod(RequestMapping requestMapping) { + private static Optional fetchHttpMethod(RequestMapping requestMapping) { return Optional.of(requestMapping.method()[0].name()); } diff --git a/src/main/java/de/samply/utils/Base64Utils.java b/src/main/java/de/samply/utils/Base64Utils.java index d5e958f7..412a2f89 100644 --- a/src/main/java/de/samply/utils/Base64Utils.java +++ b/src/main/java/de/samply/utils/Base64Utils.java @@ -1,6 +1,5 @@ package de.samply.utils; -import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Optional; @@ -14,14 +13,6 @@ public static Optional decodeIfNecessary(String element) { } } - public static Optional decodeIfNecessary(byte[] element) { - try { - return (element != null) ? Optional.of(new String(Base64.getDecoder().decode(element))) : Optional.empty(); - } catch (IllegalArgumentException e) { - return Optional.of(new String(element, StandardCharsets.UTF_8)); - } - } - public static String encode(String element) { return Base64.getEncoder().encodeToString(element.getBytes()); } diff --git a/src/main/java/de/samply/utils/DateUtils.java b/src/main/java/de/samply/utils/DateUtils.java new file mode 100644 index 00000000..0fd5bc9f --- /dev/null +++ b/src/main/java/de/samply/utils/DateUtils.java @@ -0,0 +1,30 @@ +package de.samply.utils; + +import de.samply.app.ProjectManagerConst; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +public class DateUtils { + + public static String fetchCurrentDate(String dateTimePattern) { + return DateTimeFormatter + .ofPattern(dateTimePattern) + .format(Instant.now() + .atZone(ZoneId.of(ProjectManagerConst.FORM_FILENAME_TIMESTAMP_ZONE))); + } + + public static String fetchCurrentDate(String dateTimePattern, String languageCode) { + return fetchDate(Instant.now(), dateTimePattern, languageCode); + } + + public static String fetchDate(Instant date, String dateTimePattern, String languageCode) { + return DateTimeFormatter + .ofPattern(dateTimePattern, Locale.forLanguageTag(languageCode)) + .format(date.atZone(ZoneId.of(ProjectManagerConst.FORM_FILENAME_TIMESTAMP_ZONE))); + } + + +} diff --git a/src/main/java/de/samply/utils/DirectoryUtils.java b/src/main/java/de/samply/utils/DirectoryUtils.java new file mode 100644 index 00000000..aabcad03 --- /dev/null +++ b/src/main/java/de/samply/utils/DirectoryUtils.java @@ -0,0 +1,20 @@ +package de.samply.utils; + +import org.springframework.util.StringUtils; + +import java.io.FileNotFoundException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class DirectoryUtils { + + public static String fetchExternalTemplateDirectory(String environmentVariable) throws FileNotFoundException { + if (StringUtils.hasText(environmentVariable) && Files.isDirectory(Path.of(environmentVariable))) { + return (environmentVariable.endsWith("/") || environmentVariable.endsWith("\\")) ? + environmentVariable : environmentVariable + "/"; + } else { + throw new FileNotFoundException("External Templates Directory not set or set to incorrect directory: " + environmentVariable); + } + } + +} diff --git a/src/main/java/de/samply/utils/FileExtension.java b/src/main/java/de/samply/utils/FileExtension.java new file mode 100644 index 00000000..3bea57d3 --- /dev/null +++ b/src/main/java/de/samply/utils/FileExtension.java @@ -0,0 +1,33 @@ +package de.samply.utils; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public enum FileExtension { + PDF("pdf"), + HTML("html"), + JSON("json"), + XML("xml"); + + private final String value; + + FileExtension(String value) { + this.value = value; + } + + @JsonValue + public String value() { + return value; + } + + @JsonCreator + public static FileExtension fromValue(String value) { + for (FileExtension ext : values()) { + if (ext.value.equalsIgnoreCase(value)) { + return ext; + } + } + throw new IllegalArgumentException("Unknown file extension: " + value); + } + +} diff --git a/src/main/java/de/samply/utils/FormFieldUtils.java b/src/main/java/de/samply/utils/FormFieldUtils.java new file mode 100644 index 00000000..8409b03e --- /dev/null +++ b/src/main/java/de/samply/utils/FormFieldUtils.java @@ -0,0 +1,35 @@ +package de.samply.utils; + +import de.samply.frontend.dto.FormField; +import jakarta.validation.constraints.NotNull; + +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.function.Function; +import java.util.stream.Collector; +import java.util.stream.Collectors; + +public class FormFieldUtils { + + // Comparator to sort FormFields by order -> title -> label + public static final Comparator FORM_FIELD_COMPARATOR = + Comparator.comparing(FormField::title, Comparator.nullsLast(String::compareTo)) + .thenComparing(FormField::order, Comparator.nullsLast(Integer::compareTo)) + .thenComparing(FormField::label, Comparator.nullsLast(String::compareTo)); + + public static Collector> + formFieldMapCollector() { + + return Collectors.toMap( + FormFieldUtils::fetchFormFieldKey, + Function.identity(), + (_, newValue) -> newValue, // the last one wins + LinkedHashMap::new + ); + } + + public static String fetchFormFieldKey(@NotNull FormField formField) { + return formField.title() + formField.label(); + } + +} diff --git a/src/main/java/de/samply/utils/KeyTransformer.java b/src/main/java/de/samply/utils/KeyTransformer.java index 9b0dfcdd..512836ae 100644 --- a/src/main/java/de/samply/utils/KeyTransformer.java +++ b/src/main/java/de/samply/utils/KeyTransformer.java @@ -20,7 +20,7 @@ private static String transformKey(String key) { for (char c : key.toCharArray()) { if (c == '-') { - toUpperCase = true; // next character should be uppercase + toUpperCase = true; // the next character should be uppercase } else { if (toUpperCase) { transformedKey.append(Character.toUpperCase(c)); diff --git a/src/main/java/de/samply/utils/LanguageUtils.java b/src/main/java/de/samply/utils/LanguageUtils.java new file mode 100644 index 00000000..4511b5a6 --- /dev/null +++ b/src/main/java/de/samply/utils/LanguageUtils.java @@ -0,0 +1,9 @@ +package de.samply.utils; + +public class LanguageUtils { + + public static String normalize(String languageCode) { + return languageCode == null ? null : languageCode.toLowerCase(); + } + +} diff --git a/src/main/java/de/samply/utils/MessageStatus.java b/src/main/java/de/samply/utils/MessageStatus.java index fb23e7f2..c8e19da5 100644 --- a/src/main/java/de/samply/utils/MessageStatus.java +++ b/src/main/java/de/samply/utils/MessageStatus.java @@ -10,15 +10,14 @@ public record MessageStatus(String message, HttpStatus status) { public static MessageStatus newInstance(@NotNull Throwable throwable, @NotNull String defaultErrorMessage) { String message = defaultErrorMessage; HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; - if (throwable instanceof WebClientResponseException) { - WebClientResponseException responseException = (WebClientResponseException) throwable; + if (throwable instanceof WebClientResponseException responseException) { String responseBody = responseException.getResponseBodyAsString(); - if (responseBody != null && StringUtils.hasText(responseBody)) { + if (StringUtils.hasText(responseBody)) { message += " (" + responseBody + ")"; } try { status = (HttpStatus) responseException.getStatusCode(); - } catch (IllegalArgumentException e) { + } catch (IllegalArgumentException _) { } } return new MessageStatus(message, status); diff --git a/src/main/java/de/samply/utils/ParamMetaUtils.java b/src/main/java/de/samply/utils/ParamMetaUtils.java new file mode 100644 index 00000000..87911bf3 --- /dev/null +++ b/src/main/java/de/samply/utils/ParamMetaUtils.java @@ -0,0 +1,24 @@ +package de.samply.utils; + +import de.samply.annotations.RequestParameter; +import de.samply.annotations.RequestVariable; +import de.samply.resolvers.ParamMeta; +import org.springframework.core.MethodParameter; + +public class ParamMetaUtils { + + public static ParamMeta extractParamMeta(MethodParameter parameter) { + RequestVariable rv = parameter.getParameterAnnotation(RequestVariable.class); + if (rv != null) { + return new ParamMeta(parameter, rv.name(), rv.required(), rv.notEmpty(), rv.defaultValue(), true); + } + + RequestParameter rp = parameter.getParameterAnnotation(RequestParameter.class); + if (rp != null) { + return new ParamMeta(parameter, rp.name(), rp.required(), rp.notEmpty(), rp.defaultValue(), false); + } + + return null; // should not happen if supportsParameter() is used + } + +} diff --git a/src/main/java/de/samply/utils/ProjectRolesUtils.java b/src/main/java/de/samply/utils/ProjectRolesUtils.java index 57819369..3407c6be 100644 --- a/src/main/java/de/samply/utils/ProjectRolesUtils.java +++ b/src/main/java/de/samply/utils/ProjectRolesUtils.java @@ -7,20 +7,37 @@ public class ProjectRolesUtils { - private static List bridgeheadProjectRolesOrderedInTimeDescendent = List.of(ProjectRole.FINAL, ProjectRole.PILOT, ProjectRole.DEVELOPER, ProjectRole.CREATOR, ProjectRole.BRIDGEHEAD_ADMIN, ProjectRole.PROJECT_MANAGER_ADMIN); + // O(1) lookup map for role order (descending in time) + private static final Map bridgeheadProjectRoleOrder = + Map.of( + ProjectRole.FINAL, 0, + ProjectRole.PILOT, 1, + ProjectRole.DEVELOPER, 2, + ProjectRole.CREATOR, 3, + ProjectRole.BRIDGEHEAD_ADMIN, 4, + ProjectRole.PROJECT_MANAGER_ADMIN, 5 + ); + + // Compare two ProjectRoles by their defined order + public static int compare(ProjectRole r1, ProjectRole r2) { + return Integer.compare( + bridgeheadProjectRoleOrder.getOrDefault(r1, Integer.MAX_VALUE), + bridgeheadProjectRoleOrder.getOrDefault(r2, Integer.MAX_VALUE) + ); + } + // Order a collection of ProjectRoles public static List orderCollectionInDescendentTime(Collection projectRoleSet) { return orderCollectionInDescendentTime(projectRoleSet, projectRole -> projectRole); } - public static List orderCollectionInDescendentTime(Collection tCollection, Function projectRoleFunction) { + // Generic ordering utility based on a mapping function + public static List orderCollectionInDescendentTime(Collection tCollection, + Function projectRoleFunction) { List result = new ArrayList<>(tCollection); - Collections.sort(result, Comparator.comparingInt(t -> bridgeheadProjectRolesOrderedInTimeDescendent.indexOf(projectRoleFunction.apply(t)))); + result.sort(Comparator.comparingInt(t -> bridgeheadProjectRoleOrder + .getOrDefault(projectRoleFunction.apply(t), Integer.MAX_VALUE))); return result; } - public static int compare(ProjectRole projectRole1, ProjectRole projectRole2) { - return Comparator.comparingInt(projectRole -> bridgeheadProjectRolesOrderedInTimeDescendent.indexOf(projectRole)).compare(projectRole1, projectRole2); - } - } diff --git a/src/main/java/de/samply/utils/ProjectUtils.java b/src/main/java/de/samply/utils/ProjectUtils.java new file mode 100644 index 00000000..ba2a0fa8 --- /dev/null +++ b/src/main/java/de/samply/utils/ProjectUtils.java @@ -0,0 +1,37 @@ +package de.samply.utils; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +public class ProjectUtils { + + /** + * Formats a collection of items as a comma-separated string with "and" before the last element. + * Examples: + * [T1] -> "T1" + * [T1, T2] -> "T1 and T2" + * [T1, T2, T3] -> "T1, T2 and T3" + * + * @param items collection of items (enums or strings) + * @return formatted string + */ + public static String formatWithCommasAndAnd(Collection items) { + if (items == null || items.isEmpty()) { + return ""; + } + + List list = items.stream() + .map(Object::toString) + .collect(Collectors.toList()); + + int size = list.size(); + if (size == 1) { + return list.getFirst(); + } + + return String.join(", ", list.subList(0, size - 1)) + + " and " + list.get(size - 1); + } + +} diff --git a/src/main/java/de/samply/utils/ProjectVersion.java b/src/main/java/de/samply/utils/ProjectVersion.java index 49e39144..6b4e5e87 100644 --- a/src/main/java/de/samply/utils/ProjectVersion.java +++ b/src/main/java/de/samply/utils/ProjectVersion.java @@ -21,7 +21,7 @@ public static String getProjectVersion() { try { return getProjectVersion_WithoutManagementException(); } catch (IOException | XmlPullParserException e) { - log.error("Error getting project version: " + ExceptionUtils.getStackTrace(e)); + log.error("Error getting project version: {}", ExceptionUtils.getStackTrace(e)); return ProjectManagerConst.APP_NAME; } } diff --git a/src/main/java/de/samply/utils/UserUtils.java b/src/main/java/de/samply/utils/UserUtils.java new file mode 100644 index 00000000..32979507 --- /dev/null +++ b/src/main/java/de/samply/utils/UserUtils.java @@ -0,0 +1,19 @@ +package de.samply.utils; + +import de.samply.db.model.User; + +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class UserUtils { + + public static String extractFullName(Optional optionalUser){ + return optionalUser.map(user -> Stream.of(user.getFirstName(), user.getLastName()) + .filter(name -> name != null && !name.isBlank()) + .collect(Collectors.joining(" "))) + .filter(s -> !s.isEmpty()) + .orElse(null); + } + +} diff --git a/src/main/java/de/samply/utils/WebClientFactory.java b/src/main/java/de/samply/utils/WebClientFactory.java index db36ae54..abc32f10 100644 --- a/src/main/java/de/samply/utils/WebClientFactory.java +++ b/src/main/java/de/samply/utils/WebClientFactory.java @@ -5,7 +5,9 @@ import de.samply.proxy.HttpsProxyConfiguration; import de.samply.proxy.ProxyConfiguration; import io.netty.channel.ChannelOption; +import io.netty.channel.epoll.Epoll; import io.netty.channel.epoll.EpollChannelOption; +import lombok.Getter; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.stereotype.Component; @@ -19,13 +21,15 @@ @Component public class WebClientFactory { + @Getter private final int webClientMaxNumberOfRetries; + @Getter private final int webClientTimeInSecondsAfterRetryWithFailure; private final int webClientRequestTimeoutInSeconds; private final int webClientConnectionTimeoutInSeconds; private final int webClientTcpKeepIdleInSeconds; private final int webClientTcpKeepIntervalInSeconds; - private final int webClientTcpKeepConnetionNumberOfTries; + private final int webClientTcpKeepConnectionNumberOfTries; private final int webClientBufferSizeInBytes; private Optional httpProxyConfiguration = Optional.empty(); private Optional httpsProxyConfiguration = Optional.empty(); @@ -35,7 +39,7 @@ public WebClientFactory( @Value(ProjectManagerConst.WEBCLIENT_CONNECTION_TIMEOUT_IN_SECONDS_SV) Integer webClientConnectionTimeoutInSeconds, @Value(ProjectManagerConst.WEBCLIENT_TCP_KEEP_IDLE_IN_SECONDS_SV) Integer webClientTcpKeepIdleInSeconds, @Value(ProjectManagerConst.WEBCLIENT_TCP_KEEP_INTERVAL_IN_SECONDS_SV) Integer webClientTcpKeepIntervalInSeconds, - @Value(ProjectManagerConst.WEBCLIENT_TCP_KEEP_CONNECTION_NUMBER_OF_TRIES_SV) Integer webClientTcpKeepConnetionNumberOfTries, + @Value(ProjectManagerConst.WEBCLIENT_TCP_KEEP_CONNECTION_NUMBER_OF_TRIES_SV) Integer webClientTcpKeepConnectionNumberOfTries, @Value(ProjectManagerConst.WEBCLIENT_MAX_NUMBER_OF_RETRIES_SV) Integer webClientMaxNumberOfRetries, @Value(ProjectManagerConst.WEBCLIENT_TIME_IN_SECONDS_AFTER_RETRY_WITH_FAILURE_SV) Integer webClientTimeInSecondsAfterRetryWithFailure, @Value(ProjectManagerConst.WEBCLIENT_BUFFER_SIZE_IN_BYTES_SV) Integer webClientBufferSizeInBytes, @@ -48,7 +52,7 @@ public WebClientFactory( this.webClientConnectionTimeoutInSeconds = webClientConnectionTimeoutInSeconds; this.webClientTcpKeepIdleInSeconds = webClientTcpKeepIdleInSeconds; this.webClientTcpKeepIntervalInSeconds = webClientTcpKeepIntervalInSeconds; - this.webClientTcpKeepConnetionNumberOfTries = webClientTcpKeepConnetionNumberOfTries; + this.webClientTcpKeepConnectionNumberOfTries = webClientTcpKeepConnectionNumberOfTries; this.webClientBufferSizeInBytes = webClientBufferSizeInBytes; setHttpProxies(httpProxyConfiguration, httpsProxyConfiguration); @@ -64,40 +68,39 @@ private void setHttpProxies(HttpProxyConfiguration httpProxyConfiguration, Https } public WebClient createWebClient(String baseUrl) { + HttpClient httpClient = HttpClient.create() + .responseTimeout(Duration.ofSeconds(webClientRequestTimeoutInSeconds)) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, webClientConnectionTimeoutInSeconds * 1000) + .option(ChannelOption.SO_KEEPALIVE, true); + + if (Epoll.isAvailable()) { + httpClient = httpClient + .option(EpollChannelOption.TCP_KEEPIDLE, webClientTcpKeepIdleInSeconds) + .option(EpollChannelOption.TCP_KEEPINTVL, webClientTcpKeepIntervalInSeconds) + .option(EpollChannelOption.TCP_KEEPCNT, webClientTcpKeepConnectionNumberOfTries); + } + WebClient.Builder webClientBuilder = WebClient.builder() .codecs(codecs -> codecs.defaultCodecs().maxInMemorySize(webClientBufferSizeInBytes)) - .clientConnector(new ReactorClientHttpConnector( - HttpClient.create() - .responseTimeout(Duration.ofSeconds(webClientRequestTimeoutInSeconds)) - .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, webClientConnectionTimeoutInSeconds * 1000) - .option(ChannelOption.SO_KEEPALIVE, true) - .option(EpollChannelOption.TCP_KEEPIDLE, webClientTcpKeepIdleInSeconds) - .option(EpollChannelOption.TCP_KEEPINTVL, webClientTcpKeepIntervalInSeconds) - .option(EpollChannelOption.TCP_KEEPCNT, webClientTcpKeepConnetionNumberOfTries) - )) + .clientConnector(new ReactorClientHttpConnector(httpClient)) .baseUrl(baseUrl); + httpsProxyConfiguration.ifPresent(proxyConfig -> addProxy(webClientBuilder, proxyConfig)); httpProxyConfiguration.ifPresent(proxyConfig -> addProxy(webClientBuilder, proxyConfig)); + return webClientBuilder.build(); } + private void addProxy(WebClient.Builder webClientBuilder, ProxyConfiguration proxyConfiguration) { webClientBuilder.clientConnector(new ReactorClientHttpConnector(HttpClient.create() .proxy(proxy -> proxy.type(ProxyProvider.Proxy.HTTP) .host(proxyConfiguration.getHost()) .port(proxyConfiguration.getPort()) .username(proxyConfiguration.getUsername()) - .password(password -> proxyConfiguration.getPassword()) // Use Function.identity() here if needed + .password(_ -> proxyConfiguration.getPassword()) // Use Function.identity() here if needed .nonProxyHosts(proxyConfiguration.getNoProxyPattern()) ))); } - public int getWebClientMaxNumberOfRetries() { - return webClientMaxNumberOfRetries; - } - - public int getWebClientTimeInSecondsAfterRetryWithFailure() { - return webClientTimeInSecondsAfterRetryWithFailure; - } - } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 24ebe8f1..03c7b2b3 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,14 +1,5 @@ server: port: ${APPLICATION_PORT:8097} -# ssl: -# key-store: ${KEYSTORE} -# key-store-password: ${KEYSTORE_PASSWORD} -# key-store-type: PKCS12 -# key-alias: tomcat -# trust-all: true -# trust-store-type: JKS -# trust-store: ${TRUST_STORE} -# trust-store-password: ${TRUST_STORE_PASSWORD} servlet: session: @@ -23,6 +14,9 @@ logging: spring: + profiles: + active: external-execution-not-allowed + flyway: locations: classpath:db/migration enabled: ${PROJECT_MANAGER_DB_FLYWAY_MIGRATION_ENABLED:true} @@ -36,28 +30,46 @@ spring: username: ${PROJECT_MANAGER_DB_USER:project_manager} password: ${PROJECT_MANAGER_DB_PASSWORD:project_manager} driver-class-name: org.postgresql.Driver + hikari: + minimum-idle: 5 + maximum-pool-size: 20 + idle-timeout: 30000 # Close idle connections after the 30s + connection-timeout: 30000 # Fail after the 30s if no connection is available + max-lifetime: 1800000 # Close connections after 30 min + keepalive-time: 30000 # Prevents premature connection closure by PostgreSQL + leak-detection-threshold: 2000 # Log warnings if a connection is not released within 2s + data-source-properties: + cachePrepStmts: true + prepStmtCacheSize: 250 + prepStmtCacheSqlLimit: 2048 jpa: hibernate: ddl-auto: none - naming_strategy: org.hibernate.cfg.ImprovedNamingStrategy + naming: + physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl + implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl database-platform: org.hibernate.dialect.PostgreSQLDialect properties: hibernate: format_sql: true + jdbc: + batch_size: 30 + order_inserts: true + order_updates: true database: postgresql show-sql: ${HIBERNATE_LOG:false} + open-in-view: false # Avoid memory leaks security: oauth2: + resourceserver: + jwt: + issuer-uri: ${OIDC_URL} client: provider: oidc: - issuer-uri: ${OIDC_URL}/realms/${OIDC_REALM} - authorization-uri: ${spring.security.oauth2.client.provider.oidc.issuer-uri}/protocol/openid-connect/auth - token-uri: ${spring.security.oauth2.client.provider.oidc.issuer-uri}/protocol/openid-connect/token - user-info-uri: ${spring.security.oauth2.client.provider.oidc.issuer-uri}/protocol/openid-connect/userinfo - jwk-set-uri: ${spring.security.oauth2.client.provider.oidc.issuer-uri}/protocol/openid-connect/certs + issuer-uri: ${OIDC_URL} user-name-attribute: email registration: oidc: @@ -75,36 +87,3 @@ spring: max-file-size: ${MAX_FILE_SIZE:10MB} max-request-size: ${MAX_REQEST_SIZE:10MB} -# mail: -# host: ${SMTP_HOST} -# port: ${SMTP_PORT} -# username: ${SMTP_USER} -# password: ${SMTP_PASSWORD} - # properties: - # mail: - # smtp: - # auth: true - # starttls: - # enable: true - # smtp: - # proxy: - # host: ${PROXY_HOST} - # port: ${PROXY_PORT} - - -#bridgeheads: -# config: -# frankfurt: -# explorer-code: frankfurt123 -# exporter-url: http://frankfurt-bridgehead.com/ccp-exporter -# exporter-api-key: asdfkjalsdkjasfldkj -# berlin: -# explorer-code: berlin456 -# exporter-url: http://berlin-bridgehead.com/ccp-exporter -# exporter-api-key: erewrtwwenbmtvwnbvetnbvter - -#frontend: -# base-url: "http://localhost:8099" -# sites: -# project-dashboard: "/project-dashboard" -# project-view: "/project-view" diff --git a/src/main/resources/db/migration/V001__initialize_schema_and_tables.sql b/src/main/resources/db/migration/V001__initialize_schema_and_tables.sql index ff09a612..e57eacff 100644 --- a/src/main/resources/db/migration/V001__initialize_schema_and_tables.sql +++ b/src/main/resources/db/migration/V001__initialize_schema_and_tables.sql @@ -1,56 +1,81 @@ CREATE SCHEMA IF NOT EXISTS samply; SET -search_path TO samply; + search_path TO samply; + +CREATE TABLE samply.shedlock +( + name VARCHAR(64) NOT NULL, + lock_until TIMESTAMP NOT NULL, + locked_at TIMESTAMP NOT NULL, + locked_by VARCHAR(255) NOT NULL, + PRIMARY KEY (name) +); CREATE TABLE samply.query ( - id SERIAL PRIMARY KEY, - code TEXT NOT NULL, - query TEXT NOT NULL, - query_format TEXT NOT NULL, - created_at TIMESTAMP NOT NULL, - human_readable TEXT, - explorer_url TEXT, - output_format TEXT, - template_id TEXT, - label TEXT, - description TEXT, - context TEXT + id SERIAL PRIMARY KEY, + code TEXT NOT NULL, + query TEXT NOT NULL, + query_format TEXT NOT NULL, + created_at TIMESTAMP NOT NULL, + human_readable TEXT, + explorer_url TEXT, + label TEXT, + description TEXT, + details TEXT, + context TEXT, + cohort_definition TEXT +); + +CREATE TABLE samply.query_output +( + id SERIAL PRIMARY KEY, + query_id BIGINT NOT NULL, + project_type TEXT NOT NULL, + template_id TEXT, + output_format TEXT ); CREATE TABLE samply.project +( + id SERIAL NOT NULL PRIMARY KEY, + state_machine_key TEXT NOT NULL, + code TEXT NOT NULL, + state TEXT NOT NULL, + creator_email TEXT NOT NULL, + created_at TIMESTAMP NOT NULL, + expires_at DATE, + archived_at TIMESTAMP, + modified_at TIMESTAMP NOT NULL, + query_id BIGINT, + is_custom_config_selected BOOLEAN, + results_url TEXT, + creator_results_state TEXT NOT NULL +); + +CREATE TABLE samply.project_bridgehead ( id SERIAL NOT NULL PRIMARY KEY, - state_machine_key TEXT NOT NULL, - code TEXT NOT NULL, - state TEXT NOT NULL, - creator_email TEXT NOT NULL, - created_at TIMESTAMP NOT NULL, - expires_at DATE, - archived_at TIMESTAMP, + project_id BIGINT NOT NULL, + bridgehead TEXT NOT NULL, modified_at TIMESTAMP NOT NULL, - query_id BIGINT, - type TEXT, - is_custom_config BOOLEAN, + state TEXT NOT NULL, results_url TEXT, creator_results_state TEXT NOT NULL ); -CREATE TABLE samply.project_bridgehead +CREATE TABLE samply.project_bridgehead_execution ( - id SERIAL NOT NULL PRIMARY KEY, - project_id BIGINT NOT NULL, - bridgehead TEXT NOT NULL, - modified_at TIMESTAMP NOT NULL, - state TEXT NOT NULL, + id SERIAL PRIMARY KEY, + project_bridgehead_id BIGINT NOT NULL, + query_output_id BIGINT NOT NULL, query_state TEXT NOT NULL, exporter_response TEXT, exporter_user TEXT, exporter_execution_id TEXT, exporter_dispatch_counter INT NOT NULL, - results_url TEXT, - creator_results_state TEXT NOT NULL + modified_at TIMESTAMP NOT NULL ); CREATE TABLE samply.project_bridgehead_user @@ -70,6 +95,14 @@ CREATE TABLE samply.bridgehead_admin_user bridgehead TEXT NOT NULL ); +CREATE TABLE samply.creator_user +( + id SERIAL NOT NULL PRIMARY KEY, + email TEXT NOT NULL, + bridgehead TEXT NOT NULL +); + + CREATE TABLE samply.project_manager_admin_user ( id SERIAL NOT NULL PRIMARY KEY, @@ -136,11 +169,42 @@ CREATE TABLE samply.user ( id SERIAL PRIMARY KEY, email TEXT NOT NULL, - first_name TEXT NOT NULL, - last_name TEXT NOT NULL, + first_name TEXT, + last_name TEXT, mailing_black_list BOOLEAN NOT NULL ); +CREATE TABLE samply.project_form_field +( + id SERIAL PRIMARY KEY, + project_id BIGINT NOT NULL, + label TEXT NOT NULL, + form_title TEXT NOT NULL, + value TEXT, + created_at TIMESTAMP NOT NULL, + modified_at TIMESTAMP +); + +CREATE TABLE samply.project_form +( + id SERIAL PRIMARY KEY, + project_id BIGINT NOT NULL, + form_title TEXT NOT NULL, + created_at TIMESTAMP NOT NULL, + modified_at TIMESTAMP +); + +ALTER TABLE samply.query_output + ADD CONSTRAINT fk_query_output_query + FOREIGN KEY (query_id) + REFERENCES samply.query (id) + ON DELETE CASCADE; + +ALTER TABLE samply.query_output + ADD CONSTRAINT uq_query_project_type + UNIQUE (query_id, project_type); + + ALTER TABLE samply.project ADD CONSTRAINT fk_project_query FOREIGN KEY (query_id) @@ -180,6 +244,31 @@ ALTER TABLE samply.project_coder ALTER TABLE samply.user ADD CONSTRAINT unique_email UNIQUE (email); +ALTER TABLE samply.project_form_field + ADD CONSTRAINT fk_project FOREIGN KEY (project_id) + REFERENCES samply.project (id) ON DELETE CASCADE; + +ALTER TABLE samply.project_form + ADD CONSTRAINT fk_project FOREIGN KEY (project_id) + REFERENCES samply.project (id) ON DELETE CASCADE; + +ALTER TABLE samply.project_bridgehead_execution + ADD CONSTRAINT fk_execution_bridgehead + FOREIGN KEY (project_bridgehead_id) + REFERENCES samply.project_bridgehead (id) + ON DELETE CASCADE; + +ALTER TABLE samply.project_bridgehead_execution + ADD CONSTRAINT fk_execution_query_output + FOREIGN KEY (query_output_id) + REFERENCES samply.query_output (id) + ON DELETE CASCADE; + +ALTER TABLE samply.project_bridgehead_execution + ADD CONSTRAINT uq_execution_unique + UNIQUE (project_bridgehead_id, query_output_id); + +CREATE INDEX idx_query_output_query_id ON samply.query_output (query_id); CREATE INDEX idx_project_bridgehead_project_id ON samply.project_bridgehead (project_id); CREATE INDEX idx_project_bridgehead_user_project_bridgehead_id ON samply.project_bridgehead_user (project_bridgehead_id); CREATE INDEX idx_project_document_project_id ON samply.project_document (project_id); @@ -188,3 +277,7 @@ CREATE INDEX idx_notification_project_id ON samply.notification (project_id); CREATE INDEX idx_notification_user_action_notification_id ON samply.notification_user_action (notification_id); CREATE INDEX idx_project_bridgehead_datashield_project_bridgehead_id ON samply.project_bridgehead_datashield (project_bridgehead_id); CREATE INDEX idx_project_coder_project_bridgehead_user_id ON samply.project_coder (project_bridgehead_user_id); +CREATE INDEX idx_project_form_field_project_id ON samply.project_form_field (project_id); +CREATE INDEX idx_project_form_project_id ON samply.project_form (project_id); +CREATE INDEX idx_execution_bridgehead ON samply.project_bridgehead_execution (project_bridgehead_id); +CREATE INDEX idx_execution_query_output ON samply.project_bridgehead_execution (query_output_id); \ No newline at end of file diff --git a/src/main/resources/templates/bridgehead-admin-new-project-accepted.html b/src/main/resources/templates/bridgehead-admin-new-project-accepted.html index ae2547fe..00234d1b 100644 --- a/src/main/resources/templates/bridgehead-admin-new-project-accepted.html +++ b/src/main/resources/templates/bridgehead-admin-new-project-accepted.html @@ -6,6 +6,6 @@

A new project for your bridgehead has been accepted

-This is the burocracy... +This is the bureaucracy... diff --git a/src/main/resources/templates/creator-new-project-accepted.html b/src/main/resources/templates/creator-new-project-accepted.html index 384ae335..05def19e 100644 --- a/src/main/resources/templates/creator-new-project-accepted.html +++ b/src/main/resources/templates/creator-new-project-accepted.html @@ -5,7 +5,7 @@ New Project Accepted -

You project has been accepted

-This is the burocracy... +

Your project has been accepted

+This is the bureaucracy... diff --git a/src/test/java/de/samply/app/ProjectManagerControllerTest.java b/src/test/java/de/samply/app/ProjectManagerControllerTest.java index d642f5da..c3aae56f 100644 --- a/src/test/java/de/samply/app/ProjectManagerControllerTest.java +++ b/src/test/java/de/samply/app/ProjectManagerControllerTest.java @@ -4,7 +4,6 @@ import de.samply.annotations.FrontendSiteModule; import de.samply.annotations.RoleConstraints; import de.samply.annotations.StateConstraints; -import de.samply.project.ProjectType; import de.samply.project.event.ProjectEventActionsException; import de.samply.project.event.ProjectEventService; import de.samply.project.state.ProjectBridgeheadState; @@ -21,15 +20,17 @@ class ProjectManagerControllerTest { private final ProjectEventService projectEventService; - public ProjectManagerControllerTest(ProjectEventService projectEventService) { + @SuppressWarnings("unused") + ProjectManagerControllerTest(ProjectEventService projectEventService) { this.projectEventService = projectEventService; } @GetMapping(value = "/testCreateProject") @RoleConstraints(organisationRoles = {OrganisationRole.RESEARCHER}) public ResponseEntity testCreateProject() throws ProjectEventActionsException { + @SuppressWarnings("SpellCheckingInspection") String[] bridgeheads = {"david-j-develop", "frankfurt", "berlin"}; - this.projectEventService.draft(bridgeheads, "myQuery", ProjectType.EXPORT); + this.projectEventService.draft(bridgeheads, "myQuery"); return new ResponseEntity<>("Test 6", HttpStatus.OK); } @@ -99,7 +100,7 @@ public ResponseEntity test7() { } @GetMapping(value = "/test8") - @StateConstraints(projectStates = {ProjectState.CREATED}) + @StateConstraints(projectStates = {ProjectState.REVIEW}) @FrontendSiteModule(site = "Site_2", module = "Module_2") @FrontendAction(action = "test_8") public ResponseEntity test8() { diff --git a/src/test/java/de/samply/projectmanager/ProjectManagerApplicationTests.java b/src/test/java/de/samply/projectmanager/ProjectCodeManagerApplicationTests.java similarity index 75% rename from src/test/java/de/samply/projectmanager/ProjectManagerApplicationTests.java rename to src/test/java/de/samply/projectmanager/ProjectCodeManagerApplicationTests.java index 1d1920c6..6fd04cbf 100644 --- a/src/test/java/de/samply/projectmanager/ProjectManagerApplicationTests.java +++ b/src/test/java/de/samply/projectmanager/ProjectCodeManagerApplicationTests.java @@ -6,8 +6,9 @@ @Disabled @SpringBootTest -class ProjectManagerApplicationTests { +class ProjectCodeManagerApplicationTests { + @SuppressWarnings("EmptyMethod") @Test void contextLoads() { } diff --git a/test.postman_collection.json b/test.postman_collection.json new file mode 100644 index 00000000..e719532c --- /dev/null +++ b/test.postman_collection.json @@ -0,0 +1,3595 @@ +{ + "info": { + "_postman_id": "19b131eb-4c5b-452f-aeb3-9edc8cbe0e2c", + "name": "Project Manager", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "24652658" + }, + "item": [ + { + "name": "Token Public with user researcher", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "grant_type", + "value": "password", + "type": "text" + }, + { + "key": "client_id", + "value": "bridgehead-test", + "type": "text" + }, + { + "key": "username", + "value": "researcher", + "type": "text" + }, + { + "key": "password", + "value": "researcher", + "type": "text" + } + ] + }, + "url": { + "raw": "https://login.verbis.dkfz.de/realms/test-realm-01/protocol/openid-connect/token", + "protocol": "https", + "host": [ + "login", + "verbis", + "dkfz", + "de" + ], + "path": [ + "realms", + "test-realm-01", + "protocol", + "openid-connect", + "token" + ] + } + }, + "response": [] + }, + { + "name": "Info", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/info", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "info" + ] + } + }, + "response": [] + }, + { + "name": "Actions Project Dashboard", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/actions?site=project-dashboard", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "actions" + ], + "query": [ + { + "key": "site", + "value": "project-dashboard" + } + ] + } + }, + "response": [] + }, + { + "name": "Actions Navigation", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/actions?site=nav-bar", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "actions" + ], + "query": [ + { + "key": "site", + "value": "nav-bar" + } + ] + } + }, + "response": [] + }, + { + "name": "Actions Project View", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/actions?site=project-view&project-code=634859e4dc3947ce92ea&bridgehead=jurassic-park", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "actions" + ], + "query": [ + { + "key": "site", + "value": "project-view" + }, + { + "key": "project-code", + "value": "634859e4dc3947ce92ea" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + } + ] + } + }, + "response": [] + }, + { + "name": "All Actions (Project View)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/all-actions?site=project-view&project-code=413148c5791a430e9dc7", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "all-actions" + ], + "query": [ + { + "key": "site", + "value": "project-view" + }, + { + "key": "project-code", + "value": "413148c5791a430e9dc7" + } + ] + } + }, + "response": [] + }, + { + "name": "All Actions (Project Dashboard)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/all-actions?site=project-dashboard", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "all-actions" + ], + "query": [ + { + "key": "site", + "value": "project-dashboard" + } + ] + } + }, + "response": [] + }, + { + "name": "Create CQL Query (Researcher)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{ \"query\": \"eyJsYW5nIjoiY3FsIiwibGliIjp7InJlc291cmNlVHlwZSI6IkxpYnJhcnkiLCJ1cmwiOiJ1cm46dXVpZDpiOTRkMTAwYy0zN2NkLTRiZDAtODgyNy01MzJiODRiZWE2NDgiLCJzdGF0dXMiOiJhY3RpdmUiLCJ0eXBlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9saWJyYXJ5LXR5cGUiLCJjb2RlIjoibG9naWMtbGlicmFyeSJ9XX0sImNvbnRlbnQiOlt7ImNvbnRlbnRUeXBlIjoidGV4dC9jcWwiLCJkYXRhIjoiYkdsaWNtRnllU0JTWlhSeWFXVjJaUXAxYzJsdVp5QkdTRWxTSUhabGNuTnBiMjRnSnpRdU1DNHdKd3BwYm1Oc2RXUmxJRVpJU1ZKSVpXeHdaWEp6SUhabGNuTnBiMjRnSnpRdU1DNHdKd29LWTI5a1pYTjVjM1JsYlNCc2IybHVZem9nSjJoMGRIQTZMeTlzYjJsdVl5NXZjbWNuQ2dwamIyNTBaWGgwSUZCaGRHbGxiblFLQ2tSTFZFdGZVMVJTUVZSZlIwVk9SRVZTWDFOVVVrRlVTVVpKUlZJS0NrUkxWRXRmVTFSU1FWUmZRVWRGWDFOVVVrRlVTVVpKUlZJS0NrUkxWRXRmVTFSU1FWUmZSRVZEUlVGVFJVUmZVMVJTUVZSSlJrbEZVZ29LUkV0VVMxOVRWRkpCVkY5RVNVRkhUazlUU1ZOZlUxUlNRVlJKUmtsRlVnb0tSRXRVUzE5VFZGSkJWRjlUVUVWRFNVMUZUbDlUVkZKQlZFbEdTVVZTQ2dwRVMxUkxYMU5VVWtGVVgxQlNUME5GUkZWU1JWOVRWRkpCVkVsR1NVVlNDZ3BFUzFSTFgxTlVVa0ZVWDAxRlJFbERRVlJKVDA1ZlUxUlNRVlJKUmtsRlVncEVTMVJMWDFOVVVrRlVYMFJGUmw5SlRsOUpUa2xVU1VGTVgxQlBVRlZNUVZSSlQwNVFZWFJwWlc1MExtZGxibVJsY2lBOUlDZHRZV3hsSnc9PSJ9XX0sIm1lYXN1cmUiOnsicmVzb3VyY2VUeXBlIjoiTWVhc3VyZSIsInVybCI6InVybjp1dWlkOmFjMDQwNmMxLTBlYjEtNDU4YS05ZmE4LTQ3MTZiZjhmMTBhOCIsInN0YXR1cyI6ImFjdGl2ZSIsInN1YmplY3RDb2RlYWJsZUNvbmNlcHQiOnsiY29kaW5nIjpbeyJzeXN0ZW0iOiJodHRwOi8vaGw3Lm9yZy9maGlyL3Jlc291cmNlLXR5cGVzIiwiY29kZSI6IlBhdGllbnQifV19LCJsaWJyYXJ5IjoidXJuOnV1aWQ6Yjk0ZDEwMGMtMzdjZC00YmQwLTg4MjctNTMyYjg0YmVhNjQ4Iiwic2NvcmluZyI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1zY29yaW5nIiwiY29kZSI6ImNvaG9ydCJ9XX0sImdyb3VwIjpbeyJjb2RlIjp7InRleHQiOiJwYXRpZW50cyJ9LCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6IkluSW5pdGlhbFBvcHVsYXRpb24ifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJHZW5kZXIifSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbCIsImV4cHJlc3Npb24iOiJHZW5kZXIifX0seyJjb2RlIjp7InRleHQiOiI3NTE4Ni03In0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiRGVjZWFzZWQifX0seyJjb2RlIjp7InRleHQiOiJBZ2UifSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbCIsImV4cHJlc3Npb24iOiJBZ2VDbGFzcyJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoiZGlhZ25vc2lzIn0sImV4dGVuc2lvbiI6W3sidXJsIjoiaHR0cDovL2hsNy5vcmcvZmhpci91cy9jcWZtZWFzdXJlcy9TdHJ1Y3R1cmVEZWZpbml0aW9uL2NxZm0tcG9wdWxhdGlvbkJhc2lzIiwidmFsdWVDb2RlIjoiQ29uZGl0aW9uIn1dLCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6IkRpYWdub3NpcyJ9fV0sInN0cmF0aWZpZXIiOlt7ImNvZGUiOnsidGV4dCI6ImRpYWdub3NpcyJ9LCJjcml0ZXJpYSI6eyJsYW5ndWFnZSI6InRleHQvY3FsLWlkZW50aWZpZXIiLCJleHByZXNzaW9uIjoiRGlhZ25vc2lzQ29kZSJ9fV19LHsiY29kZSI6eyJ0ZXh0Ijoic3BlY2ltZW4ifSwiZXh0ZW5zaW9uIjpbeyJ1cmwiOiJodHRwOi8vaGw3Lm9yZy9maGlyL3VzL2NxZm1lYXN1cmVzL1N0cnVjdHVyZURlZmluaXRpb24vY3FmbS1wb3B1bGF0aW9uQmFzaXMiLCJ2YWx1ZUNvZGUiOiJTcGVjaW1lbiJ9XSwicG9wdWxhdGlvbiI6W3siY29kZSI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1wb3B1bGF0aW9uIiwiY29kZSI6ImluaXRpYWwtcG9wdWxhdGlvbiJ9XX0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwtaWRlbnRpZmllciIsImV4cHJlc3Npb24iOiJTcGVjaW1lbiJ9fV0sInN0cmF0aWZpZXIiOlt7ImNvZGUiOnsidGV4dCI6InNhbXBsZV9raW5kIn0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiU2FtcGxlVHlwZSJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoicHJvY2VkdXJlcyJ9LCJleHRlbnNpb24iOlt7InVybCI6Imh0dHA6Ly9obDcub3JnL2ZoaXIvdXMvY3FmbWVhc3VyZXMvU3RydWN0dXJlRGVmaW5pdGlvbi9jcWZtLXBvcHVsYXRpb25CYXNpcyIsInZhbHVlQ29kZSI6IlByb2NlZHVyZSJ9XSwicG9wdWxhdGlvbiI6W3siY29kZSI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1wb3B1bGF0aW9uIiwiY29kZSI6ImluaXRpYWwtcG9wdWxhdGlvbiJ9XX0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwtaWRlbnRpZmllciIsImV4cHJlc3Npb24iOiJQcm9jZWR1cmUifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJQcm9jZWR1cmVUeXBlIn0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiUHJvY2VkdXJlVHlwZSJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoibWVkaWNhdGlvblN0YXRlbWVudHMifSwiZXh0ZW5zaW9uIjpbeyJ1cmwiOiJodHRwOi8vaGw3Lm9yZy9maGlyL3VzL2NxZm1lYXN1cmVzL1N0cnVjdHVyZURlZmluaXRpb24vY3FmbS1wb3B1bGF0aW9uQmFzaXMiLCJ2YWx1ZUNvZGUiOiJNZWRpY2F0aW9uU3RhdGVtZW50In1dLCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6Ik1lZGljYXRpb25TdGF0ZW1lbnQifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJNZWRpY2F0aW9uVHlwZSJ9LCJjcml0ZXJpYSI6eyJsYW5ndWFnZSI6InRleHQvY3FsIiwiZXhwcmVzc2lvbiI6IlByb2NlZHVyZVR5cGUifX1dfV19fQ==\" }", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8097/query?label=Test2&description=This is a test query for CCP&template-id=ccp&output-format=EXCEL&query-format=CQL_DATA&human-readable=(gender = male)", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "query" + ], + "query": [ + { + "key": "label", + "value": "Test2" + }, + { + "key": "description", + "value": "This is a test query for CCP" + }, + { + "key": "template-id", + "value": "ccp" + }, + { + "key": "output-format", + "value": "EXCEL" + }, + { + "key": "query-format", + "value": "CQL_DATA" + }, + { + "key": "human-readable", + "value": "(gender = male)" + } + ] + } + }, + "response": [] + }, + { + "name": "Create query and design project (Researcher) - Mit Bearer Authorization", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJnbUlXWjB3MmpjZ2h0UGphUnBRaXYxWVNlOGU0cVVMem15YVVlTmdUa1VFIn0.eyJleHAiOjE3MTA0NDU1MTIsImlhdCI6MTcxMDQ0NTIxMiwianRpIjoiMWU2Yzc3MTMtZGM3OC00ZjExLTkzMzItZmE5Y2EwYmVhYTNkIiwiaXNzIjoiaHR0cHM6Ly9sb2dpbi52ZXJiaXMuZGtmei5kZS9yZWFsbXMvdGVzdC1yZWFsbS0wMSIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiIyM2VmYTJiMS1iM2NhLTQ3ZWQtYTI2My03ZDUxNjA5OTA1YTgiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJicmlkZ2VoZWFkLXRlc3QiLCJzZXNzaW9uX3N0YXRlIjoiZmM0NjBjYTItZTUzMy00NmM1LWJhYWUtMDNhNTExMGI1ZjcxIiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwOi8vbG9jYWxob3N0Ojg0ODAiLCJodHRwOi8vbG9jYWxob3N0OjgwOTciLCJodHRwczovL2xvY2FsaG9zdDo4MDk3IiwiaHR0cHM6Ly9sb2NhbGhvc3Q6ODA5OSIsImh0dHA6Ly9sb2NhbGhvc3Q6ODc4NyIsImh0dHA6Ly9sb2NhbGhvc3Q6ODA5OSIsImh0dHA6Ly9sb2NhbGhvc3Q6OTAwMCJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy10ZXN0LXJlYWxtLTAxIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCBvcGVuaWQgYWRtaW4iLCJzaWQiOiJmYzQ2MGNhMi1lNTMzLTQ2YzUtYmFhZS0wM2E1MTEwYjVmNzEiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiZ3JvdXBzMiI6WyIvREtUS19DQ1BfSnVyYXNzaWMtcGFyayIsIi9EYXRhU0hJRUxEIl0sIm5hbWUiOiJBbGFuIEdyYW50IiwiZ3JvdXBzIjpbIkRLVEtfQ0NQX0p1cmFzc2ljLXBhcmsiLCJEYXRhU0hJRUxEIl0sInByZWZlcnJlZF91c2VybmFtZSI6InJlc2VhcmNoZXIiLCJnaXZlbl9uYW1lIjoiQWxhbiIsImZhbWlseV9uYW1lIjoiR3JhbnQiLCJlbWFpbCI6InJlc2VhcmNoZXJAdGVzdC5ka2Z6LmRlIn0.Ibrv8gw5Z_w-fcfOVOczYMWOL_nIp7zS4qGIrJa7TqFrUd9jRPIViffFH-x1Me0jP6FhOkgJt9pooMjEAItdcsrVzIcwhG-jI9un460A0piLSYl3ljz3CSv3n4yP2behQOBRMLf6Uv8Z-4KOxGrgDgUJTqWSUJwDp1JIF-o6W8D688pPKzIEHV7ETCZyY2WNEBU2BDNSMp9t_YMx4pOE2-OHTx4m3ncOVakMa6cwRwqdZDYnnQOB0dGLulF9Cg5yWD5LZUvaDLyN65Bv3dsisNs7IVOrq_G8OCmZ_wSR8CB1xzAXOF2qFYS3czRAh2dYnjnyIRHxZUbyA8lO2URVmQ", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{ \"query\": \"eyJsYW5nIjoiY3FsIiwibGliIjp7InJlc291cmNlVHlwZSI6IkxpYnJhcnkiLCJ1cmwiOiJ1cm46dXVpZDpiOTRkMTAwYy0zN2NkLTRiZDAtODgyNy01MzJiODRiZWE2NDgiLCJzdGF0dXMiOiJhY3RpdmUiLCJ0eXBlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9saWJyYXJ5LXR5cGUiLCJjb2RlIjoibG9naWMtbGlicmFyeSJ9XX0sImNvbnRlbnQiOlt7ImNvbnRlbnRUeXBlIjoidGV4dC9jcWwiLCJkYXRhIjoiYkdsaWNtRnllU0JTWlhSeWFXVjJaUXAxYzJsdVp5QkdTRWxTSUhabGNuTnBiMjRnSnpRdU1DNHdKd3BwYm1Oc2RXUmxJRVpJU1ZKSVpXeHdaWEp6SUhabGNuTnBiMjRnSnpRdU1DNHdKd29LWTI5a1pYTjVjM1JsYlNCc2IybHVZem9nSjJoMGRIQTZMeTlzYjJsdVl5NXZjbWNuQ2dwamIyNTBaWGgwSUZCaGRHbGxiblFLQ2tSTFZFdGZVMVJTUVZSZlIwVk9SRVZTWDFOVVVrRlVTVVpKUlZJS0NrUkxWRXRmVTFSU1FWUmZRVWRGWDFOVVVrRlVTVVpKUlZJS0NrUkxWRXRmVTFSU1FWUmZSRVZEUlVGVFJVUmZVMVJTUVZSSlJrbEZVZ29LUkV0VVMxOVRWRkpCVkY5RVNVRkhUazlUU1ZOZlUxUlNRVlJKUmtsRlVnb0tSRXRVUzE5VFZGSkJWRjlUVUVWRFNVMUZUbDlUVkZKQlZFbEdTVVZTQ2dwRVMxUkxYMU5VVWtGVVgxQlNUME5GUkZWU1JWOVRWRkpCVkVsR1NVVlNDZ3BFUzFSTFgxTlVVa0ZVWDAxRlJFbERRVlJKVDA1ZlUxUlNRVlJKUmtsRlVncEVTMVJMWDFOVVVrRlVYMFJGUmw5SlRsOUpUa2xVU1VGTVgxQlBVRlZNUVZSSlQwNVFZWFJwWlc1MExtZGxibVJsY2lBOUlDZHRZV3hsSnc9PSJ9XX0sIm1lYXN1cmUiOnsicmVzb3VyY2VUeXBlIjoiTWVhc3VyZSIsInVybCI6InVybjp1dWlkOmFjMDQwNmMxLTBlYjEtNDU4YS05ZmE4LTQ3MTZiZjhmMTBhOCIsInN0YXR1cyI6ImFjdGl2ZSIsInN1YmplY3RDb2RlYWJsZUNvbmNlcHQiOnsiY29kaW5nIjpbeyJzeXN0ZW0iOiJodHRwOi8vaGw3Lm9yZy9maGlyL3Jlc291cmNlLXR5cGVzIiwiY29kZSI6IlBhdGllbnQifV19LCJsaWJyYXJ5IjoidXJuOnV1aWQ6Yjk0ZDEwMGMtMzdjZC00YmQwLTg4MjctNTMyYjg0YmVhNjQ4Iiwic2NvcmluZyI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1zY29yaW5nIiwiY29kZSI6ImNvaG9ydCJ9XX0sImdyb3VwIjpbeyJjb2RlIjp7InRleHQiOiJwYXRpZW50cyJ9LCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6IkluSW5pdGlhbFBvcHVsYXRpb24ifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJHZW5kZXIifSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbCIsImV4cHJlc3Npb24iOiJHZW5kZXIifX0seyJjb2RlIjp7InRleHQiOiI3NTE4Ni03In0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiRGVjZWFzZWQifX0seyJjb2RlIjp7InRleHQiOiJBZ2UifSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbCIsImV4cHJlc3Npb24iOiJBZ2VDbGFzcyJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoiZGlhZ25vc2lzIn0sImV4dGVuc2lvbiI6W3sidXJsIjoiaHR0cDovL2hsNy5vcmcvZmhpci91cy9jcWZtZWFzdXJlcy9TdHJ1Y3R1cmVEZWZpbml0aW9uL2NxZm0tcG9wdWxhdGlvbkJhc2lzIiwidmFsdWVDb2RlIjoiQ29uZGl0aW9uIn1dLCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6IkRpYWdub3NpcyJ9fV0sInN0cmF0aWZpZXIiOlt7ImNvZGUiOnsidGV4dCI6ImRpYWdub3NpcyJ9LCJjcml0ZXJpYSI6eyJsYW5ndWFnZSI6InRleHQvY3FsLWlkZW50aWZpZXIiLCJleHByZXNzaW9uIjoiRGlhZ25vc2lzQ29kZSJ9fV19LHsiY29kZSI6eyJ0ZXh0Ijoic3BlY2ltZW4ifSwiZXh0ZW5zaW9uIjpbeyJ1cmwiOiJodHRwOi8vaGw3Lm9yZy9maGlyL3VzL2NxZm1lYXN1cmVzL1N0cnVjdHVyZURlZmluaXRpb24vY3FmbS1wb3B1bGF0aW9uQmFzaXMiLCJ2YWx1ZUNvZGUiOiJTcGVjaW1lbiJ9XSwicG9wdWxhdGlvbiI6W3siY29kZSI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1wb3B1bGF0aW9uIiwiY29kZSI6ImluaXRpYWwtcG9wdWxhdGlvbiJ9XX0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwtaWRlbnRpZmllciIsImV4cHJlc3Npb24iOiJTcGVjaW1lbiJ9fV0sInN0cmF0aWZpZXIiOlt7ImNvZGUiOnsidGV4dCI6InNhbXBsZV9raW5kIn0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiU2FtcGxlVHlwZSJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoicHJvY2VkdXJlcyJ9LCJleHRlbnNpb24iOlt7InVybCI6Imh0dHA6Ly9obDcub3JnL2ZoaXIvdXMvY3FmbWVhc3VyZXMvU3RydWN0dXJlRGVmaW5pdGlvbi9jcWZtLXBvcHVsYXRpb25CYXNpcyIsInZhbHVlQ29kZSI6IlByb2NlZHVyZSJ9XSwicG9wdWxhdGlvbiI6W3siY29kZSI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1wb3B1bGF0aW9uIiwiY29kZSI6ImluaXRpYWwtcG9wdWxhdGlvbiJ9XX0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwtaWRlbnRpZmllciIsImV4cHJlc3Npb24iOiJQcm9jZWR1cmUifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJQcm9jZWR1cmVUeXBlIn0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiUHJvY2VkdXJlVHlwZSJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoibWVkaWNhdGlvblN0YXRlbWVudHMifSwiZXh0ZW5zaW9uIjpbeyJ1cmwiOiJodHRwOi8vaGw3Lm9yZy9maGlyL3VzL2NxZm1lYXN1cmVzL1N0cnVjdHVyZURlZmluaXRpb24vY3FmbS1wb3B1bGF0aW9uQmFzaXMiLCJ2YWx1ZUNvZGUiOiJNZWRpY2F0aW9uU3RhdGVtZW50In1dLCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6Ik1lZGljYXRpb25TdGF0ZW1lbnQifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJNZWRpY2F0aW9uVHlwZSJ9LCJjcml0ZXJpYSI6eyJsYW5ndWFnZSI6InRleHQvY3FsIiwiZXhwcmVzc2lvbiI6IlByb2NlZHVyZVR5cGUifX1dfV19fQ==\" }", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8097/create-query-and-design-project?label=Test&description=This is a test query for CCP&template-id=ccp&output-format=EXCEL&query-format=CQL_DATA&human-readable=KGdlbmRlciA9IG1hbGUp&bridgeheads=star-wars,jurassic-park&project-type=EXPORT", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "create-query-and-design-project" + ], + "query": [ + { + "key": "label", + "value": "Test" + }, + { + "key": "description", + "value": "This is a test query for CCP" + }, + { + "key": "template-id", + "value": "ccp" + }, + { + "key": "output-format", + "value": "EXCEL" + }, + { + "key": "query-format", + "value": "CQL_DATA" + }, + { + "key": "human-readable", + "value": "KGdlbmRlciA9IG1hbGUp" + }, + { + "key": "bridgeheads", + "value": "star-wars,jurassic-park" + }, + { + "key": "explorer-url", + "value": "https://data.dktk.dkfz.de/redirect/12345", + "type": "text", + "disabled": true + }, + { + "key": "project-type", + "value": "EXPORT" + } + ] + } + }, + "response": [] + }, + { + "name": "Create query and design project (Researcher) - Ohne Authorization (nur JSESSIONID Cookie)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{ \"query\": \"eyJsYW5nIjoiY3FsIiwibGliIjp7InJlc291cmNlVHlwZSI6IkxpYnJhcnkiLCJ1cmwiOiJ1cm46dXVpZDpiOTRkMTAwYy0zN2NkLTRiZDAtODgyNy01MzJiODRiZWE2NDgiLCJzdGF0dXMiOiJhY3RpdmUiLCJ0eXBlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9saWJyYXJ5LXR5cGUiLCJjb2RlIjoibG9naWMtbGlicmFyeSJ9XX0sImNvbnRlbnQiOlt7ImNvbnRlbnRUeXBlIjoidGV4dC9jcWwiLCJkYXRhIjoiYkdsaWNtRnllU0JTWlhSeWFXVjJaUXAxYzJsdVp5QkdTRWxTSUhabGNuTnBiMjRnSnpRdU1DNHdKd3BwYm1Oc2RXUmxJRVpJU1ZKSVpXeHdaWEp6SUhabGNuTnBiMjRnSnpRdU1DNHdKd29LWTI5a1pYTjVjM1JsYlNCc2IybHVZem9nSjJoMGRIQTZMeTlzYjJsdVl5NXZjbWNuQ2dwamIyNTBaWGgwSUZCaGRHbGxiblFLQ2tSTFZFdGZVMVJTUVZSZlIwVk9SRVZTWDFOVVVrRlVTVVpKUlZJS0NrUkxWRXRmVTFSU1FWUmZRVWRGWDFOVVVrRlVTVVpKUlZJS0NrUkxWRXRmVTFSU1FWUmZSRVZEUlVGVFJVUmZVMVJTUVZSSlJrbEZVZ29LUkV0VVMxOVRWRkpCVkY5RVNVRkhUazlUU1ZOZlUxUlNRVlJKUmtsRlVnb0tSRXRVUzE5VFZGSkJWRjlUVUVWRFNVMUZUbDlUVkZKQlZFbEdTVVZTQ2dwRVMxUkxYMU5VVWtGVVgxQlNUME5GUkZWU1JWOVRWRkpCVkVsR1NVVlNDZ3BFUzFSTFgxTlVVa0ZVWDAxRlJFbERRVlJKVDA1ZlUxUlNRVlJKUmtsRlVncEVTMVJMWDFOVVVrRlVYMFJGUmw5SlRsOUpUa2xVU1VGTVgxQlBVRlZNUVZSSlQwNVFZWFJwWlc1MExtZGxibVJsY2lBOUlDZHRZV3hsSnc9PSJ9XX0sIm1lYXN1cmUiOnsicmVzb3VyY2VUeXBlIjoiTWVhc3VyZSIsInVybCI6InVybjp1dWlkOmFjMDQwNmMxLTBlYjEtNDU4YS05ZmE4LTQ3MTZiZjhmMTBhOCIsInN0YXR1cyI6ImFjdGl2ZSIsInN1YmplY3RDb2RlYWJsZUNvbmNlcHQiOnsiY29kaW5nIjpbeyJzeXN0ZW0iOiJodHRwOi8vaGw3Lm9yZy9maGlyL3Jlc291cmNlLXR5cGVzIiwiY29kZSI6IlBhdGllbnQifV19LCJsaWJyYXJ5IjoidXJuOnV1aWQ6Yjk0ZDEwMGMtMzdjZC00YmQwLTg4MjctNTMyYjg0YmVhNjQ4Iiwic2NvcmluZyI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1zY29yaW5nIiwiY29kZSI6ImNvaG9ydCJ9XX0sImdyb3VwIjpbeyJjb2RlIjp7InRleHQiOiJwYXRpZW50cyJ9LCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6IkluSW5pdGlhbFBvcHVsYXRpb24ifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJHZW5kZXIifSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbCIsImV4cHJlc3Npb24iOiJHZW5kZXIifX0seyJjb2RlIjp7InRleHQiOiI3NTE4Ni03In0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiRGVjZWFzZWQifX0seyJjb2RlIjp7InRleHQiOiJBZ2UifSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbCIsImV4cHJlc3Npb24iOiJBZ2VDbGFzcyJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoiZGlhZ25vc2lzIn0sImV4dGVuc2lvbiI6W3sidXJsIjoiaHR0cDovL2hsNy5vcmcvZmhpci91cy9jcWZtZWFzdXJlcy9TdHJ1Y3R1cmVEZWZpbml0aW9uL2NxZm0tcG9wdWxhdGlvbkJhc2lzIiwidmFsdWVDb2RlIjoiQ29uZGl0aW9uIn1dLCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6IkRpYWdub3NpcyJ9fV0sInN0cmF0aWZpZXIiOlt7ImNvZGUiOnsidGV4dCI6ImRpYWdub3NpcyJ9LCJjcml0ZXJpYSI6eyJsYW5ndWFnZSI6InRleHQvY3FsLWlkZW50aWZpZXIiLCJleHByZXNzaW9uIjoiRGlhZ25vc2lzQ29kZSJ9fV19LHsiY29kZSI6eyJ0ZXh0Ijoic3BlY2ltZW4ifSwiZXh0ZW5zaW9uIjpbeyJ1cmwiOiJodHRwOi8vaGw3Lm9yZy9maGlyL3VzL2NxZm1lYXN1cmVzL1N0cnVjdHVyZURlZmluaXRpb24vY3FmbS1wb3B1bGF0aW9uQmFzaXMiLCJ2YWx1ZUNvZGUiOiJTcGVjaW1lbiJ9XSwicG9wdWxhdGlvbiI6W3siY29kZSI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1wb3B1bGF0aW9uIiwiY29kZSI6ImluaXRpYWwtcG9wdWxhdGlvbiJ9XX0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwtaWRlbnRpZmllciIsImV4cHJlc3Npb24iOiJTcGVjaW1lbiJ9fV0sInN0cmF0aWZpZXIiOlt7ImNvZGUiOnsidGV4dCI6InNhbXBsZV9raW5kIn0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiU2FtcGxlVHlwZSJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoicHJvY2VkdXJlcyJ9LCJleHRlbnNpb24iOlt7InVybCI6Imh0dHA6Ly9obDcub3JnL2ZoaXIvdXMvY3FmbWVhc3VyZXMvU3RydWN0dXJlRGVmaW5pdGlvbi9jcWZtLXBvcHVsYXRpb25CYXNpcyIsInZhbHVlQ29kZSI6IlByb2NlZHVyZSJ9XSwicG9wdWxhdGlvbiI6W3siY29kZSI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1wb3B1bGF0aW9uIiwiY29kZSI6ImluaXRpYWwtcG9wdWxhdGlvbiJ9XX0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwtaWRlbnRpZmllciIsImV4cHJlc3Npb24iOiJQcm9jZWR1cmUifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJQcm9jZWR1cmVUeXBlIn0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiUHJvY2VkdXJlVHlwZSJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoibWVkaWNhdGlvblN0YXRlbWVudHMifSwiZXh0ZW5zaW9uIjpbeyJ1cmwiOiJodHRwOi8vaGw3Lm9yZy9maGlyL3VzL2NxZm1lYXN1cmVzL1N0cnVjdHVyZURlZmluaXRpb24vY3FmbS1wb3B1bGF0aW9uQmFzaXMiLCJ2YWx1ZUNvZGUiOiJNZWRpY2F0aW9uU3RhdGVtZW50In1dLCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6Ik1lZGljYXRpb25TdGF0ZW1lbnQifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJNZWRpY2F0aW9uVHlwZSJ9LCJjcml0ZXJpYSI6eyJsYW5ndWFnZSI6InRleHQvY3FsIiwiZXhwcmVzc2lvbiI6IlByb2NlZHVyZVR5cGUifX1dfV19fQ==\" }", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8097/create-query-and-design-project?label=Test&description=This is a test query for CCP&template-id=ccp&output-format=EXCEL&query-format=CQL_DATA&human-readable=KGdlbmRlciA9IG1hbGUp&bridgeheads=star-wars,jurassic-park&project-type=EXPORT", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "create-query-and-design-project" + ], + "query": [ + { + "key": "label", + "value": "Test" + }, + { + "key": "description", + "value": "This is a test query for CCP" + }, + { + "key": "template-id", + "value": "ccp" + }, + { + "key": "output-format", + "value": "EXCEL" + }, + { + "key": "query-format", + "value": "CQL_DATA" + }, + { + "key": "human-readable", + "value": "KGdlbmRlciA9IG1hbGUp" + }, + { + "key": "bridgeheads", + "value": "star-wars,jurassic-park" + }, + { + "key": "explorer-url", + "value": "https://data.dktk.dkfz.de/redirect/12345", + "type": "text", + "disabled": true + }, + { + "key": "project-type", + "value": "EXPORT" + } + ] + } + }, + "response": [] + }, + { + "name": "Create query and design project (Researcher) - Ohne Authorization (nur JSESSIONID Cookie) without PROJECT TYPE", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{ \"query\": \"eyJsYW5nIjoiY3FsIiwibGliIjp7InJlc291cmNlVHlwZSI6IkxpYnJhcnkiLCJ1cmwiOiJ1cm46dXVpZDpiOTRkMTAwYy0zN2NkLTRiZDAtODgyNy01MzJiODRiZWE2NDgiLCJzdGF0dXMiOiJhY3RpdmUiLCJ0eXBlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9saWJyYXJ5LXR5cGUiLCJjb2RlIjoibG9naWMtbGlicmFyeSJ9XX0sImNvbnRlbnQiOlt7ImNvbnRlbnRUeXBlIjoidGV4dC9jcWwiLCJkYXRhIjoiYkdsaWNtRnllU0JTWlhSeWFXVjJaUXAxYzJsdVp5QkdTRWxTSUhabGNuTnBiMjRnSnpRdU1DNHdKd3BwYm1Oc2RXUmxJRVpJU1ZKSVpXeHdaWEp6SUhabGNuTnBiMjRnSnpRdU1DNHdKd29LWTI5a1pYTjVjM1JsYlNCc2IybHVZem9nSjJoMGRIQTZMeTlzYjJsdVl5NXZjbWNuQ2dwamIyNTBaWGgwSUZCaGRHbGxiblFLQ2tSTFZFdGZVMVJTUVZSZlIwVk9SRVZTWDFOVVVrRlVTVVpKUlZJS0NrUkxWRXRmVTFSU1FWUmZRVWRGWDFOVVVrRlVTVVpKUlZJS0NrUkxWRXRmVTFSU1FWUmZSRVZEUlVGVFJVUmZVMVJTUVZSSlJrbEZVZ29LUkV0VVMxOVRWRkpCVkY5RVNVRkhUazlUU1ZOZlUxUlNRVlJKUmtsRlVnb0tSRXRVUzE5VFZGSkJWRjlUVUVWRFNVMUZUbDlUVkZKQlZFbEdTVVZTQ2dwRVMxUkxYMU5VVWtGVVgxQlNUME5GUkZWU1JWOVRWRkpCVkVsR1NVVlNDZ3BFUzFSTFgxTlVVa0ZVWDAxRlJFbERRVlJKVDA1ZlUxUlNRVlJKUmtsRlVncEVTMVJMWDFOVVVrRlVYMFJGUmw5SlRsOUpUa2xVU1VGTVgxQlBVRlZNUVZSSlQwNVFZWFJwWlc1MExtZGxibVJsY2lBOUlDZHRZV3hsSnc9PSJ9XX0sIm1lYXN1cmUiOnsicmVzb3VyY2VUeXBlIjoiTWVhc3VyZSIsInVybCI6InVybjp1dWlkOmFjMDQwNmMxLTBlYjEtNDU4YS05ZmE4LTQ3MTZiZjhmMTBhOCIsInN0YXR1cyI6ImFjdGl2ZSIsInN1YmplY3RDb2RlYWJsZUNvbmNlcHQiOnsiY29kaW5nIjpbeyJzeXN0ZW0iOiJodHRwOi8vaGw3Lm9yZy9maGlyL3Jlc291cmNlLXR5cGVzIiwiY29kZSI6IlBhdGllbnQifV19LCJsaWJyYXJ5IjoidXJuOnV1aWQ6Yjk0ZDEwMGMtMzdjZC00YmQwLTg4MjctNTMyYjg0YmVhNjQ4Iiwic2NvcmluZyI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1zY29yaW5nIiwiY29kZSI6ImNvaG9ydCJ9XX0sImdyb3VwIjpbeyJjb2RlIjp7InRleHQiOiJwYXRpZW50cyJ9LCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6IkluSW5pdGlhbFBvcHVsYXRpb24ifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJHZW5kZXIifSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbCIsImV4cHJlc3Npb24iOiJHZW5kZXIifX0seyJjb2RlIjp7InRleHQiOiI3NTE4Ni03In0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiRGVjZWFzZWQifX0seyJjb2RlIjp7InRleHQiOiJBZ2UifSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbCIsImV4cHJlc3Npb24iOiJBZ2VDbGFzcyJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoiZGlhZ25vc2lzIn0sImV4dGVuc2lvbiI6W3sidXJsIjoiaHR0cDovL2hsNy5vcmcvZmhpci91cy9jcWZtZWFzdXJlcy9TdHJ1Y3R1cmVEZWZpbml0aW9uL2NxZm0tcG9wdWxhdGlvbkJhc2lzIiwidmFsdWVDb2RlIjoiQ29uZGl0aW9uIn1dLCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6IkRpYWdub3NpcyJ9fV0sInN0cmF0aWZpZXIiOlt7ImNvZGUiOnsidGV4dCI6ImRpYWdub3NpcyJ9LCJjcml0ZXJpYSI6eyJsYW5ndWFnZSI6InRleHQvY3FsLWlkZW50aWZpZXIiLCJleHByZXNzaW9uIjoiRGlhZ25vc2lzQ29kZSJ9fV19LHsiY29kZSI6eyJ0ZXh0Ijoic3BlY2ltZW4ifSwiZXh0ZW5zaW9uIjpbeyJ1cmwiOiJodHRwOi8vaGw3Lm9yZy9maGlyL3VzL2NxZm1lYXN1cmVzL1N0cnVjdHVyZURlZmluaXRpb24vY3FmbS1wb3B1bGF0aW9uQmFzaXMiLCJ2YWx1ZUNvZGUiOiJTcGVjaW1lbiJ9XSwicG9wdWxhdGlvbiI6W3siY29kZSI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1wb3B1bGF0aW9uIiwiY29kZSI6ImluaXRpYWwtcG9wdWxhdGlvbiJ9XX0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwtaWRlbnRpZmllciIsImV4cHJlc3Npb24iOiJTcGVjaW1lbiJ9fV0sInN0cmF0aWZpZXIiOlt7ImNvZGUiOnsidGV4dCI6InNhbXBsZV9raW5kIn0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiU2FtcGxlVHlwZSJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoicHJvY2VkdXJlcyJ9LCJleHRlbnNpb24iOlt7InVybCI6Imh0dHA6Ly9obDcub3JnL2ZoaXIvdXMvY3FmbWVhc3VyZXMvU3RydWN0dXJlRGVmaW5pdGlvbi9jcWZtLXBvcHVsYXRpb25CYXNpcyIsInZhbHVlQ29kZSI6IlByb2NlZHVyZSJ9XSwicG9wdWxhdGlvbiI6W3siY29kZSI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1wb3B1bGF0aW9uIiwiY29kZSI6ImluaXRpYWwtcG9wdWxhdGlvbiJ9XX0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwtaWRlbnRpZmllciIsImV4cHJlc3Npb24iOiJQcm9jZWR1cmUifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJQcm9jZWR1cmVUeXBlIn0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiUHJvY2VkdXJlVHlwZSJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoibWVkaWNhdGlvblN0YXRlbWVudHMifSwiZXh0ZW5zaW9uIjpbeyJ1cmwiOiJodHRwOi8vaGw3Lm9yZy9maGlyL3VzL2NxZm1lYXN1cmVzL1N0cnVjdHVyZURlZmluaXRpb24vY3FmbS1wb3B1bGF0aW9uQmFzaXMiLCJ2YWx1ZUNvZGUiOiJNZWRpY2F0aW9uU3RhdGVtZW50In1dLCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6Ik1lZGljYXRpb25TdGF0ZW1lbnQifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJNZWRpY2F0aW9uVHlwZSJ9LCJjcml0ZXJpYSI6eyJsYW5ndWFnZSI6InRleHQvY3FsIiwiZXhwcmVzc2lvbiI6IlByb2NlZHVyZVR5cGUifX1dfV19fQ==\" }", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8097/create-query-and-design-project?label=Test&description=This is a test query for CCP&query-format=CQL_DATA&human-readable=KGdlbmRlciA9IG1hbGUp&bridgeheads=star-wars,jurassic-park", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "create-query-and-design-project" + ], + "query": [ + { + "key": "label", + "value": "Test" + }, + { + "key": "description", + "value": "This is a test query for CCP" + }, + { + "key": "query-format", + "value": "CQL_DATA" + }, + { + "key": "human-readable", + "value": "KGdlbmRlciA9IG1hbGUp" + }, + { + "key": "bridgeheads", + "value": "star-wars,jurassic-park" + }, + { + "key": "explorer-url", + "value": "https://data.dktk.dkfz.de/redirect/12345", + "type": "text", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Create ast query and design project (Researcher) - Ohne Authorization (nur JSESSIONID Cookie) without PROJECT TYPE", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{ \"query\": \"eyJsYW5nIjoiYXN0IiwicGF5bG9hZCI6ImV5SmhjM1FpT25zaWIzQmxjbUZ1WkNJNklrOVNJaXdpWTJocGJHUnlaVzRpT2x0N0ltOXdaWEpoYm1RaU9pSkJUa1FpTENKamFHbHNaSEpsYmlJNlczc2lhMlY1SWpvaVoyeHBiMjFmWVd4c1gyZHliM1Z3Y3lJc0ltOXdaWEpoYm1RaU9pSlBVaUlzSW1Ob2FXeGtjbVZ1SWpwYmV5SnJaWGtpT2lKSGJHbHZiU0F0SUVkeVlXUWdTU0lzSW05d1pYSmhibVFpT2lKQlRrUWlMQ0pqYUdsc1pISmxiaUk2VzNzaWIzQmxjbUZ1WkNJNklrOVNJaXdpWTJocGJHUnlaVzRpT2x0N0ltOXdaWEpoYm1RaU9pSlBVaUlzSW10bGVTSTZJbVJwWVdkdWIzTnBjeUlzSW1Ob2FXeGtjbVZ1SWpwYmV5SnJaWGtpT2lKa2FXRm5ibTl6YVhNaUxDSjBlWEJsSWpvaVJWRlZRVXhUSWl3aWRtRnNkV1VpT2lKRU5ETWlmU3g3SW10bGVTSTZJbVJwWVdkdWIzTnBjeUlzSW5SNWNHVWlPaUpGVVZWQlRGTWlMQ0oyWVd4MVpTSTZJa1EwTXk0eEluMHNleUpyWlhraU9pSmthV0ZuYm05emFYTWlMQ0owZVhCbElqb2lSVkZWUVV4VElpd2lkbUZzZFdVaU9pSkVORE11TWlKOUxIc2lhMlY1SWpvaVpHbGhaMjV2YzJseklpd2lkSGx3WlNJNklrVlJWVUZNVXlJc0luWmhiSFZsSWpvaVJEUXpMakFpZlN4N0ltdGxlU0k2SW1ScFlXZHViM05wY3lJc0luUjVjR1VpT2lKRlVWVkJURk1pTENKMllXeDFaU0k2SWtRME15NHpJbjBzZXlKclpYa2lPaUprYVdGbmJtOXphWE1pTENKMGVYQmxJam9pUlZGVlFVeFRJaXdpZG1Gc2RXVWlPaUpFTkRNdU5DSjlMSHNpYTJWNUlqb2laR2xoWjI1dmMybHpJaXdpZEhsd1pTSTZJa1ZSVlVGTVV5SXNJblpoYkhWbElqb2lSRFF6TGpjaWZTeDdJbXRsZVNJNkltUnBZV2R1YjNOcGN5SXNJblI1Y0dVaU9pSkZVVlZCVEZNaUxDSjJZV3gxWlNJNklrUTBNeTQ1SW4xZGZWMTlMSHNpYjNCbGNtRnVaQ0k2SWs5U0lpd2lZMmhwYkdSeVpXNGlPbHQ3SW10bGVTSTZJbTF2Y25Cb2IyeHZaM2tpTENKMGVYQmxJam9pUlZGVlFVeFRJaXdpZG1Gc2RXVWlPaUk1TXpnekx6RWlmU3g3SW10bGVTSTZJbTF2Y25Cb2IyeHZaM2tpTENKMGVYQmxJam9pUlZGVlFVeFRJaXdpZG1Gc2RXVWlPaUk1TXpnMEx6RWlmU3g3SW10bGVTSTZJbTF2Y25Cb2IyeHZaM2tpTENKMGVYQmxJam9pUlZGVlFVeFRJaXdpZG1Gc2RXVWlPaUk1TXprMEx6RWlmU3g3SW10bGVTSTZJbTF2Y25Cb2IyeHZaM2tpTENKMGVYQmxJam9pUlZGVlFVeFRJaXdpZG1Gc2RXVWlPaUk1TkRJeEx6RWlmVjE5WFgxZGZWMTlYWDBzSW1sa0lqb2laREZrWm1NME16a3RZalJtT0MwMFltWmhMV0UxTmpjdFltRTFZelkzWVdKa01XVTJJbjA9In0=\" }", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8097/create-query-and-design-project?label=Test&description=This is a test query for CCP&query-format=AST_DATA&human-readable=KCgoZGlhZ25vc2lzIEVRVUFMUyBENDMpIE9SIChkaWFnbm9zaXMgRVFVQUxTIEQ0My4xKSBPUiAoZGlhZ25vc2lzIEVRVUFMUyBENDMuMikgT1IgKGRpYWdub3NpcyBFUVVBTFMgRDQzLjApIE9SIChkaWFnbm9zaXMgRVFVQUxTIEQ0My4zKSBPUiAoZGlhZ25vc2lzIEVRVUFMUyBENDMuNCkgT1IgKGRpYWdub3NpcyBFUVVBTFMgRDQzLjcpIE9SIChkaWFnbm9zaXMgRVFVQUxTIEQ0My45KSkgQU5EICgobW9ycGhvbG9neSBFUVVBTFMgOTM4My8xKSBPUiAobW9ycGhvbG9neSBFUVVBTFMgOTM4NC8xKSBPUiAobW9ycGhvbG9neSBFUVVBTFMgOTM5NC8xKSBPUiAobW9ycGhvbG9neSBFUVVBTFMgOTQyMS8xKSkpCg==&bridgeheads=star-wars,jurassic-park", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "create-query-and-design-project" + ], + "query": [ + { + "key": "label", + "value": "Test" + }, + { + "key": "description", + "value": "This is a test query for CCP" + }, + { + "key": "query-format", + "value": "AST_DATA" + }, + { + "key": "human-readable", + "value": "KCgoZGlhZ25vc2lzIEVRVUFMUyBENDMpIE9SIChkaWFnbm9zaXMgRVFVQUxTIEQ0My4xKSBPUiAoZGlhZ25vc2lzIEVRVUFMUyBENDMuMikgT1IgKGRpYWdub3NpcyBFUVVBTFMgRDQzLjApIE9SIChkaWFnbm9zaXMgRVFVQUxTIEQ0My4zKSBPUiAoZGlhZ25vc2lzIEVRVUFMUyBENDMuNCkgT1IgKGRpYWdub3NpcyBFUVVBTFMgRDQzLjcpIE9SIChkaWFnbm9zaXMgRVFVQUxTIEQ0My45KSkgQU5EICgobW9ycGhvbG9neSBFUVVBTFMgOTM4My8xKSBPUiAobW9ycGhvbG9neSBFUVVBTFMgOTM4NC8xKSBPUiAobW9ycGhvbG9neSBFUVVBTFMgOTM5NC8xKSBPUiAobW9ycGhvbG9neSBFUVVBTFMgOTQyMS8xKSkpCg==" + }, + { + "key": "bridgeheads", + "value": "star-wars,jurassic-park" + }, + { + "key": "explorer-url", + "value": "https://data.dktk.dkfz.de/redirect/12345", + "type": "text", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Create query and design project (Researcher) [e260-ccp-req]", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{ \"query\": \"eyJsYW5nIjoiY3FsIiwibGliIjp7InJlc291cmNlVHlwZSI6IkxpYnJhcnkiLCJ1cmwiOiJ1cm46dXVpZDoyOTI0ZTA1MC1mNmZlLTQ2YjUtYjcwMi0xNWE5ZWVjNzQ0M2MiLCJzdGF0dXMiOiJhY3RpdmUiLCJ0eXBlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9saWJyYXJ5LXR5cGUiLCJjb2RlIjoibG9naWMtbGlicmFyeSJ9XX0sImNvbnRlbnQiOlt7ImNvbnRlbnRUeXBlIjoidGV4dC9jcWwiLCJkYXRhIjoiYkdsaWNtRnllU0JTWlhSeWFXVjJaUXAxYzJsdVp5QkdTRWxTSUhabGNuTnBiMjRnSnpRdU1DNHdKd3BwYm1Oc2RXUmxJRVpJU1ZKSVpXeHdaWEp6SUhabGNuTnBiMjRnSnpRdU1DNHdKd29LWTI5a1pYTjVjM1JsYlNCc2IybHVZem9nSjJoMGRIQTZMeTlzYjJsdVl5NXZjbWNuQ2dwamIyNTBaWGgwSUZCaGRHbGxiblFLQ2dwRVMxUkxYMU5VVWtGVVgwZEZUa1JGVWw5VFZGSkJWRWxHU1VWU0NncEVTMVJMWDFOVVVrRlVYMUJTU1UxQlVsbGZSRWxCUjA1UFUwbFRYMDVQWDFOUFVsUmZVMVJTUVZSSlJrbEZVZ3BFUzFSTFgxTlVVa0ZVWDBGSFJWOURURUZUVTE5VFZGSkJWRWxHU1VWU0NncEVTMVJMWDFOVVVrRlVYMFJGUTBWQlUwVkVYMU5VVWtGVVNVWkpSVklLQ2tSTFZFdGZVMVJTUVZSZlJFbEJSMDVQVTBsVFgxTlVVa0ZVU1VaSlJWSUtDa1JMVkV0ZlUxUlNRVlJmVTFCRlEwbE5SVTVmVTFSU1FWUkpSa2xGVWdvS1JFdFVTMTlUVkZKQlZGOVFVazlEUlVSVlVrVmZVMVJTUVZSSlJrbEZVZ29LUkV0VVMxOVRWRkpCVkY5TlJVUkpRMEZVU1U5T1gxTlVVa0ZVU1VaSlJWSUtDaUFnUkV0VVMxOVRWRkpCVkY5SVNWTlVUMHhQUjFsZlUxUlNRVlJKUmtsRlVncEVTMVJMWDFOVVVrRlVYMFJGUmw5SlRsOUpUa2xVU1VGTVgxQlBVRlZNUVZSSlQwNVFZWFJwWlc1MExtZGxibVJsY2lBOUlDZHRZV3hsSnc9PSJ9XX0sIm1lYXN1cmUiOnsicmVzb3VyY2VUeXBlIjoiTWVhc3VyZSIsInVybCI6InVybjp1dWlkOjEwMDNlNGMzLWRkZDctNGE2NC1hMmNlLTE5ODBiMmJkOGEyNCIsInN0YXR1cyI6ImFjdGl2ZSIsInN1YmplY3RDb2RlYWJsZUNvbmNlcHQiOnsiY29kaW5nIjpbeyJzeXN0ZW0iOiJodHRwOi8vaGw3Lm9yZy9maGlyL3Jlc291cmNlLXR5cGVzIiwiY29kZSI6IlBhdGllbnQifV19LCJsaWJyYXJ5IjoidXJuOnV1aWQ6MjkyNGUwNTAtZjZmZS00NmI1LWI3MDItMTVhOWVlYzc0NDNjIiwic2NvcmluZyI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1zY29yaW5nIiwiY29kZSI6ImNvaG9ydCJ9XX0sImdyb3VwIjpbeyJjb2RlIjp7InRleHQiOiJwYXRpZW50cyJ9LCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6IkluSW5pdGlhbFBvcHVsYXRpb24ifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJHZW5kZXIifSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbCIsImV4cHJlc3Npb24iOiJHZW5kZXIifX0seyJjb2RlIjp7InRleHQiOiI3NTE4Ni03In0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiRGVjZWFzZWQifX0seyJjb2RlIjp7InRleHQiOiJBZ2UifSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbCIsImV4cHJlc3Npb24iOiJBZ2VDbGFzcyJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoiZGlhZ25vc2lzIn0sImV4dGVuc2lvbiI6W3sidXJsIjoiaHR0cDovL2hsNy5vcmcvZmhpci91cy9jcWZtZWFzdXJlcy9TdHJ1Y3R1cmVEZWZpbml0aW9uL2NxZm0tcG9wdWxhdGlvbkJhc2lzIiwidmFsdWVDb2RlIjoiQ29uZGl0aW9uIn1dLCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6IkRpYWdub3NpcyJ9fV0sInN0cmF0aWZpZXIiOlt7ImNvZGUiOnsidGV4dCI6ImRpYWdub3NpcyJ9LCJjcml0ZXJpYSI6eyJsYW5ndWFnZSI6InRleHQvY3FsLWlkZW50aWZpZXIiLCJleHByZXNzaW9uIjoiRGlhZ25vc2lzQ29kZSJ9fV19LHsiY29kZSI6eyJ0ZXh0Ijoic3BlY2ltZW4ifSwiZXh0ZW5zaW9uIjpbeyJ1cmwiOiJodHRwOi8vaGw3Lm9yZy9maGlyL3VzL2NxZm1lYXN1cmVzL1N0cnVjdHVyZURlZmluaXRpb24vY3FmbS1wb3B1bGF0aW9uQmFzaXMiLCJ2YWx1ZUNvZGUiOiJTcGVjaW1lbiJ9XSwicG9wdWxhdGlvbiI6W3siY29kZSI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1wb3B1bGF0aW9uIiwiY29kZSI6ImluaXRpYWwtcG9wdWxhdGlvbiJ9XX0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwtaWRlbnRpZmllciIsImV4cHJlc3Npb24iOiJTcGVjaW1lbiJ9fV0sInN0cmF0aWZpZXIiOlt7ImNvZGUiOnsidGV4dCI6InNhbXBsZV9raW5kIn0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiU2FtcGxlVHlwZSJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoicHJvY2VkdXJlcyJ9LCJleHRlbnNpb24iOlt7InVybCI6Imh0dHA6Ly9obDcub3JnL2ZoaXIvdXMvY3FmbWVhc3VyZXMvU3RydWN0dXJlRGVmaW5pdGlvbi9jcWZtLXBvcHVsYXRpb25CYXNpcyIsInZhbHVlQ29kZSI6IlByb2NlZHVyZSJ9XSwicG9wdWxhdGlvbiI6W3siY29kZSI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1wb3B1bGF0aW9uIiwiY29kZSI6ImluaXRpYWwtcG9wdWxhdGlvbiJ9XX0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwtaWRlbnRpZmllciIsImV4cHJlc3Npb24iOiJQcm9jZWR1cmUifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJQcm9jZWR1cmVUeXBlIn0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiUHJvY2VkdXJlVHlwZSJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoibWVkaWNhdGlvblN0YXRlbWVudHMifSwiZXh0ZW5zaW9uIjpbeyJ1cmwiOiJodHRwOi8vaGw3Lm9yZy9maGlyL3VzL2NxZm1lYXN1cmVzL1N0cnVjdHVyZURlZmluaXRpb24vY3FmbS1wb3B1bGF0aW9uQmFzaXMiLCJ2YWx1ZUNvZGUiOiJNZWRpY2F0aW9uU3RhdGVtZW50In1dLCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6Ik1lZGljYXRpb25TdGF0ZW1lbnQifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJNZWRpY2F0aW9uVHlwZSJ9LCJjcml0ZXJpYSI6eyJsYW5ndWFnZSI6InRleHQvY3FsIiwiZXhwcmVzc2lvbiI6IlByb2NlZHVyZVR5cGUifX1dfSx7ImNvZGUiOnsidGV4dCI6Ikhpc3RvIn0sImV4dGVuc2lvbiI6W3sidXJsIjoiaHR0cDovL2hsNy5vcmcvZmhpci91cy9jcWZtZWFzdXJlcy9TdHJ1Y3R1cmVEZWZpbml0aW9uL2NxZm0tcG9wdWxhdGlvbkJhc2lzIiwidmFsdWVDb2RlIjoiT2JzZXJ2YXRpb24ifV0sInBvcHVsYXRpb24iOlt7ImNvZGUiOnsiY29kaW5nIjpbeyJzeXN0ZW0iOiJodHRwOi8vdGVybWlub2xvZ3kuaGw3Lm9yZy9Db2RlU3lzdGVtL21lYXN1cmUtcG9wdWxhdGlvbiIsImNvZGUiOiJpbml0aWFsLXBvcHVsYXRpb24ifV19LCJjcml0ZXJpYSI6eyJsYW5ndWFnZSI6InRleHQvY3FsLWlkZW50aWZpZXIiLCJleHByZXNzaW9uIjoiSGlzdG8ifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJIaXN0b2xvZ2llcyJ9LCJjcml0ZXJpYSI6eyJsYW5ndWFnZSI6InRleHQvY3FsLWlkZW50aWZpZXIiLCJleHByZXNzaW9uIjoiSGlzdG9sb2d5In19XX1dfX0=\" }", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://e260-ccp-req/project-manager-api/create-query-and-design-project?label=Test&description=This is a test query for CCP&template-id=ccp&output-format=EXCEL&query-format=CQL_DATA&human-readable=KGdlbmRlciA9IG1hbGUp&bridgeheads=mannheim,dktk-test,dktk-datashield-test&project-type=EXPORT", + "protocol": "https", + "host": [ + "e260-ccp-req" + ], + "path": [ + "project-manager-api", + "create-query-and-design-project" + ], + "query": [ + { + "key": "label", + "value": "Test" + }, + { + "key": "description", + "value": "This is a test query for CCP" + }, + { + "key": "template-id", + "value": "ccp" + }, + { + "key": "output-format", + "value": "EXCEL" + }, + { + "key": "query-format", + "value": "CQL_DATA" + }, + { + "key": "human-readable", + "value": "KGdlbmRlciA9IG1hbGUp" + }, + { + "key": "bridgeheads", + "value": "mannheim,dktk-test,dktk-datashield-test" + }, + { + "key": "explorer-url", + "value": "https://data.dktk.dkfz.de/redirect/12345", + "type": "text", + "disabled": true + }, + { + "key": "project-type", + "value": "EXPORT" + } + ] + } + }, + "response": [] + }, + { + "name": "Create query and design project (Researcher) [requests.dktk.dkfz.de]", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"query\": \"eyJsYW5nIjoiYXN0IiwicGF5bG9hZCI6ImV5SmhjM1FpT25zaWIzQmxjbUZ1WkNJNklrOVNJaXdpWTJocGJHUnlaVzRpT2x0N0ltOXdaWEpoYm1RaU9pSkJUa1FpTENKamFHbHNaSEpsYmlJNlczc2lhMlY1SWpvaVoyeHBiMjFmWVd4c1gyZHliM1Z3Y3lJc0ltOXdaWEpoYm1RaU9pSlBVaUlzSW1Ob2FXeGtjbVZ1SWpwYmV5SnJaWGtpT2lKSGJHbHZiU0F0SUVkeVlXUWdTU0lzSW05d1pYSmhibVFpT2lKQlRrUWlMQ0pqYUdsc1pISmxiaUk2VzNzaWIzQmxjbUZ1WkNJNklrOVNJaXdpWTJocGJHUnlaVzRpT2x0N0ltOXdaWEpoYm1RaU9pSlBVaUlzSW10bGVTSTZJbVJwWVdkdWIzTnBjeUlzSW1Ob2FXeGtjbVZ1SWpwYmV5SnJaWGtpT2lKa2FXRm5ibTl6YVhNaUxDSjBlWEJsSWpvaVJWRlZRVXhUSWl3aWRtRnNkV1VpT2lKRU5ETWlmU3g3SW10bGVTSTZJbVJwWVdkdWIzTnBjeUlzSW5SNWNHVWlPaUpGVVZWQlRGTWlMQ0oyWVd4MVpTSTZJa1EwTXk0eEluMHNleUpyWlhraU9pSmthV0ZuYm05emFYTWlMQ0owZVhCbElqb2lSVkZWUVV4VElpd2lkbUZzZFdVaU9pSkVORE11TWlKOUxIc2lhMlY1SWpvaVpHbGhaMjV2YzJseklpd2lkSGx3WlNJNklrVlJWVUZNVXlJc0luWmhiSFZsSWpvaVJEUXpMakFpZlN4N0ltdGxlU0k2SW1ScFlXZHViM05wY3lJc0luUjVjR1VpT2lKRlVWVkJURk1pTENKMllXeDFaU0k2SWtRME15NHpJbjBzZXlKclpYa2lPaUprYVdGbmJtOXphWE1pTENKMGVYQmxJam9pUlZGVlFVeFRJaXdpZG1Gc2RXVWlPaUpFTkRNdU5DSjlMSHNpYTJWNUlqb2laR2xoWjI1dmMybHpJaXdpZEhsd1pTSTZJa1ZSVlVGTVV5SXNJblpoYkhWbElqb2lSRFF6TGpjaWZTeDdJbXRsZVNJNkltUnBZV2R1YjNOcGN5SXNJblI1Y0dVaU9pSkZVVlZCVEZNaUxDSjJZV3gxWlNJNklrUTBNeTQ1SW4xZGZWMTlMSHNpYjNCbGNtRnVaQ0k2SWs5U0lpd2lZMmhwYkdSeVpXNGlPbHQ3SW10bGVTSTZJbTF2Y25Cb2IyeHZaM2tpTENKMGVYQmxJam9pUlZGVlFVeFRJaXdpZG1Gc2RXVWlPaUk1TXpnekx6RWlmU3g3SW10bGVTSTZJbTF2Y25Cb2IyeHZaM2tpTENKMGVYQmxJam9pUlZGVlFVeFRJaXdpZG1Gc2RXVWlPaUk1TXpnMEx6RWlmU3g3SW10bGVTSTZJbTF2Y25Cb2IyeHZaM2tpTENKMGVYQmxJam9pUlZGVlFVeFRJaXdpZG1Gc2RXVWlPaUk1TXprMEx6RWlmU3g3SW10bGVTSTZJbTF2Y25Cb2IyeHZaM2tpTENKMGVYQmxJam9pUlZGVlFVeFRJaXdpZG1Gc2RXVWlPaUk1TkRJeEx6RWlmVjE5WFgxZGZWMTlYWDBzSW1sa0lqb2lZelJqWkRRNVpqVXRZV1V5T0MwMFl6TmxMV0V4WVRjdE5XTXpNamM1TnpabVlqbGxJbjA9In0=\",\r\n \"explorer-ids\": \"lens-dktk-test,lens-freiburg,lens-berlin,lens-dresden\",\r\n \"query-format\": \"AST_DATA\",\r\n \"human-readable\": \"(((diagnosis EQUALS D43) OR (diagnosis EQUALS D43.1) OR (diagnosis EQUALS D43.2) OR (diagnosis EQUALS D43.0) OR (diagnosis EQUALS D43.3) OR (diagnosis EQUALS D43.4) OR (diagnosis EQUALS D43.7) OR (diagnosis EQUALS D43.9)) AND ((morphology EQUALS 9383/1) OR (morphology EQUALS 9384/1) OR (morphology EQUALS 9394/1) OR (morphology EQUALS 9421/1)))\",\r\n \"explorer-url\": \"https://demo.lens.samply.de/?collections=&query=W1t7ImlkIjoiMDVlOTVlODEtMGZiYi00ZDczLWE1YzgtZDYyOTg3ZmYzMzg5Iiwia2V5IjoiZ2xpb21fYWxsX2dyb3VwcyIsIm5hbWUiOiJHbGlvbWUsIGFsbGUgR3J1cHBlbiIsInR5cGUiOiJFUVVBTFMiLCJ2YWx1ZXMiOlt7Im5hbWUiOiJHbGlvbSAtIEdyYWQgSSIsInZhbHVlIjpbW3sidmFsdWUiOiJkaWFnbm9zaXMiLCJuYW1lIjoiRDQzLiUifV0sW3sidmFsdWUiOiJtb3JwaG9sb2d5IiwibmFtZSI6IjkzODMvMSJ9LHsidmFsdWUiOiJtb3JwaG9sb2d5IiwibmFtZSI6IjkzODQvMSJ9LHsidmFsdWUiOiJtb3JwaG9sb2d5IiwibmFtZSI6IjkzOTQvMSJ9LHsidmFsdWUiOiJtb3JwaG9sb2d5IiwibmFtZSI6Ijk0MjEvMSJ9XV0sInF1ZXJ5QmluZElkIjoiM2FiMTM1ZjctYzI3NC00YWFlLWI5YjQtYmFhNDc2OWRkZjcxIn1dfV1d\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://requests.dktk.dkfz.de/project-manager-api/create-query-and-design-project", + "protocol": "https", + "host": [ + "requests", + "dktk", + "dkfz", + "de" + ], + "path": [ + "project-manager-api", + "create-query-and-design-project" + ] + } + }, + "response": [] + }, + { + "name": "Lens: Create Query", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJnbUlXWjB3MmpjZ2h0UGphUnBRaXYxWVNlOGU0cVVMem15YVVlTmdUa1VFIn0.eyJleHAiOjE3MTIwNTYxOTIsImlhdCI6MTcxMjA1NTg5MiwianRpIjoiOTE4MjBkMDItNmI0ZS00NDQxLWFjNGEtOTEzNDk5Zjg1NWFjIiwiaXNzIjoiaHR0cHM6Ly9sb2dpbi52ZXJiaXMuZGtmei5kZS9yZWFsbXMvdGVzdC1yZWFsbS0wMSIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiIyM2VmYTJiMS1iM2NhLTQ3ZWQtYTI2My03ZDUxNjA5OTA1YTgiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJicmlkZ2VoZWFkLXRlc3QiLCJzZXNzaW9uX3N0YXRlIjoiYzJiNzZhMmUtMmE0Ny00ZjE2LTk4OGEtYTBlN2RmOGZhYjE4IiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwOi8vbG9jYWxob3N0Ojg0ODAiLCJodHRwOi8vbG9jYWxob3N0OjgwOTciLCJodHRwczovL2xvY2FsaG9zdDo4MDk3IiwiaHR0cHM6Ly9sb2NhbGhvc3Q6ODA5OSIsImh0dHA6Ly9sb2NhbGhvc3Q6ODc4NyIsImh0dHA6Ly9sb2NhbGhvc3Q6ODA5OSIsImh0dHA6Ly9sb2NhbGhvc3Q6OTAwMCJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy10ZXN0LXJlYWxtLTAxIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCBvcGVuaWQgYWRtaW4iLCJzaWQiOiJjMmI3NmEyZS0yYTQ3LTRmMTYtOTg4YS1hMGU3ZGY4ZmFiMTgiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiZ3JvdXBzMiI6WyIvREtUS19DQ1BfSnVyYXNzaWMtcGFyayIsIi9EYXRhU0hJRUxEIl0sIm5hbWUiOiJBbGFuIEdyYW50IiwiZ3JvdXBzIjpbIkRLVEtfQ0NQX0p1cmFzc2ljLXBhcmsiLCJEYXRhU0hJRUxEIl0sInByZWZlcnJlZF91c2VybmFtZSI6InJlc2VhcmNoZXIiLCJnaXZlbl9uYW1lIjoiQWxhbiIsImZhbWlseV9uYW1lIjoiR3JhbnQiLCJlbWFpbCI6InJlc2VhcmNoZXJAdGVzdC5ka2Z6LmRlIn0.vhN2IWS9bVv03JRR0uuNDuCjBI5foE8WaSWRl1CsiKyB2EhOlpYz1H8vkQBTi8ZCEEVrBFdOkKNm1d0pmL0NpTSmfWEXtDrpjNTL70BSHfDwJ2UyU9sO-WiDor3eNQ3oAOp60x7ETb0TUKaFQSThsx6AchxlPR4oxMrsG1bsOfLdhsgQKqedEqHKKuJ3BhqwaUja41-rlTTHfvrJas5fu1WTBZXKAJuOk5osTN4Mc0QaF1rZfvoerzP_lNCzNeODasIzG9pvdv3WFVn3srfzEJbOhwwSBbNlYNMXPXdmHE5rB36tUrri7HAc2wc-90BmRPD5rQbPbwJfMOG75fm16Q", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{ \"query\": \"eyJsYW5nIjoiY3FsIiwibGliIjp7InJlc291cmNlVHlwZSI6IkxpYnJhcnkiLCJ1cmwiOiJ1cm46dXVpZDpiOTRkMTAwYy0zN2NkLTRiZDAtODgyNy01MzJiODRiZWE2NDgiLCJzdGF0dXMiOiJhY3RpdmUiLCJ0eXBlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9saWJyYXJ5LXR5cGUiLCJjb2RlIjoibG9naWMtbGlicmFyeSJ9XX0sImNvbnRlbnQiOlt7ImNvbnRlbnRUeXBlIjoidGV4dC9jcWwiLCJkYXRhIjoiYkdsaWNtRnllU0JTWlhSeWFXVjJaUXAxYzJsdVp5QkdTRWxTSUhabGNuTnBiMjRnSnpRdU1DNHdKd3BwYm1Oc2RXUmxJRVpJU1ZKSVpXeHdaWEp6SUhabGNuTnBiMjRnSnpRdU1DNHdKd29LWTI5a1pYTjVjM1JsYlNCc2IybHVZem9nSjJoMGRIQTZMeTlzYjJsdVl5NXZjbWNuQ2dwamIyNTBaWGgwSUZCaGRHbGxiblFLQ2tSTFZFdGZVMVJTUVZSZlIwVk9SRVZTWDFOVVVrRlVTVVpKUlZJS0NrUkxWRXRmVTFSU1FWUmZRVWRGWDFOVVVrRlVTVVpKUlZJS0NrUkxWRXRmVTFSU1FWUmZSRVZEUlVGVFJVUmZVMVJTUVZSSlJrbEZVZ29LUkV0VVMxOVRWRkpCVkY5RVNVRkhUazlUU1ZOZlUxUlNRVlJKUmtsRlVnb0tSRXRVUzE5VFZGSkJWRjlUVUVWRFNVMUZUbDlUVkZKQlZFbEdTVVZTQ2dwRVMxUkxYMU5VVWtGVVgxQlNUME5GUkZWU1JWOVRWRkpCVkVsR1NVVlNDZ3BFUzFSTFgxTlVVa0ZVWDAxRlJFbERRVlJKVDA1ZlUxUlNRVlJKUmtsRlVncEVTMVJMWDFOVVVrRlVYMFJGUmw5SlRsOUpUa2xVU1VGTVgxQlBVRlZNUVZSSlQwNVFZWFJwWlc1MExtZGxibVJsY2lBOUlDZHRZV3hsSnc9PSJ9XX0sIm1lYXN1cmUiOnsicmVzb3VyY2VUeXBlIjoiTWVhc3VyZSIsInVybCI6InVybjp1dWlkOmFjMDQwNmMxLTBlYjEtNDU4YS05ZmE4LTQ3MTZiZjhmMTBhOCIsInN0YXR1cyI6ImFjdGl2ZSIsInN1YmplY3RDb2RlYWJsZUNvbmNlcHQiOnsiY29kaW5nIjpbeyJzeXN0ZW0iOiJodHRwOi8vaGw3Lm9yZy9maGlyL3Jlc291cmNlLXR5cGVzIiwiY29kZSI6IlBhdGllbnQifV19LCJsaWJyYXJ5IjoidXJuOnV1aWQ6Yjk0ZDEwMGMtMzdjZC00YmQwLTg4MjctNTMyYjg0YmVhNjQ4Iiwic2NvcmluZyI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1zY29yaW5nIiwiY29kZSI6ImNvaG9ydCJ9XX0sImdyb3VwIjpbeyJjb2RlIjp7InRleHQiOiJwYXRpZW50cyJ9LCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6IkluSW5pdGlhbFBvcHVsYXRpb24ifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJHZW5kZXIifSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbCIsImV4cHJlc3Npb24iOiJHZW5kZXIifX0seyJjb2RlIjp7InRleHQiOiI3NTE4Ni03In0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiRGVjZWFzZWQifX0seyJjb2RlIjp7InRleHQiOiJBZ2UifSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbCIsImV4cHJlc3Npb24iOiJBZ2VDbGFzcyJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoiZGlhZ25vc2lzIn0sImV4dGVuc2lvbiI6W3sidXJsIjoiaHR0cDovL2hsNy5vcmcvZmhpci91cy9jcWZtZWFzdXJlcy9TdHJ1Y3R1cmVEZWZpbml0aW9uL2NxZm0tcG9wdWxhdGlvbkJhc2lzIiwidmFsdWVDb2RlIjoiQ29uZGl0aW9uIn1dLCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6IkRpYWdub3NpcyJ9fV0sInN0cmF0aWZpZXIiOlt7ImNvZGUiOnsidGV4dCI6ImRpYWdub3NpcyJ9LCJjcml0ZXJpYSI6eyJsYW5ndWFnZSI6InRleHQvY3FsLWlkZW50aWZpZXIiLCJleHByZXNzaW9uIjoiRGlhZ25vc2lzQ29kZSJ9fV19LHsiY29kZSI6eyJ0ZXh0Ijoic3BlY2ltZW4ifSwiZXh0ZW5zaW9uIjpbeyJ1cmwiOiJodHRwOi8vaGw3Lm9yZy9maGlyL3VzL2NxZm1lYXN1cmVzL1N0cnVjdHVyZURlZmluaXRpb24vY3FmbS1wb3B1bGF0aW9uQmFzaXMiLCJ2YWx1ZUNvZGUiOiJTcGVjaW1lbiJ9XSwicG9wdWxhdGlvbiI6W3siY29kZSI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1wb3B1bGF0aW9uIiwiY29kZSI6ImluaXRpYWwtcG9wdWxhdGlvbiJ9XX0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwtaWRlbnRpZmllciIsImV4cHJlc3Npb24iOiJTcGVjaW1lbiJ9fV0sInN0cmF0aWZpZXIiOlt7ImNvZGUiOnsidGV4dCI6InNhbXBsZV9raW5kIn0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiU2FtcGxlVHlwZSJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoicHJvY2VkdXJlcyJ9LCJleHRlbnNpb24iOlt7InVybCI6Imh0dHA6Ly9obDcub3JnL2ZoaXIvdXMvY3FmbWVhc3VyZXMvU3RydWN0dXJlRGVmaW5pdGlvbi9jcWZtLXBvcHVsYXRpb25CYXNpcyIsInZhbHVlQ29kZSI6IlByb2NlZHVyZSJ9XSwicG9wdWxhdGlvbiI6W3siY29kZSI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1wb3B1bGF0aW9uIiwiY29kZSI6ImluaXRpYWwtcG9wdWxhdGlvbiJ9XX0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwtaWRlbnRpZmllciIsImV4cHJlc3Npb24iOiJQcm9jZWR1cmUifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJQcm9jZWR1cmVUeXBlIn0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiUHJvY2VkdXJlVHlwZSJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoibWVkaWNhdGlvblN0YXRlbWVudHMifSwiZXh0ZW5zaW9uIjpbeyJ1cmwiOiJodHRwOi8vaGw3Lm9yZy9maGlyL3VzL2NxZm1lYXN1cmVzL1N0cnVjdHVyZURlZmluaXRpb24vY3FmbS1wb3B1bGF0aW9uQmFzaXMiLCJ2YWx1ZUNvZGUiOiJNZWRpY2F0aW9uU3RhdGVtZW50In1dLCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6Ik1lZGljYXRpb25TdGF0ZW1lbnQifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJNZWRpY2F0aW9uVHlwZSJ9LCJjcml0ZXJpYSI6eyJsYW5ndWFnZSI6InRleHQvY3FsIiwiZXhwcmVzc2lvbiI6IlByb2NlZHVyZVR5cGUifX1dfV19fQ==\" }", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8097/create-query-and-design-project?query-format=CQL_DATA&human-readable=(gender = male)&explorer-ids=lens-star-wars,lens-jurassic-park&explorer-url=https://data.dktk.dkfz.de/redirect/12345", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "create-query-and-design-project" + ], + "query": [ + { + "key": "query-format", + "value": "CQL_DATA" + }, + { + "key": "human-readable", + "value": "(gender = male)" + }, + { + "key": "explorer-ids", + "value": "lens-star-wars,lens-jurassic-park" + }, + { + "key": "explorer-url", + "value": "https://data.dktk.dkfz.de/redirect/12345" + } + ] + } + }, + "response": [] + }, + { + "name": "Create query and design project with explorerIds (Researcher)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{ \"query\": \"eyJsYW5nIjoiY3FsIiwibGliIjp7InJlc291cmNlVHlwZSI6IkxpYnJhcnkiLCJ1cmwiOiJ1cm46dXVpZDpiOTRkMTAwYy0zN2NkLTRiZDAtODgyNy01MzJiODRiZWE2NDgiLCJzdGF0dXMiOiJhY3RpdmUiLCJ0eXBlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9saWJyYXJ5LXR5cGUiLCJjb2RlIjoibG9naWMtbGlicmFyeSJ9XX0sImNvbnRlbnQiOlt7ImNvbnRlbnRUeXBlIjoidGV4dC9jcWwiLCJkYXRhIjoiYkdsaWNtRnllU0JTWlhSeWFXVjJaUXAxYzJsdVp5QkdTRWxTSUhabGNuTnBiMjRnSnpRdU1DNHdKd3BwYm1Oc2RXUmxJRVpJU1ZKSVpXeHdaWEp6SUhabGNuTnBiMjRnSnpRdU1DNHdKd29LWTI5a1pYTjVjM1JsYlNCc2IybHVZem9nSjJoMGRIQTZMeTlzYjJsdVl5NXZjbWNuQ2dwamIyNTBaWGgwSUZCaGRHbGxiblFLQ2tSTFZFdGZVMVJTUVZSZlIwVk9SRVZTWDFOVVVrRlVTVVpKUlZJS0NrUkxWRXRmVTFSU1FWUmZRVWRGWDFOVVVrRlVTVVpKUlZJS0NrUkxWRXRmVTFSU1FWUmZSRVZEUlVGVFJVUmZVMVJTUVZSSlJrbEZVZ29LUkV0VVMxOVRWRkpCVkY5RVNVRkhUazlUU1ZOZlUxUlNRVlJKUmtsRlVnb0tSRXRVUzE5VFZGSkJWRjlUVUVWRFNVMUZUbDlUVkZKQlZFbEdTVVZTQ2dwRVMxUkxYMU5VVWtGVVgxQlNUME5GUkZWU1JWOVRWRkpCVkVsR1NVVlNDZ3BFUzFSTFgxTlVVa0ZVWDAxRlJFbERRVlJKVDA1ZlUxUlNRVlJKUmtsRlVncEVTMVJMWDFOVVVrRlVYMFJGUmw5SlRsOUpUa2xVU1VGTVgxQlBVRlZNUVZSSlQwNVFZWFJwWlc1MExtZGxibVJsY2lBOUlDZHRZV3hsSnc9PSJ9XX0sIm1lYXN1cmUiOnsicmVzb3VyY2VUeXBlIjoiTWVhc3VyZSIsInVybCI6InVybjp1dWlkOmFjMDQwNmMxLTBlYjEtNDU4YS05ZmE4LTQ3MTZiZjhmMTBhOCIsInN0YXR1cyI6ImFjdGl2ZSIsInN1YmplY3RDb2RlYWJsZUNvbmNlcHQiOnsiY29kaW5nIjpbeyJzeXN0ZW0iOiJodHRwOi8vaGw3Lm9yZy9maGlyL3Jlc291cmNlLXR5cGVzIiwiY29kZSI6IlBhdGllbnQifV19LCJsaWJyYXJ5IjoidXJuOnV1aWQ6Yjk0ZDEwMGMtMzdjZC00YmQwLTg4MjctNTMyYjg0YmVhNjQ4Iiwic2NvcmluZyI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1zY29yaW5nIiwiY29kZSI6ImNvaG9ydCJ9XX0sImdyb3VwIjpbeyJjb2RlIjp7InRleHQiOiJwYXRpZW50cyJ9LCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6IkluSW5pdGlhbFBvcHVsYXRpb24ifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJHZW5kZXIifSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbCIsImV4cHJlc3Npb24iOiJHZW5kZXIifX0seyJjb2RlIjp7InRleHQiOiI3NTE4Ni03In0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiRGVjZWFzZWQifX0seyJjb2RlIjp7InRleHQiOiJBZ2UifSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbCIsImV4cHJlc3Npb24iOiJBZ2VDbGFzcyJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoiZGlhZ25vc2lzIn0sImV4dGVuc2lvbiI6W3sidXJsIjoiaHR0cDovL2hsNy5vcmcvZmhpci91cy9jcWZtZWFzdXJlcy9TdHJ1Y3R1cmVEZWZpbml0aW9uL2NxZm0tcG9wdWxhdGlvbkJhc2lzIiwidmFsdWVDb2RlIjoiQ29uZGl0aW9uIn1dLCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6IkRpYWdub3NpcyJ9fV0sInN0cmF0aWZpZXIiOlt7ImNvZGUiOnsidGV4dCI6ImRpYWdub3NpcyJ9LCJjcml0ZXJpYSI6eyJsYW5ndWFnZSI6InRleHQvY3FsLWlkZW50aWZpZXIiLCJleHByZXNzaW9uIjoiRGlhZ25vc2lzQ29kZSJ9fV19LHsiY29kZSI6eyJ0ZXh0Ijoic3BlY2ltZW4ifSwiZXh0ZW5zaW9uIjpbeyJ1cmwiOiJodHRwOi8vaGw3Lm9yZy9maGlyL3VzL2NxZm1lYXN1cmVzL1N0cnVjdHVyZURlZmluaXRpb24vY3FmbS1wb3B1bGF0aW9uQmFzaXMiLCJ2YWx1ZUNvZGUiOiJTcGVjaW1lbiJ9XSwicG9wdWxhdGlvbiI6W3siY29kZSI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1wb3B1bGF0aW9uIiwiY29kZSI6ImluaXRpYWwtcG9wdWxhdGlvbiJ9XX0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwtaWRlbnRpZmllciIsImV4cHJlc3Npb24iOiJTcGVjaW1lbiJ9fV0sInN0cmF0aWZpZXIiOlt7ImNvZGUiOnsidGV4dCI6InNhbXBsZV9raW5kIn0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiU2FtcGxlVHlwZSJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoicHJvY2VkdXJlcyJ9LCJleHRlbnNpb24iOlt7InVybCI6Imh0dHA6Ly9obDcub3JnL2ZoaXIvdXMvY3FmbWVhc3VyZXMvU3RydWN0dXJlRGVmaW5pdGlvbi9jcWZtLXBvcHVsYXRpb25CYXNpcyIsInZhbHVlQ29kZSI6IlByb2NlZHVyZSJ9XSwicG9wdWxhdGlvbiI6W3siY29kZSI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1wb3B1bGF0aW9uIiwiY29kZSI6ImluaXRpYWwtcG9wdWxhdGlvbiJ9XX0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwtaWRlbnRpZmllciIsImV4cHJlc3Npb24iOiJQcm9jZWR1cmUifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJQcm9jZWR1cmVUeXBlIn0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiUHJvY2VkdXJlVHlwZSJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoibWVkaWNhdGlvblN0YXRlbWVudHMifSwiZXh0ZW5zaW9uIjpbeyJ1cmwiOiJodHRwOi8vaGw3Lm9yZy9maGlyL3VzL2NxZm1lYXN1cmVzL1N0cnVjdHVyZURlZmluaXRpb24vY3FmbS1wb3B1bGF0aW9uQmFzaXMiLCJ2YWx1ZUNvZGUiOiJNZWRpY2F0aW9uU3RhdGVtZW50In1dLCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6Ik1lZGljYXRpb25TdGF0ZW1lbnQifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJNZWRpY2F0aW9uVHlwZSJ9LCJjcml0ZXJpYSI6eyJsYW5ndWFnZSI6InRleHQvY3FsIiwiZXhwcmVzc2lvbiI6IlByb2NlZHVyZVR5cGUifX1dfV19fQ==\" }", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8097/create-query-and-design-project?label=Test&description=This is a test query for CCP&template-id=opal-ccp&output-format=OPAL&query-format=CQL_DATA&human-readable=(gender = male)&explorer-ids=lens-star-wars,lens-jurassic-park&explorer-url=https://data.dktk.dkfz.de/redirect/12345&project-type=DATASHIELD", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "create-query-and-design-project" + ], + "query": [ + { + "key": "label", + "value": "Test" + }, + { + "key": "description", + "value": "This is a test query for CCP" + }, + { + "key": "template-id", + "value": "opal-ccp" + }, + { + "key": "output-format", + "value": "OPAL" + }, + { + "key": "query-format", + "value": "CQL_DATA" + }, + { + "key": "human-readable", + "value": "(gender = male)" + }, + { + "key": "explorer-ids", + "value": "lens-star-wars,lens-jurassic-park" + }, + { + "key": "explorer-url", + "value": "https://data.dktk.dkfz.de/redirect/12345" + }, + { + "key": "project-type", + "value": "DATASHIELD" + } + ] + } + }, + "response": [] + }, + { + "name": "Edit query and project (Researcher)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{ \"query\": \"eyJsYW5nIjoiY3FsIiwibGliIjp7InJlc291cmNlVHlwZSI6IkxpYnJhcnkiLCJ1cmwiOiJ1cm46dXVpZDpiOTRkMTAwYy0zN2NkLTRiZDAtODgyNy01MzJiODRiZWE2NDgiLCJzdGF0dXMiOiJhY3RpdmUiLCJ0eXBlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9saWJyYXJ5LXR5cGUiLCJjb2RlIjoibG9naWMtbGlicmFyeSJ9XX0sImNvbnRlbnQiOlt7ImNvbnRlbnRUeXBlIjoidGV4dC9jcWwiLCJkYXRhIjoiYkdsaWNtRnllU0JTWlhSeWFXVjJaUXAxYzJsdVp5QkdTRWxTSUhabGNuTnBiMjRnSnpRdU1DNHdKd3BwYm1Oc2RXUmxJRVpJU1ZKSVpXeHdaWEp6SUhabGNuTnBiMjRnSnpRdU1DNHdKd29LWTI5a1pYTjVjM1JsYlNCc2IybHVZem9nSjJoMGRIQTZMeTlzYjJsdVl5NXZjbWNuQ2dwamIyNTBaWGgwSUZCaGRHbGxiblFLQ2tSTFZFdGZVMVJTUVZSZlIwVk9SRVZTWDFOVVVrRlVTVVpKUlZJS0NrUkxWRXRmVTFSU1FWUmZRVWRGWDFOVVVrRlVTVVpKUlZJS0NrUkxWRXRmVTFSU1FWUmZSRVZEUlVGVFJVUmZVMVJTUVZSSlJrbEZVZ29LUkV0VVMxOVRWRkpCVkY5RVNVRkhUazlUU1ZOZlUxUlNRVlJKUmtsRlVnb0tSRXRVUzE5VFZGSkJWRjlUVUVWRFNVMUZUbDlUVkZKQlZFbEdTVVZTQ2dwRVMxUkxYMU5VVWtGVVgxQlNUME5GUkZWU1JWOVRWRkpCVkVsR1NVVlNDZ3BFUzFSTFgxTlVVa0ZVWDAxRlJFbERRVlJKVDA1ZlUxUlNRVlJKUmtsRlVncEVTMVJMWDFOVVVrRlVYMFJGUmw5SlRsOUpUa2xVU1VGTVgxQlBVRlZNUVZSSlQwNVFZWFJwWlc1MExtZGxibVJsY2lBOUlDZHRZV3hsSnc9PSJ9XX0sIm1lYXN1cmUiOnsicmVzb3VyY2VUeXBlIjoiTWVhc3VyZSIsInVybCI6InVybjp1dWlkOmFjMDQwNmMxLTBlYjEtNDU4YS05ZmE4LTQ3MTZiZjhmMTBhOCIsInN0YXR1cyI6ImFjdGl2ZSIsInN1YmplY3RDb2RlYWJsZUNvbmNlcHQiOnsiY29kaW5nIjpbeyJzeXN0ZW0iOiJodHRwOi8vaGw3Lm9yZy9maGlyL3Jlc291cmNlLXR5cGVzIiwiY29kZSI6IlBhdGllbnQifV19LCJsaWJyYXJ5IjoidXJuOnV1aWQ6Yjk0ZDEwMGMtMzdjZC00YmQwLTg4MjctNTMyYjg0YmVhNjQ4Iiwic2NvcmluZyI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1zY29yaW5nIiwiY29kZSI6ImNvaG9ydCJ9XX0sImdyb3VwIjpbeyJjb2RlIjp7InRleHQiOiJwYXRpZW50cyJ9LCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6IkluSW5pdGlhbFBvcHVsYXRpb24ifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJHZW5kZXIifSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbCIsImV4cHJlc3Npb24iOiJHZW5kZXIifX0seyJjb2RlIjp7InRleHQiOiI3NTE4Ni03In0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiRGVjZWFzZWQifX0seyJjb2RlIjp7InRleHQiOiJBZ2UifSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbCIsImV4cHJlc3Npb24iOiJBZ2VDbGFzcyJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoiZGlhZ25vc2lzIn0sImV4dGVuc2lvbiI6W3sidXJsIjoiaHR0cDovL2hsNy5vcmcvZmhpci91cy9jcWZtZWFzdXJlcy9TdHJ1Y3R1cmVEZWZpbml0aW9uL2NxZm0tcG9wdWxhdGlvbkJhc2lzIiwidmFsdWVDb2RlIjoiQ29uZGl0aW9uIn1dLCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6IkRpYWdub3NpcyJ9fV0sInN0cmF0aWZpZXIiOlt7ImNvZGUiOnsidGV4dCI6ImRpYWdub3NpcyJ9LCJjcml0ZXJpYSI6eyJsYW5ndWFnZSI6InRleHQvY3FsLWlkZW50aWZpZXIiLCJleHByZXNzaW9uIjoiRGlhZ25vc2lzQ29kZSJ9fV19LHsiY29kZSI6eyJ0ZXh0Ijoic3BlY2ltZW4ifSwiZXh0ZW5zaW9uIjpbeyJ1cmwiOiJodHRwOi8vaGw3Lm9yZy9maGlyL3VzL2NxZm1lYXN1cmVzL1N0cnVjdHVyZURlZmluaXRpb24vY3FmbS1wb3B1bGF0aW9uQmFzaXMiLCJ2YWx1ZUNvZGUiOiJTcGVjaW1lbiJ9XSwicG9wdWxhdGlvbiI6W3siY29kZSI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1wb3B1bGF0aW9uIiwiY29kZSI6ImluaXRpYWwtcG9wdWxhdGlvbiJ9XX0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwtaWRlbnRpZmllciIsImV4cHJlc3Npb24iOiJTcGVjaW1lbiJ9fV0sInN0cmF0aWZpZXIiOlt7ImNvZGUiOnsidGV4dCI6InNhbXBsZV9raW5kIn0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiU2FtcGxlVHlwZSJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoicHJvY2VkdXJlcyJ9LCJleHRlbnNpb24iOlt7InVybCI6Imh0dHA6Ly9obDcub3JnL2ZoaXIvdXMvY3FmbWVhc3VyZXMvU3RydWN0dXJlRGVmaW5pdGlvbi9jcWZtLXBvcHVsYXRpb25CYXNpcyIsInZhbHVlQ29kZSI6IlByb2NlZHVyZSJ9XSwicG9wdWxhdGlvbiI6W3siY29kZSI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1wb3B1bGF0aW9uIiwiY29kZSI6ImluaXRpYWwtcG9wdWxhdGlvbiJ9XX0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwtaWRlbnRpZmllciIsImV4cHJlc3Npb24iOiJQcm9jZWR1cmUifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJQcm9jZWR1cmVUeXBlIn0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiUHJvY2VkdXJlVHlwZSJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoibWVkaWNhdGlvblN0YXRlbWVudHMifSwiZXh0ZW5zaW9uIjpbeyJ1cmwiOiJodHRwOi8vaGw3Lm9yZy9maGlyL3VzL2NxZm1lYXN1cmVzL1N0cnVjdHVyZURlZmluaXRpb24vY3FmbS1wb3B1bGF0aW9uQmFzaXMiLCJ2YWx1ZUNvZGUiOiJNZWRpY2F0aW9uU3RhdGVtZW50In1dLCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6Ik1lZGljYXRpb25TdGF0ZW1lbnQifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJNZWRpY2F0aW9uVHlwZSJ9LCJjcml0ZXJpYSI6eyJsYW5ndWFnZSI6InRleHQvY3FsIiwiZXhwcmVzc2lvbiI6IlByb2NlZHVyZVR5cGUifX1dfV19fQ==\" }", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8097/project/edit?label=Test2 changed&description=I could change the query&template-id=opal-ccp&output-format=OPAL&human-readable=(gender = male) and (vitalstatus = lebend)&bridgeheads=jurassic-park,frankfurt&explorer-url=https://data.dktk.dkfz.de/redirect/12345&project-code=2dfcd1bf620241f9b884&project-type=DATASHIELD", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "edit" + ], + "query": [ + { + "key": "label", + "value": "Test2 changed" + }, + { + "key": "description", + "value": "I could change the query" + }, + { + "key": "template-id", + "value": "opal-ccp" + }, + { + "key": "output-format", + "value": "OPAL" + }, + { + "key": "query-format", + "value": "FHIR_SEARCH", + "disabled": true + }, + { + "key": "human-readable", + "value": "(gender = male) and (vitalstatus = lebend)" + }, + { + "key": "bridgeheads", + "value": "jurassic-park,frankfurt" + }, + { + "key": "explorer-url", + "value": "https://data.dktk.dkfz.de/redirect/12345" + }, + { + "key": "project-code", + "value": "2dfcd1bf620241f9b884" + }, + { + "key": "project-type", + "value": "DATASHIELD" + } + ] + } + }, + "response": [] + }, + { + "name": "Edit query and project with explorer ids (Researcher)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{ \"query\": \"eyJsYW5nIjoiY3FsIiwibGliIjp7InJlc291cmNlVHlwZSI6IkxpYnJhcnkiLCJ1cmwiOiJ1cm46dXVpZDpiOTRkMTAwYy0zN2NkLTRiZDAtODgyNy01MzJiODRiZWE2NDgiLCJzdGF0dXMiOiJhY3RpdmUiLCJ0eXBlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9saWJyYXJ5LXR5cGUiLCJjb2RlIjoibG9naWMtbGlicmFyeSJ9XX0sImNvbnRlbnQiOlt7ImNvbnRlbnRUeXBlIjoidGV4dC9jcWwiLCJkYXRhIjoiYkdsaWNtRnllU0JTWlhSeWFXVjJaUXAxYzJsdVp5QkdTRWxTSUhabGNuTnBiMjRnSnpRdU1DNHdKd3BwYm1Oc2RXUmxJRVpJU1ZKSVpXeHdaWEp6SUhabGNuTnBiMjRnSnpRdU1DNHdKd29LWTI5a1pYTjVjM1JsYlNCc2IybHVZem9nSjJoMGRIQTZMeTlzYjJsdVl5NXZjbWNuQ2dwamIyNTBaWGgwSUZCaGRHbGxiblFLQ2tSTFZFdGZVMVJTUVZSZlIwVk9SRVZTWDFOVVVrRlVTVVpKUlZJS0NrUkxWRXRmVTFSU1FWUmZRVWRGWDFOVVVrRlVTVVpKUlZJS0NrUkxWRXRmVTFSU1FWUmZSRVZEUlVGVFJVUmZVMVJTUVZSSlJrbEZVZ29LUkV0VVMxOVRWRkpCVkY5RVNVRkhUazlUU1ZOZlUxUlNRVlJKUmtsRlVnb0tSRXRVUzE5VFZGSkJWRjlUVUVWRFNVMUZUbDlUVkZKQlZFbEdTVVZTQ2dwRVMxUkxYMU5VVWtGVVgxQlNUME5GUkZWU1JWOVRWRkpCVkVsR1NVVlNDZ3BFUzFSTFgxTlVVa0ZVWDAxRlJFbERRVlJKVDA1ZlUxUlNRVlJKUmtsRlVncEVTMVJMWDFOVVVrRlVYMFJGUmw5SlRsOUpUa2xVU1VGTVgxQlBVRlZNUVZSSlQwNVFZWFJwWlc1MExtZGxibVJsY2lBOUlDZHRZV3hsSnc9PSJ9XX0sIm1lYXN1cmUiOnsicmVzb3VyY2VUeXBlIjoiTWVhc3VyZSIsInVybCI6InVybjp1dWlkOmFjMDQwNmMxLTBlYjEtNDU4YS05ZmE4LTQ3MTZiZjhmMTBhOCIsInN0YXR1cyI6ImFjdGl2ZSIsInN1YmplY3RDb2RlYWJsZUNvbmNlcHQiOnsiY29kaW5nIjpbeyJzeXN0ZW0iOiJodHRwOi8vaGw3Lm9yZy9maGlyL3Jlc291cmNlLXR5cGVzIiwiY29kZSI6IlBhdGllbnQifV19LCJsaWJyYXJ5IjoidXJuOnV1aWQ6Yjk0ZDEwMGMtMzdjZC00YmQwLTg4MjctNTMyYjg0YmVhNjQ4Iiwic2NvcmluZyI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1zY29yaW5nIiwiY29kZSI6ImNvaG9ydCJ9XX0sImdyb3VwIjpbeyJjb2RlIjp7InRleHQiOiJwYXRpZW50cyJ9LCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6IkluSW5pdGlhbFBvcHVsYXRpb24ifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJHZW5kZXIifSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbCIsImV4cHJlc3Npb24iOiJHZW5kZXIifX0seyJjb2RlIjp7InRleHQiOiI3NTE4Ni03In0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiRGVjZWFzZWQifX0seyJjb2RlIjp7InRleHQiOiJBZ2UifSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbCIsImV4cHJlc3Npb24iOiJBZ2VDbGFzcyJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoiZGlhZ25vc2lzIn0sImV4dGVuc2lvbiI6W3sidXJsIjoiaHR0cDovL2hsNy5vcmcvZmhpci91cy9jcWZtZWFzdXJlcy9TdHJ1Y3R1cmVEZWZpbml0aW9uL2NxZm0tcG9wdWxhdGlvbkJhc2lzIiwidmFsdWVDb2RlIjoiQ29uZGl0aW9uIn1dLCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6IkRpYWdub3NpcyJ9fV0sInN0cmF0aWZpZXIiOlt7ImNvZGUiOnsidGV4dCI6ImRpYWdub3NpcyJ9LCJjcml0ZXJpYSI6eyJsYW5ndWFnZSI6InRleHQvY3FsLWlkZW50aWZpZXIiLCJleHByZXNzaW9uIjoiRGlhZ25vc2lzQ29kZSJ9fV19LHsiY29kZSI6eyJ0ZXh0Ijoic3BlY2ltZW4ifSwiZXh0ZW5zaW9uIjpbeyJ1cmwiOiJodHRwOi8vaGw3Lm9yZy9maGlyL3VzL2NxZm1lYXN1cmVzL1N0cnVjdHVyZURlZmluaXRpb24vY3FmbS1wb3B1bGF0aW9uQmFzaXMiLCJ2YWx1ZUNvZGUiOiJTcGVjaW1lbiJ9XSwicG9wdWxhdGlvbiI6W3siY29kZSI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1wb3B1bGF0aW9uIiwiY29kZSI6ImluaXRpYWwtcG9wdWxhdGlvbiJ9XX0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwtaWRlbnRpZmllciIsImV4cHJlc3Npb24iOiJTcGVjaW1lbiJ9fV0sInN0cmF0aWZpZXIiOlt7ImNvZGUiOnsidGV4dCI6InNhbXBsZV9raW5kIn0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiU2FtcGxlVHlwZSJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoicHJvY2VkdXJlcyJ9LCJleHRlbnNpb24iOlt7InVybCI6Imh0dHA6Ly9obDcub3JnL2ZoaXIvdXMvY3FmbWVhc3VyZXMvU3RydWN0dXJlRGVmaW5pdGlvbi9jcWZtLXBvcHVsYXRpb25CYXNpcyIsInZhbHVlQ29kZSI6IlByb2NlZHVyZSJ9XSwicG9wdWxhdGlvbiI6W3siY29kZSI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1wb3B1bGF0aW9uIiwiY29kZSI6ImluaXRpYWwtcG9wdWxhdGlvbiJ9XX0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwtaWRlbnRpZmllciIsImV4cHJlc3Npb24iOiJQcm9jZWR1cmUifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJQcm9jZWR1cmVUeXBlIn0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiUHJvY2VkdXJlVHlwZSJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoibWVkaWNhdGlvblN0YXRlbWVudHMifSwiZXh0ZW5zaW9uIjpbeyJ1cmwiOiJodHRwOi8vaGw3Lm9yZy9maGlyL3VzL2NxZm1lYXN1cmVzL1N0cnVjdHVyZURlZmluaXRpb24vY3FmbS1wb3B1bGF0aW9uQmFzaXMiLCJ2YWx1ZUNvZGUiOiJNZWRpY2F0aW9uU3RhdGVtZW50In1dLCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6Ik1lZGljYXRpb25TdGF0ZW1lbnQifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJNZWRpY2F0aW9uVHlwZSJ9LCJjcml0ZXJpYSI6eyJsYW5ndWFnZSI6InRleHQvY3FsIiwiZXhwcmVzc2lvbiI6IlByb2NlZHVyZVR5cGUifX1dfV19fQ==\" }", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8097/project/edit?label=Test2 changed&description=I could change the query&template-id=opal-ccp&output-format=OPAL&human-readable=(gender = male) and (vitalstatus = lebend)&explorer-ids=lens-star-wars,lens-jurassic-park&explorer-url=https://data.dktk.dkfz.de/redirect/12345&project-code=2dfcd1bf620241f9b884&query-context=key1=value1;key2=value2&project-type=DATASHIELD", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "edit" + ], + "query": [ + { + "key": "label", + "value": "Test2 changed" + }, + { + "key": "description", + "value": "I could change the query" + }, + { + "key": "template-id", + "value": "opal-ccp" + }, + { + "key": "output-format", + "value": "OPAL" + }, + { + "key": "query-format", + "value": "FHIR_SEARCH", + "disabled": true + }, + { + "key": "human-readable", + "value": "(gender = male) and (vitalstatus = lebend)" + }, + { + "key": "explorer-ids", + "value": "lens-star-wars,lens-jurassic-park" + }, + { + "key": "explorer-url", + "value": "https://data.dktk.dkfz.de/redirect/12345" + }, + { + "key": "project-code", + "value": "2dfcd1bf620241f9b884" + }, + { + "key": "query-context", + "value": "key1=value1;key2=value2" + }, + { + "key": "project-type", + "value": "DATASHIELD" + } + ] + } + }, + "response": [] + }, + { + "name": "Remove Output", + "request": { + "auth": { + "type": "noauth" + }, + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{ \"query\": \"eyJsYW5nIjoiY3FsIiwibGliIjp7InJlc291cmNlVHlwZSI6IkxpYnJhcnkiLCJ1cmwiOiJ1cm46dXVpZDpiOTRkMTAwYy0zN2NkLTRiZDAtODgyNy01MzJiODRiZWE2NDgiLCJzdGF0dXMiOiJhY3RpdmUiLCJ0eXBlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9saWJyYXJ5LXR5cGUiLCJjb2RlIjoibG9naWMtbGlicmFyeSJ9XX0sImNvbnRlbnQiOlt7ImNvbnRlbnRUeXBlIjoidGV4dC9jcWwiLCJkYXRhIjoiYkdsaWNtRnllU0JTWlhSeWFXVjJaUXAxYzJsdVp5QkdTRWxTSUhabGNuTnBiMjRnSnpRdU1DNHdKd3BwYm1Oc2RXUmxJRVpJU1ZKSVpXeHdaWEp6SUhabGNuTnBiMjRnSnpRdU1DNHdKd29LWTI5a1pYTjVjM1JsYlNCc2IybHVZem9nSjJoMGRIQTZMeTlzYjJsdVl5NXZjbWNuQ2dwamIyNTBaWGgwSUZCaGRHbGxiblFLQ2tSTFZFdGZVMVJTUVZSZlIwVk9SRVZTWDFOVVVrRlVTVVpKUlZJS0NrUkxWRXRmVTFSU1FWUmZRVWRGWDFOVVVrRlVTVVpKUlZJS0NrUkxWRXRmVTFSU1FWUmZSRVZEUlVGVFJVUmZVMVJTUVZSSlJrbEZVZ29LUkV0VVMxOVRWRkpCVkY5RVNVRkhUazlUU1ZOZlUxUlNRVlJKUmtsRlVnb0tSRXRVUzE5VFZGSkJWRjlUVUVWRFNVMUZUbDlUVkZKQlZFbEdTVVZTQ2dwRVMxUkxYMU5VVWtGVVgxQlNUME5GUkZWU1JWOVRWRkpCVkVsR1NVVlNDZ3BFUzFSTFgxTlVVa0ZVWDAxRlJFbERRVlJKVDA1ZlUxUlNRVlJKUmtsRlVncEVTMVJMWDFOVVVrRlVYMFJGUmw5SlRsOUpUa2xVU1VGTVgxQlBVRlZNUVZSSlQwNVFZWFJwWlc1MExtZGxibVJsY2lBOUlDZHRZV3hsSnc9PSJ9XX0sIm1lYXN1cmUiOnsicmVzb3VyY2VUeXBlIjoiTWVhc3VyZSIsInVybCI6InVybjp1dWlkOmFjMDQwNmMxLTBlYjEtNDU4YS05ZmE4LTQ3MTZiZjhmMTBhOCIsInN0YXR1cyI6ImFjdGl2ZSIsInN1YmplY3RDb2RlYWJsZUNvbmNlcHQiOnsiY29kaW5nIjpbeyJzeXN0ZW0iOiJodHRwOi8vaGw3Lm9yZy9maGlyL3Jlc291cmNlLXR5cGVzIiwiY29kZSI6IlBhdGllbnQifV19LCJsaWJyYXJ5IjoidXJuOnV1aWQ6Yjk0ZDEwMGMtMzdjZC00YmQwLTg4MjctNTMyYjg0YmVhNjQ4Iiwic2NvcmluZyI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1zY29yaW5nIiwiY29kZSI6ImNvaG9ydCJ9XX0sImdyb3VwIjpbeyJjb2RlIjp7InRleHQiOiJwYXRpZW50cyJ9LCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6IkluSW5pdGlhbFBvcHVsYXRpb24ifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJHZW5kZXIifSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbCIsImV4cHJlc3Npb24iOiJHZW5kZXIifX0seyJjb2RlIjp7InRleHQiOiI3NTE4Ni03In0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiRGVjZWFzZWQifX0seyJjb2RlIjp7InRleHQiOiJBZ2UifSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbCIsImV4cHJlc3Npb24iOiJBZ2VDbGFzcyJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoiZGlhZ25vc2lzIn0sImV4dGVuc2lvbiI6W3sidXJsIjoiaHR0cDovL2hsNy5vcmcvZmhpci91cy9jcWZtZWFzdXJlcy9TdHJ1Y3R1cmVEZWZpbml0aW9uL2NxZm0tcG9wdWxhdGlvbkJhc2lzIiwidmFsdWVDb2RlIjoiQ29uZGl0aW9uIn1dLCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6IkRpYWdub3NpcyJ9fV0sInN0cmF0aWZpZXIiOlt7ImNvZGUiOnsidGV4dCI6ImRpYWdub3NpcyJ9LCJjcml0ZXJpYSI6eyJsYW5ndWFnZSI6InRleHQvY3FsLWlkZW50aWZpZXIiLCJleHByZXNzaW9uIjoiRGlhZ25vc2lzQ29kZSJ9fV19LHsiY29kZSI6eyJ0ZXh0Ijoic3BlY2ltZW4ifSwiZXh0ZW5zaW9uIjpbeyJ1cmwiOiJodHRwOi8vaGw3Lm9yZy9maGlyL3VzL2NxZm1lYXN1cmVzL1N0cnVjdHVyZURlZmluaXRpb24vY3FmbS1wb3B1bGF0aW9uQmFzaXMiLCJ2YWx1ZUNvZGUiOiJTcGVjaW1lbiJ9XSwicG9wdWxhdGlvbiI6W3siY29kZSI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1wb3B1bGF0aW9uIiwiY29kZSI6ImluaXRpYWwtcG9wdWxhdGlvbiJ9XX0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwtaWRlbnRpZmllciIsImV4cHJlc3Npb24iOiJTcGVjaW1lbiJ9fV0sInN0cmF0aWZpZXIiOlt7ImNvZGUiOnsidGV4dCI6InNhbXBsZV9raW5kIn0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiU2FtcGxlVHlwZSJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoicHJvY2VkdXJlcyJ9LCJleHRlbnNpb24iOlt7InVybCI6Imh0dHA6Ly9obDcub3JnL2ZoaXIvdXMvY3FmbWVhc3VyZXMvU3RydWN0dXJlRGVmaW5pdGlvbi9jcWZtLXBvcHVsYXRpb25CYXNpcyIsInZhbHVlQ29kZSI6IlByb2NlZHVyZSJ9XSwicG9wdWxhdGlvbiI6W3siY29kZSI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1wb3B1bGF0aW9uIiwiY29kZSI6ImluaXRpYWwtcG9wdWxhdGlvbiJ9XX0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwtaWRlbnRpZmllciIsImV4cHJlc3Npb24iOiJQcm9jZWR1cmUifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJQcm9jZWR1cmVUeXBlIn0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiUHJvY2VkdXJlVHlwZSJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoibWVkaWNhdGlvblN0YXRlbWVudHMifSwiZXh0ZW5zaW9uIjpbeyJ1cmwiOiJodHRwOi8vaGw3Lm9yZy9maGlyL3VzL2NxZm1lYXN1cmVzL1N0cnVjdHVyZURlZmluaXRpb24vY3FmbS1wb3B1bGF0aW9uQmFzaXMiLCJ2YWx1ZUNvZGUiOiJNZWRpY2F0aW9uU3RhdGVtZW50In1dLCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6Ik1lZGljYXRpb25TdGF0ZW1lbnQifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJNZWRpY2F0aW9uVHlwZSJ9LCJjcml0ZXJpYSI6eyJsYW5ndWFnZSI6InRleHQvY3FsIiwiZXhwcmVzc2lvbiI6IlByb2NlZHVyZVR5cGUifX1dfV19fQ==\" }", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8097/project/output?project-type=DATASHIELD&project-code=e650191ea08143b5bb11", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "output" + ], + "query": [ + { + "key": "project-type", + "value": "DATASHIELD" + }, + { + "key": "project-code", + "value": "e650191ea08143b5bb11" + } + ] + } + }, + "response": [] + }, + { + "name": "Add Output", + "request": { + "auth": { + "type": "noauth" + }, + "method": "PUT", + "header": [], + "url": { + "raw": "http://localhost:8097/project/edit?project-type=EXPORT&project-code=e650191ea08143b5bb11&output-format=EXCEL&template-id=ccp", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "edit" + ], + "query": [ + { + "key": "project-type", + "value": "EXPORT" + }, + { + "key": "project-code", + "value": "e650191ea08143b5bb11" + }, + { + "key": "output-format", + "value": "EXCEL" + }, + { + "key": "template-id", + "value": "ccp" + } + ] + } + }, + "response": [] + }, + { + "name": "Lens: Edit Query", + "request": { + "auth": { + "type": "noauth" + }, + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{ \"query\": \"eyJsYW5nIjoiY3FsIiwibGliIjp7InJlc291cmNlVHlwZSI6IkxpYnJhcnkiLCJ1cmwiOiJ1cm46dXVpZDpiOTRkMTAwYy0zN2NkLTRiZDAtODgyNy01MzJiODRiZWE2NDgiLCJzdGF0dXMiOiJhY3RpdmUiLCJ0eXBlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9saWJyYXJ5LXR5cGUiLCJjb2RlIjoibG9naWMtbGlicmFyeSJ9XX0sImNvbnRlbnQiOlt7ImNvbnRlbnRUeXBlIjoidGV4dC9jcWwiLCJkYXRhIjoiYkdsaWNtRnllU0JTWlhSeWFXVjJaUXAxYzJsdVp5QkdTRWxTSUhabGNuTnBiMjRnSnpRdU1DNHdKd3BwYm1Oc2RXUmxJRVpJU1ZKSVpXeHdaWEp6SUhabGNuTnBiMjRnSnpRdU1DNHdKd29LWTI5a1pYTjVjM1JsYlNCc2IybHVZem9nSjJoMGRIQTZMeTlzYjJsdVl5NXZjbWNuQ2dwamIyNTBaWGgwSUZCaGRHbGxiblFLQ2tSTFZFdGZVMVJTUVZSZlIwVk9SRVZTWDFOVVVrRlVTVVpKUlZJS0NrUkxWRXRmVTFSU1FWUmZRVWRGWDFOVVVrRlVTVVpKUlZJS0NrUkxWRXRmVTFSU1FWUmZSRVZEUlVGVFJVUmZVMVJTUVZSSlJrbEZVZ29LUkV0VVMxOVRWRkpCVkY5RVNVRkhUazlUU1ZOZlUxUlNRVlJKUmtsRlVnb0tSRXRVUzE5VFZGSkJWRjlUVUVWRFNVMUZUbDlUVkZKQlZFbEdTVVZTQ2dwRVMxUkxYMU5VVWtGVVgxQlNUME5GUkZWU1JWOVRWRkpCVkVsR1NVVlNDZ3BFUzFSTFgxTlVVa0ZVWDAxRlJFbERRVlJKVDA1ZlUxUlNRVlJKUmtsRlVncEVTMVJMWDFOVVVrRlVYMFJGUmw5SlRsOUpUa2xVU1VGTVgxQlBVRlZNUVZSSlQwNVFZWFJwWlc1MExtZGxibVJsY2lBOUlDZHRZV3hsSnc9PSJ9XX0sIm1lYXN1cmUiOnsicmVzb3VyY2VUeXBlIjoiTWVhc3VyZSIsInVybCI6InVybjp1dWlkOmFjMDQwNmMxLTBlYjEtNDU4YS05ZmE4LTQ3MTZiZjhmMTBhOCIsInN0YXR1cyI6ImFjdGl2ZSIsInN1YmplY3RDb2RlYWJsZUNvbmNlcHQiOnsiY29kaW5nIjpbeyJzeXN0ZW0iOiJodHRwOi8vaGw3Lm9yZy9maGlyL3Jlc291cmNlLXR5cGVzIiwiY29kZSI6IlBhdGllbnQifV19LCJsaWJyYXJ5IjoidXJuOnV1aWQ6Yjk0ZDEwMGMtMzdjZC00YmQwLTg4MjctNTMyYjg0YmVhNjQ4Iiwic2NvcmluZyI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1zY29yaW5nIiwiY29kZSI6ImNvaG9ydCJ9XX0sImdyb3VwIjpbeyJjb2RlIjp7InRleHQiOiJwYXRpZW50cyJ9LCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6IkluSW5pdGlhbFBvcHVsYXRpb24ifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJHZW5kZXIifSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbCIsImV4cHJlc3Npb24iOiJHZW5kZXIifX0seyJjb2RlIjp7InRleHQiOiI3NTE4Ni03In0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiRGVjZWFzZWQifX0seyJjb2RlIjp7InRleHQiOiJBZ2UifSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbCIsImV4cHJlc3Npb24iOiJBZ2VDbGFzcyJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoiZGlhZ25vc2lzIn0sImV4dGVuc2lvbiI6W3sidXJsIjoiaHR0cDovL2hsNy5vcmcvZmhpci91cy9jcWZtZWFzdXJlcy9TdHJ1Y3R1cmVEZWZpbml0aW9uL2NxZm0tcG9wdWxhdGlvbkJhc2lzIiwidmFsdWVDb2RlIjoiQ29uZGl0aW9uIn1dLCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6IkRpYWdub3NpcyJ9fV0sInN0cmF0aWZpZXIiOlt7ImNvZGUiOnsidGV4dCI6ImRpYWdub3NpcyJ9LCJjcml0ZXJpYSI6eyJsYW5ndWFnZSI6InRleHQvY3FsLWlkZW50aWZpZXIiLCJleHByZXNzaW9uIjoiRGlhZ25vc2lzQ29kZSJ9fV19LHsiY29kZSI6eyJ0ZXh0Ijoic3BlY2ltZW4ifSwiZXh0ZW5zaW9uIjpbeyJ1cmwiOiJodHRwOi8vaGw3Lm9yZy9maGlyL3VzL2NxZm1lYXN1cmVzL1N0cnVjdHVyZURlZmluaXRpb24vY3FmbS1wb3B1bGF0aW9uQmFzaXMiLCJ2YWx1ZUNvZGUiOiJTcGVjaW1lbiJ9XSwicG9wdWxhdGlvbiI6W3siY29kZSI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1wb3B1bGF0aW9uIiwiY29kZSI6ImluaXRpYWwtcG9wdWxhdGlvbiJ9XX0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwtaWRlbnRpZmllciIsImV4cHJlc3Npb24iOiJTcGVjaW1lbiJ9fV0sInN0cmF0aWZpZXIiOlt7ImNvZGUiOnsidGV4dCI6InNhbXBsZV9raW5kIn0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiU2FtcGxlVHlwZSJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoicHJvY2VkdXJlcyJ9LCJleHRlbnNpb24iOlt7InVybCI6Imh0dHA6Ly9obDcub3JnL2ZoaXIvdXMvY3FmbWVhc3VyZXMvU3RydWN0dXJlRGVmaW5pdGlvbi9jcWZtLXBvcHVsYXRpb25CYXNpcyIsInZhbHVlQ29kZSI6IlByb2NlZHVyZSJ9XSwicG9wdWxhdGlvbiI6W3siY29kZSI6eyJjb2RpbmciOlt7InN5c3RlbSI6Imh0dHA6Ly90ZXJtaW5vbG9neS5obDcub3JnL0NvZGVTeXN0ZW0vbWVhc3VyZS1wb3B1bGF0aW9uIiwiY29kZSI6ImluaXRpYWwtcG9wdWxhdGlvbiJ9XX0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwtaWRlbnRpZmllciIsImV4cHJlc3Npb24iOiJQcm9jZWR1cmUifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJQcm9jZWR1cmVUeXBlIn0sImNyaXRlcmlhIjp7Imxhbmd1YWdlIjoidGV4dC9jcWwiLCJleHByZXNzaW9uIjoiUHJvY2VkdXJlVHlwZSJ9fV19LHsiY29kZSI6eyJ0ZXh0IjoibWVkaWNhdGlvblN0YXRlbWVudHMifSwiZXh0ZW5zaW9uIjpbeyJ1cmwiOiJodHRwOi8vaGw3Lm9yZy9maGlyL3VzL2NxZm1lYXN1cmVzL1N0cnVjdHVyZURlZmluaXRpb24vY3FmbS1wb3B1bGF0aW9uQmFzaXMiLCJ2YWx1ZUNvZGUiOiJNZWRpY2F0aW9uU3RhdGVtZW50In1dLCJwb3B1bGF0aW9uIjpbeyJjb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL3Rlcm1pbm9sb2d5LmhsNy5vcmcvQ29kZVN5c3RlbS9tZWFzdXJlLXBvcHVsYXRpb24iLCJjb2RlIjoiaW5pdGlhbC1wb3B1bGF0aW9uIn1dfSwiY3JpdGVyaWEiOnsibGFuZ3VhZ2UiOiJ0ZXh0L2NxbC1pZGVudGlmaWVyIiwiZXhwcmVzc2lvbiI6Ik1lZGljYXRpb25TdGF0ZW1lbnQifX1dLCJzdHJhdGlmaWVyIjpbeyJjb2RlIjp7InRleHQiOiJNZWRpY2F0aW9uVHlwZSJ9LCJjcml0ZXJpYSI6eyJsYW5ndWFnZSI6InRleHQvY3FsIiwiZXhwcmVzc2lvbiI6IlByb2NlZHVyZVR5cGUifX1dfV19fQ==\" }", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8097/project/edit?human-readable=(gender = male) and (vitalstatus = lebend)&explorer-ids=lens-star-wars,lens-jurassic-park&explorer-url=https://data.dktk.dkfz.de/redirect/12345&project-code=2dfcd1bf620241f9b884", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "edit" + ], + "query": [ + { + "key": "human-readable", + "value": "(gender = male) and (vitalstatus = lebend)" + }, + { + "key": "explorer-ids", + "value": "lens-star-wars,lens-jurassic-park" + }, + { + "key": "explorer-url", + "value": "https://data.dktk.dkfz.de/redirect/12345" + }, + { + "key": "project-code", + "value": "2dfcd1bf620241f9b884" + } + ] + } + }, + "response": [] + }, + { + "name": "Design project (researcher)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "url": { + "raw": "http://localhost:8097/project/design?bridgeheads=david-j-develop,jurassic-park&query-code=c66cbbee57a941ddac12&project-type=DATASHIELD", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "design" + ], + "query": [ + { + "key": "bridgeheads", + "value": "david-j-develop,jurassic-park" + }, + { + "key": "query-code", + "value": "c66cbbee57a941ddac12" + }, + { + "key": "project-type", + "value": "DATASHIELD" + } + ] + } + }, + "response": [] + }, + { + "name": "Create project (researcher)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "url": { + "raw": "http://localhost:8097/project?project-code=7e5b829f25224090b7b8", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project" + ], + "query": [ + { + "key": "project-code", + "value": "7e5b829f25224090b7b8" + } + ] + } + }, + "response": [] + }, + { + "name": "Fetch Form Titles", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/project/forms/titles?project-code=a1ba86d4574e45dd9962", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "forms", + "titles" + ], + "query": [ + { + "key": "project-code", + "value": "a1ba86d4574e45dd9962" + } + ] + } + }, + "response": [] + }, + { + "name": "Fetch all form fields", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/project/forms/fields?project-code=a1ba86d4574e45dd9962", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "forms", + "fields" + ], + "query": [ + { + "key": "project-code", + "value": "a1ba86d4574e45dd9962" + } + ] + } + }, + "response": [] + }, + { + "name": "Fetch form fields", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/project/forms/fields?project-code=a1ba86d4574e45dd9962&form-title=sample_data", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "forms", + "fields" + ], + "query": [ + { + "key": "project-code", + "value": "a1ba86d4574e45dd9962" + }, + { + "key": "form-title", + "value": "sample_data" + } + ] + } + }, + "response": [] + }, + { + "name": "Fetch selected project forms", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/project/forms/selected?project-code=8424df248c9a412b9d39", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "forms", + "selected" + ], + "query": [ + { + "key": "project-code", + "value": "8424df248c9a412b9d39" + } + ] + } + }, + "response": [] + }, + { + "name": "Add Selected Project Form", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "url": { + "raw": "http://localhost:8097/project/forms/selected?project-code=8424df248c9a412b9d39&form-title=sample_data", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "forms", + "selected" + ], + "query": [ + { + "key": "project-code", + "value": "8424df248c9a412b9d39" + }, + { + "key": "form-title", + "value": "sample_data" + } + ] + } + }, + "response": [] + }, + { + "name": "Remove Selected Project Form", + "request": { + "auth": { + "type": "noauth" + }, + "method": "DELETE", + "header": [], + "url": { + "raw": "http://localhost:8097/project/forms/selected?project-code=8424df248c9a412b9d39&form-title=sample_data", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "forms", + "selected" + ], + "query": [ + { + "key": "project-code", + "value": "8424df248c9a412b9d39" + }, + { + "key": "form-title", + "value": "sample_data" + } + ] + } + }, + "response": [] + }, + { + "name": "Edit form fields", + "request": { + "auth": { + "type": "noauth" + }, + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"form-fields\": [\r\n {\r\n \"title\": \"sample_data\",\r\n \"label\": \"slide_sufficiency_staining\",\r\n \"value\": \"true\"\r\n },\r\n {\r\n \"title\": \"sample_data\",\r\n \"label\": \"donor\",\r\n \"value\": \"my donor\"\r\n }\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8097/project/forms/fields?project-code=a1ba86d4574e45dd9962", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "forms", + "fields" + ], + "query": [ + { + "key": "project-code", + "value": "a1ba86d4574e45dd9962" + } + ] + } + }, + "response": [] + }, + { + "name": "Fetch form templates", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/project/forms/templates?project-code=a1ba86d4574e45dd9962", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "forms", + "templates" + ], + "query": [ + { + "key": "project-code", + "value": "a1ba86d4574e45dd9962" + } + ] + } + }, + "response": [] + }, + { + "name": "Download Form (Samples)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/download-form-as-pdf?project-code=a1ba86d4574e45dd9962&form-template=samples", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "download-form-as-pdf" + ], + "query": [ + { + "key": "project-code", + "value": "a1ba86d4574e45dd9962" + }, + { + "key": "form-template", + "value": "samples" + } + ] + } + }, + "response": [] + }, + { + "name": "Download Form (Clinical Data)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/download-form-as-pdf?project-code=4786deeb2c3c459c9e33&form-template=clinical-data&language=de", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "download-form-as-pdf" + ], + "query": [ + { + "key": "project-code", + "value": "4786deeb2c3c459c9e33" + }, + { + "key": "form-template", + "value": "clinical-data" + }, + { + "key": "language", + "value": "de" + } + ] + } + }, + "response": [] + }, + { + "name": "Fetch Project Roles (any user)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/project/roles?project-code=9cb4eef8063441d8a820&bridgehead=jurassic-park", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "roles" + ], + "query": [ + { + "key": "project-code", + "value": "9cb4eef8063441d8a820" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + } + ] + } + }, + "response": [] + }, + { + "name": "Fetch Project (any user)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/project?project-code=7ac24acc738644d186f8&bridgehead=jurassic-park", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project" + ], + "query": [ + { + "key": "project-code", + "value": "7ac24acc738644d186f8", + "type": "text" + }, + { + "key": "bridgehead", + "value": "jurassic-park", + "type": "text" + } + ] + } + }, + "response": [] + }, + { + "name": "Fetch Projects (any user)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/projects?page=0&page-size=4", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "projects" + ], + "query": [ + { + "key": "page", + "value": "0" + }, + { + "key": "page-size", + "value": "4" + } + ] + } + }, + "response": [] + }, + { + "name": "Fetch Project States (any user)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/project/states", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "states" + ] + } + }, + "response": [] + }, + { + "name": "Fetch registered bridgeheads", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/bridgeheads", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "bridgeheads" + ] + } + }, + "response": [] + }, + { + "name": "Fetch Project Bridgehead (any user)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/project/bridgeheads?project-code=2bb9caf3c03a4d8296a0", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "bridgeheads" + ], + "query": [ + { + "key": "project-code", + "value": "2bb9caf3c03a4d8296a0" + }, + { + "key": "bridgehead", + "value": "jurassic-park", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Fetch Script Description", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/script/description?project-code=ce573ab8dd1a4a53bce7&bridgehead=jurassic-park", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "script", + "description" + ], + "query": [ + { + "key": "project-code", + "value": "ce573ab8dd1a4a53bce7" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + } + ] + } + }, + "response": [] + }, + { + "name": "Fetch Votum Description", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/votum/description?project-code=ce573ab8dd1a4a53bce7&bridgehead=jurassic-park", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "votum", + "description" + ], + "query": [ + { + "key": "project-code", + "value": "ce573ab8dd1a4a53bce7" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + } + ] + } + }, + "response": [] + }, + { + "name": "Fetch Projects Modified asc(any user)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/projects?page=0&page-size=2&modified-desc=false", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "projects" + ], + "query": [ + { + "key": "page", + "value": "0" + }, + { + "key": "page-size", + "value": "2" + }, + { + "key": "modified-desc", + "value": "false", + "type": "text" + } + ] + } + }, + "response": [] + }, + { + "name": "Fetch Projects with project-state (any user)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/projects?page=0&page-size=2&project-state=DRAFT", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "projects" + ], + "query": [ + { + "key": "page", + "value": "0" + }, + { + "key": "page-size", + "value": "2" + }, + { + "key": "project-state", + "value": "DRAFT", + "type": "text" + } + ] + } + }, + "response": [] + }, + { + "name": "Fetch Projects with project-state and archived (any user)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/projects?page=0&page-size=2&project-state=DRAFT&archived=true", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "projects" + ], + "query": [ + { + "key": "page", + "value": "0" + }, + { + "key": "page-size", + "value": "2" + }, + { + "key": "project-state", + "value": "DRAFT" + }, + { + "key": "archived", + "value": "true", + "type": "text" + } + ] + } + }, + "response": [] + }, + { + "name": "Fetch Projects with project-state and not archived (any user)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/projects?page=0&page-size=2&project-state=DRAFT&archived=false", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "projects" + ], + "query": [ + { + "key": "page", + "value": "0" + }, + { + "key": "page-size", + "value": "2" + }, + { + "key": "project-state", + "value": "DRAFT" + }, + { + "key": "archived", + "value": "false" + } + ] + } + }, + "response": [] + }, + { + "name": "Fetch Projects archived (any user)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/projects?page=0&page-size=2&archived=true", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "projects" + ], + "query": [ + { + "key": "page", + "value": "0" + }, + { + "key": "page-size", + "value": "2" + }, + { + "key": "archived", + "value": "true" + } + ] + } + }, + "response": [] + }, + { + "name": "Fetch Projects not archived (any user)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/projects?page=0&page-size=2&archived=false", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "projects" + ], + "query": [ + { + "key": "page", + "value": "0" + }, + { + "key": "page-size", + "value": "2" + }, + { + "key": "archived", + "value": "false" + } + ] + } + }, + "response": [] + }, + { + "name": "Fetch Export Templates (researcher)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/exporter/templates?project-code=e650191ea08143b5bb11", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "exporter", + "templates" + ], + "query": [ + { + "key": "project-code", + "value": "e650191ea08143b5bb11" + } + ] + } + }, + "response": [] + }, + { + "name": "Fetch DataSHIELD Templates (researcher)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/exporter/templates?project-code=5e632908aac4412a892e&project-type=DATASHIELD", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "exporter", + "templates" + ], + "query": [ + { + "key": "project-code", + "value": "5e632908aac4412a892e" + }, + { + "key": "project-type", + "value": "DATASHIELD" + } + ] + } + }, + "response": [] + }, + { + "name": "Fetch Query Formats (researcher)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/query/formats?project-code=5c8bb65ffcf3405c8b3a", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "query", + "formats" + ], + "query": [ + { + "key": "project-code", + "value": "5c8bb65ffcf3405c8b3a" + } + ] + } + }, + "response": [] + }, + { + "name": "Fetch Output Formats (researcher)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/output/formats?project-code=c319ad0b3d7d463cab5a", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "output", + "formats" + ], + "query": [ + { + "key": "project-code", + "value": "c319ad0b3d7d463cab5a" + }, + { + "key": "bridgehead", + "value": "jurassic-park", + "type": "text", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Fetch Project Configurations (researcher)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/project/configurations?project-code=277ec717a5b540b28adc", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "configurations" + ], + "query": [ + { + "key": "project-code", + "value": "277ec717a5b540b28adc" + } + ] + } + }, + "response": [] + }, + { + "name": "Fetch Current Project Configuration (researcher)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/project/configuration?project-code=a004d11ade614e2b871b", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "configuration" + ], + "query": [ + { + "key": "project-code", + "value": "a004d11ade614e2b871b" + } + ] + } + }, + "response": [] + }, + { + "name": "Fetch Project Types (researcher)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/project/types?project-code=eca876afe8fb46f0ad03", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "types" + ], + "query": [ + { + "key": "project-code", + "value": "eca876afe8fb46f0ad03" + } + ] + } + }, + "response": [] + }, + { + "name": "Accept project (pm-admin)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "PUT", + "header": [], + "url": { + "raw": "http://localhost:8097/project/accept?project-code=7e5b829f25224090b7b8", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "accept" + ], + "query": [ + { + "key": "project-code", + "value": "7e5b829f25224090b7b8" + } + ] + } + }, + "response": [] + }, + { + "name": "Archive project (pm-admin)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "PUT", + "header": [], + "url": { + "raw": "http://localhost:8097/project/archive?project-code=9e1e43f738c840788f11", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "archive" + ], + "query": [ + { + "key": "project-code", + "value": "9e1e43f738c840788f11" + } + ] + } + }, + "response": [] + }, + { + "name": "Send query to bridgehead (bk-admin)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "url": { + "raw": "http://localhost:8097/project/bridgehead/query/save?project-code=14ed087fb7994c84a564&bridgehead=jurassic-park", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "bridgehead", + "query", + "save" + ], + "query": [ + { + "key": "project-code", + "value": "14ed087fb7994c84a564" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + } + ] + } + }, + "response": [] + }, + { + "name": "Send query to bridgehead (bk-admin) Multi-Project Type", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "url": { + "raw": "http://localhost:8097/project/bridgehead/query/save?project-code=edc0372212c343d1ba04&bridgehead=jurassic-park&project-type=EXPORT,SAMPLES", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "bridgehead", + "query", + "save" + ], + "query": [ + { + "key": "project-code", + "value": "edc0372212c343d1ba04" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + }, + { + "key": "project-type", + "value": "EXPORT,SAMPLES" + } + ] + } + }, + "response": [] + }, + { + "name": "Send query to bridgehead and execute (bk-admin)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "url": { + "raw": "http://localhost:8097/project/bridgehead/query/save-execute?project-code=2dfcd1bf620241f9b884&bridgehead=jurassic-park", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "bridgehead", + "query", + "save-execute" + ], + "query": [ + { + "key": "project-code", + "value": "2dfcd1bf620241f9b884" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + } + ] + } + }, + "response": [] + }, + { + "name": "Start develop stage for project (pm-admin)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "PUT", + "header": [], + "url": { + "raw": "http://localhost:8097/project/start-develop?project-code=7e5b829f25224090b7b8", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "start-develop" + ], + "query": [ + { + "key": "project-code", + "value": "7e5b829f25224090b7b8" + } + ] + } + }, + "response": [] + }, + { + "name": "Add developer to project (pm-admin)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "url": { + "raw": "http://localhost:8097/developer?project-code=2dfcd1bf620241f9b884&bridgehead=jurassic-park&email=developer@test.dkfz.de", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "developer" + ], + "query": [ + { + "key": "project-code", + "value": "2dfcd1bf620241f9b884" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + }, + { + "key": "email", + "value": "developer@test.dkfz.de" + } + ] + } + }, + "response": [] + }, + { + "name": "Accept project bridgehead (bk-admin)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "PUT", + "header": [], + "url": { + "raw": "http://localhost:8097/project/bridgehead/accept?project-code=2dfcd1bf620241f9b884&bridgehead=jurassic-park", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "bridgehead", + "accept" + ], + "query": [ + { + "key": "project-code", + "value": "2dfcd1bf620241f9b884" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + } + ] + } + }, + "response": [] + }, + { + "name": "Reject project bridgehead (bk-admin)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "PUT", + "header": [], + "url": { + "raw": "http://localhost:8097/project/bridgehead/reject?project-code=eca876afe8fb46f0ad03&bridgehead=jurassic-park", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "bridgehead", + "reject" + ], + "query": [ + { + "key": "project-code", + "value": "eca876afe8fb46f0ad03" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + } + ] + } + }, + "response": [] + }, + { + "name": "Accept script (developer)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "PUT", + "header": [], + "url": { + "raw": "http://localhost:8097/script/accept?project-code=168d35c773a845c3beab&bridgehead=jurassic-park", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "script", + "accept" + ], + "query": [ + { + "key": "project-code", + "value": "168d35c773a845c3beab" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + } + ] + } + }, + "response": [] + }, + { + "name": "Reject script (developer)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "PUT", + "header": [], + "url": { + "raw": "http://localhost:8097/script/reject?project-code=168d35c773a845c3beab&bridgehead=jurassic-park", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "script", + "reject" + ], + "query": [ + { + "key": "project-code", + "value": "168d35c773a845c3beab" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + } + ] + } + }, + "response": [] + }, + { + "name": "Request changes in script (developer)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "PUT", + "header": [], + "url": { + "raw": "http://localhost:8097/script/request-changes?project-code=168d35c773a845c3beab&bridgehead=jurassic-park", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "script", + "request-changes" + ], + "query": [ + { + "key": "project-code", + "value": "168d35c773a845c3beab" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + } + ] + } + }, + "response": [] + }, + { + "name": "Accept project results (final)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "PUT", + "header": [], + "url": { + "raw": "http://localhost:8097/project/results/accept?project-code=168d35c773a845c3beab&bridgehead=jurassic-park", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "results", + "accept" + ], + "query": [ + { + "key": "project-code", + "value": "168d35c773a845c3beab" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + } + ] + } + }, + "response": [] + }, + { + "name": "Reject project results (final)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "PUT", + "header": [], + "url": { + "raw": "http://localhost:8097/project/results/reject?project-code=168d35c773a845c3beab&bridgehead=jurassic-park", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "results", + "reject" + ], + "query": [ + { + "key": "project-code", + "value": "168d35c773a845c3beab" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + } + ] + } + }, + "response": [] + }, + { + "name": "Request changes in project (final)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "PUT", + "header": [], + "url": { + "raw": "http://localhost:8097/project/request-changes?project-code=168d35c773a845c3beab&bridgehead=jurassic-park", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "request-changes" + ], + "query": [ + { + "key": "project-code", + "value": "168d35c773a845c3beab" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + } + ] + } + }, + "response": [] + }, + { + "name": "Start pilot stage for project (pm-admin)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "PUT", + "header": [], + "url": { + "raw": "http://localhost:8097/project/start-pilot?project-code=9e1e43f738c840788f11", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "start-pilot" + ], + "query": [ + { + "key": "project-code", + "value": "9e1e43f738c840788f11" + } + ] + } + }, + "response": [] + }, + { + "name": "Add pilot user to project (pm-admin)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "url": { + "raw": "http://localhost:8097/pilot-user?project-code=3b0a76b850864fc7916e&bridgehead=jurassic-park&email=pilot@test.dkfz.de", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "pilot-user" + ], + "query": [ + { + "key": "project-code", + "value": "3b0a76b850864fc7916e" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + }, + { + "key": "email", + "value": "pilot@test.dkfz.de" + } + ] + } + }, + "response": [] + }, + { + "name": "Start final stage for project (pm-admin)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "PUT", + "header": [], + "url": { + "raw": "http://localhost:8097/project/start-final?project-code=2bb9caf3c03a4d8296a0", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "start-final" + ], + "query": [ + { + "key": "project-code", + "value": "2bb9caf3c03a4d8296a0" + } + ] + } + }, + "response": [] + }, + { + "name": "Add final user to project (pm-admin)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "url": { + "raw": "http://localhost:8097/final-user?project-code=6491e451354c42b48e67&bridgehead=jurassic-park&email=final@test.dkfz.de", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "final-user" + ], + "query": [ + { + "key": "project-code", + "value": "6491e451354c42b48e67" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + }, + { + "key": "email", + "value": "final@test.dkfz.de" + } + ] + } + }, + "response": [] + }, + { + "name": "Finish project (pm-admin)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "PUT", + "header": [], + "url": { + "raw": "http://localhost:8097/project/finish?project-code=2bb9caf3c03a4d8296a0", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "finish" + ], + "query": [ + { + "key": "project-code", + "value": "2bb9caf3c03a4d8296a0" + } + ] + } + }, + "response": [] + }, + { + "name": "Upload Votum (researcher)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "document", + "type": "file", + "src": "votum.txt" + } + ] + }, + "url": { + "raw": "http://localhost:8097/votum/upload?project-code=2bb9caf3c03a4d8296a0&bridgehead=jurassic-park&label=This is a votum", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "votum", + "upload" + ], + "query": [ + { + "key": "project-code", + "value": "2bb9caf3c03a4d8296a0" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + }, + { + "key": "label", + "value": "This is a votum" + } + ] + } + }, + "response": [] + }, + { + "name": "Download Votum (researcher)", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "document", + "type": "file", + "src": "application-form.txt" + } + ] + }, + "url": { + "raw": "http://localhost:8097/votum/download?project-code=2bb9caf3c03a4d8296a0&bridgehead=jurassic-park", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "votum", + "download" + ], + "query": [ + { + "key": "project-code", + "value": "2bb9caf3c03a4d8296a0" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + } + ] + } + }, + "response": [] + }, + { + "name": "Exists Votum (researcher)", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "document", + "type": "file", + "src": "application-form.txt" + } + ] + }, + "url": { + "raw": "http://localhost:8097/votum/exists?project-code=2bb9caf3c03a4d8296a0&bridgehead=jurassic-park", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "votum", + "exists" + ], + "query": [ + { + "key": "project-code", + "value": "2bb9caf3c03a4d8296a0" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + } + ] + } + }, + "response": [] + }, + { + "name": "Upload Script (developer)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "document", + "type": "file", + "src": "script.txt" + } + ] + }, + "url": { + "raw": "http://localhost:8097/script/upload?project-code=2bb9caf3c03a4d8296a0&label=This is a script&bridgehead=jurassic-park", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "script", + "upload" + ], + "query": [ + { + "key": "project-code", + "value": "2bb9caf3c03a4d8296a0" + }, + { + "key": "label", + "value": "This is a script" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + } + ] + } + }, + "response": [] + }, + { + "name": "Download Script (developer)", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "document", + "type": "file", + "src": "application-form.txt" + } + ] + }, + "url": { + "raw": "http://localhost:8097/script/download?project-code=2bb9caf3c03a4d8296a0&bridgehead=jurassic-park", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "script", + "download" + ], + "query": [ + { + "key": "project-code", + "value": "2bb9caf3c03a4d8296a0" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + } + ] + } + }, + "response": [] + }, + { + "name": "Upload Publication (researcher)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "document", + "type": "file", + "src": "publication2.txt" + } + ] + }, + "url": { + "raw": "http://localhost:8097/publication/upload?project-code=2bb9caf3c03a4d8296a0&label=This is a publication", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "publication", + "upload" + ], + "query": [ + { + "key": "project-code", + "value": "2bb9caf3c03a4d8296a0" + }, + { + "key": "label", + "value": "This is a publication" + } + ] + } + }, + "response": [] + }, + { + "name": "Download Publication (any)", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "document", + "type": "file", + "src": "application-form.txt" + } + ] + }, + "url": { + "raw": "http://localhost:8097/publication/download?project-code=2bb9caf3c03a4d8296a0&filename=publication.txt", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "publication", + "download" + ], + "query": [ + { + "key": "project-code", + "value": "2bb9caf3c03a4d8296a0" + }, + { + "key": "filename", + "value": "publication.txt" + } + ] + } + }, + "response": [] + }, + { + "name": "Upload Other document (developer)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "document", + "type": "file", + "src": "other-file.txt" + } + ] + }, + "url": { + "raw": "http://localhost:8097/other-document/upload?project-code=2bb9caf3c03a4d8296a0&label=This is a script&bridgehead=jurassic-park", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "other-document", + "upload" + ], + "query": [ + { + "key": "project-code", + "value": "2bb9caf3c03a4d8296a0" + }, + { + "key": "label", + "value": "This is a script" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + } + ] + } + }, + "response": [] + }, + { + "name": "Download other document (developer)", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "document", + "type": "file", + "src": "application-form.txt" + } + ] + }, + "url": { + "raw": "http://localhost:8097/other-document/download?project-code=2bb9caf3c03a4d8296a0&bridgehead=jurassic-park&filename=other-file.txt", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "other-document", + "download" + ], + "query": [ + { + "key": "project-code", + "value": "2bb9caf3c03a4d8296a0" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + }, + { + "key": "filename", + "value": "other-file.txt" + } + ] + } + }, + "response": [] + }, + { + "name": "Add publication URL (researcher)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "url": { + "raw": "http://localhost:8097/publication/url?project-code=2bb9caf3c03a4d8296a0&document-url=https://escholarship.org/uc/item/5d08883v&label=A methane seep from the deep-marine, late Eocene Keasey Formation, Rock Creek, Columbia County, Oregon", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "publication", + "url" + ], + "query": [ + { + "key": "project-code", + "value": "2bb9caf3c03a4d8296a0" + }, + { + "key": "bridgehead", + "value": "jurassic-park", + "disabled": true + }, + { + "key": "document-url", + "value": "https://escholarship.org/uc/item/5d08883v" + }, + { + "key": "label", + "value": "A methane seep from the deep-marine, late Eocene Keasey Formation, Rock Creek, Columbia County, Oregon" + } + ] + } + }, + "response": [] + }, + { + "name": "Add other documetn URL (researcher)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "url": { + "raw": "http://localhost:8097/document/url?project-code=2bb9caf3c03a4d8296a0&document-url=https://escholarship.org/uc/item/49c1b8zb&label=Acanthodian fauna from the Early Devonian (Emsian) of Death Valley, California", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "document", + "url" + ], + "query": [ + { + "key": "project-code", + "value": "2bb9caf3c03a4d8296a0" + }, + { + "key": "bridgehead", + "value": "jurassic-park", + "disabled": true + }, + { + "key": "document-url", + "value": "https://escholarship.org/uc/item/49c1b8zb" + }, + { + "key": "label", + "value": "Acanthodian fauna from the Early Devonian (Emsian) of Death Valley, California" + } + ] + } + }, + "response": [] + }, + { + "name": "Fetch publications (researcher)", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "document", + "type": "file", + "src": "application-form.txt" + } + ] + }, + "url": { + "raw": "http://localhost:8097/publications?project-code=2bb9caf3c03a4d8296a0", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "publications" + ], + "query": [ + { + "key": "project-code", + "value": "2bb9caf3c03a4d8296a0" + } + ] + } + }, + "response": [] + }, + { + "name": "Fetch other documents (researcher)", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "document", + "type": "file", + "src": "application-form.txt" + } + ] + }, + "url": { + "raw": "http://localhost:8097/other-documents?project-code=2bb9caf3c03a4d8296a0&bridgehead=jurassic-park", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "other-documents" + ], + "query": [ + { + "key": "project-code", + "value": "2bb9caf3c03a4d8296a0" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + } + ] + } + }, + "response": [] + }, + { + "name": "Fetch notifications (researcher)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/notifications?project-code=e32e15c3c016467a8dbc&bridgehead=jurassic-park", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "notifications" + ], + "query": [ + { + "key": "project-code", + "value": "e32e15c3c016467a8dbc" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + } + ] + } + }, + "response": [] + }, + { + "name": "Set notification as read", + "request": { + "auth": { + "type": "noauth" + }, + "method": "PUT", + "header": [], + "url": { + "raw": "http://localhost:8097/notification/read?notification-id=1", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "notification", + "read" + ], + "query": [ + { + "key": "notification-id", + "value": "1" + } + ] + } + }, + "response": [] + }, + { + "name": "Fetch DataSHIELD Status", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/datashield-status?project-code=413148c5791a430e9dc7&bridgehead=jurassic-park", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "datashield-status" + ], + "query": [ + { + "key": "project-code", + "value": "413148c5791a430e9dc7" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + } + ] + } + }, + "response": [] + }, + { + "name": "Download Authentication Script", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/authentication-script/download?project-code=2dfcd1bf620241f9b884&bridgehead=jurassic-park", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "authentication-script", + "download" + ], + "query": [ + { + "key": "project-code", + "value": "2dfcd1bf620241f9b884" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + } + ] + } + }, + "response": [] + }, + { + "name": "Fetch Users for Autocomplete", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/autocomplete-users?project-code=7e5b829f25224090b7b8&bridgehead=jurassic-park&partial-email=vel", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "autocomplete-users" + ], + "query": [ + { + "key": "project-code", + "value": "7e5b829f25224090b7b8" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + }, + { + "key": "partial-email", + "value": "vel" + } + ] + } + }, + "response": [] + }, + { + "name": "Fetch Project Users (PM-Admin)", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8097/project/users?project-code=6491e451354c42b48e67&bridgehead=jurassic-park", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8097", + "path": [ + "project", + "users" + ], + "query": [ + { + "key": "project-code", + "value": "6491e451354c42b48e67" + }, + { + "key": "bridgehead", + "value": "jurassic-park" + } + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file