diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fb1f5d..153cca9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ 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 - 2024-04-15] +## [0.0.1 - 2024-04-18] ### Added - First version of the project - Spring Application @@ -130,3 +130,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Create notification after sending an email - Send message while requesting changes in script or project - Send message while rejecting a project +- Exporter Job +- Check Queries in Exporter Job +- Max time to wait focus task in minutes diff --git a/src/main/java/de/samply/annotations/StateConstraints.java b/src/main/java/de/samply/annotations/StateConstraints.java index 5a1597e..661f0b7 100644 --- a/src/main/java/de/samply/annotations/StateConstraints.java +++ b/src/main/java/de/samply/annotations/StateConstraints.java @@ -2,6 +2,7 @@ import de.samply.project.state.ProjectBridgeheadState; import de.samply.project.state.ProjectState; +import de.samply.query.QueryState; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -14,5 +15,6 @@ public @interface StateConstraints { ProjectState[] projectStates() default {}; ProjectBridgeheadState[] projectBridgeheadStates() default {}; + QueryState[] queryStates() default {}; } diff --git a/src/main/java/de/samply/aop/ConstraintsService.java b/src/main/java/de/samply/aop/ConstraintsService.java index 11b647e..805bf19 100644 --- a/src/main/java/de/samply/aop/ConstraintsService.java +++ b/src/main/java/de/samply/aop/ConstraintsService.java @@ -10,6 +10,7 @@ import de.samply.project.ProjectType; import de.samply.project.state.ProjectBridgeheadState; import de.samply.project.state.ProjectState; +import de.samply.query.QueryState; import de.samply.security.SessionUser; import de.samply.user.roles.OrganisationRole; import de.samply.user.roles.OrganisationRoleToProjectRoleMapper; @@ -110,16 +111,28 @@ public Optional checkStateConstraints(Optional return Optional.of(ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).build()); } } - if (stateConstraints.get().projectBridgeheadStates().length > 0) { + if (stateConstraints.get().projectBridgeheadStates().length > 0 || stateConstraints.get().queryStates().length > 0) { Optional projectBridgehead = fetchProjectBridgehead(project.get(), bridgehead); if (projectBridgehead.isEmpty()) { return Optional.of(ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).build()); } - boolean hasAnyProjectBridgeheadStateConstraint = false; - for (ProjectBridgeheadState projectBridgeheadState : stateConstraints.get().projectBridgeheadStates()) { - if (projectBridgehead.get().getState() == projectBridgeheadState) { - hasAnyProjectBridgeheadStateConstraint = true; - break; + boolean hasAnyProjectBridgeheadStateConstraint = true; + if (stateConstraints.get().projectBridgeheadStates().length > 0) { + hasAnyProjectBridgeheadStateConstraint = false; + for (ProjectBridgeheadState projectBridgeheadState : stateConstraints.get().projectBridgeheadStates()) { + if (projectBridgehead.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; + } } } if (!hasAnyProjectBridgeheadStateConstraint) { diff --git a/src/main/java/de/samply/app/ProjectManagerConst.java b/src/main/java/de/samply/app/ProjectManagerConst.java index 8442b88..f362dd2 100644 --- a/src/main/java/de/samply/app/ProjectManagerConst.java +++ b/src/main/java/de/samply/app/ProjectManagerConst.java @@ -238,12 +238,18 @@ public class ProjectManagerConst { public static final String EXPORTER_PARAM_QUERY_CONTEXT = "query-context"; public static final String EXPORTER_PARAM_DEFAULT_OUTPUT_FORMAT = "query-default-output-format"; public static final String EXPORTER_PARAM_DEFAULT_TEMPLATE_ID = "query-default-template-id"; + public final static String EXPORTER_PARAM_QUERY_EXECUTION_ID = "query-execution-id"; public static final String EXPORTER_QUERY_CONTEXT_PROJECT_ID = "PROJECT-ID"; + public final static String EXPORTER_QUERY_CONTEXT_SEPARATOR = ";"; public final static String API_KEY = "ApiKey"; + // Focus public final static String FOCUS_METADATA_PROJECT = "exporter"; - public final static String FOCUS_TASK = "/v1/tasks"; + public final static String FOCUS_TASK_PATH = "/v1/tasks"; + public final static String FOCUS_TASK_RESULTS_PATH = "/results"; + public final static String FOCUS_TASK_WAIT_TIME_PARAM = "wait_time"; + public final static String FOCUS_TASK_WAIT_COUNT_PARAM = "wait_count"; // Token Manager Variables public final static String TOKEN_MANAGER_ROOT = "/api"; @@ -297,6 +303,7 @@ public class ProjectManagerConst { public final static String TOKEN_MANAGER_URL = "TOKEN_MANAGER_URL"; public final static String ENABLE_EMAILS = "ENABLE_EMAILS"; public final static String MANAGE_TOKENS_CRON_EXPRESSION = "MANAGE_TOKENS_CRON_EXPRESSION"; + public final static String EXPORTER_CRON_EXPRESSION = "EXPORTER_CRON_EXPRESSION"; public final static String CHECK_EXPIRED_ACTIVE_PROJECTS_CRON_EXPRESSION = "CHECK_EXPIRED_ACTIVE_PROJECTS_CRON_EXPRESSION"; public final static String EXPLORER_URL = "EXPLORER_URL"; public final static String ENABLE_TOKEN_MANAGER = "ENABLE_TOKEN_MANAGER"; @@ -309,6 +316,8 @@ public class ProjectManagerConst { 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"; // Spring Values (SV) public final static String HEAD_SV = "${"; @@ -337,6 +346,8 @@ public class ProjectManagerConst { HEAD_SV + WEBCLIENT_TCP_KEEP_INTERVAL_IN_SECONDS + ":60" + BOTTOM_SV; public final static String WEBCLIENT_TCP_KEEP_CONNECTION_NUMBER_OF_TRIES_SV = HEAD_SV + WEBCLIENT_TCP_KEEP_CONNECTION_NUMBER_OF_TRIES + ":10" + BOTTOM_SV; + public final static String MAX_TIME_TO_WAIT_FOCUS_TASK_IN_MINUTES_SV = + HEAD_SV + MAX_TIME_TO_WAIT_FOCUS_TASK_IN_MINUTES + ":5" + BOTTOM_SV; public final static String WEBCLIENT_MAX_NUMBER_OF_RETRIES_SV = HEAD_SV + WEBCLIENT_MAX_NUMBER_OF_RETRIES + ":3" + BOTTOM_SV; public final static String WEBCLIENT_TIME_IN_SECONDS_AFTER_RETRY_WITH_FAILURE_SV = @@ -359,10 +370,13 @@ public class ProjectManagerConst { public final static String FOCUS_API_KEY_SV = HEAD_SV + FOCUS_API_KEY + BOTTOM_SV; public final static String ENABLE_EMAILS_SV = HEAD_SV + ENABLE_EMAILS + ":true" + BOTTOM_SV; public final static String ENABLE_TOKEN_MANAGER_SV = HEAD_SV + ENABLE_TOKEN_MANAGER + ":true" + BOTTOM_SV; + public final static String ENABLE_EXPORTER_SV = HEAD_SV + ENABLE_EXPORTER + ":true" + BOTTOM_SV; public final static String MANAGE_TOKENS_CRON_EXPRESSION_SV = - HEAD_SV + MANAGE_TOKENS_CRON_EXPRESSION + ":#{'0 0 * * * *'}" + BOTTOM_SV; + HEAD_SV + MANAGE_TOKENS_CRON_EXPRESSION + ":#{'0 * * * * *'}" + BOTTOM_SV; + public final static String EXPORTER_CRON_EXPRESSION_SV = + HEAD_SV + EXPORTER_CRON_EXPRESSION + ":#{'45 * * * * *'}" + BOTTOM_SV; public final static String CHECK_EXPIRED_ACTIVE_PROJECTS_CRON_EXPRESSION_SV = - HEAD_SV + CHECK_EXPIRED_ACTIVE_PROJECTS_CRON_EXPRESSION + ":#{'0 0 1,13 * * *'}" + BOTTOM_SV; + HEAD_SV + CHECK_EXPIRED_ACTIVE_PROJECTS_CRON_EXPRESSION + ":#{'30 * * * * *'}" + BOTTOM_SV; 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_SV = HEAD_SV + FRONTEND_PROJECT_CONFIG + BOTTOM_SV; @@ -387,4 +401,5 @@ public class ProjectManagerConst { public final static String CUSTOM_PROJECT_CONFIGURATION = "CUSTOM"; public final static String EMAIL_SERVICE = "EMAIL_SERVICE"; + } diff --git a/src/main/java/de/samply/app/ProjectManagerController.java b/src/main/java/de/samply/app/ProjectManagerController.java index 23020bb..5aec983 100644 --- a/src/main/java/de/samply/app/ProjectManagerController.java +++ b/src/main/java/de/samply/app/ProjectManagerController.java @@ -24,6 +24,7 @@ import de.samply.query.OutputFormat; import de.samply.query.QueryFormat; import de.samply.query.QueryService; +import de.samply.query.QueryState; import de.samply.token.DataShieldTokenManagerService; import de.samply.user.UserService; import de.samply.user.roles.OrganisationRole; @@ -307,7 +308,7 @@ public ResponseEntity editProject( queryService.editQuery(projectCode, (query != null && query.trim().length() > 0 && !query.equals("{}")) ? query : null, queryFormat, label, description, outputFormat, templateId, humanReadable, explorerUrl, queryContext); return convertToResponseEntity(() -> this.frontendService.fetchExplorerRedirectUri( ProjectManagerConst.PROJECT_VIEW_SITE, - Map.of(ProjectManagerConst.QUERY_CODE, projectCode) + Map.of(ProjectManagerConst.PROJECT_CODE, projectCode) )); } @@ -491,7 +492,7 @@ public ResponseEntity rejectProject( } @RoleConstraints(projectRoles = {ProjectRole.BRIDGEHEAD_ADMIN}) - @StateConstraints(projectStates = {ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL}) + @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) @@ -504,7 +505,7 @@ public ResponseEntity acceptBridgeheadProject( } @RoleConstraints(projectRoles = {ProjectRole.BRIDGEHEAD_ADMIN}) - @StateConstraints(projectStates = {ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL}) + @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) @@ -976,7 +977,8 @@ private ByteArrayResource fetchResource(Path filePath) throws DocumentServiceExc } @RoleConstraints(projectRoles = {ProjectRole.BRIDGEHEAD_ADMIN}) - @StateConstraints(projectStates = {ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL}) + @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_QUERY_IN_BRIDGEHEAD_ACTION) @PostMapping(value = ProjectManagerConst.SAVE_QUERY_IN_BRIDGEHEAD) @@ -984,11 +986,12 @@ public ResponseEntity saveQueryInBridgehead( @NotEmpty @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, @NotEmpty @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead ) { - return convertToResponseEntity(() -> this.exporterService.sendQueryToBridgehead(projectCode, bridgehead)); + return convertToResponseEntity(() -> this.projectBridgeheadService.scheduleSendQueryToBridgehead(projectCode, bridgehead)); } @RoleConstraints(projectRoles = {ProjectRole.BRIDGEHEAD_ADMIN}) - @StateConstraints(projectStates = {ProjectState.DEVELOP, ProjectState.PILOT, ProjectState.FINAL}) + @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) @@ -996,7 +999,7 @@ public ResponseEntity saveAndExecuteQueryInBridgehead( @ProjectCode @RequestParam(name = ProjectManagerConst.PROJECT_CODE) String projectCode, @Bridgehead @RequestParam(name = ProjectManagerConst.BRIDGEHEAD) String bridgehead ) { - return convertToResponseEntity(() -> this.exporterService.sendQueryToBridgeheadAndExecute(projectCode, bridgehead)); + return convertToResponseEntity(() -> this.projectBridgeheadService.scheduleSendQueryToBridgeheadAndExecute(projectCode, bridgehead)); } @RoleConstraints(projectRoles = {ProjectRole.DEVELOPER, ProjectRole.PILOT, ProjectRole.FINAL}) diff --git a/src/main/java/de/samply/db/model/ProjectBridgehead.java b/src/main/java/de/samply/db/model/ProjectBridgehead.java index 7f2f3ac..fe4df4c 100644 --- a/src/main/java/de/samply/db/model/ProjectBridgehead.java +++ b/src/main/java/de/samply/db/model/ProjectBridgehead.java @@ -2,6 +2,7 @@ import de.samply.project.state.ProjectBridgeheadState; +import de.samply.query.QueryState; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Data; @@ -35,4 +36,18 @@ 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; + + } diff --git a/src/main/java/de/samply/db/repository/ProjectBridgeheadRepository.java b/src/main/java/de/samply/db/repository/ProjectBridgeheadRepository.java index 1963eee..a1ad68e 100644 --- a/src/main/java/de/samply/db/repository/ProjectBridgeheadRepository.java +++ b/src/main/java/de/samply/db/repository/ProjectBridgeheadRepository.java @@ -2,10 +2,10 @@ import de.samply.db.model.Project; import de.samply.db.model.ProjectBridgehead; -import de.samply.db.model.ProjectBridgeheadUser; import de.samply.project.ProjectType; 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; @@ -32,4 +32,7 @@ public interface ProjectBridgeheadRepository extends JpaRepository 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); + } diff --git a/src/main/java/de/samply/exporter/ExporterJob.java b/src/main/java/de/samply/exporter/ExporterJob.java new file mode 100644 index 0000000..9181071 --- /dev/null +++ b/src/main/java/de/samply/exporter/ExporterJob.java @@ -0,0 +1,88 @@ +package de.samply.exporter; + +import de.samply.app.ProjectManagerConst; +import de.samply.db.model.ProjectBridgehead; +import de.samply.db.repository.ProjectBridgeheadRepository; +import de.samply.project.state.ProjectState; +import de.samply.query.QueryState; +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; +import java.util.function.Function; + +@Component +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); + + public ExporterJob( + @Value(ProjectManagerConst.ENABLE_EXPORTER_SV) boolean enabled, + ExporterService exporterService, + ProjectBridgeheadRepository projectBridgeheadRepository) { + this.enabled = enabled; + this.exporterService = exporterService; + this.projectBridgeheadRepository = projectBridgeheadRepository; + } + + @Scheduled(cron = ProjectManagerConst.EXPORTER_CRON_EXPRESSION_SV) + public void checkExports() { + if (enabled) { + Mono.when( + checkQueriesToSend(), + checkQueriesToSendAndExecute(), + checkQueriesAlreadySent(), + checkQueriesAlreadySentToBeExecuted()).block(); + + } + } + + private Mono checkQueriesToSend() { + return checkQueries(QueryState.TO_BE_SENT, QueryState.SENDING, exporterService::sendQueryToBridgehead); + } + + private Mono checkQueriesToSendAndExecute() { + return checkQueries(QueryState.TO_BE_SENT_AND_EXECUTED, QueryState.SENDING_AND_EXECUTING, exporterService::sendQueryToBridgeheadAndExecute); + } + + private Mono checkQueriesAlreadySent() { + return checkQueries(QueryState.SENDING, QueryState.FINISHED, exporterService::checkIfQueryIsAlreadySent); + } + + private Mono checkQueriesAlreadySentToBeExecuted() { + return checkQueries(QueryState.SENDING_AND_EXECUTING, QueryState.FINISHED, exporterService::checkIfQueryIsAlreadySent, Optional.of( + exporterServiceResult -> + exporterService.fetchExporterExecutionIdFromExporterResponse(exporterServiceResult.result()).ifPresent(exportExecutionId -> + exporterServiceResult.projectBridgehead().setExporterExecutionId(exportExecutionId)))); + } + + private Mono checkQueries(QueryState initialQueryState, QueryState finalQueryState, + Function> exporterServiceFunction) { + return checkQueries(initialQueryState, finalQueryState, exporterServiceFunction, Optional.empty()); + } + + private Mono checkQueries(QueryState initialQueryState, QueryState finalQueryState, + Function> exporterServiceFunction, Optional> exporterServiceResultConsumer) { + return Flux.fromIterable(projectBridgeheadRepository.getByQueryStateAndProjectState(initialQueryState, activeStates)) + .flatMap(exporterServiceFunction) + .flatMap(exporterServiceResult -> { + exporterServiceResultConsumer.ifPresent(consumer -> consumer.accept(exporterServiceResult)); + ProjectBridgehead projectBridgehead = exporterServiceResult.projectBridgehead(); + projectBridgehead.setQueryState(finalQueryState); + projectBridgehead.setExporterResponse(exporterServiceResult.result()); + projectBridgehead.setModifiedAt(Instant.now()); + projectBridgeheadRepository.save(projectBridgehead); + return Mono.empty(); + }).then(); + } + +} diff --git a/src/main/java/de/samply/exporter/ExporterService.java b/src/main/java/de/samply/exporter/ExporterService.java index 712e481..06c1f83 100644 --- a/src/main/java/de/samply/exporter/ExporterService.java +++ b/src/main/java/de/samply/exporter/ExporterService.java @@ -11,14 +11,13 @@ import de.samply.db.model.Query; import de.samply.db.repository.ProjectBridgeheadDataShieldRepository; import de.samply.db.repository.ProjectBridgeheadRepository; -import de.samply.db.repository.ProjectRepository; import de.samply.exporter.focus.FocusQuery; import de.samply.exporter.focus.FocusService; import de.samply.exporter.focus.FocusServiceException; import de.samply.notification.NotificationService; import de.samply.notification.OperationType; import de.samply.project.ProjectType; -import de.samply.security.SessionUser; +import de.samply.query.QueryState; import de.samply.utils.Base64Utils; import de.samply.utils.WebClientFactory; import jakarta.validation.constraints.NotNull; @@ -27,13 +26,14 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; -import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.springframework.web.reactive.function.client.WebClient; -import org.springframework.web.reactive.function.client.WebClientResponseException; +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.Map; @@ -43,26 +43,20 @@ @Service @Slf4j public class ExporterService { + private final ProjectBridgeheadRepository projectBridgeheadRepository; private final FocusService focusService; private final WebClient webClient; - private final int webClientMaxNumberOfRetries; - 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 webClientBufferSizeInBytes; - private final SessionUser sessionUser; - private final ProjectRepository projectRepository; - private final ProjectBridgeheadRepository projectBridgeheadRepository; private final ProjectBridgeheadDataShieldRepository projectBridgeheadDataShieldRepository; private final NotificationService notificationService; private final Set exportTemplates; private final Set datashieldTemplates; private final String focusProjectManagerId; private final String focusApiKey; + + private final String focusWaitTime; + private final String focusWaitCount; + private final int maxTimeToWaitFocusTaskInMinutes; private ObjectMapper objectMapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT) .registerModule(new JavaTimeModule()).configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); @@ -72,94 +66,58 @@ public ExporterService( @Value(ProjectManagerConst.FOCUS_URL_SV) String focusUrl, @Value(ProjectManagerConst.EXPORT_TEMPLATES_SV) Set exportTemplates, @Value(ProjectManagerConst.DATASHIELD_TEMPLATES_SV) Set datashieldTemplates, - @Value(ProjectManagerConst.WEBCLIENT_REQUEST_TIMEOUT_IN_SECONDS_SV) Integer webClientRequestTimeoutInSeconds, - @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_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, + @Value(ProjectManagerConst.FOCUS_TTL_SV) String focusWaitTime, + @Value(ProjectManagerConst.FOCUS_FAILURE_STRATEGY_MAX_TRIES_SV) String focusWaitCount, + @Value(ProjectManagerConst.MAX_TIME_TO_WAIT_FOCUS_TASK_IN_MINUTES_SV) int maxTimeToWaitFocusTaskInMinutes, FocusService focusService, - ProjectRepository projectRepository, - SessionUser sessionUser, - ProjectBridgeheadRepository projectBridgeheadRepository, ProjectBridgeheadDataShieldRepository projectBridgeheadDataShieldRepository, NotificationService notificationService, - WebClientFactory webClientFactory) { + WebClientFactory webClientFactory, + ProjectBridgeheadRepository projectBridgeheadRepository) { this.focusService = focusService; - this.webClientMaxNumberOfRetries = webClientMaxNumberOfRetries; - this.webClientTimeInSecondsAfterRetryWithFailure = webClientTimeInSecondsAfterRetryWithFailure; - this.webClientRequestTimeoutInSeconds = webClientRequestTimeoutInSeconds; - this.webClientConnectionTimeoutInSeconds = webClientConnectionTimeoutInSeconds; - this.webClientTcpKeepIdleInSeconds = webClientTcpKeepIdleInSeconds; - this.webClientTcpKeepIntervalInSeconds = webClientTcpKeepIntervalInSeconds; - this.webClientTcpKeepConnetionNumberOfTries = webClientTcpKeepConnetionNumberOfTries; - this.webClientBufferSizeInBytes = webClientBufferSizeInBytes; - this.sessionUser = sessionUser; - this.projectRepository = projectRepository; - this.projectBridgeheadRepository = projectBridgeheadRepository; this.projectBridgeheadDataShieldRepository = projectBridgeheadDataShieldRepository; this.notificationService = notificationService; this.exportTemplates = exportTemplates; this.datashieldTemplates = datashieldTemplates; this.focusProjectManagerId = focusProjectManagerId; + this.focusWaitTime = focusWaitTime; + this.focusWaitCount = focusWaitCount; + this.maxTimeToWaitFocusTaskInMinutes = maxTimeToWaitFocusTaskInMinutes; this.webClient = webClientFactory.createWebClient(focusUrl); this.focusApiKey = focusApiKey; + this.projectBridgeheadRepository = projectBridgeheadRepository; } - public void sendQueryToBridgehead(@NotNull String projectCode, @NotNull String bridgehead) throws ExporterServiceException { - log.info("Sending query of project " + projectCode + " to bridgehead " + bridgehead + " ..."); - postRequest(bridgehead, projectCode, generateFocusBody(projectCode, bridgehead, false), true); - } - - public void sendQueryToBridgeheadAndExecute(@NotNull String projectCode, @NotNull String bridgehead) throws ExporterServiceException { - log.info("Sending query of project " + projectCode + " to bridgehead " + bridgehead + " to be executed..."); - postRequest(bridgehead, projectCode, generateFocusBody(projectCode, bridgehead, true), true); + public Mono sendQueryToBridgehead(ProjectBridgehead projectBridgehead) throws ExporterServiceException { + log.info("Sending query of project " + projectBridgehead.getProject().getCode() + " to bridgehead " + projectBridgehead.getBridgehead() + " ..."); + return postRequest(projectBridgehead, generateFocusBody(projectBridgehead, false), true); } - private void postRequest(String bridgehead, String projectCode, FocusQuery focusQuery, boolean toBeExecuted) throws ExporterServiceException { - postRequest(bridgehead, projectCode, focusQuery, toBeExecuted, 0); - resetProjectBridgeheadDataShield(projectCode, bridgehead); + public Mono sendQueryToBridgeheadAndExecute(ProjectBridgehead projectBridgehead) throws ExporterServiceException { + log.info("Sending query of project " + projectBridgehead.getProject().getCode() + " to bridgehead " + projectBridgehead.getBridgehead() + " to be executed..."); + return postRequest(projectBridgehead, generateFocusBody(projectBridgehead, true), true); } - private void postRequest(String bridgehead, String projectCode, FocusQuery focusQuery, boolean toBeExecuted, int numberOfRetries) { - String email = sessionUser.getEmail(); - webClient.post().uri(uriBuilder -> uriBuilder.path(ProjectManagerConst.FOCUS_TASK).build()) + private Mono postRequest(ProjectBridgehead projectBridgehead, FocusQuery focusQuery, boolean toBeExecuted) { + return webClient.post() + .uri(uriBuilder -> uriBuilder.path(ProjectManagerConst.FOCUS_TASK_PATH).build()) .header(HttpHeaders.AUTHORIZATION, fetchAuthorization()) .contentType(MediaType.APPLICATION_JSON) .bodyValue(focusQuery) - .retrieve() - .bodyToMono(String.class) - .doOnError(WebClientResponseException.class, ex -> { - HttpStatusCode statusCode = ex.getStatusCode(); - String error = ExceptionUtils.getStackTrace(ex); - log.error(error); - if (statusCode.equals(HttpStatus.BAD_REQUEST)) { - log.error("Received 400 Bad Request"); - } else if (statusCode.is5xxServerError()) { - log.error("Received Server Error: " + statusCode); + .exchangeToMono(clientResponse -> { + if (clientResponse.statusCode().equals(HttpStatus.CREATED)) { + createBridgeheadNotification(HttpStatus.OK, null, projectBridgehead, projectBridgehead.getExporterUser(), fetchBridgeheadOperationType(toBeExecuted)); + resetProjectBridgeheadDataShield(projectBridgehead); + return Mono.just(new ExporterServiceResult(projectBridgehead, focusQuery.getId())); } else { - log.error("Received HTTP Status Code: " + statusCode); + log.error("Http Error " + clientResponse.statusCode() + " posting task " + focusQuery.getId() + " : " + focusQuery.getBody() + + " for project " + projectBridgehead.getProject().getCode() + " and bridgehead " + projectBridgehead.getBridgehead()); + return clientResponse.bodyToMono(String.class).flatMap(errorBody -> { + log.error("Error: {}", errorBody); + return Mono.error(new RuntimeException(errorBody)); + }); } - // We don't use the normal retry functionality of webclient, because focus requires to change the focus query ID after every retry - if (numberOfRetries >= webClientMaxNumberOfRetries) { - createBridgeheadNotification((HttpStatus) ex.getStatusCode(), error, bridgehead, projectCode, email, toBeExecuted); - } else { - waitUntilNextRetry(); - focusQuery.setId(focusService.generateId()); // Generate new Focus Query ID - postRequest(bridgehead, projectCode, focusQuery, toBeExecuted, numberOfRetries + 1); - } - }) - .subscribe(result -> createBridgeheadNotification(HttpStatus.OK, null, bridgehead, projectCode, email, toBeExecuted)); - } - - private void waitUntilNextRetry() { - try { - Thread.sleep(webClientTimeInSecondsAfterRetryWithFailure * 1000); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } + }); } private String fetchAuthorization() { @@ -167,10 +125,9 @@ private String fetchAuthorization() { } private void createBridgeheadNotification( - HttpStatus status, String error, String bridgehead, String projectCode, String email, boolean toBeExecuted) { + HttpStatus status, String error, ProjectBridgehead projectBridgehead, String email, OperationType operationType) { notificationService.createNotification( - projectCode, bridgehead, email, - fetchBridgeheadOperationType(toBeExecuted), null, error, status); + projectBridgehead.getProject().getCode(), projectBridgehead.getBridgehead(), email, operationType, null, error, status); } private OperationType fetchBridgeheadOperationType(boolean toBeExecuted) { @@ -185,61 +142,53 @@ private String convertToBase64String(Object jsonObject) { } } - private String generateExportQueryInBase64ForExporterRequest(String projectCode) + private String generateExportQueryInBase64ForExporterRequest(ProjectBridgehead projectBridgehead) throws ExporterServiceException { - Optional project = projectRepository.findByCode(projectCode); - if (project.isEmpty()) { - throw new ExporterServiceException("Project " + projectCode + " not found"); - } - Query query = project.get().getQuery(); + Query query = projectBridgehead.getProject().getQuery(); Map result = Map.of( ProjectManagerConst.EXPORTER_PARAM_QUERY, query.getQuery(), ProjectManagerConst.EXPORTER_PARAM_QUERY_FORMAT, query.getQueryFormat().name(), ProjectManagerConst.EXPORTER_PARAM_QUERY_LABEL, query.getLabel(), ProjectManagerConst.EXPORTER_PARAM_QUERY_DESCRIPTION, query.getDescription(), - ProjectManagerConst.EXPORTER_PARAM_QUERY_CONTEXT, generateQueryContextForExporter(query.getContext(), projectCode), - ProjectManagerConst.EXPORTER_PARAM_QUERY_CONTACT_ID, project.get().getCreatorEmail(), - ProjectManagerConst.EXPORTER_PARAM_QUERY_EXECUTION_CONTACT_ID, sessionUser.getEmail(), + 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_EXPIRATION_DATE, convertToString(project.get().getExpiresAt())); + ProjectManagerConst.EXPORTER_PARAM_QUERY_EXPIRATION_DATE, convertToString(projectBridgehead.getProject().getExpiresAt())); result.values().removeIf(value -> !StringUtils.hasText(value)); return convertToBase64String(result); } - private String generateExporterQueryInBase64ForExporterCreateQuery(String projectCode) + private String generateExporterQueryInBase64ForExporterCreateQuery(Project project) throws ExporterServiceException { - Optional project = projectRepository.findByCode(projectCode); - if (project.isEmpty()) { - throw new ExporterServiceException("Project " + projectCode + " not found"); - } - Query query = project.get().getQuery(); + Query query = project.getQuery(); Map result = Map.of( ProjectManagerConst.EXPORTER_PARAM_QUERY, query.getQuery(), ProjectManagerConst.EXPORTER_PARAM_QUERY_FORMAT, query.getQueryFormat().name(), ProjectManagerConst.EXPORTER_PARAM_QUERY_LABEL, query.getLabel(), ProjectManagerConst.EXPORTER_PARAM_QUERY_DESCRIPTION, query.getDescription(), - ProjectManagerConst.EXPORTER_PARAM_QUERY_CONTEXT, generateQueryContextForExporter(query.getContext(), projectCode), - ProjectManagerConst.EXPORTER_PARAM_QUERY_CONTACT_ID, project.get().getCreatorEmail(), + ProjectManagerConst.EXPORTER_PARAM_QUERY_CONTEXT, generateQueryContextForExporter(query.getContext(), project.getCode()), + ProjectManagerConst.EXPORTER_PARAM_QUERY_CONTACT_ID, project.getCreatorEmail(), ProjectManagerConst.EXPORTER_PARAM_DEFAULT_OUTPUT_FORMAT, query.getOutputFormat().name(), ProjectManagerConst.EXPORTER_PARAM_DEFAULT_TEMPLATE_ID, query.getTemplateId(), - ProjectManagerConst.EXPORTER_PARAM_QUERY_EXPIRATION_DATE, convertToString(project.get().getExpiresAt())); + ProjectManagerConst.EXPORTER_PARAM_QUERY_EXPIRATION_DATE, convertToString(project.getExpiresAt())); result.values().removeIf(value -> !StringUtils.hasText(value)); return convertToBase64String(result); } - private FocusQuery generateFocusBody(String projectCode, String bridgehead, boolean toBeExecuted) throws ExporterServiceException { + private FocusQuery generateFocusBody(ProjectBridgehead projectBridgehead, boolean toBeExecuted) throws ExporterServiceException { try { - return generateFocusQueryWithoutExceptionHandling(projectCode, bridgehead, toBeExecuted); + return generateFocusQueryWithoutExceptionHandling(projectBridgehead, toBeExecuted); } catch (FocusServiceException e) { throw new ExporterServiceException(e); } } - private FocusQuery generateFocusQueryWithoutExceptionHandling(String projectCode, String bridgehead, boolean toBeExecuted) throws ExporterServiceException, FocusServiceException { - String exporterQueryInBase64 = (toBeExecuted) ? generateExportQueryInBase64ForExporterRequest(projectCode) : - generateExporterQueryInBase64ForExporterCreateQuery(projectCode); - return focusService.generateFocusQuery(exporterQueryInBase64, toBeExecuted, bridgehead); + private FocusQuery generateFocusQueryWithoutExceptionHandling(ProjectBridgehead projectBridgehead, boolean toBeExecuted) throws FocusServiceException { + String exporterQueryInBase64 = (toBeExecuted) ? generateExportQueryInBase64ForExporterRequest(projectBridgehead) : + generateExporterQueryInBase64ForExporterCreateQuery(projectBridgehead.getProject()); + return focusService.generateFocusQuery(exporterQueryInBase64, toBeExecuted, projectBridgehead.getBridgehead()); } @@ -250,7 +199,7 @@ private String convertToString(LocalDate date) { private String generateQueryContextForExporter(String queryContext, String projectCode) { String context = ProjectManagerConst.EXPORTER_QUERY_CONTEXT_PROJECT_ID + '=' + projectCode; if (StringUtils.hasText(queryContext)) { - context += ',' + queryContext; + context += ProjectManagerConst.EXPORTER_QUERY_CONTEXT_SEPARATOR + queryContext; } return Base64Utils.encode(context); } @@ -262,21 +211,13 @@ public Set getExporterTemplates(@NotNull ProjectType projectType) { }; } - private void resetProjectBridgeheadDataShield(@NotNull String projectCode, @NotNull String bridgehead) throws ExporterServiceException { - Optional project = this.projectRepository.findByCode(projectCode); - if (project.isEmpty()) { - throw new ExporterServiceException("Project " + projectCode + " not found"); - } - if (project.get().getType() == ProjectType.DATASHIELD) { - Optional projectBridgehead = this.projectBridgeheadRepository.findFirstByBridgeheadAndProject(bridgehead, project.get()); - if (projectBridgehead.isEmpty()) { - throw new ExporterServiceException("Bridgehead " + bridgehead + " for project " + projectCode + " not found"); - } - Optional projectBridgeheadInDataSHIELD = this.projectBridgeheadDataShieldRepository.findByProjectBridgehead(projectBridgehead.get()); + private void resetProjectBridgeheadDataShield(ProjectBridgehead projectBridgehead) { + if (projectBridgehead.getProject().getType() == ProjectType.DATASHIELD) { + Optional projectBridgeheadInDataSHIELD = this.projectBridgeheadDataShieldRepository.findByProjectBridgehead(projectBridgehead); ProjectBridgeheadDataShield result; if (projectBridgeheadInDataSHIELD.isEmpty()) { result = new ProjectBridgeheadDataShield(); - result.setProjectBridgehead(projectBridgehead.get()); + result.setProjectBridgehead(projectBridgehead); } else { result = projectBridgeheadInDataSHIELD.get(); } @@ -285,4 +226,71 @@ private void resetProjectBridgeheadDataShield(@NotNull String projectCode, @NotN } } + public Mono checkIfQueryIsAlreadySent(ProjectBridgehead projectBridgehead) { + return webClient.get() + .uri(uriBuilder -> uriBuilder + .path(ProjectManagerConst.FOCUS_TASK_PATH + "/" + projectBridgehead.getExporterResponse() + ProjectManagerConst.FOCUS_TASK_RESULTS_PATH) + .queryParam(ProjectManagerConst.FOCUS_TASK_WAIT_TIME_PARAM, focusWaitTime) + .queryParam(ProjectManagerConst.FOCUS_TASK_WAIT_COUNT_PARAM, focusWaitCount).build()) + .header(HttpHeaders.AUTHORIZATION, fetchAuthorization()) + .exchangeToMono(clientResponse -> { + if (clientResponse.statusCode().equals(HttpStatus.OK) || clientResponse.statusCode().equals(HttpStatus.PARTIAL_CONTENT)) { + OperationType operationType = projectBridgehead.getQueryState() == QueryState.SENDING ? OperationType.CHECK_SEND_QUERY : OperationType.CHECK_SEND_AND_EXECUTE_QUERY; + createBridgeheadNotification(HttpStatus.OK, null, projectBridgehead, projectBridgehead.getExporterUser(), operationType); + return clientResponse.bodyToMono(String.class).flatMap(body -> Mono.just(new ExporterServiceResult(projectBridgehead, body))); + } else { + log.error("Http Error " + clientResponse.statusCode() + " checking task " + projectBridgehead.getExporterResponse() + + " for project " + projectBridgehead.getProject().getCode() + " and bridgehead " + projectBridgehead.getBridgehead()); + if (isQueryStateToBeChangedToError((HttpStatus) clientResponse.statusCode(), projectBridgehead)) { + projectBridgehead.setQueryState(QueryState.ERROR); + projectBridgehead.setModifiedAt(Instant.now()); + projectBridgeheadRepository.save(projectBridgehead); + } + return clientResponse.bodyToMono(String.class).flatMap(errorBody -> { + log.error("Error: {}", errorBody); + return Mono.error(new RuntimeException(errorBody)); + }); + } + }); + } + + private boolean isQueryStateToBeChangedToError(HttpStatus httpStatus, ProjectBridgehead projectBridgehead) { + if (httpStatus == HttpStatus.NOT_FOUND) { + return Duration.between(projectBridgehead.getModifiedAt(), Instant.now()).toMinutes() > maxTimeToWaitFocusTaskInMinutes; + } + return httpStatus != HttpStatus.NO_CONTENT; + } + + public Optional fetchExporterExecutionIdFromExporterResponse(String exporterResponse) { + if (exporterResponse != null) { + Optional focusQuery = deserializeFocusResponse(exporterResponse); + if (focusQuery.isPresent() && focusQuery.get().length > 0 && focusQuery.get()[0].getBody() != null) { + return fetchQueryExecutionIdFromQueryExecutionIdUrl(Base64Utils.decode(focusQuery.get()[0].getBody())); + } + } + return Optional.empty(); + } + + private Optional deserializeFocusResponse(String exporterResponse) { + try { + return Optional.of(focusService.deserializeFocusResponse(exporterResponse)); + } catch (FocusServiceException e) { + log.error(ExceptionUtils.getStackTrace(e)); + return Optional.empty(); + } + } + + private Optional fetchQueryExecutionIdFromQueryExecutionIdUrl(String queryExecutionIdUrl) { + if (queryExecutionIdUrl != null) { + String searchedString = ProjectManagerConst.EXPORTER_PARAM_QUERY_EXECUTION_ID + "="; + int index = queryExecutionIdUrl.indexOf(searchedString); + if (index >= 0 && queryExecutionIdUrl.length() > index + searchedString.length()) { + String queryExecutionId = queryExecutionIdUrl.substring(index + searchedString.length()); + index = queryExecutionId.indexOf("\""); + return Optional.of(index > 0 ? queryExecutionId.substring(0, index) : queryExecutionId); + } + } + return Optional.empty(); + } + } diff --git a/src/main/java/de/samply/exporter/ExporterServiceException.java b/src/main/java/de/samply/exporter/ExporterServiceException.java index 9425b1b..2d5a551 100644 --- a/src/main/java/de/samply/exporter/ExporterServiceException.java +++ b/src/main/java/de/samply/exporter/ExporterServiceException.java @@ -1,6 +1,6 @@ package de.samply.exporter; -public class ExporterServiceException extends Exception{ +public class ExporterServiceException extends RuntimeException { public ExporterServiceException(String message) { super(message); diff --git a/src/main/java/de/samply/exporter/ExporterServiceResult.java b/src/main/java/de/samply/exporter/ExporterServiceResult.java new file mode 100644 index 0000000..2b28fab --- /dev/null +++ b/src/main/java/de/samply/exporter/ExporterServiceResult.java @@ -0,0 +1,6 @@ +package de.samply.exporter; + +import de.samply.db.model.ProjectBridgehead; + +public record ExporterServiceResult(ProjectBridgehead projectBridgehead, String result) { +} diff --git a/src/main/java/de/samply/exporter/focus/FocusQuery.java b/src/main/java/de/samply/exporter/focus/FocusQuery.java index 9c204c8..c1c61e3 100644 --- a/src/main/java/de/samply/exporter/focus/FocusQuery.java +++ b/src/main/java/de/samply/exporter/focus/FocusQuery.java @@ -20,5 +20,9 @@ public class FocusQuery { private String[] to; @JsonProperty("ttl") private String ttl; + @JsonProperty("status") + private String status; + @JsonProperty("task") + private String task; } diff --git a/src/main/java/de/samply/exporter/focus/FocusService.java b/src/main/java/de/samply/exporter/focus/FocusService.java index d7e0976..827825e 100644 --- a/src/main/java/de/samply/exporter/focus/FocusService.java +++ b/src/main/java/de/samply/exporter/focus/FocusService.java @@ -1,5 +1,7 @@ package de.samply.exporter.focus; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import de.samply.app.ProjectManagerConst; import de.samply.bridgehead.BridgeheadConfiguration; import org.springframework.beans.factory.annotation.Value; @@ -15,6 +17,7 @@ public class FocusService { private final String ttl; private final FailureStrategy failureStrategy; private final BridgeheadConfiguration bridgeheadConfiguration; + private final ObjectMapper objectMapper = new ObjectMapper(); public FocusService( @@ -69,4 +72,12 @@ private FailureStrategy createFailureStrategy(int backoff, int maxRetries) { return failureStrategy; } + public FocusQuery[] deserializeFocusResponse(String focusResponse) throws FocusServiceException { + try { + return objectMapper.readValue(focusResponse, FocusQuery[].class); + } catch (JsonProcessingException e) { + throw new FocusServiceException(e); + } + } + } diff --git a/src/main/java/de/samply/exporter/focus/FocusServiceException.java b/src/main/java/de/samply/exporter/focus/FocusServiceException.java index 0dc0b8a..be79146 100644 --- a/src/main/java/de/samply/exporter/focus/FocusServiceException.java +++ b/src/main/java/de/samply/exporter/focus/FocusServiceException.java @@ -4,4 +4,9 @@ public class FocusServiceException extends Exception{ public FocusServiceException(String message) { super(message); } + + public FocusServiceException(Throwable cause) { + super(cause); + } + } diff --git a/src/main/java/de/samply/frontend/dto/DtoFactory.java b/src/main/java/de/samply/frontend/dto/DtoFactory.java index 418b7fb..b25a7fe 100644 --- a/src/main/java/de/samply/frontend/dto/DtoFactory.java +++ b/src/main/java/de/samply/frontend/dto/DtoFactory.java @@ -91,7 +91,8 @@ public static ProjectBridgehead convert(@NotNull de.samply.db.model.ProjectBridg projectBridgehead.getProject().getCode(), projectBridgehead.getBridgehead(), projectBridgehead.getState(), - projectBridgehead.getModifiedAt() + projectBridgehead.getModifiedAt(), + projectBridgehead.getQueryState() ); } diff --git a/src/main/java/de/samply/frontend/dto/ProjectBridgehead.java b/src/main/java/de/samply/frontend/dto/ProjectBridgehead.java index 4c29a88..39fa030 100644 --- a/src/main/java/de/samply/frontend/dto/ProjectBridgehead.java +++ b/src/main/java/de/samply/frontend/dto/ProjectBridgehead.java @@ -1,6 +1,7 @@ package de.samply.frontend.dto; import de.samply.project.state.ProjectBridgeheadState; +import de.samply.query.QueryState; import java.time.Instant; @@ -8,6 +9,7 @@ public record ProjectBridgehead( String projectCode, String bridgehead, ProjectBridgeheadState state, - Instant modifiedAt + Instant modifiedAt, + QueryState queryState ) { } diff --git a/src/main/java/de/samply/notification/OperationType.java b/src/main/java/de/samply/notification/OperationType.java index 6ca879a..c8dd290 100644 --- a/src/main/java/de/samply/notification/OperationType.java +++ b/src/main/java/de/samply/notification/OperationType.java @@ -1,6 +1,8 @@ package de.samply.notification; public enum OperationType { + CHECK_SEND_QUERY, + CHECK_SEND_AND_EXECUTE_QUERY, SEND_QUERY_TO_BRIDGEHEAD_AND_EXECUTE, SEND_QUERY_TO_BRIDGEHEAD, CREATE_DATASHIELD_TOKEN, diff --git a/src/main/java/de/samply/project/ProjectBridgeheadService.java b/src/main/java/de/samply/project/ProjectBridgeheadService.java index 38a8fe5..b241c0c 100644 --- a/src/main/java/de/samply/project/ProjectBridgeheadService.java +++ b/src/main/java/de/samply/project/ProjectBridgeheadService.java @@ -9,11 +9,13 @@ import de.samply.notification.NotificationService; import de.samply.notification.OperationType; import de.samply.project.state.ProjectBridgeheadState; +import de.samply.query.QueryState; import de.samply.security.SessionUser; import de.samply.user.roles.OrganisationRole; import jakarta.validation.constraints.NotNull; import org.springframework.stereotype.Service; +import java.time.Instant; import java.util.*; @Service @@ -105,4 +107,30 @@ 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); + } + + public void scheduleSendQueryToBridgeheadAndExecute(@NotNull String projectCode, @NotNull String bridgehead) throws ProjectBridgeheadServiceException { + changeQueryState(projectCode, bridgehead, QueryState.TO_BE_SENT_AND_EXECUTED); + } + + 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()); + } + + } diff --git a/src/main/java/de/samply/query/QueryState.java b/src/main/java/de/samply/query/QueryState.java new file mode 100644 index 0000000..1ab0066 --- /dev/null +++ b/src/main/java/de/samply/query/QueryState.java @@ -0,0 +1,11 @@ +package de.samply.query; + +public enum QueryState { + CREATED, + TO_BE_SENT, + TO_BE_SENT_AND_EXECUTED, + SENDING, + SENDING_AND_EXECUTING, + ERROR, + FINISHED +} 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 519bf41..7f1a2da 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 @@ -37,11 +37,15 @@ CREATE TABLE samply.project CREATE TABLE samply.project_bridgehead ( - 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 NOT NULL PRIMARY KEY, + project_id BIGINT NOT NULL, + bridgehead TEXT NOT NULL, + modified_at TIMESTAMP NOT NULL, + state TEXT NOT NULL, + query_state TEXT NOT NULL, + exporter_response TEXT, + exporter_user TEXT, + exporter_execution_id TEXT ); CREATE TABLE samply.project_bridgehead_user