Merge branch 'RED-7962' into 'master'

RED-7962 Added rule upload and download

Closes RED-7962

See merge request redactmanager/persistence-service!225
This commit is contained in:
Dominique Eifländer 2023-11-23 16:01:15 +01:00
commit 8c090d14ba
6 changed files with 501 additions and 5 deletions

View File

@ -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_SUFFIX = "-rules.drl";
private final DossierTemplateController dossierTemplateController;
private final RulesPersistenceService rulesPersistenceService;
private final RulesValidationService rulesValidationService;
private final AuditPersistenceService auditPersistenceService;
public List<DossierTemplateModel> getAllDossierTemplates() {
return dossierTemplateController.getAllDossierTemplates();
}
public DossierTemplateModel getDossierTemplate(@PathVariable(DOSSIER_TEMPLATE_ID_PARAM) String dossierTemplateId) {
return dossierTemplateController.getDossierTemplate(dossierTemplateId);
}
@PreAuthorize("hasAuthority('" + WRITE_RULES + "')")
public ResponseEntity<RulesValidationResponse> 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<InputStreamResource> downloadEntityRules(@PathVariable(DOSSIER_TEMPLATE_ID_PARAM) String dossierTemplateId) {
return downloadRules(dossierTemplateId, RuleFileType.ENTITY);
}
@PreAuthorize("hasAuthority('" + WRITE_RULES + "')")
public ResponseEntity<RulesValidationResponse> 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<InputStreamResource> downloadComponentRules(@PathVariable(DOSSIER_TEMPLATE_ID_PARAM) String dossierTemplateId,
@PathVariable(RULE_FILE_TYPE_PARAMETER_NAME) RuleFileType ruleFileType) {
return downloadRules(dossierTemplateId, RuleFileType.COMPONENT);
}
@SneakyThrows
private ResponseEntity<RulesValidationResponse> 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.UNPROCESSABLE_ENTITY);
}
} 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<InputStreamResource> 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().toLowerCase() + RULES_DOWNLOAD_FILE_NAME_SUFFIX));
InputStream is = new ByteArrayInputStream(data);
return new ResponseEntity<>(new InputStreamResource(is), httpHeaders, HttpStatus.OK);
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<RulesValidationMessage> errors = new ArrayList<>();
/*
@Builder.Default
@Schema(description = "Show where warnings are in the uploaded rules file.")
List<RulesValidationMessage> warnings = new ArrayList<>();
@Builder.Default
@Schema(description = "Show deprecated methods are used in the uploaded rules file.")
List<RulesDeprecationMessage> deprecationWarnings = new ArrayList<>();
*/
}

View File

@ -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 = "dryRun";
@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<RulesValidationResponse> 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<InputStreamResource> 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<RulesValidationResponse> 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<InputStreamResource> downloadComponentRules(@PathVariable(DOSSIER_TEMPLATE_ID_PARAM) String dossierTemplateId, @PathVariable(RULE_FILE_TYPE_PARAMETER_NAME) RuleFileType ruleFileType);
}

View File

