diff --git a/pom.xml b/pom.xml index c32e0ec5a..5ef8c6274 100644 --- a/pom.xml +++ b/pom.xml @@ -36,7 +36,7 @@ 1.23.0 1.2.3 - 1.1.0 + 1.1.1 diff --git a/src/main/java/fr/insee/genesis/controller/dto/KraftwerkExecutionScheduleInput.java b/src/main/java/fr/insee/genesis/controller/dto/KraftwerkExecutionScheduleInput.java new file mode 100644 index 000000000..e7dc655f0 --- /dev/null +++ b/src/main/java/fr/insee/genesis/controller/dto/KraftwerkExecutionScheduleInput.java @@ -0,0 +1,33 @@ +package fr.insee.genesis.controller.dto; + +import fr.insee.genesis.controller.utils.ExportType; +import fr.insee.genesis.domain.model.context.schedule.DestinationType; +import fr.insee.genesis.domain.model.context.schedule.TrustParameters; +import fr.insee.genesis.domain.model.surveyunit.Mode; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class KraftwerkExecutionScheduleInput { + private String collectionInstrumentId; + private String scheduleUuid; + private ExportType exportType; + private String frequency; + private LocalDateTime startDate; + private LocalDateTime endDate; + private Mode mode; + private DestinationType destinationType; + private boolean addStates; + private String destinationFolder; + private boolean useAsymmetricEncryption; + private boolean useSymmetricEncryption; + private TrustParameters trustParameters; + private Integer batchSize; +} diff --git a/src/main/java/fr/insee/genesis/controller/dto/ScheduleRequestDto.java b/src/main/java/fr/insee/genesis/controller/dto/ScheduleRequestDto.java new file mode 100644 index 000000000..ccb0f7d23 --- /dev/null +++ b/src/main/java/fr/insee/genesis/controller/dto/ScheduleRequestDto.java @@ -0,0 +1,72 @@ +package fr.insee.genesis.controller.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import fr.insee.genesis.controller.utils.ExportType; +import fr.insee.genesis.controller.validation.schedule.ValidScheduleRequest; +import fr.insee.genesis.domain.model.context.schedule.DestinationType; +import fr.insee.genesis.domain.model.surveyunit.Mode; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Request used to schedule a Kraftwerk export workflow") +@ValidScheduleRequest +public class ScheduleRequestDto { + + @NotBlank + @Schema(description = "Collection instrument to call Kraftwerk on", example = "EAP2026A00", requiredMode = Schema.RequiredMode.REQUIRED) + private String collectionInstrumentId; + + @NotNull + @Schema(description = "Export type", allowableValues = {"JSON", "CSV_PARQUET"}, requiredMode = Schema.RequiredMode.REQUIRED) + private ExportType exportType; + + @NotBlank + @Schema(description = "Frequency in Spring cron format (6 inputs). Example: 0 0 6 * * *", example = "0 0 6 * * *", requiredMode = Schema.RequiredMode.REQUIRED) + private String frequency; + + @NotNull + @Schema(description = "Schedule effective date and time", example = "2024-01-01T12:00:00", requiredMode = Schema.RequiredMode.REQUIRED) + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss") + private LocalDateTime scheduleBeginDate; + + @NotNull + @Schema(description = "Schedule end date and time", example = "2024-01-01T12:00:00", requiredMode = Schema.RequiredMode.REQUIRED) + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss") + private LocalDateTime scheduleEndDate; + + private Mode mode; + + @Schema(defaultValue = "APPLISHARE") + private DestinationType destinationType = DestinationType.APPLISHARE; + + @Schema(defaultValue = "false") + private boolean useSymmetricEncryption = false; + + @Schema(defaultValue = "false") + private boolean useAsymmetricEncryption = false; + + @Schema(description = "Encryption vault path") + private String encryptionVaultPath = ""; + + @Schema(defaultValue = "false") + private boolean useSignature = false; + + @Schema(description = "Add variable states to export", example = "false", defaultValue = "false") + private boolean addStates = false; + + @NotBlank + @Schema(description = "Destination folder (Applishare or S3)", requiredMode = Schema.RequiredMode.REQUIRED) + private String destinationFolder; + + @Schema(description = "Batch size", defaultValue = "100") + private Integer batchSize; +} \ No newline at end of file diff --git a/src/main/java/fr/insee/genesis/controller/dto/rawdata/ScheduleResponseDto.java b/src/main/java/fr/insee/genesis/controller/dto/rawdata/ScheduleResponseDto.java new file mode 100644 index 000000000..87d54c309 --- /dev/null +++ b/src/main/java/fr/insee/genesis/controller/dto/rawdata/ScheduleResponseDto.java @@ -0,0 +1,42 @@ +package fr.insee.genesis.controller.dto.rawdata; + +import com.fasterxml.jackson.annotation.JsonFormat; +import fr.insee.genesis.controller.utils.ExportType; +import fr.insee.genesis.domain.model.context.schedule.DestinationType; +import fr.insee.genesis.domain.model.surveyunit.Mode; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ScheduleResponseDto { + + private String scheduleUuid; + private String collectionInstrumentId; + private LocalDateTime lastExecution; + + private String frequency; + private ExportType exportType; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss") + private LocalDateTime scheduleBeginDate; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss") + private LocalDateTime scheduleEndDate; + + private Mode mode; + private DestinationType destinationType; + private boolean useAsymmetricEncryption; + private boolean useSymmetricEncryption; + private String encryptionVaultPath; + private boolean useSignature; + private boolean addStates; + private String destinationFolder; + private Integer batchSize; +} diff --git a/src/main/java/fr/insee/genesis/controller/rest/ControllerExceptionHandler.java b/src/main/java/fr/insee/genesis/controller/rest/ControllerExceptionHandler.java new file mode 100644 index 000000000..077c98c59 --- /dev/null +++ b/src/main/java/fr/insee/genesis/controller/rest/ControllerExceptionHandler.java @@ -0,0 +1,43 @@ +package fr.insee.genesis.controller.rest; + +import org.springframework.dao.DuplicateKeyException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.HashMap; +import java.util.Map; + +@RestControllerAdvice +public class ControllerExceptionHandler { + + @ExceptionHandler(DuplicateKeyException.class) + public ResponseEntity handleDuplicate(DuplicateKeyException ex) { + return ResponseEntity + .status(HttpStatus.CONFLICT) + .body(ex.getMessage()); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidationExceptions(MethodArgumentNotValidException ex) { + + Map errors = new HashMap<>(); + + ex.getBindingResult().getFieldErrors().forEach(error -> { + errors.put(error.getField(), error.getDefaultMessage()); + }); + + ex.getBindingResult().getGlobalErrors().forEach(error -> { + errors.put(error.getObjectName(), error.getDefaultMessage()); + }); + + Map body = new HashMap<>(); + body.put("status", 400); + body.put("message", "Validation failed"); + body.put("errors", errors); + + return ResponseEntity.badRequest().body(body); + } +} diff --git a/src/main/java/fr/insee/genesis/controller/rest/DataProcessingContextController.java b/src/main/java/fr/insee/genesis/controller/rest/DataProcessingContextController.java index 803ecc2bb..93e3fd87f 100644 --- a/src/main/java/fr/insee/genesis/controller/rest/DataProcessingContextController.java +++ b/src/main/java/fr/insee/genesis/controller/rest/DataProcessingContextController.java @@ -1,7 +1,10 @@ package fr.insee.genesis.controller.rest; import fr.insee.genesis.Constants; +import fr.insee.genesis.controller.dto.KraftwerkExecutionScheduleInput; import fr.insee.genesis.controller.dto.ScheduleDto; +import fr.insee.genesis.controller.dto.ScheduleRequestDto; +import fr.insee.genesis.controller.dto.rawdata.ScheduleResponseDto; import fr.insee.genesis.domain.model.context.schedule.ServiceToCall; import fr.insee.genesis.domain.model.context.schedule.TrustParameters; import fr.insee.genesis.domain.ports.api.DataProcessingContextApiPort; @@ -9,6 +12,7 @@ import fr.insee.genesis.infrastructure.utils.FileUtils; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import jakarta.validation.Valid; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatusCode; @@ -97,6 +101,46 @@ public ResponseEntity getReviewIndicator( } } + @Operation(summary = "Create a Kraftwerk execution schedule V2") + @PostMapping(path = "/contexts/schedules/v2") + @PreAuthorize("hasRole('USER_KRAFTWERK')") + public ResponseEntity createScheduleV2( + @Valid @RequestBody ScheduleRequestDto request + ) { + try { + TrustParameters trustParameters = null; + if (request.isUseAsymmetricEncryption()) { + trustParameters = new TrustParameters( + fileUtils.getKraftwerkOutFolder(request.getCollectionInstrumentId()), + "", + request.getEncryptionVaultPath(), + request.isUseSignature() + ); + } + + KraftwerkExecutionScheduleInput scheduleInput = KraftwerkExecutionScheduleInput.builder() + .collectionInstrumentId(request.getCollectionInstrumentId()) + .exportType(request.getExportType()) + .frequency(request.getFrequency()) + .startDate(request.getScheduleBeginDate()) + .endDate(request.getScheduleEndDate()) + .mode(request.getMode()) + .destinationType(request.getDestinationType()) + .addStates(request.isAddStates()) + .destinationFolder(request.getDestinationFolder()) + .trustParameters(trustParameters) + .batchSize(request.getBatchSize()) + .build(); + + String scheduleUuid = dataProcessingContextApiPort.createKraftwerkExecutionSchedule(scheduleInput); + + return ResponseEntity.ok(scheduleUuid); + + } catch (GenesisException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatusCode.valueOf(e.getStatus())); + } + } + @Deprecated(forRemoval = true) @Operation(summary = "Schedule a Kraftwerk execution") @PutMapping(path = "/context/schedules") @@ -189,6 +233,51 @@ public ResponseEntity saveScheduleWithCollectionInstrumentId( return ResponseEntity.ok().build(); } + @Operation(summary = "Update a Kraftwerk execution schedule V2") + @PutMapping(path = "/contexts/{collectionInstrumentId}/schedules/v2/{scheduleUuid}") + @PreAuthorize("hasRole('USER_KRAFTWERK')") + public ResponseEntity updateScheduleV2( + @PathVariable("collectionInstrumentId") String collectionInstrumentId, + @PathVariable("scheduleUuid") String scheduleUuid, + @Valid @RequestBody ScheduleRequestDto request + ) { + try { + TrustParameters trustParameters = null; + if (request.isUseAsymmetricEncryption()) { + trustParameters = new TrustParameters( + fileUtils.getKraftwerkOutFolder(collectionInstrumentId), + "", + request.getEncryptionVaultPath(), + request.isUseSignature() + ); + } + + KraftwerkExecutionScheduleInput scheduleInput = KraftwerkExecutionScheduleInput.builder() + .collectionInstrumentId(collectionInstrumentId) + .scheduleUuid(scheduleUuid) + .exportType(request.getExportType()) + .frequency(request.getFrequency()) + .startDate(request.getScheduleBeginDate()) + .endDate(request.getScheduleEndDate()) + .mode(request.getMode()) + .destinationType(request.getDestinationType()) + .addStates(request.isAddStates()) + .destinationFolder(request.getDestinationFolder()) + .useAsymmetricEncryption(request.isUseAsymmetricEncryption()) + .useSymmetricEncryption(request.isUseSymmetricEncryption()) + .trustParameters(trustParameters) + .batchSize(request.getBatchSize()) + .build(); + + dataProcessingContextApiPort.updateKraftwerkExecutionSchedule(scheduleInput); + + } catch (GenesisException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatusCode.valueOf(e.getStatus())); + } + + return ResponseEntity.ok().build(); + } + @Deprecated(forRemoval = true) @Operation(summary = "Fetch all schedules") @GetMapping(path = "/context/schedules") @@ -215,6 +304,29 @@ public ResponseEntity getAllSchedulesV2() { return ResponseEntity.ok(surveyScheduleDocumentModels); } + @Operation(summary = "Fetch all schedules V2") + @GetMapping(path = "/contexts/schedules/v2") + @PreAuthorize("hasAnyRole('SCHEDULER','READER')") + public ResponseEntity getAllSchedulesV3() { + log.debug("Got GET all schedules V2 request"); + + List schedules = dataProcessingContextApiPort.getAllSchedulesV2(); + + log.info("Returning {} V2 schedule documents...", schedules.size()); + return ResponseEntity.ok(schedules); + } + + @Operation(summary = "Fetch V2 schedules by collection instrument id") + @GetMapping(path = "/contexts/{collectionInstrumentId}/schedules/v2") + @PreAuthorize("hasAnyRole('SCHEDULER','READER')") + public ResponseEntity getSchedulesV2ByCollectionInstrumentId( + @PathVariable("collectionInstrumentId") String collectionInstrumentId + ) { + List schedules = + dataProcessingContextApiPort.getSchedulesV2ByCollectionInstrumentId(collectionInstrumentId); + + return ResponseEntity.ok(schedules); + } @Deprecated(forRemoval = true) @Operation(summary = "Set last execution date of a partition with new date or nothing") @@ -280,6 +392,37 @@ public ResponseEntity deleteSchedulesByCollectionInstrumentId( return ResponseEntity.ok().build(); } + @Operation(summary = "Delete all V2 Kraftwerk execution schedules of a collection instrument id") + @DeleteMapping(path = "/contexts/{collectionInstrumentId}/schedules/v2") + @PreAuthorize("hasRole('USER_KRAFTWERK')") + public ResponseEntity deleteSchedulesV2ByCollectionInstrumentId( + @PathVariable("collectionInstrumentId") String collectionInstrumentId + ){ + try { + dataProcessingContextApiPort.deleteSchedulesV2ByCollectionInstrumentId(collectionInstrumentId); + } catch (GenesisException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatusCode.valueOf(e.getStatus())); + } + log.info("All V2 schedules deleted for collection instrument {}", collectionInstrumentId); + return ResponseEntity.ok().build(); + } + + @Operation(summary = "Delete a V2 Kraftwerk execution schedule") + @DeleteMapping(path = "/contexts/{collectionInstrumentId}/schedules/v2/{scheduleUuid}") + @PreAuthorize("hasRole('USER_KRAFTWERK')") + public ResponseEntity deleteScheduleV2( + @PathVariable(value = "collectionInstrumentId") String collectionInstrumentId, + @PathVariable(value = "scheduleUuid") String scheduleUuid + ){ + try { + dataProcessingContextApiPort.deleteScheduleV2(collectionInstrumentId, scheduleUuid); + } catch (GenesisException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatusCode.valueOf(e.getStatus())); + } + log.info("V2 schedule {} deleted for collection instrument {}", scheduleUuid, collectionInstrumentId); + return ResponseEntity.ok().build(); + } + @Operation(summary = "Delete expired schedules") @DeleteMapping(path = "/context/schedules/expired-schedules") @PreAuthorize("hasRole('SCHEDULER')") diff --git a/src/main/java/fr/insee/genesis/controller/utils/ExportType.java b/src/main/java/fr/insee/genesis/controller/utils/ExportType.java new file mode 100644 index 000000000..ee8e32351 --- /dev/null +++ b/src/main/java/fr/insee/genesis/controller/utils/ExportType.java @@ -0,0 +1,6 @@ +package fr.insee.genesis.controller.utils; + +public enum ExportType { + JSON, + CSV_PARQUET +} diff --git a/src/main/java/fr/insee/genesis/controller/validation/schedule/ScheduleRequestValidator.java b/src/main/java/fr/insee/genesis/controller/validation/schedule/ScheduleRequestValidator.java new file mode 100644 index 000000000..34f955e78 --- /dev/null +++ b/src/main/java/fr/insee/genesis/controller/validation/schedule/ScheduleRequestValidator.java @@ -0,0 +1,47 @@ +package fr.insee.genesis.controller.validation.schedule; + +import fr.insee.genesis.controller.dto.ScheduleRequestDto; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class ScheduleRequestValidator implements ConstraintValidator { + + @Override + public boolean isValid(ScheduleRequestDto value, ConstraintValidatorContext context) { + + if (value == null) { + return true; + } + + boolean valid = true; + + // Encryption rule + if (value.isUseAsymmetricEncryption()) { + if (value.getEncryptionVaultPath() == null || value.getEncryptionVaultPath().isBlank()) { + addViolation(context, "encryptionVaultPath", "encryptionVaultPath is mandatory if useAsymetricEncryption=true"); + valid = false; + } + } + + // Date rule + if (value.getScheduleBeginDate() != null && + value.getScheduleEndDate() != null && + value.getScheduleBeginDate().isAfter(value.getScheduleEndDate())) { + + addViolation(context, "scheduleEndDate", + "scheduleEndDate should be after scheduleBeginDate"); + valid = false; + } + + return valid; + } + + private void addViolation(ConstraintValidatorContext context, String field, String message) { + context.disableDefaultConstraintViolation(); + + context.buildConstraintViolationWithTemplate(message) + .addPropertyNode(field) + .addConstraintViolation(); + } + +} diff --git a/src/main/java/fr/insee/genesis/controller/validation/schedule/ValidScheduleRequest.java b/src/main/java/fr/insee/genesis/controller/validation/schedule/ValidScheduleRequest.java new file mode 100644 index 000000000..72c2ccc42 --- /dev/null +++ b/src/main/java/fr/insee/genesis/controller/validation/schedule/ValidScheduleRequest.java @@ -0,0 +1,18 @@ +package fr.insee.genesis.controller.validation.schedule; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = ScheduleRequestValidator.class) +public @interface ValidScheduleRequest { + String message() default "Invalid schedule request"; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/fr/insee/genesis/domain/model/context/DataProcessingContextModel.java b/src/main/java/fr/insee/genesis/domain/model/context/DataProcessingContextModel.java index d0a147469..625710bbf 100644 --- a/src/main/java/fr/insee/genesis/domain/model/context/DataProcessingContextModel.java +++ b/src/main/java/fr/insee/genesis/domain/model/context/DataProcessingContextModel.java @@ -1,7 +1,9 @@ package fr.insee.genesis.domain.model.context; import fr.insee.genesis.controller.dto.ScheduleDto; +import fr.insee.genesis.controller.dto.rawdata.ScheduleResponseDto; import fr.insee.genesis.domain.model.context.schedule.KraftwerkExecutionSchedule; +import fr.insee.genesis.domain.model.context.schedule.KraftwerkExecutionScheduleV2; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -18,20 +20,22 @@ @AllArgsConstructor public class DataProcessingContextModel { @Id - private ObjectId id; //Used to remove warning + private ObjectId id; // Used to remove warning @Deprecated(forRemoval = true) private String partitionId; - private String collectionInstrumentId; //QuestionnaireId + private String collectionInstrumentId; // QuestionnaireId private LocalDateTime lastExecution; - List kraftwerkExecutionScheduleList; + private List kraftwerkExecutionScheduleList; - boolean withReview; + private List kraftwerkExecutionScheduleV2List; - public ScheduleDto toScheduleDto(){ + private boolean withReview; + + public ScheduleDto toScheduleDto() { return ScheduleDto.builder() .surveyName(partitionId) .collectionInstrumentId(collectionInstrumentId) @@ -39,4 +43,46 @@ public ScheduleDto toScheduleDto(){ .kraftwerkExecutionScheduleList(kraftwerkExecutionScheduleList) .build(); } + + public List toScheduleResponseDtos() { + if (kraftwerkExecutionScheduleV2List == null || kraftwerkExecutionScheduleV2List.isEmpty()) { + return List.of(); + } + + return kraftwerkExecutionScheduleV2List.stream() + .filter(schedule -> schedule != null && schedule.getScheduleUuid() != null) + .map(schedule -> ScheduleResponseDto.builder() + .scheduleUuid(schedule.getScheduleUuid()) + .collectionInstrumentId(getResolvedCollectionInstrumentId()) + .lastExecution(lastExecution) + .frequency(schedule.getFrequency()) + .exportType(schedule.getExportType()) + .scheduleBeginDate(schedule.getScheduleBeginDate()) + .scheduleEndDate(schedule.getScheduleEndDate()) + .mode(schedule.getMode()) + .useSymmetricEncryption(schedule.isUseSymmetricEncryption()) + .useAsymmetricEncryption(schedule.getTrustParameters() != null) + .encryptionVaultPath( + schedule.getTrustParameters() != null + ? schedule.getTrustParameters().getVaultPath() + : "" + ) + .useSignature( + schedule.getTrustParameters() != null + && schedule.getTrustParameters().isUseSignature() + ) + .addStates(schedule.isAddStates()) + .destinationType(schedule.getDestinationType()) + .destinationFolder(schedule.getDestinationFolder()) + .batchSize(schedule.getBatchSize()) + .build() + ) + .toList(); + } + + public String getResolvedCollectionInstrumentId() { + return collectionInstrumentId != null && !collectionInstrumentId.isBlank() + ? collectionInstrumentId + : partitionId; + } } diff --git a/src/main/java/fr/insee/genesis/domain/model/context/schedule/DestinationType.java b/src/main/java/fr/insee/genesis/domain/model/context/schedule/DestinationType.java new file mode 100644 index 000000000..7775251ac --- /dev/null +++ b/src/main/java/fr/insee/genesis/domain/model/context/schedule/DestinationType.java @@ -0,0 +1,10 @@ +package fr.insee.genesis.domain.model.context.schedule; + +public enum DestinationType { + S3, + AUS, + APPLISHARE, + SEF, + LS3, + +} diff --git a/src/main/java/fr/insee/genesis/domain/model/context/schedule/KraftwerkExecutionScheduleV2.java b/src/main/java/fr/insee/genesis/domain/model/context/schedule/KraftwerkExecutionScheduleV2.java new file mode 100644 index 000000000..92cfa8536 --- /dev/null +++ b/src/main/java/fr/insee/genesis/domain/model/context/schedule/KraftwerkExecutionScheduleV2.java @@ -0,0 +1,34 @@ +package fr.insee.genesis.domain.model.context.schedule; + +import com.fasterxml.jackson.annotation.JsonFormat; +import fr.insee.genesis.controller.utils.ExportType; +import fr.insee.genesis.domain.model.surveyunit.Mode; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class KraftwerkExecutionScheduleV2 { + + private String scheduleUuid; + private String frequency; + private ExportType exportType; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss") + private LocalDateTime scheduleBeginDate; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss") + private LocalDateTime scheduleEndDate; + + private Mode mode; + private DestinationType destinationType; + private boolean addStates; + private String destinationFolder; + private boolean useSymmetricEncryption; + private TrustParameters trustParameters; + private Integer batchSize; +} diff --git a/src/main/java/fr/insee/genesis/domain/ports/api/DataProcessingContextApiPort.java b/src/main/java/fr/insee/genesis/domain/ports/api/DataProcessingContextApiPort.java index e6b6602fb..a1975a471 100644 --- a/src/main/java/fr/insee/genesis/domain/ports/api/DataProcessingContextApiPort.java +++ b/src/main/java/fr/insee/genesis/domain/ports/api/DataProcessingContextApiPort.java @@ -1,6 +1,8 @@ package fr.insee.genesis.domain.ports.api; +import fr.insee.genesis.controller.dto.KraftwerkExecutionScheduleInput; import fr.insee.genesis.controller.dto.ScheduleDto; +import fr.insee.genesis.controller.dto.rawdata.ScheduleResponseDto; import fr.insee.genesis.domain.model.context.DataProcessingContextModel; import fr.insee.genesis.domain.model.context.schedule.ServiceToCall; import fr.insee.genesis.domain.model.context.schedule.TrustParameters; @@ -28,14 +30,27 @@ void saveKraftwerkExecutionScheduleByCollectionInstrumentId(String collectionIns LocalDateTime endDate, TrustParameters trustParameters) throws GenesisException; + String createKraftwerkExecutionSchedule(KraftwerkExecutionScheduleInput scheduleInput) throws GenesisException; + + void updateKraftwerkExecutionSchedule(KraftwerkExecutionScheduleInput scheduleInput) throws GenesisException; + void updateLastExecutionDate(String surveyName, LocalDateTime newDate) throws GenesisException; void updateLastExecutionDateByCollectionInstrumentId(String collectionInstrumentId, LocalDateTime newDate) throws GenesisException; void deleteSchedules(String surveyName) throws GenesisException; + + void deleteScheduleV2(String collectionInstrumentId, String scheduleUuid) throws GenesisException; + void deleteSchedulesByCollectionInstrumentId(String collectionInstrumentId) throws GenesisException; + void deleteSchedulesV2ByCollectionInstrumentId(String collectionInstrumentId) throws GenesisException; + + List getSchedulesV2ByCollectionInstrumentId(String collectionInstrumentId); + List getAllSchedules(); + List getAllSchedulesV2(); + void deleteExpiredSchedules(String logFolder) throws GenesisException; long countSchedules(); diff --git a/src/main/java/fr/insee/genesis/domain/service/context/DataProcessingContextService.java b/src/main/java/fr/insee/genesis/domain/service/context/DataProcessingContextService.java index 43cfb5638..c6195fa78 100644 --- a/src/main/java/fr/insee/genesis/domain/service/context/DataProcessingContextService.java +++ b/src/main/java/fr/insee/genesis/domain/service/context/DataProcessingContextService.java @@ -3,9 +3,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import fr.insee.genesis.Constants; +import fr.insee.genesis.controller.dto.KraftwerkExecutionScheduleInput; import fr.insee.genesis.controller.dto.ScheduleDto; +import fr.insee.genesis.controller.dto.rawdata.ScheduleResponseDto; import fr.insee.genesis.domain.model.context.DataProcessingContextModel; import fr.insee.genesis.domain.model.context.schedule.KraftwerkExecutionSchedule; +import fr.insee.genesis.domain.model.context.schedule.KraftwerkExecutionScheduleV2; import fr.insee.genesis.domain.model.context.schedule.ServiceToCall; import fr.insee.genesis.domain.model.context.schedule.TrustParameters; import fr.insee.genesis.domain.model.surveyunit.SurveyUnitModel; @@ -17,6 +20,7 @@ import fr.insee.genesis.infrastructure.mappers.DataProcessingContextMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import java.io.IOException; @@ -27,7 +31,9 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; +import java.util.UUID; @Service @Slf4j @@ -127,6 +133,123 @@ public void saveKraftwerkExecutionScheduleByCollectionInstrumentId(String collec dataProcessingContextPersistancePort.save(DataProcessingContextMapper.INSTANCE.modelToDocument(dataProcessingContextModel)); } + @Override + public String createKraftwerkExecutionSchedule(KraftwerkExecutionScheduleInput scheduleInput) { + + DataProcessingContextModel dataProcessingContextModel = + dataProcessingContextPersistancePort.findByCollectionInstrumentId( + scheduleInput.getCollectionInstrumentId() + ); + + if (dataProcessingContextModel == null) { + dataProcessingContextModel = DataProcessingContextModel.builder() + .collectionInstrumentId(scheduleInput.getCollectionInstrumentId()) + .withReview(false) + .kraftwerkExecutionScheduleV2List(new ArrayList<>()) + .build(); + } + + if (dataProcessingContextModel.getKraftwerkExecutionScheduleV2List() == null) { + dataProcessingContextModel.setKraftwerkExecutionScheduleV2List(new ArrayList<>()); + } + + String scheduleUuid = UUID.randomUUID().toString(); + + Optional scheduleAlreadyExists = dataProcessingContextModel.getKraftwerkExecutionScheduleV2List() + .stream() + .filter(schedule -> + schedule.getMode()==scheduleInput.getMode() && schedule.getExportType() == scheduleInput.getExportType() + ) + .findFirst(); + + if (scheduleAlreadyExists.isPresent()){ + throw new DuplicateKeyException(String.format("Schedule already exists for collectionInstrumentId %s with mode %s and exportType %s. Use update endpoint with scheduleUuid %s", + scheduleInput.getCollectionInstrumentId(), + scheduleInput.getMode(), + scheduleInput.getExportType(), + scheduleAlreadyExists.get().getScheduleUuid())); + } + + KraftwerkExecutionScheduleV2 newSchedule = new KraftwerkExecutionScheduleV2( + scheduleUuid, + scheduleInput.getFrequency(), + scheduleInput.getExportType(), + scheduleInput.getStartDate(), + scheduleInput.getEndDate(), + scheduleInput.getMode(), + scheduleInput.getDestinationType(), + scheduleInput.isAddStates(), + scheduleInput.getDestinationFolder(), + scheduleInput.isUseSymmetricEncryption(), + scheduleInput.getTrustParameters(), + scheduleInput.getBatchSize() + ); + + dataProcessingContextModel.getKraftwerkExecutionScheduleV2List().add(newSchedule); + + dataProcessingContextPersistancePort.save( + DataProcessingContextMapper.INSTANCE.modelToDocument(dataProcessingContextModel) + ); + + return scheduleUuid; + } + + @Override + public void updateKraftwerkExecutionSchedule(KraftwerkExecutionScheduleInput scheduleInput) throws GenesisException { + + DataProcessingContextModel dataProcessingContextModel = + dataProcessingContextPersistancePort.findByCollectionInstrumentId( + scheduleInput.getCollectionInstrumentId() + ); + + if (dataProcessingContextModel == null) { + throw new GenesisException(404, "Collection instrument not found"); + } + + if (dataProcessingContextModel.getKraftwerkExecutionScheduleV2List() == null + || dataProcessingContextModel.getKraftwerkExecutionScheduleV2List().isEmpty()) { + throw new GenesisException(404, "No V2 schedule found for this collection instrument"); + } + + KraftwerkExecutionScheduleV2 scheduleToUpdate = dataProcessingContextModel.getKraftwerkExecutionScheduleV2List() + .stream() + .filter(schedule -> scheduleInput.getScheduleUuid().equals(schedule.getScheduleUuid())) + .findFirst() + .orElseThrow(() -> new GenesisException(404, "V2 schedule not found")); + + Optional tripletAlreadyExists = dataProcessingContextModel.getKraftwerkExecutionScheduleV2List() + .stream() + .filter(schedule -> + schedule.getMode()==scheduleInput.getMode() && schedule.getExportType() == scheduleInput.getExportType() + ) + .filter(schedule -> !schedule.getScheduleUuid().equals(scheduleInput.getScheduleUuid())) + .findFirst(); + + if (tripletAlreadyExists.isPresent()){ + throw new DuplicateKeyException(String.format("Schedule already exists for collectionInstrumentId %s with mode %s and exportType %s. Modify scheduleUuid %s instead", + scheduleInput.getCollectionInstrumentId(), + scheduleInput.getMode(), + scheduleInput.getExportType(), + tripletAlreadyExists.get().getScheduleUuid())); + } + + scheduleToUpdate.setFrequency(scheduleInput.getFrequency()); + scheduleToUpdate.setExportType(scheduleInput.getExportType()); + scheduleToUpdate.setScheduleBeginDate(scheduleInput.getStartDate()); + scheduleToUpdate.setScheduleEndDate(scheduleInput.getEndDate()); + scheduleToUpdate.setMode(scheduleInput.getMode()); + scheduleToUpdate.setDestinationType(scheduleInput.getDestinationType()); + scheduleToUpdate.setAddStates(scheduleInput.isAddStates()); + scheduleToUpdate.setDestinationFolder(scheduleInput.getDestinationFolder()); + scheduleToUpdate.setUseSymmetricEncryption(scheduleInput.isUseSymmetricEncryption()); + scheduleToUpdate.setTrustParameters(scheduleInput.getTrustParameters()); + scheduleToUpdate.setBatchSize(scheduleInput.getBatchSize()); + + dataProcessingContextPersistancePort.save( + DataProcessingContextMapper.INSTANCE.modelToDocument(dataProcessingContextModel) + ); + } + @Deprecated(forRemoval = true) @Override public void updateLastExecutionDate(String partitionId, LocalDateTime newDate) throws GenesisException { @@ -164,6 +287,31 @@ public void deleteSchedules(String partitionId) throws GenesisException { dataProcessingContextPersistancePort.save(DataProcessingContextMapper.INSTANCE.modelToDocument(dataProcessingContextModel)); } + @Override + public void deleteScheduleV2(String collectionInstrumentId, String scheduleUuid) throws GenesisException { + DataProcessingContextModel dataProcessingContextModel = + dataProcessingContextPersistancePort.findByCollectionInstrumentId(collectionInstrumentId); + if (dataProcessingContextModel == null) { + throw new GenesisException(404, NOT_FOUND_MESSAGE); + } + + if (dataProcessingContextModel.getKraftwerkExecutionScheduleV2List() == null + || dataProcessingContextModel.getKraftwerkExecutionScheduleV2List().isEmpty()) { + throw new GenesisException(404, "No V2 schedule found for this collection instrument"); + } + + boolean removed = dataProcessingContextModel.getKraftwerkExecutionScheduleV2List() + .removeIf(schedule -> scheduleUuid.equals(schedule.getScheduleUuid())); + + if (!removed) { + throw new GenesisException(404, "V2 schedule not found"); + } + + dataProcessingContextPersistancePort.save( + DataProcessingContextMapper.INSTANCE.modelToDocument(dataProcessingContextModel) + ); + } + @Override public void deleteSchedulesByCollectionInstrumentId(String collectionInstrumentId) throws GenesisException { DataProcessingContextModel dataProcessingContextModel = @@ -175,6 +323,32 @@ public void deleteSchedulesByCollectionInstrumentId(String collectionInstrumentI dataProcessingContextPersistancePort.save(DataProcessingContextMapper.INSTANCE.modelToDocument(dataProcessingContextModel)); } + @Override + public void deleteSchedulesV2ByCollectionInstrumentId(String collectionInstrumentId) throws GenesisException { + DataProcessingContextModel dataProcessingContextModel = + dataProcessingContextPersistancePort.findByCollectionInstrumentId(collectionInstrumentId); + + if (dataProcessingContextModel == null) { + throw new GenesisException(404, NOT_FOUND_MESSAGE); + } + + dataProcessingContextModel.setKraftwerkExecutionScheduleV2List(new ArrayList<>()); + + dataProcessingContextPersistancePort.save( + DataProcessingContextMapper.INSTANCE.modelToDocument(dataProcessingContextModel) + ); + } + + @Override + public List getSchedulesV2ByCollectionInstrumentId(String collectionInstrumentId) { + List dataProcessingContextModels = + dataProcessingContextPersistancePort.findByCollectionInstrumentIds(List.of(collectionInstrumentId)); + + return dataProcessingContextModels.stream() + .flatMap(model -> model.toScheduleResponseDtos().stream()) + .toList(); + } + @Override public List getAllSchedules() { List scheduleDtos = new ArrayList<>(); @@ -189,6 +363,18 @@ public List getAllSchedules() { return scheduleDtos; } + @Override + public List getAllSchedulesV2() { + List dataProcessingContextModels = + DataProcessingContextMapper.INSTANCE.listDocumentToListModel( + dataProcessingContextPersistancePort.findAll() + ); + + return dataProcessingContextModels.stream() + .flatMap(model -> model.toScheduleResponseDtos().stream()) + .toList(); + } + @Override public void deleteExpiredSchedules(String logFolder) throws GenesisException { List dataProcessingContextModels = @@ -217,7 +403,7 @@ public void deleteExpiredSchedules(String logFolder) throws GenesisException { Files.write(jsonLogPath, jsonToWrite.getBytes()); } } - } catch (IOException e) { + } catch (IOException _) { String name = context.getCollectionInstrumentId()!=null?context.getCollectionInstrumentId() :context.getPartitionId(); throw new GenesisException(500,String.format("An error occured trying to delete expired schedules for %s",name)); } diff --git a/src/main/java/fr/insee/genesis/infrastructure/document/context/DataProcessingContextDocument.java b/src/main/java/fr/insee/genesis/infrastructure/document/context/DataProcessingContextDocument.java index 2a4b5b8e3..7f170a437 100644 --- a/src/main/java/fr/insee/genesis/infrastructure/document/context/DataProcessingContextDocument.java +++ b/src/main/java/fr/insee/genesis/infrastructure/document/context/DataProcessingContextDocument.java @@ -2,6 +2,7 @@ import fr.insee.genesis.Constants; import fr.insee.genesis.domain.model.context.schedule.KraftwerkExecutionSchedule; +import fr.insee.genesis.domain.model.context.schedule.KraftwerkExecutionScheduleV2; import lombok.Data; import org.bson.types.ObjectId; import org.springframework.data.annotation.Id; @@ -24,5 +25,6 @@ public class DataProcessingContextDocument{ private String collectionInstrumentId; // QuestionnaireId private LocalDateTime lastExecution; private List kraftwerkExecutionScheduleList; + private List kraftwerkExecutionScheduleV2List; private boolean withReview; } diff --git a/src/main/java/fr/insee/genesis/infrastructure/repository/DataProcessingContextMongoDBRepository.java b/src/main/java/fr/insee/genesis/infrastructure/repository/DataProcessingContextMongoDBRepository.java index 0f7e2d72f..e018b56c7 100644 --- a/src/main/java/fr/insee/genesis/infrastructure/repository/DataProcessingContextMongoDBRepository.java +++ b/src/main/java/fr/insee/genesis/infrastructure/repository/DataProcessingContextMongoDBRepository.java @@ -17,6 +17,7 @@ public interface DataProcessingContextMongoDBRepository extends MongoRepository< @Query(value = "{ 'collectionInstrumentId' : {$in: ?0} }") List findByCollectionInstrumentIdList(List collectionInstrumentIds); + @Deprecated(forRemoval = true) @Query(value = "{ 'partitionId' : ?0 }", delete = true) void deleteByPartitionId(String partitionId); diff --git a/src/test/java/fr/insee/genesis/controller/rest/DataProcessingContextControllerTest.java b/src/test/java/fr/insee/genesis/controller/rest/DataProcessingContextControllerTest.java index 4f08ec285..3e91b3968 100644 --- a/src/test/java/fr/insee/genesis/controller/rest/DataProcessingContextControllerTest.java +++ b/src/test/java/fr/insee/genesis/controller/rest/DataProcessingContextControllerTest.java @@ -533,6 +533,7 @@ void deleteExpiredScheduleTest_execution() { null, null, new ArrayList<>(), + null, false ); KraftwerkExecutionSchedule kraftwerkExecutionSchedule = new KraftwerkExecutionSchedule( @@ -581,6 +582,7 @@ void deleteExpiredScheduleTest_execution_collectionInstrumentId() { "TESTSURVEYADDED_CI", null, new ArrayList<>(), + null, false ); KraftwerkExecutionSchedule kraftwerkExecutionSchedule = new KraftwerkExecutionSchedule( @@ -629,6 +631,7 @@ void deleteExpiredScheduleTest_wholeSurvey() { "TESTSURVEYADDED", null, new ArrayList<>(), + null, false ); KraftwerkExecutionSchedule kraftwerkExecutionSchedule = new KraftwerkExecutionSchedule( @@ -678,6 +681,7 @@ void deleteExpiredScheduleTest_wholeSurvey_collectionInstrumentId() { "TESTSURVEYADDED_CI", null, new ArrayList<>(), + null, false ); KraftwerkExecutionSchedule kraftwerkExecutionSchedule = new KraftwerkExecutionSchedule( @@ -727,6 +731,7 @@ void deleteExpiredScheduleTest_appendLog() { "TESTSURVEYADDED2", null, new ArrayList<>(), + null, false ); KraftwerkExecutionSchedule kraftwerkExecutionSchedule = new KraftwerkExecutionSchedule( @@ -786,6 +791,7 @@ void deleteExpiredScheduleTest_appendLog_collectionInstrumentId() { "TESTSURVEYADDED2_CI", null, new ArrayList<>(), + null, false ); KraftwerkExecutionSchedule kraftwerkExecutionSchedule = new KraftwerkExecutionSchedule(