diff --git a/persistence-service-v1/persistence-service-external-api-impl-v2/src/main/java/com/iqser/red/persistence/service/v2/external/api/impl/controller/DossierTemplateControllerV2.java b/persistence-service-v1/persistence-service-external-api-impl-v2/src/main/java/com/iqser/red/persistence/service/v2/external/api/impl/controller/DossierTemplateControllerV2.java index 1d0946f4d..2dea6931a 100644 --- a/persistence-service-v1/persistence-service-external-api-impl-v2/src/main/java/com/iqser/red/persistence/service/v2/external/api/impl/controller/DossierTemplateControllerV2.java +++ b/persistence-service-v1/persistence-service-external-api-impl-v2/src/main/java/com/iqser/red/persistence/service/v2/external/api/impl/controller/DossierTemplateControllerV2.java @@ -1,30 +1,166 @@ package com.iqser.red.persistence.service.v2.external.api.impl.controller; +import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.READ_RULES; +import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.WRITE_RULES; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import org.springframework.core.io.InputStreamResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + import com.iqser.red.persistence.service.v1.external.api.impl.controller.DossierTemplateController; +import com.iqser.red.service.persistence.management.v1.processor.exception.BadRequestException; +import com.iqser.red.service.persistence.management.v1.processor.service.RulesValidationService; +import com.iqser.red.service.persistence.management.v1.processor.service.persistence.AuditPersistenceService; +import com.iqser.red.service.persistence.management.v1.processor.service.persistence.RulesPersistenceService; +import com.iqser.red.service.persistence.management.v1.processor.utils.StringEncodingUtils; +import com.iqser.red.service.persistence.service.v1.api.shared.model.AuditCategory; import com.iqser.red.service.persistence.service.v1.api.shared.model.DossierTemplateModel; +import com.iqser.red.service.persistence.service.v1.api.shared.model.RuleFileType; +import com.iqser.red.service.persistence.service.v1.api.shared.model.audit.AuditRequest; +import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.rules.RulesUploadRequest; +import com.iqser.red.service.persistence.service.v2.api.external.model.RulesValidationMessage; +import com.iqser.red.service.persistence.service.v2.api.external.model.RulesValidationResponse; import com.iqser.red.service.persistence.service.v2.api.external.resource.DossierTemplateResource; +import com.iqser.red.service.redaction.v1.model.DroolsSyntaxValidation; +import com.knecon.fforesight.keycloakcommons.security.KeycloakSecurity; + +import feign.FeignException; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RestController; - -import java.util.List; +import lombok.SneakyThrows; @RestController @RequiredArgsConstructor @Tag(name = "1. Dossier templates endpoints", description = "Provides operations related to dossier templates") public class DossierTemplateControllerV2 implements DossierTemplateResource { + private static final String RULES_DOWNLOAD_FILE_NAME = "rules.drl"; + private final DossierTemplateController dossierTemplateController; + private final RulesPersistenceService rulesPersistenceService; + private final RulesValidationService rulesValidationService; + private final AuditPersistenceService auditPersistenceService; + public List getAllDossierTemplates() { + return dossierTemplateController.getAllDossierTemplates(); } public DossierTemplateModel getDossierTemplate(@PathVariable(DOSSIER_TEMPLATE_ID_PARAM) String dossierTemplateId) { + return dossierTemplateController.getDossierTemplate(dossierTemplateId); } + @PreAuthorize("hasAuthority('" + WRITE_RULES + "')") + public ResponseEntity uploadEntityRules(@PathVariable(DOSSIER_TEMPLATE_ID_PARAM) String dossierTemplateId, + @Schema(type = "string", format = "binary", name = "file") @RequestPart(name = "file") MultipartFile file, + @Parameter(name = DRY_RUN_PARAM, description = "If true rules will be only validated not stored.") @RequestParam(value = DRY_RUN_PARAM, required = false, defaultValue = "false") boolean dryRun) { + + return uploadRules(dossierTemplateId, RuleFileType.ENTITY, file, dryRun); + } + + + @PreAuthorize("hasAuthority('" + READ_RULES + "')") + public ResponseEntity downloadEntityRules(@PathVariable(DOSSIER_TEMPLATE_ID_PARAM) String dossierTemplateId) { + + return downloadRules(dossierTemplateId, RuleFileType.ENTITY); + } + + + @PreAuthorize("hasAuthority('" + WRITE_RULES + "')") + public ResponseEntity uploadComponentRules(@PathVariable(DOSSIER_TEMPLATE_ID_PARAM) String dossierTemplateId, + @Schema(type = "string", format = "binary", name = "file") @RequestPart(name = "file") MultipartFile file, + @Parameter(name = DRY_RUN_PARAM, description = "If true rules will be only validated not stored.") @RequestParam(value = DRY_RUN_PARAM, required = false, defaultValue = "false") boolean dryRun) { + + return uploadRules(dossierTemplateId, RuleFileType.COMPONENT, file, dryRun); + } + + + @PreAuthorize("hasAuthority('" + READ_RULES + "')") + public ResponseEntity downloadComponentRules(@PathVariable(DOSSIER_TEMPLATE_ID_PARAM) String dossierTemplateId, + @PathVariable(RULE_FILE_TYPE_PARAMETER_NAME) RuleFileType ruleFileType) { + + return downloadRules(dossierTemplateId, RuleFileType.COMPONENT); + } + + + @SneakyThrows + private ResponseEntity uploadRules(String dossierTemplateId, RuleFileType ruleFileType, MultipartFile file, boolean dryRun) { + + var rulesUploadRequest = RulesUploadRequest.builder() + .rules(new String(file.getBytes(), StandardCharsets.UTF_8)) + .dossierTemplateId(dossierTemplateId) + .ruleFileType(ruleFileType) + .build(); + try { + DroolsSyntaxValidation droolsSyntaxValidation = rulesValidationService.validateRules(rulesUploadRequest.getRuleFileType(), rulesUploadRequest.getRules()); + if (!droolsSyntaxValidation.isCompiled()) { + var rulesSyntaxErrorMessages = droolsSyntaxValidation.getDroolsSyntaxErrorMessages() + .stream() + .map(errorMessage -> RulesValidationMessage.builder() + .line(errorMessage.getLine()) + .column(errorMessage.getColumn()) + .message(errorMessage.getMessage()) + .build()) + .toList(); + + // TODO Add warning and deprecations to response + return new ResponseEntity<>(RulesValidationResponse.builder().errors(rulesSyntaxErrorMessages).build(), HttpStatus.BAD_REQUEST); + } + } catch (FeignException e) { + if (e.status() == HttpStatus.BAD_REQUEST.value()) { + throw new BadRequestException("The provided rule string is not a valid drools rule file!"); + } + } + + if (!dryRun) { + rulesPersistenceService.setRules(rulesUploadRequest.getRules(), rulesUploadRequest.getDossierTemplateId(), rulesUploadRequest.getRuleFileType()); + } + + auditPersistenceService.audit(AuditRequest.builder() + .userId(KeycloakSecurity.getUserId()) + .objectId(rulesUploadRequest.getDossierTemplateId()) + .category(AuditCategory.DOSSIER_TEMPLATE.name()) + .message(String.format("%s Rules have been updated", rulesUploadRequest.getRuleFileType())) + .build()); + + // TODO Add warning and deprecations to response + return new ResponseEntity<>(RulesValidationResponse.builder().build(), HttpStatus.OK); + } + + + private ResponseEntity downloadRules(String dossierTemplateId, RuleFileType ruleFileType) { + + var ruleEntity = rulesPersistenceService.getRules(dossierTemplateId, ruleFileType); + + var data = ruleEntity.getValue().getBytes(StandardCharsets.UTF_8); + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM); + + httpHeaders.add("Content-Disposition", "attachment" + "; filename*=utf-8''" + StringEncodingUtils.urlEncode(ruleFileType.name() + "_" + RULES_DOWNLOAD_FILE_NAME)); + + InputStream is = new ByteArrayInputStream(data); + + return new ResponseEntity<>(new InputStreamResource(is), httpHeaders, HttpStatus.OK); + } + } diff --git a/persistence-service-v1/persistence-service-external-api-v2/src/main/java/com/iqser/red/service/persistence/service/v2/api/external/model/RulesDeprecationMessage.java b/persistence-service-v1/persistence-service-external-api-v2/src/main/java/com/iqser/red/service/persistence/service/v2/api/external/model/RulesDeprecationMessage.java new file mode 100644 index 000000000..c4f2f3681 --- /dev/null +++ b/persistence-service-v1/persistence-service-external-api-v2/src/main/java/com/iqser/red/service/persistence/service/v2/api/external/model/RulesDeprecationMessage.java @@ -0,0 +1,28 @@ +package com.iqser.red.service.persistence.service.v2.api.external.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldDefaults; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +@Schema(description = "Show where deprecated methods are used in the rules file.") +public class RulesDeprecationMessage { + + @Schema(description = "The Line where the deprecated method is used.") + Integer line; + @Schema(description = "The Column where the deprecated method is used.") + Integer column; + @Schema(description = "The deprecated message, should point to method that should be used instead.") + String message; + @Schema(description = "The name of the deprecated method.") + String methodName; + +} diff --git a/persistence-service-v1/persistence-service-external-api-v2/src/main/java/com/iqser/red/service/persistence/service/v2/api/external/model/RulesValidationMessage.java b/persistence-service-v1/persistence-service-external-api-v2/src/main/java/com/iqser/red/service/persistence/service/v2/api/external/model/RulesValidationMessage.java new file mode 100644 index 000000000..6c78ba432 --- /dev/null +++ b/persistence-service-v1/persistence-service-external-api-v2/src/main/java/com/iqser/red/service/persistence/service/v2/api/external/model/RulesValidationMessage.java @@ -0,0 +1,26 @@ +package com.iqser.red.service.persistence.service.v2.api.external.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldDefaults; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +@Schema(description = "Shows where to find problems in the uploaded rules file") +public class RulesValidationMessage { + + @Schema(description = "The Line where it occurred.") + Integer line; + @Schema(description = "The Column where it occurred.") + Integer column; + @Schema(description = "The error or warning message.") + String message; + +} diff --git a/persistence-service-v1/persistence-service-external-api-v2/src/main/java/com/iqser/red/service/persistence/service/v2/api/external/model/RulesValidationResponse.java b/persistence-service-v1/persistence-service-external-api-v2/src/main/java/com/iqser/red/service/persistence/service/v2/api/external/model/RulesValidationResponse.java new file mode 100644 index 000000000..e0fdd2eec --- /dev/null +++ b/persistence-service-v1/persistence-service-external-api-v2/src/main/java/com/iqser/red/service/persistence/service/v2/api/external/model/RulesValidationResponse.java @@ -0,0 +1,34 @@ +package com.iqser.red.service.persistence.service.v2.api.external.model; + +import java.util.ArrayList; +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldDefaults; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +@Schema(description = "Object containing rules validation result") +public class RulesValidationResponse { + + @Builder.Default + @Schema(description = "Show where errors are in the uploaded rules file.") + List errors = new ArrayList<>(); + + /* + @Builder.Default + @Schema(description = "Show where warnings are in the uploaded rules file.") + List warnings = new ArrayList<>(); + @Builder.Default + @Schema(description = "Show deprecated methods are used in the uploaded rules file.") + List deprecationWarnings = new ArrayList<>(); +*/ +} diff --git a/persistence-service-v1/persistence-service-external-api-v2/src/main/java/com/iqser/red/service/persistence/service/v2/api/external/resource/DossierTemplateResource.java b/persistence-service-v1/persistence-service-external-api-v2/src/main/java/com/iqser/red/service/persistence/service/v2/api/external/resource/DossierTemplateResource.java index dd0f0e2a2..3c30b6671 100644 --- a/persistence-service-v1/persistence-service-external-api-v2/src/main/java/com/iqser/red/service/persistence/service/v2/api/external/resource/DossierTemplateResource.java +++ b/persistence-service-v1/persistence-service-external-api-v2/src/main/java/com/iqser/red/service/persistence/service/v2/api/external/resource/DossierTemplateResource.java @@ -1,14 +1,27 @@ package com.iqser.red.service.persistence.service.v2.api.external.resource; import com.iqser.red.service.persistence.service.v1.api.shared.model.DossierTemplateModel; +import com.iqser.red.service.persistence.service.v1.api.shared.model.RuleFileType; +import com.iqser.red.service.persistence.service.v2.api.external.model.RulesValidationResponse; + import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; + +import org.springframework.core.io.InputStreamResource; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.multipart.MultipartFile; import java.util.List; @@ -18,9 +31,16 @@ public interface DossierTemplateResource { String DOSSIER_TEMPLATE_PATH = "/dossier-templates"; String PATH = ExternalApiConstants.BASE_PATH + DOSSIER_TEMPLATE_PATH; + + String ENTITY_RULES_PATH = "/entity-rules"; + String COMPONENT_RULES_PATH = "/component-rules"; String DOSSIER_TEMPLATE_ID_PARAM = "dossierTemplateId"; String DOSSIER_TEMPLATE_ID_PATH_VARIABLE = "/{" + DOSSIER_TEMPLATE_ID_PARAM + "}"; + String RULE_FILE_TYPE_PARAMETER_NAME = "ruleFileType"; + + String DRY_RUN_PARAM = "dry-run"; + @GetMapping(value = PATH, produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Lists all existing DossierTemplates.", description = "None") @@ -34,5 +54,37 @@ public interface DossierTemplateResource { DossierTemplateModel getDossierTemplate(@PathVariable(DOSSIER_TEMPLATE_ID_PARAM) String dossierTemplateId); + @ResponseStatus(value = HttpStatus.NO_CONTENT) + @PostMapping(value = PATH + DOSSIER_TEMPLATE_ID_PATH_VARIABLE + ENTITY_RULES_PATH, consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "Upload a component or entity rules file in drools format for a specific DossierTemplate.") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Rules upload successful."), @ApiResponse(responseCode = "400", description = "Uploaded rules could not be verified.")}) + ResponseEntity uploadEntityRules(@PathVariable(DOSSIER_TEMPLATE_ID_PARAM) String dossierTemplateId, + @Schema(type = "string", format = "binary", name = "file") @RequestPart(name = "file") MultipartFile file, + @Parameter(name = DRY_RUN_PARAM, description = "If true rules will be only validated not stored.") @RequestParam(value = DRY_RUN_PARAM, required = false ,defaultValue = "false") boolean dryRun); + + @ResponseBody + @ResponseStatus(value = HttpStatus.OK) + @Operation(summary = "Returns file containing the currently used Drools rules.") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK")}) + @GetMapping(value = PATH + DOSSIER_TEMPLATE_ID_PATH_VARIABLE + ENTITY_RULES_PATH, produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + ResponseEntity downloadEntityRules(@PathVariable(DOSSIER_TEMPLATE_ID_PARAM) String dossierTemplateId); + + + + @ResponseStatus(value = HttpStatus.NO_CONTENT) + @PostMapping(value = PATH + DOSSIER_TEMPLATE_ID_PATH_VARIABLE + COMPONENT_RULES_PATH, consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "Upload a component or entity rules file in drools format for a specific DossierTemplate.") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Rules upload successful."), @ApiResponse(responseCode = "400", description = "Uploaded rules could not be verified.")}) + ResponseEntity uploadComponentRules(@PathVariable(DOSSIER_TEMPLATE_ID_PARAM) String dossierTemplateId, + @Schema(type = "string", format = "binary", name = "file") @RequestPart(name = "file") MultipartFile file, + @Parameter(name = DRY_RUN_PARAM, description = "If true rules will be only validated not stored.") @RequestParam(value = DRY_RUN_PARAM, required = false ,defaultValue = "false") boolean dryRun); + + @ResponseBody + @ResponseStatus(value = HttpStatus.OK) + @Operation(summary = "Returns file containing the currently used Drools rules.") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK")}) + @GetMapping(value = PATH + DOSSIER_TEMPLATE_ID_PATH_VARIABLE + COMPONENT_RULES_PATH, produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + ResponseEntity downloadComponentRules(@PathVariable(DOSSIER_TEMPLATE_ID_PARAM) String dossierTemplateId, @PathVariable(RULE_FILE_TYPE_PARAMETER_NAME) RuleFileType ruleFileType); + }