@ -108,6 +108,170 @@ paths:
$ref: '#/components/responses/429'
"500":
$ref: '#/components/responses/500'
/api/dossier-templates/{dossierTemplateId}/entity-rules:
get:
operationId: downloadEntityRules
tags:
- 1. Dossier Templates
summary: Download the entity rules of a specific dossier template.
description: |
Utilize this endpoint to download the entity rules of a designated dossier template. The file is named 'entity-rules.drl'
and contains the set of rules to annotate the entities in an analyzed file. The content of this file is in the Drools Rule Language
(DRL) format. Please find more details about the DRL in the
[Drools Language Reference](https://docs.drools.org/8.44.0.Final/drools-docs/drools/language-reference/index.html).
parameters:
- $ref: '#/components/parameters/dossierTemplateId'
responses:
"200":
headers:
Content-Disposition:
schema:
type: string
example: attachment; filename*=utf-8''entity-rules.drl
content:
application/octet-stream:
schema:
type: string
format: binary
description: |
Successfully downloaded the requested rules file.
"400":
$ref: '#/components/responses/400'
"401":
$ref: '#/components/responses/401'
"403":
$ref: '#/components/responses/403'
"404":
$ref: '#/components/responses/404-dossier-template'
"429":
$ref: '#/components/responses/429'
"500":
$ref: '#/components/responses/500'
post:
operationId: uploadEntityRules
tags:
- 1. Dossier Templates
summary: Upload or validate a entity rules file for a specific dossier template.
description: |
Utilize this endpoint to upload the entity rules to a designated dossier template. With the 'dryRun' parameter,
you have the possibility to just validate the rules file without actually saving it in the dossier template.
The uploaded rule file will be saved only if 'dryRun' is set to `false` and the response code is `200`. In this
case, the response object does not contain errors.
parameters:
- $ref: '#/components/parameters/dossierTemplateId'
- $ref: '#/components/parameters/dryRun'
requestBody:
content:
multipart/form-data:
schema:
$ref: '#/components/schemas/UploadRequest'
responses:
"200":
content:
application/json:
schema:
$ref: '#/components/schemas/RuleValidation'
example:
errors: [ ]
description: |
Successfully uploaded or validated the entity rules file. The returned response object will not contain any errors.
"400":
$ref: '#/components/responses/400'
"401":
$ref: '#/components/responses/401'
"403":
$ref: '#/components/responses/403'
"404":
$ref: '#/components/responses/404-dossier'
"422":
$ref: '#/components/responses/422-rules'
"429":
$ref: '#/components/responses/429'
"500":
$ref: '#/components/responses/500'
/api/dossier-templates/{dossierTemplateId}/component-rules:
get:
operationId: downloadComponentRules
tags:
- 1. Dossier Templates
summary: Download the component rules of a specific dossier template.
description: |
Utilize this endpoint to download the component rules of a designated dossier template. The file is named 'component-rules.drl'
and contains the set of rules to build components based on entities of an analyzed file. The content of this file is in the Drools Rule Language
(DRL) format. Please find more details about the DRL in the
[Drools Language Reference](https://docs.drools.org/8.44.0.Final/drools-docs/drools/language-reference/index.html).
parameters:
- $ref: '#/components/parameters/dossierTemplateId'
responses:
"200":
headers:
Content-Disposition:
schema:
type: string
example: attachment; filename*=utf-8''component-rules.drl
content:
application/octet-stream:
schema:
type: string
format: binary
description: |
Successfully downloaded the requested rules file.
"400":
$ref: '#/components/responses/400'
"401":
$ref: '#/components/responses/401'
"403":
$ref: '#/components/responses/403'
"404":
$ref: '#/components/responses/404-dossier-template'
"429":
$ref: '#/components/responses/429'
"500":
$ref: '#/components/responses/500'
post:
operationId: uploadComponentRules
tags:
- 1. Dossier Templates
summary: Upload or validate a component rules file for a specific dossier template.
description: |
Utilize this endpoint to upload the component rules to a designated dossier template. With the 'dryRun' parameter,
you have the possibility to just validate the rules file without actually saving it in the dossier template.
The uploaded rule file will be saved only if 'dryRun' is set to `false` and the response code is `200`. In this
case, the response object does not contain errors.
parameters:
- $ref: '#/components/parameters/dossierTemplateId'
- $ref: '#/components/parameters/dryRun'
requestBody:
content:
multipart/form-data:
schema:
$ref: '#/components/schemas/UploadRequest'
responses:
"200":
content:
application/json:
schema:
$ref: '#/components/schemas/RuleValidation'
example:
errors: [ ]
description: |
Successfully uploaded or validated the component rules file. The returned response object will not contain any errors.
"400":
$ref: '#/components/responses/400'
"401":
$ref: '#/components/responses/401'
"403":
$ref: '#/components/responses/403'
"404":
$ref: '#/components/responses/404-dossier'
"422":
$ref: '#/components/responses/422-rules'
"429":
$ref: '#/components/responses/429'
"500":
$ref: '#/components/responses/500'
/api/dossier-templates/{dossierTemplateId}/dossiers:
get:
operationId: getDossiers
@ -664,6 +828,13 @@ components:
$ref: '#/components/schemas/ErrorMessage'
description: |
Name conflict: The provided name is already in use by another dossier. It needs to be unique in the scope of your workspace.
422-rules:
content:
'*/*':
schema:
$ref: '#/components/schemas/RuleValidation'
description: |
Invalid rules file: There were validation errors, the rules file is unprocessable.
"429":
content:
'*/*':
@ -687,6 +858,18 @@ components:
style: simple
explode: false
description: The identifier of a dossier template
dryRun:
name: dryRun
in: query
required: false
schema:
default: false
type: boolean
style: form
explode: true
description: |
A toggle to activate the dry-run mode: If set to `false` (default), the request will update the system.
If set to `true`, the request will just be evaluated without actual changes in the system.
dossierId:
name: dossierId
in: path
@ -1681,7 +1864,7 @@ components:
description: A detailed description of the error, providing insights on the problem and potentially how to resolve or avoid it.
example:
timestamp: "2023-09-21T12:45:00Z"
message: "Invalid input provided for the 'name' field."
message: "An error occurred while processing the request."
FileAttributes:
type: object
description: Additional file attributes that can be set or imported
@ -2202,6 +2385,43 @@ components:
type: string
type: array
type: object
RuleValidationMessage:
description: Object containing information about an uploaded rules file.
example:
line: 123
column: 45
message: "Unable to Analyse Expression ..."
properties:
line:
description: The line number where the error or warning occurs.
format: int32
type: integer
column:
description: The column number where the error or warning occurs.
format: int32
type: integer
message:
description: The error or warning message that describes the details.
type: string
type: object
RuleValidation:
description: |
Information about the uploaded rules file. The `error` field is empty if there were no validation errors in the uploaded rules file.
example:
errors:
- line: 123
column: 45
message: "Unable to Analyse Expression ..."
- line: 234
column: 5
message: "Invalid rule syntax ..."
properties:
errors:
description: List of errors found in the uploaded rules file.
items:
$ref: '#/components/schemas/RuleValidationMessage'
type: array
type: object
LicenseReport:
type: object
description: A comprehensive report of licensing metrics and usage statistics.