From 95b351d3feb349de320ba348f0e0568f5bc85bf5 Mon Sep 17 00:00:00 2001 From: aoezyetimoglu Date: Thu, 15 Apr 2021 16:26:47 +0200 Subject: [PATCH 01/80] Testcommit --- .../service/redaction/v1/server/RedactionIntegrationTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java index 0c459750..be77a283 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java @@ -151,6 +151,7 @@ public class RedactionIntegrationTest { @Before public void stubClients() { + //Testkommentar when(rulesClient.getVersion(TEST_RULESET_ID)).thenReturn(0L); when(rulesClient.getRules(TEST_RULESET_ID)).thenReturn(new RulesResponse(RULES)); From 4f1bfb972f1d52af838318a7e1c9a202ebe5705c Mon Sep 17 00:00:00 2001 From: aoezyetimoglu Date: Wed, 21 Apr 2021 11:14:43 +0200 Subject: [PATCH 02/80] Adding persistence tests for file-management-service-processor --- redaction-service-v1/redaction-service-server-v1/pom.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/redaction-service-v1/redaction-service-server-v1/pom.xml b/redaction-service-v1/redaction-service-server-v1/pom.xml index b76c67d7..31788d3a 100644 --- a/redaction-service-v1/redaction-service-server-v1/pom.xml +++ b/redaction-service-v1/redaction-service-server-v1/pom.xml @@ -85,6 +85,12 @@ test-commons test + + junit + junit + 4.12 + test + From d614fa6001dd46b82da63e219fe924d7152768e7 Mon Sep 17 00:00:00 2001 From: cschabert Date: Fri, 21 May 2021 16:36:17 +0200 Subject: [PATCH 03/80] Swich to maven buld script --- bamboo-specs/pom.xml | 2 +- .../src/main/java/buildjob/PlanSpec.java | 22 ++------ .../src/main/resources/scripts/build-java.sh | 51 +++++++++++++++++++ 3 files changed, 57 insertions(+), 18 deletions(-) create mode 100755 bamboo-specs/src/main/resources/scripts/build-java.sh diff --git a/bamboo-specs/pom.xml b/bamboo-specs/pom.xml index 13ff387e..f0524e0f 100644 --- a/bamboo-specs/pom.xml +++ b/bamboo-specs/pom.xml @@ -5,7 +5,7 @@ com.atlassian.bamboo bamboo-specs-parent - 7.1.2 + 7.2.2 diff --git a/bamboo-specs/src/main/java/buildjob/PlanSpec.java b/bamboo-specs/src/main/java/buildjob/PlanSpec.java index e2c7fe08..b19ca4b7 100644 --- a/bamboo-specs/src/main/java/buildjob/PlanSpec.java +++ b/bamboo-specs/src/main/java/buildjob/PlanSpec.java @@ -21,6 +21,8 @@ import com.atlassian.bamboo.specs.builders.task.VcsTagTask; import com.atlassian.bamboo.specs.builders.trigger.BitbucketServerTrigger; import com.atlassian.bamboo.specs.model.task.InjectVariablesScope; import com.atlassian.bamboo.specs.util.BambooServer; +import com.atlassian.bamboo.specs.builders.task.ScriptTask; +import com.atlassian.bamboo.specs.model.task.ScriptTaskProperties.Location; import static com.atlassian.bamboo.specs.builders.task.TestParserTask.createJUnitParserTask; @@ -84,23 +86,9 @@ public class PlanSpec { .checkoutItems(new CheckoutItem().defaultRepository()), new ScriptTask() .description("Build") - .environmentVariables("MAVEN_OPTS="+JVM_ARGS) - .inlineBody("#!/bin/bash\n" + - "set -e\n" + - - "export MAVEN_OPTS=\"$MAVEN_OPTS "+JVM_ARGS +"\"\n" + - - "if [[ \"${bamboo.version_tag}\" != \"dev\" ]]; then ${bamboo_capability_system_builder_mvn3_Maven_3}/bin/mvn --no-transfer-progress -f ${bamboo_build_working_directory}/" + SERVICE_NAME + "-v1/pom.xml versions:set -DnewVersion=${bamboo.version_tag}; fi\n" + - "if [[ \"${bamboo.version_tag}\" != \"dev\" ]]; then ${bamboo_capability_system_builder_mvn3_Maven_3}/bin/mvn --no-transfer-progress -f ${bamboo_build_working_directory}/" + SERVICE_NAME + "-image-v1/pom.xml versions:set -DnewVersion=${bamboo.version_tag}; fi\n" + - - "if [[ \"${bamboo.version_tag}\" = \"dev\" ]]; then ${bamboo_capability_system_builder_mvn3_Maven_3}/bin/mvn -f ${bamboo_build_working_directory}/" + SERVICE_NAME + "-v1/pom.xml --no-transfer-progress clean install -Djava.security.egd=file:/dev/./urandom; fi\n" + - "if [[ \"${bamboo.version_tag}\" != \"dev\" ]]; then ${bamboo_capability_system_builder_mvn3_Maven_3}/bin/mvn -f ${bamboo_build_working_directory}/" + SERVICE_NAME + "-v1/pom.xml --no-transfer-progress clean deploy -e -DdeployAtEnd=true -Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true -Dmaven.wagon.http.ssl.ignore.validity.dates=true -DaltDeploymentRepository=iqser_release::default::https://nexus.iqser.com/repository/red-platform-releases; fi\n" + - - "${bamboo_capability_system_builder_mvn3_Maven_3}/bin/mvn --no-transfer-progress -f ${bamboo_build_working_directory}/" + SERVICE_NAME + "-image-v1/pom.xml package\n" + - "${bamboo_capability_system_builder_mvn3_Maven_3}/bin/mvn --no-transfer-progress -f ${bamboo_build_working_directory}/" + SERVICE_NAME + "-image-v1/pom.xml docker:push\n" + - - "if [[ \"${bamboo.version_tag}\" = \"dev\" ]]; then echo \"gitTag=${bamboo.planRepository.1.branch}_${bamboo.buildNumber}\" > git.tag; fi\n" + - "if [[ \"${bamboo.version_tag}\" != \"dev\" ]]; then echo \"gitTag=${bamboo.version_tag}\" > git.tag; fi\n"), + .location(Location.FILE) + .fileFromPath("bamboo-specs/src/main/resources/scripts/build-java.sh") + .argument(SERVICE_NAME), createJUnitParserTask() .description("Resultparser") .resultDirectories("**/test-reports/*.xml, **/target/surefire-reports/*.xml, **/target/failsafe-reports/*.xml") diff --git a/bamboo-specs/src/main/resources/scripts/build-java.sh b/bamboo-specs/src/main/resources/scripts/build-java.sh new file mode 100755 index 00000000..60dfe783 --- /dev/null +++ b/bamboo-specs/src/main/resources/scripts/build-java.sh @@ -0,0 +1,51 @@ +#!/bin/bash +set -e + +SERVICE_NAME=$1 + +if [[ "${bamboo_version_tag}" = "dev" ]] +then + ${bamboo_capability_system_builder_mvn3_Maven_3}/bin/mvn \ + -f ${bamboo_build_working_directory}/$SERVICE_NAME-v1/pom.xml \ + --no-transfer-progress \ + clean install \ + -Djava.security.egd=file:/dev/./urandomelse +else + ${bamboo_capability_system_builder_mvn3_Maven_3}/bin/mvn \ + --no-transfer-progress \ + -f ${bamboo_build_working_directory}/$SERVICE_NAME-v1/pom.xml \ + versions:set \ + -DnewVersion=${bamboo_version_tag} + ${bamboo_capability_system_builder_mvn3_Maven_3}/bin/mvn \ + --no-transfer-progress \ + -f ${bamboo_build_working_directory}/$SERVICE_NAME-image-v1/pom.xml \ + versions:set \ + -DnewVersion=${bamboo_version_tag} + ${bamboo_capability_system_builder_mvn3_Maven_3}/bin/mvn \ + -f ${bamboo_build_working_directory}/$SERVICE_NAME-v1/pom.xml \ + --no-transfer-progress \ + clean deploy \ + -e \ + -DdeployAtEnd=true \ + -Dmaven.wagon.http.ssl.insecure=true \ + -Dmaven.wagon.http.ssl.allowall=true \ + -Dmaven.wagon.http.ssl.ignore.validity.dates=true \ + -DaltDeploymentRepository=iqser_release::default::https://nexus.iqser.com/repository/red-platform-releases +fi + +${bamboo_capability_system_builder_mvn3_Maven_3}/bin/mvn \ + --no-transfer-progress \ + -f ${bamboo_build_working_directory}/$SERVICE_NAME-image-v1/pom.xml \ + package + +${bamboo_capability_system_builder_mvn3_Maven_3}/bin/mvn \ + --no-transfer-progress \ + -f ${bamboo_build_working_directory}/$SERVICE_NAME-image-v1/pom.xml \ + docker:push + +if [[ "${bamboo_version_tag}" = "dev" ]] +then + echo "gitTag=${bamboo_planRepository_1_branch}_${bamboo_buildNumber}" > git.tag +else + echo "gitTag=${bamboo_version_tag}" > git.tag +fi From dc1dba00c11f4778dc5483f47fc147b258b830d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Tue, 25 May 2021 13:45:33 +0200 Subject: [PATCH 04/80] RED-1536: Fixed changelog creation --- .../iqser/red/service/redaction/v1/model/RedactionLogEntry.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java index deb34209..d8074d4f 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java @@ -13,7 +13,7 @@ import java.util.List; @Builder @NoArgsConstructor @AllArgsConstructor -@EqualsAndHashCode(of = "id") +@EqualsAndHashCode public class RedactionLogEntry { private String id; From 35dec94ccdfc569f24d6983c5b2810dfd0f80e7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Fri, 28 May 2021 13:42:36 +0200 Subject: [PATCH 05/80] Fixed missing whitespaces --- .../v1/server/parsing/PDFAreaTextStripper.java | 11 +++++++++++ .../v1/server/parsing/PDFLinesTextStripper.java | 12 ++++++++++++ 2 files changed, 23 insertions(+) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/parsing/PDFAreaTextStripper.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/parsing/PDFAreaTextStripper.java index 9b52bf7b..8925d426 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/parsing/PDFAreaTextStripper.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/parsing/PDFAreaTextStripper.java @@ -46,6 +46,17 @@ public class PDFAreaTextStripper extends PDFTextStripperByArea { startIndex = i; } + + if (textPositions.get(i).getRotation() == 0 && i > 0 && textPositions.get(i).getX() > textPositions.get(i - 1).getEndX() + 1) { + List sublist = textPositions.subList(startIndex, i); + if (!(sublist.isEmpty() || sublist.size() == 1 && (sublist.get(0) + .getUnicode() + .equals(" ") || sublist.get(0).getUnicode().equals("\u00A0")))) { + textPositionSequences.add(new TextPositionSequence(sublist, pageNumber)); + } + startIndex = i; + } + if (i > 0 && (textPositions.get(i).getUnicode().equals(" ") || textPositions.get(i) .getUnicode() .equals("\u00A0")) && i <= textPositions.size() - 2) { diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/parsing/PDFLinesTextStripper.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/parsing/PDFLinesTextStripper.java index aa69cbbc..45bcef6a 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/parsing/PDFLinesTextStripper.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/parsing/PDFLinesTextStripper.java @@ -300,6 +300,18 @@ public class PDFLinesTextStripper extends PDFTextStripper { startIndex = i; } + + if (textPositions.get(i).getRotation() == 0 && i > 0 && textPositions.get(i).getX() > textPositions.get(i - 1).getEndX() + 1) { + List sublist = textPositions.subList(startIndex, i); + if (!(sublist.isEmpty() || sublist.size() == 1 && (sublist.get(0) + .getUnicode() + .equals(" ") || sublist.get(0).getUnicode().equals("\u00A0")))) { + textPositionSequences.add(new TextPositionSequence(sublist, pageNumber)); + } + startIndex = i; + } + + if (i > 0 && (textPositions.get(i).getUnicode().equals(" ") || textPositions.get(i) .getUnicode() .equals("\u00A0")) && i <= textPositions.size() - 2) { From e01074ef27b4aaabd15cd33a3cc2f29bc95066de Mon Sep 17 00:00:00 2001 From: Timo Date: Mon, 31 May 2021 10:56:48 +0300 Subject: [PATCH 06/80] migrated ruleset and projectid to new naming --- .../redaction/v1/model/AnalyzeRequest.java | 4 +- .../redaction/v1/model/AnalyzeResult.java | 2 +- .../redaction/v1/model/AnnotateRequest.java | 2 +- .../v1/model/RedactionChangeLog.java | 2 +- .../redaction/v1/model/RedactionLog.java | 2 +- .../redaction/v1/model/RedactionRequest.java | 4 +- .../v1/resources/RedactionResource.java | 4 +- .../controller/RedactionController.java | 22 +-- .../queue/RedactionMessageReceiver.java | 4 +- .../model/DictionaryRepresentation.java | 2 +- .../redaction/model/DictionaryVersion.java | 2 +- .../service/AnalyzeResponseService.java | 4 +- .../redaction/service/DictionaryService.java | 87 ++++++------ .../service/DroolsExecutionService.java | 38 +++--- .../service/EntityRedactionService.java | 12 +- .../redaction/service/ReanalyzeService.java | 52 +++---- .../service/RedactionChangeLogService.java | 8 +- .../service/RedactionLogCreatorService.java | 90 ++++++------ .../storage/RedactionStorageService.java | 20 +-- .../v1/server/RedactionIntegrationTest.java | 116 ++++++++-------- .../service/EntityRedactionServiceTest.java | 128 +++++++++--------- 21 files changed, 300 insertions(+), 305 deletions(-) diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeRequest.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeRequest.java index 113bdd43..4aa290e9 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeRequest.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeRequest.java @@ -13,9 +13,9 @@ import java.time.OffsetDateTime; @AllArgsConstructor public class AnalyzeRequest { - private String projectId; + private String dossierId; private String fileId; - private String ruleSetId; + private String dossierTemplateId; private boolean reanalyseOnlyIfPossible; private ManualRedactions manualRedactions; private OffsetDateTime lastProcessed; diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeResult.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeResult.java index 2f1ffbee..1ce9d759 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeResult.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeResult.java @@ -11,7 +11,7 @@ import lombok.NoArgsConstructor; @AllArgsConstructor public class AnalyzeResult { - private String projectId; + private String dossierId; private String fileId; private long duration; private int numberOfPages; diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnnotateRequest.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnnotateRequest.java index 4f65d74e..30d0a62f 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnnotateRequest.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnnotateRequest.java @@ -11,6 +11,6 @@ import lombok.NoArgsConstructor; @AllArgsConstructor public class AnnotateRequest { - private String projectId; + private String dossierId; private String fileId; } diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionChangeLog.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionChangeLog.java index 1270b800..10a41b05 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionChangeLog.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionChangeLog.java @@ -17,6 +17,6 @@ public class RedactionChangeLog { private long dictionaryVersion = -1; private long rulesVersion = -1; - private String ruleSetId; + private String dossierTemplateId; } diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLog.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLog.java index 9c28f5fc..7c916084 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLog.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLog.java @@ -13,7 +13,7 @@ public class RedactionLog { private List redactionLogEntry; private List legalBasis; - private String ruleSetId; + private String dossierTemplateId; private long dictionaryVersion = -1; private long rulesVersion = -1; diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionRequest.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionRequest.java index fd525887..263e7692 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionRequest.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionRequest.java @@ -11,8 +11,8 @@ import lombok.NoArgsConstructor; @AllArgsConstructor public class RedactionRequest { - private String projectId; + private String dossierId; private String fileId; - private String ruleSetId; + private String dossierTemplateId; private ManualRedactions manualRedactions; } diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/resources/RedactionResource.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/resources/RedactionResource.java index de766ba5..cdf45fcb 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/resources/RedactionResource.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/resources/RedactionResource.java @@ -10,7 +10,7 @@ public interface RedactionResource { String SERVICE_NAME = "redaction-service-v1"; - String RULE_SET_PARAMETER_NAME = "ruleSetId"; + String RULE_SET_PARAMETER_NAME = "dossierTemplateId"; String RULE_SET_PATH_VARIABLE = "/{" + RULE_SET_PARAMETER_NAME + "}"; @@ -27,7 +27,7 @@ public interface RedactionResource { RedactionResult htmlTables(@RequestBody RedactionRequest redactionRequest); @PostMapping(value = "/rules/update" + RULE_SET_PATH_VARIABLE, consumes = MediaType.APPLICATION_JSON_VALUE) - void updateRules(@PathVariable(RULE_SET_PARAMETER_NAME) String ruleSetId); + void updateRules(@PathVariable(RULE_SET_PARAMETER_NAME) String dossierTemplateId); @PostMapping(value = "/rules/test", consumes = MediaType.APPLICATION_JSON_VALUE) void testRules(@RequestBody String rules); diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java index 31f8583b..12381286 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java @@ -43,14 +43,14 @@ public class RedactionController implements RedactionResource { public AnnotateResponse annotate(@RequestBody AnnotateRequest annotateRequest) { - var storedObjectStream = redactionStorageService.getStoredObject(RedactionStorageService.StorageIdUtils.getStorageId(annotateRequest.getProjectId(), annotateRequest.getFileId(), FileType.ORIGIN)); - var redactionLog = redactionStorageService.getRedactionLog(annotateRequest.getProjectId(), annotateRequest.getFileId()); - var sectionsGrid = redactionStorageService.getSectionGrid(annotateRequest.getProjectId(), annotateRequest.getFileId()); + var storedObjectStream = redactionStorageService.getStoredObject(RedactionStorageService.StorageIdUtils.getStorageId(annotateRequest.getDossierId(), annotateRequest.getFileId(), FileType.ORIGIN)); + var redactionLog = redactionStorageService.getRedactionLog(annotateRequest.getDossierId(), annotateRequest.getFileId()); + var sectionsGrid = redactionStorageService.getSectionGrid(annotateRequest.getDossierId(), annotateRequest.getFileId()); try (PDDocument pdDocument = PDDocument.load(storedObjectStream, MemoryUsageSetting.setupTempFileOnly())) { pdDocument.setAllSecurityToBeRemoved(true); - dictionaryService.updateDictionary(redactionLog.getRuleSetId(), annotateRequest.getProjectId()); + dictionaryService.updateDictionary(redactionLog.getDossierTemplateId(), annotateRequest.getDossierId()); annotationService.annotate(pdDocument, redactionLog, sectionsGrid); try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { @@ -66,11 +66,11 @@ public class RedactionController implements RedactionResource { @Override public RedactionResult classify(@RequestBody RedactionRequest redactionRequest) { - var storedObjectStream = redactionStorageService.getStoredObject(RedactionStorageService.StorageIdUtils.getStorageId(redactionRequest.getProjectId(), redactionRequest.getFileId(), FileType.ORIGIN)); + var storedObjectStream = redactionStorageService.getStoredObject(RedactionStorageService.StorageIdUtils.getStorageId(redactionRequest.getDossierId(), redactionRequest.getFileId(), FileType.ORIGIN)); try { Document classifiedDoc = pdfSegmentationService.parseDocument(storedObjectStream); - storedObjectStream = redactionStorageService.getStoredObject(RedactionStorageService.StorageIdUtils.getStorageId(redactionRequest.getProjectId(), redactionRequest.getFileId(), FileType.ORIGIN)); + storedObjectStream = redactionStorageService.getStoredObject(RedactionStorageService.StorageIdUtils.getStorageId(redactionRequest.getDossierId(), redactionRequest.getFileId(), FileType.ORIGIN)); try (PDDocument pdDocument = PDDocument.load(storedObjectStream)) { pdDocument.setAllSecurityToBeRemoved(true); @@ -91,11 +91,11 @@ public class RedactionController implements RedactionResource { @Override public RedactionResult sections(@RequestBody RedactionRequest redactionRequest) { - var storedObjectStream = redactionStorageService.getStoredObject(RedactionStorageService.StorageIdUtils.getStorageId(redactionRequest.getProjectId(), redactionRequest.getFileId(), FileType.ORIGIN)); + var storedObjectStream = redactionStorageService.getStoredObject(RedactionStorageService.StorageIdUtils.getStorageId(redactionRequest.getDossierId(), redactionRequest.getFileId(), FileType.ORIGIN)); try { Document classifiedDoc = pdfSegmentationService.parseDocument(storedObjectStream); - storedObjectStream = redactionStorageService.getStoredObject(RedactionStorageService.StorageIdUtils.getStorageId(redactionRequest.getProjectId(), redactionRequest.getFileId(), FileType.ORIGIN)); + storedObjectStream = redactionStorageService.getStoredObject(RedactionStorageService.StorageIdUtils.getStorageId(redactionRequest.getDossierId(), redactionRequest.getFileId(), FileType.ORIGIN)); try (PDDocument pdDocument = PDDocument.load(storedObjectStream)) { pdDocument.setAllSecurityToBeRemoved(true); @@ -120,7 +120,7 @@ public class RedactionController implements RedactionResource { Document classifiedDoc; try { - var storedObjectStream = redactionStorageService.getStoredObject(RedactionStorageService.StorageIdUtils.getStorageId(redactionRequest.getProjectId(), redactionRequest.getFileId(), FileType.ORIGIN)); + var storedObjectStream = redactionStorageService.getStoredObject(RedactionStorageService.StorageIdUtils.getStorageId(redactionRequest.getDossierId(), redactionRequest.getFileId(), FileType.ORIGIN)); classifiedDoc = pdfSegmentationService.parseDocument(storedObjectStream, true); } catch (Exception e) { throw new RedactionException(e); @@ -143,9 +143,9 @@ public class RedactionController implements RedactionResource { @Override - public void updateRules(@PathVariable(RULE_SET_PARAMETER_NAME) String ruleSetId) { + public void updateRules(@PathVariable(RULE_SET_PARAMETER_NAME) String dossierTemplateId) { - droolsExecutionService.updateRules(ruleSetId); + droolsExecutionService.updateRules(dossierTemplateId); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/queue/RedactionMessageReceiver.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/queue/RedactionMessageReceiver.java index 2a08e4c8..667e21fc 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/queue/RedactionMessageReceiver.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/queue/RedactionMessageReceiver.java @@ -38,7 +38,7 @@ public class RedactionMessageReceiver { } log.info("Successfully analyzed {}", analyzeRequest); - fileStatusProcessingUpdateClient.analysisSuccessful(analyzeRequest.getProjectId(), analyzeRequest.getFileId(), result); + fileStatusProcessingUpdateClient.analysisSuccessful(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), result); } @RabbitHandler @@ -48,7 +48,7 @@ public class RedactionMessageReceiver { var analyzeRequest = objectMapper.readValue(in, AnalyzeRequest.class); log.info("Failed to process analyze request: {}", analyzeRequest); - fileStatusProcessingUpdateClient.analysisFailed(analyzeRequest.getProjectId(), analyzeRequest.getFileId()); + fileStatusProcessingUpdateClient.analysisFailed(analyzeRequest.getDossierId(), analyzeRequest.getFileId()); } } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/DictionaryRepresentation.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/DictionaryRepresentation.java index 0f7b6820..1f1e6da7 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/DictionaryRepresentation.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/DictionaryRepresentation.java @@ -10,7 +10,7 @@ import java.util.Map; @Data public class DictionaryRepresentation { - private String ruleSetId; + private String dossierTemplateId; private long dictionaryVersion = -1; private List dictionary = new ArrayList<>(); private float[] defaultColor; diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/DictionaryVersion.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/DictionaryVersion.java index 6a69bb60..067306a7 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/DictionaryVersion.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/DictionaryVersion.java @@ -11,6 +11,6 @@ import lombok.NoArgsConstructor; @AllArgsConstructor public class DictionaryVersion { - long rulesetVersion; + long dossierTemplateVersion; long dossierVersion; } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java index c6572912..bae6d1d0 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java @@ -9,7 +9,7 @@ import org.springframework.stereotype.Service; @Service public class AnalyzeResponseService { - public AnalyzeResult createAnalyzeResponse(String projectId, String fileId, long duration, int pageCount, RedactionLog redactionLog, RedactionChangeLog redactionChangeLog) { + public AnalyzeResult createAnalyzeResponse(String dossierId, String fileId, long duration, int pageCount, RedactionLog redactionLog, RedactionChangeLog redactionChangeLog) { boolean hasHints = redactionLog.getRedactionLogEntry().stream().anyMatch(RedactionLogEntry::isHint); boolean hasRequests = redactionLog.getRedactionLogEntry() @@ -31,7 +31,7 @@ public class AnalyzeResponseService { .isEmpty() && redactionChangeLog.getRedactionLogEntry().stream().anyMatch(entry -> !entry.getType().equals("false_positive")); return AnalyzeResult.builder() - .projectId(projectId) + .dossierId(dossierId) .fileId(fileId) .duration(duration) .numberOfPages(pageCount) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java index d9577bd0..bca42897 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java @@ -1,19 +1,12 @@ package com.iqser.red.service.redaction.v1.server.redaction.service; -import static com.iqser.red.service.configuration.v1.api.resource.DictionaryResource.GLOBAL_DOSSIER; - import com.iqser.red.service.configuration.v1.api.model.Colors; import com.iqser.red.service.configuration.v1.api.model.DictionaryEntry; import com.iqser.red.service.configuration.v1.api.model.TypeResponse; import com.iqser.red.service.configuration.v1.api.model.TypeResult; import com.iqser.red.service.redaction.v1.server.client.DictionaryClient; import com.iqser.red.service.redaction.v1.server.redaction.model.Dictionary; -import com.iqser.red.service.redaction.v1.server.redaction.model.DictionaryIncrement; -import com.iqser.red.service.redaction.v1.server.redaction.model.DictionaryIncrementValue; -import com.iqser.red.service.redaction.v1.server.redaction.model.DictionaryModel; -import com.iqser.red.service.redaction.v1.server.redaction.model.DictionaryRepresentation; -import com.iqser.red.service.redaction.v1.server.redaction.model.DictionaryVersion; - +import com.iqser.red.service.redaction.v1.server.redaction.model.*; import feign.FeignException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -25,6 +18,8 @@ import java.awt.Color; import java.util.*; import java.util.stream.Collectors; +import static com.iqser.red.service.configuration.v1.api.resource.DictionaryResource.GLOBAL_DOSSIER; + @Slf4j @Service @RequiredArgsConstructor @@ -36,30 +31,30 @@ public class DictionaryService { private final Map dictionariesByDossier = new HashMap<>(); - public DictionaryVersion updateDictionary(String ruleSetId, String dossierId) { + public DictionaryVersion updateDictionary(String dossierTemplateId, String dossierId) { - long rulesetDictionaryVersion = dictionaryClient.getVersion(ruleSetId, GLOBAL_DOSSIER); - var rulesetDictionary = dictionariesByRuleSets.get(ruleSetId); - if (rulesetDictionary == null || rulesetDictionaryVersion > rulesetDictionary.getDictionaryVersion()) { - updateDictionaryEntry(ruleSetId, rulesetDictionaryVersion, GLOBAL_DOSSIER); + long dossierTemplateDictionaryVersion = dictionaryClient.getVersion(dossierTemplateId, GLOBAL_DOSSIER); + var dossierTemplateDictionary = dictionariesByRuleSets.get(dossierTemplateId); + if (dossierTemplateDictionary == null || dossierTemplateDictionaryVersion > dossierTemplateDictionary.getDictionaryVersion()) { + updateDictionaryEntry(dossierTemplateId, dossierTemplateDictionaryVersion, GLOBAL_DOSSIER); } - long dossierDictionaryVersion = dictionaryClient.getVersion(ruleSetId, dossierId); + long dossierDictionaryVersion = dictionaryClient.getVersion(dossierTemplateId, dossierId); var dossierDictionary = dictionariesByDossier.get(dossierId); if (dossierDictionary == null || dossierDictionaryVersion > dossierDictionary.getDictionaryVersion()) { - updateDictionaryEntry(ruleSetId, dossierDictionaryVersion, dossierId); + updateDictionaryEntry(dossierTemplateId, dossierDictionaryVersion, dossierId); } - return DictionaryVersion.builder().rulesetVersion(rulesetDictionaryVersion).dossierVersion(dossierDictionaryVersion).build(); + return DictionaryVersion.builder().dossierTemplateVersion(dossierTemplateDictionaryVersion).dossierVersion(dossierDictionaryVersion).build(); } - public DictionaryIncrement getDictionaryIncrements(String ruleSetId, DictionaryVersion fromVersion, String dossierId) { + public DictionaryIncrement getDictionaryIncrements(String dossierTemplateId, DictionaryVersion fromVersion, String dossierId) { - DictionaryVersion version = updateDictionary(ruleSetId, dossierId); + DictionaryVersion version = updateDictionary(dossierTemplateId, dossierId); Set newValues = new HashSet<>(); - List dictionaryModels = dictionariesByRuleSets.get(ruleSetId).getDictionary(); + List dictionaryModels = dictionariesByRuleSets.get(dossierTemplateId).getDictionary(); dictionaryModels.forEach(dictionaryModel -> { dictionaryModel.getEntries().forEach(dictionaryEntry -> { if (dictionaryEntry.getVersion() > fromVersion.getRulesetVersion()) { @@ -83,12 +78,12 @@ public class DictionaryService { } - private void updateDictionaryEntry(String ruleSetId, long version, String dossierId) { + private void updateDictionaryEntry(String dossierTemplateId, long version, String dossierId) { try { DictionaryRepresentation dictionaryRepresentation = new DictionaryRepresentation(); - TypeResponse typeResponse = dictionaryClient.getAllTypes(ruleSetId, dossierId); + TypeResponse typeResponse = dictionaryClient.getAllTypes(dossierTemplateId, dossierId); if (typeResponse != null && CollectionUtils.isNotEmpty(typeResponse.getTypes())) { List dictionary = typeResponse.getTypes() @@ -100,18 +95,18 @@ public class DictionaryService { dictionary.forEach(dm -> dictionaryRepresentation.getLocalAccessMap().put(dm.getType(), dm)); - Colors colors = dictionaryClient.getColors(ruleSetId); + Colors colors = dictionaryClient.getColors(dossierTemplateId); dictionaryRepresentation.setDefaultColor(convertColor(colors.getDefaultColor())); dictionaryRepresentation.setRequestAddColor(convertColor(colors.getRequestAdd())); dictionaryRepresentation.setRequestRemoveColor(convertColor(colors.getRequestRemove())); dictionaryRepresentation.setNotRedactedColor(convertColor(colors.getNotRedacted())); - dictionaryRepresentation.setRuleSetId(ruleSetId); + dictionaryRepresentation.setDossierTemplateId(dossierTemplateId); dictionaryRepresentation.setDictionaryVersion(version); dictionaryRepresentation.setDictionary(dictionary); if(dossierId.equals(GLOBAL_DOSSIER)) { - dictionariesByRuleSets.put(ruleSetId, dictionaryRepresentation); + dictionariesByRuleSets.put(dossierTemplateId, dictionaryRepresentation); } else { dictionariesByDossier.put(dossierId, dictionaryRepresentation); } @@ -123,12 +118,12 @@ public class DictionaryService { } - public void updateExternalDictionary(Dictionary dictionary, String ruleSetId) { + public void updateExternalDictionary(Dictionary dictionary, String dossierTemplateId) { dictionary.getDictionaryModels().forEach(dm -> { if (dm.isRecommendation() && !dm.getLocalEntries().isEmpty()) { - dictionaryClient.addEntries(dm.getType(), ruleSetId, new ArrayList<>(dm.getLocalEntries()), false, GLOBAL_DOSSIER); - long externalVersion = dictionaryClient.getVersion(ruleSetId, GLOBAL_DOSSIER); + dictionaryClient.addEntries(dm.getType(), dossierTemplateId, new ArrayList<>(dm.getLocalEntries()), false, GLOBAL_DOSSIER); + long externalVersion = dictionaryClient.getVersion(dossierTemplateId, GLOBAL_DOSSIER); if (externalVersion == dictionary.getVersion().getRulesetVersion() + 1) { dictionary.getVersion().setRulesetVersion(externalVersion); } @@ -156,9 +151,9 @@ public class DictionaryService { } - public boolean isCaseInsensitiveDictionary(String type, String ruleSetId) { + public boolean isCaseInsensitiveDictionary(String type, String dossierTemplateId) { - DictionaryModel dictionaryModel = dictionariesByRuleSets.get(ruleSetId).getLocalAccessMap().get(type); + DictionaryModel dictionaryModel = dictionariesByRuleSets.get(dossierTemplateId).getLocalAccessMap().get(type); if (dictionaryModel != null) { return dictionaryModel.isCaseInsensitive(); } @@ -166,19 +161,19 @@ public class DictionaryService { } - public float[] getColor(String type, String ruleSetId) { + public float[] getColor(String type, String dossierTemplateId) { - DictionaryModel model = dictionariesByRuleSets.get(ruleSetId).getLocalAccessMap().get(type); + DictionaryModel model = dictionariesByRuleSets.get(dossierTemplateId).getLocalAccessMap().get(type); if (model != null) { return model.getColor(); } - return dictionariesByRuleSets.get(ruleSetId).getDefaultColor(); + return dictionariesByRuleSets.get(dossierTemplateId).getDefaultColor(); } - public boolean isHint(String type, String ruleSetId) { + public boolean isHint(String type, String dossierTemplateId) { - DictionaryModel model = dictionariesByRuleSets.get(ruleSetId).getLocalAccessMap().get(type); + DictionaryModel model = dictionariesByRuleSets.get(dossierTemplateId).getLocalAccessMap().get(type); if (model != null) { return model.isHint(); } @@ -186,9 +181,9 @@ public class DictionaryService { } - public boolean isRecommendation(String type, String ruleSetId) { + public boolean isRecommendation(String type, String dossierTemplateId) { - DictionaryModel model = dictionariesByRuleSets.get(ruleSetId).getLocalAccessMap().get(type); + DictionaryModel model = dictionariesByRuleSets.get(dossierTemplateId).getLocalAccessMap().get(type); if (model != null) { return model.isRecommendation(); } @@ -196,12 +191,12 @@ public class DictionaryService { } - public Dictionary getDeepCopyDictionary(String ruleSetId, String dossierId) { + public Dictionary getDeepCopyDictionary(String dossierTemplateId, String dossierId) { List copy = new ArrayList<>(); - var rulesetRepresentation = dictionariesByRuleSets.get(ruleSetId); - rulesetRepresentation.getDictionary().forEach(dm -> { + var dossierTemplateRepresentation = dictionariesByRuleSets.get(dossierTemplateId); + dossierTemplateRepresentation.getDictionary().forEach(dm -> { copy.add(SerializationUtils.clone(dm)); }); @@ -215,25 +210,25 @@ public class DictionaryService { dossierDictionaryVersion = dossierRepresentation.getDictionaryVersion(); } - return new Dictionary(copy, DictionaryVersion.builder().rulesetVersion(rulesetRepresentation.getDictionaryVersion()).dossierVersion(dossierDictionaryVersion).build()); + return new Dictionary(copy, DictionaryVersion.builder().dossierTemplateVersion(dossierTemplateRepresentation.getDictionaryVersion()).dossierVersion(dossierDictionaryVersion).build()); } - public float[] getRequestRemoveColor(String ruleSetId) { + public float[] getRequestRemoveColor(String dossierTemplateId) { - return dictionariesByRuleSets.get(ruleSetId).getRequestAddColor(); + return dictionariesByRuleSets.get(dossierTemplateId).getRequestAddColor(); } - public float[] getNotRedactedColor(String ruleSetId) { + public float[] getNotRedactedColor(String dossierTemplateId) { - return dictionariesByRuleSets.get(ruleSetId).getNotRedactedColor(); + return dictionariesByRuleSets.get(dossierTemplateId).getNotRedactedColor(); } - public float[] getRequestAddColor(String ruleSetId) { + public float[] getRequestAddColor(String dossierTemplateId) { - return dictionariesByRuleSets.get(ruleSetId).getRequestAddColor(); + return dictionariesByRuleSets.get(dossierTemplateId).getRequestAddColor(); } } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DroolsExecutionService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DroolsExecutionService.java index 708efa12..bd26cc12 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DroolsExecutionService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DroolsExecutionService.java @@ -28,14 +28,14 @@ public class DroolsExecutionService { private final Map kieContainers = new HashMap<>(); - private final Map rulesVersionPerRuleSetId = new HashMap<>(); + private final Map rulesVersionPerDossierTemplateId = new HashMap<>(); - public KieContainer getKieContainer(String ruleSetId) { + public KieContainer getKieContainer(String dossierTemplateId) { - KieContainer container = kieContainers.get(ruleSetId); + KieContainer container = kieContainers.get(dossierTemplateId); if (container == null) { - return createOrUpdateKieContainer(ruleSetId); + return createOrUpdateKieContainer(dossierTemplateId); } else { return container; } @@ -55,43 +55,43 @@ public class DroolsExecutionService { } - public KieContainer updateRules(String ruleSetId) { + public KieContainer updateRules(String dossierTemplateId) { - long version = rulesClient.getVersion(ruleSetId); - Long rulesVersion = rulesVersionPerRuleSetId.get(ruleSetId); + long version = rulesClient.getVersion(dossierTemplateId); + Long rulesVersion = rulesVersionPerDossierTemplateId.get(dossierTemplateId); if (rulesVersion == null) { rulesVersion = -1L; } if (version > rulesVersion.longValue()) { - rulesVersionPerRuleSetId.put(ruleSetId, version); - return createOrUpdateKieContainer(ruleSetId); + rulesVersionPerDossierTemplateId.put(dossierTemplateId, version); + return createOrUpdateKieContainer(dossierTemplateId); } - return getKieContainer(ruleSetId); + return getKieContainer(dossierTemplateId); } - private KieContainer createOrUpdateKieContainer(String ruleSetId) { + private KieContainer createOrUpdateKieContainer(String dossierTemplateId) { try { - RulesResponse rules = rulesClient.getRules(ruleSetId); + RulesResponse rules = rulesClient.getRules(dossierTemplateId); if (rules == null || StringUtils.isEmpty(rules.getRules())) { throw new RuntimeException("Rules cannot be empty."); } KieServices kieServices = KieServices.Factory.get(); - KieModule kieModule = getKieModule(ruleSetId, rules.getRules(), kieServices); + KieModule kieModule = getKieModule(dossierTemplateId, rules.getRules(), kieServices); - var container = kieContainers.get(ruleSetId); + var container = kieContainers.get(dossierTemplateId); if (container != null) { container.updateToVersion(kieModule.getReleaseId()); return container; } container = kieServices.newKieContainer(kieModule.getReleaseId()); - kieContainers.put(ruleSetId, container); + kieContainers.put(dossierTemplateId, container); return container; } catch (Exception e) { throw new RulesValidationException("Could not update rules: " + e.getMessage(), e); @@ -100,11 +100,11 @@ public class DroolsExecutionService { } - private KieModule getKieModule(String ruleSetId, String rules, KieServices kieServices) { + private KieModule getKieModule(String dossierTemplateId, String rules, KieServices kieServices) { KieFileSystem kieFileSystem = kieServices.newKieFileSystem(); InputStream input = new ByteArrayInputStream(rules.getBytes(StandardCharsets.UTF_8)); - kieFileSystem.write("src/main/resources/drools/rules" + ruleSetId + ".drl", kieServices.getResources() + kieFileSystem.write("src/main/resources/drools/rules" + dossierTemplateId + ".drl", kieServices.getResources() .newInputStreamResource(input)); KieBuilder kieBuilder = kieServices.newKieBuilder(kieFileSystem); kieBuilder.buildAll(); @@ -122,9 +122,9 @@ public class DroolsExecutionService { } - public long getRulesVersion(String ruleSetId) { + public long getRulesVersion(String dossierTemplateId) { - Long rulesVersion = rulesVersionPerRuleSetId.get(ruleSetId); + Long rulesVersion = rulesVersionPerDossierTemplateId.get(dossierTemplateId); if (rulesVersion == null) { return -1; } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java index 3e8875af..02da3aa7 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java @@ -33,13 +33,13 @@ public class EntityRedactionService { private final SurroundingWordsService surroundingWordsService; - public void processDocument(Document classifiedDoc, String ruleSetId, ManualRedactions manualRedactions, String dossierId) { + public void processDocument(Document classifiedDoc, String dossierTemplateId, ManualRedactions manualRedactions, String dossierId) { - dictionaryService.updateDictionary(ruleSetId, dossierId); - KieContainer container = droolsExecutionService.updateRules(ruleSetId); - long rulesVersion = droolsExecutionService.getRulesVersion(ruleSetId); + dictionaryService.updateDictionary(dossierTemplateId, dossierId); + KieContainer container = droolsExecutionService.updateRules(dossierTemplateId); + long rulesVersion = droolsExecutionService.getRulesVersion(dossierTemplateId); - Dictionary dictionary = dictionaryService.getDeepCopyDictionary(ruleSetId, dossierId); + Dictionary dictionary = dictionaryService.getDeepCopyDictionary(dossierTemplateId, dossierId); Set documentEntities = new HashSet<>(findEntities(classifiedDoc, container, manualRedactions, dictionary, false, null)); @@ -75,7 +75,7 @@ public class EntityRedactionService { } } - dictionaryService.updateExternalDictionary(dictionary, ruleSetId); + dictionaryService.updateExternalDictionary(dictionary, dossierTemplateId); classifiedDoc.setDictionaryVersion(dictionary.getVersion()); classifiedDoc.setRulesVersion(rulesVersion); diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java index 511d689e..9c0d75b8 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java @@ -50,7 +50,7 @@ public class ReanalyzeService { try { var storedObjectStream = redactionStorageService.getStoredObject(RedactionStorageService.StorageIdUtils.getStorageId(analyzeRequest - .getProjectId(), analyzeRequest.getFileId(), FileType.ORIGIN)); + .getDossierId(), analyzeRequest.getFileId(), FileType.ORIGIN)); classifiedDoc = pdfSegmentationService.parseDocument(storedObjectStream); pageCount = classifiedDoc.getPages().size(); } catch (Exception e) { @@ -58,35 +58,35 @@ public class ReanalyzeService { } log.info("Document structure analysis successful, starting redaction analysis..."); - entityRedactionService.processDocument(classifiedDoc, analyzeRequest.getRuleSetId(), analyzeRequest.getManualRedactions(), analyzeRequest - .getProjectId()); + entityRedactionService.processDocument(classifiedDoc, analyzeRequest.getDossierTemplateId(), analyzeRequest.getManualRedactions(), analyzeRequest + .getDossierId()); redactionLogCreatorService.createRedactionLog(classifiedDoc, pageCount, analyzeRequest.getManualRedactions(), analyzeRequest - .getRuleSetId()); + .getDossierTemplateId()); log.info("Redaction analysis successful..."); - var legalBasis = legalBasisClient.getLegalBasisMapping(analyzeRequest.getRuleSetId()); + var legalBasis = legalBasisClient.getLegalBasisMapping(analyzeRequest.getDossierTemplateId()); var redactionLog = new RedactionLog(classifiedDoc.getRedactionLogEntities(),legalBasis, - analyzeRequest.getRuleSetId(), + analyzeRequest.getDossierTemplateId(), classifiedDoc.getDictionaryVersion().getRulesetVersion(), classifiedDoc.getRulesVersion(), classifiedDoc.getDictionaryVersion().getDossierVersion(), - legalBasisClient.getVersion(analyzeRequest.getRuleSetId())); + legalBasisClient.getVersion(analyzeRequest.getDossierTemplateId())); - log.info("Analyzed with rules {} and dictionary {} for ruleSet: {}", classifiedDoc.getRulesVersion(), classifiedDoc - .getDictionaryVersion(), analyzeRequest.getRuleSetId()); + log.info("Analyzed with rules {} and dictionary {} for dossierTemplate: {}", classifiedDoc.getRulesVersion(), classifiedDoc + .getDictionaryVersion(), analyzeRequest.getDossierTemplateId()); // first create changelog - this only happens when we migrate files analyzed via the old process and we don't want to loose changeLog data - var changeLog = redactionChangeLogService.createAndStoreChangeLog(analyzeRequest.getProjectId(), analyzeRequest.getFileId(), redactionLog); + var changeLog = redactionChangeLogService.createAndStoreChangeLog(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), redactionLog); // store redactionLog - redactionStorageService.storeObject(analyzeRequest.getProjectId(), analyzeRequest.getFileId(), FileType.REDACTION_LOG, redactionLog); - redactionStorageService.storeObject(analyzeRequest.getProjectId(), analyzeRequest.getFileId(), FileType.TEXT, new Text(pageCount, classifiedDoc + redactionStorageService.storeObject(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), FileType.REDACTION_LOG, redactionLog); + redactionStorageService.storeObject(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), FileType.TEXT, new Text(pageCount, classifiedDoc .getSectionText())); - redactionStorageService.storeObject(analyzeRequest.getProjectId(), analyzeRequest.getFileId(), FileType.SECTION_GRID, classifiedDoc + redactionStorageService.storeObject(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), FileType.SECTION_GRID, classifiedDoc .getSectionGrid()); long duration = System.currentTimeMillis() - startTime; - return analyzeResponseService.createAnalyzeResponse(analyzeRequest.getProjectId(), analyzeRequest.getFileId(), duration, pageCount, redactionLog, changeLog); + return analyzeResponseService.createAnalyzeResponse(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), duration, pageCount, redactionLog, changeLog); } @@ -95,16 +95,16 @@ public class ReanalyzeService { long startTime = System.currentTimeMillis(); - var redactionLog = redactionStorageService.getRedactionLog(analyzeRequest.getProjectId(), analyzeRequest.getFileId()); - var text = redactionStorageService.getText(analyzeRequest.getProjectId(), analyzeRequest.getFileId()); + var redactionLog = redactionStorageService.getRedactionLog(analyzeRequest.getDossierId(), analyzeRequest.getFileId()); + var text = redactionStorageService.getText(analyzeRequest.getDossierId(), analyzeRequest.getFileId()); // not yet ready for reanalysis if (redactionLog == null || text == null || text.getNumberOfPages() == 0) { return analyze(analyzeRequest); } - DictionaryIncrement dictionaryIncrement = dictionaryService.getDictionaryIncrements(analyzeRequest.getRuleSetId(), new DictionaryVersion(redactionLog - .getDictionaryVersion(), redactionLog.getDossierDictionaryVersion()), analyzeRequest.getProjectId()); + DictionaryIncrement dictionaryIncrement = dictionaryService.getDictionaryIncrements(analyzeRequest.getDossierTemplateId(), new DictionaryVersion(redactionLog + .getDictionaryVersion(), redactionLog.getDossierDictionaryVersion()), analyzeRequest.getDossierId()); Set manualForceAndRemoveIds = getForceAndRemoveIds(analyzeRequest.getManualRedactions()); Map> comments = null; @@ -164,9 +164,9 @@ public class ReanalyzeService { //-- - KieContainer kieContainer = droolsExecutionService.updateRules(analyzeRequest.getRuleSetId()); + KieContainer kieContainer = droolsExecutionService.updateRules(analyzeRequest.getDossierTemplateId()); - Dictionary dictionary = dictionaryService.getDeepCopyDictionary(analyzeRequest.getRuleSetId(), analyzeRequest.getProjectId()); + Dictionary dictionary = dictionaryService.getDeepCopyDictionary(analyzeRequest.getDossierTemplateId(), analyzeRequest.getDossierId()); List sectionSearchableTextPairs = new ArrayList<>(); for (SectionText reanalysisSection : reanalysisSections) { @@ -229,16 +229,16 @@ public class ReanalyzeService { for (int page = 1; page <= text.getNumberOfPages(); page++) { if (entitiesPerPage.get(page) != null) { newRedactionLogEntries.addAll(redactionLogCreatorService.addEntries(entitiesPerPage, analyzeRequest.getManualRedactions(), page, analyzeRequest - .getRuleSetId())); + .getDossierTemplateId())); } if (imagesPerPage.get(page) != null) { newRedactionLogEntries.addAll(redactionLogCreatorService.addImageEntries(imagesPerPage, analyzeRequest.getManualRedactions(), page, analyzeRequest - .getRuleSetId())); + .getDossierTemplateId())); } newRedactionLogEntries.addAll(redactionLogCreatorService.addManualAddEntries(manualAdds, comments, page, analyzeRequest - .getRuleSetId())); + .getDossierTemplateId())); } redactionLog.getRedactionLogEntry() @@ -256,12 +256,12 @@ public class ReanalyzeService { redactionLog.setDictionaryVersion(dictionaryIncrement.getDictionaryVersion().getRulesetVersion()); redactionLog.setDossierDictionaryVersion(dictionaryIncrement.getDictionaryVersion().getDossierVersion()); - var changeLog = redactionChangeLogService.createAndStoreChangeLog(analyzeRequest.getProjectId(), analyzeRequest.getFileId(), redactionLog); - redactionStorageService.storeObject(analyzeRequest.getProjectId(), analyzeRequest.getFileId(), FileType.REDACTION_LOG, redactionLog); + var changeLog = redactionChangeLogService.createAndStoreChangeLog(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), redactionLog); + redactionStorageService.storeObject(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), FileType.REDACTION_LOG, redactionLog); long duration = System.currentTimeMillis() - startTime; - return analyzeResponseService.createAnalyzeResponse(analyzeRequest.getProjectId(), analyzeRequest.getFileId(), duration, text + return analyzeResponseService.createAnalyzeResponse(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), duration, text .getNumberOfPages(), redactionLog, changeLog); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionChangeLogService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionChangeLogService.java index 72bb7954..1d586ad1 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionChangeLogService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionChangeLogService.java @@ -22,12 +22,12 @@ public class RedactionChangeLogService { private final RedactionStorageService redactionStorageService; - public RedactionChangeLog createAndStoreChangeLog(String projectId, String fileId, RedactionLog currentRedactionLog) { + public RedactionChangeLog createAndStoreChangeLog(String dossierId, String fileId, RedactionLog currentRedactionLog) { try { - RedactionLog previousRedactionLog = redactionStorageService.getRedactionLog(projectId, fileId); + RedactionLog previousRedactionLog = redactionStorageService.getRedactionLog(dossierId, fileId); var changeLog = createChangeLog(currentRedactionLog, previousRedactionLog); - redactionStorageService.storeObject(projectId, fileId, FileType.REDACTION_CHANGELOG, changeLog); + redactionStorageService.storeObject(dossierId, fileId, FileType.REDACTION_CHANGELOG, changeLog); return changeLog; } catch (Exception e) { log.debug("Previous redaction log not available"); @@ -58,7 +58,7 @@ public class RedactionChangeLogService { .collect(Collectors.toList())); return new RedactionChangeLog(changeLogEntries, currentRedactionLog.getDictionaryVersion(), currentRedactionLog.getRulesVersion(), currentRedactionLog - .getRuleSetId()); + .getDossierTemplateId()); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java index 4a591420..9aef4dc7 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java @@ -32,7 +32,7 @@ public class RedactionLogCreatorService { public void createRedactionLog(Document classifiedDoc, int numberOfPages, ManualRedactions manualRedactions, - String ruleSetId) { + String dossierTemplateId) { Set manualRedactionPages = getManualRedactionPages(manualRedactions); @@ -42,24 +42,24 @@ public class RedactionLogCreatorService { if (classifiedDoc.getEntities().get(page) != null) { classifiedDoc.getRedactionLogEntities() - .addAll(addEntries(classifiedDoc.getEntities(), manualRedactions, page, ruleSetId)); + .addAll(addEntries(classifiedDoc.getEntities(), manualRedactions, page, dossierTemplateId)); } if (manualRedactionPages.contains(page)) { classifiedDoc.getRedactionLogEntities() - .addAll(addManualAddEntries(manualRedactions.getEntriesToAdd(), manualRedactions.getComments(), page, ruleSetId)); + .addAll(addManualAddEntries(manualRedactions.getEntriesToAdd(), manualRedactions.getComments(), page, dossierTemplateId)); } if (classifiedDoc.getImages().get(page) != null && !classifiedDoc.getImages().get(page).isEmpty()) { classifiedDoc.getRedactionLogEntities() - .addAll(addImageEntries(classifiedDoc.getImages(), manualRedactions, page, ruleSetId)); + .addAll(addImageEntries(classifiedDoc.getImages(), manualRedactions, page, dossierTemplateId)); } } } public List addImageEntries(Map> images, ManualRedactions manualRedactions, - int pageNumber, String ruleSetId) { + int pageNumber, String dossierTemplateId) { List redactionLogEntities = new ArrayList<>(); @@ -69,14 +69,14 @@ public class RedactionLogCreatorService { RedactionLogEntry redactionLogEntry = RedactionLogEntry.builder() .id(id) - .color(getColorForImage(image, ruleSetId, false)) + .color(getColorForImage(image, dossierTemplateId, false)) .isImage(true) .type(image.getType()) .redacted(image.isRedaction()) .reason(image.getRedactionReason()) .legalBasis(image.getLegalBasis()) .matchedRule(image.getMatchedRule()) - .isHint(dictionaryService.isHint(image.getType(), ruleSetId)) + .isHint(dictionaryService.isHint(image.getType(), dossierTemplateId)) .manual(false) .isDictionaryEntry(false) .isRecommendation(false) @@ -96,11 +96,11 @@ public class RedactionLogCreatorService { redactionLogEntry.setRedacted(false); redactionLogEntry.setStatus(Status.APPROVED); manualOverrideReason = image.getRedactionReason() + ", removed by manual override"; - redactionLogEntry.setColor(getColorForImage(image, ruleSetId, false)); + redactionLogEntry.setColor(getColorForImage(image, dossierTemplateId, false)); } else if (manualRemoval.getStatus().equals(Status.REQUESTED)) { manualOverrideReason = image.getRedactionReason() + ", requested to remove"; redactionLogEntry.setStatus(Status.REQUESTED); - redactionLogEntry.setColor(getColorForImage(image, ruleSetId, true)); + redactionLogEntry.setColor(getColorForImage(image, dossierTemplateId, true)); } else { redactionLogEntry.setStatus(Status.DECLINED); } @@ -121,13 +121,13 @@ public class RedactionLogCreatorService { image.setRedaction(true); redactionLogEntry.setRedacted(true); redactionLogEntry.setStatus(Status.APPROVED); - redactionLogEntry.setColor(getColorForImage(image, ruleSetId, false)); + redactionLogEntry.setColor(getColorForImage(image, dossierTemplateId, false)); manualOverrideReason = image.getRedactionReason() + ", forced by manual override"; redactionLogEntry.setLegalBasis(manualForceRedact.getLegalBasis()); } else if (manualForceRedact.getStatus().equals(Status.REQUESTED)) { manualOverrideReason = image.getRedactionReason() + ", requested to force redact"; redactionLogEntry.setStatus(Status.REQUESTED); - redactionLogEntry.setColor(getColorForImage(image, ruleSetId, true)); + redactionLogEntry.setColor(getColorForImage(image, dossierTemplateId, true)); redactionLogEntry.setLegalBasis(manualForceRedact.getLegalBasis()); } else { redactionLogEntry.setStatus(Status.DECLINED); @@ -166,7 +166,7 @@ public class RedactionLogCreatorService { public List addEntries(Map> entities, ManualRedactions manualRedactions, - int page, String ruleSetId) { + int page, String dossierTemplateId) { List redactionLogEntities = new ArrayList<>(); @@ -180,7 +180,7 @@ public class RedactionLogCreatorService { for (EntityPositionSequence entityPositionSequence : entity.getPositionSequences()) { - RedactionLogEntry redactionLogEntry = createRedactionLogEntry(entity, ruleSetId); + RedactionLogEntry redactionLogEntry = createRedactionLogEntry(entity, dossierTemplateId); if (processedIds.contains(entityPositionSequence.getId())) { // TODO refactor this outer loop jump as soon as we have the time. @@ -199,11 +199,11 @@ public class RedactionLogCreatorService { redactionLogEntry.setRedacted(false); redactionLogEntry.setStatus(Status.APPROVED); manualOverrideReason = entity.getRedactionReason() + ", removed by manual override"; - redactionLogEntry.setColor(getColor(entity, ruleSetId, false)); + redactionLogEntry.setColor(getColor(entity, dossierTemplateId, false)); } else if (manualRemoval.getStatus().equals(Status.REQUESTED)) { manualOverrideReason = entity.getRedactionReason() + ", requested to remove"; redactionLogEntry.setStatus(Status.REQUESTED); - redactionLogEntry.setColor(getColor(entity, ruleSetId, true)); + redactionLogEntry.setColor(getColor(entity, dossierTemplateId, true)); } else { redactionLogEntry.setStatus(Status.DECLINED); } @@ -224,13 +224,13 @@ public class RedactionLogCreatorService { entity.setRedaction(true); redactionLogEntry.setRedacted(true); redactionLogEntry.setStatus(Status.APPROVED); - redactionLogEntry.setColor(getColor(entity, ruleSetId, false)); + redactionLogEntry.setColor(getColor(entity, dossierTemplateId, false)); manualOverrideReason = entity.getRedactionReason() + ", forced by manual override"; redactionLogEntry.setLegalBasis(manualForceRedact.getLegalBasis()); } else if (manualForceRedact.getStatus().equals(Status.REQUESTED)) { manualOverrideReason = entity.getRedactionReason() + ", requested to force redact"; redactionLogEntry.setStatus(Status.REQUESTED); - redactionLogEntry.setColor(getColor(entity, ruleSetId, true)); + redactionLogEntry.setColor(getColor(entity, dossierTemplateId, true)); redactionLogEntry.setLegalBasis(manualForceRedact.getLegalBasis()); } else { redactionLogEntry.setStatus(Status.DECLINED); @@ -299,7 +299,7 @@ public class RedactionLogCreatorService { public List addManualAddEntries(Set manualAdds, Map> comments, int page, - String ruleSetId) { + String dossierTemplateId) { List redactionLogEntities = new ArrayList<>(); @@ -311,7 +311,7 @@ public class RedactionLogCreatorService { String id = manualRedactionEntry.getId(); - RedactionLogEntry redactionLogEntry = createRedactionLogEntry(manualRedactionEntry, id, ruleSetId); + RedactionLogEntry redactionLogEntry = createRedactionLogEntry(manualRedactionEntry, id, dossierTemplateId); List rectanglesOnPage = new ArrayList<>(); for (Rectangle rectangle : manualRedactionEntry.getPositions()) { @@ -338,11 +338,11 @@ public class RedactionLogCreatorService { private RedactionLogEntry createRedactionLogEntry(ManualRedactionEntry manualRedactionEntry, String id, - String ruleSetId) { + String dossierTemplateId) { return RedactionLogEntry.builder() .id(id) - .color(getColorForManualAdd(manualRedactionEntry.getType(), ruleSetId, manualRedactionEntry.getStatus())) + .color(getColorForManualAdd(manualRedactionEntry.getType(), dossierTemplateId, manualRedactionEntry.getStatus())) .reason(manualRedactionEntry.getReason()) .legalBasis(manualRedactionEntry.getLegalBasis()) .value(manualRedactionEntry.getValue()) @@ -360,17 +360,17 @@ public class RedactionLogCreatorService { } - private RedactionLogEntry createRedactionLogEntry(Entity entity, String ruleSetId) { + private RedactionLogEntry createRedactionLogEntry(Entity entity, String dossierTemplateId) { return RedactionLogEntry.builder() - .color(getColor(entity, ruleSetId, false)) + .color(getColor(entity, dossierTemplateId, false)) .reason(entity.getRedactionReason()) .legalBasis(entity.getLegalBasis()) .value(entity.getWord()) .type(entity.getType()) .redacted(entity.isRedaction()) - .isHint(isHint(entity, ruleSetId)) - .isRecommendation(isRecommendation(entity, ruleSetId)) + .isHint(isHint(entity, dossierTemplateId)) + .isRecommendation(isRecommendation(entity, dossierTemplateId)) .section(entity.getHeadline()) .sectionNumber(entity.getSectionNumber()) .matchedRule(entity.getMatchedRule()) @@ -384,56 +384,56 @@ public class RedactionLogCreatorService { } - private float[] getColor(Entity entity, String ruleSetId, boolean requestedToRemove) { + private float[] getColor(Entity entity, String dossierTemplateId, boolean requestedToRemove) { if (requestedToRemove) { - return dictionaryService.getRequestRemoveColor(ruleSetId); + return dictionaryService.getRequestRemoveColor(dossierTemplateId); } - if (!entity.isRedaction() && !isHint(entity, ruleSetId)) { - return dictionaryService.getNotRedactedColor(ruleSetId); + if (!entity.isRedaction() && !isHint(entity, dossierTemplateId)) { + return dictionaryService.getNotRedactedColor(dossierTemplateId); } - return dictionaryService.getColor(entity.getType(), ruleSetId); + return dictionaryService.getColor(entity.getType(), dossierTemplateId); } - private float[] getColorForManualAdd(String type, String ruleSetId, Status status) { + private float[] getColorForManualAdd(String type, String dossierTemplateId, Status status) { if (status.equals(Status.REQUESTED)) { - return dictionaryService.getRequestAddColor(ruleSetId); + return dictionaryService.getRequestAddColor(dossierTemplateId); } else if (status.equals(Status.DECLINED)) { - return dictionaryService.getNotRedactedColor(ruleSetId); + return dictionaryService.getNotRedactedColor(dossierTemplateId); } - return getColor(type, ruleSetId); + return getColor(type, dossierTemplateId); } - private float[] getColor(String type, String ruleSetId) { + private float[] getColor(String type, String dossierTemplateId) { - return dictionaryService.getColor(type, ruleSetId); + return dictionaryService.getColor(type, dossierTemplateId); } - private float[] getColorForImage(Image image, String ruleSetId, boolean requestedToRemove) { + private float[] getColorForImage(Image image, String dossierTemplateId, boolean requestedToRemove) { if (requestedToRemove) { - return dictionaryService.getRequestRemoveColor(ruleSetId); + return dictionaryService.getRequestRemoveColor(dossierTemplateId); } - if (!image.isRedaction() && !dictionaryService.isHint(image.getType(), ruleSetId)) { - return dictionaryService.getNotRedactedColor(ruleSetId); + if (!image.isRedaction() && !dictionaryService.isHint(image.getType(), dossierTemplateId)) { + return dictionaryService.getNotRedactedColor(dossierTemplateId); } - return dictionaryService.getColor(image.getType(), ruleSetId); + return dictionaryService.getColor(image.getType(), dossierTemplateId); } - private boolean isHint(Entity entity, String ruleSetId) { + private boolean isHint(Entity entity, String dossierTemplateId) { - return dictionaryService.isHint(entity.getType(), ruleSetId); + return dictionaryService.isHint(entity.getType(), dossierTemplateId); } - private boolean isRecommendation(Entity entity, String ruleSetId) { + private boolean isRecommendation(Entity entity, String dossierTemplateId) { - return dictionaryService.isRecommendation(entity.getType(), ruleSetId); + return dictionaryService.isRecommendation(entity.getType(), dossierTemplateId); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/storage/RedactionStorageService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/storage/RedactionStorageService.java index f350b7ac..021beeff 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/storage/RedactionStorageService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/storage/RedactionStorageService.java @@ -32,16 +32,16 @@ public class RedactionStorageService { @SneakyThrows - public void storeObject(String projectId, String fileId, FileType fileType, Object any) { - storageService.storeObject(StorageIdUtils.getStorageId(projectId, fileId, fileType), objectMapper.writeValueAsBytes(any)); + public void storeObject(String dossierId, String fileId, FileType fileType, Object any) { + storageService.storeObject(StorageIdUtils.getStorageId(dossierId, fileId, fileType), objectMapper.writeValueAsBytes(any)); } - public RedactionLog getRedactionLog(String projectId, String fileId) { + public RedactionLog getRedactionLog(String dossierId, String fileId) { InputStreamResource inputStreamResource; try { - inputStreamResource = storageService.getObject(StorageIdUtils.getStorageId(projectId, fileId, FileType.REDACTION_LOG)); + inputStreamResource = storageService.getObject(StorageIdUtils.getStorageId(dossierId, fileId, FileType.REDACTION_LOG)); } catch (StorageObjectDoesNotExist e) { log.debug("Text not available."); return null; @@ -55,11 +55,11 @@ public class RedactionStorageService { } - public Text getText(String projectId, String fileId) { + public Text getText(String dossierId, String fileId) { InputStreamResource inputStreamResource; try { - inputStreamResource = storageService.getObject(StorageIdUtils.getStorageId(projectId, fileId, FileType.TEXT)); + inputStreamResource = storageService.getObject(StorageIdUtils.getStorageId(dossierId, fileId, FileType.TEXT)); } catch (StorageObjectDoesNotExist e) { log.debug("Text not available."); return null; @@ -73,9 +73,9 @@ public class RedactionStorageService { } - public SectionGrid getSectionGrid(String projectId, String fileId) { + public SectionGrid getSectionGrid(String dossierId, String fileId) { - var sectionGrid = storageService.getObject(StorageIdUtils.getStorageId(projectId, fileId, FileType.SECTION_GRID)); + var sectionGrid = storageService.getObject(StorageIdUtils.getStorageId(dossierId, fileId, FileType.SECTION_GRID)); try { return objectMapper.readValue(sectionGrid.getInputStream(), SectionGrid.class); } catch (IOException e) { @@ -95,8 +95,8 @@ public class RedactionStorageService { public static class StorageIdUtils { - public static String getStorageId(String projectId, String fileId, FileType fileType) { - return projectId + "/" + fileId + "." + fileType.name() + fileType.getExtension(); + public static String getStorageId(String dossierId, String fileId, FileType fileType) { + return dossierId + "/" + fileId + "." + fileType.name() + fileType.getExtension(); } } diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java index 6f14d819..c7f8a107 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java @@ -128,8 +128,8 @@ public class RedactionIntegrationTest { private final Colors colors = new Colors(); private final Map reanlysisVersions = new HashMap<>(); - private final static String TEST_RULESET_ID = "123"; - private final static String TEST_PROJECT_ID = "123"; + private final static String TEST_DOSSIER_TEMPLATE_ID = "123"; + private final static String TEST_DOSSIER_ID = "123"; private final static String TEST_FILE_ID = "123"; @Configuration @@ -173,21 +173,21 @@ public class RedactionIntegrationTest { @Before public void stubClients() { - when(rulesClient.getVersion(TEST_RULESET_ID)).thenReturn(0L); - when(rulesClient.getRules(TEST_RULESET_ID)).thenReturn(new RulesResponse(RULES)); + when(rulesClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(0L); + when(rulesClient.getRules(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(new RulesResponse(RULES)); loadDictionaryForTest(); loadTypeForTest(); - when(dictionaryClient.getVersion(TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(0L); - when(dictionaryClient.getAllTypes(TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(TypeResponse.builder() + when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(0L); + when(dictionaryClient.getAllTypes(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(TypeResponse.builder() .types(getTypeResponse()) .build()); - when(dictionaryClient.getVersion(TEST_RULESET_ID, TEST_PROJECT_ID)).thenReturn(0L); - when(dictionaryClient.getAllTypes(TEST_RULESET_ID, TEST_PROJECT_ID)).thenReturn(TypeResponse.builder() + when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, TEST_DOSSIER_ID)).thenReturn(0L); + when(dictionaryClient.getAllTypes(TEST_DOSSIER_TEMPLATE_ID, TEST_DOSSIER_ID)).thenReturn(TypeResponse.builder() .types(List.of(TypeResult.builder() .type(DOSSIER_REDACTIONS) - .ruleSetId(TEST_RULESET_ID) + .ruleSetId(TEST_DOSSIER_TEMPLATE_ID) .hexColor( "#ffe187") .isHint(hintTypeMap.get(DOSSIER_REDACTIONS)) .isCaseInsensitive(caseInSensitiveMap.get(DOSSIER_REDACTIONS)) @@ -196,28 +196,28 @@ public class RedactionIntegrationTest { .build())) .build()); - when(dictionaryClient.getDictionaryForType(VERTEBRATE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(VERTEBRATE, false)); - when(dictionaryClient.getDictionaryForType(ADDRESS, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(ADDRESS, false)); - when(dictionaryClient.getDictionaryForType(AUTHOR, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(AUTHOR, false)); - when(dictionaryClient.getDictionaryForType(SPONSOR, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(SPONSOR, false)); - when(dictionaryClient.getDictionaryForType(NO_REDACTION_INDICATOR, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(NO_REDACTION_INDICATOR, false)); - when(dictionaryClient.getDictionaryForType(REDACTION_INDICATOR, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(REDACTION_INDICATOR, false)); - when(dictionaryClient.getDictionaryForType(HINT_ONLY, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(HINT_ONLY, false)); - when(dictionaryClient.getDictionaryForType(MUST_REDACT, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(MUST_REDACT, false)); - when(dictionaryClient.getDictionaryForType(PUBLISHED_INFORMATION, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(PUBLISHED_INFORMATION, false)); - when(dictionaryClient.getDictionaryForType(TEST_METHOD, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(TEST_METHOD, false)); - when(dictionaryClient.getDictionaryForType(PII, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(PII, false)); - when(dictionaryClient.getDictionaryForType(RECOMMENDATION_AUTHOR, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(RECOMMENDATION_AUTHOR, false)); - when(dictionaryClient.getDictionaryForType(RECOMMENDATION_ADDRESS, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(RECOMMENDATION_ADDRESS, false)); - when(dictionaryClient.getDictionaryForType(FALSE_POSITIVE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(FALSE_POSITIVE, false)); - when(dictionaryClient.getDictionaryForType(PURITY, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(PURITY, false)); - when(dictionaryClient.getDictionaryForType(IMAGE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(IMAGE, false)); - when(dictionaryClient.getDictionaryForType(OCR, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(OCR, false)); - when(dictionaryClient.getDictionaryForType(LOGO, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(LOGO, false)); - when(dictionaryClient.getDictionaryForType(SIGNATURE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(SIGNATURE, false)); - when(dictionaryClient.getDictionaryForType(FORMULA, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(FORMULA, false)); - when(dictionaryClient.getDictionaryForType(DOSSIER_REDACTIONS, TEST_RULESET_ID, TEST_PROJECT_ID)).thenReturn(getDictionaryResponse(DOSSIER_REDACTIONS, true)); - when(dictionaryClient.getColors(TEST_RULESET_ID)).thenReturn(colors); + when(dictionaryClient.getDictionaryForType(VERTEBRATE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(VERTEBRATE, false)); + when(dictionaryClient.getDictionaryForType(ADDRESS, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(ADDRESS, false)); + when(dictionaryClient.getDictionaryForType(AUTHOR, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(AUTHOR, false)); + when(dictionaryClient.getDictionaryForType(SPONSOR, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(SPONSOR, false)); + when(dictionaryClient.getDictionaryForType(NO_REDACTION_INDICATOR, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(NO_REDACTION_INDICATOR, false)); + when(dictionaryClient.getDictionaryForType(REDACTION_INDICATOR, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(REDACTION_INDICATOR, false)); + when(dictionaryClient.getDictionaryForType(HINT_ONLY, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(HINT_ONLY, false)); + when(dictionaryClient.getDictionaryForType(MUST_REDACT, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(MUST_REDACT, false)); + when(dictionaryClient.getDictionaryForType(PUBLISHED_INFORMATION, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(PUBLISHED_INFORMATION, false)); + when(dictionaryClient.getDictionaryForType(TEST_METHOD, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(TEST_METHOD, false)); + when(dictionaryClient.getDictionaryForType(PII, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(PII, false)); + when(dictionaryClient.getDictionaryForType(RECOMMENDATION_AUTHOR, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(RECOMMENDATION_AUTHOR, false)); + when(dictionaryClient.getDictionaryForType(RECOMMENDATION_ADDRESS, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(RECOMMENDATION_ADDRESS, false)); + when(dictionaryClient.getDictionaryForType(FALSE_POSITIVE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(FALSE_POSITIVE, false)); + when(dictionaryClient.getDictionaryForType(PURITY, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(PURITY, false)); + when(dictionaryClient.getDictionaryForType(IMAGE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(IMAGE, false)); + when(dictionaryClient.getDictionaryForType(OCR, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(OCR, false)); + when(dictionaryClient.getDictionaryForType(LOGO, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(LOGO, false)); + when(dictionaryClient.getDictionaryForType(SIGNATURE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(SIGNATURE, false)); + when(dictionaryClient.getDictionaryForType(FORMULA, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(FORMULA, false)); + when(dictionaryClient.getDictionaryForType(DOSSIER_REDACTIONS, TEST_DOSSIER_TEMPLATE_ID, TEST_DOSSIER_ID)).thenReturn(getDictionaryResponse(DOSSIER_REDACTIONS, true)); + when(dictionaryClient.getColors(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(colors); } @@ -461,7 +461,7 @@ public class RedactionIntegrationTest { .stream() .map(typeColor -> TypeResult.builder() .type(typeColor.getKey()) - .ruleSetId(TEST_RULESET_ID) + .ruleSetId(TEST_DOSSIER_TEMPLATE_ID) .hexColor(typeColor.getValue()) .isHint(hintTypeMap.get(typeColor.getKey())) .isCaseInsensitive(caseInSensitiveMap.get(typeColor.getKey())) @@ -525,7 +525,7 @@ public class RedactionIntegrationTest { Map> duplicates = new HashMap<>(); - var redactionLog = redactionStorageService.getRedactionLog(TEST_PROJECT_ID, TEST_FILE_ID); + var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); redactionLog.getRedactionLogEntry().forEach(entry -> { duplicates.computeIfAbsent(entry.getId(), v -> new ArrayList<>()).add(entry); @@ -536,10 +536,10 @@ public class RedactionIntegrationTest { }); dictionary.get(AUTHOR).add("Drinking water"); - when(dictionaryClient.getVersion(TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(1L); + when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(1L); AnnotateResponse annotateResponse = redactionController.annotate(AnnotateRequest.builder() - .projectId(TEST_PROJECT_ID) + .dossierId(TEST_DOSSIER_ID) .fileId(TEST_FILE_ID) .build()); @@ -581,7 +581,7 @@ public class RedactionIntegrationTest { Map> duplicates = new HashMap<>(); - var redactionLog = redactionStorageService.getRedactionLog(TEST_PROJECT_ID, TEST_FILE_ID); + var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); redactionLog.getRedactionLogEntry().forEach(entry -> { duplicates.computeIfAbsent(entry.getId(), v -> new ArrayList<>()).add(entry); @@ -592,7 +592,7 @@ public class RedactionIntegrationTest { }); dictionary.get(AUTHOR).add("Drinking water"); - when(dictionaryClient.getVersion(TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(1L); + when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(1L); long rstart = System.currentTimeMillis(); reanalyzeService.reanalyze(request); @@ -636,8 +636,8 @@ public class RedactionIntegrationTest { AnalyzeResult result = reanalyzeService.analyze(request); - var redactionLog = redactionStorageService.getRedactionLog(TEST_PROJECT_ID, TEST_FILE_ID); - var text = redactionStorageService.getText(TEST_PROJECT_ID, TEST_FILE_ID); + var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); + var text = redactionStorageService.getText(TEST_DOSSIER_ID, TEST_FILE_ID); redactionLog.getRedactionLogEntry().forEach(entry -> { if (entry.isImage()) { @@ -650,7 +650,7 @@ public class RedactionIntegrationTest { System.out.println("first analysis duration: " + (end - start)); try (FileOutputStream fileOutputStream = new FileOutputStream("/tmp/Test.json")) { - fileOutputStream.write(objectMapper.writeValueAsBytes(redactionStorageService.getText(TEST_PROJECT_ID, TEST_FILE_ID))); + fileOutputStream.write(objectMapper.writeValueAsBytes(redactionStorageService.getText(TEST_DOSSIER_ID, TEST_FILE_ID))); } int correctFound = 0; @@ -683,9 +683,9 @@ public class RedactionIntegrationTest { dictionary.get(VERTEBRATE).add("s-metolachlor"); reanlysisVersions.put("s-metolachlor", 3L); - when(dictionaryClient.getVersion(TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(3L); + when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(3L); - when(dictionaryClient.getDictionaryForType(VERTEBRATE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(VERTEBRATE, false)); + when(dictionaryClient.getDictionaryForType(VERTEBRATE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(VERTEBRATE, false)); start = System.currentTimeMillis(); AnalyzeResult reanalyzeResult = reanalyzeService.reanalyze(request); @@ -694,7 +694,7 @@ public class RedactionIntegrationTest { System.out.println("reanalysis analysis duration: " + (end - start)); AnnotateResponse annotateResponse = redactionController.annotate(AnnotateRequest.builder() - .projectId(TEST_PROJECT_ID) + .dossierId(TEST_DOSSIER_ID) .fileId(TEST_FILE_ID) .build()); @@ -715,7 +715,7 @@ public class RedactionIntegrationTest { AnalyzeResult result = reanalyzeService.analyze(request); AnnotateResponse annotateResponse = redactionController.annotate(AnnotateRequest.builder() - .projectId(TEST_PROJECT_ID) + .dossierId(TEST_DOSSIER_ID) .fileId(TEST_FILE_ID) .build()); @@ -786,7 +786,7 @@ public class RedactionIntegrationTest { AnnotateResponse annotateResponse = redactionController.annotate(AnnotateRequest.builder() - .projectId(TEST_PROJECT_ID) + .dossierId(TEST_DOSSIER_ID) .fileId(TEST_FILE_ID) .build()); @@ -811,9 +811,9 @@ public class RedactionIntegrationTest { AnalyzeRequest request = prepareStorage(pdfFileResource.getInputStream()); RedactionRequest redactionRequest = RedactionRequest.builder() - .projectId(request.getProjectId()) + .dossierId(request.getDossierId()) .fileId(request.getFileId()) - .ruleSetId(request.getRuleSetId()) + .dossierTemplateId(request.getDossierTemplateId()) .build(); RedactionResult result = redactionController.classify(redactionRequest); @@ -833,9 +833,9 @@ public class RedactionIntegrationTest { AnalyzeRequest request = prepareStorage(pdfFileResource.getInputStream()); RedactionRequest redactionRequest = RedactionRequest.builder() - .projectId(request.getProjectId()) + .dossierId(request.getDossierId()) .fileId(request.getFileId()) - .ruleSetId(request.getRuleSetId()) + .dossierTemplateId(request.getDossierTemplateId()) .build(); RedactionResult result = redactionController.sections(redactionRequest); @@ -855,9 +855,9 @@ public class RedactionIntegrationTest { AnalyzeRequest request = prepareStorage(pdfFileResource.getInputStream()); RedactionRequest redactionRequest = RedactionRequest.builder() - .projectId(request.getProjectId()) + .dossierId(request.getDossierId()) .fileId(request.getFileId()) - .ruleSetId(request.getRuleSetId()) + .dossierTemplateId(request.getDossierTemplateId()) .build(); RedactionResult result = redactionController.htmlTables(redactionRequest); @@ -877,9 +877,9 @@ public class RedactionIntegrationTest { AnalyzeRequest request = prepareStorage(pdfFileResource.getInputStream()); RedactionRequest redactionRequest = RedactionRequest.builder() - .projectId(request.getProjectId()) + .dossierId(request.getDossierId()) .fileId(request.getFileId()) - .ruleSetId(request.getRuleSetId()) + .dossierTemplateId(request.getDossierTemplateId()) .build(); RedactionResult result = redactionController.htmlTables(redactionRequest); @@ -899,7 +899,7 @@ public class RedactionIntegrationTest { AnalyzeResult result = reanalyzeService.analyze(request); - var redactionLog = redactionStorageService.getRedactionLog(TEST_PROJECT_ID, TEST_FILE_ID); + var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); redactionLog.getRedactionLogEntry().forEach(entry -> { if (!entry.isHint()) { @@ -920,15 +920,15 @@ public class RedactionIntegrationTest { private AnalyzeRequest prepareStorage(InputStream stream) { AnalyzeRequest request = AnalyzeRequest.builder() - .ruleSetId(TEST_RULESET_ID) - .projectId(TEST_PROJECT_ID) + .dossierTemplateId(TEST_DOSSIER_TEMPLATE_ID) + .dossierId(TEST_DOSSIER_ID) .fileId(TEST_FILE_ID) .lastProcessed(OffsetDateTime.now()) .build(); var bytes = IOUtils.toByteArray(stream); - storageService.storeObject(RedactionStorageService.StorageIdUtils.getStorageId(TEST_PROJECT_ID, TEST_FILE_ID, FileType.ORIGIN), bytes); + storageService.storeObject(RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.ORIGIN), bytes); return request; @@ -947,7 +947,7 @@ public class RedactionIntegrationTest { AnalyzeResult result = reanalyzeService.analyze(request); AnnotateResponse annotateResponse = redactionController.annotate(AnnotateRequest.builder() - .projectId(TEST_PROJECT_ID) + .dossierId(TEST_DOSSIER_ID) .fileId(TEST_FILE_ID) .build()); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionServiceTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionServiceTest.java index 512710d0..e4afe9d9 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionServiceTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionServiceTest.java @@ -82,7 +82,7 @@ public class EntityRedactionServiceTest { @MockBean private LegalBasisClient legalBasisClient; - private final static String TEST_RULESET_ID = "123"; + private final static String TEST_DOSSIER_TEMPLATE_ID = "123"; @Configuration @EnableAutoConfiguration(exclude = {RabbitAutoConfiguration.class}) @@ -138,20 +138,20 @@ public class EntityRedactionServiceTest { DictionaryResponse dictionaryResponse = DictionaryResponse.builder() .entries(toDictionaryEntry(Arrays.asList("Casey, H.W.", "O’Loughlin, C.K.", "Salamon, C.M.", "Smith, S.H."))) .build(); - when(dictionaryClient.getVersion(TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); - when(dictionaryClient.getDictionaryForType(AUTHOR_CODE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); + when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); + when(dictionaryClient.getDictionaryForType(AUTHOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); DictionaryResponse addressResponse = DictionaryResponse.builder() .entries(toDictionaryEntry(Collections.singletonList("Toxigenics, Inc., Decatur, IL 62526, USA"))) .build(); - when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); + when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); DictionaryResponse sponsorResponse = DictionaryResponse.builder() .entries(Collections.emptyList()) .build(); - when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); + when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); Document classifiedDoc = pdfSegmentationService.parseDocument(pdfFileResource.getInputStream()); - entityRedactionService.processDocument(classifiedDoc, TEST_RULESET_ID, null, "dossierId"); + entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId"); assertThat(classifiedDoc.getEntities()).hasSize(1); // one page assertThat(classifiedDoc.getEntities().get(1)).hasSize(7);// 3 author cells, 1 address, 1 Y and 2 N entities } @@ -165,19 +165,19 @@ public class EntityRedactionServiceTest { DictionaryResponse dictionaryResponse = DictionaryResponse.builder() .entries(toDictionaryEntry(Arrays.asList("Casey, H.W.", "O’Loughlin, C.K.", "Salamon, C.M.", "Smith, S.H."))) .build(); - when(dictionaryClient.getVersion(TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); - when(dictionaryClient.getDictionaryForType(AUTHOR_CODE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); + when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); + when(dictionaryClient.getDictionaryForType(AUTHOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); DictionaryResponse addressResponse = DictionaryResponse.builder() .entries(toDictionaryEntry(Collections.singletonList("Toxigenics, Inc., Decatur, IL 62526, USA"))) .build(); - when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); + when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); DictionaryResponse sponsorResponse = DictionaryResponse.builder() .entries(Collections.emptyList()) .build(); - when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); + when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); Document classifiedDoc = pdfSegmentationService.parseDocument(pdfFileResource.getInputStream()); - entityRedactionService.processDocument(classifiedDoc, TEST_RULESET_ID, null, "dossierId"); + entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId"); assertThat(classifiedDoc.getEntities()).hasSize(1); // one page assertThat(classifiedDoc.getEntities().get(1)).hasSize(7);// 3 author cells, 1 address, 1 Y and 2 N entities } @@ -188,21 +188,21 @@ public class EntityRedactionServiceTest { ClassPathResource pdfFileResource = new ClassPathResource("files/Cyprodinil/40 Cyprodinil - EU AIR3 - LCA Section 1" + " Supplement - Identity of the active substance - Reference list.pdf"); - when(dictionaryClient.getVersion(TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); + when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); DictionaryResponse dictionaryResponse = DictionaryResponse.builder() .entries(toDictionaryEntry(new ArrayList<>(ResourceLoader.load("dictionaries/CBI_author.txt")))) .build(); - when(dictionaryClient.getDictionaryForType(AUTHOR_CODE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); + when(dictionaryClient.getDictionaryForType(AUTHOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); DictionaryResponse addressResponse = DictionaryResponse.builder() .entries(toDictionaryEntry(new ArrayList<>(ResourceLoader.load("dictionaries/CBI_address.txt")))) .build(); - when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); + when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); DictionaryResponse sponsorResponse = DictionaryResponse.builder() .entries(Collections.emptyList()) .build(); - when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); + when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); Document classifiedDoc = pdfSegmentationService.parseDocument(pdfFileResource.getInputStream()); - entityRedactionService.processDocument(classifiedDoc, TEST_RULESET_ID, null, "dossierId"); + entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId"); assertThat(classifiedDoc.getEntities() .entrySet() .stream() @@ -210,7 +210,7 @@ public class EntityRedactionServiceTest { pdfFileResource = new ClassPathResource("files/Compounds/27 A8637C - EU AIR3 - MCP Section 1 - Identity of " + "the plant protection product.pdf"); classifiedDoc = pdfSegmentationService.parseDocument(pdfFileResource.getInputStream()); - entityRedactionService.processDocument(classifiedDoc, TEST_RULESET_ID, null, "dossierId"); + entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId"); assertThat(classifiedDoc.getEntities() .entrySet() .stream() @@ -221,21 +221,21 @@ public class EntityRedactionServiceTest { public void testFalsePositiveInWrongCell() throws IOException { ClassPathResource pdfFileResource = new ClassPathResource("files/Minimal Examples/Row With Ambiguous Redaction.pdf"); - when(dictionaryClient.getVersion(TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); + when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); DictionaryResponse dictionaryResponse = DictionaryResponse.builder() .entries(toDictionaryEntry(new ArrayList<>(ResourceLoader.load("dictionaries/CBI_author.txt")))) .build(); - when(dictionaryClient.getDictionaryForType(AUTHOR_CODE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); + when(dictionaryClient.getDictionaryForType(AUTHOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); DictionaryResponse addressResponse = DictionaryResponse.builder() .entries(toDictionaryEntry(new ArrayList<>(ResourceLoader.load("dictionaries/CBI_address.txt")))) .build(); - when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); + when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); DictionaryResponse sponsorResponse = DictionaryResponse.builder() .entries(toDictionaryEntry(new ArrayList<>(ResourceLoader.load("dictionaries/CBI_sponsor.txt")))) .build(); - when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); + when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); Document classifiedDoc = pdfSegmentationService.parseDocument(pdfFileResource.getInputStream()); - entityRedactionService.processDocument(classifiedDoc, TEST_RULESET_ID, null, "dossierId"); + entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId"); assertThat(classifiedDoc.getEntities()).hasSize(1); // one page assertThat(classifiedDoc.getEntities().get(1).stream() .filter(entity -> entity.getMatchedRule() == 9) @@ -283,26 +283,26 @@ public class EntityRedactionServiceTest { "\"Reg (EC) No 1107/2009 Art. 63 (2g)\");\n" + " section.redactBetween(\"Contact:\", \"Tel.:\", \"address\", 6,true, \"Applicant information was found\", \"Reg (EC) No 1107/2009 Art. 63 (2g)\");\n" + " end"; - when(rulesClient.getVersion(TEST_RULESET_ID)).thenReturn(RULES_VERSION.incrementAndGet()); - when(rulesClient.getRules(TEST_RULESET_ID)).thenReturn(new RulesResponse(tableRules)); - droolsExecutionService.updateRules(TEST_RULESET_ID); + when(rulesClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(RULES_VERSION.incrementAndGet()); + when(rulesClient.getRules(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(new RulesResponse(tableRules)); + droolsExecutionService.updateRules(TEST_DOSSIER_TEMPLATE_ID); ClassPathResource pdfFileResource = new ClassPathResource("files/Minimal Examples/Applicant Producer Table.pdf"); - when(dictionaryClient.getVersion(TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); + when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); DictionaryResponse dictionaryResponse = DictionaryResponse.builder() .entries(toDictionaryEntry(new ArrayList<>(ResourceLoader.load("dictionaries/CBI_author.txt")))) .build(); - when(dictionaryClient.getDictionaryForType(AUTHOR_CODE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); + when(dictionaryClient.getDictionaryForType(AUTHOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); DictionaryResponse addressResponse = DictionaryResponse.builder() .entries(toDictionaryEntry(new ArrayList<>(ResourceLoader.load("dictionaries/CBI_address.txt")))) .build(); - when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); + when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); DictionaryResponse sponsorResponse = DictionaryResponse.builder() .entries(Collections.emptyList()) .build(); - when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); + when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); Document classifiedDoc = pdfSegmentationService.parseDocument(pdfFileResource.getInputStream()); - entityRedactionService.processDocument(classifiedDoc, TEST_RULESET_ID, null, "dossierId"); + entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId"); assertThat(classifiedDoc.getEntities()).hasSize(1); // one page assertThat(classifiedDoc.getEntities().get(1).stream() .filter(entity -> entity.getMatchedRule() == 6) @@ -322,26 +322,26 @@ public class EntityRedactionServiceTest { "Section(searchText.toLowerCase().contains(\"batches produced at\"))\n" + " then\n" + " section" + ".redactIfPrecededBy(\"batches produced at\", \"sponsor\", 11, \"Redacted because it represents a " + "sponsor company\", \"Reg (EC) No 1107/2009 Art. 63 (2g)\");\n" + " end"; - when(rulesClient.getVersion(TEST_RULESET_ID)).thenReturn(RULES_VERSION.incrementAndGet()); - when(rulesClient.getRules(TEST_RULESET_ID)).thenReturn(new RulesResponse(tableRules)); - droolsExecutionService.updateRules(TEST_RULESET_ID); + when(rulesClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(RULES_VERSION.incrementAndGet()); + when(rulesClient.getRules(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(new RulesResponse(tableRules)); + droolsExecutionService.updateRules(TEST_DOSSIER_TEMPLATE_ID); ClassPathResource pdfFileResource = new ClassPathResource("files/Minimal Examples/batches_new_line.pdf"); - when(dictionaryClient.getVersion(TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); + when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); DictionaryResponse addressResponse = DictionaryResponse.builder() .entries(Collections.emptyList()) .build(); - when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); + when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); DictionaryResponse authorResponse = DictionaryResponse.builder() .entries(Collections.emptyList()) .build(); - when(dictionaryClient.getDictionaryForType(AUTHOR_CODE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(authorResponse); + when(dictionaryClient.getDictionaryForType(AUTHOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(authorResponse); DictionaryResponse dictionaryResponse = DictionaryResponse.builder() .entries(toDictionaryEntry(new ArrayList<>(ResourceLoader.load("dictionaries/CBI_sponsor.txt")))) .build(); - when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); + when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); Document classifiedDoc = pdfSegmentationService.parseDocument(pdfFileResource.getInputStream()); - entityRedactionService.processDocument(classifiedDoc, TEST_RULESET_ID, null, "dossierId"); + entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId"); assertThat(classifiedDoc.getEntities()).hasSize(1); // one page assertThat(classifiedDoc.getEntities().get(1).stream() .filter(entity -> entity.getMatchedRule() == 11) @@ -359,19 +359,19 @@ public class EntityRedactionServiceTest { .entries(toDictionaryEntry(Arrays.asList("Bissig R.", "Thanei P."))) .build(); - when(dictionaryClient.getVersion(TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); - when(dictionaryClient.getDictionaryForType(AUTHOR_CODE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); + when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); + when(dictionaryClient.getDictionaryForType(AUTHOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); DictionaryResponse addressResponse = DictionaryResponse.builder() .entries(toDictionaryEntry(Collections.singletonList("Novartis Crop Protection AG, Basel, Switzerland"))) .build(); - when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); + when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); DictionaryResponse sponsorResponse = DictionaryResponse.builder() .entries(Collections.emptyList()) .build(); - when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); + when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); Document classifiedDoc = pdfSegmentationService.parseDocument(pdfFileResource.getInputStream()); - entityRedactionService.processDocument(classifiedDoc, TEST_RULESET_ID, null, "dossierId"); + entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId"); assertThat(classifiedDoc.getEntities()).hasSize(2); // two pages assertThat(classifiedDoc.getEntities().get(1).stream().filter(entity -> entity.getMatchedRule() == 9).count()).isEqualTo(8); assertThat(classifiedDoc.getEntities().get(2).stream().filter(entity -> entity.getMatchedRule() == 9).count()).isEqualTo(5); // 2 names, 1 address, 2 Y @@ -382,15 +382,15 @@ public class EntityRedactionServiceTest { .entries(toDictionaryEntry(Arrays.asList("Tribolet, R.", "Muir, G.", "Kühne-Thu, H.", "Close, C."))) .build(); - when(dictionaryClient.getVersion(TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); - when(dictionaryClient.getDictionaryForType(AUTHOR_CODE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); + when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); + when(dictionaryClient.getDictionaryForType(AUTHOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); addressResponse = DictionaryResponse.builder() .entries(toDictionaryEntry(Collections.singletonList("Novartis Crop Protection AG, Basel, Switzerland"))) .build(); - when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); + when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); classifiedDoc = pdfSegmentationService.parseDocument(pdfFileResource.getInputStream()); - entityRedactionService.processDocument(classifiedDoc, TEST_RULESET_ID, null, "dossierId"); + entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId"); assertThat(classifiedDoc.getEntities()).hasSize(1); // one page assertThat(classifiedDoc.getEntities().get(1).stream().filter(entity -> entity.getMatchedRule() == 9).count()).isEqualTo(3); assertThat(classifiedDoc.getEntities().get(1).stream().filter(entity -> entity.getMatchedRule() == 8).count()).isEqualTo(9); @@ -407,19 +407,19 @@ public class EntityRedactionServiceTest { .entries(toDictionaryEntry(Collections.singletonList("Aldershof S."))) .build(); - when(dictionaryClient.getVersion(TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); - when(dictionaryClient.getDictionaryForType(AUTHOR_CODE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); + when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); + when(dictionaryClient.getDictionaryForType(AUTHOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); DictionaryResponse addressResponse = DictionaryResponse.builder() .entries(toDictionaryEntry(Collections.singletonList("Novartis Crop Protection AG, Basel, Switzerland"))) .build(); - when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); + when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); DictionaryResponse sponsorResponse = DictionaryResponse.builder() .entries(Collections.emptyList()) .build(); - when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); + when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); Document classifiedDoc = pdfSegmentationService.parseDocument(pdfFileResource.getInputStream()); - entityRedactionService.processDocument(classifiedDoc, TEST_RULESET_ID, null, "dossierId"); + entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId"); assertThat(classifiedDoc.getEntities()).hasSize(1); // one page assertThat(classifiedDoc.getEntities().get(1).stream().filter(entity -> entity.getMatchedRule() == 8).count()).isEqualTo(6); } @@ -446,31 +446,31 @@ public class EntityRedactionServiceTest { "\"Yes\"))\n" + " then\n" + " section.redactCell(\"Author(s)\", 9, \"name\", false, \"Redacted because row is a vertebrate study\", \"Reg (EC) No 1107/2009 Art. 63 (2g)\");\n" + - " section.redact(\"address\", 9, \"Redacted because row is a vertebrate study\", \"Reg (EC) No" + + " section.redact(\"address\", 9, \"Redacted because row is a vertebrate sgitudy\", \"Reg (EC) No" + " 1107/2009 Art. 63 (2g)\");\n" + " section.highlightCell(\"Vertebrate study Y/N\", 9, \"must_redact\");\n" + " end"; - when(rulesClient.getVersion(TEST_RULESET_ID)).thenReturn(RULES_VERSION.incrementAndGet()); - when(rulesClient.getRules(TEST_RULESET_ID)).thenReturn(new RulesResponse(tableRules)); + when(rulesClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(RULES_VERSION.incrementAndGet()); + when(rulesClient.getRules(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(new RulesResponse(tableRules)); TypeResponse typeResponse = TypeResponse.builder() .types(Arrays.asList( - TypeResult.builder().ruleSetId(TEST_RULESET_ID).type(AUTHOR_CODE).hexColor("#ffff00").build(), - TypeResult.builder().ruleSetId(TEST_RULESET_ID).type(ADDRESS_CODE).hexColor("#ff00ff").build(), - TypeResult.builder().ruleSetId(TEST_RULESET_ID).type(SPONSOR_CODE).hexColor("#00ffff").build())) + TypeResult.builder().ruleSetId(TEST_DOSSIER_TEMPLATE_ID).type(AUTHOR_CODE).hexColor("#ffff00").build(), + TypeResult.builder().ruleSetId(TEST_DOSSIER_TEMPLATE_ID).type(ADDRESS_CODE).hexColor("#ff00ff").build(), + TypeResult.builder().ruleSetId(TEST_DOSSIER_TEMPLATE_ID).type(SPONSOR_CODE).hexColor("#00ffff").build())) .build(); - when(dictionaryClient.getVersion(TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); - when(dictionaryClient.getAllTypes(TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(typeResponse); + when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); + when(dictionaryClient.getAllTypes(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(typeResponse); // Default empty return to prevent NPEs DictionaryResponse dictionaryResponse = DictionaryResponse.builder() .build(); - when(dictionaryClient.getDictionaryForType(AUTHOR_CODE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); + when(dictionaryClient.getDictionaryForType(AUTHOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); DictionaryResponse addressResponse = DictionaryResponse.builder() .build(); - when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); + when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); DictionaryResponse sponsorResponse = DictionaryResponse.builder() .build(); - when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_RULESET_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); + when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); Colors colors = new Colors(); colors.setDefaultColor("#acfc00"); @@ -478,7 +478,7 @@ public class EntityRedactionServiceTest { colors.setRequestAdd("#04b093"); colors.setRequestRemove("#04b093"); - when(dictionaryClient.getColors(TEST_RULESET_ID)).thenReturn(colors); + when(dictionaryClient.getColors(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(colors); } From ceaaed4fc721c9488582d5fce223a9ced928862a Mon Sep 17 00:00:00 2001 From: Timo Date: Mon, 31 May 2021 11:40:02 +0300 Subject: [PATCH 07/80] migrated ruleset and projectid to new naming --- .../redaction/v1/model/AnnotateRequest.java | 1 + .../v1/model/RedactionChangeLog.java | 4 +-- .../redaction/v1/model/RedactionLog.java | 8 +---- .../controller/RedactionController.java | 2 +- .../redaction/service/DictionaryService.java | 32 +++++++++---------- .../redaction/service/ReanalyzeService.java | 7 ++-- .../service/RedactionChangeLogService.java | 7 ++-- 7 files changed, 29 insertions(+), 32 deletions(-) diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnnotateRequest.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnnotateRequest.java index 30d0a62f..b80a7365 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnnotateRequest.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnnotateRequest.java @@ -12,5 +12,6 @@ import lombok.NoArgsConstructor; public class AnnotateRequest { private String dossierId; + private String dossierTemplateId; private String fileId; } diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionChangeLog.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionChangeLog.java index 10a41b05..74e385c0 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionChangeLog.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionChangeLog.java @@ -15,8 +15,8 @@ public class RedactionChangeLog { private List redactionLogEntry = new ArrayList<>(); private long dictionaryVersion = -1; + private long dossierDictionaryVersion = -1; private long rulesVersion = -1; - - private String dossierTemplateId; + private long legalBasisVersion = -1; } diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLog.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLog.java index 7c916084..71a8413e 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLog.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLog.java @@ -13,15 +13,9 @@ public class RedactionLog { private List redactionLogEntry; private List legalBasis; - private String dossierTemplateId; - private long dictionaryVersion = -1; - private long rulesVersion = -1; private long dossierDictionaryVersion = -1; + private long rulesVersion = -1; private long legalBasisVersion = -1; - - - - } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java index 12381286..a2e285e4 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java @@ -50,7 +50,7 @@ public class RedactionController implements RedactionResource { try (PDDocument pdDocument = PDDocument.load(storedObjectStream, MemoryUsageSetting.setupTempFileOnly())) { pdDocument.setAllSecurityToBeRemoved(true); - dictionaryService.updateDictionary(redactionLog.getDossierTemplateId(), annotateRequest.getDossierId()); + dictionaryService.updateDictionary(annotateRequest.getDossierTemplateId(), annotateRequest.getDossierId()); annotationService.annotate(pdDocument, redactionLog, sectionsGrid); try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java index bca42897..6de8a1ee 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java @@ -27,14 +27,14 @@ public class DictionaryService { private final DictionaryClient dictionaryClient; - private final Map dictionariesByRuleSets = new HashMap<>(); + private final Map dictionariesByDossierTemplate = new HashMap<>(); private final Map dictionariesByDossier = new HashMap<>(); public DictionaryVersion updateDictionary(String dossierTemplateId, String dossierId) { long dossierTemplateDictionaryVersion = dictionaryClient.getVersion(dossierTemplateId, GLOBAL_DOSSIER); - var dossierTemplateDictionary = dictionariesByRuleSets.get(dossierTemplateId); + var dossierTemplateDictionary = dictionariesByDossierTemplate.get(dossierTemplateId); if (dossierTemplateDictionary == null || dossierTemplateDictionaryVersion > dossierTemplateDictionary.getDictionaryVersion()) { updateDictionaryEntry(dossierTemplateId, dossierTemplateDictionaryVersion, GLOBAL_DOSSIER); } @@ -54,10 +54,10 @@ public class DictionaryService { DictionaryVersion version = updateDictionary(dossierTemplateId, dossierId); Set newValues = new HashSet<>(); - List dictionaryModels = dictionariesByRuleSets.get(dossierTemplateId).getDictionary(); + List dictionaryModels = dictionariesByDossierTemplate.get(dossierTemplateId).getDictionary(); dictionaryModels.forEach(dictionaryModel -> { dictionaryModel.getEntries().forEach(dictionaryEntry -> { - if (dictionaryEntry.getVersion() > fromVersion.getRulesetVersion()) { + if (dictionaryEntry.getVersion() > fromVersion.getDossierTemplateVersion()) { newValues.add(new DictionaryIncrementValue(dictionaryEntry.getValue(), dictionaryModel.isCaseInsensitive())); } }); @@ -106,7 +106,7 @@ public class DictionaryService { dictionaryRepresentation.setDictionary(dictionary); if(dossierId.equals(GLOBAL_DOSSIER)) { - dictionariesByRuleSets.put(dossierTemplateId, dictionaryRepresentation); + dictionariesByDossierTemplate.put(dossierTemplateId, dictionaryRepresentation); } else { dictionariesByDossier.put(dossierId, dictionaryRepresentation); } @@ -124,8 +124,8 @@ public class DictionaryService { if (dm.isRecommendation() && !dm.getLocalEntries().isEmpty()) { dictionaryClient.addEntries(dm.getType(), dossierTemplateId, new ArrayList<>(dm.getLocalEntries()), false, GLOBAL_DOSSIER); long externalVersion = dictionaryClient.getVersion(dossierTemplateId, GLOBAL_DOSSIER); - if (externalVersion == dictionary.getVersion().getRulesetVersion() + 1) { - dictionary.getVersion().setRulesetVersion(externalVersion); + if (externalVersion == dictionary.getVersion().getDossierTemplateVersion() + 1) { + dictionary.getVersion().setDossierTemplateVersion(externalVersion); } } }); @@ -153,7 +153,7 @@ public class DictionaryService { public boolean isCaseInsensitiveDictionary(String type, String dossierTemplateId) { - DictionaryModel dictionaryModel = dictionariesByRuleSets.get(dossierTemplateId).getLocalAccessMap().get(type); + DictionaryModel dictionaryModel = dictionariesByDossierTemplate.get(dossierTemplateId).getLocalAccessMap().get(type); if (dictionaryModel != null) { return dictionaryModel.isCaseInsensitive(); } @@ -163,17 +163,17 @@ public class DictionaryService { public float[] getColor(String type, String dossierTemplateId) { - DictionaryModel model = dictionariesByRuleSets.get(dossierTemplateId).getLocalAccessMap().get(type); + DictionaryModel model = dictionariesByDossierTemplate.get(dossierTemplateId).getLocalAccessMap().get(type); if (model != null) { return model.getColor(); } - return dictionariesByRuleSets.get(dossierTemplateId).getDefaultColor(); + return dictionariesByDossierTemplate.get(dossierTemplateId).getDefaultColor(); } public boolean isHint(String type, String dossierTemplateId) { - DictionaryModel model = dictionariesByRuleSets.get(dossierTemplateId).getLocalAccessMap().get(type); + DictionaryModel model = dictionariesByDossierTemplate.get(dossierTemplateId).getLocalAccessMap().get(type); if (model != null) { return model.isHint(); } @@ -183,7 +183,7 @@ public class DictionaryService { public boolean isRecommendation(String type, String dossierTemplateId) { - DictionaryModel model = dictionariesByRuleSets.get(dossierTemplateId).getLocalAccessMap().get(type); + DictionaryModel model = dictionariesByDossierTemplate.get(dossierTemplateId).getLocalAccessMap().get(type); if (model != null) { return model.isRecommendation(); } @@ -195,7 +195,7 @@ public class DictionaryService { List copy = new ArrayList<>(); - var dossierTemplateRepresentation = dictionariesByRuleSets.get(dossierTemplateId); + var dossierTemplateRepresentation = dictionariesByDossierTemplate.get(dossierTemplateId); dossierTemplateRepresentation.getDictionary().forEach(dm -> { copy.add(SerializationUtils.clone(dm)); }); @@ -216,19 +216,19 @@ public class DictionaryService { public float[] getRequestRemoveColor(String dossierTemplateId) { - return dictionariesByRuleSets.get(dossierTemplateId).getRequestAddColor(); + return dictionariesByDossierTemplate.get(dossierTemplateId).getRequestAddColor(); } public float[] getNotRedactedColor(String dossierTemplateId) { - return dictionariesByRuleSets.get(dossierTemplateId).getNotRedactedColor(); + return dictionariesByDossierTemplate.get(dossierTemplateId).getNotRedactedColor(); } public float[] getRequestAddColor(String dossierTemplateId) { - return dictionariesByRuleSets.get(dossierTemplateId).getRequestAddColor(); + return dictionariesByDossierTemplate.get(dossierTemplateId).getRequestAddColor(); } } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java index 9c0d75b8..3872f49f 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java @@ -67,10 +67,9 @@ public class ReanalyzeService { var legalBasis = legalBasisClient.getLegalBasisMapping(analyzeRequest.getDossierTemplateId()); var redactionLog = new RedactionLog(classifiedDoc.getRedactionLogEntities(),legalBasis, - analyzeRequest.getDossierTemplateId(), - classifiedDoc.getDictionaryVersion().getRulesetVersion(), - classifiedDoc.getRulesVersion(), + classifiedDoc.getDictionaryVersion().getDossierTemplateVersion(), classifiedDoc.getDictionaryVersion().getDossierVersion(), + classifiedDoc.getRulesVersion(), legalBasisClient.getVersion(analyzeRequest.getDossierTemplateId())); log.info("Analyzed with rules {} and dictionary {} for dossierTemplate: {}", classifiedDoc.getRulesVersion(), classifiedDoc @@ -253,7 +252,7 @@ public class ReanalyzeService { RedactionLog redactionLog, Text text, DictionaryIncrement dictionaryIncrement) { - redactionLog.setDictionaryVersion(dictionaryIncrement.getDictionaryVersion().getRulesetVersion()); + redactionLog.setDictionaryVersion(dictionaryIncrement.getDictionaryVersion().getDossierTemplateVersion()); redactionLog.setDossierDictionaryVersion(dictionaryIncrement.getDictionaryVersion().getDossierVersion()); var changeLog = redactionChangeLogService.createAndStoreChangeLog(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), redactionLog); diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionChangeLogService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionChangeLogService.java index 1d586ad1..53fc805e 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionChangeLogService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionChangeLogService.java @@ -57,8 +57,11 @@ public class RedactionChangeLogService { .map(entry -> convert(entry, ChangeType.REMOVED)) .collect(Collectors.toList())); - return new RedactionChangeLog(changeLogEntries, currentRedactionLog.getDictionaryVersion(), currentRedactionLog.getRulesVersion(), currentRedactionLog - .getDossierTemplateId()); + return new RedactionChangeLog(changeLogEntries, + currentRedactionLog.getDictionaryVersion(), + currentRedactionLog.getDossierDictionaryVersion(), + currentRedactionLog.getRulesVersion(), + currentRedactionLog.getLegalBasisVersion()); } From f60b0f25e579da2270e6925cca78b51ccdc71b38 Mon Sep 17 00:00:00 2001 From: Timo Date: Mon, 31 May 2021 15:04:15 +0300 Subject: [PATCH 08/80] rename project and ruleSet - finalization with dependencies --- redaction-service-v1/redaction-service-api-v1/pom.xml | 2 +- redaction-service-v1/redaction-service-server-v1/pom.xml | 2 +- .../v1/server/redaction/service/DictionaryService.java | 4 ++-- .../redaction/v1/server/RedactionIntegrationTest.java | 4 ++-- .../redaction/service/EntityRedactionServiceTest.java | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/redaction-service-v1/redaction-service-api-v1/pom.xml b/redaction-service-v1/redaction-service-api-v1/pom.xml index cb0d850a..e4b7cd36 100644 --- a/redaction-service-v1/redaction-service-api-v1/pom.xml +++ b/redaction-service-v1/redaction-service-api-v1/pom.xml @@ -20,7 +20,7 @@ com.iqser.red.service configuration-service-api-v1 - 2.7.0 + 2.11.0 com.iqser.red.service diff --git a/redaction-service-v1/redaction-service-server-v1/pom.xml b/redaction-service-v1/redaction-service-server-v1/pom.xml index 91b484a2..2bf40817 100644 --- a/redaction-service-v1/redaction-service-server-v1/pom.xml +++ b/redaction-service-v1/redaction-service-server-v1/pom.xml @@ -24,7 +24,7 @@ com.iqser.red.service file-management-service-api-v1 - 2.7.4 + 2.25.0 com.iqser.red.service diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java index 6de8a1ee..0518eb3f 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java @@ -89,7 +89,7 @@ public class DictionaryService { List dictionary = typeResponse.getTypes() .stream() .map(t -> new DictionaryModel(t.getType(), t.getRank(), convertColor(t.getHexColor()), t.isCaseInsensitive(), t - .isHint(), t.isRecommendation(), convertEntries(t, dossierId), new HashSet<>(),dossierId.equals(GLOBAL_DOSSIER) ? false : true)) + .isHint(), t.isRecommendation(), convertEntries(t, dossierId), new HashSet<>(), !dossierId.equals(GLOBAL_DOSSIER))) .sorted(Comparator.comparingInt(DictionaryModel::getRank).reversed()) .collect(Collectors.toList()); @@ -134,7 +134,7 @@ public class DictionaryService { private Set convertEntries(TypeResult t, String dossierId) { - Set entries = new HashSet<>(dictionaryClient.getDictionaryForType(t.getType(), t.getRuleSetId(), dossierId) + Set entries = new HashSet<>(dictionaryClient.getDictionaryForType(t.getType(), t.getDossierTemplateId(), dossierId) .getEntries()); if (t.isCaseInsensitive()) { diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java index c7f8a107..cb0f03da 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java @@ -187,7 +187,7 @@ public class RedactionIntegrationTest { when(dictionaryClient.getAllTypes(TEST_DOSSIER_TEMPLATE_ID, TEST_DOSSIER_ID)).thenReturn(TypeResponse.builder() .types(List.of(TypeResult.builder() .type(DOSSIER_REDACTIONS) - .ruleSetId(TEST_DOSSIER_TEMPLATE_ID) + .dossierTemplateId(TEST_DOSSIER_TEMPLATE_ID) .hexColor( "#ffe187") .isHint(hintTypeMap.get(DOSSIER_REDACTIONS)) .isCaseInsensitive(caseInSensitiveMap.get(DOSSIER_REDACTIONS)) @@ -461,7 +461,7 @@ public class RedactionIntegrationTest { .stream() .map(typeColor -> TypeResult.builder() .type(typeColor.getKey()) - .ruleSetId(TEST_DOSSIER_TEMPLATE_ID) + .dossierTemplateId(TEST_DOSSIER_TEMPLATE_ID) .hexColor(typeColor.getValue()) .isHint(hintTypeMap.get(typeColor.getKey())) .isCaseInsensitive(caseInSensitiveMap.get(typeColor.getKey())) diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionServiceTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionServiceTest.java index e4afe9d9..01379d9b 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionServiceTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionServiceTest.java @@ -454,9 +454,9 @@ public class EntityRedactionServiceTest { when(rulesClient.getRules(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(new RulesResponse(tableRules)); TypeResponse typeResponse = TypeResponse.builder() .types(Arrays.asList( - TypeResult.builder().ruleSetId(TEST_DOSSIER_TEMPLATE_ID).type(AUTHOR_CODE).hexColor("#ffff00").build(), - TypeResult.builder().ruleSetId(TEST_DOSSIER_TEMPLATE_ID).type(ADDRESS_CODE).hexColor("#ff00ff").build(), - TypeResult.builder().ruleSetId(TEST_DOSSIER_TEMPLATE_ID).type(SPONSOR_CODE).hexColor("#00ffff").build())) + TypeResult.builder().dossierTemplateId(TEST_DOSSIER_TEMPLATE_ID).type(AUTHOR_CODE).hexColor("#ffff00").build(), + TypeResult.builder().dossierTemplateId(TEST_DOSSIER_TEMPLATE_ID).type(ADDRESS_CODE).hexColor("#ff00ff").build(), + TypeResult.builder().dossierTemplateId(TEST_DOSSIER_TEMPLATE_ID).type(SPONSOR_CODE).hexColor("#00ffff").build())) .build(); when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); when(dictionaryClient.getAllTypes(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(typeResponse); From 53ce6cb47cd5806e1880dcfeaec173e2efed13ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Tue, 1 Jun 2021 10:47:55 +0200 Subject: [PATCH 09/80] RED-1535: Improved document parsing, added orientation to textblock --- .../classification/model/Orientation.java | 6 ++ .../classification/model/TextBlock.java | 1 + .../service/BlockificationService.java | 93 +++++++++++++++++-- .../model/AbstractTextContainer.java | 4 + .../service/PdfVisualisationService.java | 2 +- .../v1/server/RedactionIntegrationTest.java | 6 +- .../resources/dictionaries/CBI_address.txt | 1 + 7 files changed, 101 insertions(+), 12 deletions(-) create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/classification/model/Orientation.java diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/classification/model/Orientation.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/classification/model/Orientation.java new file mode 100644 index 00000000..e0f4e1a9 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/classification/model/Orientation.java @@ -0,0 +1,6 @@ +package com.iqser.red.service.redaction.v1.server.classification.model; + +public enum Orientation { + + NONE, LEFT, RIGHT +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/classification/model/TextBlock.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/classification/model/TextBlock.java index 63cfc11c..396006ae 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/classification/model/TextBlock.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/classification/model/TextBlock.java @@ -32,6 +32,7 @@ public class TextBlock extends AbstractTextContainer { private String classification; + public TextBlock(float minX, float maxX, float minY, float maxY, List sequences, int rotation) { this.minX = minX; this.maxX = maxX; diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/classification/service/BlockificationService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/classification/service/BlockificationService.java index 4badfec4..d394bbe6 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/classification/service/BlockificationService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/classification/service/BlockificationService.java @@ -1,6 +1,7 @@ package com.iqser.red.service.redaction.v1.server.classification.service; import com.iqser.red.service.redaction.v1.server.classification.model.FloatFrequencyCounter; +import com.iqser.red.service.redaction.v1.server.classification.model.Orientation; import com.iqser.red.service.redaction.v1.server.classification.model.Page; import com.iqser.red.service.redaction.v1.server.classification.model.StringFrequencyCounter; import com.iqser.red.service.redaction.v1.server.classification.model.TextBlock; @@ -11,16 +12,21 @@ import com.iqser.red.service.redaction.v1.server.tableextraction.model.Cell; import com.iqser.red.service.redaction.v1.server.tableextraction.model.Rectangle; import com.iqser.red.service.redaction.v1.server.tableextraction.model.Ruling; import com.iqser.red.service.redaction.v1.server.tableextraction.model.Table; + import org.springframework.stereotype.Service; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; @Service @SuppressWarnings("all") public class BlockificationService { - public Page blockify(List textPositions, List horizontalRulingLines, List verticalRulingLines) { + static final float THRESHOLD = 1f; + + public Page blockify(List textPositions, List horizontalRulingLines, + List verticalRulingLines) { List chunkWords = new ArrayList<>(); List chunkBlockList1 = new ArrayList<>(); @@ -28,21 +34,37 @@ public class BlockificationService { float minX = 1000, maxX = 0, minY = 1000, maxY = 0; TextPositionSequence prev = null; + boolean wasSplitted = false; + Float splitX1 = null; for (TextPositionSequence word : textPositions) { boolean lineSeparation = minY - word.getY2() > word.getHeight() * 1.25; boolean startFromTop = word.getY1() > maxY + word.getHeight(); + boolean splitByX = prev != null && maxX + 50 < word.getX1() && prev.getY1() == word.getY1(); + boolean newLineAfterSplit = prev != null && word.getY1() != prev.getY1() && wasSplitted && splitX1 != word.getX1(); + boolean splittedByRuling = word.getRotation() == 0 && isSplittedByRuling(maxX, minY, word.getX1(), word.getY1(), verticalRulingLines) || word + .getRotation() == 0 && isSplittedByRuling(minX, minY, word.getX1(), word.getY2(), horizontalRulingLines) || word + .getRotation() == 90 && isSplittedByRuling(maxX, minY, word.getX1(), word.getY1(), horizontalRulingLines) || word + .getRotation() == 90 && isSplittedByRuling(minX, minY, word.getX1(), word.getY2(), verticalRulingLines); - if (prev != null && (lineSeparation || startFromTop || word.getRotation() == 0 && isSplittedByRuling(maxX, minY, word - .getX1(), word.getY1(), verticalRulingLines) || word.getRotation() == 0 && isSplittedByRuling(minX, minY, word - .getX1(), word.getY2(), horizontalRulingLines) || word.getRotation() == 90 && isSplittedByRuling(maxX, minY, word - .getX1(), word.getY1(), horizontalRulingLines) || word.getRotation() == 90 && isSplittedByRuling(minX, minY, word - .getX1(), word.getY2(), verticalRulingLines))) { + if (prev != null && (lineSeparation || startFromTop || splitByX || newLineAfterSplit || splittedByRuling)) { TextBlock cb1 = buildTextBlock(chunkWords); chunkBlockList1.add(cb1); chunkWords = new ArrayList<>(); + if (splitByX && !splittedByRuling) { + wasSplitted = true; + cb1.setOrientation(Orientation.LEFT); + splitX1 = word.getX1(); + } + + if (newLineAfterSplit && !splittedByRuling) { + wasSplitted = false; + cb1.setOrientation(Orientation.RIGHT); + splitX1 = null; + } + minX = 1000; maxX = 0; minY = 1000; @@ -72,9 +94,62 @@ public class BlockificationService { chunkBlockList1.add(cb1); } + Iterator itty = chunkBlockList1.iterator(); + + TextBlock previousLeft = null; + TextBlock previousRight = null; + while (itty.hasNext()) { + TextBlock block = (TextBlock) itty.next(); + + if(previousLeft != null && block.getOrientation().equals(Orientation.LEFT)){ + if (previousLeft.getMinY() > block.getMinY() && block.getMaxY() + block.getMostPopularWordHeight() > previousLeft.getMinY()){ + previousLeft.add(block); + itty.remove(); + continue; + } + } + + if(previousRight != null && block.getOrientation().equals(Orientation.RIGHT)){ + if (previousRight.getMinY() > block.getMinY() && block.getMaxY() + block.getMostPopularWordHeight() > previousRight.getMinY()){ + previousRight.add(block); + itty.remove(); + continue; + } + } + + if (block.getOrientation().equals(Orientation.LEFT)) { + previousLeft = block; + } else if (block.getOrientation().equals(Orientation.RIGHT)) { + previousRight = block; + } + } + + + itty = chunkBlockList1.iterator(); + TextBlock previous = null; + while (itty.hasNext()) { + TextBlock block = (TextBlock) itty.next(); + + if(previous != null && previous.getOrientation().equals(Orientation.LEFT) && block.getOrientation().equals(Orientation.LEFT) && equalsWithThreshold(block.getMaxY(), previous + .getMaxY())|| + previous != null && previous.getOrientation().equals(Orientation.LEFT) && block.getOrientation().equals(Orientation.RIGHT) && equalsWithThreshold(block.getMaxY(), previous + .getMaxY())){ + previous.add(block); + itty.remove(); + continue; + } + + previous = block; + } + + return new Page(chunkBlockList1); } + private boolean equalsWithThreshold(float f1, float f2){ + return Math.abs(f1 - f2) < THRESHOLD; + } + private TextBlock buildTextBlock(List wordBlockList) { @@ -117,7 +192,8 @@ public class BlockificationService { } - private boolean isSplittedByRuling(float previousX2, float previousY1, float currentX1, float currentY1, List rulingLines) { + private boolean isSplittedByRuling(float previousX2, float previousY1, float currentX1, float currentY1, + List rulingLines) { for (Ruling ruling : rulingLines) { if (ruling.intersectsLine(previousX2, previousY1, currentX1, currentY1)) { @@ -128,7 +204,8 @@ public class BlockificationService { } - public Rectangle calculateBodyTextFrame(List pages, FloatFrequencyCounter documentFontSizeCounter, boolean landscape) { + public Rectangle calculateBodyTextFrame(List pages, FloatFrequencyCounter documentFontSizeCounter, + boolean landscape) { float minX = 10000; float maxX = -100; diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/tableextraction/model/AbstractTextContainer.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/tableextraction/model/AbstractTextContainer.java index b050e27b..cb7cfde5 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/tableextraction/model/AbstractTextContainer.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/tableextraction/model/AbstractTextContainer.java @@ -2,6 +2,8 @@ package com.iqser.red.service.redaction.v1.server.tableextraction.model; import com.fasterxml.jackson.annotation.JsonIgnore; import com.iqser.red.service.redaction.v1.model.Rectangle; +import com.iqser.red.service.redaction.v1.server.classification.model.Orientation; + import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -18,6 +20,8 @@ public abstract class AbstractTextContainer { protected String classification; protected int page; + private Orientation orientation = Orientation.NONE; + public abstract String getText(); public boolean contains(AbstractTextContainer other) { diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/visualization/service/PdfVisualisationService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/visualization/service/PdfVisualisationService.java index 06ccb399..390810a4 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/visualization/service/PdfVisualisationService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/visualization/service/PdfVisualisationService.java @@ -102,7 +102,7 @@ public class PdfVisualisationService { contentStream.newLineAtOffset(textBlock.getMinX(), textBlock.getMaxY()); - contentStream.showText(textBlock.getClassification()); + contentStream.showText(textBlock.getClassification() + textBlock.getOrientation()); contentStream.endText(); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java index cb0f03da..a878400d 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java @@ -680,8 +680,8 @@ public class RedactionIntegrationTest { dictionary.get(AUTHOR).add("physical"); reanlysisVersions.put("physical", 2L); - dictionary.get(VERTEBRATE).add("s-metolachlor"); - reanlysisVersions.put("s-metolachlor", 3L); +// dictionary.get(VERTEBRATE).add("s-metolachlor"); +// reanlysisVersions.put("s-metolachlor", 3L); when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(3L); @@ -805,7 +805,7 @@ public class RedactionIntegrationTest { public void classificationTest() throws IOException { System.out.println("classificationTest"); - ClassPathResource pdfFileResource = new ClassPathResource("files/Trinexapac/93 Trinexapac-ethyl_RAR_03_Volume_3CA_B-1_2017-03-31.pdf"); + ClassPathResource pdfFileResource = new ClassPathResource("files/new/Single Study - Oral (Gavage) Mouse.pdf"); AnalyzeRequest request = prepareStorage(pdfFileResource.getInputStream()); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/resources/dictionaries/CBI_address.txt b/redaction-service-v1/redaction-service-server-v1/src/test/resources/dictionaries/CBI_address.txt index c04fedbe..8de2feb7 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/resources/dictionaries/CBI_address.txt +++ b/redaction-service-v1/redaction-service-server-v1/src/test/resources/dictionaries/CBI_address.txt @@ -1652,3 +1652,4 @@ Zoecon Corp. Zoecon Corp., Palo Alto, USA Zyma SA Zyma SA, Nyon, Switzerland +Mambo-Tox Ltd. Biomedical Sciences Building Bassett Crescent East Southampton SO16 7PX UK From ab8a710318802d4a836c006638dddb987f5d29c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Wed, 2 Jun 2021 09:41:22 +0200 Subject: [PATCH 10/80] Fixed text parsing orientation problem --- .../classification/service/BlockificationService.java | 11 ++++++++++- .../src/test/resources/dictionaries/CBI_address.txt | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/classification/service/BlockificationService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/classification/service/BlockificationService.java index d394bbe6..abe13409 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/classification/service/BlockificationService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/classification/service/BlockificationService.java @@ -49,6 +49,11 @@ public class BlockificationService { if (prev != null && (lineSeparation || startFromTop || splitByX || newLineAfterSplit || splittedByRuling)) { + Orientation prevOrientation = null; + if(!chunkBlockList1.isEmpty()) { + prevOrientation = chunkBlockList1.get(chunkBlockList1.size() - 1).getOrientation(); + } + TextBlock cb1 = buildTextBlock(chunkWords); chunkBlockList1.add(cb1); chunkWords = new ArrayList<>(); @@ -57,12 +62,16 @@ public class BlockificationService { wasSplitted = true; cb1.setOrientation(Orientation.LEFT); splitX1 = word.getX1(); - } + } else if (newLineAfterSplit && !splittedByRuling) { wasSplitted = false; cb1.setOrientation(Orientation.RIGHT); splitX1 = null; + } else + + if(prevOrientation != null && prevOrientation.equals(Orientation.RIGHT) && (lineSeparation || !startFromTop || !splitByX || !newLineAfterSplit || !splittedByRuling)){ + cb1.setOrientation(Orientation.LEFT); } minX = 1000; diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/resources/dictionaries/CBI_address.txt b/redaction-service-v1/redaction-service-server-v1/src/test/resources/dictionaries/CBI_address.txt index 8de2feb7..b6ad4398 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/resources/dictionaries/CBI_address.txt +++ b/redaction-service-v1/redaction-service-server-v1/src/test/resources/dictionaries/CBI_address.txt @@ -1653,3 +1653,4 @@ Zoecon Corp., Palo Alto, USA Zyma SA Zyma SA, Nyon, Switzerland Mambo-Tox Ltd. Biomedical Sciences Building Bassett Crescent East Southampton SO16 7PX UK +Syngenta Environmental Sciences Jealott’s Hill International Research Centre Bracknell, Berkshire RG42 6EY UK From 418faac923e79c17f2d2b88a02920ab459fc36bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Wed, 2 Jun 2021 12:53:30 +0200 Subject: [PATCH 11/80] Ignore failing Ruling expansion --- .../redaction/v1/server/tableextraction/model/Ruling.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/tableextraction/model/Ruling.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/tableextraction/model/Ruling.java index e90c52b2..ee64ceac 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/tableextraction/model/Ruling.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/tableextraction/model/Ruling.java @@ -246,8 +246,12 @@ public class Ruling extends Line2D.Float { public Ruling expand(float amount) { Ruling r = (Ruling) this.clone(); - r.setStart(this.getStart() - amount); - r.setEnd(this.getEnd() + amount); + try { + r.setStart(this.getStart() - amount); + r.setEnd(this.getEnd() + amount); + } catch (UnsupportedOperationException e){ + log.warn("Could not expand ruling!"); + } return r; } From 6bd8c2cf0f61eba94717d5478cdefd87f1cb4dbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Wed, 2 Jun 2021 13:47:48 +0200 Subject: [PATCH 12/80] Fixed endless processing --- .../redaction/v1/server/queue/MessagingConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/queue/MessagingConfiguration.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/queue/MessagingConfiguration.java index 965163c0..6a5e25c5 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/queue/MessagingConfiguration.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/queue/MessagingConfiguration.java @@ -21,7 +21,7 @@ public class MessagingConfiguration { return QueueBuilder.durable(REDACTION_QUEUE) .withArgument("x-dead-letter-exchange", "") - .withArgument("x-dead-letter-routing-key", REDACTION_QUEUE) + .withArgument("x-dead-letter-routing-key", REDACTION_DQL) .maxPriority(2) .build(); } From b8dc0e448d9101db8099aaf638ac929a16c87c3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Tue, 15 Jun 2021 12:29:46 +0200 Subject: [PATCH 13/80] RED-1472: Fixed image merging in rotated pages --- .../segmentation/ImageMergeService.java | 165 ++++++++++++++++ .../segmentation/PdfSegmentationService.java | 179 +++--------------- 2 files changed, 187 insertions(+), 157 deletions(-) create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/segmentation/ImageMergeService.java diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/segmentation/ImageMergeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/segmentation/ImageMergeService.java new file mode 100644 index 00000000..73a94909 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/segmentation/ImageMergeService.java @@ -0,0 +1,165 @@ +package com.iqser.red.service.redaction.v1.server.segmentation; + +import java.awt.Graphics; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.stereotype.Service; + +import com.iqser.red.service.redaction.v1.server.redaction.model.PdfImage; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ImageMergeService { + + + public List mergeImages(List images, int rotation){ + + List mergedList = processImages(images, rotation); + + List imagesInImage = new ArrayList<>(); + for(PdfImage image: mergedList){ + for (PdfImage inner: mergedList){ + if(image != inner && image.getPosition().contains(inner.getPosition().getX(), inner.getPosition().getY(), inner.getPosition().getWidth(), inner.getPosition().getHeight())){ + imagesInImage.add(inner); + } + } + } + mergedList.removeAll(imagesInImage); + + return mergedList; + } + + + //merge images, if they are separated during pdf import, return new list of Pdfimages + private List processImages(List imageList, int rotation) { + if (imageList.size() > 1) { + List mergedList = new ArrayList<>(); + int countElementsInList = 0; + boolean beginImage = true; + + // a List of Boolean, true = candidate for merging, false = no merging + List candidatesList = getCandidatesList(imageList, rotation); + + // loop through list, if there are candidates for merging (true), merge images and add it to mergedList + for (int i = 0; i < candidatesList.size(); i++) { + if (candidatesList.get(i)) { + if (beginImage) { + //begin of image, merge two parts of imageList + PdfImage mergedImage = mergeTwoImages(imageList.get(i), imageList.get(i + 1), rotation); + // image merge successful + if (mergedImage != null) { + mergedList.add(mergedImage); + countElementsInList++; + } + } else { + //middle of an image, merge current piece auf mergedList with image of imageList + PdfImage mergedImage = mergeTwoImages(mergedList.get(countElementsInList - 1), imageList.get(i + 1), rotation); + // image merge successful + if (mergedImage != null) { + mergedList.set(countElementsInList - 1, mergedImage); + } + } + beginImage = false; + } else { + // if the last candidate is false, then both images i and i+1 must be added + if (i == candidatesList.size() - 1) { + if (countElementsInList > 0 && mergedList.get(countElementsInList - 1) == imageList.get(i)) { + mergedList.add(imageList.get(i + 1)); + } else { + mergedList.add(imageList.get(i)); + mergedList.add(imageList.get(i + 1)); + } + } else { + //first image is not splitted, add i to resultlist + if (beginImage) { + mergedList.add(imageList.get(i)); + countElementsInList++; + } else { + // i is the end of an image, add begin of new image + mergedList.add(imageList.get(i + 1)); + countElementsInList++; + beginImage = false; + } + } + } + } + return mergedList; + } else { + return imageList; + } + } + + private PdfImage mergeTwoImages(PdfImage image1, PdfImage image2, int rotation) { + + // diese Angaben von getPosition scheinen nicht richtig zu sein, damit werden teile des Bildes abgeschnitten + double width = image1.getPosition().getWidth(); + double width2 = image2.getPosition().getWidth(); + double height1 = image1.getPosition().getHeight(); + double height2 = image2.getPosition().getHeight(); + // mit den Werten, die unter Image gespeichert sind, funktioniert es + double img1height = image1.getImage().getHeight(); + double img1width = image1.getImage().getWidth(); + double img2height = image2.getImage().getHeight(); + + BufferedImage mergedImage = new BufferedImage((int) img1width, (int) (img1height + img2height), BufferedImage.TYPE_INT_RGB); + Graphics mergedImageGraphics = mergedImage.getGraphics(); + try { + mergedImageGraphics.drawImage(image1.getImage(), 0, 0, null); + mergedImageGraphics.drawImage(image2.getImage(), 0, (int) (img1height), null); + + // set Image, Position and type for merged Image + //set position for merged image with values of image1 and the height of both + Rectangle2D pos = new Rectangle2D.Float(); + pos.setRect(image1.getPosition().getX(), image2.getPosition().getY(), rotation == 90 ? width + width2: width, rotation == 90 ? height1 : height1 + height2); + PdfImage newPdfImage = new PdfImage(mergedImage, pos, image1.getPage()); + // Graphics need to be disposed + + image1.getImage().flush(); + image2.getImage().flush(); + + mergedImage.flush(); + mergedImageGraphics.dispose(); + + return newPdfImage; + } catch (Exception e) { + // failed to merge image + log.error("Failed to merge image", e); + return null; + } + + + } + + //make a list of true and false, if the image is a candidate for merging + private List getCandidatesList(List imageList, int rotation) { + List candidatesList = new ArrayList<>(); + for (int i = 0; i < imageList.size(); i++) { + if (i >= 1) { + candidatesList.add(isCandidateForMerging(imageList.get(i - 1), imageList.get(i), rotation)); + } + } + return candidatesList; + } + + // evaluate if two images are candidates for merging, depending on their coordinates, width and height + private boolean isCandidateForMerging(PdfImage image1, PdfImage image2, int rotation) { + double x1 = rotation == 90 ? image1.getPosition().getY() : image1.getPosition().getX(); + double y1 = rotation == 90 ? image1.getPosition().getX() : image1.getPosition().getY(); + double width1 = rotation == 90 ? image1.getPosition().getHeight() : image1.getPosition().getWidth(); + double x2 = rotation == 90 ? image2.getPosition().getY() : image2.getPosition().getX(); + double y2 = rotation == 90 ? image2.getPosition().getX() : image2.getPosition().getY(); + double width2 = rotation == 90 ? image2.getPosition().getHeight() : image2.getPosition().getWidth(); + double height2 = rotation == 90 ? image2.getPosition().getWidth() : image2.getPosition().getHeight(); + //if the x-coordinates and widths of images are equal and the height is equal to difference between y-coordinates, + // then it is the same picture and has to be merged -> return true + return x1 == x2 && width1 == width2 && Math.ceil(height2) == Math.ceil(rotation == 90 ? y2 - y1 : y1 - y2) && width2 > (height2 / 6); + } + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/segmentation/PdfSegmentationService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/segmentation/PdfSegmentationService.java index a33a009d..22643b0b 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/segmentation/PdfSegmentationService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/segmentation/PdfSegmentationService.java @@ -1,6 +1,19 @@ package com.iqser.red.service.redaction.v1.server.segmentation; -import com.iqser.red.service.redaction.v1.model.Rectangle; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.io.IOUtils; +import org.apache.pdfbox.io.MemoryUsageSetting; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.springframework.stereotype.Service; + import com.iqser.red.service.redaction.v1.server.classification.model.Document; import com.iqser.red.service.redaction.v1.server.classification.model.Page; import com.iqser.red.service.redaction.v1.server.classification.model.TextBlock; @@ -15,24 +28,9 @@ import com.iqser.red.service.redaction.v1.server.tableextraction.model.AbstractT import com.iqser.red.service.redaction.v1.server.tableextraction.model.CleanRulings; import com.iqser.red.service.redaction.v1.server.tableextraction.service.RulingCleaningService; import com.iqser.red.service.redaction.v1.server.tableextraction.service.TableExtractionService; + import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.io.IOUtils; -import org.apache.pdfbox.io.MemoryUsageSetting; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDPage; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.springframework.stereotype.Service; - -import java.awt.Graphics; -import java.awt.geom.Rectangle2D; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; @Slf4j @Service @@ -47,13 +45,17 @@ public class PdfSegmentationService { private final ClassificationService classificationService; private final SectionsBuilderService sectionsBuilderService; private final ImageClassificationService imageClassificationService; + private final ImageMergeService imageMergeService; public Document parseDocument(InputStream documentInputStream) throws IOException { + return parseDocument(documentInputStream, false); } + public Document parseDocument(InputStream documentInputStream, boolean ignoreImages) throws IOException { + PDDocument pdDocument = null; try { //create tempFile @@ -64,7 +66,6 @@ public class PdfSegmentationService { Document document = new Document(); List pages = new ArrayList<>(); - pdDocument = reinitializePDDocument(tempFile, null); long pageCount = pdDocument.getNumberOfPages(); @@ -101,32 +102,19 @@ public class PdfSegmentationService { page.setRotation(rotation); page.setLandscape(isLandscape || isRotated); page.setPageNumber(pageNumber); - List mergedList = processImages(stripper.getImages()); - - List imagesInImage = new ArrayList<>(); - for(PdfImage image: mergedList){ - for (PdfImage inner: mergedList){ - if(image != inner && image.getPosition().contains(inner.getPosition().getX(), inner.getPosition().getY(), inner.getPosition().getWidth(), inner.getPosition().getHeight())){ - imagesInImage.add(inner); - } - } - } - mergedList.removeAll(imagesInImage); + List mergedList = imageMergeService.mergeImages(stripper.getImages(), rotation); page.setImages(mergedList); tableExtractionService.extractTables(cleanRulings, page); buildPageStatistics(page); increaseDocumentStatistics(page, document); - if (!ignoreImages) { imageClassificationService.classifyImages(page); } pages.add(page); - - } document.setPages(pages); @@ -149,7 +137,9 @@ public class PdfSegmentationService { } } + private PDDocument reinitializePDDocument(File tempFile, PDDocument pdDocument) throws IOException { + if (pdDocument != null) { pdDocument.close(); } @@ -164,130 +154,6 @@ public class PdfSegmentationService { return newPDDocument; } - //merge images, if they are separated during pdf import, return new list of Pdfimages - private List processImages(List imageList) { - if (imageList.size() > 1) { - List mergedList = new ArrayList<>(); - int countElementsInList = 0; - boolean beginImage = true; - - // a List of Boolean, true = candidate for merging, false = no merging - List candidatesList = getCandidatesList(imageList); - - // loop through list, if there are candidates for merging (true), merge images and add it to mergedList - for (int i = 0; i < candidatesList.size(); i++) { - if (candidatesList.get(i)) { - if (beginImage) { - //begin of image, merge two parts of imageList - PdfImage mergedImage = mergeTwoImages(imageList.get(i), imageList.get(i + 1)); - // image merge successful - if (mergedImage != null) { - mergedList.add(mergedImage); - countElementsInList++; - } - } else { - //middle of an image, merge current piece auf mergedList with image of imageList - PdfImage mergedImage = mergeTwoImages(mergedList.get(countElementsInList - 1), imageList.get(i + 1)); - // image merge successful - if (mergedImage != null) { - mergedList.set(countElementsInList - 1, mergedImage); - } - } - beginImage = false; - } else { - // if the last candidate is false, then both images i and i+1 must be added - if (i == candidatesList.size() - 1) { - if (countElementsInList > 0 && mergedList.get(countElementsInList - 1) == imageList.get(i)) { - mergedList.add(imageList.get(i + 1)); - } else { - mergedList.add(imageList.get(i)); - mergedList.add(imageList.get(i + 1)); - } - } else { - //first image is not splitted, add i to resultlist - if (beginImage) { - mergedList.add(imageList.get(i)); - countElementsInList++; - } else { - // i is the end of an image, add begin of new image - mergedList.add(imageList.get(i + 1)); - countElementsInList++; - beginImage = false; - } - } - } - } - return mergedList; - } else { - return imageList; - } - } - - private PdfImage mergeTwoImages(PdfImage image1, PdfImage image2) { - - // diese Angaben von getPosition scheinen nicht richtig zu sein, damit werden teile des Bildes abgeschnitten - double width = image1.getPosition().getWidth(); - double height1 = image1.getPosition().getHeight(); - double height2 = image2.getPosition().getHeight(); - // mit den Werten, die unter Image gespeichert sind, funktioniert es - double img1height = image1.getImage().getHeight(); - double img1width = image1.getImage().getWidth(); - double img2height = image2.getImage().getHeight(); - - BufferedImage mergedImage = new BufferedImage((int) img1width, (int) (img1height + img2height), BufferedImage.TYPE_INT_RGB); - Graphics mergedImageGraphics = mergedImage.getGraphics(); - try { - mergedImageGraphics.drawImage(image1.getImage(), 0, 0, null); - mergedImageGraphics.drawImage(image2.getImage(), 0, (int) (img1height), null); - - // set Image, Position and type for merged Image - //set position for merged image with values of image1 and the height of both - Rectangle2D pos = new Rectangle2D.Float(); - pos.setRect(image1.getPosition().getX(), image2.getPosition().getY(), width, height1 + height2); - PdfImage newPdfImage = new PdfImage(mergedImage, pos, image1.getPage()); - // Graphics need to be disposed - - image1.getImage().flush(); - image2.getImage().flush(); - - mergedImage.flush(); - mergedImageGraphics.dispose(); - - return newPdfImage; - } catch (Exception e) { - // failed to merge image - log.error("Failed to merge image", e); - return null; - } - - - } - - //make a list of true and false, if the image is a candidate for merging - private List getCandidatesList(List imageList) { - List candidatesList = new ArrayList<>(); - for (int i = 0; i < imageList.size(); i++) { - if (i >= 1) { - candidatesList.add(isCandidateForMerging(imageList.get(i - 1), imageList.get(i))); - } - } - return candidatesList; - } - - // evaluate if two images are candidates for merging, depending on their coordinates, width and height - private boolean isCandidateForMerging(PdfImage image1, PdfImage image2) { - double x1 = image1.getPosition().getX(); - double y1 = image1.getPosition().getY(); - double width1 = image1.getPosition().getWidth(); - double x2 = image2.getPosition().getX(); - double y2 = image2.getPosition().getY(); - double width2 = image2.getPosition().getWidth(); - double height2 = image2.getPosition().getHeight(); - //if the x-coordinates and widths of images are equal and the height is equal to difference between y-coordinates, - // then it is the same picture and has to be merged -> return true - return x1 == x2 && width1 == width2 && Math.ceil(height2) == Math.ceil(y1 - y2) && width2 > (height2 / 6); - } - private void increaseDocumentStatistics(Page page, Document document) { @@ -319,5 +185,4 @@ public class PdfSegmentationService { } - } From 143877ff2d6003597cd2a8fc0fe8f835f9d96de5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Wed, 16 Jun 2021 10:54:33 +0200 Subject: [PATCH 14/80] RED-1539: Fixed missing textBefore & textAfter on reanalyis that leads to wrong changelog --- .../redaction/v1/server/redaction/service/ReanalyzeService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java index 3872f49f..9277d8b9 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java @@ -172,7 +172,7 @@ public class ReanalyzeService { Set entities = entityRedactionService.findEntities(reanalysisSection.getSearchableText(), reanalysisSection .getHeadline(), reanalysisSection.getSectionNumber(), dictionary, false); - if (reanalysisSection.getCellStarts() != null) { + if (reanalysisSection.getCellStarts() != null && !reanalysisSection.getCellStarts().isEmpty()) { surroundingWordsService.addSurroundingText(entities, reanalysisSection.getSearchableText(), dictionary, reanalysisSection .getCellStarts()); } else { From b166ab29ee16ab1bec7a8c310c6bd1cda238ae3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Thu, 17 Jun 2021 12:16:07 +0200 Subject: [PATCH 15/80] RED-1212: Enabled to exclude pages --- .../redaction/v1/model/AnalyzeRequest.java | 2 + .../v1/model/RedactionChangeLogEntry.java | 2 + .../redaction/v1/model/RedactionLogEntry.java | 2 + .../service/AnalyzeResponseService.java | 3 + .../redaction/service/ReanalyzeService.java | 75 ++++++++++++++----- .../service/RedactionChangeLogService.java | 1 + .../v1/server/RedactionIntegrationTest.java | 1 + 7 files changed, 68 insertions(+), 18 deletions(-) diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeRequest.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeRequest.java index 4aa290e9..1b7fa890 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeRequest.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeRequest.java @@ -6,6 +6,7 @@ import lombok.Data; import lombok.NoArgsConstructor; import java.time.OffsetDateTime; +import java.util.Set; @Data @Builder @@ -19,6 +20,7 @@ public class AnalyzeRequest { private boolean reanalyseOnlyIfPossible; private ManualRedactions manualRedactions; private OffsetDateTime lastProcessed; + private Set excludedPages; } diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionChangeLogEntry.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionChangeLogEntry.java index 3dfbacce..a53d3b0e 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionChangeLogEntry.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionChangeLogEntry.java @@ -44,4 +44,6 @@ public class RedactionChangeLogEntry { private boolean isDossierDictionaryEntry; + private boolean excluded; + } diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java index d8074d4f..f4c0dfd8 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java @@ -49,4 +49,6 @@ public class RedactionLogEntry { private boolean isDossierDictionaryEntry; + private boolean excluded; + } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java index bae6d1d0..e8a3a885 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java @@ -14,16 +14,19 @@ public class AnalyzeResponseService { boolean hasRequests = redactionLog.getRedactionLogEntry() .stream() + .filter(entry -> !entry.isExcluded()) .anyMatch(entry -> entry.isManual() && entry.getStatus() .equals(com.iqser.red.service.redaction.v1.model.Status.REQUESTED)); boolean hasRedactions = redactionLog.getRedactionLogEntry() .stream() + .filter(entry -> !entry.isExcluded()) .anyMatch(entry -> entry.isRedacted() && !entry.isManual() || entry.isManual() && entry.getStatus() .equals(com.iqser.red.service.redaction.v1.model.Status.APPROVED)); boolean hasImages = redactionLog.getRedactionLogEntry() .stream() + .filter(entry -> !entry.isExcluded()) .anyMatch(entry -> entry.isHint() && entry.getType().equals("image")); boolean hasUpdates = redactionChangeLog != null && redactionChangeLog.getRedactionLogEntry() != null && !redactionChangeLog diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java index 9277d8b9..9d3628e7 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java @@ -1,14 +1,44 @@ package com.iqser.red.service.redaction.v1.server.redaction.service; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.kie.api.runtime.KieContainer; +import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.RequestBody; + import com.iqser.red.service.file.management.v1.api.model.FileType; -import com.iqser.red.service.redaction.v1.model.*; +import com.iqser.red.service.redaction.v1.model.AnalyzeRequest; +import com.iqser.red.service.redaction.v1.model.AnalyzeResult; +import com.iqser.red.service.redaction.v1.model.Comment; +import com.iqser.red.service.redaction.v1.model.IdRemoval; +import com.iqser.red.service.redaction.v1.model.ManualForceRedact; +import com.iqser.red.service.redaction.v1.model.ManualRedactionEntry; +import com.iqser.red.service.redaction.v1.model.ManualRedactions; +import com.iqser.red.service.redaction.v1.model.Rectangle; +import com.iqser.red.service.redaction.v1.model.RedactionLog; +import com.iqser.red.service.redaction.v1.model.RedactionLogEntry; +import com.iqser.red.service.redaction.v1.model.SectionArea; import com.iqser.red.service.redaction.v1.server.classification.model.Document; import com.iqser.red.service.redaction.v1.server.classification.model.SectionText; import com.iqser.red.service.redaction.v1.server.classification.model.Text; import com.iqser.red.service.redaction.v1.server.client.LegalBasisClient; import com.iqser.red.service.redaction.v1.server.exception.RedactionException; import com.iqser.red.service.redaction.v1.server.redaction.model.Dictionary; -import com.iqser.red.service.redaction.v1.server.redaction.model.*; +import com.iqser.red.service.redaction.v1.server.redaction.model.DictionaryIncrement; +import com.iqser.red.service.redaction.v1.server.redaction.model.DictionaryVersion; +import com.iqser.red.service.redaction.v1.server.redaction.model.Entity; +import com.iqser.red.service.redaction.v1.server.redaction.model.EntityPositionSequence; +import com.iqser.red.service.redaction.v1.server.redaction.model.Image; +import com.iqser.red.service.redaction.v1.server.redaction.model.RedRectangle2D; +import com.iqser.red.service.redaction.v1.server.redaction.model.Section; +import com.iqser.red.service.redaction.v1.server.redaction.model.SectionSearchableTextPair; import com.iqser.red.service.redaction.v1.server.redaction.utils.EntitySearchUtils; import com.iqser.red.service.redaction.v1.server.segmentation.PdfSegmentationService; import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService; @@ -17,14 +47,6 @@ import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import org.kie.api.runtime.KieContainer; -import org.springframework.stereotype.Service; -import org.springframework.web.bind.annotation.RequestBody; - -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; - @Slf4j @Service @RequiredArgsConstructor @@ -41,6 +63,7 @@ public class ReanalyzeService { private final AnalyzeResponseService analyzeResponseService; private final LegalBasisClient legalBasisClient; + public AnalyzeResult analyze(AnalyzeRequest analyzeRequest) { long startTime = System.currentTimeMillis(); @@ -66,11 +89,11 @@ public class ReanalyzeService { log.info("Redaction analysis successful..."); var legalBasis = legalBasisClient.getLegalBasisMapping(analyzeRequest.getDossierTemplateId()); - var redactionLog = new RedactionLog(classifiedDoc.getRedactionLogEntities(),legalBasis, - classifiedDoc.getDictionaryVersion().getDossierTemplateVersion(), - classifiedDoc.getDictionaryVersion().getDossierVersion(), - classifiedDoc.getRulesVersion(), - legalBasisClient.getVersion(analyzeRequest.getDossierTemplateId())); + var redactionLog = new RedactionLog(classifiedDoc.getRedactionLogEntities(), legalBasis, classifiedDoc.getDictionaryVersion() + .getDossierTemplateVersion(), classifiedDoc.getDictionaryVersion() + .getDossierVersion(), classifiedDoc.getRulesVersion(), legalBasisClient.getVersion(analyzeRequest.getDossierTemplateId())); + + excludeExcludedPages(redactionLog, analyzeRequest.getExcludedPages()); log.info("Analyzed with rules {} and dictionary {} for dossierTemplate: {}", classifiedDoc.getRulesVersion(), classifiedDoc .getDictionaryVersion(), analyzeRequest.getDossierTemplateId()); @@ -165,7 +188,8 @@ public class ReanalyzeService { KieContainer kieContainer = droolsExecutionService.updateRules(analyzeRequest.getDossierTemplateId()); - Dictionary dictionary = dictionaryService.getDeepCopyDictionary(analyzeRequest.getDossierTemplateId(), analyzeRequest.getDossierId()); + Dictionary dictionary = dictionaryService.getDeepCopyDictionary(analyzeRequest.getDossierTemplateId(), analyzeRequest + .getDossierId()); List sectionSearchableTextPairs = new ArrayList<>(); for (SectionText reanalysisSection : reanalysisSections) { @@ -240,8 +264,7 @@ public class ReanalyzeService { .getDossierTemplateId())); } - redactionLog.getRedactionLogEntry() - .removeIf(entry -> sectionsToReanalyse.contains(entry.getSectionNumber())); + redactionLog.getRedactionLogEntry().removeIf(entry -> sectionsToReanalyse.contains(entry.getSectionNumber())); redactionLog.getRedactionLogEntry().addAll(newRedactionLogEntries); return finalizeAnalysis(analyzeRequest, startTime, redactionLog, text, dictionaryIncrement); @@ -255,6 +278,8 @@ public class ReanalyzeService { redactionLog.setDictionaryVersion(dictionaryIncrement.getDictionaryVersion().getDossierTemplateVersion()); redactionLog.setDossierDictionaryVersion(dictionaryIncrement.getDictionaryVersion().getDossierVersion()); + excludeExcludedPages(redactionLog, analyzeRequest.getExcludedPages()); + var changeLog = redactionChangeLogService.createAndStoreChangeLog(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), redactionLog); redactionStorageService.storeObject(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), FileType.REDACTION_LOG, redactionLog); @@ -292,4 +317,18 @@ public class ReanalyzeService { .build(); } + + private void excludeExcludedPages(RedactionLog redactionLog, Set excludedPages) { + + redactionLog.getRedactionLogEntry().forEach(entry -> { + entry.getPositions().forEach(pos -> { + if (excludedPages != null && excludedPages.contains(pos.getPage())) { + entry.setExcluded(true); + } else { + entry.setExcluded(false); + } + }); + }); + } + } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionChangeLogService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionChangeLogService.java index 53fc805e..1cc743e5 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionChangeLogService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionChangeLogService.java @@ -90,6 +90,7 @@ public class RedactionChangeLogService { .comments(entry.getComments()) .changeType(changeType) .isDossierDictionaryEntry(entry.isDossierDictionaryEntry()) + .excluded(entry.isExcluded()) .build(); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java index a878400d..30d02971 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java @@ -633,6 +633,7 @@ public class RedactionIntegrationTest { long start = System.currentTimeMillis(); ClassPathResource pdfFileResource = new ClassPathResource("files/new/Single Study - Oral (Gavage) Mouse.pdf"); AnalyzeRequest request = prepareStorage(pdfFileResource.getInputStream()); + request.setExcludedPages(Set.of(1)); AnalyzeResult result = reanalyzeService.analyze(request); From 8ff3e463eae960a35e11fce7589e24131ccb51f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Fri, 18 Jun 2021 11:28:43 +0200 Subject: [PATCH 16/80] RED-1326: Enabled to recategorize images --- .../v1/model/ManualImageRecategorization.java | 21 ++++++++++ .../v1/model/ManualRedactionType.java | 2 +- .../redaction/v1/model/ManualRedactions.java | 3 ++ .../redaction/v1/model/RedactionLogEntry.java | 2 + .../service/RedactionLogCreatorService.java | 40 +++++++++++++++++-- 5 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualImageRecategorization.java diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualImageRecategorization.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualImageRecategorization.java new file mode 100644 index 00000000..7dc9120c --- /dev/null +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualImageRecategorization.java @@ -0,0 +1,21 @@ +package com.iqser.red.service.redaction.v1.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ManualImageRecategorization { + + private String id; + private String user; + private Status status; + private String type; + private String legalBasis; + private boolean redacted; + +} \ No newline at end of file diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualRedactionType.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualRedactionType.java index 83df7d67..31f78218 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualRedactionType.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualRedactionType.java @@ -1,5 +1,5 @@ package com.iqser.red.service.redaction.v1.model; public enum ManualRedactionType { - ADD, REMOVE, FORCE_REDACT + ADD, REMOVE, FORCE_REDACT, RECATEGORIZE } diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualRedactions.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualRedactions.java index af866d09..64426a07 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualRedactions.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualRedactions.java @@ -26,6 +26,9 @@ public class ManualRedactions { @Builder.Default private Set entriesToAdd = new HashSet<>(); + @Builder.Default + private Set imageRecategorizations = new HashSet<>(); + @Builder.Default private Map> comments = new HashMap<>(); diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java index f4c0dfd8..2c9e5610 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java @@ -51,4 +51,6 @@ public class RedactionLogEntry { private boolean excluded; + private String recategorizationType; + } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java index 9aef4dc7..92ac8a28 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java @@ -13,7 +13,9 @@ import com.iqser.red.service.redaction.v1.server.redaction.utils.IdBuilder; import com.iqser.red.service.redaction.v1.server.tableextraction.model.AbstractTextContainer; import com.iqser.red.service.redaction.v1.server.tableextraction.model.Cell; import com.iqser.red.service.redaction.v1.server.tableextraction.model.Table; + import lombok.RequiredArgsConstructor; + import org.apache.commons.collections4.CollectionUtils; import org.springframework.stereotype.Service; @@ -87,6 +89,36 @@ public class RedactionLogCreatorService { .section(image.getSection()) .build(); + if (manualRedactions != null && !manualRedactions.getImageRecategorizations().isEmpty()) { + for (ManualImageRecategorization recategorization : manualRedactions.getImageRecategorizations()) { + if (recategorization.getId().equals(id)) { + String manualOverrideReason = null; + if (recategorization.getStatus().equals(Status.APPROVED)) { + image.setType(recategorization.getType()); + image.setRedaction(recategorization.isRedacted()); + image.setLegalBasis(recategorization.getLegalBasis()); + redactionLogEntry.setRedacted(recategorization.isRedacted()); + redactionLogEntry.setStatus(Status.APPROVED); + redactionLogEntry.setLegalBasis(recategorization.getLegalBasis()); + manualOverrideReason = image.getRedactionReason() + ", recategorized by manual override"; + redactionLogEntry.setColor(getColorForImage(image, dossierTemplateId, false)); + } else if (recategorization.getStatus().equals(Status.REQUESTED)) { + manualOverrideReason = image.getRedactionReason() + ", requested to recategorize"; + redactionLogEntry.setStatus(Status.REQUESTED); + redactionLogEntry.setColor(getColorForImage(image, dossierTemplateId, true)); + redactionLogEntry.setRecategorizationType(recategorization.getType()); + } else { + redactionLogEntry.setStatus(Status.DECLINED); + } + + image.setRedactionReason(manualOverrideReason != null ? manualOverrideReason : image.getRedactionReason()); + redactionLogEntry.setReason(manualOverrideReason); + redactionLogEntry.setManual(true); + redactionLogEntry.setManualRedactionType(ManualRedactionType.RECATEGORIZE); + } + } + } + if (manualRedactions != null && !manualRedactions.getIdsToRemove().isEmpty()) { for (IdRemoval manualRemoval : manualRedactions.getIdsToRemove()) { if (manualRemoval.getId().equals(id)) { @@ -276,20 +308,22 @@ public class RedactionLogCreatorService { List rectangles = new ArrayList<>(); if (textPositions.size() == 1) { - rectangles.add( TextPositionSequence.fromData(textPositions, page).getRectangle()); + rectangles.add(TextPositionSequence.fromData(textPositions, page).getRectangle()); } else { float y = textPositions.get(0).getYDirAdj(); int startIndex = 0; for (int i = 1; i < textPositions.size(); i++) { float yDirAdj = textPositions.get(i).getYDirAdj(); if (yDirAdj != y) { - rectangles.add( TextPositionSequence.fromData(textPositions.subList(startIndex, i), page).getRectangle()); + rectangles.add(TextPositionSequence.fromData(textPositions.subList(startIndex, i), page) + .getRectangle()); y = yDirAdj; startIndex = i; } } if (startIndex != textPositions.size()) { - rectangles.add( TextPositionSequence.fromData(textPositions.subList(startIndex, textPositions.size()), page).getRectangle()); + rectangles.add(TextPositionSequence.fromData(textPositions.subList(startIndex, textPositions.size()), page) + .getRectangle()); } } From c5f5b9f7b928c31e25a000a6d9c055aab3e9fc00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Mon, 21 Jun 2021 15:10:21 +0200 Subject: [PATCH 17/80] RED-1326: Fixed image recatigorization in reanalysis --- .../redaction/service/ReanalyzeService.java | 6 +++--- .../service/RedactionLogCreatorService.java | 2 ++ .../v1/server/RedactionIntegrationTest.java | 17 +++++++++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java index 9d3628e7..17fed969 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java @@ -19,6 +19,7 @@ import com.iqser.red.service.redaction.v1.model.AnalyzeResult; import com.iqser.red.service.redaction.v1.model.Comment; import com.iqser.red.service.redaction.v1.model.IdRemoval; import com.iqser.red.service.redaction.v1.model.ManualForceRedact; +import com.iqser.red.service.redaction.v1.model.ManualImageRecategorization; import com.iqser.red.service.redaction.v1.model.ManualRedactionEntry; import com.iqser.red.service.redaction.v1.model.ManualRedactions; import com.iqser.red.service.redaction.v1.model.Rectangle; @@ -296,9 +297,8 @@ public class ReanalyzeService { return new HashSet<>(); } - return Stream.concat(manualRedactions.getIdsToRemove() - .stream() - .map(IdRemoval::getId), manualRedactions.getForceRedacts().stream().map(ManualForceRedact::getId)) + return Stream.concat(manualRedactions.getImageRecategorizations().stream().map(ManualImageRecategorization::getId), + Stream.concat(manualRedactions.getIdsToRemove().stream().map(IdRemoval::getId), manualRedactions.getForceRedacts().stream().map(ManualForceRedact::getId))) .collect(Collectors.toSet()); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java index 92ac8a28..6a6a33e0 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java @@ -97,6 +97,8 @@ public class RedactionLogCreatorService { image.setType(recategorization.getType()); image.setRedaction(recategorization.isRedacted()); image.setLegalBasis(recategorization.getLegalBasis()); + redactionLogEntry.setType(recategorization.getType()); + redactionLogEntry.setHint(dictionaryService.isHint(recategorization.getType(), dossierTemplateId)); redactionLogEntry.setRedacted(recategorization.isRedacted()); redactionLogEntry.setStatus(Status.APPROVED); redactionLogEntry.setLegalBasis(recategorization.getLegalBasis()); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java index 30d02971..ab810467 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java @@ -689,8 +689,25 @@ public class RedactionIntegrationTest { when(dictionaryClient.getDictionaryForType(VERTEBRATE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(VERTEBRATE, false)); start = System.currentTimeMillis(); + + ManualRedactions manualRedactions = new ManualRedactions(); + + manualRedactions.setImageRecategorizations(Set.of(ManualImageRecategorization.builder() + .id("37eee3e9d589a5cc529bfec38c3ba479") + .status(Status.APPROVED) + .type("signature") + .redacted(true) + .legalBasis("Article 39(e)(1) and Article 39(e)(2) of Regulation (EC) No 178/2002") + .build())); + + request.setManualRedactions(manualRedactions); + + AnalyzeResult reanalyzeResult = reanalyzeService.reanalyze(request); + + redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); + end = System.currentTimeMillis(); System.out.println("reanalysis analysis duration: " + (end - start)); From 7c99581f32fa4ac963cf0bd32bbc0dfe05903d0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Tue, 22 Jun 2021 12:09:35 +0200 Subject: [PATCH 18/80] RED-1446: Enabled to change legal basis manually --- .../v1/model/ManualLegalBasisChange.java | 19 +++++++ .../v1/model/ManualRedactionType.java | 2 +- .../redaction/v1/model/ManualRedactions.java | 3 ++ .../redaction/v1/model/RedactionLogEntry.java | 1 + .../redaction/service/ReanalyzeService.java | 6 ++- .../service/RedactionLogCreatorService.java | 51 +++++++++++++++++++ .../v1/server/RedactionIntegrationTest.java | 8 +++ 7 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualLegalBasisChange.java diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualLegalBasisChange.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualLegalBasisChange.java new file mode 100644 index 00000000..4f0d211f --- /dev/null +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualLegalBasisChange.java @@ -0,0 +1,19 @@ +package com.iqser.red.service.redaction.v1.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ManualLegalBasisChange { + + private String id; + private String user; + private Status status; + private String legalBasis; + +} \ No newline at end of file diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualRedactionType.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualRedactionType.java index 31f78218..0bb0c607 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualRedactionType.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualRedactionType.java @@ -1,5 +1,5 @@ package com.iqser.red.service.redaction.v1.model; public enum ManualRedactionType { - ADD, REMOVE, FORCE_REDACT, RECATEGORIZE + ADD, REMOVE, FORCE_REDACT, RECATEGORIZE, LEGAL_BASIS_CHANGE } diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualRedactions.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualRedactions.java index 64426a07..baffede0 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualRedactions.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualRedactions.java @@ -29,6 +29,9 @@ public class ManualRedactions { @Builder.Default private Set imageRecategorizations = new HashSet<>(); + @Builder.Default + private Set manualLegalBasisChanges = new HashSet<>(); + @Builder.Default private Map> comments = new HashMap<>(); diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java index 2c9e5610..e347f0b3 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java @@ -52,5 +52,6 @@ public class RedactionLogEntry { private boolean excluded; private String recategorizationType; + private String legalBasisChangeValue; } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java index 17fed969..4f20e29f 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java @@ -20,6 +20,7 @@ import com.iqser.red.service.redaction.v1.model.Comment; import com.iqser.red.service.redaction.v1.model.IdRemoval; import com.iqser.red.service.redaction.v1.model.ManualForceRedact; import com.iqser.red.service.redaction.v1.model.ManualImageRecategorization; +import com.iqser.red.service.redaction.v1.model.ManualLegalBasisChange; import com.iqser.red.service.redaction.v1.model.ManualRedactionEntry; import com.iqser.red.service.redaction.v1.model.ManualRedactions; import com.iqser.red.service.redaction.v1.model.Rectangle; @@ -297,8 +298,9 @@ public class ReanalyzeService { return new HashSet<>(); } - return Stream.concat(manualRedactions.getImageRecategorizations().stream().map(ManualImageRecategorization::getId), - Stream.concat(manualRedactions.getIdsToRemove().stream().map(IdRemoval::getId), manualRedactions.getForceRedacts().stream().map(ManualForceRedact::getId))) + return Stream.concat(manualRedactions.getManualLegalBasisChanges().stream().map(ManualLegalBasisChange::getId), + Stream.concat(manualRedactions.getImageRecategorizations().stream().map(ManualImageRecategorization::getId), + Stream.concat(manualRedactions.getIdsToRemove().stream().map(IdRemoval::getId), manualRedactions.getForceRedacts().stream().map(ManualForceRedact::getId)))) .collect(Collectors.toSet()); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java index 6a6a33e0..ba094a48 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java @@ -175,6 +175,32 @@ public class RedactionLogCreatorService { } } + + if (manualRedactions != null && !manualRedactions.getManualLegalBasisChanges().isEmpty()) { + for (ManualLegalBasisChange manualLegalBasisChange : manualRedactions.getManualLegalBasisChanges()) { + if (manualLegalBasisChange.getId().equals(id)) { + String manualOverrideReason = null; + if (manualLegalBasisChange.getStatus().equals(Status.APPROVED)) { + redactionLogEntry.setStatus(Status.APPROVED); + manualOverrideReason = image.getRedactionReason() + ", legal basis was manually changed"; + redactionLogEntry.setLegalBasis(manualLegalBasisChange.getLegalBasis()); + } else if (manualLegalBasisChange.getStatus().equals(Status.REQUESTED)) { + manualOverrideReason = image.getRedactionReason() + ", legal basis change requested"; + redactionLogEntry.setStatus(Status.REQUESTED); + redactionLogEntry.setColor(getColorForImage(image, dossierTemplateId, true)); + redactionLogEntry.setLegalBasisChangeValue(manualLegalBasisChange.getLegalBasis()); + } else { + redactionLogEntry.setStatus(Status.DECLINED); + } + + image.setRedactionReason(manualOverrideReason != null ? manualOverrideReason : image.getRedactionReason()); + redactionLogEntry.setReason(manualOverrideReason); + redactionLogEntry.setManual(true); + redactionLogEntry.setManualRedactionType(ManualRedactionType.LEGAL_BASIS_CHANGE); + } + } + } + redactionLogEntities.add(redactionLogEntry); } @@ -278,6 +304,31 @@ public class RedactionLogCreatorService { } } + if (manualRedactions != null && !manualRedactions.getManualLegalBasisChanges().isEmpty()) { + for (ManualLegalBasisChange manualLegalBasisChange : manualRedactions.getManualLegalBasisChanges()) { + if (manualLegalBasisChange.getId().equals(entityPositionSequence.getId())) { + String manualOverrideReason = null; + if (manualLegalBasisChange.getStatus().equals(Status.APPROVED)) { + redactionLogEntry.setStatus(Status.APPROVED); + manualOverrideReason = entity.getRedactionReason() + ", legal basis was manually changed"; + redactionLogEntry.setLegalBasis(manualLegalBasisChange.getLegalBasis()); + } else if (manualLegalBasisChange.getStatus().equals(Status.REQUESTED)) { + manualOverrideReason = entity.getRedactionReason() + ", legal basis change requested"; + redactionLogEntry.setStatus(Status.REQUESTED); + redactionLogEntry.setColor(getColor(entity, dossierTemplateId, true)); + redactionLogEntry.setLegalBasisChangeValue(manualLegalBasisChange.getLegalBasis()); + } else { + redactionLogEntry.setStatus(Status.DECLINED); + } + + entity.setRedactionReason(manualOverrideReason != null ? manualOverrideReason : entity.getRedactionReason()); + redactionLogEntry.setReason(manualOverrideReason); + redactionLogEntry.setManual(true); + redactionLogEntry.setManualRedactionType(ManualRedactionType.LEGAL_BASIS_CHANGE); + } + } + } + if (CollectionUtils.isNotEmpty(entityPositionSequence.getSequences())) { List rectanglesPerLine = getRectanglesPerLine(entityPositionSequence.getSequences() .stream() diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java index ab810467..57cfb128 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java @@ -775,6 +775,7 @@ public class RedactionIntegrationTest { .status(Status.APPROVED) .build())); + manualRedactions.getComments().put("e5be0f1d941bbb92a068e198648d06c4", List.of(comment)); manualRedactions.getComments().put("0836727c3508a0b2ea271da69c04cc2f", List.of(comment)); manualRedactions.getComments().put(manualAddId, List.of(comment)); @@ -794,14 +795,21 @@ public class RedactionIntegrationTest { request.setManualRedactions(manualRedactions); AnalyzeResult result = reanalyzeService.analyze(request); + manualRedactions.getEntriesToAdd().add(manualRedactionEntry); manualRedactions.setIdsToRemove(Set.of(IdRemoval.builder() .id("5b940b2cb401ed9f5be6fc24f6e77bcf") .status(Status.APPROVED) .build())); + manualRedactions.setManualLegalBasisChanges(Set.of(ManualLegalBasisChange.builder() + .id("675eba69b0c2917de55462c817adaa05") + .legalBasis("Manual Legal Basis Change") + .status(Status.APPROVED) + .build())); reanalyzeService.reanalyze(request); + var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); AnnotateResponse annotateResponse = redactionController.annotate(AnnotateRequest.builder() .dossierId(TEST_DOSSIER_ID) From 5d5b3529cd8e45493cc91df3cb8b1f2a3ba14b4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Fri, 25 Jun 2021 10:57:20 +0200 Subject: [PATCH 19/80] RED-1637: Fixed hasImages flag --- .../v1/server/redaction/service/AnalyzeResponseService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java index e8a3a885..eceedae2 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java @@ -27,7 +27,7 @@ public class AnalyzeResponseService { boolean hasImages = redactionLog.getRedactionLogEntry() .stream() .filter(entry -> !entry.isExcluded()) - .anyMatch(entry -> entry.isHint() && entry.getType().equals("image")); + .anyMatch(entry -> entry.isHint() && entry.getType().equals("image") || entry.isImage()); boolean hasUpdates = redactionChangeLog != null && redactionChangeLog.getRedactionLogEntry() != null && !redactionChangeLog .getRedactionLogEntry() From 6ae6d467fc5fec4b385cde1c70bae7a21143342f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Tue, 29 Jun 2021 10:24:51 +0200 Subject: [PATCH 20/80] RED-1445: Enabled to use file attributes in rules --- .../redaction/v1/model/AnalyzeRequest.java | 7 +++++ .../redaction/v1/model/FileAttribute.java | 19 ++++++++++++ .../v1/server/redaction/model/Section.java | 19 ++++++++++++ .../service/EntityRedactionService.java | 31 +++++++++++-------- .../redaction/service/ReanalyzeService.java | 3 +- .../v1/server/RedactionIntegrationTest.java | 2 ++ .../service/EntityRedactionServiceTest.java | 20 ++++++------ .../src/test/resources/drools/rules.drl | 2 +- 8 files changed, 78 insertions(+), 25 deletions(-) create mode 100644 redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/FileAttribute.java diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeRequest.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeRequest.java index 1b7fa890..7a891277 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeRequest.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeRequest.java @@ -6,6 +6,10 @@ import lombok.Data; import lombok.NoArgsConstructor; import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Set; @Data @@ -22,5 +26,8 @@ public class AnalyzeRequest { private OffsetDateTime lastProcessed; private Set excludedPages; + @Builder.Default + private List fileAttributes = new ArrayList<>(); + } diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/FileAttribute.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/FileAttribute.java new file mode 100644 index 00000000..aea494c9 --- /dev/null +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/FileAttribute.java @@ -0,0 +1,19 @@ +package com.iqser.red.service.redaction.v1.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FileAttribute { + + private String id; + private String label; + private String placeholder; + private String value; + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Section.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Section.java index fa44f983..dc878a20 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Section.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Section.java @@ -1,5 +1,6 @@ package com.iqser.red.service.redaction.v1.server.redaction.model; +import com.iqser.red.service.redaction.v1.model.FileAttribute; import com.iqser.red.service.redaction.v1.server.classification.model.TextBlock; import com.iqser.red.service.redaction.v1.server.redaction.utils.EntitySearchUtils; import com.iqser.red.service.redaction.v1.server.redaction.utils.Patterns; @@ -8,9 +9,11 @@ import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; @@ -52,6 +55,22 @@ public class Section { @Builder.Default private Set images = new HashSet<>(); + @Builder.Default + private List fileAttributes = new ArrayList<>(); + + + public boolean fileAttributeByIdEquals(String id, String value){ + return fileAttributes != null && fileAttributes.stream().filter(attribute -> id.equals(attribute.getId()) && value.equals(attribute.getValue())).findFirst().isPresent(); + } + + public boolean fileAttributeByPlaceholderEquals(String placeholder, String value){ + return fileAttributes != null && fileAttributes.stream().filter(attribute -> placeholder.equals(attribute.getPlaceholder()) && value.equals(attribute.getValue())).findFirst().isPresent(); + } + + public boolean fileAttributeByLabelEquals(String label, String value){ + return fileAttributes != null && fileAttributes.stream().filter(attribute -> label.equals(attribute.getLabel()) && value.equals(attribute.getValue())).findFirst().isPresent(); + } + public boolean rowEquals(String headerName, String value) { diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java index 02da3aa7..e6d00c15 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java @@ -1,5 +1,6 @@ package com.iqser.red.service.redaction.v1.server.redaction.service; +import com.iqser.red.service.redaction.v1.model.FileAttribute; import com.iqser.red.service.redaction.v1.model.ManualRedactionEntry; import com.iqser.red.service.redaction.v1.model.ManualRedactions; import com.iqser.red.service.redaction.v1.model.Point; @@ -33,7 +34,7 @@ public class EntityRedactionService { private final SurroundingWordsService surroundingWordsService; - public void processDocument(Document classifiedDoc, String dossierTemplateId, ManualRedactions manualRedactions, String dossierId) { + public void processDocument(Document classifiedDoc, String dossierTemplateId, ManualRedactions manualRedactions, String dossierId, List fileAttributes) { dictionaryService.updateDictionary(dossierTemplateId, dossierId); KieContainer container = droolsExecutionService.updateRules(dossierTemplateId); @@ -41,7 +42,7 @@ public class EntityRedactionService { Dictionary dictionary = dictionaryService.getDeepCopyDictionary(dossierTemplateId, dossierId); - Set documentEntities = new HashSet<>(findEntities(classifiedDoc, container, manualRedactions, dictionary, false, null)); + Set documentEntities = new HashSet<>(findEntities(classifiedDoc, container, manualRedactions, dictionary, false, null, fileAttributes)); if (dictionary.hasLocalEntries()) { @@ -53,7 +54,7 @@ public class EntityRedactionService { } }); - Set foundByLocal = findEntities(classifiedDoc, container, manualRedactions, dictionary, true, hintsPerSectionNumber); + Set foundByLocal = findEntities(classifiedDoc, container, manualRedactions, dictionary, true, hintsPerSectionNumber, fileAttributes); EntitySearchUtils.addEntitiesWithHigherRank(documentEntities, foundByLocal, dictionary); EntitySearchUtils.removeEntitiesContainedInLarger(documentEntities); } @@ -84,7 +85,7 @@ public class EntityRedactionService { private Set findEntities(Document classifiedDoc, KieContainer kieContainer, ManualRedactions manualRedactions, Dictionary dictionary, boolean local, - Map> hintsPerSectionNumber) { + Map> hintsPerSectionNumber, List fileAttributes) { Set documentEntities = new HashSet<>(); @@ -95,31 +96,31 @@ public class EntityRedactionService { List tables = paragraph.getTables(); for (Table table : tables) { if (table.getColCount() == 2) { - sectionSearchableTextPairs.addAll(processTableAsOneText(classifiedDoc, table, manualRedactions, sectionNumber, dictionary, local, hintsPerSectionNumber)); + sectionSearchableTextPairs.addAll(processTableAsOneText(classifiedDoc, table, manualRedactions, sectionNumber, dictionary, local, hintsPerSectionNumber, fileAttributes)); } else { - sectionSearchableTextPairs.addAll(processTablePerRow(classifiedDoc, table, manualRedactions, sectionNumber, dictionary, local, hintsPerSectionNumber)); + sectionSearchableTextPairs.addAll(processTablePerRow(classifiedDoc, table, manualRedactions, sectionNumber, dictionary, local, hintsPerSectionNumber, fileAttributes)); } sectionNumber.incrementAndGet(); } sectionSearchableTextPairs.add(processText(classifiedDoc, paragraph.getSearchableText(), paragraph.getTextBlocks(), paragraph .getHeadline(), manualRedactions, sectionNumber, dictionary, local, hintsPerSectionNumber, paragraph - .getImages())); + .getImages(), fileAttributes)); sectionNumber.incrementAndGet(); } for (Header header : classifiedDoc.getHeaders()) { - sectionSearchableTextPairs.add(processText(classifiedDoc, header.getSearchableText(), header.getTextBlocks(), "Header", manualRedactions, sectionNumber, dictionary, local, hintsPerSectionNumber, new ArrayList<>())); + sectionSearchableTextPairs.add(processText(classifiedDoc, header.getSearchableText(), header.getTextBlocks(), "Header", manualRedactions, sectionNumber, dictionary, local, hintsPerSectionNumber, new ArrayList<>(), fileAttributes)); sectionNumber.incrementAndGet(); } for (Footer footer : classifiedDoc.getFooters()) { - sectionSearchableTextPairs.add(processText(classifiedDoc, footer.getSearchableText(), footer.getTextBlocks(), "Footer", manualRedactions, sectionNumber, dictionary, local, hintsPerSectionNumber, new ArrayList<>())); + sectionSearchableTextPairs.add(processText(classifiedDoc, footer.getSearchableText(), footer.getTextBlocks(), "Footer", manualRedactions, sectionNumber, dictionary, local, hintsPerSectionNumber, new ArrayList<>(), fileAttributes)); sectionNumber.incrementAndGet(); } for (UnclassifiedText unclassifiedText : classifiedDoc.getUnclassifiedTexts()) { sectionSearchableTextPairs.add(processText(classifiedDoc, unclassifiedText.getSearchableText(), unclassifiedText - .getTextBlocks(), "", manualRedactions, sectionNumber, dictionary, local, hintsPerSectionNumber, new ArrayList<>())); + .getTextBlocks(), "", manualRedactions, sectionNumber, dictionary, local, hintsPerSectionNumber, new ArrayList<>(), fileAttributes)); sectionNumber.incrementAndGet(); } @@ -164,7 +165,7 @@ public class EntityRedactionService { ManualRedactions manualRedactions, AtomicInteger sectionNumber, Dictionary dictionary, boolean local, - Map> hintsPerSectionNumber) { + Map> hintsPerSectionNumber, List fileAttributes) { List sectionSearchableTextPairs = new ArrayList<>(); @@ -229,6 +230,7 @@ public class EntityRedactionService { .tabularData(tabularData) .searchableText(searchableRow) .dictionary(dictionary) + .fileAttributes(fileAttributes) .build(), searchableRow)); if (!local) { @@ -252,7 +254,8 @@ public class EntityRedactionService { ManualRedactions manualRedactions, AtomicInteger sectionNumber, Dictionary dictionary, boolean local, - Map> hintsPerSectionNumber) { + Map> hintsPerSectionNumber, + List fileAttributes) { List sectionSearchableTextPairs = new ArrayList<>(); SearchableText entireTableText = new SearchableText(); @@ -296,6 +299,7 @@ public class EntityRedactionService { .sectionNumber(sectionNumber.intValue()) .searchableText(entireTableText) .dictionary(dictionary) + .fileAttributes(fileAttributes) .build(), entireTableText)); if (!local) { @@ -315,7 +319,7 @@ public class EntityRedactionService { ManualRedactions manualRedactions, AtomicInteger sectionNumber, Dictionary dictionary, boolean local, Map> hintsPerSectionNumber, - List images) { + List images, List fileAttributes) { if (!local) { SectionText sectionText = new SectionText(); @@ -355,6 +359,7 @@ public class EntityRedactionService { .images(images.stream() .map(image -> convert(image, sectionNumber.intValue(), headline)) .collect(Collectors.toSet())) + .fileAttributes(fileAttributes) .build(), searchableText); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java index 4f20e29f..edaac63d 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java @@ -84,7 +84,7 @@ public class ReanalyzeService { log.info("Document structure analysis successful, starting redaction analysis..."); entityRedactionService.processDocument(classifiedDoc, analyzeRequest.getDossierTemplateId(), analyzeRequest.getManualRedactions(), analyzeRequest - .getDossierId()); + .getDossierId(), analyzeRequest.getFileAttributes()); redactionLogCreatorService.createRedactionLog(classifiedDoc, pageCount, analyzeRequest.getManualRedactions(), analyzeRequest .getDossierTemplateId()); @@ -217,6 +217,7 @@ public class ReanalyzeService { .searchableText(reanalysisSection.getSearchableText()) .dictionary(dictionary) .images(reanalysisSection.getImages()) + .fileAttributes(analyzeRequest.getFileAttributes()) .build(), reanalysisSection.getSearchableText())); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java index 57cfb128..305df362 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java @@ -635,6 +635,8 @@ public class RedactionIntegrationTest { AnalyzeRequest request = prepareStorage(pdfFileResource.getInputStream()); request.setExcludedPages(Set.of(1)); + request.setFileAttributes(List.of(FileAttribute.builder().id("fileAttributeId").label("Vertebrate Study").placeholder("{fileattributes.vertebrateStudy}").value("true").build())); + AnalyzeResult result = reanalyzeService.analyze(request); var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionServiceTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionServiceTest.java index 01379d9b..1ab88196 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionServiceTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionServiceTest.java @@ -151,7 +151,7 @@ public class EntityRedactionServiceTest { when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); Document classifiedDoc = pdfSegmentationService.parseDocument(pdfFileResource.getInputStream()); - entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId"); + entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId", null); assertThat(classifiedDoc.getEntities()).hasSize(1); // one page assertThat(classifiedDoc.getEntities().get(1)).hasSize(7);// 3 author cells, 1 address, 1 Y and 2 N entities } @@ -177,7 +177,7 @@ public class EntityRedactionServiceTest { when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); Document classifiedDoc = pdfSegmentationService.parseDocument(pdfFileResource.getInputStream()); - entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId"); + entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId", null); assertThat(classifiedDoc.getEntities()).hasSize(1); // one page assertThat(classifiedDoc.getEntities().get(1)).hasSize(7);// 3 author cells, 1 address, 1 Y and 2 N entities } @@ -202,7 +202,7 @@ public class EntityRedactionServiceTest { .build(); when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); Document classifiedDoc = pdfSegmentationService.parseDocument(pdfFileResource.getInputStream()); - entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId"); + entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId", null); assertThat(classifiedDoc.getEntities() .entrySet() .stream() @@ -210,7 +210,7 @@ public class EntityRedactionServiceTest { pdfFileResource = new ClassPathResource("files/Compounds/27 A8637C - EU AIR3 - MCP Section 1 - Identity of " + "the plant protection product.pdf"); classifiedDoc = pdfSegmentationService.parseDocument(pdfFileResource.getInputStream()); - entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId"); + entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId", null); assertThat(classifiedDoc.getEntities() .entrySet() .stream() @@ -235,7 +235,7 @@ public class EntityRedactionServiceTest { .build(); when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); Document classifiedDoc = pdfSegmentationService.parseDocument(pdfFileResource.getInputStream()); - entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId"); + entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId", null); assertThat(classifiedDoc.getEntities()).hasSize(1); // one page assertThat(classifiedDoc.getEntities().get(1).stream() .filter(entity -> entity.getMatchedRule() == 9) @@ -302,7 +302,7 @@ public class EntityRedactionServiceTest { .build(); when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); Document classifiedDoc = pdfSegmentationService.parseDocument(pdfFileResource.getInputStream()); - entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId"); + entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId", null); assertThat(classifiedDoc.getEntities()).hasSize(1); // one page assertThat(classifiedDoc.getEntities().get(1).stream() .filter(entity -> entity.getMatchedRule() == 6) @@ -341,7 +341,7 @@ public class EntityRedactionServiceTest { .build(); when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); Document classifiedDoc = pdfSegmentationService.parseDocument(pdfFileResource.getInputStream()); - entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId"); + entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId", null); assertThat(classifiedDoc.getEntities()).hasSize(1); // one page assertThat(classifiedDoc.getEntities().get(1).stream() .filter(entity -> entity.getMatchedRule() == 11) @@ -371,7 +371,7 @@ public class EntityRedactionServiceTest { .build(); when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); Document classifiedDoc = pdfSegmentationService.parseDocument(pdfFileResource.getInputStream()); - entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId"); + entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId", null); assertThat(classifiedDoc.getEntities()).hasSize(2); // two pages assertThat(classifiedDoc.getEntities().get(1).stream().filter(entity -> entity.getMatchedRule() == 9).count()).isEqualTo(8); assertThat(classifiedDoc.getEntities().get(2).stream().filter(entity -> entity.getMatchedRule() == 9).count()).isEqualTo(5); // 2 names, 1 address, 2 Y @@ -390,7 +390,7 @@ public class EntityRedactionServiceTest { when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); classifiedDoc = pdfSegmentationService.parseDocument(pdfFileResource.getInputStream()); - entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId"); + entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId", null); assertThat(classifiedDoc.getEntities()).hasSize(1); // one page assertThat(classifiedDoc.getEntities().get(1).stream().filter(entity -> entity.getMatchedRule() == 9).count()).isEqualTo(3); assertThat(classifiedDoc.getEntities().get(1).stream().filter(entity -> entity.getMatchedRule() == 8).count()).isEqualTo(9); @@ -419,7 +419,7 @@ public class EntityRedactionServiceTest { .build(); when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); Document classifiedDoc = pdfSegmentationService.parseDocument(pdfFileResource.getInputStream()); - entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId"); + entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId", null); assertThat(classifiedDoc.getEntities()).hasSize(1); // one page assertThat(classifiedDoc.getEntities().get(1).stream().filter(entity -> entity.getMatchedRule() == 8).count()).isEqualTo(6); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/rules.drl b/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/rules.drl index 5f7e24f2..853d7fac 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/rules.drl +++ b/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/rules.drl @@ -268,7 +268,7 @@ rule "18: Redact contact information if Producer is found" rule "19: Redact AUTHOR(S)" when - Section(searchText.contains("AUTHOR(S):")) + Section(searchText.contains("AUTHOR(S):") && fileAttributeByPlaceholderEquals("{fileattributes.vertebrateStudy}", "true")) then section.redactLinesBetween("AUTHOR(S):", "COMPLETION DATE:", "PII", 19, true, "AUTHOR(S) was found", "Reg (EC) No 1107/2009 Art. 63 (2e)"); end From 24ed2498ea1be2fc4a4daeda81c9a5371e8ad7ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Thu, 1 Jul 2021 09:16:32 +0200 Subject: [PATCH 21/80] RED-1728: Do not show isHint flag if only false_positive entries are included in redactionlog --- .../v1/server/redaction/service/AnalyzeResponseService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java index eceedae2..80589a8f 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java @@ -10,7 +10,7 @@ import org.springframework.stereotype.Service; public class AnalyzeResponseService { public AnalyzeResult createAnalyzeResponse(String dossierId, String fileId, long duration, int pageCount, RedactionLog redactionLog, RedactionChangeLog redactionChangeLog) { - boolean hasHints = redactionLog.getRedactionLogEntry().stream().anyMatch(RedactionLogEntry::isHint); + boolean hasHints = redactionLog.getRedactionLogEntry().stream().anyMatch(entry -> entry.isHint() && !entry.getType().equals("false_positive")); boolean hasRequests = redactionLog.getRedactionLogEntry() .stream() From 041a3c87ae908fb8e127d4719aa421cbf6c3621e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Mon, 5 Jul 2021 14:28:53 +0200 Subject: [PATCH 22/80] RED-809: Added flag to AnalysisResult to see if a reanalysis or normal analysis was performed --- .../iqser/red/service/redaction/v1/model/AnalyzeResult.java | 2 ++ .../v1/server/redaction/service/ReanalyzeService.java | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeResult.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeResult.java index 1ce9d759..5eadb70f 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeResult.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeResult.java @@ -25,6 +25,8 @@ public class AnalyzeResult { private long rulesVersion; private long legalBasisVersion; + private boolean wasReanalyzed; + } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java index edaac63d..ddc67900 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java @@ -269,8 +269,9 @@ public class ReanalyzeService { redactionLog.getRedactionLogEntry().removeIf(entry -> sectionsToReanalyse.contains(entry.getSectionNumber())); redactionLog.getRedactionLogEntry().addAll(newRedactionLogEntries); - return finalizeAnalysis(analyzeRequest, startTime, redactionLog, text, dictionaryIncrement); - + AnalyzeResult analyzeResult = finalizeAnalysis(analyzeRequest, startTime, redactionLog, text, dictionaryIncrement); + analyzeResult.setWasReanalyzed(true); + return analyzeResult; } From c36a3813c42d37fada16d39479d4205d4890a7b5 Mon Sep 17 00:00:00 2001 From: Timo Date: Wed, 7 Jul 2021 23:05:49 +0300 Subject: [PATCH 23/80] added a preview method that allows us to see how the redaction-log will look like after analysis, this is useful for the UI since we do no longer need to duplicate this code in the UI --- .../v1/resources/RedactionResource.java | 5 +- .../controller/RedactionController.java | 12 +- .../v1/server/redaction/model/Entity.java | 2 +- .../v1/server/redaction/model/Image.java | 2 +- .../server/redaction/model/ReasonHolder.java | 14 + .../service/RedactionLogCreatorService.java | 522 ++++++++++-------- 6 files changed, 335 insertions(+), 222 deletions(-) create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/ReasonHolder.java diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/resources/RedactionResource.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/resources/RedactionResource.java index cdf45fcb..0a6037a6 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/resources/RedactionResource.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/resources/RedactionResource.java @@ -8,8 +8,6 @@ import org.springframework.web.bind.annotation.RequestBody; public interface RedactionResource { - String SERVICE_NAME = "redaction-service-v1"; - String RULE_SET_PARAMETER_NAME = "dossierTemplateId"; String RULE_SET_PATH_VARIABLE = "/{" + RULE_SET_PARAMETER_NAME + "}"; @@ -32,4 +30,7 @@ public interface RedactionResource { @PostMapping(value = "/rules/test", consumes = MediaType.APPLICATION_JSON_VALUE) void testRules(@RequestBody String rules); + @PostMapping(value = "/redaction-log/preview", consumes = MediaType.APPLICATION_JSON_VALUE) + RedactionLog getRedactionLogPreview(@RequestBody RedactionRequest redactionRequest); + } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java index a2e285e4..0149d6a0 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java @@ -3,6 +3,7 @@ package com.iqser.red.service.redaction.v1.server.controller; import com.iqser.red.service.file.management.v1.api.model.FileType; import com.iqser.red.service.redaction.v1.model.AnnotateRequest; import com.iqser.red.service.redaction.v1.model.AnnotateResponse; +import com.iqser.red.service.redaction.v1.model.RedactionLog; import com.iqser.red.service.redaction.v1.model.RedactionRequest; import com.iqser.red.service.redaction.v1.model.RedactionResult; import com.iqser.red.service.redaction.v1.resources.RedactionResource; @@ -12,6 +13,7 @@ import com.iqser.red.service.redaction.v1.server.exception.RedactionException; import com.iqser.red.service.redaction.v1.server.redaction.service.AnnotationService; import com.iqser.red.service.redaction.v1.server.redaction.service.DictionaryService; import com.iqser.red.service.redaction.v1.server.redaction.service.DroolsExecutionService; +import com.iqser.red.service.redaction.v1.server.redaction.service.RedactionLogCreatorService; import com.iqser.red.service.redaction.v1.server.segmentation.PdfSegmentationService; import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService; import com.iqser.red.service.redaction.v1.server.tableextraction.model.AbstractTextContainer; @@ -39,7 +41,7 @@ public class RedactionController implements RedactionResource { private final AnnotationService annotationService; private final PdfSegmentationService pdfSegmentationService; private final RedactionStorageService redactionStorageService; - + private final RedactionLogCreatorService redactionLogCreatorService; public AnnotateResponse annotate(@RequestBody AnnotateRequest annotateRequest) { @@ -155,6 +157,14 @@ public class RedactionController implements RedactionResource { droolsExecutionService.testRules(rules); } + @Override + public RedactionLog getRedactionLogPreview(RedactionRequest redactionRequest) { + + var redactionLog = redactionStorageService.getRedactionLog(redactionRequest.getDossierId(), redactionRequest.getFileId()); + + return redactionLogCreatorService.getRedactionLogPreview(redactionLog, redactionRequest.getDossierTemplateId(), redactionRequest.getManualRedactions()); + } + private RedactionResult convert(PDDocument document, int numberOfPages) throws IOException { diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Entity.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Entity.java index c9fdc711..2ae553db 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Entity.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Entity.java @@ -9,7 +9,7 @@ import java.util.List; @Data @EqualsAndHashCode(onlyExplicitlyIncluded = true) -public class Entity { +public class Entity implements ReasonHolder { private final String word; diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Image.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Image.java index 766d607d..5aab9c7a 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Image.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Image.java @@ -9,7 +9,7 @@ import lombok.NoArgsConstructor; @Builder @NoArgsConstructor @AllArgsConstructor -public class Image { +public class Image implements ReasonHolder { private String type; private RedRectangle2D position; diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/ReasonHolder.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/ReasonHolder.java new file mode 100644 index 00000000..51d9b0fd --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/ReasonHolder.java @@ -0,0 +1,14 @@ +package com.iqser.red.service.redaction.v1.server.redaction.model; + +public interface ReasonHolder { + + String getRedactionReason(); + + void setRedactionReason(String reason); + + boolean isRedaction(); + + void setRedaction(boolean value); + + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java index ba094a48..135d90a2 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java @@ -9,21 +9,17 @@ import com.iqser.red.service.redaction.v1.server.parsing.model.TextPositionSeque import com.iqser.red.service.redaction.v1.server.redaction.model.Entity; import com.iqser.red.service.redaction.v1.server.redaction.model.EntityPositionSequence; import com.iqser.red.service.redaction.v1.server.redaction.model.Image; +import com.iqser.red.service.redaction.v1.server.redaction.model.ReasonHolder; import com.iqser.red.service.redaction.v1.server.redaction.utils.IdBuilder; import com.iqser.red.service.redaction.v1.server.tableextraction.model.AbstractTextContainer; import com.iqser.red.service.redaction.v1.server.tableextraction.model.Cell; import com.iqser.red.service.redaction.v1.server.tableextraction.model.Table; - import lombok.RequiredArgsConstructor; - import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; @Service @@ -71,7 +67,7 @@ public class RedactionLogCreatorService { RedactionLogEntry redactionLogEntry = RedactionLogEntry.builder() .id(id) - .color(getColorForImage(image, dossierTemplateId, false)) + .color(getColorForImage(image.getType(), dossierTemplateId, false, image.isRedaction())) .isImage(true) .type(image.getType()) .redacted(image.isRedaction()) @@ -89,117 +85,7 @@ public class RedactionLogCreatorService { .section(image.getSection()) .build(); - if (manualRedactions != null && !manualRedactions.getImageRecategorizations().isEmpty()) { - for (ManualImageRecategorization recategorization : manualRedactions.getImageRecategorizations()) { - if (recategorization.getId().equals(id)) { - String manualOverrideReason = null; - if (recategorization.getStatus().equals(Status.APPROVED)) { - image.setType(recategorization.getType()); - image.setRedaction(recategorization.isRedacted()); - image.setLegalBasis(recategorization.getLegalBasis()); - redactionLogEntry.setType(recategorization.getType()); - redactionLogEntry.setHint(dictionaryService.isHint(recategorization.getType(), dossierTemplateId)); - redactionLogEntry.setRedacted(recategorization.isRedacted()); - redactionLogEntry.setStatus(Status.APPROVED); - redactionLogEntry.setLegalBasis(recategorization.getLegalBasis()); - manualOverrideReason = image.getRedactionReason() + ", recategorized by manual override"; - redactionLogEntry.setColor(getColorForImage(image, dossierTemplateId, false)); - } else if (recategorization.getStatus().equals(Status.REQUESTED)) { - manualOverrideReason = image.getRedactionReason() + ", requested to recategorize"; - redactionLogEntry.setStatus(Status.REQUESTED); - redactionLogEntry.setColor(getColorForImage(image, dossierTemplateId, true)); - redactionLogEntry.setRecategorizationType(recategorization.getType()); - } else { - redactionLogEntry.setStatus(Status.DECLINED); - } - - image.setRedactionReason(manualOverrideReason != null ? manualOverrideReason : image.getRedactionReason()); - redactionLogEntry.setReason(manualOverrideReason); - redactionLogEntry.setManual(true); - redactionLogEntry.setManualRedactionType(ManualRedactionType.RECATEGORIZE); - } - } - } - - if (manualRedactions != null && !manualRedactions.getIdsToRemove().isEmpty()) { - for (IdRemoval manualRemoval : manualRedactions.getIdsToRemove()) { - if (manualRemoval.getId().equals(id)) { - String manualOverrideReason = null; - if (manualRemoval.getStatus().equals(Status.APPROVED)) { - image.setRedaction(false); - redactionLogEntry.setRedacted(false); - redactionLogEntry.setStatus(Status.APPROVED); - manualOverrideReason = image.getRedactionReason() + ", removed by manual override"; - redactionLogEntry.setColor(getColorForImage(image, dossierTemplateId, false)); - } else if (manualRemoval.getStatus().equals(Status.REQUESTED)) { - manualOverrideReason = image.getRedactionReason() + ", requested to remove"; - redactionLogEntry.setStatus(Status.REQUESTED); - redactionLogEntry.setColor(getColorForImage(image, dossierTemplateId, true)); - } else { - redactionLogEntry.setStatus(Status.DECLINED); - } - - image.setRedactionReason(manualOverrideReason != null ? manualOverrideReason : image.getRedactionReason()); - redactionLogEntry.setReason(manualOverrideReason); - redactionLogEntry.setManual(true); - redactionLogEntry.setManualRedactionType(ManualRedactionType.REMOVE); - } - } - } - - if (manualRedactions != null && !manualRedactions.getForceRedacts().isEmpty()) { - for (ManualForceRedact manualForceRedact : manualRedactions.getForceRedacts()) { - if (manualForceRedact.getId().equals(id)) { - String manualOverrideReason = null; - if (manualForceRedact.getStatus().equals(Status.APPROVED)) { - image.setRedaction(true); - redactionLogEntry.setRedacted(true); - redactionLogEntry.setStatus(Status.APPROVED); - redactionLogEntry.setColor(getColorForImage(image, dossierTemplateId, false)); - manualOverrideReason = image.getRedactionReason() + ", forced by manual override"; - redactionLogEntry.setLegalBasis(manualForceRedact.getLegalBasis()); - } else if (manualForceRedact.getStatus().equals(Status.REQUESTED)) { - manualOverrideReason = image.getRedactionReason() + ", requested to force redact"; - redactionLogEntry.setStatus(Status.REQUESTED); - redactionLogEntry.setColor(getColorForImage(image, dossierTemplateId, true)); - redactionLogEntry.setLegalBasis(manualForceRedact.getLegalBasis()); - } else { - redactionLogEntry.setStatus(Status.DECLINED); - } - - image.setRedactionReason(manualOverrideReason != null ? manualOverrideReason : image.getRedactionReason()); - redactionLogEntry.setReason(manualOverrideReason); - redactionLogEntry.setManual(true); - redactionLogEntry.setManualRedactionType(ManualRedactionType.FORCE_REDACT); - } - } - } - - - if (manualRedactions != null && !manualRedactions.getManualLegalBasisChanges().isEmpty()) { - for (ManualLegalBasisChange manualLegalBasisChange : manualRedactions.getManualLegalBasisChanges()) { - if (manualLegalBasisChange.getId().equals(id)) { - String manualOverrideReason = null; - if (manualLegalBasisChange.getStatus().equals(Status.APPROVED)) { - redactionLogEntry.setStatus(Status.APPROVED); - manualOverrideReason = image.getRedactionReason() + ", legal basis was manually changed"; - redactionLogEntry.setLegalBasis(manualLegalBasisChange.getLegalBasis()); - } else if (manualLegalBasisChange.getStatus().equals(Status.REQUESTED)) { - manualOverrideReason = image.getRedactionReason() + ", legal basis change requested"; - redactionLogEntry.setStatus(Status.REQUESTED); - redactionLogEntry.setColor(getColorForImage(image, dossierTemplateId, true)); - redactionLogEntry.setLegalBasisChangeValue(manualLegalBasisChange.getLegalBasis()); - } else { - redactionLogEntry.setStatus(Status.DECLINED); - } - - image.setRedactionReason(manualOverrideReason != null ? manualOverrideReason : image.getRedactionReason()); - redactionLogEntry.setReason(manualOverrideReason); - redactionLogEntry.setManual(true); - redactionLogEntry.setManualRedactionType(ManualRedactionType.LEGAL_BASIS_CHANGE); - } - } - } + processImageEntry(manualRedactions, dossierTemplateId, image, redactionLogEntry); redactionLogEntities.add(redactionLogEntry); } @@ -207,6 +93,118 @@ public class RedactionLogCreatorService { return redactionLogEntities; } + private void processImageEntry(ManualRedactions manualRedactions, String dossierTemplateId, ReasonHolder image, RedactionLogEntry redactionLogEntry) { + if (manualRedactions != null && !manualRedactions.getImageRecategorizations().isEmpty()) { + for (ManualImageRecategorization recategorization : manualRedactions.getImageRecategorizations()) { + if (recategorization.getId().equals(redactionLogEntry.getId())) { + String manualOverrideReason = null; + if (recategorization.getStatus().equals(Status.APPROVED)) { + image.setRedaction(recategorization.isRedacted()); + redactionLogEntry.setType(recategorization.getType()); + redactionLogEntry.setHint(dictionaryService.isHint(recategorization.getType(), dossierTemplateId)); + redactionLogEntry.setRedacted(recategorization.isRedacted()); + redactionLogEntry.setStatus(Status.APPROVED); + redactionLogEntry.setLegalBasis(recategorization.getLegalBasis()); + manualOverrideReason = mergeReasonIfNecessary(image.getRedactionReason(), ", recategorized by manual override"); + redactionLogEntry.setColor(getColorForImage(redactionLogEntry.getType(), dossierTemplateId, false, redactionLogEntry.isRedacted())); + } else if (recategorization.getStatus().equals(Status.REQUESTED)) { + manualOverrideReason = mergeReasonIfNecessary(image.getRedactionReason(), ", requested to recategorize"); + redactionLogEntry.setStatus(Status.REQUESTED); + redactionLogEntry.setColor(getColorForImage(redactionLogEntry.getType(), dossierTemplateId, true, redactionLogEntry.isRedacted())); + redactionLogEntry.setRecategorizationType(recategorization.getType()); + } else { + redactionLogEntry.setStatus(Status.DECLINED); + } + + image.setRedactionReason(manualOverrideReason != null ? manualOverrideReason : image.getRedactionReason()); + redactionLogEntry.setReason(manualOverrideReason); + redactionLogEntry.setManual(true); + redactionLogEntry.setManualRedactionType(ManualRedactionType.RECATEGORIZE); + } + } + } + + if (manualRedactions != null && !manualRedactions.getIdsToRemove().isEmpty()) { + for (IdRemoval manualRemoval : manualRedactions.getIdsToRemove()) { + if (manualRemoval.getId().equals(redactionLogEntry.getId())) { + String manualOverrideReason = null; + if (manualRemoval.getStatus().equals(Status.APPROVED)) { + image.setRedaction(false); + redactionLogEntry.setRedacted(false); + redactionLogEntry.setStatus(Status.APPROVED); + manualOverrideReason = mergeReasonIfNecessary(image.getRedactionReason(), ", removed by manual override"); + redactionLogEntry.setColor(getColorForImage(redactionLogEntry.getType(), dossierTemplateId, false, redactionLogEntry.isRedacted())); + } else if (manualRemoval.getStatus().equals(Status.REQUESTED)) { + manualOverrideReason = mergeReasonIfNecessary(image.getRedactionReason(), ", requested to remove"); + redactionLogEntry.setStatus(Status.REQUESTED); + redactionLogEntry.setColor(getColorForImage(redactionLogEntry.getType(), dossierTemplateId, true, redactionLogEntry.isRedacted())); + } else { + redactionLogEntry.setStatus(Status.DECLINED); + } + + image.setRedactionReason(manualOverrideReason != null ? manualOverrideReason : image.getRedactionReason()); + redactionLogEntry.setReason(manualOverrideReason); + redactionLogEntry.setManual(true); + redactionLogEntry.setManualRedactionType(ManualRedactionType.REMOVE); + } + } + } + + if (manualRedactions != null && !manualRedactions.getForceRedacts().isEmpty()) { + for (ManualForceRedact manualForceRedact : manualRedactions.getForceRedacts()) { + if (manualForceRedact.getId().equals(redactionLogEntry.getId())) { + String manualOverrideReason = null; + if (manualForceRedact.getStatus().equals(Status.APPROVED)) { + image.setRedaction(true); + redactionLogEntry.setRedacted(true); + redactionLogEntry.setStatus(Status.APPROVED); + redactionLogEntry.setColor(getColorForImage(redactionLogEntry.getType(), dossierTemplateId, false, redactionLogEntry.isRedacted())); + manualOverrideReason = mergeReasonIfNecessary(image.getRedactionReason(), ", forced by manual override"); + redactionLogEntry.setLegalBasis(manualForceRedact.getLegalBasis()); + } else if (manualForceRedact.getStatus().equals(Status.REQUESTED)) { + manualOverrideReason = mergeReasonIfNecessary(image.getRedactionReason(), ", requested to force redact"); + redactionLogEntry.setStatus(Status.REQUESTED); + redactionLogEntry.setColor(getColorForImage(redactionLogEntry.getType(), dossierTemplateId, true, redactionLogEntry.isRedacted())); + redactionLogEntry.setLegalBasis(manualForceRedact.getLegalBasis()); + } else { + redactionLogEntry.setStatus(Status.DECLINED); + } + + image.setRedactionReason(manualOverrideReason != null ? manualOverrideReason : image.getRedactionReason()); + redactionLogEntry.setReason(manualOverrideReason); + redactionLogEntry.setManual(true); + redactionLogEntry.setManualRedactionType(ManualRedactionType.FORCE_REDACT); + } + } + } + + + if (manualRedactions != null && !manualRedactions.getManualLegalBasisChanges().isEmpty()) { + for (ManualLegalBasisChange manualLegalBasisChange : manualRedactions.getManualLegalBasisChanges()) { + if (manualLegalBasisChange.getId().equals(redactionLogEntry.getId())) { + String manualOverrideReason = null; + if (manualLegalBasisChange.getStatus().equals(Status.APPROVED)) { + redactionLogEntry.setStatus(Status.APPROVED); + manualOverrideReason = mergeReasonIfNecessary(image.getRedactionReason(), ", legal basis was manually changed"); + redactionLogEntry.setLegalBasis(manualLegalBasisChange.getLegalBasis()); + } else if (manualLegalBasisChange.getStatus().equals(Status.REQUESTED)) { + manualOverrideReason = mergeReasonIfNecessary(image.getRedactionReason(), ", legal basis change requested"); + redactionLogEntry.setStatus(Status.REQUESTED); + redactionLogEntry.setColor(getColorForImage(redactionLogEntry.getType(), dossierTemplateId, true, redactionLogEntry.isRedacted())); + redactionLogEntry.setLegalBasisChangeValue(manualLegalBasisChange.getLegalBasis()); + } else { + redactionLogEntry.setStatus(Status.DECLINED); + } + + image.setRedactionReason(manualOverrideReason != null ? manualOverrideReason : image.getRedactionReason()); + redactionLogEntry.setReason(manualOverrideReason); + redactionLogEntry.setManual(true); + redactionLogEntry.setManualRedactionType(ManualRedactionType.LEGAL_BASIS_CHANGE); + } + } + } + } + private Set getManualRedactionPages(ManualRedactions manualRedactions) { @@ -236,7 +234,6 @@ public class RedactionLogCreatorService { entityLoop: for (Entity entity : entities.get(page)) { - List comments = null; for (EntityPositionSequence entityPositionSequence : entity.getPositionSequences()) { @@ -249,85 +246,8 @@ public class RedactionLogCreatorService { processedIds.add(entityPositionSequence.getId()); } - if (manualRedactions != null && !manualRedactions.getIdsToRemove().isEmpty()) { - for (IdRemoval manualRemoval : manualRedactions.getIdsToRemove()) { - if (manualRemoval.getId().equals(entityPositionSequence.getId())) { - comments = manualRedactions.getComments().get(manualRemoval.getId()); - String manualOverrideReason = null; - if (manualRemoval.getStatus().equals(Status.APPROVED)) { - entity.setRedaction(false); - redactionLogEntry.setRedacted(false); - redactionLogEntry.setStatus(Status.APPROVED); - manualOverrideReason = entity.getRedactionReason() + ", removed by manual override"; - redactionLogEntry.setColor(getColor(entity, dossierTemplateId, false)); - } else if (manualRemoval.getStatus().equals(Status.REQUESTED)) { - manualOverrideReason = entity.getRedactionReason() + ", requested to remove"; - redactionLogEntry.setStatus(Status.REQUESTED); - redactionLogEntry.setColor(getColor(entity, dossierTemplateId, true)); - } else { - redactionLogEntry.setStatus(Status.DECLINED); - } - - entity.setRedactionReason(manualOverrideReason != null ? manualOverrideReason : entity.getRedactionReason()); - redactionLogEntry.setReason(manualOverrideReason); - redactionLogEntry.setManual(true); - redactionLogEntry.setManualRedactionType(ManualRedactionType.REMOVE); - } - } - } - - if (manualRedactions != null && !manualRedactions.getForceRedacts().isEmpty()) { - for (ManualForceRedact manualForceRedact : manualRedactions.getForceRedacts()) { - if (manualForceRedact.getId().equals(entityPositionSequence.getId())) { - String manualOverrideReason = null; - if (manualForceRedact.getStatus().equals(Status.APPROVED)) { - entity.setRedaction(true); - redactionLogEntry.setRedacted(true); - redactionLogEntry.setStatus(Status.APPROVED); - redactionLogEntry.setColor(getColor(entity, dossierTemplateId, false)); - manualOverrideReason = entity.getRedactionReason() + ", forced by manual override"; - redactionLogEntry.setLegalBasis(manualForceRedact.getLegalBasis()); - } else if (manualForceRedact.getStatus().equals(Status.REQUESTED)) { - manualOverrideReason = entity.getRedactionReason() + ", requested to force redact"; - redactionLogEntry.setStatus(Status.REQUESTED); - redactionLogEntry.setColor(getColor(entity, dossierTemplateId, true)); - redactionLogEntry.setLegalBasis(manualForceRedact.getLegalBasis()); - } else { - redactionLogEntry.setStatus(Status.DECLINED); - } - - entity.setRedactionReason(manualOverrideReason != null ? manualOverrideReason : entity.getRedactionReason()); - redactionLogEntry.setReason(manualOverrideReason); - redactionLogEntry.setManual(true); - redactionLogEntry.setManualRedactionType(ManualRedactionType.FORCE_REDACT); - } - } - } - - if (manualRedactions != null && !manualRedactions.getManualLegalBasisChanges().isEmpty()) { - for (ManualLegalBasisChange manualLegalBasisChange : manualRedactions.getManualLegalBasisChanges()) { - if (manualLegalBasisChange.getId().equals(entityPositionSequence.getId())) { - String manualOverrideReason = null; - if (manualLegalBasisChange.getStatus().equals(Status.APPROVED)) { - redactionLogEntry.setStatus(Status.APPROVED); - manualOverrideReason = entity.getRedactionReason() + ", legal basis was manually changed"; - redactionLogEntry.setLegalBasis(manualLegalBasisChange.getLegalBasis()); - } else if (manualLegalBasisChange.getStatus().equals(Status.REQUESTED)) { - manualOverrideReason = entity.getRedactionReason() + ", legal basis change requested"; - redactionLogEntry.setStatus(Status.REQUESTED); - redactionLogEntry.setColor(getColor(entity, dossierTemplateId, true)); - redactionLogEntry.setLegalBasisChangeValue(manualLegalBasisChange.getLegalBasis()); - } else { - redactionLogEntry.setStatus(Status.DECLINED); - } - - entity.setRedactionReason(manualOverrideReason != null ? manualOverrideReason : entity.getRedactionReason()); - redactionLogEntry.setReason(manualOverrideReason); - redactionLogEntry.setManual(true); - redactionLogEntry.setManualRedactionType(ManualRedactionType.LEGAL_BASIS_CHANGE); - } - } - } + redactionLogEntry.setId(entityPositionSequence.getId()); + processRedactionLogEntry(manualRedactions, dossierTemplateId, redactionLogEntry, entity); if (CollectionUtils.isNotEmpty(entityPositionSequence.getSequences())) { List rectanglesPerLine = getRectanglesPerLine(entityPositionSequence.getSequences() @@ -335,16 +255,11 @@ public class RedactionLogCreatorService { .flatMap(seq -> seq.getTextPositions().stream()) .collect(Collectors.toList()), page); - if (manualRedactions != null) { - comments = manualRedactions.getComments().get(entityPositionSequence.getId()); - } - redactionLogEntry.setComments(comments); redactionLogEntry.getPositions().addAll(rectanglesPerLine); } - redactionLogEntry.setId(entityPositionSequence.getId()); // FIXME ids should never be null. Figure out why this happens. if (redactionLogEntry.getId() != null) { @@ -356,6 +271,109 @@ public class RedactionLogCreatorService { return redactionLogEntities; } + private void processRedactionLogEntry(ManualRedactions manualRedactions, String dossierTemplateId, RedactionLogEntry redactionLogEntry, ReasonHolder reasonHolder) { + + List comments = null; + + if (manualRedactions != null && !manualRedactions.getIdsToRemove().isEmpty()) { + for (IdRemoval manualRemoval : manualRedactions.getIdsToRemove()) { + if (manualRemoval.getId().equals(redactionLogEntry.getId())) { + comments = manualRedactions.getComments().get(manualRemoval.getId()); + String manualOverrideReason = null; + if (manualRemoval.getStatus().equals(Status.APPROVED)) { + reasonHolder.setRedaction(false); + redactionLogEntry.setRedacted(false); + redactionLogEntry.setStatus(Status.APPROVED); + manualOverrideReason = mergeReasonIfNecessary(reasonHolder.getRedactionReason(), ", removed by manual override"); + redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, false, redactionLogEntry.isRedacted())); + } else if (manualRemoval.getStatus().equals(Status.REQUESTED)) { + manualOverrideReason = mergeReasonIfNecessary(reasonHolder.getRedactionReason(), ", requested to remove"); + redactionLogEntry.setStatus(Status.REQUESTED); + redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, true, redactionLogEntry.isRedacted())); + } else { + redactionLogEntry.setStatus(Status.DECLINED); + } + + reasonHolder.setRedactionReason(manualOverrideReason != null ? manualOverrideReason : reasonHolder.getRedactionReason()); + redactionLogEntry.setReason(manualOverrideReason); + redactionLogEntry.setManual(true); + redactionLogEntry.setManualRedactionType(ManualRedactionType.REMOVE); + } + } + } + + if (manualRedactions != null && !manualRedactions.getForceRedacts().isEmpty()) { + for (ManualForceRedact manualForceRedact : manualRedactions.getForceRedacts()) { + if (manualForceRedact.getId().equals(redactionLogEntry.getId())) { + String manualOverrideReason = null; + if (manualForceRedact.getStatus().equals(Status.APPROVED)) { + reasonHolder.setRedaction(true); + redactionLogEntry.setRedacted(true); + redactionLogEntry.setStatus(Status.APPROVED); + redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, false, redactionLogEntry.isRedacted())); + manualOverrideReason = mergeReasonIfNecessary(reasonHolder.getRedactionReason(), ", forced by manual override"); + redactionLogEntry.setLegalBasis(manualForceRedact.getLegalBasis()); + } else if (manualForceRedact.getStatus().equals(Status.REQUESTED)) { + manualOverrideReason = mergeReasonIfNecessary(reasonHolder.getRedactionReason(), ", requested to force redact"); + redactionLogEntry.setStatus(Status.REQUESTED); + redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, true, redactionLogEntry.isRedacted())); + redactionLogEntry.setLegalBasis(manualForceRedact.getLegalBasis()); + } else { + redactionLogEntry.setStatus(Status.DECLINED); + } + + reasonHolder.setRedactionReason(manualOverrideReason != null ? manualOverrideReason : reasonHolder.getRedactionReason()); + redactionLogEntry.setReason(manualOverrideReason); + redactionLogEntry.setManual(true); + redactionLogEntry.setManualRedactionType(ManualRedactionType.FORCE_REDACT); + } + } + } + + if (manualRedactions != null && !manualRedactions.getManualLegalBasisChanges().isEmpty()) { + for (ManualLegalBasisChange manualLegalBasisChange : manualRedactions.getManualLegalBasisChanges()) { + if (manualLegalBasisChange.getId().equals(redactionLogEntry.getId())) { + String manualOverrideReason = null; + if (manualLegalBasisChange.getStatus().equals(Status.APPROVED)) { + redactionLogEntry.setStatus(Status.APPROVED); + manualOverrideReason = mergeReasonIfNecessary(reasonHolder.getRedactionReason(), ", legal basis was manually changed"); + redactionLogEntry.setLegalBasis(manualLegalBasisChange.getLegalBasis()); + } else if (manualLegalBasisChange.getStatus().equals(Status.REQUESTED)) { + manualOverrideReason = mergeReasonIfNecessary(reasonHolder.getRedactionReason(), ", legal basis change requested"); + redactionLogEntry.setStatus(Status.REQUESTED); + redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, true, redactionLogEntry.isRedacted())); + redactionLogEntry.setLegalBasisChangeValue(manualLegalBasisChange.getLegalBasis()); + } else { + redactionLogEntry.setStatus(Status.DECLINED); + } + + reasonHolder.setRedactionReason(manualOverrideReason != null ? manualOverrideReason : reasonHolder.getRedactionReason()); + redactionLogEntry.setReason(manualOverrideReason); + redactionLogEntry.setManual(true); + redactionLogEntry.setManualRedactionType(ManualRedactionType.LEGAL_BASIS_CHANGE); + } + } + } + + + if (manualRedactions != null) { + comments = manualRedactions.getComments().get(redactionLogEntry.getId()); + } + + redactionLogEntry.setComments(comments); + } + + private String mergeReasonIfNecessary(String currentReason, String addition) { + if (currentReason != null) { + if (!currentReason.contains(addition)) { + currentReason += addition; + } + return currentReason; + } else { + return ""; + } + } + private List getRectanglesPerLine(List textPositions, int page) { @@ -450,14 +468,14 @@ public class RedactionLogCreatorService { private RedactionLogEntry createRedactionLogEntry(Entity entity, String dossierTemplateId) { return RedactionLogEntry.builder() - .color(getColor(entity, dossierTemplateId, false)) + .color(getColor(entity.getType(), dossierTemplateId, false, entity.isRedaction())) .reason(entity.getRedactionReason()) .legalBasis(entity.getLegalBasis()) .value(entity.getWord()) .type(entity.getType()) .redacted(entity.isRedaction()) - .isHint(isHint(entity, dossierTemplateId)) - .isRecommendation(isRecommendation(entity, dossierTemplateId)) + .isHint(isHint(entity.getType(), dossierTemplateId)) + .isRecommendation(isRecommendation(entity.getType(), dossierTemplateId)) .section(entity.getHeadline()) .sectionNumber(entity.getSectionNumber()) .matchedRule(entity.getMatchedRule()) @@ -471,15 +489,15 @@ public class RedactionLogCreatorService { } - private float[] getColor(Entity entity, String dossierTemplateId, boolean requestedToRemove) { + private float[] getColor(String type, String dossierTemplateId, boolean requestedToRemove, boolean isRedaction) { if (requestedToRemove) { return dictionaryService.getRequestRemoveColor(dossierTemplateId); } - if (!entity.isRedaction() && !isHint(entity, dossierTemplateId)) { + if (!isRedaction && !isHint(type, dossierTemplateId)) { return dictionaryService.getNotRedactedColor(dossierTemplateId); } - return dictionaryService.getColor(entity.getType(), dossierTemplateId); + return dictionaryService.getColor(type, dossierTemplateId); } @@ -500,27 +518,27 @@ public class RedactionLogCreatorService { } - private float[] getColorForImage(Image image, String dossierTemplateId, boolean requestedToRemove) { + private float[] getColorForImage(String type, String dossierTemplateId, boolean requestedToRemove, boolean isRedaction) { if (requestedToRemove) { return dictionaryService.getRequestRemoveColor(dossierTemplateId); } - if (!image.isRedaction() && !dictionaryService.isHint(image.getType(), dossierTemplateId)) { + if (!isRedaction && !dictionaryService.isHint(type, dossierTemplateId)) { return dictionaryService.getNotRedactedColor(dossierTemplateId); } - return dictionaryService.getColor(image.getType(), dossierTemplateId); + return dictionaryService.getColor(type, dossierTemplateId); } - private boolean isHint(Entity entity, String dossierTemplateId) { + private boolean isHint(String type, String dossierTemplateId) { - return dictionaryService.isHint(entity.getType(), dossierTemplateId); + return dictionaryService.isHint(type, dossierTemplateId); } - private boolean isRecommendation(Entity entity, String dossierTemplateId) { + private boolean isRecommendation(String type, String dossierTemplateId) { - return dictionaryService.isRecommendation(entity.getType(), dossierTemplateId); + return dictionaryService.isRecommendation(type, dossierTemplateId); } @@ -567,4 +585,74 @@ public class RedactionLogCreatorService { } } + public RedactionLog getRedactionLogPreview(RedactionLog redactionLog, String dossierTemplateId, ManualRedactions manualRedactions) { + + + var manualRedactionPages = getManualRedactionPages(manualRedactions); + + // generate all manual entries + var manualRedactionLogEntries = new HashMap(); + for (var page : manualRedactionPages) { + + var pageEntries = addManualAddEntries(manualRedactions.getEntriesToAdd(), manualRedactions.getComments(), page, dossierTemplateId); + + for (var entry : pageEntries) { + manualRedactionLogEntries.put(entry.getId(), entry); + } + } + + for (var manualEntry : manualRedactionLogEntries.values()) { + var existingEntry = redactionLog.getRedactionLogEntry().stream().filter(e -> e.getId().equals(manualEntry.getId())).findAny(); + if (existingEntry.isPresent()) { + // if it has already been processed of sorts, update it + BeanUtils.copyProperties(manualEntry, existingEntry.get()); + } else { + // not yet in the redaction-log - add it + redactionLog.getRedactionLogEntry().add(manualEntry); + } + } + + + for (RedactionLogEntry entry : redactionLog.getRedactionLogEntry()) { + + var reasonHolder = new PreviewReasonHolder(entry); + + if (entry.isImage()) { + processImageEntry(manualRedactions, dossierTemplateId, reasonHolder, entry); + } + + processRedactionLogEntry(manualRedactions, dossierTemplateId, entry, reasonHolder); + } + + return redactionLog; + } + + public static class PreviewReasonHolder implements ReasonHolder { + + private final RedactionLogEntry entry; + + public PreviewReasonHolder(RedactionLogEntry entry) { + this.entry = entry; + } + + @Override + public String getRedactionReason() { + return entry.getReason(); + } + + @Override + public void setRedactionReason(String reason) { + entry.setReason(reason); + } + + @Override + public boolean isRedaction() { + return entry.isRedacted(); + } + + @Override + public void setRedaction(boolean value) { + entry.setRedacted(value); + } + } } From 9225b9959ded9768c32ed5e23b50e2428117952f Mon Sep 17 00:00:00 2001 From: Timo Date: Wed, 7 Jul 2021 23:17:03 +0300 Subject: [PATCH 24/80] updated platform dep version and fixed pmd issue --- redaction-service-v1/pom.xml | 4 ++-- .../server/redaction/service/RedactionLogCreatorService.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/redaction-service-v1/pom.xml b/redaction-service-v1/pom.xml index ee796600..eddd4fbb 100644 --- a/redaction-service-v1/pom.xml +++ b/redaction-service-v1/pom.xml @@ -5,7 +5,7 @@ platform-dependency com.iqser.red - 1.1.2 + 1.1.3 4.0.0 @@ -32,7 +32,7 @@ com.iqser.red platform-commons-dependency - 1.3.1 + 1.3.6 import pom diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java index 135d90a2..5c5e1e70 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java @@ -366,7 +366,7 @@ public class RedactionLogCreatorService { private String mergeReasonIfNecessary(String currentReason, String addition) { if (currentReason != null) { if (!currentReason.contains(addition)) { - currentReason += addition; + return currentReason + addition; } return currentReason; } else { From abb48d46f95ab218c790dece094a44b8e9ccf86c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Thu, 8 Jul 2021 11:31:27 +0200 Subject: [PATCH 25/80] RED-1755: Fixed hasHints flag at excluded pages --- .../redaction/service/AnalyzeResponseService.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java index 80589a8f..ff772521 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java @@ -4,13 +4,19 @@ import com.iqser.red.service.redaction.v1.model.AnalyzeResult; import com.iqser.red.service.redaction.v1.model.RedactionChangeLog; import com.iqser.red.service.redaction.v1.model.RedactionLog; import com.iqser.red.service.redaction.v1.model.RedactionLogEntry; + import org.springframework.stereotype.Service; @Service public class AnalyzeResponseService { - public AnalyzeResult createAnalyzeResponse(String dossierId, String fileId, long duration, int pageCount, RedactionLog redactionLog, RedactionChangeLog redactionChangeLog) { - boolean hasHints = redactionLog.getRedactionLogEntry().stream().anyMatch(entry -> entry.isHint() && !entry.getType().equals("false_positive")); + public AnalyzeResult createAnalyzeResponse(String dossierId, String fileId, long duration, int pageCount, + RedactionLog redactionLog, RedactionChangeLog redactionChangeLog) { + + boolean hasHints = redactionLog.getRedactionLogEntry() + .stream() + .filter(entry -> !entry.isExcluded()) + .anyMatch(entry -> entry.isHint() && !entry.getType().equals("false_positive")); boolean hasRequests = redactionLog.getRedactionLogEntry() .stream() @@ -31,7 +37,9 @@ public class AnalyzeResponseService { boolean hasUpdates = redactionChangeLog != null && redactionChangeLog.getRedactionLogEntry() != null && !redactionChangeLog .getRedactionLogEntry() - .isEmpty() && redactionChangeLog.getRedactionLogEntry().stream().anyMatch(entry -> !entry.getType().equals("false_positive")); + .isEmpty() && redactionChangeLog.getRedactionLogEntry() + .stream() + .anyMatch(entry -> !entry.getType().equals("false_positive")); return AnalyzeResult.builder() .dossierId(dossierId) @@ -49,4 +57,5 @@ public class AnalyzeResponseService { .dossierDictionaryVersion(redactionLog.getDossierDictionaryVersion()) .build(); } + } From 6741b8a596fcb7a014fdbca371329f1cf46d1fd7 Mon Sep 17 00:00:00 2001 From: Timo Date: Mon, 12 Jul 2021 10:33:30 +0300 Subject: [PATCH 26/80] added dictionary entries --- .../service/RedactionLogCreatorService.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java index 5c5e1e70..c2a32f4c 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java @@ -624,9 +624,27 @@ public class RedactionLogCreatorService { processRedactionLogEntry(manualRedactions, dossierTemplateId, entry, reasonHolder); } + + handleAddToDictionary(redactionLog, manualRedactions, dossierTemplateId); + return redactionLog; } + + private void handleAddToDictionary(RedactionLog redactionLog, ManualRedactions manualRedactions, String dossierTemplateId) { + + for (var manualRedaction : manualRedactions.getEntriesToAdd()) { + + if (manualRedaction.isAddToDictionary() || manualRedaction.isAddToDossierDictionary()) { + var redactionLogEntry = createRedactionLogEntry(manualRedaction, manualRedaction.getId(), dossierTemplateId); + redactionLogEntry.setDictionaryEntry(manualRedaction.isAddToDossierDictionary()); + redactionLogEntry.setDossierDictionaryEntry(manualRedaction.isAddToDossierDictionary()); + + redactionLog.getRedactionLogEntry().add(redactionLogEntry); + } + } + } + public static class PreviewReasonHolder implements ReasonHolder { private final RedactionLogEntry entry; From cc1c3122bb3a4704a8ca1ff858a35f335d7d058b Mon Sep 17 00:00:00 2001 From: Timo Date: Mon, 12 Jul 2021 10:44:42 +0300 Subject: [PATCH 27/80] fixed dictionary add corner case --- .../redaction/service/RedactionLogCreatorService.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java index c2a32f4c..03a5333b 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java @@ -640,7 +640,15 @@ public class RedactionLogCreatorService { redactionLogEntry.setDictionaryEntry(manualRedaction.isAddToDossierDictionary()); redactionLogEntry.setDossierDictionaryEntry(manualRedaction.isAddToDossierDictionary()); - redactionLog.getRedactionLogEntry().add(redactionLogEntry); + var found = redactionLog.getRedactionLogEntry().stream().filter(r -> r.getId().equalsIgnoreCase(redactionLogEntry.getId())).findAny(); + if (found.isPresent()) { + found.get().setDictionaryEntry(manualRedaction.isAddToDossierDictionary()); + found.get().setDossierDictionaryEntry(manualRedaction.isAddToDossierDictionary()); + } else { + redactionLog.getRedactionLogEntry().add(redactionLogEntry); + } + + } } } From f713ea2655bd50566e29cc4a96181c7d272b4668 Mon Sep 17 00:00:00 2001 From: Timo Date: Mon, 12 Jul 2021 11:16:48 +0300 Subject: [PATCH 28/80] added some logging --- .../redaction/v1/server/controller/RedactionController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java index 0149d6a0..ace46234 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java @@ -160,6 +160,7 @@ public class RedactionController implements RedactionResource { @Override public RedactionLog getRedactionLogPreview(RedactionRequest redactionRequest) { + log.info("Requested preview for: {}", redactionRequest); var redactionLog = redactionStorageService.getRedactionLog(redactionRequest.getDossierId(), redactionRequest.getFileId()); return redactionLogCreatorService.getRedactionLogPreview(redactionLog, redactionRequest.getDossierTemplateId(), redactionRequest.getManualRedactions()); From 79660f1623056f6cc592d48c76fba5c2f08dd304 Mon Sep 17 00:00:00 2001 From: Timo Date: Mon, 12 Jul 2021 11:18:13 +0300 Subject: [PATCH 29/80] added some logging --- .../redaction/v1/server/redaction/service/DictionaryService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java index 0518eb3f..71cd08b2 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java @@ -163,6 +163,7 @@ public class DictionaryService { public float[] getColor(String type, String dossierTemplateId) { + log.info("requested : {} / {}",type,dossierTemplateId); DictionaryModel model = dictionariesByDossierTemplate.get(dossierTemplateId).getLocalAccessMap().get(type); if (model != null) { return model.getColor(); From 3517841d24bc69028391043157d9fcf142600ddf Mon Sep 17 00:00:00 2001 From: Timo Date: Mon, 12 Jul 2021 11:43:24 +0300 Subject: [PATCH 30/80] positions for dict entries --- .../v1/server/redaction/service/RedactionLogCreatorService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java index 03a5333b..a83d3751 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java @@ -639,11 +639,13 @@ public class RedactionLogCreatorService { var redactionLogEntry = createRedactionLogEntry(manualRedaction, manualRedaction.getId(), dossierTemplateId); redactionLogEntry.setDictionaryEntry(manualRedaction.isAddToDossierDictionary()); redactionLogEntry.setDossierDictionaryEntry(manualRedaction.isAddToDossierDictionary()); + redactionLogEntry.setPositions(manualRedaction.getPositions()); var found = redactionLog.getRedactionLogEntry().stream().filter(r -> r.getId().equalsIgnoreCase(redactionLogEntry.getId())).findAny(); if (found.isPresent()) { found.get().setDictionaryEntry(manualRedaction.isAddToDossierDictionary()); found.get().setDossierDictionaryEntry(manualRedaction.isAddToDossierDictionary()); + found.get().setPositions(manualRedaction.getPositions()); } else { redactionLog.getRedactionLogEntry().add(redactionLogEntry); } From 279fdc69854224d145cc4045803c9985fe932ce9 Mon Sep 17 00:00:00 2001 From: Timo Date: Tue, 13 Jul 2021 11:34:08 +0300 Subject: [PATCH 31/80] added dates to model classes for manual redactions --- .../iqser/red/service/redaction/v1/model/IdRemoval.java | 8 +++++++- .../red/service/redaction/v1/model/ManualForceRedact.java | 8 +++++++- .../redaction/v1/model/ManualImageRecategorization.java | 8 +++++++- .../redaction/v1/model/ManualLegalBasisChange.java | 8 +++++++- .../service/redaction/v1/model/ManualRedactionEntry.java | 5 +++++ .../redaction/service/RedactionLogCreatorService.java | 2 ++ 6 files changed, 35 insertions(+), 4 deletions(-) diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/IdRemoval.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/IdRemoval.java index 2b7b506d..ff5a7329 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/IdRemoval.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/IdRemoval.java @@ -5,6 +5,8 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import java.time.OffsetDateTime; + @Data @Builder @AllArgsConstructor @@ -16,4 +18,8 @@ public class IdRemoval { private Status status; private boolean removeFromDictionary; -} \ No newline at end of file + private OffsetDateTime requestDate; + private OffsetDateTime processedDate; + private OffsetDateTime softDeletedTime; + +} diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualForceRedact.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualForceRedact.java index 3af345dd..c005e6d3 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualForceRedact.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualForceRedact.java @@ -5,6 +5,8 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import java.time.OffsetDateTime; + @Data @Builder @AllArgsConstructor @@ -16,4 +18,8 @@ public class ManualForceRedact { private Status status; private String legalBasis; -} \ No newline at end of file + private OffsetDateTime requestDate; + private OffsetDateTime processedDate; + private OffsetDateTime softDeletedTime; + +} diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualImageRecategorization.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualImageRecategorization.java index 7dc9120c..17e07f5c 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualImageRecategorization.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualImageRecategorization.java @@ -5,6 +5,8 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import java.time.OffsetDateTime; + @Data @Builder @AllArgsConstructor @@ -18,4 +20,8 @@ public class ManualImageRecategorization { private String legalBasis; private boolean redacted; -} \ No newline at end of file + private OffsetDateTime requestDate; + private OffsetDateTime processedDate; + private OffsetDateTime softDeletedTime; + +} diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualLegalBasisChange.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualLegalBasisChange.java index 4f0d211f..39210d07 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualLegalBasisChange.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualLegalBasisChange.java @@ -5,6 +5,8 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import java.time.OffsetDateTime; + @Data @Builder @AllArgsConstructor @@ -16,4 +18,8 @@ public class ManualLegalBasisChange { private Status status; private String legalBasis; -} \ No newline at end of file + private OffsetDateTime requestDate; + private OffsetDateTime processedDate; + private OffsetDateTime softDeletedTime; + +} diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualRedactionEntry.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualRedactionEntry.java index eb4fbecf..df14617b 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualRedactionEntry.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualRedactionEntry.java @@ -5,6 +5,7 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.List; @@ -29,4 +30,8 @@ public class ManualRedactionEntry { private boolean addToDossierDictionary; + private OffsetDateTime requestDate; + private OffsetDateTime processedDate; + private OffsetDateTime softDeletedTime; + } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java index a83d3751..fe4e95c1 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java @@ -641,6 +641,8 @@ public class RedactionLogCreatorService { redactionLogEntry.setDossierDictionaryEntry(manualRedaction.isAddToDossierDictionary()); redactionLogEntry.setPositions(manualRedaction.getPositions()); + manualRedaction. + var found = redactionLog.getRedactionLogEntry().stream().filter(r -> r.getId().equalsIgnoreCase(redactionLogEntry.getId())).findAny(); if (found.isPresent()) { found.get().setDictionaryEntry(manualRedaction.isAddToDossierDictionary()); From af46bdca90aab6aab9e3ac5c5a9808f8eea8c760 Mon Sep 17 00:00:00 2001 From: Timo Date: Tue, 13 Jul 2021 11:35:18 +0300 Subject: [PATCH 32/80] handling add to dict requests --- .../service/RedactionLogCreatorService.java | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java index fe4e95c1..aa60d857 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java @@ -635,24 +635,26 @@ public class RedactionLogCreatorService { for (var manualRedaction : manualRedactions.getEntriesToAdd()) { - if (manualRedaction.isAddToDictionary() || manualRedaction.isAddToDossierDictionary()) { - var redactionLogEntry = createRedactionLogEntry(manualRedaction, manualRedaction.getId(), dossierTemplateId); - redactionLogEntry.setDictionaryEntry(manualRedaction.isAddToDossierDictionary()); - redactionLogEntry.setDossierDictionaryEntry(manualRedaction.isAddToDossierDictionary()); - redactionLogEntry.setPositions(manualRedaction.getPositions()); + if (manualRedaction.getProcessedDate() == null) { + + if (manualRedaction.isAddToDictionary() || manualRedaction.isAddToDossierDictionary()) { + var redactionLogEntry = createRedactionLogEntry(manualRedaction, manualRedaction.getId(), dossierTemplateId); + redactionLogEntry.setDictionaryEntry(manualRedaction.isAddToDossierDictionary()); + redactionLogEntry.setDossierDictionaryEntry(manualRedaction.isAddToDossierDictionary()); + redactionLogEntry.setPositions(manualRedaction.getPositions()); + + + var found = redactionLog.getRedactionLogEntry().stream().filter(r -> r.getId().equalsIgnoreCase(redactionLogEntry.getId())).findAny(); + if (found.isPresent()) { + found.get().setDictionaryEntry(manualRedaction.isAddToDossierDictionary()); + found.get().setDossierDictionaryEntry(manualRedaction.isAddToDossierDictionary()); + found.get().setPositions(manualRedaction.getPositions()); + } else { + redactionLog.getRedactionLogEntry().add(redactionLogEntry); + } - manualRedaction. - var found = redactionLog.getRedactionLogEntry().stream().filter(r -> r.getId().equalsIgnoreCase(redactionLogEntry.getId())).findAny(); - if (found.isPresent()) { - found.get().setDictionaryEntry(manualRedaction.isAddToDossierDictionary()); - found.get().setDossierDictionaryEntry(manualRedaction.isAddToDossierDictionary()); - found.get().setPositions(manualRedaction.getPositions()); - } else { - redactionLog.getRedactionLogEntry().add(redactionLogEntry); } - - } } } From 2d0a6d4d29034c1afee5a122a87135614e099fbd Mon Sep 17 00:00:00 2001 From: Timo Date: Tue, 13 Jul 2021 11:39:44 +0300 Subject: [PATCH 33/80] fixed pmd --- .../service/RedactionLogCreatorService.java | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java index aa60d857..ee628067 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java @@ -635,26 +635,24 @@ public class RedactionLogCreatorService { for (var manualRedaction : manualRedactions.getEntriesToAdd()) { - if (manualRedaction.getProcessedDate() == null) { - - if (manualRedaction.isAddToDictionary() || manualRedaction.isAddToDossierDictionary()) { - var redactionLogEntry = createRedactionLogEntry(manualRedaction, manualRedaction.getId(), dossierTemplateId); - redactionLogEntry.setDictionaryEntry(manualRedaction.isAddToDossierDictionary()); - redactionLogEntry.setDossierDictionaryEntry(manualRedaction.isAddToDossierDictionary()); - redactionLogEntry.setPositions(manualRedaction.getPositions()); - - - var found = redactionLog.getRedactionLogEntry().stream().filter(r -> r.getId().equalsIgnoreCase(redactionLogEntry.getId())).findAny(); - if (found.isPresent()) { - found.get().setDictionaryEntry(manualRedaction.isAddToDossierDictionary()); - found.get().setDossierDictionaryEntry(manualRedaction.isAddToDossierDictionary()); - found.get().setPositions(manualRedaction.getPositions()); - } else { - redactionLog.getRedactionLogEntry().add(redactionLogEntry); - } + // not yet processed, and dictionary modifying, show in redaction-log preview + if (manualRedaction.getProcessedDate() == null && manualRedaction.isAddToDictionary() || manualRedaction.isAddToDossierDictionary()) { + var redactionLogEntry = createRedactionLogEntry(manualRedaction, manualRedaction.getId(), dossierTemplateId); + redactionLogEntry.setDictionaryEntry(manualRedaction.isAddToDossierDictionary()); + redactionLogEntry.setDossierDictionaryEntry(manualRedaction.isAddToDossierDictionary()); + redactionLogEntry.setPositions(manualRedaction.getPositions()); + var found = redactionLog.getRedactionLogEntry().stream().filter(r -> r.getId().equalsIgnoreCase(redactionLogEntry.getId())).findAny(); + if (found.isPresent()) { + found.get().setDictionaryEntry(manualRedaction.isAddToDossierDictionary()); + found.get().setDossierDictionaryEntry(manualRedaction.isAddToDossierDictionary()); + found.get().setPositions(manualRedaction.getPositions()); + } else { + redactionLog.getRedactionLogEntry().add(redactionLogEntry); } + + } } } From 9da43429393a875b82e0ea73dfcd8093bede91c5 Mon Sep 17 00:00:00 2001 From: Timo Date: Tue, 13 Jul 2021 12:43:07 +0300 Subject: [PATCH 34/80] changed to fonts 3.8 to fix build --- redaction-service-image-v1/src/main/docker/Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/redaction-service-image-v1/src/main/docker/Dockerfile b/redaction-service-image-v1/src/main/docker/Dockerfile index 3ab50727..dec2d219 100644 --- a/redaction-service-image-v1/src/main/docker/Dockerfile +++ b/redaction-service-image-v1/src/main/docker/Dockerfile @@ -13,6 +13,6 @@ RUN apt-get update \ wget cabextract xfonts-utils fonts-liberation \ && rm -rf /var/lib/apt/lists/* -RUN curl http://ftp.br.debian.org/debian/pool/contrib/m/msttcorefonts/ttf-mscorefonts-installer_3.7_all.deb -o /tmp/ttf-mscorefonts-installer_3.7_all.deb \ - && dpkg -i /tmp/ttf-mscorefonts-installer_3.7_all.deb \ - && rm /tmp/ttf-mscorefonts-installer_3.7_all.deb \ +RUN curl http://ftp.br.debian.org/debian/pool/contrib/m/msttcorefonts/ttf-mscorefonts-installer_3.8_all.deb -o /tmp/ttf-mscorefonts-installer_3.8_all.deb \ + && dpkg -i /tmp/ttf-mscorefonts-installer_3.8_all.deb \ + && rm /tmp/ttf-mscorefonts-installer_3.8_all.deb \ From b42dd58824784b356b952550de92a848de412acd Mon Sep 17 00:00:00 2001 From: Timo Date: Tue, 13 Jul 2021 15:18:06 +0300 Subject: [PATCH 35/80] update dictionary before preview --- .../redaction/v1/server/controller/RedactionController.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java index ace46234..ebbaffc2 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java @@ -161,8 +161,9 @@ public class RedactionController implements RedactionResource { public RedactionLog getRedactionLogPreview(RedactionRequest redactionRequest) { log.info("Requested preview for: {}", redactionRequest); - var redactionLog = redactionStorageService.getRedactionLog(redactionRequest.getDossierId(), redactionRequest.getFileId()); + dictionaryService.updateDictionary(redactionRequest.getDossierTemplateId(), redactionRequest.getDossierId()); + var redactionLog = redactionStorageService.getRedactionLog(redactionRequest.getDossierId(), redactionRequest.getFileId()); return redactionLogCreatorService.getRedactionLogPreview(redactionLog, redactionRequest.getDossierTemplateId(), redactionRequest.getManualRedactions()); } From 58dd6841d60ad8562ae0aedf3e0aea2889fb8a4c Mon Sep 17 00:00:00 2001 From: Timo Date: Tue, 13 Jul 2021 15:35:39 +0300 Subject: [PATCH 36/80] fixed typo --- .../server/redaction/service/RedactionLogCreatorService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java index ee628067..6c989a9e 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java @@ -638,14 +638,14 @@ public class RedactionLogCreatorService { // not yet processed, and dictionary modifying, show in redaction-log preview if (manualRedaction.getProcessedDate() == null && manualRedaction.isAddToDictionary() || manualRedaction.isAddToDossierDictionary()) { var redactionLogEntry = createRedactionLogEntry(manualRedaction, manualRedaction.getId(), dossierTemplateId); - redactionLogEntry.setDictionaryEntry(manualRedaction.isAddToDossierDictionary()); + redactionLogEntry.setDictionaryEntry(manualRedaction.isAddToDictionary()); redactionLogEntry.setDossierDictionaryEntry(manualRedaction.isAddToDossierDictionary()); redactionLogEntry.setPositions(manualRedaction.getPositions()); var found = redactionLog.getRedactionLogEntry().stream().filter(r -> r.getId().equalsIgnoreCase(redactionLogEntry.getId())).findAny(); if (found.isPresent()) { - found.get().setDictionaryEntry(manualRedaction.isAddToDossierDictionary()); + found.get().setDictionaryEntry(manualRedaction.isAddToDictionary()); found.get().setDossierDictionaryEntry(manualRedaction.isAddToDossierDictionary()); found.get().setPositions(manualRedaction.getPositions()); } else { From 5e50c0bfde741c7780bb9048c03830cf05bbf4a8 Mon Sep 17 00:00:00 2001 From: Timo Date: Tue, 13 Jul 2021 16:34:44 +0300 Subject: [PATCH 37/80] added logs for dict --- .../redaction/v1/server/redaction/service/DictionaryService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java index 71cd08b2..fd460dbd 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java @@ -33,6 +33,7 @@ public class DictionaryService { public DictionaryVersion updateDictionary(String dossierTemplateId, String dossierId) { + log.info("Updating dictionary data for: {} / {}", dossierTemplateId, dossierId); long dossierTemplateDictionaryVersion = dictionaryClient.getVersion(dossierTemplateId, GLOBAL_DOSSIER); var dossierTemplateDictionary = dictionariesByDossierTemplate.get(dossierTemplateId); if (dossierTemplateDictionary == null || dossierTemplateDictionaryVersion > dossierTemplateDictionary.getDictionaryVersion()) { From 71ba89306715d88ddadaa7780f4935a99bd107d6 Mon Sep 17 00:00:00 2001 From: Timo Date: Wed, 14 Jul 2021 11:20:37 +0300 Subject: [PATCH 38/80] id removal modifying dict flag --- .../server/redaction/service/RedactionLogCreatorService.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java index 6c989a9e..cd5416ae 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java @@ -146,6 +146,8 @@ public class RedactionLogCreatorService { redactionLogEntry.setReason(manualOverrideReason); redactionLogEntry.setManual(true); redactionLogEntry.setManualRedactionType(ManualRedactionType.REMOVE); + redactionLogEntry.setDictionaryEntry(manualRemoval.isRemoveFromDictionary()); + redactionLogEntry.setDossierDictionaryEntry(manualRemoval.isRemoveFromDictionary()); } } } @@ -298,6 +300,8 @@ public class RedactionLogCreatorService { redactionLogEntry.setReason(manualOverrideReason); redactionLogEntry.setManual(true); redactionLogEntry.setManualRedactionType(ManualRedactionType.REMOVE); + redactionLogEntry.setDictionaryEntry(manualRemoval.isRemoveFromDictionary()); + redactionLogEntry.setDossierDictionaryEntry(manualRemoval.isRemoveFromDictionary()); } } } @@ -326,6 +330,7 @@ public class RedactionLogCreatorService { redactionLogEntry.setReason(manualOverrideReason); redactionLogEntry.setManual(true); redactionLogEntry.setManualRedactionType(ManualRedactionType.FORCE_REDACT); + } } } From cb41187be660b5f4180c11e890bae57ebc79308a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Wed, 14 Jul 2021 12:24:27 +0200 Subject: [PATCH 39/80] RED-1814: Apply manual image recategorization before rules execution --- .../v1/model/ManualImageRecategorization.java | 2 - .../service/EntityRedactionService.java | 40 ++++++++++++++----- .../redaction/service/ReanalyzeService.java | 26 ++++++++++-- .../service/RedactionLogCreatorService.java | 7 +--- .../v1/server/RedactionIntegrationTest.java | 2 - 5 files changed, 55 insertions(+), 22 deletions(-) diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualImageRecategorization.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualImageRecategorization.java index 17e07f5c..14924878 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualImageRecategorization.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualImageRecategorization.java @@ -17,8 +17,6 @@ public class ManualImageRecategorization { private String user; private Status status; private String type; - private String legalBasis; - private boolean redacted; private OffsetDateTime requestDate; private OffsetDateTime processedDate; diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java index e6d00c15..2a26bfec 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java @@ -1,19 +1,24 @@ package com.iqser.red.service.redaction.v1.server.redaction.service; import com.iqser.red.service.redaction.v1.model.FileAttribute; +import com.iqser.red.service.redaction.v1.model.ManualImageRecategorization; import com.iqser.red.service.redaction.v1.model.ManualRedactionEntry; import com.iqser.red.service.redaction.v1.model.ManualRedactions; import com.iqser.red.service.redaction.v1.model.Point; import com.iqser.red.service.redaction.v1.model.Rectangle; import com.iqser.red.service.redaction.v1.model.SectionArea; +import com.iqser.red.service.redaction.v1.model.Status; import com.iqser.red.service.redaction.v1.server.classification.model.*; import com.iqser.red.service.redaction.v1.server.redaction.model.Dictionary; import com.iqser.red.service.redaction.v1.server.redaction.model.*; import com.iqser.red.service.redaction.v1.server.redaction.utils.EntitySearchUtils; +import com.iqser.red.service.redaction.v1.server.redaction.utils.IdBuilder; import com.iqser.red.service.redaction.v1.server.tableextraction.model.Cell; import com.iqser.red.service.redaction.v1.server.tableextraction.model.Table; + import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; + import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.kie.api.runtime.KieContainer; @@ -34,7 +39,8 @@ public class EntityRedactionService { private final SurroundingWordsService surroundingWordsService; - public void processDocument(Document classifiedDoc, String dossierTemplateId, ManualRedactions manualRedactions, String dossierId, List fileAttributes) { + public void processDocument(Document classifiedDoc, String dossierTemplateId, ManualRedactions manualRedactions, + String dossierId, List fileAttributes) { dictionaryService.updateDictionary(dossierTemplateId, dossierId); KieContainer container = droolsExecutionService.updateRules(dossierTemplateId); @@ -85,7 +91,8 @@ public class EntityRedactionService { private Set findEntities(Document classifiedDoc, KieContainer kieContainer, ManualRedactions manualRedactions, Dictionary dictionary, boolean local, - Map> hintsPerSectionNumber, List fileAttributes) { + Map> hintsPerSectionNumber, + List fileAttributes) { Set documentEntities = new HashSet<>(); @@ -165,7 +172,8 @@ public class EntityRedactionService { ManualRedactions manualRedactions, AtomicInteger sectionNumber, Dictionary dictionary, boolean local, - Map> hintsPerSectionNumber, List fileAttributes) { + Map> hintsPerSectionNumber, + List fileAttributes) { List sectionSearchableTextPairs = new ArrayList<>(); @@ -334,7 +342,7 @@ public class EntityRedactionService { sectionText.setSectionNumber(sectionNumber.intValue()); sectionText.setTable(false); sectionText.setImages(images.stream() - .map(image -> convert(image, sectionNumber.intValue(), headline)) + .map(image -> convertAndRecategorize(image, sectionNumber.intValue(), headline, manualRedactions)) .collect(Collectors.toSet())); sectionText.setTextBlocks(paragraphTextBlocks); classifiedDoc.getSectionText().add(sectionText); @@ -357,7 +365,7 @@ public class EntityRedactionService { .searchableText(searchableText) .dictionary(dictionary) .images(images.stream() - .map(image -> convert(image, sectionNumber.intValue(), headline)) + .map(image -> convertAndRecategorize(image, sectionNumber.intValue(), headline, manualRedactions)) .collect(Collectors.toSet())) .fileAttributes(fileAttributes) .build(), searchableText); @@ -376,9 +384,11 @@ public class EntityRedactionService { String lowercaseInputString = searchableString.toLowerCase(); for (DictionaryModel model : dictionary.getDictionaryModels()) { if (model.isCaseInsensitive()) { - found.addAll(EntitySearchUtils.find(lowercaseInputString, model.getValues(local), model.getType(), headline, sectionNumber, local, model.isDossierDictionary())); + found.addAll(EntitySearchUtils.find(lowercaseInputString, model.getValues(local), model.getType(), headline, sectionNumber, local, model + .isDossierDictionary())); } else { - found.addAll(EntitySearchUtils.find(searchableString, model.getValues(local), model.getType(), headline, sectionNumber, local, model.isDossierDictionary())); + found.addAll(EntitySearchUtils.find(searchableString, model.getValues(local), model.getType(), headline, sectionNumber, local, model + .isDossierDictionary())); } } @@ -406,9 +416,9 @@ public class EntityRedactionService { } - private Image convert(PdfImage pdfImage, int sectionNumber, String headline) { + private Image convertAndRecategorize(PdfImage pdfImage, int sectionNumber, String headline, ManualRedactions manualRedactions) { - return Image.builder() + Image image = Image.builder() .type(pdfImage.getImageType().equals(ImageType.OTHER) ? "image" : pdfImage.getImageType() .name() .toLowerCase(Locale.ROOT)) @@ -417,6 +427,18 @@ public class EntityRedactionService { .section(headline) .page(pdfImage.getPage()) .build(); + + String imageId = IdBuilder.buildId(image.getPosition(), image.getPage()); + if (manualRedactions != null && manualRedactions.getImageRecategorizations() != null) { + for (ManualImageRecategorization imageRecategorization : manualRedactions.getImageRecategorizations()) { + if (imageRecategorization.getStatus().equals(Status.APPROVED) && imageRecategorization.getId() + .equals(imageId)) { + image.setType(imageRecategorization.getType()); + } + } + } + + return image; } } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java index ddc67900..91f7398d 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java @@ -27,6 +27,7 @@ import com.iqser.red.service.redaction.v1.model.Rectangle; import com.iqser.red.service.redaction.v1.model.RedactionLog; import com.iqser.red.service.redaction.v1.model.RedactionLogEntry; import com.iqser.red.service.redaction.v1.model.SectionArea; +import com.iqser.red.service.redaction.v1.model.Status; import com.iqser.red.service.redaction.v1.server.classification.model.Document; import com.iqser.red.service.redaction.v1.server.classification.model.SectionText; import com.iqser.red.service.redaction.v1.server.classification.model.Text; @@ -42,6 +43,7 @@ import com.iqser.red.service.redaction.v1.server.redaction.model.RedRectangle2D; import com.iqser.red.service.redaction.v1.server.redaction.model.Section; import com.iqser.red.service.redaction.v1.server.redaction.model.SectionSearchableTextPair; import com.iqser.red.service.redaction.v1.server.redaction.utils.EntitySearchUtils; +import com.iqser.red.service.redaction.v1.server.redaction.utils.IdBuilder; import com.iqser.red.service.redaction.v1.server.segmentation.PdfSegmentationService; import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService; @@ -205,6 +207,20 @@ public class ReanalyzeService { surroundingWordsService.addSurroundingText(entities, reanalysisSection.getSearchableText(), dictionary); } + if (reanalysisSection.getImages() != null && !reanalysisSection.getImages() + .isEmpty() && analyzeRequest.getManualRedactions() != null && analyzeRequest.getManualRedactions() + .getImageRecategorizations() != null) { + for (Image image : reanalysisSection.getImages()) { + String imageId = IdBuilder.buildId(image.getPosition(), image.getPage()); + for(ManualImageRecategorization imageRecategorization: analyzeRequest.getManualRedactions() + .getImageRecategorizations()){ + if(imageRecategorization.getStatus().equals(Status.APPROVED) && imageRecategorization.getId().equals(imageId)){ + image.setType(imageRecategorization.getType()); + } + } + } + } + sectionSearchableTextPairs.add(new SectionSearchableTextPair(Section.builder() .isLocal(false) .dictionaryTypes(dictionary.getTypes()) @@ -300,9 +316,13 @@ public class ReanalyzeService { return new HashSet<>(); } - return Stream.concat(manualRedactions.getManualLegalBasisChanges().stream().map(ManualLegalBasisChange::getId), - Stream.concat(manualRedactions.getImageRecategorizations().stream().map(ManualImageRecategorization::getId), - Stream.concat(manualRedactions.getIdsToRemove().stream().map(IdRemoval::getId), manualRedactions.getForceRedacts().stream().map(ManualForceRedact::getId)))) + return Stream.concat(manualRedactions.getManualLegalBasisChanges() + .stream() + .map(ManualLegalBasisChange::getId), Stream.concat(manualRedactions.getImageRecategorizations() + .stream() + .map(ManualImageRecategorization::getId), Stream.concat(manualRedactions.getIdsToRemove() + .stream() + .map(IdRemoval::getId), manualRedactions.getForceRedacts().stream().map(ManualForceRedact::getId)))) .collect(Collectors.toSet()); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java index cd5416ae..4a4a9bdf 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java @@ -99,14 +99,9 @@ public class RedactionLogCreatorService { if (recategorization.getId().equals(redactionLogEntry.getId())) { String manualOverrideReason = null; if (recategorization.getStatus().equals(Status.APPROVED)) { - image.setRedaction(recategorization.isRedacted()); - redactionLogEntry.setType(recategorization.getType()); - redactionLogEntry.setHint(dictionaryService.isHint(recategorization.getType(), dossierTemplateId)); - redactionLogEntry.setRedacted(recategorization.isRedacted()); redactionLogEntry.setStatus(Status.APPROVED); - redactionLogEntry.setLegalBasis(recategorization.getLegalBasis()); + redactionLogEntry.setType(recategorization.getType()); manualOverrideReason = mergeReasonIfNecessary(image.getRedactionReason(), ", recategorized by manual override"); - redactionLogEntry.setColor(getColorForImage(redactionLogEntry.getType(), dossierTemplateId, false, redactionLogEntry.isRedacted())); } else if (recategorization.getStatus().equals(Status.REQUESTED)) { manualOverrideReason = mergeReasonIfNecessary(image.getRedactionReason(), ", requested to recategorize"); redactionLogEntry.setStatus(Status.REQUESTED); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java index 305df362..8904ea6c 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java @@ -698,8 +698,6 @@ public class RedactionIntegrationTest { .id("37eee3e9d589a5cc529bfec38c3ba479") .status(Status.APPROVED) .type("signature") - .redacted(true) - .legalBasis("Article 39(e)(1) and Article 39(e)(2) of Regulation (EC) No 178/2002") .build())); request.setManualRedactions(manualRedactions); From 84db91d74dabc82306911972a0ffface1bcdff3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Wed, 14 Jul 2021 17:03:22 +0200 Subject: [PATCH 40/80] RED-1813: Fixed to large images --- .../server/parsing/PDFLinesTextStripper.java | 26 ++++--------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/parsing/PDFLinesTextStripper.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/parsing/PDFLinesTextStripper.java index 45bcef6a..d35e51b9 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/parsing/PDFLinesTextStripper.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/parsing/PDFLinesTextStripper.java @@ -189,18 +189,17 @@ public class PDFLinesTextStripper extends PDFTextStripper { COSName objectName = (COSName) arguments.get(0); PDXObject xobject = getResources().getXObject(objectName); if (xobject instanceof PDImageXObject) { - PDImageXObject pdfImage = (PDImageXObject) xobject; + PDImageXObject image = (PDImageXObject)xobject; - Rectangle2D imageBounds = calculateImagePosition(pdfImage); + Matrix ctmNew = getGraphicsState().getCurrentTransformationMatrix(); - Rectangle2D rect = new Rectangle2D.Float((float) imageBounds.getX(), (float) imageBounds.getY(), (float) imageBounds - .getWidth(), (float) imageBounds.getHeight()); + Rectangle2D rect = new Rectangle2D.Float(ctmNew.getTranslateX(), ctmNew.getTranslateY(), ctmNew.getScaleX(), ctmNew.getScaleY()); // Memory Hack - sofReference kills me - FieldUtils.writeField(pdfImage, "cachedImageSubsampling", -1, true); + FieldUtils.writeField(image, "cachedImageSubsampling", -1, true); if (rect.getHeight() > 2 && rect.getWidth() > 2) { - this.images.add(new PdfImage(pdfImage.getImage(), rect, pageNumber)); + this.images.add(new PdfImage(image.getImage(), rect, pageNumber)); } } } catch (Exception e) { @@ -209,21 +208,6 @@ public class PDFLinesTextStripper extends PDFTextStripper { } - private Rectangle2D calculateImagePosition(PDImageXObject pdfImage) throws IOException { - - Matrix ctm = getGraphicsState().getCurrentTransformationMatrix(); - - Rectangle2D imageBounds = pdfImage.getImage().getRaster().getBounds(); - - AffineTransform imageTransform = new AffineTransform(ctm.createAffineTransform()); - imageTransform.scale(1.0 / pdfImage.getWidth(), -1.0 / pdfImage.getHeight()); - imageTransform.translate(0, -pdfImage.getHeight()); - - AffineTransform pageTransform = new AffineTransform(); - pageTransform.concatenate(imageTransform); - - return pageTransform.createTransformedShape(imageBounds).getBounds2D(); - } private float floatValue(COSBase value) { From 93844d8e76e0806cf00292b591a9f97ee8e61230 Mon Sep 17 00:00:00 2001 From: Timo Date: Mon, 19 Jul 2021 11:39:19 +0300 Subject: [PATCH 41/80] fixed undo after reanalysis --- .../service/RedactionLogCreatorService.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java index 4a4a9bdf..7bd49fbc 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java @@ -613,6 +613,7 @@ public class RedactionLogCreatorService { } + Set entriesToRemoveBecauseOfUndoActions = new HashSet<>(); for (RedactionLogEntry entry : redactionLog.getRedactionLogEntry()) { var reasonHolder = new PreviewReasonHolder(entry); @@ -622,14 +623,66 @@ public class RedactionLogCreatorService { } processRedactionLogEntry(manualRedactions, dossierTemplateId, entry, reasonHolder); + var shouldRemove = postProcessRedactionLogEntryForDeletedManualRedactions(manualRedactions, entry); + if (shouldRemove) { + entriesToRemoveBecauseOfUndoActions.add(entry.getId()); + } } + redactionLog.setRedactionLogEntry(redactionLog.getRedactionLogEntry() + .stream().filter(entry -> !entriesToRemoveBecauseOfUndoActions.contains(entry.getId())).collect(Collectors.toList())); + handleAddToDictionary(redactionLog, manualRedactions, dossierTemplateId); return redactionLog; } + private boolean postProcessRedactionLogEntryForDeletedManualRedactions(ManualRedactions manualRedactions, RedactionLogEntry entry) { + if (entry.isManual()) { + + if (entry.getManualRedactionType() == ManualRedactionType.ADD) { + var manualRedactionExists = manualRedactions.getEntriesToAdd().stream().anyMatch(entryToAdd -> entryToAdd.getId().equalsIgnoreCase(entry.getId())); + return !manualRedactionExists; + } + + if (entry.getManualRedactionType() == ManualRedactionType.FORCE_REDACT) { + var forceRedactExists = manualRedactions.getForceRedacts().stream().anyMatch(entryToAdd -> entryToAdd.getId().equalsIgnoreCase(entry.getId())); + if (!forceRedactExists) { + entry.setRedacted(false); + entry.setManual(false); + entry.setStatus(null); + } + } + + if (entry.getManualRedactionType() == ManualRedactionType.REMOVE) { + var removeIdExists = manualRedactions.getIdsToRemove().stream().anyMatch(entryToAdd -> entryToAdd.getId().equalsIgnoreCase(entry.getId())); + if (!removeIdExists) { + entry.setRedacted(true); + entry.setManual(false); + entry.setStatus(null); + } + } + + // Cannot undo Already Approved change because UI won't allow it for now + if (Status.REQUESTED.equals(entry.getStatus())) { + if (entry.getManualRedactionType() == ManualRedactionType.RECATEGORIZE) { + entry.setManual(false); + entry.setStatus(null); + entry.setRecategorizationType(null); + } + if (entry.getManualRedactionType() == ManualRedactionType.LEGAL_BASIS_CHANGE) { + entry.setManual(false); + entry.setStatus(null); + entry.setLegalBasisChangeValue(null); + } + + } + } + + return false; + } + private void handleAddToDictionary(RedactionLog redactionLog, ManualRedactions manualRedactions, String dossierTemplateId) { From 0b89c55a1d0aacf9fd84efdb5c59a881d4ae1a4d Mon Sep 17 00:00:00 2001 From: Timo Date: Mon, 19 Jul 2021 11:57:29 +0300 Subject: [PATCH 42/80] fixed undo after reanalysis --- .../service/RedactionLogCreatorService.java | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java index 7bd49fbc..273a4f69 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java @@ -666,17 +666,22 @@ public class RedactionLogCreatorService { // Cannot undo Already Approved change because UI won't allow it for now if (Status.REQUESTED.equals(entry.getStatus())) { - if (entry.getManualRedactionType() == ManualRedactionType.RECATEGORIZE) { - entry.setManual(false); - entry.setStatus(null); - entry.setRecategorizationType(null); + var recategorizeExists = manualRedactions.getImageRecategorizations().stream().anyMatch(entryToAdd -> entryToAdd.getId().equalsIgnoreCase(entry.getId())); + if (!recategorizeExists) { + if (entry.getManualRedactionType() == ManualRedactionType.RECATEGORIZE) { + entry.setManual(false); + entry.setStatus(null); + entry.setRecategorizationType(null); + } } - if (entry.getManualRedactionType() == ManualRedactionType.LEGAL_BASIS_CHANGE) { - entry.setManual(false); - entry.setStatus(null); - entry.setLegalBasisChangeValue(null); + var legalBasisChangeExists = manualRedactions.getImageRecategorizations().stream().anyMatch(entryToAdd -> entryToAdd.getId().equalsIgnoreCase(entry.getId())); + if (!legalBasisChangeExists) { + if (entry.getManualRedactionType() == ManualRedactionType.LEGAL_BASIS_CHANGE) { + entry.setManual(false); + entry.setStatus(null); + entry.setLegalBasisChangeValue(null); + } } - } } From d4486ea6f4d31d68a2c75ec5e192f2a3bd62443c Mon Sep 17 00:00:00 2001 From: Timo Date: Mon, 19 Jul 2021 11:59:53 +0300 Subject: [PATCH 43/80] fixed PMD --- .../service/RedactionLogCreatorService.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java index 273a4f69..8518891f 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java @@ -666,17 +666,17 @@ public class RedactionLogCreatorService { // Cannot undo Already Approved change because UI won't allow it for now if (Status.REQUESTED.equals(entry.getStatus())) { - var recategorizeExists = manualRedactions.getImageRecategorizations().stream().anyMatch(entryToAdd -> entryToAdd.getId().equalsIgnoreCase(entry.getId())); - if (!recategorizeExists) { - if (entry.getManualRedactionType() == ManualRedactionType.RECATEGORIZE) { + if (entry.getManualRedactionType() == ManualRedactionType.RECATEGORIZE) { + var recategorizeExists = manualRedactions.getImageRecategorizations().stream().anyMatch(entryToAdd -> entryToAdd.getId().equalsIgnoreCase(entry.getId())); + if (!recategorizeExists) { entry.setManual(false); entry.setStatus(null); entry.setRecategorizationType(null); } } - var legalBasisChangeExists = manualRedactions.getImageRecategorizations().stream().anyMatch(entryToAdd -> entryToAdd.getId().equalsIgnoreCase(entry.getId())); - if (!legalBasisChangeExists) { - if (entry.getManualRedactionType() == ManualRedactionType.LEGAL_BASIS_CHANGE) { + if (entry.getManualRedactionType() == ManualRedactionType.LEGAL_BASIS_CHANGE) { + var legalBasisChangeExists = manualRedactions.getImageRecategorizations().stream().anyMatch(entryToAdd -> entryToAdd.getId().equalsIgnoreCase(entry.getId())); + if (!legalBasisChangeExists) { entry.setManual(false); entry.setStatus(null); entry.setLegalBasisChangeValue(null); From a39bbd9afa814577543a33c35dd27c96ec993e79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Mon, 19 Jul 2021 13:12:20 +0200 Subject: [PATCH 44/80] RED-1184: Always find reanaylsis sections with caseinsentive values to remove values if type has changed from caseinsensitive to casesensitive --- .../server/redaction/utils/EntitySearchUtils.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/utils/EntitySearchUtils.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/utils/EntitySearchUtils.java index f14eeb59..0daca578 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/utils/EntitySearchUtils.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/utils/EntitySearchUtils.java @@ -5,6 +5,7 @@ import com.iqser.red.service.redaction.v1.server.redaction.model.DictionaryIncre import com.iqser.red.service.redaction.v1.server.redaction.model.Entity; import com.iqser.red.service.redaction.v1.server.redaction.model.EntityPositionSequence; import com.iqser.red.service.redaction.v1.server.redaction.model.SearchableText; + import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; @@ -17,7 +18,6 @@ import java.util.stream.Collectors; @SuppressWarnings("PMD") public class EntitySearchUtils { - public boolean sectionContainsAny(String sectionText, Set values) { String inputString = sectionText.toLowerCase(Locale.ROOT); @@ -38,9 +38,7 @@ public class EntitySearchUtils { if (startIndex > -1 && (startIndex == 0 || Character.isWhitespace(inputString.charAt(startIndex - 1)) || isSeparator(inputString .charAt(startIndex - 1))) && (stopIndex == inputString.length() || isSeparator(inputString.charAt(stopIndex)))) { - if (value.isCaseinsensitive() || !value.isCaseinsensitive() && sectionText.substring(startIndex, stopIndex).equals(value.getValue())) { - return true; - } + return true; } } while (startIndex > -1); } @@ -98,8 +96,8 @@ public class EntitySearchUtils { .sorted(Comparator.comparing(Entity::getStart)) .collect(Collectors.toList()); Entity firstEntity = orderedEntities.get(0); - List positionSequences = text.getSequences(firstEntity.getWord().trim(), dictionary.isCaseInsensitiveDictionary(firstEntity - .getType()), firstEntity.getTargetSequences()); + List positionSequences = text.getSequences(firstEntity.getWord() + .trim(), dictionary.isCaseInsensitiveDictionary(firstEntity.getType()), firstEntity.getTargetSequences()); for (int i = 0; i <= orderedEntities.size() - 1; i++) { try { @@ -133,6 +131,7 @@ public class EntitySearchUtils { public void addEntitiesWithHigherRank(Set entities, Set found, Dictionary dictionary) { + found.forEach(f -> addEntitiesWithHigherRank(entities, f, dictionary)); } @@ -148,9 +147,11 @@ public class EntitySearchUtils { entities.add(found); } + public void addEntitiesIgnoreRank(Set entities, Set found) { // HashSet keeps old value but we want the new. entities.removeAll(found); entities.addAll(found); } + } From e809004f1638f4982856e6c7fded719894adb3e8 Mon Sep 17 00:00:00 2001 From: Timo Date: Mon, 19 Jul 2021 14:57:59 +0300 Subject: [PATCH 45/80] add comments --- .../server/redaction/service/RedactionLogCreatorService.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java index 8518891f..b1707ae6 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java @@ -627,6 +627,9 @@ public class RedactionLogCreatorService { if (shouldRemove) { entriesToRemoveBecauseOfUndoActions.add(entry.getId()); } + + + entry.setComments(manualRedactions.getComments().get(entry.getId()); } redactionLog.setRedactionLogEntry(redactionLog.getRedactionLogEntry() From e9c5b955805b42e3e62ae0aad7affcad05610833 Mon Sep 17 00:00:00 2001 From: Timo Date: Mon, 19 Jul 2021 16:30:29 +0300 Subject: [PATCH 46/80] fixed compile err --- .../v1/server/redaction/service/RedactionLogCreatorService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java index b1707ae6..18e23420 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java @@ -629,7 +629,7 @@ public class RedactionLogCreatorService { } - entry.setComments(manualRedactions.getComments().get(entry.getId()); + entry.setComments(manualRedactions.getComments().get(entry.getId())); } redactionLog.setRedactionLogEntry(redactionLog.getRedactionLogEntry() From ba0cde241fd11ed6fe0bd623561470a108e1b89f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Tue, 20 Jul 2021 10:01:49 +0200 Subject: [PATCH 47/80] RED-1813: Added imageHasTransparency to redaction log --- .../red/service/redaction/v1/model/RedactionLogEntry.java | 1 + .../redaction/v1/server/parsing/PDFLinesTextStripper.java | 3 +-- .../service/redaction/v1/server/redaction/model/Image.java | 1 + .../service/redaction/v1/server/redaction/model/PdfImage.java | 4 +++- .../v1/server/redaction/service/EntityRedactionService.java | 1 + .../v1/server/redaction/service/ReanalyzeService.java | 1 + .../server/redaction/service/RedactionLogCreatorService.java | 1 + .../redaction/v1/server/segmentation/ImageMergeService.java | 2 +- 8 files changed, 10 insertions(+), 4 deletions(-) diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java index e347f0b3..e837ddf9 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java @@ -46,6 +46,7 @@ public class RedactionLogEntry { private int endOffset; private boolean isImage; + private boolean imageHasTransparency; private boolean isDossierDictionaryEntry; diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/parsing/PDFLinesTextStripper.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/parsing/PDFLinesTextStripper.java index d35e51b9..d3f6f2a0 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/parsing/PDFLinesTextStripper.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/parsing/PDFLinesTextStripper.java @@ -190,7 +190,6 @@ public class PDFLinesTextStripper extends PDFTextStripper { PDXObject xobject = getResources().getXObject(objectName); if (xobject instanceof PDImageXObject) { PDImageXObject image = (PDImageXObject)xobject; - Matrix ctmNew = getGraphicsState().getCurrentTransformationMatrix(); Rectangle2D rect = new Rectangle2D.Float(ctmNew.getTranslateX(), ctmNew.getTranslateY(), ctmNew.getScaleX(), ctmNew.getScaleY()); @@ -199,7 +198,7 @@ public class PDFLinesTextStripper extends PDFTextStripper { FieldUtils.writeField(image, "cachedImageSubsampling", -1, true); if (rect.getHeight() > 2 && rect.getWidth() > 2) { - this.images.add(new PdfImage(image.getImage(), rect, pageNumber)); + this.images.add(new PdfImage(image.getImage(), rect, pageNumber, image.getImage().getColorModel().hasAlpha())); } } } catch (Exception e) { diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Image.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Image.java index 5aab9c7a..63d382d5 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Image.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Image.java @@ -20,5 +20,6 @@ public class Image implements ReasonHolder { private int sectionNumber; private String section; private int page; + private boolean hasTransparency; } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/PdfImage.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/PdfImage.java index 1631717f..dbb390c0 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/PdfImage.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/PdfImage.java @@ -18,14 +18,16 @@ public class PdfImage { private RedRectangle2D position; private ImageType imageType; private boolean isAppendedToParagraph; + private boolean hasTransparency; @NonNull private int page; - public PdfImage(BufferedImage image, Rectangle2D position, int page) { + public PdfImage(BufferedImage image, Rectangle2D position, int page, boolean hasTransparency) { this.image = image; this.position = new RedRectangle2D(position.getX(), position.getY(), position.getWidth(), position.getHeight()); this.page = page; + this.hasTransparency = hasTransparency; } } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java index 2a26bfec..389b7186 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java @@ -426,6 +426,7 @@ public class EntityRedactionService { .sectionNumber(sectionNumber) .section(headline) .page(pdfImage.getPage()) + .hasTransparency(pdfImage.isHasTransparency()) .build(); String imageId = IdBuilder.buildId(image.getPosition(), image.getPage()); diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java index 91f7398d..fa3f388c 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java @@ -338,6 +338,7 @@ public class ReanalyzeService { .sectionNumber(entry.getSectionNumber()) .section(entry.getSection()) .page(position.getPage()) + .hasTransparency(entry.isImageHasTransparency()) .build(); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java index 18e23420..885326c9 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java @@ -83,6 +83,7 @@ public class RedactionLogCreatorService { .getWidth(), (float) image.getPosition().getHeight(), pageNumber))) .sectionNumber(image.getSectionNumber()) .section(image.getSection()) + .imageHasTransparency(image.isHasTransparency()) .build(); processImageEntry(manualRedactions, dossierTemplateId, image, redactionLogEntry); diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/segmentation/ImageMergeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/segmentation/ImageMergeService.java index 73a94909..4c58bcd8 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/segmentation/ImageMergeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/segmentation/ImageMergeService.java @@ -118,7 +118,7 @@ public class ImageMergeService { //set position for merged image with values of image1 and the height of both Rectangle2D pos = new Rectangle2D.Float(); pos.setRect(image1.getPosition().getX(), image2.getPosition().getY(), rotation == 90 ? width + width2: width, rotation == 90 ? height1 : height1 + height2); - PdfImage newPdfImage = new PdfImage(mergedImage, pos, image1.getPage()); + PdfImage newPdfImage = new PdfImage(mergedImage, pos, image1.getPage(), image1.isHasTransparency() || image2.isHasTransparency()); // Graphics need to be disposed image1.getImage().flush(); From 1ac294fc0197990936ba62b3889474f63844bc21 Mon Sep 17 00:00:00 2001 From: Timo Date: Wed, 21 Jul 2021 15:58:52 +0300 Subject: [PATCH 48/80] redaction-log preview generation and removal of manual redactions from redaction-log --- .../redaction/v1/model/AnalyzeRequest.java | 2 - .../v1/model/ManualRedactionEntry.java | 5 +- .../redaction/v1/model/RedactionLog.java | 7 + .../redaction/v1/model/RedactionLogEntry.java | 2 + .../redaction/v1/model/SectionRectangle.java | 1 + .../v1/resources/RedactionResource.java | 2 +- .../controller/RedactionController.java | 8 +- .../redaction/service/DictionaryService.java | 2 +- .../service/EntityRedactionService.java | 40 +- .../redaction/service/ReanalyzeService.java | 102 +--- .../service/RedactionLogCreatorService.java | 547 +----------------- .../service/RedactionLogMergeService.java | 241 ++++++++ 12 files changed, 312 insertions(+), 647 deletions(-) create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeRequest.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeRequest.java index 7a891277..4a7be09b 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeRequest.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeRequest.java @@ -7,9 +7,7 @@ import lombok.NoArgsConstructor; import java.time.OffsetDateTime; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Set; @Data diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualRedactionEntry.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualRedactionEntry.java index df14617b..69eb03cf 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualRedactionEntry.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualRedactionEntry.java @@ -23,11 +23,8 @@ public class ManualRedactionEntry { private String legalBasis; private List positions = new ArrayList<>(); private Status status; + private boolean addToDictionary; - - private String section; - private int sectionNumber; - private boolean addToDossierDictionary; private OffsetDateTime requestDate; diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLog.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLog.java index 71a8413e..276363f8 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLog.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLog.java @@ -10,6 +10,13 @@ import java.util.List; @AllArgsConstructor public class RedactionLog { + + /** + * Version 0 Redaction Logs have manual redactions merged inside them + * Version 1 Redaction Logs only contain system ( rule/dictionary ) redactions. Manual Redactions are merged in at runtime. + */ + private long computationalVersion; + private List redactionLogEntry; private List legalBasis; diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java index e837ddf9..9f7a0146 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java @@ -9,6 +9,8 @@ import lombok.NoArgsConstructor; import java.util.ArrayList; import java.util.List; + + @Data @Builder @NoArgsConstructor diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/SectionRectangle.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/SectionRectangle.java index 38031b36..3a21bb47 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/SectionRectangle.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/SectionRectangle.java @@ -30,4 +30,5 @@ public class SectionRectangle { private int numberOfParts; private List tableCells; + } diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/resources/RedactionResource.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/resources/RedactionResource.java index 0a6037a6..3abe15f5 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/resources/RedactionResource.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/resources/RedactionResource.java @@ -31,6 +31,6 @@ public interface RedactionResource { void testRules(@RequestBody String rules); @PostMapping(value = "/redaction-log/preview", consumes = MediaType.APPLICATION_JSON_VALUE) - RedactionLog getRedactionLogPreview(@RequestBody RedactionRequest redactionRequest); + RedactionLog getRedactionLog(@RequestBody RedactionRequest redactionRequest); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java index ebbaffc2..047538cf 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java @@ -13,7 +13,7 @@ import com.iqser.red.service.redaction.v1.server.exception.RedactionException; import com.iqser.red.service.redaction.v1.server.redaction.service.AnnotationService; import com.iqser.red.service.redaction.v1.server.redaction.service.DictionaryService; import com.iqser.red.service.redaction.v1.server.redaction.service.DroolsExecutionService; -import com.iqser.red.service.redaction.v1.server.redaction.service.RedactionLogCreatorService; +import com.iqser.red.service.redaction.v1.server.redaction.service.RedactionLogMergeService; import com.iqser.red.service.redaction.v1.server.segmentation.PdfSegmentationService; import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService; import com.iqser.red.service.redaction.v1.server.tableextraction.model.AbstractTextContainer; @@ -41,7 +41,7 @@ public class RedactionController implements RedactionResource { private final AnnotationService annotationService; private final PdfSegmentationService pdfSegmentationService; private final RedactionStorageService redactionStorageService; - private final RedactionLogCreatorService redactionLogCreatorService; + private final RedactionLogMergeService redactionLogMergeService; public AnnotateResponse annotate(@RequestBody AnnotateRequest annotateRequest) { @@ -158,13 +158,13 @@ public class RedactionController implements RedactionResource { } @Override - public RedactionLog getRedactionLogPreview(RedactionRequest redactionRequest) { + public RedactionLog getRedactionLog(RedactionRequest redactionRequest) { log.info("Requested preview for: {}", redactionRequest); dictionaryService.updateDictionary(redactionRequest.getDossierTemplateId(), redactionRequest.getDossierId()); var redactionLog = redactionStorageService.getRedactionLog(redactionRequest.getDossierId(), redactionRequest.getFileId()); - return redactionLogCreatorService.getRedactionLogPreview(redactionLog, redactionRequest.getDossierTemplateId(), redactionRequest.getManualRedactions()); + return redactionLogMergeService.mergeRedactionLogData(redactionLog, redactionRequest.getDossierTemplateId(), redactionRequest.getManualRedactions()); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java index fd460dbd..5afff568 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java @@ -218,7 +218,7 @@ public class DictionaryService { public float[] getRequestRemoveColor(String dossierTemplateId) { - return dictionariesByDossierTemplate.get(dossierTemplateId).getRequestAddColor(); + return dictionariesByDossierTemplate.get(dossierTemplateId).getRequestRemoveColor(); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java index 389b7186..ea1079a4 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java @@ -1,13 +1,6 @@ package com.iqser.red.service.redaction.v1.server.redaction.service; -import com.iqser.red.service.redaction.v1.model.FileAttribute; -import com.iqser.red.service.redaction.v1.model.ManualImageRecategorization; -import com.iqser.red.service.redaction.v1.model.ManualRedactionEntry; -import com.iqser.red.service.redaction.v1.model.ManualRedactions; -import com.iqser.red.service.redaction.v1.model.Point; -import com.iqser.red.service.redaction.v1.model.Rectangle; -import com.iqser.red.service.redaction.v1.model.SectionArea; -import com.iqser.red.service.redaction.v1.model.Status; +import com.iqser.red.service.redaction.v1.model.*; import com.iqser.red.service.redaction.v1.server.classification.model.*; import com.iqser.red.service.redaction.v1.server.redaction.model.Dictionary; import com.iqser.red.service.redaction.v1.server.redaction.model.*; @@ -15,10 +8,8 @@ import com.iqser.red.service.redaction.v1.server.redaction.utils.EntitySearchUti import com.iqser.red.service.redaction.v1.server.redaction.utils.IdBuilder; import com.iqser.red.service.redaction.v1.server.tableextraction.model.Cell; import com.iqser.red.service.redaction.v1.server.tableextraction.model.Table; - import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; - import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.kie.api.runtime.KieContainer; @@ -103,9 +94,9 @@ public class EntityRedactionService { List
tables = paragraph.getTables(); for (Table table : tables) { if (table.getColCount() == 2) { - sectionSearchableTextPairs.addAll(processTableAsOneText(classifiedDoc, table, manualRedactions, sectionNumber, dictionary, local, hintsPerSectionNumber, fileAttributes)); + sectionSearchableTextPairs.addAll(processTableAsOneText(classifiedDoc, table, sectionNumber, dictionary, local, hintsPerSectionNumber, fileAttributes)); } else { - sectionSearchableTextPairs.addAll(processTablePerRow(classifiedDoc, table, manualRedactions, sectionNumber, dictionary, local, hintsPerSectionNumber, fileAttributes)); + sectionSearchableTextPairs.addAll(processTablePerRow(classifiedDoc, table, sectionNumber, dictionary, local, hintsPerSectionNumber, fileAttributes)); } sectionNumber.incrementAndGet(); } @@ -169,7 +160,6 @@ public class EntityRedactionService { private List processTablePerRow(Document classifiedDoc, Table table, - ManualRedactions manualRedactions, AtomicInteger sectionNumber, Dictionary dictionary, boolean local, Map> hintsPerSectionNumber, @@ -198,7 +188,6 @@ public class EntityRedactionService { sectionText.getSectionAreas().add(sectionArea); sectionText.getTextBlocks().addAll(cell.getTextBlocks()); - addSectionToManualRedactions(cell.getTextBlocks(), manualRedactions, table.getHeadline(), sectionNumber.intValue()); int cellStart = start; if (!cell.isHeaderCell()) { @@ -259,7 +248,6 @@ public class EntityRedactionService { private List processTableAsOneText(Document classifiedDoc, Table table, - ManualRedactions manualRedactions, AtomicInteger sectionNumber, Dictionary dictionary, boolean local, Map> hintsPerSectionNumber, @@ -288,7 +276,6 @@ public class EntityRedactionService { for (TextBlock textBlock : cell.getTextBlocks()) { entireTableText.addAll(textBlock.getSequences()); } - addSectionToManualRedactions(cell.getTextBlocks(), manualRedactions, table.getHeadline(), sectionNumber.intValue()); } } @@ -348,7 +335,6 @@ public class EntityRedactionService { classifiedDoc.getSectionText().add(sectionText); } - addSectionToManualRedactions(paragraphTextBlocks, manualRedactions, headline, sectionNumber.intValue()); Set entities = findEntities(searchableText, headline, sectionNumber.intValue(), dictionary, local); surroundingWordsService.addSurroundingText(entities, searchableText, dictionary); @@ -396,26 +382,6 @@ public class EntityRedactionService { } - private void addSectionToManualRedactions(List textBlocks, ManualRedactions manualRedactions, - String section, int sectionNumber) { - - if (manualRedactions == null || manualRedactions.getEntriesToAdd().isEmpty()) { - return; - } - - for (TextBlock textBlock : textBlocks) { - for (ManualRedactionEntry manualRedactionEntry : manualRedactions.getEntriesToAdd()) { - for (Rectangle rectangle : manualRedactionEntry.getPositions()) { - if (textBlock.contains(rectangle)) { - manualRedactionEntry.setSection(section); - manualRedactionEntry.setSectionNumber(sectionNumber); - } - } - } - } - } - - private Image convertAndRecategorize(PdfImage pdfImage, int sectionNumber, String headline, ManualRedactions manualRedactions) { Image image = Image.builder() diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java index fa3f388c..17dfdf4b 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java @@ -1,55 +1,28 @@ package com.iqser.red.service.redaction.v1.server.redaction.service; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.kie.api.runtime.KieContainer; -import org.springframework.stereotype.Service; -import org.springframework.web.bind.annotation.RequestBody; - import com.iqser.red.service.file.management.v1.api.model.FileType; -import com.iqser.red.service.redaction.v1.model.AnalyzeRequest; -import com.iqser.red.service.redaction.v1.model.AnalyzeResult; -import com.iqser.red.service.redaction.v1.model.Comment; -import com.iqser.red.service.redaction.v1.model.IdRemoval; -import com.iqser.red.service.redaction.v1.model.ManualForceRedact; -import com.iqser.red.service.redaction.v1.model.ManualImageRecategorization; -import com.iqser.red.service.redaction.v1.model.ManualLegalBasisChange; -import com.iqser.red.service.redaction.v1.model.ManualRedactionEntry; -import com.iqser.red.service.redaction.v1.model.ManualRedactions; -import com.iqser.red.service.redaction.v1.model.Rectangle; -import com.iqser.red.service.redaction.v1.model.RedactionLog; -import com.iqser.red.service.redaction.v1.model.RedactionLogEntry; -import com.iqser.red.service.redaction.v1.model.SectionArea; -import com.iqser.red.service.redaction.v1.model.Status; +import com.iqser.red.service.redaction.v1.model.*; import com.iqser.red.service.redaction.v1.server.classification.model.Document; import com.iqser.red.service.redaction.v1.server.classification.model.SectionText; import com.iqser.red.service.redaction.v1.server.classification.model.Text; import com.iqser.red.service.redaction.v1.server.client.LegalBasisClient; import com.iqser.red.service.redaction.v1.server.exception.RedactionException; import com.iqser.red.service.redaction.v1.server.redaction.model.Dictionary; -import com.iqser.red.service.redaction.v1.server.redaction.model.DictionaryIncrement; -import com.iqser.red.service.redaction.v1.server.redaction.model.DictionaryVersion; -import com.iqser.red.service.redaction.v1.server.redaction.model.Entity; -import com.iqser.red.service.redaction.v1.server.redaction.model.EntityPositionSequence; -import com.iqser.red.service.redaction.v1.server.redaction.model.Image; -import com.iqser.red.service.redaction.v1.server.redaction.model.RedRectangle2D; -import com.iqser.red.service.redaction.v1.server.redaction.model.Section; -import com.iqser.red.service.redaction.v1.server.redaction.model.SectionSearchableTextPair; +import com.iqser.red.service.redaction.v1.server.redaction.model.*; import com.iqser.red.service.redaction.v1.server.redaction.utils.EntitySearchUtils; import com.iqser.red.service.redaction.v1.server.redaction.utils.IdBuilder; import com.iqser.red.service.redaction.v1.server.segmentation.PdfSegmentationService; import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService; - import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.kie.api.runtime.KieContainer; +import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.RequestBody; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; @Slf4j @Service @@ -87,13 +60,13 @@ public class ReanalyzeService { entityRedactionService.processDocument(classifiedDoc, analyzeRequest.getDossierTemplateId(), analyzeRequest.getManualRedactions(), analyzeRequest .getDossierId(), analyzeRequest.getFileAttributes()); - redactionLogCreatorService.createRedactionLog(classifiedDoc, pageCount, analyzeRequest.getManualRedactions(), analyzeRequest + redactionLogCreatorService.createRedactionLog(classifiedDoc, pageCount, analyzeRequest .getDossierTemplateId()); log.info("Redaction analysis successful..."); var legalBasis = legalBasisClient.getLegalBasisMapping(analyzeRequest.getDossierTemplateId()); - var redactionLog = new RedactionLog(classifiedDoc.getRedactionLogEntities(), legalBasis, classifiedDoc.getDictionaryVersion() + var redactionLog = new RedactionLog(1, classifiedDoc.getRedactionLogEntities(), legalBasis, classifiedDoc.getDictionaryVersion() .getDossierTemplateVersion(), classifiedDoc.getDictionaryVersion() .getDossierVersion(), classifiedDoc.getRulesVersion(), legalBasisClient.getVersion(analyzeRequest.getDossierTemplateId())); @@ -132,20 +105,12 @@ public class ReanalyzeService { DictionaryIncrement dictionaryIncrement = dictionaryService.getDictionaryIncrements(analyzeRequest.getDossierTemplateId(), new DictionaryVersion(redactionLog .getDictionaryVersion(), redactionLog.getDossierDictionaryVersion()), analyzeRequest.getDossierId()); - Set manualForceAndRemoveIds = getForceAndRemoveIds(analyzeRequest.getManualRedactions()); - Map> comments = null; - Set manualAdds = null; - - if (analyzeRequest.getManualRedactions() != null) { - // TODO comments will be removed from redactionLog, so we ignore this first. - comments = analyzeRequest.getManualRedactions().getComments(); - manualAdds = analyzeRequest.getManualRedactions().getEntriesToAdd(); - } + Set relevantManuallyModifiedAnnotationIds = getRelevantManuallyModifiedAnnotationIds(analyzeRequest.getManualRedactions()); Set sectionsToReanalyse = new HashSet<>(); Map> imageEntries = new HashMap<>(); for (RedactionLogEntry entry : redactionLog.getRedactionLogEntry()) { - if (entry.isManual() || manualForceAndRemoveIds.contains(entry.getId())) { + if (entry.isManual() || relevantManuallyModifiedAnnotationIds.contains(entry.getId())) { sectionsToReanalyse.add(entry.getSectionNumber()); } if (entry.isImage() || entry.getType().equals("image")) { @@ -159,23 +124,11 @@ public class ReanalyzeService { sectionsToReanalyse.add(sectionText.getSectionNumber()); } - if (manualAdds != null) { - for (SectionArea sectionArea : sectionText.getSectionAreas()) { - for (ManualRedactionEntry manualAdd : manualAdds) { - for (Rectangle manualPosition : manualAdd.getPositions()) { - if (sectionArea.contains(manualPosition)) { - manualAdd.setSection(sectionText.getHeadline()); - manualAdd.setSectionNumber(sectionText.getSectionNumber()); - } - } - } - } - } } log.info("Should reanalyze {} sections for request: {}", sectionsToReanalyse.size(), analyzeRequest); - if (sectionsToReanalyse.isEmpty() && (manualAdds == null || manualAdds.isEmpty())) { + if (sectionsToReanalyse.isEmpty()) { return finalizeAnalysis(analyzeRequest, startTime, redactionLog, text, dictionaryIncrement); } @@ -212,9 +165,9 @@ public class ReanalyzeService { .getImageRecategorizations() != null) { for (Image image : reanalysisSection.getImages()) { String imageId = IdBuilder.buildId(image.getPosition(), image.getPage()); - for(ManualImageRecategorization imageRecategorization: analyzeRequest.getManualRedactions() - .getImageRecategorizations()){ - if(imageRecategorization.getStatus().equals(Status.APPROVED) && imageRecategorization.getId().equals(imageId)){ + for (ManualImageRecategorization imageRecategorization : analyzeRequest.getManualRedactions() + .getImageRecategorizations()) { + if (imageRecategorization.getStatus().equals(Status.APPROVED) && imageRecategorization.getId().equals(imageId)) { image.setType(imageRecategorization.getType()); } } @@ -270,17 +223,15 @@ public class ReanalyzeService { List newRedactionLogEntries = new ArrayList<>(); for (int page = 1; page <= text.getNumberOfPages(); page++) { if (entitiesPerPage.get(page) != null) { - newRedactionLogEntries.addAll(redactionLogCreatorService.addEntries(entitiesPerPage, analyzeRequest.getManualRedactions(), page, analyzeRequest + newRedactionLogEntries.addAll(redactionLogCreatorService.addEntries(entitiesPerPage, page, analyzeRequest .getDossierTemplateId())); } if (imagesPerPage.get(page) != null) { - newRedactionLogEntries.addAll(redactionLogCreatorService.addImageEntries(imagesPerPage, analyzeRequest.getManualRedactions(), page, analyzeRequest + newRedactionLogEntries.addAll(redactionLogCreatorService.addImageEntries(imagesPerPage, page, analyzeRequest .getDossierTemplateId())); } - newRedactionLogEntries.addAll(redactionLogCreatorService.addManualAddEntries(manualAdds, comments, page, analyzeRequest - .getDossierTemplateId())); } redactionLog.getRedactionLogEntry().removeIf(entry -> sectionsToReanalyse.contains(entry.getSectionNumber())); @@ -310,7 +261,7 @@ public class ReanalyzeService { } - private Set getForceAndRemoveIds(ManualRedactions manualRedactions) { + private Set getRelevantManuallyModifiedAnnotationIds(ManualRedactions manualRedactions) { if (manualRedactions == null) { return new HashSet<>(); @@ -345,15 +296,10 @@ public class ReanalyzeService { private void excludeExcludedPages(RedactionLog redactionLog, Set excludedPages) { - redactionLog.getRedactionLogEntry().forEach(entry -> { - entry.getPositions().forEach(pos -> { - if (excludedPages != null && excludedPages.contains(pos.getPage())) { - entry.setExcluded(true); - } else { - entry.setExcluded(false); - } - }); - }); + redactionLog.getRedactionLogEntry().forEach(entry -> + entry.getPositions().forEach(pos -> + entry.setExcluded(excludedPages != null && excludedPages.contains(pos.getPage())) + )); } } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java index 885326c9..ec1f78db 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java @@ -1,6 +1,10 @@ package com.iqser.red.service.redaction.v1.server.redaction.service; -import com.iqser.red.service.redaction.v1.model.*; +import com.iqser.red.service.redaction.v1.model.CellRectangle; +import com.iqser.red.service.redaction.v1.model.Point; +import com.iqser.red.service.redaction.v1.model.Rectangle; +import com.iqser.red.service.redaction.v1.model.RedactionLogEntry; +import com.iqser.red.service.redaction.v1.model.SectionRectangle; import com.iqser.red.service.redaction.v1.server.classification.model.Document; import com.iqser.red.service.redaction.v1.server.classification.model.Paragraph; import com.iqser.red.service.redaction.v1.server.classification.model.TextBlock; @@ -9,17 +13,19 @@ import com.iqser.red.service.redaction.v1.server.parsing.model.TextPositionSeque import com.iqser.red.service.redaction.v1.server.redaction.model.Entity; import com.iqser.red.service.redaction.v1.server.redaction.model.EntityPositionSequence; import com.iqser.red.service.redaction.v1.server.redaction.model.Image; -import com.iqser.red.service.redaction.v1.server.redaction.model.ReasonHolder; import com.iqser.red.service.redaction.v1.server.redaction.utils.IdBuilder; import com.iqser.red.service.redaction.v1.server.tableextraction.model.AbstractTextContainer; import com.iqser.red.service.redaction.v1.server.tableextraction.model.Cell; import com.iqser.red.service.redaction.v1.server.tableextraction.model.Table; import lombok.RequiredArgsConstructor; import org.apache.commons.collections4.CollectionUtils; -import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; -import java.util.*; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; @Service @@ -29,10 +35,7 @@ public class RedactionLogCreatorService { private final DictionaryService dictionaryService; - public void createRedactionLog(Document classifiedDoc, int numberOfPages, ManualRedactions manualRedactions, - String dossierTemplateId) { - - Set manualRedactionPages = getManualRedactionPages(manualRedactions); + public void createRedactionLog(Document classifiedDoc, int numberOfPages, String dossierTemplateId) { for (int page = 1; page <= numberOfPages; page++) { @@ -40,24 +43,18 @@ public class RedactionLogCreatorService { if (classifiedDoc.getEntities().get(page) != null) { classifiedDoc.getRedactionLogEntities() - .addAll(addEntries(classifiedDoc.getEntities(), manualRedactions, page, dossierTemplateId)); - } - - if (manualRedactionPages.contains(page)) { - classifiedDoc.getRedactionLogEntities() - .addAll(addManualAddEntries(manualRedactions.getEntriesToAdd(), manualRedactions.getComments(), page, dossierTemplateId)); + .addAll(addEntries(classifiedDoc.getEntities(), page, dossierTemplateId)); } if (classifiedDoc.getImages().get(page) != null && !classifiedDoc.getImages().get(page).isEmpty()) { classifiedDoc.getRedactionLogEntities() - .addAll(addImageEntries(classifiedDoc.getImages(), manualRedactions, page, dossierTemplateId)); + .addAll(addImageEntries(classifiedDoc.getImages(), page, dossierTemplateId)); } } } - public List addImageEntries(Map> images, ManualRedactions manualRedactions, - int pageNumber, String dossierTemplateId) { + public List addImageEntries(Map> images, int pageNumber, String dossierTemplateId) { List redactionLogEntities = new ArrayList<>(); @@ -67,7 +64,7 @@ public class RedactionLogCreatorService { RedactionLogEntry redactionLogEntry = RedactionLogEntry.builder() .id(id) - .color(getColorForImage(image.getType(), dossierTemplateId, false, image.isRedaction())) + .color(getColor(image.getType(), dossierTemplateId, image.isRedaction())) .isImage(true) .type(image.getType()) .redacted(image.isRedaction()) @@ -86,7 +83,6 @@ public class RedactionLogCreatorService { .imageHasTransparency(image.isHasTransparency()) .build(); - processImageEntry(manualRedactions, dossierTemplateId, image, redactionLogEntry); redactionLogEntities.add(redactionLogEntry); } @@ -94,135 +90,8 @@ public class RedactionLogCreatorService { return redactionLogEntities; } - private void processImageEntry(ManualRedactions manualRedactions, String dossierTemplateId, ReasonHolder image, RedactionLogEntry redactionLogEntry) { - if (manualRedactions != null && !manualRedactions.getImageRecategorizations().isEmpty()) { - for (ManualImageRecategorization recategorization : manualRedactions.getImageRecategorizations()) { - if (recategorization.getId().equals(redactionLogEntry.getId())) { - String manualOverrideReason = null; - if (recategorization.getStatus().equals(Status.APPROVED)) { - redactionLogEntry.setStatus(Status.APPROVED); - redactionLogEntry.setType(recategorization.getType()); - manualOverrideReason = mergeReasonIfNecessary(image.getRedactionReason(), ", recategorized by manual override"); - } else if (recategorization.getStatus().equals(Status.REQUESTED)) { - manualOverrideReason = mergeReasonIfNecessary(image.getRedactionReason(), ", requested to recategorize"); - redactionLogEntry.setStatus(Status.REQUESTED); - redactionLogEntry.setColor(getColorForImage(redactionLogEntry.getType(), dossierTemplateId, true, redactionLogEntry.isRedacted())); - redactionLogEntry.setRecategorizationType(recategorization.getType()); - } else { - redactionLogEntry.setStatus(Status.DECLINED); - } - image.setRedactionReason(manualOverrideReason != null ? manualOverrideReason : image.getRedactionReason()); - redactionLogEntry.setReason(manualOverrideReason); - redactionLogEntry.setManual(true); - redactionLogEntry.setManualRedactionType(ManualRedactionType.RECATEGORIZE); - } - } - } - - if (manualRedactions != null && !manualRedactions.getIdsToRemove().isEmpty()) { - for (IdRemoval manualRemoval : manualRedactions.getIdsToRemove()) { - if (manualRemoval.getId().equals(redactionLogEntry.getId())) { - String manualOverrideReason = null; - if (manualRemoval.getStatus().equals(Status.APPROVED)) { - image.setRedaction(false); - redactionLogEntry.setRedacted(false); - redactionLogEntry.setStatus(Status.APPROVED); - manualOverrideReason = mergeReasonIfNecessary(image.getRedactionReason(), ", removed by manual override"); - redactionLogEntry.setColor(getColorForImage(redactionLogEntry.getType(), dossierTemplateId, false, redactionLogEntry.isRedacted())); - } else if (manualRemoval.getStatus().equals(Status.REQUESTED)) { - manualOverrideReason = mergeReasonIfNecessary(image.getRedactionReason(), ", requested to remove"); - redactionLogEntry.setStatus(Status.REQUESTED); - redactionLogEntry.setColor(getColorForImage(redactionLogEntry.getType(), dossierTemplateId, true, redactionLogEntry.isRedacted())); - } else { - redactionLogEntry.setStatus(Status.DECLINED); - } - - image.setRedactionReason(manualOverrideReason != null ? manualOverrideReason : image.getRedactionReason()); - redactionLogEntry.setReason(manualOverrideReason); - redactionLogEntry.setManual(true); - redactionLogEntry.setManualRedactionType(ManualRedactionType.REMOVE); - redactionLogEntry.setDictionaryEntry(manualRemoval.isRemoveFromDictionary()); - redactionLogEntry.setDossierDictionaryEntry(manualRemoval.isRemoveFromDictionary()); - } - } - } - - if (manualRedactions != null && !manualRedactions.getForceRedacts().isEmpty()) { - for (ManualForceRedact manualForceRedact : manualRedactions.getForceRedacts()) { - if (manualForceRedact.getId().equals(redactionLogEntry.getId())) { - String manualOverrideReason = null; - if (manualForceRedact.getStatus().equals(Status.APPROVED)) { - image.setRedaction(true); - redactionLogEntry.setRedacted(true); - redactionLogEntry.setStatus(Status.APPROVED); - redactionLogEntry.setColor(getColorForImage(redactionLogEntry.getType(), dossierTemplateId, false, redactionLogEntry.isRedacted())); - manualOverrideReason = mergeReasonIfNecessary(image.getRedactionReason(), ", forced by manual override"); - redactionLogEntry.setLegalBasis(manualForceRedact.getLegalBasis()); - } else if (manualForceRedact.getStatus().equals(Status.REQUESTED)) { - manualOverrideReason = mergeReasonIfNecessary(image.getRedactionReason(), ", requested to force redact"); - redactionLogEntry.setStatus(Status.REQUESTED); - redactionLogEntry.setColor(getColorForImage(redactionLogEntry.getType(), dossierTemplateId, true, redactionLogEntry.isRedacted())); - redactionLogEntry.setLegalBasis(manualForceRedact.getLegalBasis()); - } else { - redactionLogEntry.setStatus(Status.DECLINED); - } - - image.setRedactionReason(manualOverrideReason != null ? manualOverrideReason : image.getRedactionReason()); - redactionLogEntry.setReason(manualOverrideReason); - redactionLogEntry.setManual(true); - redactionLogEntry.setManualRedactionType(ManualRedactionType.FORCE_REDACT); - } - } - } - - - if (manualRedactions != null && !manualRedactions.getManualLegalBasisChanges().isEmpty()) { - for (ManualLegalBasisChange manualLegalBasisChange : manualRedactions.getManualLegalBasisChanges()) { - if (manualLegalBasisChange.getId().equals(redactionLogEntry.getId())) { - String manualOverrideReason = null; - if (manualLegalBasisChange.getStatus().equals(Status.APPROVED)) { - redactionLogEntry.setStatus(Status.APPROVED); - manualOverrideReason = mergeReasonIfNecessary(image.getRedactionReason(), ", legal basis was manually changed"); - redactionLogEntry.setLegalBasis(manualLegalBasisChange.getLegalBasis()); - } else if (manualLegalBasisChange.getStatus().equals(Status.REQUESTED)) { - manualOverrideReason = mergeReasonIfNecessary(image.getRedactionReason(), ", legal basis change requested"); - redactionLogEntry.setStatus(Status.REQUESTED); - redactionLogEntry.setColor(getColorForImage(redactionLogEntry.getType(), dossierTemplateId, true, redactionLogEntry.isRedacted())); - redactionLogEntry.setLegalBasisChangeValue(manualLegalBasisChange.getLegalBasis()); - } else { - redactionLogEntry.setStatus(Status.DECLINED); - } - - image.setRedactionReason(manualOverrideReason != null ? manualOverrideReason : image.getRedactionReason()); - redactionLogEntry.setReason(manualOverrideReason); - redactionLogEntry.setManual(true); - redactionLogEntry.setManualRedactionType(ManualRedactionType.LEGAL_BASIS_CHANGE); - } - } - } - } - - - private Set getManualRedactionPages(ManualRedactions manualRedactions) { - - Set manualRedactionPages = new HashSet<>(); - - if (manualRedactions == null) { - return manualRedactionPages; - } - - manualRedactions.getEntriesToAdd().forEach(entry -> { - entry.getPositions().forEach(pos -> { - manualRedactionPages.add(pos.getPage()); - }); - }); - return manualRedactionPages; - } - - - public List addEntries(Map> entities, ManualRedactions manualRedactions, - int page, String dossierTemplateId) { + public List addEntries(Map> entities, int page, String dossierTemplateId) { List redactionLogEntities = new ArrayList<>(); @@ -245,7 +114,6 @@ public class RedactionLogCreatorService { } redactionLogEntry.setId(entityPositionSequence.getId()); - processRedactionLogEntry(manualRedactions, dossierTemplateId, redactionLogEntry, entity); if (CollectionUtils.isNotEmpty(entityPositionSequence.getSequences())) { List rectanglesPerLine = getRectanglesPerLine(entityPositionSequence.getSequences() @@ -269,112 +137,6 @@ public class RedactionLogCreatorService { return redactionLogEntities; } - private void processRedactionLogEntry(ManualRedactions manualRedactions, String dossierTemplateId, RedactionLogEntry redactionLogEntry, ReasonHolder reasonHolder) { - - List comments = null; - - if (manualRedactions != null && !manualRedactions.getIdsToRemove().isEmpty()) { - for (IdRemoval manualRemoval : manualRedactions.getIdsToRemove()) { - if (manualRemoval.getId().equals(redactionLogEntry.getId())) { - comments = manualRedactions.getComments().get(manualRemoval.getId()); - String manualOverrideReason = null; - if (manualRemoval.getStatus().equals(Status.APPROVED)) { - reasonHolder.setRedaction(false); - redactionLogEntry.setRedacted(false); - redactionLogEntry.setStatus(Status.APPROVED); - manualOverrideReason = mergeReasonIfNecessary(reasonHolder.getRedactionReason(), ", removed by manual override"); - redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, false, redactionLogEntry.isRedacted())); - } else if (manualRemoval.getStatus().equals(Status.REQUESTED)) { - manualOverrideReason = mergeReasonIfNecessary(reasonHolder.getRedactionReason(), ", requested to remove"); - redactionLogEntry.setStatus(Status.REQUESTED); - redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, true, redactionLogEntry.isRedacted())); - } else { - redactionLogEntry.setStatus(Status.DECLINED); - } - - reasonHolder.setRedactionReason(manualOverrideReason != null ? manualOverrideReason : reasonHolder.getRedactionReason()); - redactionLogEntry.setReason(manualOverrideReason); - redactionLogEntry.setManual(true); - redactionLogEntry.setManualRedactionType(ManualRedactionType.REMOVE); - redactionLogEntry.setDictionaryEntry(manualRemoval.isRemoveFromDictionary()); - redactionLogEntry.setDossierDictionaryEntry(manualRemoval.isRemoveFromDictionary()); - } - } - } - - if (manualRedactions != null && !manualRedactions.getForceRedacts().isEmpty()) { - for (ManualForceRedact manualForceRedact : manualRedactions.getForceRedacts()) { - if (manualForceRedact.getId().equals(redactionLogEntry.getId())) { - String manualOverrideReason = null; - if (manualForceRedact.getStatus().equals(Status.APPROVED)) { - reasonHolder.setRedaction(true); - redactionLogEntry.setRedacted(true); - redactionLogEntry.setStatus(Status.APPROVED); - redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, false, redactionLogEntry.isRedacted())); - manualOverrideReason = mergeReasonIfNecessary(reasonHolder.getRedactionReason(), ", forced by manual override"); - redactionLogEntry.setLegalBasis(manualForceRedact.getLegalBasis()); - } else if (manualForceRedact.getStatus().equals(Status.REQUESTED)) { - manualOverrideReason = mergeReasonIfNecessary(reasonHolder.getRedactionReason(), ", requested to force redact"); - redactionLogEntry.setStatus(Status.REQUESTED); - redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, true, redactionLogEntry.isRedacted())); - redactionLogEntry.setLegalBasis(manualForceRedact.getLegalBasis()); - } else { - redactionLogEntry.setStatus(Status.DECLINED); - } - - reasonHolder.setRedactionReason(manualOverrideReason != null ? manualOverrideReason : reasonHolder.getRedactionReason()); - redactionLogEntry.setReason(manualOverrideReason); - redactionLogEntry.setManual(true); - redactionLogEntry.setManualRedactionType(ManualRedactionType.FORCE_REDACT); - - } - } - } - - if (manualRedactions != null && !manualRedactions.getManualLegalBasisChanges().isEmpty()) { - for (ManualLegalBasisChange manualLegalBasisChange : manualRedactions.getManualLegalBasisChanges()) { - if (manualLegalBasisChange.getId().equals(redactionLogEntry.getId())) { - String manualOverrideReason = null; - if (manualLegalBasisChange.getStatus().equals(Status.APPROVED)) { - redactionLogEntry.setStatus(Status.APPROVED); - manualOverrideReason = mergeReasonIfNecessary(reasonHolder.getRedactionReason(), ", legal basis was manually changed"); - redactionLogEntry.setLegalBasis(manualLegalBasisChange.getLegalBasis()); - } else if (manualLegalBasisChange.getStatus().equals(Status.REQUESTED)) { - manualOverrideReason = mergeReasonIfNecessary(reasonHolder.getRedactionReason(), ", legal basis change requested"); - redactionLogEntry.setStatus(Status.REQUESTED); - redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, true, redactionLogEntry.isRedacted())); - redactionLogEntry.setLegalBasisChangeValue(manualLegalBasisChange.getLegalBasis()); - } else { - redactionLogEntry.setStatus(Status.DECLINED); - } - - reasonHolder.setRedactionReason(manualOverrideReason != null ? manualOverrideReason : reasonHolder.getRedactionReason()); - redactionLogEntry.setReason(manualOverrideReason); - redactionLogEntry.setManual(true); - redactionLogEntry.setManualRedactionType(ManualRedactionType.LEGAL_BASIS_CHANGE); - } - } - } - - - if (manualRedactions != null) { - comments = manualRedactions.getComments().get(redactionLogEntry.getId()); - } - - redactionLogEntry.setComments(comments); - } - - private String mergeReasonIfNecessary(String currentReason, String addition) { - if (currentReason != null) { - if (!currentReason.contains(addition)) { - return currentReason + addition; - } - return currentReason; - } else { - return ""; - } - } - private List getRectanglesPerLine(List textPositions, int page) { @@ -403,73 +165,10 @@ public class RedactionLogCreatorService { } - public List addManualAddEntries(Set manualAdds, - Map> comments, int page, - String dossierTemplateId) { - - List redactionLogEntities = new ArrayList<>(); - - if (manualAdds == null) { - return redactionLogEntities; - } - - for (ManualRedactionEntry manualRedactionEntry : manualAdds) { - - String id = manualRedactionEntry.getId(); - - RedactionLogEntry redactionLogEntry = createRedactionLogEntry(manualRedactionEntry, id, dossierTemplateId); - - List rectanglesOnPage = new ArrayList<>(); - for (Rectangle rectangle : manualRedactionEntry.getPositions()) { - if (page == rectangle.getPage()) { - rectanglesOnPage.add(rectangle); - redactionLogEntry.getPositions().add(rectangle); - } - } - - redactionLogEntry.setComments(comments.get(id)); - if (!rectanglesOnPage.isEmpty() && !approvedAndShouldBeInDictionary(manualRedactionEntry)) { - redactionLogEntities.add(redactionLogEntry); - } - } - - return redactionLogEntities; - } - - - private boolean approvedAndShouldBeInDictionary(ManualRedactionEntry manualRedactionEntry) { - - return manualRedactionEntry.getStatus().equals(Status.APPROVED) && manualRedactionEntry.isAddToDictionary(); - } - - - private RedactionLogEntry createRedactionLogEntry(ManualRedactionEntry manualRedactionEntry, String id, - String dossierTemplateId) { - - return RedactionLogEntry.builder() - .id(id) - .color(getColorForManualAdd(manualRedactionEntry.getType(), dossierTemplateId, manualRedactionEntry.getStatus())) - .reason(manualRedactionEntry.getReason()) - .legalBasis(manualRedactionEntry.getLegalBasis()) - .value(manualRedactionEntry.getValue()) - .type(manualRedactionEntry.getType()) - .redacted(true) - .isHint(false) - .section(manualRedactionEntry.getSection()) - .sectionNumber(manualRedactionEntry.getSectionNumber()) - .manual(true) - .status(manualRedactionEntry.getStatus()) - .manualRedactionType(ManualRedactionType.ADD) - .isDictionaryEntry(false) - .isDossierDictionaryEntry(manualRedactionEntry.isAddToDossierDictionary()) - .build(); - } - - private RedactionLogEntry createRedactionLogEntry(Entity entity, String dossierTemplateId) { return RedactionLogEntry.builder() - .color(getColor(entity.getType(), dossierTemplateId, false, entity.isRedaction())) + .color(getColor(entity.getType(), dossierTemplateId, entity.isRedaction())) .reason(entity.getRedactionReason()) .legalBasis(entity.getLegalBasis()) .value(entity.getWord()) @@ -490,59 +189,6 @@ public class RedactionLogCreatorService { } - private float[] getColor(String type, String dossierTemplateId, boolean requestedToRemove, boolean isRedaction) { - - if (requestedToRemove) { - return dictionaryService.getRequestRemoveColor(dossierTemplateId); - } - if (!isRedaction && !isHint(type, dossierTemplateId)) { - return dictionaryService.getNotRedactedColor(dossierTemplateId); - } - return dictionaryService.getColor(type, dossierTemplateId); - } - - - private float[] getColorForManualAdd(String type, String dossierTemplateId, Status status) { - - if (status.equals(Status.REQUESTED)) { - return dictionaryService.getRequestAddColor(dossierTemplateId); - } else if (status.equals(Status.DECLINED)) { - return dictionaryService.getNotRedactedColor(dossierTemplateId); - } - return getColor(type, dossierTemplateId); - } - - - private float[] getColor(String type, String dossierTemplateId) { - - return dictionaryService.getColor(type, dossierTemplateId); - } - - - private float[] getColorForImage(String type, String dossierTemplateId, boolean requestedToRemove, boolean isRedaction) { - - if (requestedToRemove) { - return dictionaryService.getRequestRemoveColor(dossierTemplateId); - } - if (!isRedaction && !dictionaryService.isHint(type, dossierTemplateId)) { - return dictionaryService.getNotRedactedColor(dossierTemplateId); - } - return dictionaryService.getColor(type, dossierTemplateId); - } - - - private boolean isHint(String type, String dossierTemplateId) { - - return dictionaryService.isHint(type, dossierTemplateId); - } - - - private boolean isRecommendation(String type, String dossierTemplateId) { - - return dictionaryService.isRecommendation(type, dossierTemplateId); - } - - private void addSectionGrid(Document classifiedDoc, int page) { for (Paragraph paragraph : classifiedDoc.getParagraphs()) { @@ -586,165 +232,26 @@ public class RedactionLogCreatorService { } } - public RedactionLog getRedactionLogPreview(RedactionLog redactionLog, String dossierTemplateId, ManualRedactions manualRedactions) { + private float[] getColor(String type, String dossierTemplateId, boolean isRedaction) { - var manualRedactionPages = getManualRedactionPages(manualRedactions); - - // generate all manual entries - var manualRedactionLogEntries = new HashMap(); - for (var page : manualRedactionPages) { - - var pageEntries = addManualAddEntries(manualRedactions.getEntriesToAdd(), manualRedactions.getComments(), page, dossierTemplateId); - - for (var entry : pageEntries) { - manualRedactionLogEntries.put(entry.getId(), entry); - } + if (!isRedaction && !isHint(type, dossierTemplateId)) { + return dictionaryService.getNotRedactedColor(dossierTemplateId); } - - for (var manualEntry : manualRedactionLogEntries.values()) { - var existingEntry = redactionLog.getRedactionLogEntry().stream().filter(e -> e.getId().equals(manualEntry.getId())).findAny(); - if (existingEntry.isPresent()) { - // if it has already been processed of sorts, update it - BeanUtils.copyProperties(manualEntry, existingEntry.get()); - } else { - // not yet in the redaction-log - add it - redactionLog.getRedactionLogEntry().add(manualEntry); - } - } - - - Set entriesToRemoveBecauseOfUndoActions = new HashSet<>(); - for (RedactionLogEntry entry : redactionLog.getRedactionLogEntry()) { - - var reasonHolder = new PreviewReasonHolder(entry); - - if (entry.isImage()) { - processImageEntry(manualRedactions, dossierTemplateId, reasonHolder, entry); - } - - processRedactionLogEntry(manualRedactions, dossierTemplateId, entry, reasonHolder); - var shouldRemove = postProcessRedactionLogEntryForDeletedManualRedactions(manualRedactions, entry); - if (shouldRemove) { - entriesToRemoveBecauseOfUndoActions.add(entry.getId()); - } - - - entry.setComments(manualRedactions.getComments().get(entry.getId())); - } - - redactionLog.setRedactionLogEntry(redactionLog.getRedactionLogEntry() - .stream().filter(entry -> !entriesToRemoveBecauseOfUndoActions.contains(entry.getId())).collect(Collectors.toList())); - - - handleAddToDictionary(redactionLog, manualRedactions, dossierTemplateId); - - return redactionLog; - } - - private boolean postProcessRedactionLogEntryForDeletedManualRedactions(ManualRedactions manualRedactions, RedactionLogEntry entry) { - if (entry.isManual()) { - - if (entry.getManualRedactionType() == ManualRedactionType.ADD) { - var manualRedactionExists = manualRedactions.getEntriesToAdd().stream().anyMatch(entryToAdd -> entryToAdd.getId().equalsIgnoreCase(entry.getId())); - return !manualRedactionExists; - } - - if (entry.getManualRedactionType() == ManualRedactionType.FORCE_REDACT) { - var forceRedactExists = manualRedactions.getForceRedacts().stream().anyMatch(entryToAdd -> entryToAdd.getId().equalsIgnoreCase(entry.getId())); - if (!forceRedactExists) { - entry.setRedacted(false); - entry.setManual(false); - entry.setStatus(null); - } - } - - if (entry.getManualRedactionType() == ManualRedactionType.REMOVE) { - var removeIdExists = manualRedactions.getIdsToRemove().stream().anyMatch(entryToAdd -> entryToAdd.getId().equalsIgnoreCase(entry.getId())); - if (!removeIdExists) { - entry.setRedacted(true); - entry.setManual(false); - entry.setStatus(null); - } - } - - // Cannot undo Already Approved change because UI won't allow it for now - if (Status.REQUESTED.equals(entry.getStatus())) { - if (entry.getManualRedactionType() == ManualRedactionType.RECATEGORIZE) { - var recategorizeExists = manualRedactions.getImageRecategorizations().stream().anyMatch(entryToAdd -> entryToAdd.getId().equalsIgnoreCase(entry.getId())); - if (!recategorizeExists) { - entry.setManual(false); - entry.setStatus(null); - entry.setRecategorizationType(null); - } - } - if (entry.getManualRedactionType() == ManualRedactionType.LEGAL_BASIS_CHANGE) { - var legalBasisChangeExists = manualRedactions.getImageRecategorizations().stream().anyMatch(entryToAdd -> entryToAdd.getId().equalsIgnoreCase(entry.getId())); - if (!legalBasisChangeExists) { - entry.setManual(false); - entry.setStatus(null); - entry.setLegalBasisChangeValue(null); - } - } - } - } - - return false; + return dictionaryService.getColor(type, dossierTemplateId); } - private void handleAddToDictionary(RedactionLog redactionLog, ManualRedactions manualRedactions, String dossierTemplateId) { + private boolean isHint(String type, String dossierTemplateId) { - for (var manualRedaction : manualRedactions.getEntriesToAdd()) { - - // not yet processed, and dictionary modifying, show in redaction-log preview - if (manualRedaction.getProcessedDate() == null && manualRedaction.isAddToDictionary() || manualRedaction.isAddToDossierDictionary()) { - var redactionLogEntry = createRedactionLogEntry(manualRedaction, manualRedaction.getId(), dossierTemplateId); - redactionLogEntry.setDictionaryEntry(manualRedaction.isAddToDictionary()); - redactionLogEntry.setDossierDictionaryEntry(manualRedaction.isAddToDossierDictionary()); - redactionLogEntry.setPositions(manualRedaction.getPositions()); - - - var found = redactionLog.getRedactionLogEntry().stream().filter(r -> r.getId().equalsIgnoreCase(redactionLogEntry.getId())).findAny(); - if (found.isPresent()) { - found.get().setDictionaryEntry(manualRedaction.isAddToDictionary()); - found.get().setDossierDictionaryEntry(manualRedaction.isAddToDossierDictionary()); - found.get().setPositions(manualRedaction.getPositions()); - } else { - redactionLog.getRedactionLogEntry().add(redactionLogEntry); - } - - - } - } + return dictionaryService.isHint(type, dossierTemplateId); } - public static class PreviewReasonHolder implements ReasonHolder { - private final RedactionLogEntry entry; + private boolean isRecommendation(String type, String dossierTemplateId) { - public PreviewReasonHolder(RedactionLogEntry entry) { - this.entry = entry; - } - - @Override - public String getRedactionReason() { - return entry.getReason(); - } - - @Override - public void setRedactionReason(String reason) { - entry.setReason(reason); - } - - @Override - public boolean isRedaction() { - return entry.isRedacted(); - } - - @Override - public void setRedaction(boolean value) { - entry.setRedacted(value); - } + return dictionaryService.isRecommendation(type, dossierTemplateId); } + + } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java new file mode 100644 index 00000000..ac19c236 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java @@ -0,0 +1,241 @@ +package com.iqser.red.service.redaction.v1.server.redaction.service; + +import com.iqser.red.service.redaction.v1.model.*; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Service +@RequiredArgsConstructor +public class RedactionLogMergeService { + + private final DictionaryService dictionaryService; + + + public RedactionLog mergeRedactionLogData(RedactionLog redactionLog, String dossierTemplateId, ManualRedactions manualRedactions) { + + if (manualRedactions != null) { + + var manualRedactionLogEntries = addManualAddEntries(manualRedactions.getEntriesToAdd(), manualRedactions.getComments(), dossierTemplateId); + + redactionLog.getRedactionLogEntry().addAll(manualRedactionLogEntries); + + for (RedactionLogEntry entry : redactionLog.getRedactionLogEntry()) { + processRedactionLogEntry(manualRedactions, dossierTemplateId, entry); + entry.setComments(manualRedactions.getComments().get(entry.getId())); + } + + } + + return redactionLog; + } + + + private void processRedactionLogEntry(ManualRedactions manualRedactions, String dossierTemplateId, RedactionLogEntry redactionLogEntry) { + + List comments = null; + + + if (manualRedactions != null && !manualRedactions.getImageRecategorizations().isEmpty()) { + for (ManualImageRecategorization recategorization : manualRedactions.getImageRecategorizations()) { + if (recategorization.getId().equals(redactionLogEntry.getId())) { + String manualOverrideReason = null; + if (recategorization.getStatus().equals(Status.APPROVED)) { + redactionLogEntry.setStatus(Status.APPROVED); + redactionLogEntry.setType(recategorization.getType()); + manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", recategorized by manual override"); + } else if (recategorization.getStatus().equals(Status.REQUESTED)) { + manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", requested to recategorize"); + redactionLogEntry.setStatus(Status.REQUESTED); + redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, false, redactionLogEntry.isRedacted(), false)); + redactionLogEntry.setRecategorizationType(recategorization.getType()); + } else { + redactionLogEntry.setStatus(Status.DECLINED); + } + + redactionLogEntry.setReason(manualOverrideReason); + redactionLogEntry.setManual(true); + redactionLogEntry.setManualRedactionType(ManualRedactionType.RECATEGORIZE); + } + } + } + + if (manualRedactions != null && !manualRedactions.getIdsToRemove().isEmpty()) { + for (IdRemoval manualRemoval : manualRedactions.getIdsToRemove()) { + if (manualRemoval.getId().equals(redactionLogEntry.getId())) { + comments = manualRedactions.getComments().get(manualRemoval.getId()); + String manualOverrideReason = null; + if (manualRemoval.getStatus().equals(Status.APPROVED)) { + redactionLogEntry.setRedacted(false); + redactionLogEntry.setStatus(Status.APPROVED); + manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", removed by manual override"); + redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, false, redactionLogEntry.isRedacted(), true)); + } else if (manualRemoval.getStatus().equals(Status.REQUESTED)) { + manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", requested to remove"); + redactionLogEntry.setStatus(Status.REQUESTED); + redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, true, redactionLogEntry.isRedacted(), false)); + } else { + redactionLogEntry.setStatus(Status.DECLINED); + } + + redactionLogEntry.setReason(manualOverrideReason); + redactionLogEntry.setManual(true); + redactionLogEntry.setManualRedactionType(ManualRedactionType.REMOVE); + redactionLogEntry.setDictionaryEntry(manualRemoval.isRemoveFromDictionary()); + redactionLogEntry.setDossierDictionaryEntry(manualRemoval.isRemoveFromDictionary()); + } + } + } + + if (manualRedactions != null && !manualRedactions.getForceRedacts().isEmpty()) { + for (ManualForceRedact manualForceRedact : manualRedactions.getForceRedacts()) { + if (manualForceRedact.getId().equals(redactionLogEntry.getId())) { + String manualOverrideReason = null; + if (manualForceRedact.getStatus().equals(Status.APPROVED)) { + redactionLogEntry.setRedacted(true); + redactionLogEntry.setStatus(Status.APPROVED); + redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, false, redactionLogEntry.isRedacted(), false)); + manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", forced by manual override"); + redactionLogEntry.setLegalBasis(manualForceRedact.getLegalBasis()); + } else if (manualForceRedact.getStatus().equals(Status.REQUESTED)) { + manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", requested to force redact"); + redactionLogEntry.setStatus(Status.REQUESTED); + redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, true, redactionLogEntry.isRedacted(), false)); + redactionLogEntry.setLegalBasis(manualForceRedact.getLegalBasis()); + } else { + redactionLogEntry.setStatus(Status.DECLINED); + } + + redactionLogEntry.setReason(manualOverrideReason); + redactionLogEntry.setManual(true); + redactionLogEntry.setManualRedactionType(ManualRedactionType.FORCE_REDACT); + + } + } + } + + if (manualRedactions != null && !manualRedactions.getManualLegalBasisChanges().isEmpty()) { + for (ManualLegalBasisChange manualLegalBasisChange : manualRedactions.getManualLegalBasisChanges()) { + if (manualLegalBasisChange.getId().equals(redactionLogEntry.getId())) { + String manualOverrideReason = null; + if (manualLegalBasisChange.getStatus().equals(Status.APPROVED)) { + redactionLogEntry.setStatus(Status.APPROVED); + manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", legal basis was manually changed"); + redactionLogEntry.setLegalBasis(manualLegalBasisChange.getLegalBasis()); + } else if (manualLegalBasisChange.getStatus().equals(Status.REQUESTED)) { + manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", legal basis change requested"); + redactionLogEntry.setStatus(Status.REQUESTED); + redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, true, redactionLogEntry.isRedacted(), false)); + redactionLogEntry.setLegalBasisChangeValue(manualLegalBasisChange.getLegalBasis()); + } else { + redactionLogEntry.setStatus(Status.DECLINED); + } + + redactionLogEntry.setReason(manualOverrideReason); + redactionLogEntry.setManual(true); + redactionLogEntry.setManualRedactionType(ManualRedactionType.LEGAL_BASIS_CHANGE); + } + } + } + + + if (manualRedactions != null) { + comments = manualRedactions.getComments().get(redactionLogEntry.getId()); + } + + redactionLogEntry.setComments(comments); + } + + private String mergeReasonIfNecessary(String currentReason, String addition) { + if (currentReason != null) { + if (!currentReason.contains(addition)) { + return currentReason + addition; + } + return currentReason; + } else { + return ""; + } + } + + + public List addManualAddEntries(Set manualAdds, Map> comments, String dossierTemplateId) { + + List redactionLogEntries = new ArrayList<>(); + + for (ManualRedactionEntry manualRedactionEntry : manualAdds) { + + if (!approvedAndShouldBeInDictionary(manualRedactionEntry)) { + RedactionLogEntry redactionLogEntry = createRedactionLogEntry(manualRedactionEntry, manualRedactionEntry.getId(), dossierTemplateId); + redactionLogEntry.setPositions(manualRedactionEntry.getPositions()); + redactionLogEntry.setComments(comments.get(manualRedactionEntry.getId())); + } + } + + return redactionLogEntries; + } + + + private boolean approvedAndShouldBeInDictionary(ManualRedactionEntry manualRedactionEntry) { + + return manualRedactionEntry.getStatus().equals(Status.APPROVED) && (manualRedactionEntry.isAddToDictionary() || manualRedactionEntry.isAddToDossierDictionary()); + } + + + private RedactionLogEntry createRedactionLogEntry(ManualRedactionEntry manualRedactionEntry, String id, + String dossierTemplateId) { + + return RedactionLogEntry.builder() + .id(id) + .color(getColorForManualAdd(manualRedactionEntry.getType(), dossierTemplateId, manualRedactionEntry.getStatus())) + .reason(manualRedactionEntry.getReason()) + .isDictionaryEntry(manualRedactionEntry.isAddToDictionary()) + .isDossierDictionaryEntry(manualRedactionEntry.isAddToDossierDictionary()) + .legalBasis(manualRedactionEntry.getLegalBasis()) + .value(manualRedactionEntry.getValue()) + .type(manualRedactionEntry.getType()) + .redacted(true) + .isHint(false) + .section(null) + .sectionNumber(-1) + .manual(true) + .status(manualRedactionEntry.getStatus()) + .manualRedactionType(ManualRedactionType.ADD) + .isDictionaryEntry(false) + .isDossierDictionaryEntry(manualRedactionEntry.isAddToDossierDictionary()) + .build(); + } + + + private float[] getColor(String type, String dossierTemplateId, boolean requested, boolean isRedaction, boolean skipped) { + if (requested) { + return dictionaryService.getRequestRemoveColor(dossierTemplateId); + } + if ((!isRedaction && !dictionaryService.isHint(type, dossierTemplateId)) || skipped) { + return dictionaryService.getNotRedactedColor(dossierTemplateId); + } + return dictionaryService.getColor(type, dossierTemplateId); + } + + + private float[] getColorForManualAdd(String type, String dossierTemplateId, Status status) { + + if (status.equals(Status.REQUESTED)) { + return dictionaryService.getRequestAddColor(dossierTemplateId); + } else if (status.equals(Status.DECLINED)) { + return dictionaryService.getNotRedactedColor(dossierTemplateId); + } + return getColor(type, dossierTemplateId); + } + + + private float[] getColor(String type, String dossierTemplateId) { + + return dictionaryService.getColor(type, dossierTemplateId); + } + + +} From d2ffb099876943aac4e2d47553f0e624cdc7c401 Mon Sep 17 00:00:00 2001 From: Timo Date: Wed, 21 Jul 2021 16:01:16 +0300 Subject: [PATCH 49/80] redaction-log preview generation and removal of manual redactions from redaction-log --- .../v1/server/controller/RedactionController.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java index 047538cf..901104d0 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java @@ -164,7 +164,13 @@ public class RedactionController implements RedactionResource { dictionaryService.updateDictionary(redactionRequest.getDossierTemplateId(), redactionRequest.getDossierId()); var redactionLog = redactionStorageService.getRedactionLog(redactionRequest.getDossierId(), redactionRequest.getFileId()); - return redactionLogMergeService.mergeRedactionLogData(redactionLog, redactionRequest.getDossierTemplateId(), redactionRequest.getManualRedactions()); + + if (redactionLog.getComputationalVersion() == 0) { + // old redaction logs are returned directly + return redactionLog; + } else { + return redactionLogMergeService.mergeRedactionLogData(redactionLog, redactionRequest.getDossierTemplateId(), redactionRequest.getManualRedactions()); + } } From 25e559475047beead9c067e070d3a8e4eafcacad Mon Sep 17 00:00:00 2001 From: Timo Date: Wed, 21 Jul 2021 17:25:50 +0300 Subject: [PATCH 50/80] fixed pmd --- .../v1/server/redaction/service/RedactionLogMergeService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java index ac19c236..6e368db7 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java @@ -214,7 +214,7 @@ public class RedactionLogMergeService { if (requested) { return dictionaryService.getRequestRemoveColor(dossierTemplateId); } - if ((!isRedaction && !dictionaryService.isHint(type, dossierTemplateId)) || skipped) { + if (skipped || (!isRedaction && !dictionaryService.isHint(type, dossierTemplateId))) { return dictionaryService.getNotRedactedColor(dossierTemplateId); } return dictionaryService.getColor(type, dossierTemplateId); From 26e73121e9f3526c2fd68a3965f17adf74e28ede Mon Sep 17 00:00:00 2001 From: Timo Date: Wed, 21 Jul 2021 17:28:57 +0300 Subject: [PATCH 51/80] fixed pmd --- .../v1/server/redaction/service/RedactionLogMergeService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java index 6e368db7..ba77eed7 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java @@ -214,7 +214,7 @@ public class RedactionLogMergeService { if (requested) { return dictionaryService.getRequestRemoveColor(dossierTemplateId); } - if (skipped || (!isRedaction && !dictionaryService.isHint(type, dossierTemplateId))) { + if (skipped || !isRedaction && !dictionaryService.isHint(type, dossierTemplateId)) { return dictionaryService.getNotRedactedColor(dossierTemplateId); } return dictionaryService.getColor(type, dossierTemplateId); From da14646cb7d8057c8189736565a024ef13921502 Mon Sep 17 00:00:00 2001 From: Timo Date: Thu, 22 Jul 2021 18:52:36 +0300 Subject: [PATCH 52/80] some logs --- .../redaction/v1/server/controller/RedactionController.java | 1 + .../v1/server/redaction/service/RedactionLogMergeService.java | 3 +++ 2 files changed, 4 insertions(+) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java index 901104d0..7691239a 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java @@ -165,6 +165,7 @@ public class RedactionController implements RedactionResource { var redactionLog = redactionStorageService.getRedactionLog(redactionRequest.getDossierId(), redactionRequest.getFileId()); + log.info("Loaded redaction log with computationalVersion: {}",redactionLog.getComputationalVersion()); if (redactionLog.getComputationalVersion() == 0) { // old redaction logs are returned directly return redactionLog; diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java index ba77eed7..7f78e87a 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java @@ -2,6 +2,7 @@ package com.iqser.red.service.redaction.v1.server.redaction.service; import com.iqser.red.service.redaction.v1.model.*; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.ArrayList; @@ -9,6 +10,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +@Slf4j @Service @RequiredArgsConstructor public class RedactionLogMergeService { @@ -18,6 +20,7 @@ public class RedactionLogMergeService { public RedactionLog mergeRedactionLogData(RedactionLog redactionLog, String dossierTemplateId, ManualRedactions manualRedactions) { + log.info("Merging Redaction log with manual redactions "); if (manualRedactions != null) { var manualRedactionLogEntries = addManualAddEntries(manualRedactions.getEntriesToAdd(), manualRedactions.getComments(), dossierTemplateId); From 00dd6b90081e7e99ac64a27d115a195d6ab0bc2a Mon Sep 17 00:00:00 2001 From: Timo Date: Thu, 22 Jul 2021 22:26:16 +0300 Subject: [PATCH 53/80] manual redaction fixes --- .../v1/server/redaction/service/RedactionLogMergeService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java index 7f78e87a..25d076cb 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java @@ -20,13 +20,14 @@ public class RedactionLogMergeService { public RedactionLog mergeRedactionLogData(RedactionLog redactionLog, String dossierTemplateId, ManualRedactions manualRedactions) { - log.info("Merging Redaction log with manual redactions "); + log.info("Merging Redaction log with manual redactions {}", manualRedactions); if (manualRedactions != null) { var manualRedactionLogEntries = addManualAddEntries(manualRedactions.getEntriesToAdd(), manualRedactions.getComments(), dossierTemplateId); redactionLog.getRedactionLogEntry().addAll(manualRedactionLogEntries); + log.info("Adding {}", manualRedactionLogEntries); for (RedactionLogEntry entry : redactionLog.getRedactionLogEntry()) { processRedactionLogEntry(manualRedactions, dossierTemplateId, entry); entry.setComments(manualRedactions.getComments().get(entry.getId())); @@ -175,6 +176,7 @@ public class RedactionLogMergeService { RedactionLogEntry redactionLogEntry = createRedactionLogEntry(manualRedactionEntry, manualRedactionEntry.getId(), dossierTemplateId); redactionLogEntry.setPositions(manualRedactionEntry.getPositions()); redactionLogEntry.setComments(comments.get(manualRedactionEntry.getId())); + redactionLogEntries.add(redactionLogEntry); } } From 0832ea15c09335cfb0d9f0139764ab6c99a22423 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Mon, 26 Jul 2021 14:54:13 +0200 Subject: [PATCH 54/80] RED-1806: Use localDictionary also in Reanlysis to fix problem at removing values from false_positive that affect rules that should find values in entire document --- .../service/EntityRedactionService.java | 115 ++++++++------ .../redaction/service/ReanalyzeService.java | 145 ++++++++++-------- .../v1/server/RedactionIntegrationTest.java | 102 +++++++----- .../resources/dictionaries/CBI_author.txt | 1 - .../resources/dictionaries/false_positive.txt | 3 +- 5 files changed, 211 insertions(+), 155 deletions(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java index ea1079a4..c1dc3f11 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java @@ -43,35 +43,13 @@ public class EntityRedactionService { if (dictionary.hasLocalEntries()) { - Map> hintsPerSectionNumber = new HashMap<>(); - documentEntities.stream().forEach(entity -> { - if (dictionary.isHint(entity.getType()) && entity.isDictionaryEntry()) { - hintsPerSectionNumber.computeIfAbsent(entity.getSectionNumber(), (x) -> new HashSet<>()) - .add(entity); - } - }); - + Map> hintsPerSectionNumber = getHintsPerSection(documentEntities, dictionary); Set foundByLocal = findEntities(classifiedDoc, container, manualRedactions, dictionary, true, hintsPerSectionNumber, fileAttributes); EntitySearchUtils.addEntitiesWithHigherRank(documentEntities, foundByLocal, dictionary); EntitySearchUtils.removeEntitiesContainedInLarger(documentEntities); } - for (Entity entity : documentEntities) { - Map> sequenceOnPage = new HashMap<>(); - for (EntityPositionSequence entityPositionSequence : entity.getPositionSequences()) { - sequenceOnPage.computeIfAbsent(entityPositionSequence.getPageNumber(), (x) -> new ArrayList<>()) - .add(entityPositionSequence); - } - - for (Map.Entry> entry : sequenceOnPage.entrySet()) { - classifiedDoc.getEntities() - .computeIfAbsent(entry.getKey(), (x) -> new ArrayList<>()) - .add(new Entity(entity.getWord(), entity.getType(), entity.isRedaction(), entity.getRedactionReason(), entry - .getValue(), entity.getHeadline(), entity.getMatchedRule(), entity.getSectionNumber(), entity - .getLegalBasis(), entity.isDictionaryEntry(), entity.getTextBefore(), entity.getTextAfter(), entity - .getStart(), entity.getEnd(), entity.isDossierDictionaryEntry())); - } - } + classifiedDoc.setEntities(convertToEnititesPerPage(documentEntities)); dictionaryService.updateExternalDictionary(dictionary, dossierTemplateId); @@ -80,6 +58,39 @@ public class EntityRedactionService { } + public Map> convertToEnititesPerPage(Set entities){ + Map> entitiesPerPage = new HashMap<>(); + for (Entity entity : entities) { + Map> sequenceOnPage = new HashMap<>(); + for (EntityPositionSequence entityPositionSequence : entity.getPositionSequences()) { + sequenceOnPage.computeIfAbsent(entityPositionSequence.getPageNumber(), (x) -> new ArrayList<>()) + .add(entityPositionSequence); + } + + for (Map.Entry> entry : sequenceOnPage.entrySet()) { + entitiesPerPage + .computeIfAbsent(entry.getKey(), (x) -> new ArrayList<>()) + .add(new Entity(entity.getWord(), entity.getType(), entity.isRedaction(), entity.getRedactionReason(), entry + .getValue(), entity.getHeadline(), entity.getMatchedRule(), entity.getSectionNumber(), entity + .getLegalBasis(), entity.isDictionaryEntry(), entity.getTextBefore(), entity.getTextAfter(), entity + .getStart(), entity.getEnd(), entity.isDossierDictionaryEntry())); + } + } + return entitiesPerPage; + } + + + public Map> getHintsPerSection(Set entities, Dictionary dictionary){ + Map> hintsPerSectionNumber = new HashMap<>(); + entities.stream().forEach(entity -> { + if (dictionary.isHint(entity.getType()) && entity.isDictionaryEntry()) { + hintsPerSectionNumber.computeIfAbsent(entity.getSectionNumber(), (x) -> new HashSet<>()) + .add(entity); + } + }); + return hintsPerSectionNumber; + } + private Set findEntities(Document classifiedDoc, KieContainer kieContainer, ManualRedactions manualRedactions, Dictionary dictionary, boolean local, Map> hintsPerSectionNumber, @@ -123,42 +134,46 @@ public class EntityRedactionService { } sectionSearchableTextPairs.forEach(sectionSearchableTextPair -> { - Section analysedRowSection = droolsExecutionService.executeRules(kieContainer, sectionSearchableTextPair.getSection()); - documentEntities.addAll(analysedRowSection.getEntities()); + Section analysedSection = droolsExecutionService.executeRules(kieContainer, sectionSearchableTextPair.getSection()); + documentEntities.addAll(analysedSection.getEntities()); - for (Image image : analysedRowSection.getImages()) { + for (Image image : analysedSection.getImages()) { classifiedDoc.getImages().computeIfAbsent(image.getPage(), (a) -> new HashSet<>()).add(image); } - analysedRowSection.getLocalDictionaryAdds().keySet().forEach(key -> { - if (dictionary.isRecommendation(key)) { - analysedRowSection.getLocalDictionaryAdds().get(key).forEach(value -> { - if (!dictionary.containsValue(key, value)) { - dictionary.getLocalAccessMap().get(key).getLocalEntries().add(value); - } - }); - } else { - analysedRowSection.getLocalDictionaryAdds().get(key).forEach(value -> { - - if (dictionary.getLocalAccessMap().get(key) == null) { - log.warn("Dictionary {} is null", key); - } - - if (dictionary.getLocalAccessMap().get(key).getLocalEntries() == null) { - log.warn("Dictionary {} localEntries is null", key); - } - - dictionary.getLocalAccessMap().get(key).getLocalEntries().add(value); - }); - } - }); - + addLocalValuesToDictionary(analysedSection, dictionary); }); return documentEntities; } + public void addLocalValuesToDictionary(Section analysedSection, Dictionary dictionary){ + analysedSection.getLocalDictionaryAdds().keySet().forEach(key -> { + if (dictionary.isRecommendation(key)) { + analysedSection.getLocalDictionaryAdds().get(key).forEach(value -> { + if (!dictionary.containsValue(key, value)) { + dictionary.getLocalAccessMap().get(key).getLocalEntries().add(value); + } + }); + } else { + analysedSection.getLocalDictionaryAdds().get(key).forEach(value -> { + + if (dictionary.getLocalAccessMap().get(key) == null) { + log.warn("Dictionary {} is null", key); + } + + if (dictionary.getLocalAccessMap().get(key).getLocalEntries() == null) { + log.warn("Dictionary {} localEntries is null", key); + } + + dictionary.getLocalAccessMap().get(key).getLocalEntries().add(value); + }); + } + }); + } + + private List processTablePerRow(Document classifiedDoc, Table table, AtomicInteger sectionNumber, Dictionary dictionary, boolean local, diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java index 17dfdf4b..810619a1 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java @@ -13,9 +13,11 @@ import com.iqser.red.service.redaction.v1.server.redaction.utils.EntitySearchUti import com.iqser.red.service.redaction.v1.server.redaction.utils.IdBuilder; import com.iqser.red.service.redaction.v1.server.segmentation.PdfSegmentationService; import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService; + import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; + import org.kie.api.runtime.KieContainer; import org.springframework.stereotype.Service; import org.springframework.web.bind.annotation.RequestBody; @@ -60,8 +62,7 @@ public class ReanalyzeService { entityRedactionService.processDocument(classifiedDoc, analyzeRequest.getDossierTemplateId(), analyzeRequest.getManualRedactions(), analyzeRequest .getDossierId(), analyzeRequest.getFileAttributes()); - redactionLogCreatorService.createRedactionLog(classifiedDoc, pageCount, analyzeRequest - .getDossierTemplateId()); + redactionLogCreatorService.createRedactionLog(classifiedDoc, pageCount, analyzeRequest.getDossierTemplateId()); log.info("Redaction analysis successful..."); @@ -105,6 +106,59 @@ public class ReanalyzeService { DictionaryIncrement dictionaryIncrement = dictionaryService.getDictionaryIncrements(analyzeRequest.getDossierTemplateId(), new DictionaryVersion(redactionLog .getDictionaryVersion(), redactionLog.getDossierDictionaryVersion()), analyzeRequest.getDossierId()); + Set sectionsToReanalyse = findSectionsToReanalyse(dictionaryIncrement, redactionLog, text, analyzeRequest); + + if (sectionsToReanalyse.isEmpty()) { + return finalizeAnalysis(analyzeRequest, startTime, redactionLog, text, dictionaryIncrement); + } + + List reanalysisSections = text.getSectionTexts() + .stream() + .filter(sectionText -> sectionsToReanalyse.contains(sectionText.getSectionNumber())) + .collect(Collectors.toList()); + + KieContainer kieContainer = droolsExecutionService.updateRules(analyzeRequest.getDossierTemplateId()); + + Dictionary dictionary = dictionaryService.getDeepCopyDictionary(analyzeRequest.getDossierTemplateId(), analyzeRequest + .getDossierId()); + + Map> imagesPerPage = new HashMap<>(); + Set entities = findEntities(reanalysisSections, dictionary, kieContainer, analyzeRequest, false, null, imagesPerPage); + + if (dictionary.hasLocalEntries()) { + Map> hintsPerSectionNumber = entityRedactionService.getHintsPerSection(entities, dictionary); + Set foundByLocal = findEntities(reanalysisSections, dictionary, kieContainer, analyzeRequest, true, hintsPerSectionNumber, imagesPerPage); + EntitySearchUtils.addEntitiesWithHigherRank(entities, foundByLocal, dictionary); + EntitySearchUtils.removeEntitiesContainedInLarger(entities); + } + + Map> entitiesPerPage = entityRedactionService.convertToEnititesPerPage(entities); + + List newRedactionLogEntries = new ArrayList<>(); + for (int page = 1; page <= text.getNumberOfPages(); page++) { + if (entitiesPerPage.get(page) != null) { + newRedactionLogEntries.addAll(redactionLogCreatorService.addEntries(entitiesPerPage, page, analyzeRequest + .getDossierTemplateId())); + } + + if (imagesPerPage.get(page) != null) { + newRedactionLogEntries.addAll(redactionLogCreatorService.addImageEntries(imagesPerPage, page, analyzeRequest + .getDossierTemplateId())); + } + + } + + redactionLog.getRedactionLogEntry().removeIf(entry -> sectionsToReanalyse.contains(entry.getSectionNumber())); + redactionLog.getRedactionLogEntry().addAll(newRedactionLogEntries); + AnalyzeResult analyzeResult = finalizeAnalysis(analyzeRequest, startTime, redactionLog, text, dictionaryIncrement); + analyzeResult.setWasReanalyzed(true); + return analyzeResult; + } + + + private Set findSectionsToReanalyse(DictionaryIncrement dictionaryIncrement, RedactionLog redactionLog, + Text text, AnalyzeRequest analyzeRequest) { + Set relevantManuallyModifiedAnnotationIds = getRelevantManuallyModifiedAnnotationIds(analyzeRequest.getManualRedactions()); Set sectionsToReanalyse = new HashSet<>(); @@ -128,31 +182,20 @@ public class ReanalyzeService { log.info("Should reanalyze {} sections for request: {}", sectionsToReanalyse.size(), analyzeRequest); - if (sectionsToReanalyse.isEmpty()) { - return finalizeAnalysis(analyzeRequest, startTime, redactionLog, text, dictionaryIncrement); - } + return sectionsToReanalyse; + } - List reanalysisSections = new ArrayList<>(); - for (SectionText sectionText : text.getSectionTexts()) { - - if (sectionsToReanalyse.contains(sectionText.getSectionNumber())) { - reanalysisSections.add(sectionText); - } - } - - //-- - - KieContainer kieContainer = droolsExecutionService.updateRules(analyzeRequest.getDossierTemplateId()); - - Dictionary dictionary = dictionaryService.getDeepCopyDictionary(analyzeRequest.getDossierTemplateId(), analyzeRequest - .getDossierId()); + private Set findEntities(List reanalysisSections, Dictionary dictionary, + KieContainer kieContainer, AnalyzeRequest analyzeRequest, boolean local, + Map> hintsPerSectionNumber, + Map> imagesPerPage) { List sectionSearchableTextPairs = new ArrayList<>(); for (SectionText reanalysisSection : reanalysisSections) { Set entities = entityRedactionService.findEntities(reanalysisSection.getSearchableText(), reanalysisSection - .getHeadline(), reanalysisSection.getSectionNumber(), dictionary, false); + .getHeadline(), reanalysisSection.getSectionNumber(), dictionary, local); if (reanalysisSection.getCellStarts() != null && !reanalysisSection.getCellStarts().isEmpty()) { surroundingWordsService.addSurroundingText(entities, reanalysisSection.getSearchableText(), dictionary, reanalysisSection .getCellStarts()); @@ -160,14 +203,15 @@ public class ReanalyzeService { surroundingWordsService.addSurroundingText(entities, reanalysisSection.getSearchableText(), dictionary); } - if (reanalysisSection.getImages() != null && !reanalysisSection.getImages() + if (!local && reanalysisSection.getImages() != null && !reanalysisSection.getImages() .isEmpty() && analyzeRequest.getManualRedactions() != null && analyzeRequest.getManualRedactions() .getImageRecategorizations() != null) { for (Image image : reanalysisSection.getImages()) { String imageId = IdBuilder.buildId(image.getPosition(), image.getPage()); for (ManualImageRecategorization imageRecategorization : analyzeRequest.getManualRedactions() .getImageRecategorizations()) { - if (imageRecategorization.getStatus().equals(Status.APPROVED) && imageRecategorization.getId().equals(imageId)) { + if (imageRecategorization.getStatus().equals(Status.APPROVED) && imageRecategorization.getId() + .equals(imageId)) { image.setType(imageRecategorization.getType()); } } @@ -177,7 +221,10 @@ public class ReanalyzeService { sectionSearchableTextPairs.add(new SectionSearchableTextPair(Section.builder() .isLocal(false) .dictionaryTypes(dictionary.getTypes()) - .entities(entities) + .entities(hintsPerSectionNumber != null && hintsPerSectionNumber.containsKey(reanalysisSection.getSectionNumber()) ? Stream + .concat(entities.stream(), hintsPerSectionNumber.get(reanalysisSection.getSectionNumber()) + .stream()) + .collect(Collectors.toSet()) : entities) .text(reanalysisSection.getSearchableText().getAsStringWithLinebreaks()) .searchText(reanalysisSection.getSearchableText().toString()) .headline(reanalysisSection.getHeadline()) @@ -191,54 +238,19 @@ public class ReanalyzeService { } Set entities = new HashSet<>(); - Map> imagesPerPage = new HashMap<>(); sectionSearchableTextPairs.forEach(sectionSearchableTextPair -> { - Section analysedRowSection = droolsExecutionService.executeRules(kieContainer, sectionSearchableTextPair.getSection()); - entities.addAll(analysedRowSection.getEntities()); + Section analysedSection = droolsExecutionService.executeRules(kieContainer, sectionSearchableTextPair.getSection()); + entities.addAll(analysedSection.getEntities()); EntitySearchUtils.removeEntitiesContainedInLarger(entities); - for (Image image : analysedRowSection.getImages()) { + for (Image image : analysedSection.getImages()) { imagesPerPage.computeIfAbsent(image.getPage(), (a) -> new HashSet<>()).add(image); } + entityRedactionService.addLocalValuesToDictionary(analysedSection, dictionary); }); - Map> entitiesPerPage = new HashMap<>(); - for (Entity entity : entities) { - Map> sequenceOnPage = new HashMap<>(); - for (EntityPositionSequence entityPositionSequence : entity.getPositionSequences()) { - sequenceOnPage.computeIfAbsent(entityPositionSequence.getPageNumber(), (x) -> new ArrayList<>()) - .add(entityPositionSequence); - } - - for (Map.Entry> entry : sequenceOnPage.entrySet()) { - entitiesPerPage.computeIfAbsent(entry.getKey(), (x) -> new ArrayList<>()) - .add(new Entity(entity.getWord(), entity.getType(), entity.isRedaction(), entity.getRedactionReason(), entry - .getValue(), entity.getHeadline(), entity.getMatchedRule(), entity.getSectionNumber(), entity - .getLegalBasis(), entity.isDictionaryEntry(), entity.getTextBefore(), entity.getTextAfter(), entity - .getStart(), entity.getEnd(), entity.isDossierDictionaryEntry())); - } - } - - List newRedactionLogEntries = new ArrayList<>(); - for (int page = 1; page <= text.getNumberOfPages(); page++) { - if (entitiesPerPage.get(page) != null) { - newRedactionLogEntries.addAll(redactionLogCreatorService.addEntries(entitiesPerPage, page, analyzeRequest - .getDossierTemplateId())); - } - - if (imagesPerPage.get(page) != null) { - newRedactionLogEntries.addAll(redactionLogCreatorService.addImageEntries(imagesPerPage, page, analyzeRequest - .getDossierTemplateId())); - } - - } - - redactionLog.getRedactionLogEntry().removeIf(entry -> sectionsToReanalyse.contains(entry.getSectionNumber())); - redactionLog.getRedactionLogEntry().addAll(newRedactionLogEntries); - AnalyzeResult analyzeResult = finalizeAnalysis(analyzeRequest, startTime, redactionLog, text, dictionaryIncrement); - analyzeResult.setWasReanalyzed(true); - return analyzeResult; + return entities; } @@ -296,10 +308,9 @@ public class ReanalyzeService { private void excludeExcludedPages(RedactionLog redactionLog, Set excludedPages) { - redactionLog.getRedactionLogEntry().forEach(entry -> - entry.getPositions().forEach(pos -> - entry.setExcluded(excludedPages != null && excludedPages.contains(pos.getPage())) - )); + redactionLog.getRedactionLogEntry() + .forEach(entry -> entry.getPositions() + .forEach(pos -> entry.setExcluded(excludedPages != null && excludedPages.contains(pos.getPage())))); } } diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java index 8904ea6c..6949ff40 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java @@ -18,7 +18,9 @@ import com.iqser.red.service.redaction.v1.server.redaction.utils.ResourceLoader; import com.iqser.red.service.redaction.v1.server.redaction.utils.TextNormalizationUtilities; import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService; import com.iqser.red.storage.commons.service.StorageService; + import lombok.SneakyThrows; + import org.apache.commons.io.IOUtils; import org.junit.After; import org.junit.Before; @@ -84,7 +86,6 @@ public class RedactionIntegrationTest { private static final String PII = "PII"; - @Autowired private RedactionController redactionController; @@ -127,6 +128,7 @@ public class RedactionIntegrationTest { private final Map rankTypeMap = new HashMap<>(); private final Colors colors = new Colors(); private final Map reanlysisVersions = new HashMap<>(); + private final Set deleted = new HashSet<>(); private final static String TEST_DOSSIER_TEMPLATE_ID = "123"; private final static String TEST_DOSSIER_ID = "123"; @@ -152,18 +154,20 @@ public class RedactionIntegrationTest { return kieServices.newKieContainer(kieModule.getReleaseId()); } + @Bean @Primary public StorageService inmemoryStorage() { + return new FileSystemBackedStorageService(); } - } @After public void cleanupStorage() { + if (this.storageService instanceof FileSystemBackedStorageService) { ((FileSystemBackedStorageService) this.storageService).clearStorage(); } @@ -179,7 +183,8 @@ public class RedactionIntegrationTest { loadDictionaryForTest(); loadTypeForTest(); when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(0L); - when(dictionaryClient.getAllTypes(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(TypeResponse.builder() + when(dictionaryClient.getAllTypes(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(TypeResponse + .builder() .types(getTypeResponse()) .build()); @@ -188,7 +193,7 @@ public class RedactionIntegrationTest { .types(List.of(TypeResult.builder() .type(DOSSIER_REDACTIONS) .dossierTemplateId(TEST_DOSSIER_TEMPLATE_ID) - .hexColor( "#ffe187") + .hexColor("#ffe187") .isHint(hintTypeMap.get(DOSSIER_REDACTIONS)) .isCaseInsensitive(caseInSensitiveMap.get(DOSSIER_REDACTIONS)) .isRecommendation(recommendationTypeMap.get(DOSSIER_REDACTIONS)) @@ -196,26 +201,42 @@ public class RedactionIntegrationTest { .build())) .build()); - when(dictionaryClient.getDictionaryForType(VERTEBRATE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(VERTEBRATE, false)); - when(dictionaryClient.getDictionaryForType(ADDRESS, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(ADDRESS, false)); - when(dictionaryClient.getDictionaryForType(AUTHOR, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(AUTHOR, false)); - when(dictionaryClient.getDictionaryForType(SPONSOR, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(SPONSOR, false)); - when(dictionaryClient.getDictionaryForType(NO_REDACTION_INDICATOR, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(NO_REDACTION_INDICATOR, false)); - when(dictionaryClient.getDictionaryForType(REDACTION_INDICATOR, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(REDACTION_INDICATOR, false)); - when(dictionaryClient.getDictionaryForType(HINT_ONLY, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(HINT_ONLY, false)); - when(dictionaryClient.getDictionaryForType(MUST_REDACT, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(MUST_REDACT, false)); - when(dictionaryClient.getDictionaryForType(PUBLISHED_INFORMATION, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(PUBLISHED_INFORMATION, false)); - when(dictionaryClient.getDictionaryForType(TEST_METHOD, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(TEST_METHOD, false)); + when(dictionaryClient.getDictionaryForType(VERTEBRATE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + .thenReturn(getDictionaryResponse(VERTEBRATE, false)); + when(dictionaryClient.getDictionaryForType(ADDRESS, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + .thenReturn(getDictionaryResponse(ADDRESS, false)); + when(dictionaryClient.getDictionaryForType(AUTHOR, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + .thenReturn(getDictionaryResponse(AUTHOR, false)); + when(dictionaryClient.getDictionaryForType(SPONSOR, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + .thenReturn(getDictionaryResponse(SPONSOR, false)); + when(dictionaryClient.getDictionaryForType(NO_REDACTION_INDICATOR, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + .thenReturn(getDictionaryResponse(NO_REDACTION_INDICATOR, false)); + when(dictionaryClient.getDictionaryForType(REDACTION_INDICATOR, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + .thenReturn(getDictionaryResponse(REDACTION_INDICATOR, false)); + when(dictionaryClient.getDictionaryForType(HINT_ONLY, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + .thenReturn(getDictionaryResponse(HINT_ONLY, false)); + when(dictionaryClient.getDictionaryForType(MUST_REDACT, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + .thenReturn(getDictionaryResponse(MUST_REDACT, false)); + when(dictionaryClient.getDictionaryForType(PUBLISHED_INFORMATION, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + .thenReturn(getDictionaryResponse(PUBLISHED_INFORMATION, false)); + when(dictionaryClient.getDictionaryForType(TEST_METHOD, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + .thenReturn(getDictionaryResponse(TEST_METHOD, false)); when(dictionaryClient.getDictionaryForType(PII, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(PII, false)); - when(dictionaryClient.getDictionaryForType(RECOMMENDATION_AUTHOR, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(RECOMMENDATION_AUTHOR, false)); - when(dictionaryClient.getDictionaryForType(RECOMMENDATION_ADDRESS, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(RECOMMENDATION_ADDRESS, false)); - when(dictionaryClient.getDictionaryForType(FALSE_POSITIVE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(FALSE_POSITIVE, false)); - when(dictionaryClient.getDictionaryForType(PURITY, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(PURITY, false)); + when(dictionaryClient.getDictionaryForType(RECOMMENDATION_AUTHOR, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + .thenReturn(getDictionaryResponse(RECOMMENDATION_AUTHOR, false)); + when(dictionaryClient.getDictionaryForType(RECOMMENDATION_ADDRESS, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + .thenReturn(getDictionaryResponse(RECOMMENDATION_ADDRESS, false)); + when(dictionaryClient.getDictionaryForType(FALSE_POSITIVE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + .thenReturn(getDictionaryResponse(FALSE_POSITIVE, false)); + when(dictionaryClient.getDictionaryForType(PURITY, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + .thenReturn(getDictionaryResponse(PURITY, false)); when(dictionaryClient.getDictionaryForType(IMAGE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(IMAGE, false)); when(dictionaryClient.getDictionaryForType(OCR, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(OCR, false)); when(dictionaryClient.getDictionaryForType(LOGO, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(LOGO, false)); - when(dictionaryClient.getDictionaryForType(SIGNATURE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(SIGNATURE, false)); - when(dictionaryClient.getDictionaryForType(FORMULA, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(FORMULA, false)); + when(dictionaryClient.getDictionaryForType(SIGNATURE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + .thenReturn(getDictionaryResponse(SIGNATURE, false)); + when(dictionaryClient.getDictionaryForType(FORMULA, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + .thenReturn(getDictionaryResponse(FORMULA, false)); when(dictionaryClient.getDictionaryForType(DOSSIER_REDACTIONS, TEST_DOSSIER_TEMPLATE_ID, TEST_DOSSIER_ID)).thenReturn(getDictionaryResponse(DOSSIER_REDACTIONS, true)); when(dictionaryClient.getColors(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(colors); } @@ -477,7 +498,8 @@ public class RedactionIntegrationTest { return DictionaryResponse.builder() .hexColor(typeColorMap.get(type)) - .entries(isDossierDictionary ? toDictionaryEntry(dossierDictionary.get(type)) : toDictionaryEntry(dictionary.get(type))) + .entries(isDossierDictionary ? toDictionaryEntry(dossierDictionary.get(type)) : toDictionaryEntry(dictionary + .get(type))) .isHint(hintTypeMap.get(type)) .isCaseInsensitive(caseInSensitiveMap.get(type)) .isRecommendation(recommendationTypeMap.get(type)) @@ -490,7 +512,8 @@ public class RedactionIntegrationTest { List dictionaryEntries = new ArrayList<>(); entries.forEach(entry -> { - dictionaryEntries.add(new DictionaryEntry(entry, reanlysisVersions.containsKey(entry) ? reanlysisVersions.get(entry) : 0L, false)); + dictionaryEntries.add(new DictionaryEntry(entry, reanlysisVersions.containsKey(entry) ? reanlysisVersions.get(entry) : 0L, deleted + .contains(entry) ? true : false)); }); return dictionaryEntries; } @@ -498,6 +521,7 @@ public class RedactionIntegrationTest { @Test public void test270Rotated() { + AnalyzeRequest request = prepareStorage("files/Minimal Examples/270Rotated.pdf"); MemoryStats.printMemoryStats(); AnalyzeResult result = reanalyzeService.analyze(request); @@ -508,12 +532,14 @@ public class RedactionIntegrationTest { @Test @Ignore public void testLargeScannedFileOOM() { + AnalyzeRequest request = prepareStorage("scanned/VV-377031.pdf"); MemoryStats.printMemoryStats(); AnalyzeResult result = reanalyzeService.analyze(request); assertThat(result).isNotNull(); } + @Test public void testMergedImages() throws IOException { @@ -552,14 +578,13 @@ public class RedactionIntegrationTest { long rend = System.currentTimeMillis(); System.out.println("reanalysis analysis duration: " + (rend - rstart)); - long end = System.currentTimeMillis(); System.out.println("duration: " + (end - start)); - } + @Test @Ignore public void noExceptionShouldBeThrownForAnyFiles() throws IOException { @@ -635,7 +660,12 @@ public class RedactionIntegrationTest { AnalyzeRequest request = prepareStorage(pdfFileResource.getInputStream()); request.setExcludedPages(Set.of(1)); - request.setFileAttributes(List.of(FileAttribute.builder().id("fileAttributeId").label("Vertebrate Study").placeholder("{fileattributes.vertebrateStudy}").value("true").build())); + request.setFileAttributes(List.of(FileAttribute.builder() + .id("fileAttributeId") + .label("Vertebrate Study") + .placeholder("{fileattributes.vertebrateStudy}") + .value("true") + .build())); AnalyzeResult result = reanalyzeService.analyze(request); @@ -683,12 +713,18 @@ public class RedactionIntegrationTest { dictionary.get(AUTHOR).add("physical"); reanlysisVersions.put("physical", 2L); -// dictionary.get(VERTEBRATE).add("s-metolachlor"); -// reanlysisVersions.put("s-metolachlor", 3L); + deleted.add("David Chubb"); + + dictionary.get(FALSE_POSITIVE).add("David Chubb"); + reanlysisVersions.put("David Chubb", 3L); when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(3L); - when(dictionaryClient.getDictionaryForType(VERTEBRATE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(VERTEBRATE, false)); + when(dictionaryClient.getDictionaryForType(VERTEBRATE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + .thenReturn(getDictionaryResponse(VERTEBRATE, false)); + + when(dictionaryClient.getDictionaryForType(FALSE_POSITIVE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + .thenReturn(getDictionaryResponse(FALSE_POSITIVE, false)); start = System.currentTimeMillis(); @@ -702,10 +738,8 @@ public class RedactionIntegrationTest { request.setManualRedactions(manualRedactions); - AnalyzeResult reanalyzeResult = reanalyzeService.reanalyze(request); - redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); end = System.currentTimeMillis(); @@ -775,7 +809,6 @@ public class RedactionIntegrationTest { .status(Status.APPROVED) .build())); - manualRedactions.getComments().put("e5be0f1d941bbb92a068e198648d06c4", List.of(comment)); manualRedactions.getComments().put("0836727c3508a0b2ea271da69c04cc2f", List.of(comment)); manualRedactions.getComments().put(manualAddId, List.of(comment)); @@ -790,12 +823,10 @@ public class RedactionIntegrationTest { // manualRedactions.getEntriesToAdd().add(manualRedactionEntry); - AnalyzeRequest request = prepareStorage(pdfFileResource.getInputStream()); request.setManualRedactions(manualRedactions); AnalyzeResult result = reanalyzeService.analyze(request); - manualRedactions.getEntriesToAdd().add(manualRedactionEntry); manualRedactions.setIdsToRemove(Set.of(IdRemoval.builder() .id("5b940b2cb401ed9f5be6fc24f6e77bcf") @@ -816,7 +847,6 @@ public class RedactionIntegrationTest { .fileId(TEST_FILE_ID) .build()); - try (FileOutputStream fileOutputStream = new FileOutputStream("/tmp/Annotated.pdf")) { fileOutputStream.write(annotateResponse.getDocument()); } @@ -833,7 +863,6 @@ public class RedactionIntegrationTest { System.out.println("classificationTest"); ClassPathResource pdfFileResource = new ClassPathResource("files/new/Single Study - Oral (Gavage) Mouse.pdf"); - AnalyzeRequest request = prepareStorage(pdfFileResource.getInputStream()); RedactionRequest redactionRequest = RedactionRequest.builder() @@ -934,8 +963,10 @@ public class RedactionIntegrationTest { }); } + @SneakyThrows private AnalyzeRequest prepareStorage(String file) { + ClassPathResource pdfFileResource = new ClassPathResource(file); return prepareStorage(pdfFileResource.getInputStream()); @@ -967,7 +998,6 @@ public class RedactionIntegrationTest { long start = System.currentTimeMillis(); ClassPathResource pdfFileResource = new ClassPathResource("files/Minimal Examples/sponsor_companies.pdf"); - AnalyzeRequest request = prepareStorage(pdfFileResource.getInputStream()); AnalyzeResult result = reanalyzeService.analyze(request); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/resources/dictionaries/CBI_author.txt b/redaction-service-v1/redaction-service-server-v1/src/test/resources/dictionaries/CBI_author.txt index 766ebd06..3d53e6d2 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/resources/dictionaries/CBI_author.txt +++ b/redaction-service-v1/redaction-service-server-v1/src/test/resources/dictionaries/CBI_author.txt @@ -1676,7 +1676,6 @@ da Silva Rejane Das R Das, R. Daughtry, CST -David Chubb David Chubb|Lorraine Britton David Clarke Davies diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/resources/dictionaries/false_positive.txt b/redaction-service-v1/redaction-service-server-v1/src/test/resources/dictionaries/false_positive.txt index 0bb75f29..a696cee0 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/resources/dictionaries/false_positive.txt +++ b/redaction-service-v1/redaction-service-server-v1/src/test/resources/dictionaries/false_positive.txt @@ -235,4 +235,5 @@ N/A No details reported Not available Test facility -TBD \ No newline at end of file +TBD +David Chubb \ No newline at end of file From 289077e5e386e3a0785e2c849ae06bcb01202d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Thu, 29 Jul 2021 11:40:25 +0200 Subject: [PATCH 55/80] Fixed dossier dictionary and dictionary rank sorting --- .../v1/server/redaction/service/DictionaryService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java index 5afff568..72d0d387 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java @@ -212,7 +212,7 @@ public class DictionaryService { dossierDictionaryVersion = dossierRepresentation.getDictionaryVersion(); } - return new Dictionary(copy, DictionaryVersion.builder().dossierTemplateVersion(dossierTemplateRepresentation.getDictionaryVersion()).dossierVersion(dossierDictionaryVersion).build()); + return new Dictionary(copy.stream().sorted(Comparator.comparingInt(DictionaryModel::getRank).reversed()).collect(Collectors.toList()), DictionaryVersion.builder().dossierTemplateVersion(dossierTemplateRepresentation.getDictionaryVersion()).dossierVersion(dossierDictionaryVersion).build()); } From 77ece8b0d680326db8e7afee4270631f968b41b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Thu, 29 Jul 2021 13:15:07 +0200 Subject: [PATCH 56/80] Simplyfied hyphen removal regex --- .../v1/server/redaction/utils/TextNormalizationUtilities.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/utils/TextNormalizationUtilities.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/utils/TextNormalizationUtilities.java index b740286b..342f6b03 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/utils/TextNormalizationUtilities.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/utils/TextNormalizationUtilities.java @@ -12,7 +12,7 @@ public class TextNormalizationUtilities { * @return Text without line-break hyphenation. */ public static String removeHyphenLineBreaks(String text) { - return text.replaceAll("([^\\s\\d\\-]{2,})[\\-\\u00AD]\\R|\n\r(.+ )", "$1$2"); + return text.replaceAll("([^\\s\\d\\-]{2,})[\\-\\u00AD]\\R", "$1"); } } From cf12e58c77b30fd4ff790a3b038b3ba625aa5a28 Mon Sep 17 00:00:00 2001 From: Timo Bejan Date: Thu, 29 Jul 2021 21:07:14 +0300 Subject: [PATCH 57/80] smart reanalysis based on sections to reanalyse provided by search service --- .../red/service/redaction/v1/model/AnalyzeRequest.java | 7 ++++++- .../v1/server/redaction/service/ReanalyzeService.java | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeRequest.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeRequest.java index 4a7be09b..8f287ddd 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeRequest.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeRequest.java @@ -7,6 +7,7 @@ import lombok.NoArgsConstructor; import java.time.OffsetDateTime; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Set; @@ -22,7 +23,11 @@ public class AnalyzeRequest { private boolean reanalyseOnlyIfPossible; private ManualRedactions manualRedactions; private OffsetDateTime lastProcessed; - private Set excludedPages; + + @Builder.Default + private Set excludedPages = new HashSet<>(); + @Builder.Default + private Set sectionsToReanalyse = new HashSet<>(); @Builder.Default private List fileAttributes = new ArrayList<>(); diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java index 810619a1..e2a19920 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java @@ -106,7 +106,8 @@ public class ReanalyzeService { DictionaryIncrement dictionaryIncrement = dictionaryService.getDictionaryIncrements(analyzeRequest.getDossierTemplateId(), new DictionaryVersion(redactionLog .getDictionaryVersion(), redactionLog.getDossierDictionaryVersion()), analyzeRequest.getDossierId()); - Set sectionsToReanalyse = findSectionsToReanalyse(dictionaryIncrement, redactionLog, text, analyzeRequest); + Set sectionsToReanalyse = !analyzeRequest.getSectionsToReanalyse().isEmpty() ? analyzeRequest.getSectionsToReanalyse() : + findSectionsToReanalyse(dictionaryIncrement, redactionLog, text, analyzeRequest); if (sectionsToReanalyse.isEmpty()) { return finalizeAnalysis(analyzeRequest, startTime, redactionLog, text, dictionaryIncrement); From af1fb030258257a3839439b57ec74733bd0e67dc Mon Sep 17 00:00:00 2001 From: Timo Bejan Date: Thu, 5 Aug 2021 10:36:59 +0300 Subject: [PATCH 58/80] fixed typo --- .../v1/server/redaction/service/RedactionLogMergeService.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java index 25d076cb..52ca7f10 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java @@ -209,8 +209,6 @@ public class RedactionLogMergeService { .manual(true) .status(manualRedactionEntry.getStatus()) .manualRedactionType(ManualRedactionType.ADD) - .isDictionaryEntry(false) - .isDossierDictionaryEntry(manualRedactionEntry.isAddToDossierDictionary()) .build(); } From b08cdd3a07f763c6b310a3403c69d4b9652a0e5c Mon Sep 17 00:00:00 2001 From: Timo Bejan Date: Thu, 5 Aug 2021 10:59:01 +0300 Subject: [PATCH 59/80] cleaned up code, added analysisVersion for analyseresult and interogation, usefull later --- .../redaction/v1/model/AnalyzeResult.java | 2 ++ .../redaction/v1/model/RedactionLog.java | 2 +- .../v1/model/RedactionServiceDetails.java | 14 +++++++++++ .../v1/resources/RedactionResource.java | 6 ++--- .../controller/RedactionController.java | 25 ++++++++----------- .../service/AnalyzeResponseService.java | 7 ++++++ .../service/DroolsExecutionService.java | 4 +-- .../redaction/service/ReanalyzeService.java | 4 ++- .../settings/RedactionServiceSettings.java | 2 ++ 9 files changed, 44 insertions(+), 22 deletions(-) create mode 100644 redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionServiceDetails.java diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeResult.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeResult.java index 5eadb70f..8d569c81 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeResult.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeResult.java @@ -27,6 +27,8 @@ public class AnalyzeResult { private boolean wasReanalyzed; + private int analysisVersion; + } diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLog.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLog.java index 276363f8..942e36a6 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLog.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLog.java @@ -15,7 +15,7 @@ public class RedactionLog { * Version 0 Redaction Logs have manual redactions merged inside them * Version 1 Redaction Logs only contain system ( rule/dictionary ) redactions. Manual Redactions are merged in at runtime. */ - private long computationalVersion; + private long analysisVersion; private List redactionLogEntry; private List legalBasis; diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionServiceDetails.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionServiceDetails.java new file mode 100644 index 00000000..4bb34591 --- /dev/null +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionServiceDetails.java @@ -0,0 +1,14 @@ +package com.iqser.red.service.redaction.v1.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class RedactionServiceDetails { + + private int analysisVersion; + +} diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/resources/RedactionResource.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/resources/RedactionResource.java index 3abe15f5..18871367 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/resources/RedactionResource.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/resources/RedactionResource.java @@ -24,13 +24,13 @@ public interface RedactionResource { @PostMapping(value = "/debug/htmlTables", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) RedactionResult htmlTables(@RequestBody RedactionRequest redactionRequest); - @PostMapping(value = "/rules/update" + RULE_SET_PATH_VARIABLE, consumes = MediaType.APPLICATION_JSON_VALUE) - void updateRules(@PathVariable(RULE_SET_PARAMETER_NAME) String dossierTemplateId); - @PostMapping(value = "/rules/test", consumes = MediaType.APPLICATION_JSON_VALUE) void testRules(@RequestBody String rules); @PostMapping(value = "/redaction-log/preview", consumes = MediaType.APPLICATION_JSON_VALUE) RedactionLog getRedactionLog(@RequestBody RedactionRequest redactionRequest); + @PostMapping(value = "/details", produces = MediaType.APPLICATION_JSON_VALUE) + RedactionServiceDetails getRedactionServiceDetails(); + } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java index 7691239a..33fa3ebf 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java @@ -1,11 +1,7 @@ package com.iqser.red.service.redaction.v1.server.controller; import com.iqser.red.service.file.management.v1.api.model.FileType; -import com.iqser.red.service.redaction.v1.model.AnnotateRequest; -import com.iqser.red.service.redaction.v1.model.AnnotateResponse; -import com.iqser.red.service.redaction.v1.model.RedactionLog; -import com.iqser.red.service.redaction.v1.model.RedactionRequest; -import com.iqser.red.service.redaction.v1.model.RedactionResult; +import com.iqser.red.service.redaction.v1.model.*; import com.iqser.red.service.redaction.v1.resources.RedactionResource; import com.iqser.red.service.redaction.v1.server.classification.model.Document; import com.iqser.red.service.redaction.v1.server.classification.model.Page; @@ -15,6 +11,7 @@ import com.iqser.red.service.redaction.v1.server.redaction.service.DictionarySer import com.iqser.red.service.redaction.v1.server.redaction.service.DroolsExecutionService; import com.iqser.red.service.redaction.v1.server.redaction.service.RedactionLogMergeService; import com.iqser.red.service.redaction.v1.server.segmentation.PdfSegmentationService; +import com.iqser.red.service.redaction.v1.server.settings.RedactionServiceSettings; import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService; import com.iqser.red.service.redaction.v1.server.tableextraction.model.AbstractTextContainer; import com.iqser.red.service.redaction.v1.server.tableextraction.model.Table; @@ -42,6 +39,7 @@ public class RedactionController implements RedactionResource { private final PdfSegmentationService pdfSegmentationService; private final RedactionStorageService redactionStorageService; private final RedactionLogMergeService redactionLogMergeService; + private final RedactionServiceSettings redactionServiceSettings; public AnnotateResponse annotate(@RequestBody AnnotateRequest annotateRequest) { @@ -143,14 +141,6 @@ public class RedactionController implements RedactionResource { } - - @Override - public void updateRules(@PathVariable(RULE_SET_PARAMETER_NAME) String dossierTemplateId) { - - droolsExecutionService.updateRules(dossierTemplateId); - } - - @Override public void testRules(@RequestBody String rules) { @@ -165,8 +155,8 @@ public class RedactionController implements RedactionResource { var redactionLog = redactionStorageService.getRedactionLog(redactionRequest.getDossierId(), redactionRequest.getFileId()); - log.info("Loaded redaction log with computationalVersion: {}",redactionLog.getComputationalVersion()); - if (redactionLog.getComputationalVersion() == 0) { + log.info("Loaded redaction log with computationalVersion: {}", redactionLog.getAnalysisVersion()); + if (redactionLog.getAnalysisVersion() == 0) { // old redaction logs are returned directly return redactionLog; } else { @@ -174,6 +164,11 @@ public class RedactionController implements RedactionResource { } } + @Override + public RedactionServiceDetails getRedactionServiceDetails() { + return new RedactionServiceDetails(redactionServiceSettings.getAnalysisVersion()); + } + private RedactionResult convert(PDDocument document, int numberOfPages) throws IOException { diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java index ff772521..2ddbcc46 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java @@ -5,11 +5,17 @@ import com.iqser.red.service.redaction.v1.model.RedactionChangeLog; import com.iqser.red.service.redaction.v1.model.RedactionLog; import com.iqser.red.service.redaction.v1.model.RedactionLogEntry; +import com.iqser.red.service.redaction.v1.server.settings.RedactionServiceSettings; +import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service +@RequiredArgsConstructor public class AnalyzeResponseService { + private final RedactionServiceSettings redactionServiceSettings; + public AnalyzeResult createAnalyzeResponse(String dossierId, String fileId, long duration, int pageCount, RedactionLog redactionLog, RedactionChangeLog redactionChangeLog) { @@ -51,6 +57,7 @@ public class AnalyzeResponseService { .hasRequests(hasRequests) .hasImages(hasImages) .hasUpdates(hasUpdates) + .analysisVersion(redactionServiceSettings.getAnalysisVersion()) .rulesVersion(redactionLog.getRulesVersion()) .dictionaryVersion(redactionLog.getDictionaryVersion()) .legalBasisVersion(redactionLog.getLegalBasisVersion()) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DroolsExecutionService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DroolsExecutionService.java index bd26cc12..0ac53bc6 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DroolsExecutionService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DroolsExecutionService.java @@ -63,7 +63,7 @@ public class DroolsExecutionService { rulesVersion = -1L; } - if (version > rulesVersion.longValue()) { + if (version > rulesVersion) { rulesVersionPerDossierTemplateId.put(dossierTemplateId, version); return createOrUpdateKieContainer(dossierTemplateId); } @@ -128,7 +128,7 @@ public class DroolsExecutionService { if (rulesVersion == null) { return -1; } - return rulesVersion.longValue(); + return rulesVersion; } } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java index e2a19920..e3b37c5f 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java @@ -12,6 +12,7 @@ import com.iqser.red.service.redaction.v1.server.redaction.model.*; import com.iqser.red.service.redaction.v1.server.redaction.utils.EntitySearchUtils; import com.iqser.red.service.redaction.v1.server.redaction.utils.IdBuilder; import com.iqser.red.service.redaction.v1.server.segmentation.PdfSegmentationService; +import com.iqser.red.service.redaction.v1.server.settings.RedactionServiceSettings; import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService; import lombok.RequiredArgsConstructor; @@ -41,6 +42,7 @@ public class ReanalyzeService { private final RedactionChangeLogService redactionChangeLogService; private final AnalyzeResponseService analyzeResponseService; private final LegalBasisClient legalBasisClient; + private final RedactionServiceSettings redactionServiceSettings; public AnalyzeResult analyze(AnalyzeRequest analyzeRequest) { @@ -67,7 +69,7 @@ public class ReanalyzeService { log.info("Redaction analysis successful..."); var legalBasis = legalBasisClient.getLegalBasisMapping(analyzeRequest.getDossierTemplateId()); - var redactionLog = new RedactionLog(1, classifiedDoc.getRedactionLogEntities(), legalBasis, classifiedDoc.getDictionaryVersion() + var redactionLog = new RedactionLog(redactionServiceSettings.getAnalysisVersion(), classifiedDoc.getRedactionLogEntities(), legalBasis, classifiedDoc.getDictionaryVersion() .getDossierTemplateVersion(), classifiedDoc.getDictionaryVersion() .getDossierVersion(), classifiedDoc.getRulesVersion(), legalBasisClient.getVersion(analyzeRequest.getDossierTemplateId())); diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/settings/RedactionServiceSettings.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/settings/RedactionServiceSettings.java index 3d5b0b5b..dfc34079 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/settings/RedactionServiceSettings.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/settings/RedactionServiceSettings.java @@ -15,4 +15,6 @@ public class RedactionServiceSettings { private float maxImageCropboxRatio = 0.9f; + private int analysisVersion = 1; + } From 4bdca5644a7296bd542278ffa2f64e670855fbe5 Mon Sep 17 00:00:00 2001 From: Timo Bejan Date: Thu, 5 Aug 2021 13:13:48 +0300 Subject: [PATCH 60/80] cleanup --- .../v1/model/RedactionServiceDetails.java | 14 -------------- .../redaction/v1/resources/RedactionResource.java | 7 ------- .../v1/server/controller/RedactionController.java | 7 ------- 3 files changed, 28 deletions(-) delete mode 100644 redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionServiceDetails.java diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionServiceDetails.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionServiceDetails.java deleted file mode 100644 index 4bb34591..00000000 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionServiceDetails.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.iqser.red.service.redaction.v1.model; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class RedactionServiceDetails { - - private int analysisVersion; - -} diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/resources/RedactionResource.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/resources/RedactionResource.java index 18871367..d6bb66f9 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/resources/RedactionResource.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/resources/RedactionResource.java @@ -8,10 +8,6 @@ import org.springframework.web.bind.annotation.RequestBody; public interface RedactionResource { - String RULE_SET_PARAMETER_NAME = "dossierTemplateId"; - String RULE_SET_PATH_VARIABLE = "/{" + RULE_SET_PARAMETER_NAME + "}"; - - @PostMapping(value = "/annotate", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) AnnotateResponse annotate(@RequestBody AnnotateRequest annotateRequest); @@ -30,7 +26,4 @@ public interface RedactionResource { @PostMapping(value = "/redaction-log/preview", consumes = MediaType.APPLICATION_JSON_VALUE) RedactionLog getRedactionLog(@RequestBody RedactionRequest redactionRequest); - @PostMapping(value = "/details", produces = MediaType.APPLICATION_JSON_VALUE) - RedactionServiceDetails getRedactionServiceDetails(); - } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java index 33fa3ebf..95815664 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java @@ -39,7 +39,6 @@ public class RedactionController implements RedactionResource { private final PdfSegmentationService pdfSegmentationService; private final RedactionStorageService redactionStorageService; private final RedactionLogMergeService redactionLogMergeService; - private final RedactionServiceSettings redactionServiceSettings; public AnnotateResponse annotate(@RequestBody AnnotateRequest annotateRequest) { @@ -164,12 +163,6 @@ public class RedactionController implements RedactionResource { } } - @Override - public RedactionServiceDetails getRedactionServiceDetails() { - return new RedactionServiceDetails(redactionServiceSettings.getAnalysisVersion()); - } - - private RedactionResult convert(PDDocument document, int numberOfPages) throws IOException { try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { From 30715cdcb85e94911c3903fe95b1cb902ed6556f Mon Sep 17 00:00:00 2001 From: Timo Bejan Date: Thu, 5 Aug 2021 15:35:06 +0300 Subject: [PATCH 61/80] added user-id to all manual entries --- .../red/service/redaction/v1/model/RedactionLogEntry.java | 1 + .../server/redaction/service/RedactionLogMergeService.java | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java index 9f7a0146..38497fa5 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java @@ -36,6 +36,7 @@ public class RedactionLogEntry { private boolean manual; private Status status; private ManualRedactionType manualRedactionType; + private String manualRedactionUserId; private boolean isDictionaryEntry; private String textBefore; diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java index 52ca7f10..cade6618 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java @@ -61,6 +61,7 @@ public class RedactionLogMergeService { redactionLogEntry.setStatus(Status.DECLINED); } + redactionLogEntry.setManualRedactionUserId(recategorization.getUser()); redactionLogEntry.setReason(manualOverrideReason); redactionLogEntry.setManual(true); redactionLogEntry.setManualRedactionType(ManualRedactionType.RECATEGORIZE); @@ -88,6 +89,7 @@ public class RedactionLogMergeService { redactionLogEntry.setReason(manualOverrideReason); redactionLogEntry.setManual(true); + redactionLogEntry.setManualRedactionUserId(manualRemoval.getUser()); redactionLogEntry.setManualRedactionType(ManualRedactionType.REMOVE); redactionLogEntry.setDictionaryEntry(manualRemoval.isRemoveFromDictionary()); redactionLogEntry.setDossierDictionaryEntry(manualRemoval.isRemoveFromDictionary()); @@ -114,6 +116,7 @@ public class RedactionLogMergeService { redactionLogEntry.setStatus(Status.DECLINED); } + redactionLogEntry.setManualRedactionUserId(manualForceRedact.getUser()); redactionLogEntry.setReason(manualOverrideReason); redactionLogEntry.setManual(true); redactionLogEntry.setManualRedactionType(ManualRedactionType.FORCE_REDACT); @@ -139,6 +142,7 @@ public class RedactionLogMergeService { redactionLogEntry.setStatus(Status.DECLINED); } + redactionLogEntry.setManualRedactionUserId(manualLegalBasisChange.getUser()); redactionLogEntry.setReason(manualOverrideReason); redactionLogEntry.setManual(true); redactionLogEntry.setManualRedactionType(ManualRedactionType.LEGAL_BASIS_CHANGE); @@ -209,6 +213,7 @@ public class RedactionLogMergeService { .manual(true) .status(manualRedactionEntry.getStatus()) .manualRedactionType(ManualRedactionType.ADD) + .manualRedactionUserId(manualRedactionEntry.getUser()) .build(); } From e8a4ef172c994ab8dd99f84f7a725e3dddfaca5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Wed, 11 Aug 2021 09:45:00 +0200 Subject: [PATCH 62/80] RED-1908: Removed RedactionChangeLog added changes to RedactionLog --- .../service/redaction/v1/model/Change.java | 18 +++ .../redaction/v1/model/ChangeType.java | 2 +- .../v1/model/RedactionChangeLog.java | 22 --- .../v1/model/RedactionChangeLogEntry.java | 49 ------ .../v1/model/RedactionLogChanges.java | 17 ++ .../redaction/v1/model/RedactionLogEntry.java | 4 + .../service/AnalyzeResponseService.java | 12 +- .../redaction/service/ReanalyzeService.java | 14 +- .../service/RedactionChangeLogService.java | 145 +++++++++--------- .../v1/server/RedactionIntegrationTest.java | 17 ++ 10 files changed, 142 insertions(+), 158 deletions(-) create mode 100644 redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/Change.java delete mode 100644 redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionChangeLog.java delete mode 100644 redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionChangeLogEntry.java create mode 100644 redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogChanges.java diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/Change.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/Change.java new file mode 100644 index 00000000..ebc59a84 --- /dev/null +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/Change.java @@ -0,0 +1,18 @@ +package com.iqser.red.service.redaction.v1.model; + +import java.time.OffsetDateTime; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class Change { + + private ChangeType type; + private OffsetDateTime dateTime; +} diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ChangeType.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ChangeType.java index 0c902a8f..53438d3c 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ChangeType.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ChangeType.java @@ -1,5 +1,5 @@ package com.iqser.red.service.redaction.v1.model; public enum ChangeType { - ADDED, REMOVED + ADDED, REMOVED, CHANGED } diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionChangeLog.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionChangeLog.java deleted file mode 100644 index 74e385c0..00000000 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionChangeLog.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.iqser.red.service.redaction.v1.model; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.ArrayList; -import java.util.List; - -@Data -@AllArgsConstructor -@NoArgsConstructor -public class RedactionChangeLog { - - private List redactionLogEntry = new ArrayList<>(); - - private long dictionaryVersion = -1; - private long dossierDictionaryVersion = -1; - private long rulesVersion = -1; - private long legalBasisVersion = -1; - -} diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionChangeLogEntry.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionChangeLogEntry.java deleted file mode 100644 index a53d3b0e..00000000 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionChangeLogEntry.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.iqser.red.service.redaction.v1.model; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.ArrayList; -import java.util.List; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class RedactionChangeLogEntry { - - private String id; - private String type; - private String value; - private String reason; - private int matchedRule; - private String legalBasis; - private boolean redacted; - private boolean isHint; - private boolean isRecommendation; - private String section; - private float[] color; - - @Builder.Default - private List positions = new ArrayList<>(); - private int sectionNumber; - private boolean manual; - private Status status; - private ManualRedactionType manualRedactionType; - private boolean isDictionaryEntry; - - private String textBefore; - private String textAfter; - - @Builder.Default - private List comments = new ArrayList<>(); - - private ChangeType changeType; - - private boolean isDossierDictionaryEntry; - - private boolean excluded; - -} diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogChanges.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogChanges.java new file mode 100644 index 00000000..77fe374d --- /dev/null +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogChanges.java @@ -0,0 +1,17 @@ +package com.iqser.red.service.redaction.v1.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class RedactionLogChanges { + + private RedactionLog redactionLog; + private boolean hasChanges; + +} diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java index 38497fa5..dc14eebc 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java @@ -58,4 +58,8 @@ public class RedactionLogEntry { private String recategorizationType; private String legalBasisChangeValue; + @EqualsAndHashCode.Exclude + @Builder.Default + private List changes = new ArrayList<>(); + } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java index 2ddbcc46..bd8042ff 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java @@ -1,12 +1,10 @@ package com.iqser.red.service.redaction.v1.server.redaction.service; import com.iqser.red.service.redaction.v1.model.AnalyzeResult; -import com.iqser.red.service.redaction.v1.model.RedactionChangeLog; import com.iqser.red.service.redaction.v1.model.RedactionLog; -import com.iqser.red.service.redaction.v1.model.RedactionLogEntry; import com.iqser.red.service.redaction.v1.server.settings.RedactionServiceSettings; -import lombok.NoArgsConstructor; + import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -17,7 +15,7 @@ public class AnalyzeResponseService { private final RedactionServiceSettings redactionServiceSettings; public AnalyzeResult createAnalyzeResponse(String dossierId, String fileId, long duration, int pageCount, - RedactionLog redactionLog, RedactionChangeLog redactionChangeLog) { + RedactionLog redactionLog, boolean hasUpdates) { boolean hasHints = redactionLog.getRedactionLogEntry() .stream() @@ -41,12 +39,6 @@ public class AnalyzeResponseService { .filter(entry -> !entry.isExcluded()) .anyMatch(entry -> entry.isHint() && entry.getType().equals("image") || entry.isImage()); - boolean hasUpdates = redactionChangeLog != null && redactionChangeLog.getRedactionLogEntry() != null && !redactionChangeLog - .getRedactionLogEntry() - .isEmpty() && redactionChangeLog.getRedactionLogEntry() - .stream() - .anyMatch(entry -> !entry.getType().equals("false_positive")); - return AnalyzeResult.builder() .dossierId(dossierId) .fileId(fileId) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java index e3b37c5f..21df6b0f 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java @@ -78,9 +78,9 @@ public class ReanalyzeService { log.info("Analyzed with rules {} and dictionary {} for dossierTemplate: {}", classifiedDoc.getRulesVersion(), classifiedDoc .getDictionaryVersion(), analyzeRequest.getDossierTemplateId()); - // first create changelog - this only happens when we migrate files analyzed via the old process and we don't want to loose changeLog data - var changeLog = redactionChangeLogService.createAndStoreChangeLog(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), redactionLog); - // store redactionLog + var redactionLogChange = redactionChangeLogService.computeChanges(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), redactionLog); + redactionLog = redactionLogChange.getRedactionLog(); + redactionStorageService.storeObject(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), FileType.REDACTION_LOG, redactionLog); redactionStorageService.storeObject(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), FileType.TEXT, new Text(pageCount, classifiedDoc .getSectionText())); @@ -88,7 +88,7 @@ public class ReanalyzeService { .getSectionGrid()); long duration = System.currentTimeMillis() - startTime; - return analyzeResponseService.createAnalyzeResponse(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), duration, pageCount, redactionLog, changeLog); + return analyzeResponseService.createAnalyzeResponse(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), duration, pageCount, redactionLog, redactionLogChange.isHasChanges()); } @@ -266,13 +266,13 @@ public class ReanalyzeService { excludeExcludedPages(redactionLog, analyzeRequest.getExcludedPages()); - var changeLog = redactionChangeLogService.createAndStoreChangeLog(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), redactionLog); - redactionStorageService.storeObject(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), FileType.REDACTION_LOG, redactionLog); + var redactionLogChange = redactionChangeLogService.computeChanges(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), redactionLog); + redactionStorageService.storeObject(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), FileType.REDACTION_LOG, redactionLogChange.getRedactionLog()); long duration = System.currentTimeMillis() - startTime; return analyzeResponseService.createAnalyzeResponse(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), duration, text - .getNumberOfPages(), redactionLog, changeLog); + .getNumberOfPages(), redactionLogChange.getRedactionLog(), redactionLogChange.isHasChanges()); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionChangeLogService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionChangeLogService.java index 1cc743e5..41772291 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionChangeLogService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionChangeLogService.java @@ -1,19 +1,25 @@ package com.iqser.red.service.redaction.v1.server.redaction.service; -import com.iqser.red.service.file.management.v1.api.model.FileType; -import com.iqser.red.service.redaction.v1.model.ChangeType; -import com.iqser.red.service.redaction.v1.model.RedactionChangeLog; -import com.iqser.red.service.redaction.v1.model.RedactionChangeLogEntry; -import com.iqser.red.service.redaction.v1.model.RedactionLog; -import com.iqser.red.service.redaction.v1.model.RedactionLogEntry; -import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + import org.springframework.stereotype.Service; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; +import com.iqser.red.service.redaction.v1.model.Change; +import com.iqser.red.service.redaction.v1.model.ChangeType; +import com.iqser.red.service.redaction.v1.model.RedactionLog; +import com.iqser.red.service.redaction.v1.model.RedactionLogChanges; +import com.iqser.red.service.redaction.v1.model.RedactionLogEntry; +import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; @Slf4j @Service @@ -22,76 +28,77 @@ public class RedactionChangeLogService { private final RedactionStorageService redactionStorageService; - public RedactionChangeLog createAndStoreChangeLog(String dossierId, String fileId, RedactionLog currentRedactionLog) { - try { - RedactionLog previousRedactionLog = redactionStorageService.getRedactionLog(dossierId, fileId); - var changeLog = createChangeLog(currentRedactionLog, previousRedactionLog); - redactionStorageService.storeObject(dossierId, fileId, FileType.REDACTION_CHANGELOG, changeLog); - return changeLog; - } catch (Exception e) { - log.debug("Previous redaction log not available"); - return null; - } - - } - - - private RedactionChangeLog createChangeLog(RedactionLog currentRedactionLog, RedactionLog previousRedactionLog) { + public RedactionLogChanges computeChanges(String dossierId, String fileId, RedactionLog currentRedactionLog) { + RedactionLog previousRedactionLog = redactionStorageService.getRedactionLog(dossierId, fileId); if (previousRedactionLog == null) { - return null; + currentRedactionLog.getRedactionLogEntry().forEach(entry -> { + entry.getChanges().add(new Change(ChangeType.ADDED, OffsetDateTime.now())); + }); + return new RedactionLogChanges(currentRedactionLog, false); } - List added = new ArrayList<>(currentRedactionLog.getRedactionLogEntry()); - added.removeAll(previousRedactionLog.getRedactionLogEntry()); + List notRemovedPreviousEntries = previousRedactionLog.getRedactionLogEntry() + .stream() + .filter(entry -> !entry.getChanges() + .get(entry.getChanges().size() - 1) + .getType() + .equals(ChangeType.REMOVED)) + .collect(Collectors.toList()); - List removed = new ArrayList<>(previousRedactionLog.getRedactionLogEntry()); + Set added = new HashSet<>(currentRedactionLog.getRedactionLogEntry()); + added.removeAll(notRemovedPreviousEntries); + + Set removed = new HashSet<>(notRemovedPreviousEntries); removed.removeAll(currentRedactionLog.getRedactionLogEntry()); - List changeLogEntries = added.stream() - .map(entry -> convert(entry, ChangeType.ADDED)) - .collect(Collectors.toList()); - changeLogEntries.addAll(removed.stream() - .map(entry -> convert(entry, ChangeType.REMOVED)) - .collect(Collectors.toList())); + Map addedIds = new HashMap<>(); + added.forEach(entry -> { + addedIds.put(entry.getId(), entry); + }); - return new RedactionChangeLog(changeLogEntries, - currentRedactionLog.getDictionaryVersion(), - currentRedactionLog.getDossierDictionaryVersion(), - currentRedactionLog.getRulesVersion(), - currentRedactionLog.getLegalBasisVersion()); - } + Set removedIds = new HashSet<>(); + removed.forEach(entry -> { + removedIds.add(entry.getId()); + }); + List newRedactionLogEntries = previousRedactionLog.getRedactionLogEntry(); - private RedactionChangeLogEntry convert(RedactionLogEntry entry, ChangeType changeType) { + List toRemove = new ArrayList<>(); + newRedactionLogEntries.forEach(entry -> { + if (removedIds.contains(entry.getId()) && addedIds.containsKey(entry.getId())) { + List changes = entry.getChanges(); + changes.add(new Change(ChangeType.CHANGED, OffsetDateTime.now())); + var newEntry = addedIds.get(entry.getId()); + newEntry.setChanges(changes); + addedIds.put(entry.getId(), newEntry); + toRemove.add(entry); + } else if (removedIds.contains(entry.getId())) { + entry.getChanges().add(new Change(ChangeType.REMOVED, OffsetDateTime.now())); + } else if (addedIds.containsKey(entry.getId())) { + List changes = entry.getChanges(); + changes.add(new Change(ChangeType.ADDED, OffsetDateTime.now())); + var newEntry = addedIds.get(entry.getId()); + newEntry.setChanges(changes); + addedIds.put(entry.getId(), newEntry); + toRemove.add(entry); + } + }); - return RedactionChangeLogEntry.builder() - .id(entry.getId()) - .type(entry.getType()) - .value(entry.getValue()) - .reason(entry.getReason()) - .matchedRule(entry.getMatchedRule()) - .legalBasis(entry.getLegalBasis()) - .redacted(entry.isRedacted()) - .isHint(entry.isHint()) - .isRecommendation(entry.isRecommendation()) - .section(entry.getSection()) - .color(entry.getColor()) - .positions(entry.getPositions()) - .sectionNumber(entry.getSectionNumber()) - .manual(entry.isManual()) - .status(entry.getStatus()) - .manualRedactionType(entry.getManualRedactionType()) - .isDictionaryEntry(entry.isDictionaryEntry()) - .textBefore(entry.getTextBefore()) - .textAfter(entry.getTextAfter()) - .comments(entry.getComments()) - .changeType(changeType) - .isDossierDictionaryEntry(entry.isDossierDictionaryEntry()) - .excluded(entry.isExcluded()) - .build(); + newRedactionLogEntries.removeAll(toRemove); + + addedIds.forEach((k, v) -> { + if(v.getChanges().isEmpty()) { + v.getChanges().add(new Change(ChangeType.ADDED, OffsetDateTime.now())); + } + newRedactionLogEntries.add(v); + }); + + currentRedactionLog.setRedactionLogEntry(newRedactionLogEntries); + + return new RedactionLogChanges(currentRedactionLog, !addedIds.isEmpty() || !removedIds.isEmpty()); } } diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java index 6949ff40..5a4daeb8 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java @@ -714,10 +714,13 @@ public class RedactionIntegrationTest { reanlysisVersions.put("physical", 2L); deleted.add("David Chubb"); + deleted.add("mouse"); dictionary.get(FALSE_POSITIVE).add("David Chubb"); reanlysisVersions.put("David Chubb", 3L); + reanlysisVersions.put("mouse", 3L); + when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(3L); when(dictionaryClient.getDictionaryForType(VERTEBRATE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) @@ -754,6 +757,20 @@ public class RedactionIntegrationTest { fileOutputStream.write(annotateResponse.getDocument()); } + + deleted.remove("mouse"); + reanlysisVersions.put("mouse", 4L); + + when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(4L); + + when(dictionaryClient.getDictionaryForType(VERTEBRATE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + .thenReturn(getDictionaryResponse(VERTEBRATE, false)); + + reanalyzeService.reanalyze(request); + + redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); + + System.out.println("hi"); } From bb80b7ae7550d70a98b4d500d3870a13585b57e8 Mon Sep 17 00:00:00 2001 From: Timo Bejan Date: Fri, 13 Aug 2021 11:19:54 +0300 Subject: [PATCH 63/80] Proper Order of manual actions based on time --- .../service/RedactionLogMergeService.java | 268 ++++++++++-------- 1 file changed, 153 insertions(+), 115 deletions(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java index cade6618..8d1b734a 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java @@ -1,14 +1,17 @@ package com.iqser.red.service.redaction.v1.server.redaction.service; +import com.iqser.red.service.file.management.v1.api.model.manual.ForceRedactionRequest; import com.iqser.red.service.redaction.v1.model.*; +import lombok.AllArgsConstructor; +import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.kie.api.definition.rule.All; import org.springframework.stereotype.Service; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.time.OffsetDateTime; +import java.util.*; +import java.util.stream.Collectors; @Slf4j @Service @@ -27,9 +30,13 @@ public class RedactionLogMergeService { redactionLog.getRedactionLogEntry().addAll(manualRedactionLogEntries); + var manualRedactionWrappers = createManualRedactionWrappers(manualRedactions); + log.info("Adding {}", manualRedactionLogEntries); for (RedactionLogEntry entry : redactionLog.getRedactionLogEntry()) { - processRedactionLogEntry(manualRedactions, dossierTemplateId, entry); + + processRedactionLogEntry(manualRedactionWrappers.stream().filter(mr -> entry.getId().equals(mr.getId())) + .collect(Collectors.toList()), dossierTemplateId, entry); entry.setComments(manualRedactions.getComments().get(entry.getId())); } @@ -39,123 +46,140 @@ public class RedactionLogMergeService { } - private void processRedactionLogEntry(ManualRedactions manualRedactions, String dossierTemplateId, RedactionLogEntry redactionLogEntry) { - - List comments = null; - - - if (manualRedactions != null && !manualRedactions.getImageRecategorizations().isEmpty()) { - for (ManualImageRecategorization recategorization : manualRedactions.getImageRecategorizations()) { - if (recategorization.getId().equals(redactionLogEntry.getId())) { - String manualOverrideReason = null; - if (recategorization.getStatus().equals(Status.APPROVED)) { - redactionLogEntry.setStatus(Status.APPROVED); - redactionLogEntry.setType(recategorization.getType()); - manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", recategorized by manual override"); - } else if (recategorization.getStatus().equals(Status.REQUESTED)) { - manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", requested to recategorize"); - redactionLogEntry.setStatus(Status.REQUESTED); - redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, false, redactionLogEntry.isRedacted(), false)); - redactionLogEntry.setRecategorizationType(recategorization.getType()); - } else { - redactionLogEntry.setStatus(Status.DECLINED); - } - - redactionLogEntry.setManualRedactionUserId(recategorization.getUser()); - redactionLogEntry.setReason(manualOverrideReason); - redactionLogEntry.setManual(true); - redactionLogEntry.setManualRedactionType(ManualRedactionType.RECATEGORIZE); - } + private List createManualRedactionWrappers(ManualRedactions manualRedactions) { + List manualRedactionWrappers = new ArrayList<>(); + manualRedactions.getForceRedacts().forEach(item -> { + if (item.getSoftDeletedTime() != null) { + manualRedactionWrappers.add(new ManualRedactionWrapper(item.getId(), item.getRequestDate(), item)); } - } - - if (manualRedactions != null && !manualRedactions.getIdsToRemove().isEmpty()) { - for (IdRemoval manualRemoval : manualRedactions.getIdsToRemove()) { - if (manualRemoval.getId().equals(redactionLogEntry.getId())) { - comments = manualRedactions.getComments().get(manualRemoval.getId()); - String manualOverrideReason = null; - if (manualRemoval.getStatus().equals(Status.APPROVED)) { - redactionLogEntry.setRedacted(false); - redactionLogEntry.setStatus(Status.APPROVED); - manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", removed by manual override"); - redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, false, redactionLogEntry.isRedacted(), true)); - } else if (manualRemoval.getStatus().equals(Status.REQUESTED)) { - manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", requested to remove"); - redactionLogEntry.setStatus(Status.REQUESTED); - redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, true, redactionLogEntry.isRedacted(), false)); - } else { - redactionLogEntry.setStatus(Status.DECLINED); - } - - redactionLogEntry.setReason(manualOverrideReason); - redactionLogEntry.setManual(true); - redactionLogEntry.setManualRedactionUserId(manualRemoval.getUser()); - redactionLogEntry.setManualRedactionType(ManualRedactionType.REMOVE); - redactionLogEntry.setDictionaryEntry(manualRemoval.isRemoveFromDictionary()); - redactionLogEntry.setDossierDictionaryEntry(manualRemoval.isRemoveFromDictionary()); - } + }); + manualRedactions.getEntriesToAdd().forEach(item -> { + if (item.getSoftDeletedTime() != null) { + manualRedactionWrappers.add(new ManualRedactionWrapper(item.getId(), item.getRequestDate(), item)); } - } - - if (manualRedactions != null && !manualRedactions.getForceRedacts().isEmpty()) { - for (ManualForceRedact manualForceRedact : manualRedactions.getForceRedacts()) { - if (manualForceRedact.getId().equals(redactionLogEntry.getId())) { - String manualOverrideReason = null; - if (manualForceRedact.getStatus().equals(Status.APPROVED)) { - redactionLogEntry.setRedacted(true); - redactionLogEntry.setStatus(Status.APPROVED); - redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, false, redactionLogEntry.isRedacted(), false)); - manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", forced by manual override"); - redactionLogEntry.setLegalBasis(manualForceRedact.getLegalBasis()); - } else if (manualForceRedact.getStatus().equals(Status.REQUESTED)) { - manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", requested to force redact"); - redactionLogEntry.setStatus(Status.REQUESTED); - redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, true, redactionLogEntry.isRedacted(), false)); - redactionLogEntry.setLegalBasis(manualForceRedact.getLegalBasis()); - } else { - redactionLogEntry.setStatus(Status.DECLINED); - } - - redactionLogEntry.setManualRedactionUserId(manualForceRedact.getUser()); - redactionLogEntry.setReason(manualOverrideReason); - redactionLogEntry.setManual(true); - redactionLogEntry.setManualRedactionType(ManualRedactionType.FORCE_REDACT); - - } + }); + manualRedactions.getIdsToRemove().forEach(item -> { + if (item.getSoftDeletedTime() != null) { + manualRedactionWrappers.add(new ManualRedactionWrapper(item.getId(), item.getRequestDate(), item)); } - } - - if (manualRedactions != null && !manualRedactions.getManualLegalBasisChanges().isEmpty()) { - for (ManualLegalBasisChange manualLegalBasisChange : manualRedactions.getManualLegalBasisChanges()) { - if (manualLegalBasisChange.getId().equals(redactionLogEntry.getId())) { - String manualOverrideReason = null; - if (manualLegalBasisChange.getStatus().equals(Status.APPROVED)) { - redactionLogEntry.setStatus(Status.APPROVED); - manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", legal basis was manually changed"); - redactionLogEntry.setLegalBasis(manualLegalBasisChange.getLegalBasis()); - } else if (manualLegalBasisChange.getStatus().equals(Status.REQUESTED)) { - manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", legal basis change requested"); - redactionLogEntry.setStatus(Status.REQUESTED); - redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, true, redactionLogEntry.isRedacted(), false)); - redactionLogEntry.setLegalBasisChangeValue(manualLegalBasisChange.getLegalBasis()); - } else { - redactionLogEntry.setStatus(Status.DECLINED); - } - - redactionLogEntry.setManualRedactionUserId(manualLegalBasisChange.getUser()); - redactionLogEntry.setReason(manualOverrideReason); - redactionLogEntry.setManual(true); - redactionLogEntry.setManualRedactionType(ManualRedactionType.LEGAL_BASIS_CHANGE); - } + }); + manualRedactions.getManualLegalBasisChanges().forEach(item -> { + if (item.getSoftDeletedTime() != null) { + manualRedactionWrappers.add(new ManualRedactionWrapper(item.getId(), item.getRequestDate(), item)); } - } + }); + manualRedactions.getImageRecategorizations().forEach(item -> { + if (item.getSoftDeletedTime() != null) { + manualRedactionWrappers.add(new ManualRedactionWrapper(item.getId(), item.getRequestDate(), item)); + } + }); - if (manualRedactions != null) { - comments = manualRedactions.getComments().get(redactionLogEntry.getId()); - } + Collections.sort(manualRedactionWrappers); + return manualRedactionWrappers; + } + + private void processRedactionLogEntry(List manualRedactionWrappers, String dossierTemplateId, RedactionLogEntry redactionLogEntry) { + + + manualRedactionWrappers.forEach(mrw -> { + + if (mrw.getItem() instanceof ManualImageRecategorization) { + var imageRecategorization = (ManualImageRecategorization) mrw.getItem(); + String manualOverrideReason = null; + if (imageRecategorization.getStatus().equals(Status.APPROVED)) { + redactionLogEntry.setStatus(Status.APPROVED); + redactionLogEntry.setType(imageRecategorization.getType()); + manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", recategorized by manual override"); + } else if (imageRecategorization.getStatus().equals(Status.REQUESTED)) { + manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", requested to recategorize"); + redactionLogEntry.setStatus(Status.REQUESTED); + redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, false, redactionLogEntry.isRedacted(), false)); + redactionLogEntry.setRecategorizationType(imageRecategorization.getType()); + } else { + redactionLogEntry.setStatus(Status.DECLINED); + } + + redactionLogEntry.setManualRedactionUserId(imageRecategorization.getUser()); + redactionLogEntry.setReason(manualOverrideReason); + redactionLogEntry.setManual(true); + redactionLogEntry.setManualRedactionType(ManualRedactionType.RECATEGORIZE); + } + + if (mrw.getItem() instanceof IdRemoval) { + var manualRemoval = (IdRemoval) mrw.getItem(); + String manualOverrideReason = null; + if (manualRemoval.getStatus().equals(Status.APPROVED)) { + redactionLogEntry.setRedacted(false); + redactionLogEntry.setStatus(Status.APPROVED); + manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", removed by manual override"); + redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, false, redactionLogEntry.isRedacted(), true)); + } else if (manualRemoval.getStatus().equals(Status.REQUESTED)) { + manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", requested to remove"); + redactionLogEntry.setStatus(Status.REQUESTED); + redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, true, redactionLogEntry.isRedacted(), false)); + } else { + redactionLogEntry.setStatus(Status.DECLINED); + } + + redactionLogEntry.setReason(manualOverrideReason); + redactionLogEntry.setManual(true); + redactionLogEntry.setManualRedactionUserId(manualRemoval.getUser()); + redactionLogEntry.setManualRedactionType(ManualRedactionType.REMOVE); + redactionLogEntry.setDictionaryEntry(manualRemoval.isRemoveFromDictionary()); + redactionLogEntry.setDossierDictionaryEntry(manualRemoval.isRemoveFromDictionary()); + } + + + if (mrw.getItem() instanceof ManualForceRedact) { + var manualForceRedact = (ManualForceRedact) mrw.getItem(); + String manualOverrideReason = null; + if (manualForceRedact.getStatus().equals(Status.APPROVED)) { + redactionLogEntry.setRedacted(true); + redactionLogEntry.setStatus(Status.APPROVED); + redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, false, redactionLogEntry.isRedacted(), false)); + manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", forced by manual override"); + redactionLogEntry.setLegalBasis(manualForceRedact.getLegalBasis()); + } else if (manualForceRedact.getStatus().equals(Status.REQUESTED)) { + manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", requested to force redact"); + redactionLogEntry.setStatus(Status.REQUESTED); + redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, true, redactionLogEntry.isRedacted(), false)); + redactionLogEntry.setLegalBasis(manualForceRedact.getLegalBasis()); + } else { + redactionLogEntry.setStatus(Status.DECLINED); + } + + redactionLogEntry.setManualRedactionUserId(manualForceRedact.getUser()); + redactionLogEntry.setReason(manualOverrideReason); + redactionLogEntry.setManual(true); + redactionLogEntry.setManualRedactionType(ManualRedactionType.FORCE_REDACT); + + } + + if (mrw.getItem() instanceof ManualLegalBasisChange) { + var manualLegalBasisChange = (ManualLegalBasisChange) mrw.getItem(); + String manualOverrideReason = null; + if (manualLegalBasisChange.getStatus().equals(Status.APPROVED)) { + redactionLogEntry.setStatus(Status.APPROVED); + manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", legal basis was manually changed"); + redactionLogEntry.setLegalBasis(manualLegalBasisChange.getLegalBasis()); + } else if (manualLegalBasisChange.getStatus().equals(Status.REQUESTED)) { + manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", legal basis change requested"); + redactionLogEntry.setStatus(Status.REQUESTED); + redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, true, redactionLogEntry.isRedacted(), false)); + redactionLogEntry.setLegalBasisChangeValue(manualLegalBasisChange.getLegalBasis()); + } else { + redactionLogEntry.setStatus(Status.DECLINED); + } + + redactionLogEntry.setManualRedactionUserId(manualLegalBasisChange.getUser()); + redactionLogEntry.setReason(manualOverrideReason); + redactionLogEntry.setManual(true); + redactionLogEntry.setManualRedactionType(ManualRedactionType.LEGAL_BASIS_CHANGE); + } + + }); - redactionLogEntry.setComments(comments); } private String mergeReasonIfNecessary(String currentReason, String addition) { @@ -245,5 +269,19 @@ public class RedactionLogMergeService { return dictionaryService.getColor(type, dossierTemplateId); } + @Data + @AllArgsConstructor + private static class ManualRedactionWrapper implements Comparable { + + private String id; + private OffsetDateTime date; + private Object item; + + @Override + public int compareTo(ManualRedactionWrapper o) { + return this.date.compareTo(o.date); + } + } + } From e43e8bccbf283ccb5ea1eeb405020229fb66ea3c Mon Sep 17 00:00:00 2001 From: Timo Bejan Date: Fri, 13 Aug 2021 13:00:32 +0200 Subject: [PATCH 64/80] RedactionLogMergeService.java edited online with Bitbucket --- .../service/RedactionLogMergeService.java | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java index 8d1b734a..ce09f06c 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java @@ -48,34 +48,36 @@ public class RedactionLogMergeService { private List createManualRedactionWrappers(ManualRedactions manualRedactions) { List manualRedactionWrappers = new ArrayList<>(); - manualRedactions.getForceRedacts().forEach(item -> { - if (item.getSoftDeletedTime() != null) { - manualRedactionWrappers.add(new ManualRedactionWrapper(item.getId(), item.getRequestDate(), item)); - } - }); - manualRedactions.getEntriesToAdd().forEach(item -> { - if (item.getSoftDeletedTime() != null) { - manualRedactionWrappers.add(new ManualRedactionWrapper(item.getId(), item.getRequestDate(), item)); - } - }); - manualRedactions.getIdsToRemove().forEach(item -> { - if (item.getSoftDeletedTime() != null) { - manualRedactionWrappers.add(new ManualRedactionWrapper(item.getId(), item.getRequestDate(), item)); - } - }); - manualRedactions.getManualLegalBasisChanges().forEach(item -> { - if (item.getSoftDeletedTime() != null) { - manualRedactionWrappers.add(new ManualRedactionWrapper(item.getId(), item.getRequestDate(), item)); - } - }); + + log.info("Processing Manual Redactions: {}", manualRedactions); + manualRedactions.getImageRecategorizations().forEach(item -> { - if (item.getSoftDeletedTime() != null) { + if (item.getSoftDeletedTime() == null) { + manualRedactionWrappers.add(new ManualRedactionWrapper(item.getId(), item.getRequestDate(), item)); + } + }); + + manualRedactions.getIdsToRemove().forEach(item -> { + if (item.getSoftDeletedTime() == null) { + manualRedactionWrappers.add(new ManualRedactionWrapper(item.getId(), item.getRequestDate(), item)); + } + }); + + manualRedactions.getForceRedacts().forEach(item -> { + if (item.getSoftDeletedTime() == null) { + manualRedactionWrappers.add(new ManualRedactionWrapper(item.getId(), item.getRequestDate(), item)); + } + }); + + manualRedactions.getManualLegalBasisChanges().forEach(item -> { + if (item.getSoftDeletedTime() == null) { manualRedactionWrappers.add(new ManualRedactionWrapper(item.getId(), item.getRequestDate(), item)); } }); Collections.sort(manualRedactionWrappers); + log.info("Obtained manual redaction wrappers: {}", manualRedactionWrappers); return manualRedactionWrappers; } From 5312d5ffdfcd8ab11d87815f4a90745f68d7ca29 Mon Sep 17 00:00:00 2001 From: Timo Bejan Date: Fri, 13 Aug 2021 13:19:42 +0200 Subject: [PATCH 65/80] RedactionLogMergeService.java edited online with Bitbucket --- .../service/RedactionLogMergeService.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java index ce09f06c..406d01e8 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java @@ -37,6 +37,7 @@ public class RedactionLogMergeService { processRedactionLogEntry(manualRedactionWrappers.stream().filter(mr -> entry.getId().equals(mr.getId())) .collect(Collectors.toList()), dossierTemplateId, entry); + entry.setComments(manualRedactions.getComments().get(entry.getId())); } @@ -47,30 +48,30 @@ public class RedactionLogMergeService { private List createManualRedactionWrappers(ManualRedactions manualRedactions) { + log.info("Processing Manual Redactions: {}", manualRedactions); + List manualRedactionWrappers = new ArrayList<>(); - log.info("Processing Manual Redactions: {}", manualRedactions); - manualRedactions.getImageRecategorizations().forEach(item -> { - if (item.getSoftDeletedTime() == null) { + if (item.getSoftDeletedTime() != null) { manualRedactionWrappers.add(new ManualRedactionWrapper(item.getId(), item.getRequestDate(), item)); } }); manualRedactions.getIdsToRemove().forEach(item -> { - if (item.getSoftDeletedTime() == null) { + if (item.getSoftDeletedTime() != null) { manualRedactionWrappers.add(new ManualRedactionWrapper(item.getId(), item.getRequestDate(), item)); } }); manualRedactions.getForceRedacts().forEach(item -> { - if (item.getSoftDeletedTime() == null) { + if (item.getSoftDeletedTime() != null) { manualRedactionWrappers.add(new ManualRedactionWrapper(item.getId(), item.getRequestDate(), item)); } }); manualRedactions.getManualLegalBasisChanges().forEach(item -> { - if (item.getSoftDeletedTime() == null) { + if (item.getSoftDeletedTime() != null) { manualRedactionWrappers.add(new ManualRedactionWrapper(item.getId(), item.getRequestDate(), item)); } }); @@ -84,6 +85,8 @@ public class RedactionLogMergeService { private void processRedactionLogEntry(List manualRedactionWrappers, String dossierTemplateId, RedactionLogEntry redactionLogEntry) { + log.info("Processing: {} for: {}", manualRedactionWrappers, redactionLogEntry); + manualRedactionWrappers.forEach(mrw -> { if (mrw.getItem() instanceof ManualImageRecategorization) { @@ -287,3 +290,4 @@ public class RedactionLogMergeService { } + From 1f12f9571f582032a5e10386a00525a22b9a131c Mon Sep 17 00:00:00 2001 From: Timo Bejan Date: Fri, 13 Aug 2021 13:56:13 +0200 Subject: [PATCH 66/80] RedactionLogMergeService.java edited online with Bitbucket --- .../service/RedactionLogMergeService.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java index 406d01e8..86b90b7f 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java @@ -32,7 +32,6 @@ public class RedactionLogMergeService { var manualRedactionWrappers = createManualRedactionWrappers(manualRedactions); - log.info("Adding {}", manualRedactionLogEntries); for (RedactionLogEntry entry : redactionLog.getRedactionLogEntry()) { processRedactionLogEntry(manualRedactionWrappers.stream().filter(mr -> entry.getId().equals(mr.getId())) @@ -48,44 +47,44 @@ public class RedactionLogMergeService { private List createManualRedactionWrappers(ManualRedactions manualRedactions) { - log.info("Processing Manual Redactions: {}", manualRedactions); + List manualRedactionWrappers = new ArrayList<>(); manualRedactions.getImageRecategorizations().forEach(item -> { - if (item.getSoftDeletedTime() != null) { + if (item.getSoftDeletedTime() == null) { manualRedactionWrappers.add(new ManualRedactionWrapper(item.getId(), item.getRequestDate(), item)); } }); manualRedactions.getIdsToRemove().forEach(item -> { - if (item.getSoftDeletedTime() != null) { + if (item.getSoftDeletedTime() == null) { manualRedactionWrappers.add(new ManualRedactionWrapper(item.getId(), item.getRequestDate(), item)); } }); manualRedactions.getForceRedacts().forEach(item -> { - if (item.getSoftDeletedTime() != null) { + if (item.getSoftDeletedTime() == null) { manualRedactionWrappers.add(new ManualRedactionWrapper(item.getId(), item.getRequestDate(), item)); } }); manualRedactions.getManualLegalBasisChanges().forEach(item -> { - if (item.getSoftDeletedTime() != null) { + if (item.getSoftDeletedTime() == null) { manualRedactionWrappers.add(new ManualRedactionWrapper(item.getId(), item.getRequestDate(), item)); } }); Collections.sort(manualRedactionWrappers); - log.info("Obtained manual redaction wrappers: {}", manualRedactionWrappers); + return manualRedactionWrappers; } private void processRedactionLogEntry(List manualRedactionWrappers, String dossierTemplateId, RedactionLogEntry redactionLogEntry) { - log.info("Processing: {} for: {}", manualRedactionWrappers, redactionLogEntry); + manualRedactionWrappers.forEach(mrw -> { From 4d514d0e5ed1c22436bd8ba58578022d5fb8c4db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Tue, 17 Aug 2021 11:28:52 +0200 Subject: [PATCH 67/80] RED-2015: Fixed (Manual) Redactions in download reports/files for excluded pages --- .../redaction/v1/model/RedactionRequest.java | 5 + .../controller/RedactionController.java | 2 +- .../redaction/service/ReanalyzeService.java | 8 +- .../service/RedactionLogMergeService.java | 92 +++++++++++++------ 4 files changed, 75 insertions(+), 32 deletions(-) diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionRequest.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionRequest.java index 263e7692..d92f0edf 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionRequest.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionRequest.java @@ -1,5 +1,8 @@ package com.iqser.red.service.redaction.v1.model; +import java.util.HashSet; +import java.util.Set; + import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -15,4 +18,6 @@ public class RedactionRequest { private String fileId; private String dossierTemplateId; private ManualRedactions manualRedactions; + @Builder.Default + private Set excludedPages = new HashSet<>(); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java index 95815664..2ea691d7 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java @@ -159,7 +159,7 @@ public class RedactionController implements RedactionResource { // old redaction logs are returned directly return redactionLog; } else { - return redactionLogMergeService.mergeRedactionLogData(redactionLog, redactionRequest.getDossierTemplateId(), redactionRequest.getManualRedactions()); + return redactionLogMergeService.mergeRedactionLogData(redactionLog, redactionRequest.getDossierTemplateId(), redactionRequest.getManualRedactions(), redactionRequest.getExcludedPages()); } } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java index 21df6b0f..1daf766b 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java @@ -311,9 +311,11 @@ public class ReanalyzeService { private void excludeExcludedPages(RedactionLog redactionLog, Set excludedPages) { - redactionLog.getRedactionLogEntry() - .forEach(entry -> entry.getPositions() - .forEach(pos -> entry.setExcluded(excludedPages != null && excludedPages.contains(pos.getPage())))); + if(excludedPages != null && !excludedPages.isEmpty()) { + redactionLog.getRedactionLogEntry().forEach(entry -> entry.getPositions().forEach(pos -> { if (excludedPages.contains(pos.getPage())) { + entry.setExcluded(true); + }})); + } } } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java index 86b90b7f..5c1d9fcc 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogMergeService.java @@ -1,17 +1,31 @@ package com.iqser.red.service.redaction.v1.server.redaction.service; -import com.iqser.red.service.file.management.v1.api.model.manual.ForceRedactionRequest; -import com.iqser.red.service.redaction.v1.model.*; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; + +import com.iqser.red.service.redaction.v1.model.Comment; +import com.iqser.red.service.redaction.v1.model.IdRemoval; +import com.iqser.red.service.redaction.v1.model.ManualForceRedact; +import com.iqser.red.service.redaction.v1.model.ManualImageRecategorization; +import com.iqser.red.service.redaction.v1.model.ManualLegalBasisChange; +import com.iqser.red.service.redaction.v1.model.ManualRedactionEntry; +import com.iqser.red.service.redaction.v1.model.ManualRedactionType; +import com.iqser.red.service.redaction.v1.model.ManualRedactions; +import com.iqser.red.service.redaction.v1.model.RedactionLog; +import com.iqser.red.service.redaction.v1.model.RedactionLogEntry; +import com.iqser.red.service.redaction.v1.model.Status; + import lombok.AllArgsConstructor; import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.kie.api.definition.rule.All; -import org.springframework.stereotype.Service; - -import java.time.OffsetDateTime; -import java.util.*; -import java.util.stream.Collectors; @Slf4j @Service @@ -21,7 +35,8 @@ public class RedactionLogMergeService { private final DictionaryService dictionaryService; - public RedactionLog mergeRedactionLogData(RedactionLog redactionLog, String dossierTemplateId, ManualRedactions manualRedactions) { + public RedactionLog mergeRedactionLogData(RedactionLog redactionLog, String dossierTemplateId, + ManualRedactions manualRedactions, Set excludedPages) { log.info("Merging Redaction log with manual redactions {}", manualRedactions); if (manualRedactions != null) { @@ -34,10 +49,19 @@ public class RedactionLogMergeService { for (RedactionLogEntry entry : redactionLog.getRedactionLogEntry()) { - processRedactionLogEntry(manualRedactionWrappers.stream().filter(mr -> entry.getId().equals(mr.getId())) + processRedactionLogEntry(manualRedactionWrappers.stream() + .filter(mr -> entry.getId().equals(mr.getId())) .collect(Collectors.toList()), dossierTemplateId, entry); entry.setComments(manualRedactions.getComments().get(entry.getId())); + + if (excludedPages != null && !excludedPages.isEmpty()) { + entry.getPositions().forEach(pos -> { + if (excludedPages.contains(pos.getPage())) { + entry.setExcluded(true); + } + }); + } } } @@ -47,8 +71,7 @@ public class RedactionLogMergeService { private List createManualRedactionWrappers(ManualRedactions manualRedactions) { - - + List manualRedactionWrappers = new ArrayList<>(); manualRedactions.getImageRecategorizations().forEach(item -> { @@ -75,16 +98,14 @@ public class RedactionLogMergeService { } }); - Collections.sort(manualRedactionWrappers); return manualRedactionWrappers; } - private void processRedactionLogEntry(List manualRedactionWrappers, String dossierTemplateId, RedactionLogEntry redactionLogEntry) { - - + private void processRedactionLogEntry(List manualRedactionWrappers, + String dossierTemplateId, RedactionLogEntry redactionLogEntry) { manualRedactionWrappers.forEach(mrw -> { @@ -98,7 +119,8 @@ public class RedactionLogMergeService { } else if (imageRecategorization.getStatus().equals(Status.REQUESTED)) { manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", requested to recategorize"); redactionLogEntry.setStatus(Status.REQUESTED); - redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, false, redactionLogEntry.isRedacted(), false)); + redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, false, redactionLogEntry + .isRedacted(), false)); redactionLogEntry.setRecategorizationType(imageRecategorization.getType()); } else { redactionLogEntry.setStatus(Status.DECLINED); @@ -117,11 +139,13 @@ public class RedactionLogMergeService { redactionLogEntry.setRedacted(false); redactionLogEntry.setStatus(Status.APPROVED); manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", removed by manual override"); - redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, false, redactionLogEntry.isRedacted(), true)); + redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, false, redactionLogEntry + .isRedacted(), true)); } else if (manualRemoval.getStatus().equals(Status.REQUESTED)) { manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", requested to remove"); redactionLogEntry.setStatus(Status.REQUESTED); - redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, true, redactionLogEntry.isRedacted(), false)); + redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, true, redactionLogEntry + .isRedacted(), false)); } else { redactionLogEntry.setStatus(Status.DECLINED); } @@ -134,20 +158,21 @@ public class RedactionLogMergeService { redactionLogEntry.setDossierDictionaryEntry(manualRemoval.isRemoveFromDictionary()); } - if (mrw.getItem() instanceof ManualForceRedact) { var manualForceRedact = (ManualForceRedact) mrw.getItem(); String manualOverrideReason = null; if (manualForceRedact.getStatus().equals(Status.APPROVED)) { redactionLogEntry.setRedacted(true); redactionLogEntry.setStatus(Status.APPROVED); - redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, false, redactionLogEntry.isRedacted(), false)); + redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, false, redactionLogEntry + .isRedacted(), false)); manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", forced by manual override"); redactionLogEntry.setLegalBasis(manualForceRedact.getLegalBasis()); } else if (manualForceRedact.getStatus().equals(Status.REQUESTED)) { manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", requested to force redact"); redactionLogEntry.setStatus(Status.REQUESTED); - redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, true, redactionLogEntry.isRedacted(), false)); + redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, true, redactionLogEntry + .isRedacted(), false)); redactionLogEntry.setLegalBasis(manualForceRedact.getLegalBasis()); } else { redactionLogEntry.setStatus(Status.DECLINED); @@ -170,7 +195,8 @@ public class RedactionLogMergeService { } else if (manualLegalBasisChange.getStatus().equals(Status.REQUESTED)) { manualOverrideReason = mergeReasonIfNecessary(redactionLogEntry.getReason(), ", legal basis change requested"); redactionLogEntry.setStatus(Status.REQUESTED); - redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, true, redactionLogEntry.isRedacted(), false)); + redactionLogEntry.setColor(getColor(redactionLogEntry.getType(), dossierTemplateId, true, redactionLogEntry + .isRedacted(), false)); redactionLogEntry.setLegalBasisChangeValue(manualLegalBasisChange.getLegalBasis()); } else { redactionLogEntry.setStatus(Status.DECLINED); @@ -186,7 +212,9 @@ public class RedactionLogMergeService { } + private String mergeReasonIfNecessary(String currentReason, String addition) { + if (currentReason != null) { if (!currentReason.contains(addition)) { return currentReason + addition; @@ -198,14 +226,16 @@ public class RedactionLogMergeService { } - public List addManualAddEntries(Set manualAdds, Map> comments, String dossierTemplateId) { + public List addManualAddEntries(Set manualAdds, + Map> comments, String dossierTemplateId) { List redactionLogEntries = new ArrayList<>(); for (ManualRedactionEntry manualRedactionEntry : manualAdds) { if (!approvedAndShouldBeInDictionary(manualRedactionEntry)) { - RedactionLogEntry redactionLogEntry = createRedactionLogEntry(manualRedactionEntry, manualRedactionEntry.getId(), dossierTemplateId); + RedactionLogEntry redactionLogEntry = createRedactionLogEntry(manualRedactionEntry, manualRedactionEntry + .getId(), dossierTemplateId); redactionLogEntry.setPositions(manualRedactionEntry.getPositions()); redactionLogEntry.setComments(comments.get(manualRedactionEntry.getId())); redactionLogEntries.add(redactionLogEntry); @@ -218,7 +248,8 @@ public class RedactionLogMergeService { private boolean approvedAndShouldBeInDictionary(ManualRedactionEntry manualRedactionEntry) { - return manualRedactionEntry.getStatus().equals(Status.APPROVED) && (manualRedactionEntry.isAddToDictionary() || manualRedactionEntry.isAddToDossierDictionary()); + return manualRedactionEntry.getStatus() + .equals(Status.APPROVED) && (manualRedactionEntry.isAddToDictionary() || manualRedactionEntry.isAddToDossierDictionary()); } @@ -246,7 +277,9 @@ public class RedactionLogMergeService { } - private float[] getColor(String type, String dossierTemplateId, boolean requested, boolean isRedaction, boolean skipped) { + private float[] getColor(String type, String dossierTemplateId, boolean requested, boolean isRedaction, + boolean skipped) { + if (requested) { return dictionaryService.getRequestRemoveColor(dossierTemplateId); } @@ -273,6 +306,7 @@ public class RedactionLogMergeService { return dictionaryService.getColor(type, dossierTemplateId); } + @Data @AllArgsConstructor private static class ManualRedactionWrapper implements Comparable { @@ -281,12 +315,14 @@ public class RedactionLogMergeService { private OffsetDateTime date; private Object item; + @Override public int compareTo(ManualRedactionWrapper o) { + return this.date.compareTo(o.date); } - } + } } From 078dc2c3c3508a2407a527ff3512239998a30869 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Wed, 18 Aug 2021 10:02:37 +0200 Subject: [PATCH 68/80] RED-1755: Calculate falgs in file-management-service async --- .../redaction/v1/model/AnalyzeResult.java | 4 --- .../service/AnalyzeResponseService.java | 25 ------------------- 2 files changed, 29 deletions(-) diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeResult.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeResult.java index 8d569c81..4fc3fada 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeResult.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/AnalyzeResult.java @@ -15,10 +15,6 @@ public class AnalyzeResult { private String fileId; private long duration; private int numberOfPages; - private boolean hasHints; - private boolean hasRequests; - private boolean hasRedactions; - private boolean hasImages; private boolean hasUpdates; private long dictionaryVersion; private long dossierDictionaryVersion; diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java index bd8042ff..63fbadf6 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java @@ -17,37 +17,12 @@ public class AnalyzeResponseService { public AnalyzeResult createAnalyzeResponse(String dossierId, String fileId, long duration, int pageCount, RedactionLog redactionLog, boolean hasUpdates) { - boolean hasHints = redactionLog.getRedactionLogEntry() - .stream() - .filter(entry -> !entry.isExcluded()) - .anyMatch(entry -> entry.isHint() && !entry.getType().equals("false_positive")); - - boolean hasRequests = redactionLog.getRedactionLogEntry() - .stream() - .filter(entry -> !entry.isExcluded()) - .anyMatch(entry -> entry.isManual() && entry.getStatus() - .equals(com.iqser.red.service.redaction.v1.model.Status.REQUESTED)); - - boolean hasRedactions = redactionLog.getRedactionLogEntry() - .stream() - .filter(entry -> !entry.isExcluded()) - .anyMatch(entry -> entry.isRedacted() && !entry.isManual() || entry.isManual() && entry.getStatus() - .equals(com.iqser.red.service.redaction.v1.model.Status.APPROVED)); - - boolean hasImages = redactionLog.getRedactionLogEntry() - .stream() - .filter(entry -> !entry.isExcluded()) - .anyMatch(entry -> entry.isHint() && entry.getType().equals("image") || entry.isImage()); return AnalyzeResult.builder() .dossierId(dossierId) .fileId(fileId) .duration(duration) .numberOfPages(pageCount) - .hasHints(hasHints) - .hasRedactions(hasRedactions) - .hasRequests(hasRequests) - .hasImages(hasImages) .hasUpdates(hasUpdates) .analysisVersion(redactionServiceSettings.getAnalysisVersion()) .rulesVersion(redactionLog.getRulesVersion()) From bcd469bb657f003813b09fd98bdbb2f624aa5e99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Fri, 20 Aug 2021 14:51:22 +0200 Subject: [PATCH 69/80] Added equalsIgnoreCase fileattributes methods for rules --- .../v1/server/redaction/model/Section.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Section.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Section.java index dc878a20..17209efb 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Section.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Section.java @@ -72,6 +72,19 @@ public class Section { } + public boolean fileAttributeByIdEqualsIgnoreCase(String id, String value){ + return fileAttributes != null && fileAttributes.stream().filter(attribute -> id.equals(attribute.getId()) && value.equalsIgnoreCase(attribute.getValue())).findFirst().isPresent(); + } + + public boolean fileAttributeByPlaceholderEqualsIgnoreCase(String placeholder, String value){ + return fileAttributes != null && fileAttributes.stream().filter(attribute -> placeholder.equals(attribute.getPlaceholder()) && value.equalsIgnoreCase(attribute.getValue())).findFirst().isPresent(); + } + + public boolean fileAttributeByLabelEqualsIgnoreCase(String label, String value){ + return fileAttributes != null && fileAttributes.stream().filter(attribute -> label.equals(attribute.getLabel()) && value.equalsIgnoreCase(attribute.getValue())).findFirst().isPresent(); + } + + public boolean rowEquals(String headerName, String value) { String cleanHeaderName = headerName.replaceAll("\n", "").replaceAll(" ", "").replaceAll("-", ""); From 76bf6773db1f32f0b178537ebbff7bdd2f1056c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Thu, 26 Aug 2021 11:53:02 +0200 Subject: [PATCH 70/80] RED-1920: Integrated entity-recognition-service --- .../client/EntityRecognitionClient.java | 18 ++++++ .../client/model/EntityRecogintionEntity.java | 19 +++++++ .../model/EntityRecognitionRequest.java | 18 ++++++ .../model/EntityRecognitionResponse.java | 21 +++++++ .../client/model/EntityRecognitionResult.java | 20 +++++++ .../model/EntityRecognitionSection.java | 16 ++++++ .../service/EntityRedactionService.java | 57 ++++++++++++++++--- .../settings/RedactionServiceSettings.java | 2 + .../src/test/resources/application.yml | 2 + 9 files changed, 165 insertions(+), 8 deletions(-) create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/EntityRecognitionClient.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/model/EntityRecogintionEntity.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/model/EntityRecognitionRequest.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/model/EntityRecognitionResponse.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/model/EntityRecognitionResult.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/model/EntityRecognitionSection.java diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/EntityRecognitionClient.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/EntityRecognitionClient.java new file mode 100644 index 00000000..589e6dc3 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/EntityRecognitionClient.java @@ -0,0 +1,18 @@ +package com.iqser.red.service.redaction.v1.server.client; + +import java.util.List; +import java.util.Map; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; + +import com.iqser.red.service.redaction.v1.server.client.model.EntityRecogintionEntity; +import com.iqser.red.service.redaction.v1.server.client.model.EntityRecognitionRequest; + +@FeignClient(name = "EntityRecognitionClient", url = "${entity-recognition-service.url}") +public interface EntityRecognitionClient { + + @PostMapping(value = "/find_authors", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + Map>> findAuthors(EntityRecognitionRequest entityRecognitionRequest); +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/model/EntityRecogintionEntity.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/model/EntityRecogintionEntity.java new file mode 100644 index 00000000..b86a1b66 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/model/EntityRecogintionEntity.java @@ -0,0 +1,19 @@ +package com.iqser.red.service.redaction.v1.server.client.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class EntityRecogintionEntity { + + private String value; + private int startOffset; + private int endOffset; + private String type; + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/model/EntityRecognitionRequest.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/model/EntityRecognitionRequest.java new file mode 100644 index 00000000..7e15bc69 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/model/EntityRecognitionRequest.java @@ -0,0 +1,18 @@ +package com.iqser.red.service.redaction.v1.server.client.model; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class EntityRecognitionRequest { + + private List data; + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/model/EntityRecognitionResponse.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/model/EntityRecognitionResponse.java new file mode 100644 index 00000000..af0d6beb --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/model/EntityRecognitionResponse.java @@ -0,0 +1,21 @@ +package com.iqser.red.service.redaction.v1.server.client.model; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class EntityRecognitionResponse { + + @Builder.Default + private Map> result = new HashMap<>(); + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/model/EntityRecognitionResult.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/model/EntityRecognitionResult.java new file mode 100644 index 00000000..cb894b5d --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/model/EntityRecognitionResult.java @@ -0,0 +1,20 @@ +package com.iqser.red.service.redaction.v1.server.client.model; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class EntityRecognitionResult { + + @Builder.Default + private Map> entities = new HashMap<>(); +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/model/EntityRecognitionSection.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/model/EntityRecognitionSection.java new file mode 100644 index 00000000..3172aa52 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/model/EntityRecognitionSection.java @@ -0,0 +1,16 @@ +package com.iqser.red.service.redaction.v1.server.client.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class EntityRecognitionSection { + + private int sectionNumber; + private String text; +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java index c1dc3f11..67e86ca0 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java @@ -2,14 +2,22 @@ package com.iqser.red.service.redaction.v1.server.redaction.service; import com.iqser.red.service.redaction.v1.model.*; import com.iqser.red.service.redaction.v1.server.classification.model.*; +import com.iqser.red.service.redaction.v1.server.client.EntityRecognitionClient; +import com.iqser.red.service.redaction.v1.server.client.model.EntityRecogintionEntity; +import com.iqser.red.service.redaction.v1.server.client.model.EntityRecognitionRequest; +import com.iqser.red.service.redaction.v1.server.client.model.EntityRecognitionResponse; +import com.iqser.red.service.redaction.v1.server.client.model.EntityRecognitionSection; import com.iqser.red.service.redaction.v1.server.redaction.model.Dictionary; import com.iqser.red.service.redaction.v1.server.redaction.model.*; import com.iqser.red.service.redaction.v1.server.redaction.utils.EntitySearchUtils; import com.iqser.red.service.redaction.v1.server.redaction.utils.IdBuilder; +import com.iqser.red.service.redaction.v1.server.settings.RedactionServiceSettings; import com.iqser.red.service.redaction.v1.server.tableextraction.model.Cell; import com.iqser.red.service.redaction.v1.server.tableextraction.model.Table; + import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; + import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.kie.api.runtime.KieContainer; @@ -28,6 +36,8 @@ public class EntityRedactionService { private final DictionaryService dictionaryService; private final DroolsExecutionService droolsExecutionService; private final SurroundingWordsService surroundingWordsService; + private final EntityRecognitionClient entityRecognitionClient; + private final RedactionServiceSettings redactionServiceSettings; public void processDocument(Document classifiedDoc, String dossierTemplateId, ManualRedactions manualRedactions, @@ -58,7 +68,8 @@ public class EntityRedactionService { } - public Map> convertToEnititesPerPage(Set entities){ + public Map> convertToEnititesPerPage(Set entities) { + Map> entitiesPerPage = new HashMap<>(); for (Entity entity : entities) { Map> sequenceOnPage = new HashMap<>(); @@ -68,8 +79,7 @@ public class EntityRedactionService { } for (Map.Entry> entry : sequenceOnPage.entrySet()) { - entitiesPerPage - .computeIfAbsent(entry.getKey(), (x) -> new ArrayList<>()) + entitiesPerPage.computeIfAbsent(entry.getKey(), (x) -> new ArrayList<>()) .add(new Entity(entity.getWord(), entity.getType(), entity.isRedaction(), entity.getRedactionReason(), entry .getValue(), entity.getHeadline(), entity.getMatchedRule(), entity.getSectionNumber(), entity .getLegalBasis(), entity.isDictionaryEntry(), entity.getTextBefore(), entity.getTextAfter(), entity @@ -80,17 +90,18 @@ public class EntityRedactionService { } - public Map> getHintsPerSection(Set entities, Dictionary dictionary){ + public Map> getHintsPerSection(Set entities, Dictionary dictionary) { + Map> hintsPerSectionNumber = new HashMap<>(); entities.stream().forEach(entity -> { if (dictionary.isHint(entity.getType()) && entity.isDictionaryEntry()) { - hintsPerSectionNumber.computeIfAbsent(entity.getSectionNumber(), (x) -> new HashSet<>()) - .add(entity); + hintsPerSectionNumber.computeIfAbsent(entity.getSectionNumber(), (x) -> new HashSet<>()).add(entity); } }); return hintsPerSectionNumber; } + private Set findEntities(Document classifiedDoc, KieContainer kieContainer, ManualRedactions manualRedactions, Dictionary dictionary, boolean local, Map> hintsPerSectionNumber, @@ -148,7 +159,8 @@ public class EntityRedactionService { } - public void addLocalValuesToDictionary(Section analysedSection, Dictionary dictionary){ + public void addLocalValuesToDictionary(Section analysedSection, Dictionary dictionary) { + analysedSection.getLocalDictionaryAdds().keySet().forEach(key -> { if (dictionary.isRecommendation(key)) { analysedSection.getLocalDictionaryAdds().get(key).forEach(value -> { @@ -393,11 +405,16 @@ public class EntityRedactionService { } } + if (redactionServiceSettings.isEnableEntityRecognition() && !local) { + found.addAll(getAiEntities(sectionNumber, searchableString, headline)); + } + return EntitySearchUtils.clearAndFindPositions(found, searchableText, dictionary); } - private Image convertAndRecategorize(PdfImage pdfImage, int sectionNumber, String headline, ManualRedactions manualRedactions) { + private Image convertAndRecategorize(PdfImage pdfImage, int sectionNumber, String headline, + ManualRedactions manualRedactions) { Image image = Image.builder() .type(pdfImage.getImageType().equals(ImageType.OTHER) ? "image" : pdfImage.getImageType() @@ -423,4 +440,28 @@ public class EntityRedactionService { return image; } + + private Set getAiEntities(int sectionNumber, String searchableString, String headline) { + + Set found = new HashSet<>(); + + Map>> response = entityRecognitionClient.findAuthors(EntityRecognitionRequest + .builder() + .data(List.of(EntityRecognitionSection.builder() + .sectionNumber(sectionNumber) + .text(searchableString) + .build())) + .build()); + + EntityRecognitionResponse entityRecognitionResponse = new EntityRecognitionResponse(response.get("result:")); + + if (entityRecognitionResponse.getResult() != null && entityRecognitionResponse.getResult() + .containsKey(String.valueOf(sectionNumber))) { + entityRecognitionResponse.getResult().get(String.valueOf(sectionNumber)).forEach(res -> { + found.add(new Entity(res.getValue(), res.getType(), res.getStartOffset(), res.getEndOffset(), headline, sectionNumber, false, false)); + }); + } + return found; + } + } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/settings/RedactionServiceSettings.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/settings/RedactionServiceSettings.java index dfc34079..901d8042 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/settings/RedactionServiceSettings.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/settings/RedactionServiceSettings.java @@ -17,4 +17,6 @@ public class RedactionServiceSettings { private int analysisVersion = 1; + private boolean enableEntityRecognition = true; + } diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/resources/application.yml b/redaction-service-v1/redaction-service-server-v1/src/test/resources/application.yml index 4b511179..72e05696 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/resources/application.yml +++ b/redaction-service-v1/redaction-service-server-v1/src/test/resources/application.yml @@ -1,6 +1,7 @@ configuration-service.url: "http://configuration-service-v1:8080" image-service.url: "http://image-service-v1:8080" file-management-service.url: "http://file-management-service-v1:8080" +entity-recognition-service.url: "http://entity-recognition-service-v1:8080" ribbon: ConnectTimeout: 600000 @@ -17,3 +18,4 @@ platform.multi-tenancy: redaction-service: enable-image-classification: false + enable-entity-recognition: false From 0ab0ff5f501baa6fb4b3038b2aa4417a6729ce32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Thu, 26 Aug 2021 14:40:26 +0200 Subject: [PATCH 71/80] Fixed missing endpoint variable --- .../src/main/resources/application.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/resources/application.yml b/redaction-service-v1/redaction-service-server-v1/src/main/resources/application.yml index 671d3b20..d8aa5ace 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/resources/application.yml +++ b/redaction-service-v1/redaction-service-server-v1/src/main/resources/application.yml @@ -4,6 +4,7 @@ info: configuration-service.url: "http://configuration-service-v1:8080" file-management-service.url: "http://file-management-service-v1:8080" image-service.url: "http://image-service-v1:8080" +entity-recognition-service.url: "http://entity-recognition-service-v1:8080" server: port: 8080 From 5939c3d460dde4585280c1372306acfaae89dd61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Fri, 3 Sep 2021 10:59:34 +0200 Subject: [PATCH 72/80] RED-1970: Seperated structur analysis from entity analysis --- .../v1/model/StructureAnalyzeRequest.java | 18 + .../server/classification/model/Document.java | 19 +- .../queue/RedactionMessageReceiver.java | 21 +- .../server/redaction/model/PageEntities.java | 23 + .../service/AnalyzeResponseService.java | 35 -- .../redaction/service/AnalyzeService.java | 270 +++++++++ .../redaction/service/DictionaryService.java | 3 +- .../service/EntityRedactionService.java | 436 ++++----------- .../redaction/service/ReanalyzeService.java | 321 ----------- .../service/RedactionChangeLogService.java | 3 + .../service/RedactionLogCreatorService.java | 109 +--- .../service/SectionGridCreatorService.java | 76 +++ .../service/SectionTextBuilderService.java | 210 +++++++ .../v1/server/RedactionIntegrationTest.java | 44 +- .../service/EntityRedactionServiceTest.java | 511 ------------------ .../utils/EntitySearchUtilsTest.java | 29 + 16 files changed, 816 insertions(+), 1312 deletions(-) create mode 100644 redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/StructureAnalyzeRequest.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/PageEntities.java delete mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeService.java delete mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/SectionGridCreatorService.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/SectionTextBuilderService.java delete mode 100644 redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionServiceTest.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/utils/EntitySearchUtilsTest.java diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/StructureAnalyzeRequest.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/StructureAnalyzeRequest.java new file mode 100644 index 00000000..bd464e36 --- /dev/null +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/StructureAnalyzeRequest.java @@ -0,0 +1,18 @@ +package com.iqser.red.service.redaction.v1.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class StructureAnalyzeRequest { + + private String dossierId; + private String fileId; + +} + diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/classification/model/Document.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/classification/model/Document.java index d312bd8c..393e2fcd 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/classification/model/Document.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/classification/model/Document.java @@ -1,19 +1,14 @@ package com.iqser.red.service.redaction.v1.server.classification.model; -import com.iqser.red.service.redaction.v1.model.RedactionLogEntry; +import java.util.ArrayList; +import java.util.List; + import com.iqser.red.service.redaction.v1.model.SectionGrid; import com.iqser.red.service.redaction.v1.server.redaction.model.DictionaryVersion; -import com.iqser.red.service.redaction.v1.server.redaction.model.Entity; -import com.iqser.red.service.redaction.v1.server.redaction.model.Image; + import lombok.Data; import lombok.NoArgsConstructor; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - @Data @NoArgsConstructor public class Document { @@ -23,20 +18,14 @@ public class Document { private List
headers = new ArrayList<>(); private List
footers = new ArrayList<>(); private List unclassifiedTexts = new ArrayList<>(); - private Map> entities = new HashMap<>(); private FloatFrequencyCounter textHeightCounter = new FloatFrequencyCounter(); private FloatFrequencyCounter fontSizeCounter = new FloatFrequencyCounter(); private StringFrequencyCounter fontCounter = new StringFrequencyCounter(); private StringFrequencyCounter fontStyleCounter = new StringFrequencyCounter(); private boolean headlines; - private List redactionLogEntities = new ArrayList<>(); private SectionGrid sectionGrid = new SectionGrid(); private DictionaryVersion dictionaryVersion; private long rulesVersion; - private List sectionText = new ArrayList<>(); - - private Map> images = new HashMap<>(); - } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/queue/RedactionMessageReceiver.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/queue/RedactionMessageReceiver.java index 667e21fc..b4aed110 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/queue/RedactionMessageReceiver.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/queue/RedactionMessageReceiver.java @@ -4,10 +4,13 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.iqser.red.service.redaction.v1.model.AnalyzeRequest; import com.iqser.red.service.redaction.v1.model.AnalyzeResult; +import com.iqser.red.service.redaction.v1.model.StructureAnalyzeRequest; import com.iqser.red.service.redaction.v1.server.client.FileStatusProcessingUpdateClient; -import com.iqser.red.service.redaction.v1.server.redaction.service.ReanalyzeService; +import com.iqser.red.service.redaction.v1.server.redaction.service.AnalyzeService; + import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; + import org.springframework.amqp.rabbit.annotation.RabbitHandler; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Service; @@ -21,9 +24,10 @@ import static com.iqser.red.service.redaction.v1.server.queue.MessagingConfigura public class RedactionMessageReceiver { private final ObjectMapper objectMapper; - private final ReanalyzeService reanalyzeService; + private final AnalyzeService analyzeService; private final FileStatusProcessingUpdateClient fileStatusProcessingUpdateClient; + @RabbitHandler @RabbitListener(queues = REDACTION_QUEUE) public void receiveAnalyzeRequest(String in) throws JsonProcessingException { @@ -32,15 +36,22 @@ public class RedactionMessageReceiver { log.info("Processing analyze request: {}", analyzeRequest); AnalyzeResult result; if (analyzeRequest.isReanalyseOnlyIfPossible()) { - result = reanalyzeService.reanalyze(analyzeRequest); + result = analyzeService.reanalyze(analyzeRequest); + log.info("Successfully reanalyzed dossier {} file {} took: {}", analyzeRequest.getDossierId(), analyzeRequest + .getFileId(), result.getDuration()); } else { - result = reanalyzeService.analyze(analyzeRequest); + // TODO Seperate stucture analysis by other queue + analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(analyzeRequest.getDossierId(), analyzeRequest.getFileId())); + + result = analyzeService.analyze(analyzeRequest); + log.info("Successfully analyzed dossier {} file {} took: {}", analyzeRequest.getDossierId(), analyzeRequest.getFileId(), result + .getDuration()); } - log.info("Successfully analyzed {}", analyzeRequest); fileStatusProcessingUpdateClient.analysisSuccessful(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), result); } + @RabbitHandler @RabbitListener(queues = REDACTION_DQL) public void receiveAnalyzeRequestDQL(String in) throws JsonProcessingException { diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/PageEntities.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/PageEntities.java new file mode 100644 index 00000000..d5e51db2 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/PageEntities.java @@ -0,0 +1,23 @@ +package com.iqser.red.service.redaction.v1.server.redaction.model; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +@Data +@AllArgsConstructor +public class PageEntities { + + @Builder.Default + private Map> entitiesPerPage = new HashMap<>(); + + @Builder.Default + private Map> imagesPerPage = new HashMap<>(); + + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java deleted file mode 100644 index 63fbadf6..00000000 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeResponseService.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.iqser.red.service.redaction.v1.server.redaction.service; - -import com.iqser.red.service.redaction.v1.model.AnalyzeResult; -import com.iqser.red.service.redaction.v1.model.RedactionLog; - -import com.iqser.red.service.redaction.v1.server.settings.RedactionServiceSettings; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class AnalyzeResponseService { - - private final RedactionServiceSettings redactionServiceSettings; - - public AnalyzeResult createAnalyzeResponse(String dossierId, String fileId, long duration, int pageCount, - RedactionLog redactionLog, boolean hasUpdates) { - - - return AnalyzeResult.builder() - .dossierId(dossierId) - .fileId(fileId) - .duration(duration) - .numberOfPages(pageCount) - .hasUpdates(hasUpdates) - .analysisVersion(redactionServiceSettings.getAnalysisVersion()) - .rulesVersion(redactionLog.getRulesVersion()) - .dictionaryVersion(redactionLog.getDictionaryVersion()) - .legalBasisVersion(redactionLog.getLegalBasisVersion()) - .dossierDictionaryVersion(redactionLog.getDossierDictionaryVersion()) - .build(); - } - -} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeService.java new file mode 100644 index 00000000..1e257ac6 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeService.java @@ -0,0 +1,270 @@ +package com.iqser.red.service.redaction.v1.server.redaction.service; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.kie.api.runtime.KieContainer; +import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.RequestBody; + +import com.iqser.red.service.file.management.v1.api.model.FileType; +import com.iqser.red.service.redaction.v1.model.AnalyzeRequest; +import com.iqser.red.service.redaction.v1.model.AnalyzeResult; +import com.iqser.red.service.redaction.v1.model.IdRemoval; +import com.iqser.red.service.redaction.v1.model.ManualForceRedact; +import com.iqser.red.service.redaction.v1.model.ManualImageRecategorization; +import com.iqser.red.service.redaction.v1.model.ManualLegalBasisChange; +import com.iqser.red.service.redaction.v1.model.ManualRedactions; +import com.iqser.red.service.redaction.v1.model.Rectangle; +import com.iqser.red.service.redaction.v1.model.RedactionLog; +import com.iqser.red.service.redaction.v1.model.RedactionLogEntry; +import com.iqser.red.service.redaction.v1.model.StructureAnalyzeRequest; +import com.iqser.red.service.redaction.v1.server.classification.model.Document; +import com.iqser.red.service.redaction.v1.server.classification.model.SectionText; +import com.iqser.red.service.redaction.v1.server.classification.model.Text; +import com.iqser.red.service.redaction.v1.server.client.LegalBasisClient; +import com.iqser.red.service.redaction.v1.server.exception.RedactionException; +import com.iqser.red.service.redaction.v1.server.redaction.model.Dictionary; +import com.iqser.red.service.redaction.v1.server.redaction.model.DictionaryIncrement; +import com.iqser.red.service.redaction.v1.server.redaction.model.DictionaryVersion; +import com.iqser.red.service.redaction.v1.server.redaction.model.Image; +import com.iqser.red.service.redaction.v1.server.redaction.model.PageEntities; +import com.iqser.red.service.redaction.v1.server.redaction.model.RedRectangle2D; +import com.iqser.red.service.redaction.v1.server.redaction.utils.EntitySearchUtils; +import com.iqser.red.service.redaction.v1.server.segmentation.PdfSegmentationService; +import com.iqser.red.service.redaction.v1.server.settings.RedactionServiceSettings; +import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AnalyzeService { + + private final DictionaryService dictionaryService; + private final DroolsExecutionService droolsExecutionService; + private final EntityRedactionService entityRedactionService; + private final RedactionLogCreatorService redactionLogCreatorService; + private final RedactionStorageService redactionStorageService; + private final PdfSegmentationService pdfSegmentationService; + private final RedactionChangeLogService redactionChangeLogService; + private final LegalBasisClient legalBasisClient; + private final RedactionServiceSettings redactionServiceSettings; + private final SectionTextBuilderService sectionTextBuilderService; + private final SectionGridCreatorService sectionGridCreatorService; + + + public void analyzeDocumentStructure(StructureAnalyzeRequest analyzeRequest) { + + long startTime = System.currentTimeMillis(); + + var pageCount = 0; + Document classifiedDoc; + + try { + var storedObjectStream = redactionStorageService.getStoredObject(RedactionStorageService.StorageIdUtils.getStorageId(analyzeRequest + .getDossierId(), analyzeRequest.getFileId(), FileType.ORIGIN)); + classifiedDoc = pdfSegmentationService.parseDocument(storedObjectStream); + pageCount = classifiedDoc.getPages().size(); + } catch (Exception e) { + throw new RedactionException(e); + } + + List sectionTexts = sectionTextBuilderService.buildSectionText(classifiedDoc); + sectionGridCreatorService.createSectionGrid(classifiedDoc, pageCount); + + redactionStorageService.storeObject(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), FileType.TEXT, new Text(pageCount, sectionTexts)); + redactionStorageService.storeObject(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), FileType.SECTION_GRID, classifiedDoc + .getSectionGrid()); + + log.info("Document structure analysis successful, took: {}", System.currentTimeMillis() - startTime); + } + + + public AnalyzeResult analyze(AnalyzeRequest analyzeRequest) { + + long startTime = System.currentTimeMillis(); + + var text = redactionStorageService.getText(analyzeRequest.getDossierId(), analyzeRequest.getFileId()); + + dictionaryService.updateDictionary(analyzeRequest.getDossierTemplateId(), analyzeRequest.getDossierId()); + KieContainer kieContainer = droolsExecutionService.updateRules(analyzeRequest.getDossierTemplateId()); + long rulesVersion = droolsExecutionService.getRulesVersion(analyzeRequest.getDossierTemplateId()); + Dictionary dictionary = dictionaryService.getDeepCopyDictionary(analyzeRequest.getDossierTemplateId(), analyzeRequest + .getDossierId()); + + PageEntities pageEntities = entityRedactionService.findEntities(dictionary, text.getSectionTexts(), kieContainer, analyzeRequest); + + dictionaryService.updateExternalDictionary(dictionary, analyzeRequest.getDossierTemplateId()); + + List redactionLogEntries = redactionLogCreatorService.createRedactionLog(pageEntities, text.getNumberOfPages(), analyzeRequest + .getDossierTemplateId()); + + var legalBasis = legalBasisClient.getLegalBasisMapping(analyzeRequest.getDossierTemplateId()); + var redactionLog = new RedactionLog(redactionServiceSettings.getAnalysisVersion(), redactionLogEntries, legalBasis, dictionary + .getVersion() + .getDossierTemplateVersion(), dictionary.getVersion() + .getDossierVersion(), rulesVersion, legalBasisClient.getVersion(analyzeRequest.getDossierTemplateId())); + + return finalizeAnalysis(analyzeRequest, startTime, redactionLog, text, dictionary.getVersion(), false); + } + + + @SneakyThrows + public AnalyzeResult reanalyze(@RequestBody AnalyzeRequest analyzeRequest) { + + long startTime = System.currentTimeMillis(); + + var redactionLog = redactionStorageService.getRedactionLog(analyzeRequest.getDossierId(), analyzeRequest.getFileId()); + var text = redactionStorageService.getText(analyzeRequest.getDossierId(), analyzeRequest.getFileId()); + + // not yet ready for reanalysis + if (redactionLog == null || text == null || text.getNumberOfPages() == 0) { + return analyze(analyzeRequest); + } + + DictionaryIncrement dictionaryIncrement = dictionaryService.getDictionaryIncrements(analyzeRequest.getDossierTemplateId(), new DictionaryVersion(redactionLog + .getDictionaryVersion(), redactionLog.getDossierDictionaryVersion()), analyzeRequest.getDossierId()); + + Set sectionsToReanalyse = !analyzeRequest.getSectionsToReanalyse() + .isEmpty() ? analyzeRequest.getSectionsToReanalyse() : findSectionsToReanalyse(dictionaryIncrement, redactionLog, text, analyzeRequest); + + if (sectionsToReanalyse.isEmpty()) { + return finalizeAnalysis(analyzeRequest, startTime, redactionLog, text, dictionaryIncrement.getDictionaryVersion(), true); + } + + List reanalysisSections = text.getSectionTexts() + .stream() + .filter(sectionText -> sectionsToReanalyse.contains(sectionText.getSectionNumber())) + .collect(Collectors.toList()); + + KieContainer kieContainer = droolsExecutionService.updateRules(analyzeRequest.getDossierTemplateId()); + + Dictionary dictionary = dictionaryService.getDeepCopyDictionary(analyzeRequest.getDossierTemplateId(), analyzeRequest + .getDossierId()); + + PageEntities pageEntities = entityRedactionService.findEntities(dictionary, reanalysisSections, kieContainer, analyzeRequest); + var newRedactionLogEntries = redactionLogCreatorService.createRedactionLog(pageEntities, text.getNumberOfPages(), analyzeRequest + .getDossierTemplateId()); + + redactionLog.getRedactionLogEntry().removeIf(entry -> sectionsToReanalyse.contains(entry.getSectionNumber())); + redactionLog.getRedactionLogEntry().addAll(newRedactionLogEntries); + return finalizeAnalysis(analyzeRequest, startTime, redactionLog, text, dictionaryIncrement.getDictionaryVersion(), true); + } + + + private Set findSectionsToReanalyse(DictionaryIncrement dictionaryIncrement, RedactionLog redactionLog, + Text text, AnalyzeRequest analyzeRequest) { + + long start = System.currentTimeMillis(); + Set relevantManuallyModifiedAnnotationIds = getRelevantManuallyModifiedAnnotationIds(analyzeRequest.getManualRedactions()); + + Set sectionsToReanalyse = new HashSet<>(); + Map> imageEntries = new HashMap<>(); + for (RedactionLogEntry entry : redactionLog.getRedactionLogEntry()) { + if (entry.isManual() || relevantManuallyModifiedAnnotationIds.contains(entry.getId())) { + sectionsToReanalyse.add(entry.getSectionNumber()); + } + if (entry.isImage() || entry.getType().equals("image")) { + imageEntries.computeIfAbsent(entry.getSectionNumber(), x -> new HashSet<>()).add(convert(entry)); + } + } + + for (SectionText sectionText : text.getSectionTexts()) { + + if (EntitySearchUtils.sectionContainsAny(sectionText.getText(), dictionaryIncrement.getValues())) { + sectionsToReanalyse.add(sectionText.getSectionNumber()); + } + + } + + log.info("Should reanalyze {} sections for request: {}, took: {}", sectionsToReanalyse.size(), analyzeRequest, System.currentTimeMillis() - start); + + return sectionsToReanalyse; + } + + + private AnalyzeResult finalizeAnalysis(@RequestBody AnalyzeRequest analyzeRequest, long startTime, + RedactionLog redactionLog, Text text, DictionaryVersion dictionaryVersion, + boolean isReanalysis) { + + redactionLog.setDictionaryVersion(dictionaryVersion.getDossierTemplateVersion()); + redactionLog.setDossierDictionaryVersion(dictionaryVersion.getDossierVersion()); + + excludeExcludedPages(redactionLog, analyzeRequest.getExcludedPages()); + + var redactionLogChange = redactionChangeLogService.computeChanges(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), redactionLog); + redactionStorageService.storeObject(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), FileType.REDACTION_LOG, redactionLogChange + .getRedactionLog()); + + long duration = System.currentTimeMillis() - startTime; + + return AnalyzeResult.builder() + .dossierId(analyzeRequest.getDossierId()) + .fileId(analyzeRequest.getFileId()) + .duration(duration) + .numberOfPages(text.getNumberOfPages()) + .hasUpdates(redactionLogChange.isHasChanges()) + .analysisVersion(redactionServiceSettings.getAnalysisVersion()) + .rulesVersion(redactionLog.getRulesVersion()) + .dictionaryVersion(redactionLog.getDictionaryVersion()) + .legalBasisVersion(redactionLog.getLegalBasisVersion()) + .dossierDictionaryVersion(redactionLog.getDossierDictionaryVersion()) + .wasReanalyzed(isReanalysis) + .build(); + } + + + private Set getRelevantManuallyModifiedAnnotationIds(ManualRedactions manualRedactions) { + + if (manualRedactions == null) { + return new HashSet<>(); + } + + return Stream.concat(manualRedactions.getManualLegalBasisChanges() + .stream() + .map(ManualLegalBasisChange::getId), Stream.concat(manualRedactions.getImageRecategorizations() + .stream() + .map(ManualImageRecategorization::getId), Stream.concat(manualRedactions.getIdsToRemove() + .stream() + .map(IdRemoval::getId), manualRedactions.getForceRedacts().stream().map(ManualForceRedact::getId)))) + .collect(Collectors.toSet()); + } + + + public Image convert(RedactionLogEntry entry) { + + Rectangle position = entry.getPositions().get(0); + + return Image.builder() + .type(entry.getType()) + .position(new RedRectangle2D(position.getTopLeft().getX(), position.getTopLeft() + .getY(), position.getWidth(), position.getHeight())) + .sectionNumber(entry.getSectionNumber()) + .section(entry.getSection()) + .page(position.getPage()) + .hasTransparency(entry.isImageHasTransparency()) + .build(); + } + + + private void excludeExcludedPages(RedactionLog redactionLog, Set excludedPages) { + + if (excludedPages != null && !excludedPages.isEmpty()) { + redactionLog.getRedactionLogEntry().forEach(entry -> entry.getPositions().forEach(pos -> { + if (excludedPages.contains(pos.getPage())) { + entry.setExcluded(true); + } + })); + } + } + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java index 72d0d387..9631d5b8 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java @@ -33,7 +33,7 @@ public class DictionaryService { public DictionaryVersion updateDictionary(String dossierTemplateId, String dossierId) { - log.info("Updating dictionary data for: {} / {}", dossierTemplateId, dossierId); + log.info("Updating dictionary data for dossierTemplate {} and dossier {}", dossierTemplateId, dossierId); long dossierTemplateDictionaryVersion = dictionaryClient.getVersion(dossierTemplateId, GLOBAL_DOSSIER); var dossierTemplateDictionary = dictionariesByDossierTemplate.get(dossierTemplateId); if (dossierTemplateDictionary == null || dossierTemplateDictionaryVersion > dossierTemplateDictionary.getDictionaryVersion()) { @@ -164,7 +164,6 @@ public class DictionaryService { public float[] getColor(String type, String dossierTemplateId) { - log.info("requested : {} / {}",type,dossierTemplateId); DictionaryModel model = dictionariesByDossierTemplate.get(dossierTemplateId).getLocalAccessMap().get(type); if (model != null) { return model.getColor(); diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java index 67e86ca0..0c302dcc 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java @@ -1,74 +1,142 @@ package com.iqser.red.service.redaction.v1.server.redaction.service; -import com.iqser.red.service.redaction.v1.model.*; -import com.iqser.red.service.redaction.v1.server.classification.model.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.lang3.StringUtils; +import org.kie.api.runtime.KieContainer; +import org.springframework.stereotype.Service; + +import com.iqser.red.service.redaction.v1.model.AnalyzeRequest; +import com.iqser.red.service.redaction.v1.model.ManualImageRecategorization; +import com.iqser.red.service.redaction.v1.model.Status; +import com.iqser.red.service.redaction.v1.server.classification.model.SectionText; import com.iqser.red.service.redaction.v1.server.client.EntityRecognitionClient; import com.iqser.red.service.redaction.v1.server.client.model.EntityRecogintionEntity; import com.iqser.red.service.redaction.v1.server.client.model.EntityRecognitionRequest; import com.iqser.red.service.redaction.v1.server.client.model.EntityRecognitionResponse; import com.iqser.red.service.redaction.v1.server.client.model.EntityRecognitionSection; import com.iqser.red.service.redaction.v1.server.redaction.model.Dictionary; -import com.iqser.red.service.redaction.v1.server.redaction.model.*; +import com.iqser.red.service.redaction.v1.server.redaction.model.DictionaryModel; +import com.iqser.red.service.redaction.v1.server.redaction.model.Entity; +import com.iqser.red.service.redaction.v1.server.redaction.model.EntityPositionSequence; +import com.iqser.red.service.redaction.v1.server.redaction.model.Image; +import com.iqser.red.service.redaction.v1.server.redaction.model.PageEntities; +import com.iqser.red.service.redaction.v1.server.redaction.model.SearchableText; +import com.iqser.red.service.redaction.v1.server.redaction.model.Section; +import com.iqser.red.service.redaction.v1.server.redaction.model.SectionSearchableTextPair; import com.iqser.red.service.redaction.v1.server.redaction.utils.EntitySearchUtils; import com.iqser.red.service.redaction.v1.server.redaction.utils.IdBuilder; import com.iqser.red.service.redaction.v1.server.settings.RedactionServiceSettings; -import com.iqser.red.service.redaction.v1.server.tableextraction.model.Cell; -import com.iqser.red.service.redaction.v1.server.tableextraction.model.Table; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; -import org.kie.api.runtime.KieContainer; -import org.springframework.stereotype.Service; - -import java.util.*; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; -import java.util.stream.Stream; - @Slf4j @Service @RequiredArgsConstructor public class EntityRedactionService { - private final DictionaryService dictionaryService; - private final DroolsExecutionService droolsExecutionService; - private final SurroundingWordsService surroundingWordsService; private final EntityRecognitionClient entityRecognitionClient; private final RedactionServiceSettings redactionServiceSettings; + private final DroolsExecutionService droolsExecutionService; + private final SurroundingWordsService surroundingWordsService; - public void processDocument(Document classifiedDoc, String dossierTemplateId, ManualRedactions manualRedactions, - String dossierId, List fileAttributes) { + public PageEntities findEntities(Dictionary dictionary, List sectionTexts, KieContainer kieContainer, + AnalyzeRequest analyzeRequest) { - dictionaryService.updateDictionary(dossierTemplateId, dossierId); - KieContainer container = droolsExecutionService.updateRules(dossierTemplateId); - long rulesVersion = droolsExecutionService.getRulesVersion(dossierTemplateId); - - Dictionary dictionary = dictionaryService.getDeepCopyDictionary(dossierTemplateId, dossierId); - - Set documentEntities = new HashSet<>(findEntities(classifiedDoc, container, manualRedactions, dictionary, false, null, fileAttributes)); + Map> imagesPerPage = new HashMap<>(); + Set entities = findEntities(sectionTexts, dictionary, kieContainer, analyzeRequest, false, null, imagesPerPage); if (dictionary.hasLocalEntries()) { - - Map> hintsPerSectionNumber = getHintsPerSection(documentEntities, dictionary); - Set foundByLocal = findEntities(classifiedDoc, container, manualRedactions, dictionary, true, hintsPerSectionNumber, fileAttributes); - EntitySearchUtils.addEntitiesWithHigherRank(documentEntities, foundByLocal, dictionary); - EntitySearchUtils.removeEntitiesContainedInLarger(documentEntities); + Map> hintsPerSectionNumber = getHintsPerSection(entities, dictionary); + Set foundByLocal = findEntities(sectionTexts, dictionary, kieContainer, analyzeRequest, true, hintsPerSectionNumber, imagesPerPage); + EntitySearchUtils.addEntitiesWithHigherRank(entities, foundByLocal, dictionary); + EntitySearchUtils.removeEntitiesContainedInLarger(entities); } - classifiedDoc.setEntities(convertToEnititesPerPage(documentEntities)); - - dictionaryService.updateExternalDictionary(dictionary, dossierTemplateId); - - classifiedDoc.setDictionaryVersion(dictionary.getVersion()); - classifiedDoc.setRulesVersion(rulesVersion); + Map> entitiesPerPage = convertToEnititesPerPage(entities); + return new PageEntities(entitiesPerPage, imagesPerPage); } - public Map> convertToEnititesPerPage(Set entities) { + public Set findEntities(List reanalysisSections, Dictionary dictionary, + KieContainer kieContainer, AnalyzeRequest analyzeRequest, boolean local, + Map> hintsPerSectionNumber, + Map> imagesPerPage) { + + List sectionSearchableTextPairs = new ArrayList<>(); + for (SectionText reanalysisSection : reanalysisSections) { + + Set entities = findEntities(reanalysisSection.getSearchableText(), reanalysisSection.getHeadline(), reanalysisSection + .getSectionNumber(), dictionary, local); + if (reanalysisSection.getCellStarts() != null && !reanalysisSection.getCellStarts().isEmpty()) { + surroundingWordsService.addSurroundingText(entities, reanalysisSection.getSearchableText(), dictionary, reanalysisSection + .getCellStarts()); + } else { + surroundingWordsService.addSurroundingText(entities, reanalysisSection.getSearchableText(), dictionary); + } + + if (!local && reanalysisSection.getImages() != null && !reanalysisSection.getImages() + .isEmpty() && analyzeRequest.getManualRedactions() != null && analyzeRequest.getManualRedactions() + .getImageRecategorizations() != null) { + for (Image image : reanalysisSection.getImages()) { + String imageId = IdBuilder.buildId(image.getPosition(), image.getPage()); + for (ManualImageRecategorization imageRecategorization : analyzeRequest.getManualRedactions() + .getImageRecategorizations()) { + if (imageRecategorization.getStatus().equals(Status.APPROVED) && imageRecategorization.getId() + .equals(imageId)) { + image.setType(imageRecategorization.getType()); + } + } + } + } + + sectionSearchableTextPairs.add(new SectionSearchableTextPair(Section.builder() + .isLocal(false) + .dictionaryTypes(dictionary.getTypes()) + .entities(hintsPerSectionNumber != null && hintsPerSectionNumber.containsKey(reanalysisSection.getSectionNumber()) ? Stream + .concat(entities.stream(), hintsPerSectionNumber.get(reanalysisSection.getSectionNumber()) + .stream()) + .collect(Collectors.toSet()) : entities) + .text(reanalysisSection.getSearchableText().getAsStringWithLinebreaks()) + .searchText(reanalysisSection.getSearchableText().toString()) + .headline(reanalysisSection.getHeadline()) + .sectionNumber(reanalysisSection.getSectionNumber()) + .tabularData(reanalysisSection.getTabularData()) + .searchableText(reanalysisSection.getSearchableText()) + .dictionary(dictionary) + .images(reanalysisSection.getImages()) + .fileAttributes(analyzeRequest.getFileAttributes()) + .build(), reanalysisSection.getSearchableText())); + } + + Set entities = new HashSet<>(); + sectionSearchableTextPairs.forEach(sectionSearchableTextPair -> { + Section analysedSection = droolsExecutionService.executeRules(kieContainer, sectionSearchableTextPair.getSection()); + EntitySearchUtils.removeEntitiesContainedInLarger(analysedSection.getEntities()); + entities.addAll(analysedSection.getEntities()); + + if(!local) { + for (Image image : analysedSection.getImages()) { + imagesPerPage.computeIfAbsent(image.getPage(), (a) -> new HashSet<>()).add(image); + } + addLocalValuesToDictionary(analysedSection, dictionary); + } + }); + + return entities; + } + + + private Map> convertToEnititesPerPage(Set entities) { Map> entitiesPerPage = new HashMap<>(); for (Entity entity : entities) { @@ -90,7 +158,7 @@ public class EntityRedactionService { } - public Map> getHintsPerSection(Set entities, Dictionary dictionary) { + private Map> getHintsPerSection(Set entities, Dictionary dictionary) { Map> hintsPerSectionNumber = new HashMap<>(); entities.stream().forEach(entity -> { @@ -102,64 +170,7 @@ public class EntityRedactionService { } - private Set findEntities(Document classifiedDoc, KieContainer kieContainer, - ManualRedactions manualRedactions, Dictionary dictionary, boolean local, - Map> hintsPerSectionNumber, - List fileAttributes) { - - Set documentEntities = new HashSet<>(); - - AtomicInteger sectionNumber = new AtomicInteger(1); - List sectionSearchableTextPairs = new ArrayList<>(); - for (Paragraph paragraph : classifiedDoc.getParagraphs()) { - - List
tables = paragraph.getTables(); - for (Table table : tables) { - if (table.getColCount() == 2) { - sectionSearchableTextPairs.addAll(processTableAsOneText(classifiedDoc, table, sectionNumber, dictionary, local, hintsPerSectionNumber, fileAttributes)); - } else { - sectionSearchableTextPairs.addAll(processTablePerRow(classifiedDoc, table, sectionNumber, dictionary, local, hintsPerSectionNumber, fileAttributes)); - } - sectionNumber.incrementAndGet(); - } - sectionSearchableTextPairs.add(processText(classifiedDoc, paragraph.getSearchableText(), paragraph.getTextBlocks(), paragraph - .getHeadline(), manualRedactions, sectionNumber, dictionary, local, hintsPerSectionNumber, paragraph - .getImages(), fileAttributes)); - sectionNumber.incrementAndGet(); - } - - for (Header header : classifiedDoc.getHeaders()) { - sectionSearchableTextPairs.add(processText(classifiedDoc, header.getSearchableText(), header.getTextBlocks(), "Header", manualRedactions, sectionNumber, dictionary, local, hintsPerSectionNumber, new ArrayList<>(), fileAttributes)); - sectionNumber.incrementAndGet(); - } - - for (Footer footer : classifiedDoc.getFooters()) { - sectionSearchableTextPairs.add(processText(classifiedDoc, footer.getSearchableText(), footer.getTextBlocks(), "Footer", manualRedactions, sectionNumber, dictionary, local, hintsPerSectionNumber, new ArrayList<>(), fileAttributes)); - sectionNumber.incrementAndGet(); - } - - for (UnclassifiedText unclassifiedText : classifiedDoc.getUnclassifiedTexts()) { - sectionSearchableTextPairs.add(processText(classifiedDoc, unclassifiedText.getSearchableText(), unclassifiedText - .getTextBlocks(), "", manualRedactions, sectionNumber, dictionary, local, hintsPerSectionNumber, new ArrayList<>(), fileAttributes)); - sectionNumber.incrementAndGet(); - } - - sectionSearchableTextPairs.forEach(sectionSearchableTextPair -> { - Section analysedSection = droolsExecutionService.executeRules(kieContainer, sectionSearchableTextPair.getSection()); - documentEntities.addAll(analysedSection.getEntities()); - - for (Image image : analysedSection.getImages()) { - classifiedDoc.getImages().computeIfAbsent(image.getPage(), (a) -> new HashSet<>()).add(image); - } - - addLocalValuesToDictionary(analysedSection, dictionary); - }); - - return documentEntities; - } - - - public void addLocalValuesToDictionary(Section analysedSection, Dictionary dictionary) { + private void addLocalValuesToDictionary(Section analysedSection, Dictionary dictionary) { analysedSection.getLocalDictionaryAdds().keySet().forEach(key -> { if (dictionary.isRecommendation(key)) { @@ -186,206 +197,7 @@ public class EntityRedactionService { } - private List processTablePerRow(Document classifiedDoc, Table table, - AtomicInteger sectionNumber, Dictionary dictionary, - boolean local, - Map> hintsPerSectionNumber, - List fileAttributes) { - - List sectionSearchableTextPairs = new ArrayList<>(); - - for (List row : table.getRows()) { - SearchableText searchableRow = new SearchableText(); - Map tabularData = new HashMap<>(); - int start = 0; - List cellStarts = new ArrayList<>(); - SectionText sectionText = new SectionText(); - for (Cell cell : row) { - - if (CollectionUtils.isEmpty(cell.getTextBlocks())) { - continue; - } - - SectionArea sectionArea = new SectionArea(new Point((float) cell.getX(), (float) cell.getY()), (float) cell - .getWidth(), (float) cell.getHeight(), cell.getTextBlocks() - .get(0) - .getSequences() - .get(0) - .getPage()); - sectionText.getSectionAreas().add(sectionArea); - sectionText.getTextBlocks().addAll(cell.getTextBlocks()); - - int cellStart = start; - - if (!cell.isHeaderCell()) { - cell.getHeaderCells().forEach(headerCell -> { - StringBuilder headerBuilder = new StringBuilder(); - headerCell.getTextBlocks().forEach(textBlock -> headerBuilder.append(textBlock.getText())); - String headerName = headerBuilder.toString() - .replaceAll("\n", "") - .replaceAll(" ", "") - .replaceAll("-", ""); - sectionArea.setHeader(headerName); - tabularData.put(headerName, new CellValue(cell.getTextBlocks(), cellStart)); - }); - } - - for (TextBlock textBlock : cell.getTextBlocks()) { - // TODO avoid cell overlap merging. - searchableRow.addAll(textBlock.getSequences()); - } - cellStarts.add(cellStart); - start = start + cell.toString().trim().length() + 1; - - } - Set rowEntities = findEntities(searchableRow, table.getHeadline(), sectionNumber.intValue(), dictionary, local); - surroundingWordsService.addSurroundingText(rowEntities, searchableRow, dictionary, cellStarts); - - sectionSearchableTextPairs.add(new SectionSearchableTextPair(Section.builder() - .isLocal(local) - .dictionaryTypes(dictionary.getTypes()) - .entities(hintsPerSectionNumber != null && hintsPerSectionNumber.containsKey(sectionNumber.intValue()) ? Stream - .concat(rowEntities.stream(), hintsPerSectionNumber.get(sectionNumber.intValue()).stream()) - .collect(Collectors.toSet()) : rowEntities) - .text(searchableRow.getAsStringWithLinebreaks()) - .searchText(searchableRow.toString()) - .headline(table.getHeadline()) - .sectionNumber(sectionNumber.intValue()) - .tabularData(tabularData) - .searchableText(searchableRow) - .dictionary(dictionary) - .fileAttributes(fileAttributes) - .build(), searchableRow)); - - if (!local) { - sectionText.setText(searchableRow.toString()); - sectionText.setHeadline(table.getHeadline()); - sectionText.setSectionNumber(sectionNumber.intValue()); - sectionText.setTable(true); - sectionText.setTabularData(tabularData); - sectionText.setCellStarts(cellStarts); - classifiedDoc.getSectionText().add(sectionText); - } - - sectionNumber.incrementAndGet(); - } - - return sectionSearchableTextPairs; - } - - - private List processTableAsOneText(Document classifiedDoc, Table table, - AtomicInteger sectionNumber, Dictionary dictionary, - boolean local, - Map> hintsPerSectionNumber, - List fileAttributes) { - - List sectionSearchableTextPairs = new ArrayList<>(); - SearchableText entireTableText = new SearchableText(); - SectionText sectionText = new SectionText(); - for (List row : table.getRows()) { - for (Cell cell : row) { - if (CollectionUtils.isEmpty(cell.getTextBlocks())) { - continue; - } - - if (!local) { - SectionArea sectionArea = new SectionArea(new Point((float) cell.getX(), (float) cell.getY()), (float) cell - .getWidth(), (float) cell.getHeight(), cell.getTextBlocks() - .get(0) - .getSequences() - .get(0) - .getPage()); - sectionText.getTextBlocks().addAll(cell.getTextBlocks()); - sectionText.getSectionAreas().add(sectionArea); - } - - for (TextBlock textBlock : cell.getTextBlocks()) { - entireTableText.addAll(textBlock.getSequences()); - } - } - } - - Set rowEntities = findEntities(entireTableText, table.getHeadline(), sectionNumber.intValue(), dictionary, local); - surroundingWordsService.addSurroundingText(rowEntities, entireTableText, dictionary); - - sectionSearchableTextPairs.add(new SectionSearchableTextPair(Section.builder() - .isLocal(local) - .dictionaryTypes(dictionary.getTypes()) - .entities(hintsPerSectionNumber != null && hintsPerSectionNumber.containsKey(sectionNumber.intValue()) ? Stream - .concat(rowEntities.stream(), hintsPerSectionNumber.get(sectionNumber.intValue()).stream()) - .collect(Collectors.toSet()) : rowEntities) - .text(entireTableText.getAsStringWithLinebreaks()) - .searchText(entireTableText.toString()) - .headline(table.getHeadline()) - .sectionNumber(sectionNumber.intValue()) - .searchableText(entireTableText) - .dictionary(dictionary) - .fileAttributes(fileAttributes) - .build(), entireTableText)); - - if (!local) { - sectionText.setText(entireTableText.toString()); - sectionText.setHeadline(table.getHeadline()); - sectionText.setSectionNumber(sectionNumber.intValue()); - sectionText.setTable(true); - classifiedDoc.getSectionText().add(sectionText); - } - - return sectionSearchableTextPairs; - } - - - private SectionSearchableTextPair processText(Document classifiedDoc, SearchableText searchableText, - List paragraphTextBlocks, String headline, - ManualRedactions manualRedactions, AtomicInteger sectionNumber, - Dictionary dictionary, boolean local, - Map> hintsPerSectionNumber, - List images, List fileAttributes) { - - if (!local) { - SectionText sectionText = new SectionText(); - for (TextBlock paragraphTextBlock : paragraphTextBlocks) { - SectionArea sectionArea = new SectionArea(new Point(paragraphTextBlock.getMinX(), paragraphTextBlock.getMinY()), paragraphTextBlock - .getWidth(), paragraphTextBlock.getHeight(), paragraphTextBlock.getPage()); - sectionText.getSectionAreas().add(sectionArea); - } - - sectionText.setText(searchableText.toString()); - sectionText.setHeadline(headline); - sectionText.setSectionNumber(sectionNumber.intValue()); - sectionText.setTable(false); - sectionText.setImages(images.stream() - .map(image -> convertAndRecategorize(image, sectionNumber.intValue(), headline, manualRedactions)) - .collect(Collectors.toSet())); - sectionText.setTextBlocks(paragraphTextBlocks); - classifiedDoc.getSectionText().add(sectionText); - } - - Set entities = findEntities(searchableText, headline, sectionNumber.intValue(), dictionary, local); - surroundingWordsService.addSurroundingText(entities, searchableText, dictionary); - - return new SectionSearchableTextPair(Section.builder() - .isLocal(local) - .dictionaryTypes(dictionary.getTypes()) - .entities(hintsPerSectionNumber != null && hintsPerSectionNumber.containsKey(sectionNumber.intValue()) ? Stream - .concat(entities.stream(), hintsPerSectionNumber.get(sectionNumber.intValue()).stream()) - .collect(Collectors.toSet()) : entities) - .text(searchableText.getAsStringWithLinebreaks()) - .searchText(searchableText.toString()) - .headline(headline) - .sectionNumber(sectionNumber.intValue()) - .searchableText(searchableText) - .dictionary(dictionary) - .images(images.stream() - .map(image -> convertAndRecategorize(image, sectionNumber.intValue(), headline, manualRedactions)) - .collect(Collectors.toSet())) - .fileAttributes(fileAttributes) - .build(), searchableText); - } - - - public Set findEntities(SearchableText searchableText, String headline, int sectionNumber, + private Set findEntities(SearchableText searchableText, String headline, int sectionNumber, Dictionary dictionary, boolean local) { Set found = new HashSet<>(); @@ -413,34 +225,6 @@ public class EntityRedactionService { } - private Image convertAndRecategorize(PdfImage pdfImage, int sectionNumber, String headline, - ManualRedactions manualRedactions) { - - Image image = Image.builder() - .type(pdfImage.getImageType().equals(ImageType.OTHER) ? "image" : pdfImage.getImageType() - .name() - .toLowerCase(Locale.ROOT)) - .position(pdfImage.getPosition()) - .sectionNumber(sectionNumber) - .section(headline) - .page(pdfImage.getPage()) - .hasTransparency(pdfImage.isHasTransparency()) - .build(); - - String imageId = IdBuilder.buildId(image.getPosition(), image.getPage()); - if (manualRedactions != null && manualRedactions.getImageRecategorizations() != null) { - for (ManualImageRecategorization imageRecategorization : manualRedactions.getImageRecategorizations()) { - if (imageRecategorization.getStatus().equals(Status.APPROVED) && imageRecategorization.getId() - .equals(imageId)) { - image.setType(imageRecategorization.getType()); - } - } - } - - return image; - } - - private Set getAiEntities(int sectionNumber, String searchableString, String headline) { Set found = new HashSet<>(); diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java deleted file mode 100644 index 1daf766b..00000000 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/ReanalyzeService.java +++ /dev/null @@ -1,321 +0,0 @@ -package com.iqser.red.service.redaction.v1.server.redaction.service; - -import com.iqser.red.service.file.management.v1.api.model.FileType; -import com.iqser.red.service.redaction.v1.model.*; -import com.iqser.red.service.redaction.v1.server.classification.model.Document; -import com.iqser.red.service.redaction.v1.server.classification.model.SectionText; -import com.iqser.red.service.redaction.v1.server.classification.model.Text; -import com.iqser.red.service.redaction.v1.server.client.LegalBasisClient; -import com.iqser.red.service.redaction.v1.server.exception.RedactionException; -import com.iqser.red.service.redaction.v1.server.redaction.model.Dictionary; -import com.iqser.red.service.redaction.v1.server.redaction.model.*; -import com.iqser.red.service.redaction.v1.server.redaction.utils.EntitySearchUtils; -import com.iqser.red.service.redaction.v1.server.redaction.utils.IdBuilder; -import com.iqser.red.service.redaction.v1.server.segmentation.PdfSegmentationService; -import com.iqser.red.service.redaction.v1.server.settings.RedactionServiceSettings; -import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService; - -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; - -import org.kie.api.runtime.KieContainer; -import org.springframework.stereotype.Service; -import org.springframework.web.bind.annotation.RequestBody; - -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -@Slf4j -@Service -@RequiredArgsConstructor -public class ReanalyzeService { - - private final DictionaryService dictionaryService; - private final DroolsExecutionService droolsExecutionService; - private final SurroundingWordsService surroundingWordsService; - private final EntityRedactionService entityRedactionService; - private final RedactionLogCreatorService redactionLogCreatorService; - private final RedactionStorageService redactionStorageService; - private final PdfSegmentationService pdfSegmentationService; - private final RedactionChangeLogService redactionChangeLogService; - private final AnalyzeResponseService analyzeResponseService; - private final LegalBasisClient legalBasisClient; - private final RedactionServiceSettings redactionServiceSettings; - - - public AnalyzeResult analyze(AnalyzeRequest analyzeRequest) { - - long startTime = System.currentTimeMillis(); - - var pageCount = 0; - Document classifiedDoc; - - try { - var storedObjectStream = redactionStorageService.getStoredObject(RedactionStorageService.StorageIdUtils.getStorageId(analyzeRequest - .getDossierId(), analyzeRequest.getFileId(), FileType.ORIGIN)); - classifiedDoc = pdfSegmentationService.parseDocument(storedObjectStream); - pageCount = classifiedDoc.getPages().size(); - } catch (Exception e) { - throw new RedactionException(e); - } - log.info("Document structure analysis successful, starting redaction analysis..."); - - entityRedactionService.processDocument(classifiedDoc, analyzeRequest.getDossierTemplateId(), analyzeRequest.getManualRedactions(), analyzeRequest - .getDossierId(), analyzeRequest.getFileAttributes()); - redactionLogCreatorService.createRedactionLog(classifiedDoc, pageCount, analyzeRequest.getDossierTemplateId()); - - log.info("Redaction analysis successful..."); - - var legalBasis = legalBasisClient.getLegalBasisMapping(analyzeRequest.getDossierTemplateId()); - var redactionLog = new RedactionLog(redactionServiceSettings.getAnalysisVersion(), classifiedDoc.getRedactionLogEntities(), legalBasis, classifiedDoc.getDictionaryVersion() - .getDossierTemplateVersion(), classifiedDoc.getDictionaryVersion() - .getDossierVersion(), classifiedDoc.getRulesVersion(), legalBasisClient.getVersion(analyzeRequest.getDossierTemplateId())); - - excludeExcludedPages(redactionLog, analyzeRequest.getExcludedPages()); - - log.info("Analyzed with rules {} and dictionary {} for dossierTemplate: {}", classifiedDoc.getRulesVersion(), classifiedDoc - .getDictionaryVersion(), analyzeRequest.getDossierTemplateId()); - - var redactionLogChange = redactionChangeLogService.computeChanges(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), redactionLog); - redactionLog = redactionLogChange.getRedactionLog(); - - redactionStorageService.storeObject(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), FileType.REDACTION_LOG, redactionLog); - redactionStorageService.storeObject(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), FileType.TEXT, new Text(pageCount, classifiedDoc - .getSectionText())); - redactionStorageService.storeObject(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), FileType.SECTION_GRID, classifiedDoc - .getSectionGrid()); - - long duration = System.currentTimeMillis() - startTime; - return analyzeResponseService.createAnalyzeResponse(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), duration, pageCount, redactionLog, redactionLogChange.isHasChanges()); - } - - - @SneakyThrows - public AnalyzeResult reanalyze(@RequestBody AnalyzeRequest analyzeRequest) { - - long startTime = System.currentTimeMillis(); - - var redactionLog = redactionStorageService.getRedactionLog(analyzeRequest.getDossierId(), analyzeRequest.getFileId()); - var text = redactionStorageService.getText(analyzeRequest.getDossierId(), analyzeRequest.getFileId()); - - // not yet ready for reanalysis - if (redactionLog == null || text == null || text.getNumberOfPages() == 0) { - return analyze(analyzeRequest); - } - - DictionaryIncrement dictionaryIncrement = dictionaryService.getDictionaryIncrements(analyzeRequest.getDossierTemplateId(), new DictionaryVersion(redactionLog - .getDictionaryVersion(), redactionLog.getDossierDictionaryVersion()), analyzeRequest.getDossierId()); - - Set sectionsToReanalyse = !analyzeRequest.getSectionsToReanalyse().isEmpty() ? analyzeRequest.getSectionsToReanalyse() : - findSectionsToReanalyse(dictionaryIncrement, redactionLog, text, analyzeRequest); - - if (sectionsToReanalyse.isEmpty()) { - return finalizeAnalysis(analyzeRequest, startTime, redactionLog, text, dictionaryIncrement); - } - - List reanalysisSections = text.getSectionTexts() - .stream() - .filter(sectionText -> sectionsToReanalyse.contains(sectionText.getSectionNumber())) - .collect(Collectors.toList()); - - KieContainer kieContainer = droolsExecutionService.updateRules(analyzeRequest.getDossierTemplateId()); - - Dictionary dictionary = dictionaryService.getDeepCopyDictionary(analyzeRequest.getDossierTemplateId(), analyzeRequest - .getDossierId()); - - Map> imagesPerPage = new HashMap<>(); - Set entities = findEntities(reanalysisSections, dictionary, kieContainer, analyzeRequest, false, null, imagesPerPage); - - if (dictionary.hasLocalEntries()) { - Map> hintsPerSectionNumber = entityRedactionService.getHintsPerSection(entities, dictionary); - Set foundByLocal = findEntities(reanalysisSections, dictionary, kieContainer, analyzeRequest, true, hintsPerSectionNumber, imagesPerPage); - EntitySearchUtils.addEntitiesWithHigherRank(entities, foundByLocal, dictionary); - EntitySearchUtils.removeEntitiesContainedInLarger(entities); - } - - Map> entitiesPerPage = entityRedactionService.convertToEnititesPerPage(entities); - - List newRedactionLogEntries = new ArrayList<>(); - for (int page = 1; page <= text.getNumberOfPages(); page++) { - if (entitiesPerPage.get(page) != null) { - newRedactionLogEntries.addAll(redactionLogCreatorService.addEntries(entitiesPerPage, page, analyzeRequest - .getDossierTemplateId())); - } - - if (imagesPerPage.get(page) != null) { - newRedactionLogEntries.addAll(redactionLogCreatorService.addImageEntries(imagesPerPage, page, analyzeRequest - .getDossierTemplateId())); - } - - } - - redactionLog.getRedactionLogEntry().removeIf(entry -> sectionsToReanalyse.contains(entry.getSectionNumber())); - redactionLog.getRedactionLogEntry().addAll(newRedactionLogEntries); - AnalyzeResult analyzeResult = finalizeAnalysis(analyzeRequest, startTime, redactionLog, text, dictionaryIncrement); - analyzeResult.setWasReanalyzed(true); - return analyzeResult; - } - - - private Set findSectionsToReanalyse(DictionaryIncrement dictionaryIncrement, RedactionLog redactionLog, - Text text, AnalyzeRequest analyzeRequest) { - - Set relevantManuallyModifiedAnnotationIds = getRelevantManuallyModifiedAnnotationIds(analyzeRequest.getManualRedactions()); - - Set sectionsToReanalyse = new HashSet<>(); - Map> imageEntries = new HashMap<>(); - for (RedactionLogEntry entry : redactionLog.getRedactionLogEntry()) { - if (entry.isManual() || relevantManuallyModifiedAnnotationIds.contains(entry.getId())) { - sectionsToReanalyse.add(entry.getSectionNumber()); - } - if (entry.isImage() || entry.getType().equals("image")) { - imageEntries.computeIfAbsent(entry.getSectionNumber(), x -> new HashSet<>()).add(convert(entry)); - } - } - - for (SectionText sectionText : text.getSectionTexts()) { - - if (EntitySearchUtils.sectionContainsAny(sectionText.getText(), dictionaryIncrement.getValues())) { - sectionsToReanalyse.add(sectionText.getSectionNumber()); - } - - } - - log.info("Should reanalyze {} sections for request: {}", sectionsToReanalyse.size(), analyzeRequest); - - return sectionsToReanalyse; - } - - - private Set findEntities(List reanalysisSections, Dictionary dictionary, - KieContainer kieContainer, AnalyzeRequest analyzeRequest, boolean local, - Map> hintsPerSectionNumber, - Map> imagesPerPage) { - - List sectionSearchableTextPairs = new ArrayList<>(); - for (SectionText reanalysisSection : reanalysisSections) { - - Set entities = entityRedactionService.findEntities(reanalysisSection.getSearchableText(), reanalysisSection - .getHeadline(), reanalysisSection.getSectionNumber(), dictionary, local); - if (reanalysisSection.getCellStarts() != null && !reanalysisSection.getCellStarts().isEmpty()) { - surroundingWordsService.addSurroundingText(entities, reanalysisSection.getSearchableText(), dictionary, reanalysisSection - .getCellStarts()); - } else { - surroundingWordsService.addSurroundingText(entities, reanalysisSection.getSearchableText(), dictionary); - } - - if (!local && reanalysisSection.getImages() != null && !reanalysisSection.getImages() - .isEmpty() && analyzeRequest.getManualRedactions() != null && analyzeRequest.getManualRedactions() - .getImageRecategorizations() != null) { - for (Image image : reanalysisSection.getImages()) { - String imageId = IdBuilder.buildId(image.getPosition(), image.getPage()); - for (ManualImageRecategorization imageRecategorization : analyzeRequest.getManualRedactions() - .getImageRecategorizations()) { - if (imageRecategorization.getStatus().equals(Status.APPROVED) && imageRecategorization.getId() - .equals(imageId)) { - image.setType(imageRecategorization.getType()); - } - } - } - } - - sectionSearchableTextPairs.add(new SectionSearchableTextPair(Section.builder() - .isLocal(false) - .dictionaryTypes(dictionary.getTypes()) - .entities(hintsPerSectionNumber != null && hintsPerSectionNumber.containsKey(reanalysisSection.getSectionNumber()) ? Stream - .concat(entities.stream(), hintsPerSectionNumber.get(reanalysisSection.getSectionNumber()) - .stream()) - .collect(Collectors.toSet()) : entities) - .text(reanalysisSection.getSearchableText().getAsStringWithLinebreaks()) - .searchText(reanalysisSection.getSearchableText().toString()) - .headline(reanalysisSection.getHeadline()) - .sectionNumber(reanalysisSection.getSectionNumber()) - .tabularData(reanalysisSection.getTabularData()) - .searchableText(reanalysisSection.getSearchableText()) - .dictionary(dictionary) - .images(reanalysisSection.getImages()) - .fileAttributes(analyzeRequest.getFileAttributes()) - .build(), reanalysisSection.getSearchableText())); - } - - Set entities = new HashSet<>(); - sectionSearchableTextPairs.forEach(sectionSearchableTextPair -> { - Section analysedSection = droolsExecutionService.executeRules(kieContainer, sectionSearchableTextPair.getSection()); - entities.addAll(analysedSection.getEntities()); - EntitySearchUtils.removeEntitiesContainedInLarger(entities); - - for (Image image : analysedSection.getImages()) { - imagesPerPage.computeIfAbsent(image.getPage(), (a) -> new HashSet<>()).add(image); - } - - entityRedactionService.addLocalValuesToDictionary(analysedSection, dictionary); - }); - - return entities; - } - - - private AnalyzeResult finalizeAnalysis(@RequestBody AnalyzeRequest analyzeRequest, long startTime, - RedactionLog redactionLog, Text text, - DictionaryIncrement dictionaryIncrement) { - - redactionLog.setDictionaryVersion(dictionaryIncrement.getDictionaryVersion().getDossierTemplateVersion()); - redactionLog.setDossierDictionaryVersion(dictionaryIncrement.getDictionaryVersion().getDossierVersion()); - - excludeExcludedPages(redactionLog, analyzeRequest.getExcludedPages()); - - var redactionLogChange = redactionChangeLogService.computeChanges(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), redactionLog); - redactionStorageService.storeObject(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), FileType.REDACTION_LOG, redactionLogChange.getRedactionLog()); - - long duration = System.currentTimeMillis() - startTime; - - return analyzeResponseService.createAnalyzeResponse(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), duration, text - .getNumberOfPages(), redactionLogChange.getRedactionLog(), redactionLogChange.isHasChanges()); - } - - - private Set getRelevantManuallyModifiedAnnotationIds(ManualRedactions manualRedactions) { - - if (manualRedactions == null) { - return new HashSet<>(); - } - - return Stream.concat(manualRedactions.getManualLegalBasisChanges() - .stream() - .map(ManualLegalBasisChange::getId), Stream.concat(manualRedactions.getImageRecategorizations() - .stream() - .map(ManualImageRecategorization::getId), Stream.concat(manualRedactions.getIdsToRemove() - .stream() - .map(IdRemoval::getId), manualRedactions.getForceRedacts().stream().map(ManualForceRedact::getId)))) - .collect(Collectors.toSet()); - } - - - public Image convert(RedactionLogEntry entry) { - - Rectangle position = entry.getPositions().get(0); - - return Image.builder() - .type(entry.getType()) - .position(new RedRectangle2D(position.getTopLeft().getX(), position.getTopLeft() - .getY(), position.getWidth(), position.getHeight())) - .sectionNumber(entry.getSectionNumber()) - .section(entry.getSection()) - .page(position.getPage()) - .hasTransparency(entry.isImageHasTransparency()) - .build(); - } - - - private void excludeExcludedPages(RedactionLog redactionLog, Set excludedPages) { - - if(excludedPages != null && !excludedPages.isEmpty()) { - redactionLog.getRedactionLogEntry().forEach(entry -> entry.getPositions().forEach(pos -> { if (excludedPages.contains(pos.getPage())) { - entry.setExcluded(true); - }})); - } - } - -} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionChangeLogService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionChangeLogService.java index 41772291..516c0487 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionChangeLogService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionChangeLogService.java @@ -31,6 +31,8 @@ public class RedactionChangeLogService { public RedactionLogChanges computeChanges(String dossierId, String fileId, RedactionLog currentRedactionLog) { + long start = System.currentTimeMillis(); + RedactionLog previousRedactionLog = redactionStorageService.getRedactionLog(dossierId, fileId); if (previousRedactionLog == null) { @@ -98,6 +100,7 @@ public class RedactionChangeLogService { currentRedactionLog.setRedactionLogEntry(newRedactionLogEntries); + log.info("Change computation took: {}", System.currentTimeMillis() - start); return new RedactionLogChanges(currentRedactionLog, !addedIds.isEmpty() || !removedIds.isEmpty()); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java index ec1f78db..0eaea701 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java @@ -1,26 +1,5 @@ package com.iqser.red.service.redaction.v1.server.redaction.service; -import com.iqser.red.service.redaction.v1.model.CellRectangle; -import com.iqser.red.service.redaction.v1.model.Point; -import com.iqser.red.service.redaction.v1.model.Rectangle; -import com.iqser.red.service.redaction.v1.model.RedactionLogEntry; -import com.iqser.red.service.redaction.v1.model.SectionRectangle; -import com.iqser.red.service.redaction.v1.server.classification.model.Document; -import com.iqser.red.service.redaction.v1.server.classification.model.Paragraph; -import com.iqser.red.service.redaction.v1.server.classification.model.TextBlock; -import com.iqser.red.service.redaction.v1.server.parsing.model.RedTextPosition; -import com.iqser.red.service.redaction.v1.server.parsing.model.TextPositionSequence; -import com.iqser.red.service.redaction.v1.server.redaction.model.Entity; -import com.iqser.red.service.redaction.v1.server.redaction.model.EntityPositionSequence; -import com.iqser.red.service.redaction.v1.server.redaction.model.Image; -import com.iqser.red.service.redaction.v1.server.redaction.utils.IdBuilder; -import com.iqser.red.service.redaction.v1.server.tableextraction.model.AbstractTextContainer; -import com.iqser.red.service.redaction.v1.server.tableextraction.model.Cell; -import com.iqser.red.service.redaction.v1.server.tableextraction.model.Table; -import lombok.RequiredArgsConstructor; -import org.apache.commons.collections4.CollectionUtils; -import org.springframework.stereotype.Service; - import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -28,6 +7,22 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.stereotype.Service; + +import com.iqser.red.service.redaction.v1.model.Point; +import com.iqser.red.service.redaction.v1.model.Rectangle; +import com.iqser.red.service.redaction.v1.model.RedactionLogEntry; +import com.iqser.red.service.redaction.v1.server.parsing.model.RedTextPosition; +import com.iqser.red.service.redaction.v1.server.parsing.model.TextPositionSequence; +import com.iqser.red.service.redaction.v1.server.redaction.model.Entity; +import com.iqser.red.service.redaction.v1.server.redaction.model.EntityPositionSequence; +import com.iqser.red.service.redaction.v1.server.redaction.model.Image; +import com.iqser.red.service.redaction.v1.server.redaction.model.PageEntities; +import com.iqser.red.service.redaction.v1.server.redaction.utils.IdBuilder; + +import lombok.RequiredArgsConstructor; + @Service @RequiredArgsConstructor public class RedactionLogCreatorService { @@ -35,26 +30,27 @@ public class RedactionLogCreatorService { private final DictionaryService dictionaryService; - public void createRedactionLog(Document classifiedDoc, int numberOfPages, String dossierTemplateId) { + public List createRedactionLog(PageEntities pageEntities, int numberOfPages, + String dossierTemplateId) { + + List entries = new ArrayList<>(); for (int page = 1; page <= numberOfPages; page++) { - - addSectionGrid(classifiedDoc, page); - - if (classifiedDoc.getEntities().get(page) != null) { - classifiedDoc.getRedactionLogEntities() - .addAll(addEntries(classifiedDoc.getEntities(), page, dossierTemplateId)); + if (pageEntities.getEntitiesPerPage().get(page) != null) { + entries.addAll(addEntries(pageEntities.getEntitiesPerPage(), page, dossierTemplateId)); } - if (classifiedDoc.getImages().get(page) != null && !classifiedDoc.getImages().get(page).isEmpty()) { - classifiedDoc.getRedactionLogEntities() - .addAll(addImageEntries(classifiedDoc.getImages(), page, dossierTemplateId)); + if (pageEntities.getImagesPerPage().get(page) != null) { + entries.addAll(addImageEntries(pageEntities.getImagesPerPage(), page, dossierTemplateId)); } } + + return entries; } - public List addImageEntries(Map> images, int pageNumber, String dossierTemplateId) { + public List addImageEntries(Map> images, int pageNumber, + String dossierTemplateId) { List redactionLogEntities = new ArrayList<>(); @@ -83,7 +79,6 @@ public class RedactionLogCreatorService { .imageHasTransparency(image.isHasTransparency()) .build(); - redactionLogEntities.add(redactionLogEntry); } @@ -101,7 +96,6 @@ public class RedactionLogCreatorService { entityLoop: for (Entity entity : entities.get(page)) { - for (EntityPositionSequence entityPositionSequence : entity.getPositionSequences()) { RedactionLogEntry redactionLogEntry = createRedactionLogEntry(entity, dossierTemplateId); @@ -121,12 +115,10 @@ public class RedactionLogCreatorService { .flatMap(seq -> seq.getTextPositions().stream()) .collect(Collectors.toList()), page); - redactionLogEntry.getPositions().addAll(rectanglesPerLine); } - // FIXME ids should never be null. Figure out why this happens. if (redactionLogEntry.getId() != null) { redactionLogEntities.add(redactionLogEntry); @@ -189,50 +181,6 @@ public class RedactionLogCreatorService { } - private void addSectionGrid(Document classifiedDoc, int page) { - - for (Paragraph paragraph : classifiedDoc.getParagraphs()) { - - for (int i = 0; i <= paragraph.getPageBlocks().size() - 1; i++) { - - AbstractTextContainer textBlock = paragraph.getPageBlocks().get(i); - - if (textBlock.getPage() != page) { - continue; - } - - if (textBlock instanceof TextBlock) { - - classifiedDoc.getSectionGrid() - .getRectanglesPerPage() - .computeIfAbsent(page, (x) -> new ArrayList<>()) - .add(new SectionRectangle(new Point(textBlock.getMinX(), textBlock.getMinY()), textBlock.getWidth(), textBlock - .getHeight(), i + 1, paragraph.getPageBlocks().size())); - - } else if (textBlock instanceof Table) { - - List cellRectangles = new ArrayList<>(); - for (List row : ((Table) textBlock).getRows()) { - for (Cell cell : row) { - if (cell != null) { - cellRectangles.add(new CellRectangle(new Point((float) cell.getX(), (float) cell.getY()), (float) cell - .getWidth(), (float) cell.getHeight())); - } - } - } - - classifiedDoc.getSectionGrid() - .getRectanglesPerPage() - .computeIfAbsent(page, (x) -> new ArrayList<>()) - .add(new SectionRectangle(new Point(textBlock.getMinX(), textBlock.getMinY()), textBlock.getWidth(), textBlock - .getHeight(), i + 1, paragraph.getPageBlocks().size(), cellRectangles)); - - } - } - } - } - - private float[] getColor(String type, String dossierTemplateId, boolean isRedaction) { if (!isRedaction && !isHint(type, dossierTemplateId)) { @@ -253,5 +201,4 @@ public class RedactionLogCreatorService { return dictionaryService.isRecommendation(type, dossierTemplateId); } - } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/SectionGridCreatorService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/SectionGridCreatorService.java new file mode 100644 index 00000000..0994b03a --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/SectionGridCreatorService.java @@ -0,0 +1,76 @@ +package com.iqser.red.service.redaction.v1.server.redaction.service; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.stereotype.Service; + +import com.iqser.red.service.redaction.v1.model.CellRectangle; +import com.iqser.red.service.redaction.v1.model.Point; +import com.iqser.red.service.redaction.v1.model.SectionRectangle; +import com.iqser.red.service.redaction.v1.server.classification.model.Document; +import com.iqser.red.service.redaction.v1.server.classification.model.Paragraph; +import com.iqser.red.service.redaction.v1.server.classification.model.TextBlock; +import com.iqser.red.service.redaction.v1.server.tableextraction.model.AbstractTextContainer; +import com.iqser.red.service.redaction.v1.server.tableextraction.model.Cell; +import com.iqser.red.service.redaction.v1.server.tableextraction.model.Table; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class SectionGridCreatorService { + + + public void createSectionGrid(Document classifiedDoc, int numberOfPages) { + + for (int page = 1; page <= numberOfPages; page++) { + addSectionGrid(classifiedDoc, page); + } + } + + + private void addSectionGrid(Document classifiedDoc, int page) { + + for (Paragraph paragraph : classifiedDoc.getParagraphs()) { + + for (int i = 0; i <= paragraph.getPageBlocks().size() - 1; i++) { + + AbstractTextContainer textBlock = paragraph.getPageBlocks().get(i); + + if (textBlock.getPage() != page) { + continue; + } + + if (textBlock instanceof TextBlock) { + + classifiedDoc.getSectionGrid() + .getRectanglesPerPage() + .computeIfAbsent(page, (x) -> new ArrayList<>()) + .add(new SectionRectangle(new Point(textBlock.getMinX(), textBlock.getMinY()), textBlock.getWidth(), textBlock + .getHeight(), i + 1, paragraph.getPageBlocks().size())); + + } else if (textBlock instanceof Table) { + + List cellRectangles = new ArrayList<>(); + for (List row : ((Table) textBlock).getRows()) { + for (Cell cell : row) { + if (cell != null) { + cellRectangles.add(new CellRectangle(new Point((float) cell.getX(), (float) cell.getY()), (float) cell + .getWidth(), (float) cell.getHeight())); + } + } + } + + classifiedDoc.getSectionGrid() + .getRectanglesPerPage() + .computeIfAbsent(page, (x) -> new ArrayList<>()) + .add(new SectionRectangle(new Point(textBlock.getMinX(), textBlock.getMinY()), textBlock.getWidth(), textBlock + .getHeight(), i + 1, paragraph.getPageBlocks().size(), cellRectangles)); + + } + } + } + } + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/SectionTextBuilderService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/SectionTextBuilderService.java new file mode 100644 index 00000000..8fe206d3 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/SectionTextBuilderService.java @@ -0,0 +1,210 @@ +package com.iqser.red.service.redaction.v1.server.redaction.service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.stereotype.Service; + +import com.iqser.red.service.redaction.v1.model.Point; +import com.iqser.red.service.redaction.v1.model.SectionArea; +import com.iqser.red.service.redaction.v1.server.classification.model.Document; +import com.iqser.red.service.redaction.v1.server.classification.model.Footer; +import com.iqser.red.service.redaction.v1.server.classification.model.Header; +import com.iqser.red.service.redaction.v1.server.classification.model.Paragraph; +import com.iqser.red.service.redaction.v1.server.classification.model.SectionText; +import com.iqser.red.service.redaction.v1.server.classification.model.TextBlock; +import com.iqser.red.service.redaction.v1.server.classification.model.UnclassifiedText; +import com.iqser.red.service.redaction.v1.server.redaction.model.CellValue; +import com.iqser.red.service.redaction.v1.server.redaction.model.Image; +import com.iqser.red.service.redaction.v1.server.redaction.model.ImageType; +import com.iqser.red.service.redaction.v1.server.redaction.model.PdfImage; +import com.iqser.red.service.redaction.v1.server.redaction.model.SearchableText; +import com.iqser.red.service.redaction.v1.server.tableextraction.model.Cell; +import com.iqser.red.service.redaction.v1.server.tableextraction.model.Table; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SectionTextBuilderService { + + public List buildSectionText(Document classifiedDoc) { + + List sectionTexts = new ArrayList<>(); + AtomicInteger sectionNumber = new AtomicInteger(1); + for (Paragraph paragraph : classifiedDoc.getParagraphs()) { + + List
tables = paragraph.getTables(); + for (Table table : tables) { + if (table.getColCount() == 2) { + sectionTexts.add(processTableAsOneText(table, sectionNumber)); + } else { + sectionTexts.addAll(processTablePerRow(table, sectionNumber)); + } + sectionNumber.incrementAndGet(); + } + sectionTexts.add(processText(paragraph.getSearchableText(), paragraph.getTextBlocks(), paragraph.getHeadline(), sectionNumber, paragraph + .getImages())); + sectionNumber.incrementAndGet(); + } + + for (Header header : classifiedDoc.getHeaders()) { + sectionTexts.add(processText(header.getSearchableText(), header.getTextBlocks(), "Header", sectionNumber, new ArrayList<>())); + sectionNumber.incrementAndGet(); + } + + for (Footer footer : classifiedDoc.getFooters()) { + sectionTexts.add(processText(footer.getSearchableText(), footer.getTextBlocks(), "Footer", sectionNumber, new ArrayList<>())); + sectionNumber.incrementAndGet(); + } + + for (UnclassifiedText unclassifiedText : classifiedDoc.getUnclassifiedTexts()) { + sectionTexts.add(processText(unclassifiedText.getSearchableText(), unclassifiedText.getTextBlocks(), "", sectionNumber, new ArrayList<>())); + sectionNumber.incrementAndGet(); + } + + return sectionTexts; + } + + + private List processTablePerRow(Table table, AtomicInteger sectionNumber) { + + List sectionTexts = new ArrayList<>(); + for (List row : table.getRows()) { + SearchableText searchableRow = new SearchableText(); + Map tabularData = new HashMap<>(); + int start = 0; + List cellStarts = new ArrayList<>(); + SectionText sectionText = new SectionText(); + for (Cell cell : row) { + + if (CollectionUtils.isEmpty(cell.getTextBlocks())) { + continue; + } + + SectionArea sectionArea = new SectionArea(new Point((float) cell.getX(), (float) cell.getY()), (float) cell + .getWidth(), (float) cell.getHeight(), cell.getTextBlocks() + .get(0) + .getSequences() + .get(0) + .getPage()); + sectionText.getSectionAreas().add(sectionArea); + sectionText.getTextBlocks().addAll(cell.getTextBlocks()); + + int cellStart = start; + + if (!cell.isHeaderCell()) { + cell.getHeaderCells().forEach(headerCell -> { + StringBuilder headerBuilder = new StringBuilder(); + headerCell.getTextBlocks().forEach(textBlock -> headerBuilder.append(textBlock.getText())); + String headerName = headerBuilder.toString() + .replaceAll("\n", "") + .replaceAll(" ", "") + .replaceAll("-", ""); + sectionArea.setHeader(headerName); + tabularData.put(headerName, new CellValue(cell.getTextBlocks(), cellStart)); + }); + } + + for (TextBlock textBlock : cell.getTextBlocks()) { + // TODO avoid cell overlap merging. + searchableRow.addAll(textBlock.getSequences()); + } + cellStarts.add(cellStart); + start = start + cell.toString().trim().length() + 1; + + } + + sectionText.setText(searchableRow.toString()); + sectionText.setHeadline(table.getHeadline()); + sectionText.setSectionNumber(sectionNumber.intValue()); + sectionText.setTable(true); + sectionText.setTabularData(tabularData); + sectionText.setCellStarts(cellStarts); + sectionTexts.add(sectionText); + + sectionNumber.incrementAndGet(); + } + + return sectionTexts; + } + + + private SectionText processTableAsOneText(Table table, AtomicInteger sectionNumber) { + + SearchableText entireTableText = new SearchableText(); + SectionText sectionText = new SectionText(); + for (List row : table.getRows()) { + for (Cell cell : row) { + if (CollectionUtils.isEmpty(cell.getTextBlocks())) { + continue; + } + + SectionArea sectionArea = new SectionArea(new Point((float) cell.getX(), (float) cell.getY()), (float) cell + .getWidth(), (float) cell.getHeight(), cell.getTextBlocks() + .get(0) + .getSequences() + .get(0) + .getPage()); + sectionText.getTextBlocks().addAll(cell.getTextBlocks()); + sectionText.getSectionAreas().add(sectionArea); + + for (TextBlock textBlock : cell.getTextBlocks()) { + entireTableText.addAll(textBlock.getSequences()); + } + } + } + + sectionText.setText(entireTableText.toString()); + sectionText.setHeadline(table.getHeadline()); + sectionText.setSectionNumber(sectionNumber.intValue()); + sectionText.setTable(true); + return sectionText; + } + + + private SectionText processText(SearchableText searchableText, List paragraphTextBlocks, String headline, + AtomicInteger sectionNumber, List images) { + + SectionText sectionText = new SectionText(); + for (TextBlock paragraphTextBlock : paragraphTextBlocks) { + SectionArea sectionArea = new SectionArea(new Point(paragraphTextBlock.getMinX(), paragraphTextBlock.getMinY()), paragraphTextBlock + .getWidth(), paragraphTextBlock.getHeight(), paragraphTextBlock.getPage()); + sectionText.getSectionAreas().add(sectionArea); + } + + sectionText.setText(searchableText.toString()); + sectionText.setHeadline(headline); + sectionText.setSectionNumber(sectionNumber.intValue()); + sectionText.setTable(false); + sectionText.setImages(images.stream() + .map(image -> convertImage(image, sectionNumber.intValue(), headline)) + .collect(Collectors.toSet())); + sectionText.setTextBlocks(paragraphTextBlocks); + return sectionText; + } + + + private Image convertImage(PdfImage pdfImage, int sectionNumber, String headline) { + + return Image.builder() + .type(pdfImage.getImageType().equals(ImageType.OTHER) ? "image" : pdfImage.getImageType() + .name() + .toLowerCase(Locale.ROOT)) + .position(pdfImage.getPosition()) + .sectionNumber(sectionNumber) + .section(headline) + .page(pdfImage.getPage()) + .hasTransparency(pdfImage.isHasTransparency()) + .build(); + } + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java index 5a4daeb8..05a5ccc3 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java @@ -13,7 +13,7 @@ import com.iqser.red.service.redaction.v1.server.client.LegalBasisClient; import com.iqser.red.service.redaction.v1.server.client.RulesClient; import com.iqser.red.service.redaction.v1.server.controller.RedactionController; import com.iqser.red.service.redaction.v1.server.memory.MemoryStats; -import com.iqser.red.service.redaction.v1.server.redaction.service.ReanalyzeService; +import com.iqser.red.service.redaction.v1.server.redaction.service.AnalyzeService; import com.iqser.red.service.redaction.v1.server.redaction.utils.ResourceLoader; import com.iqser.red.service.redaction.v1.server.redaction.utils.TextNormalizationUtilities; import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService; @@ -90,7 +90,7 @@ public class RedactionIntegrationTest { private RedactionController redactionController; @Autowired - private ReanalyzeService reanalyzeService; + private AnalyzeService analyzeService; @Autowired private ObjectMapper objectMapper; @@ -524,7 +524,8 @@ public class RedactionIntegrationTest { AnalyzeRequest request = prepareStorage("files/Minimal Examples/270Rotated.pdf"); MemoryStats.printMemoryStats(); - AnalyzeResult result = reanalyzeService.analyze(request); + analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); + AnalyzeResult result = analyzeService.analyze(request); assertThat(result).isNotNull(); } @@ -535,7 +536,8 @@ public class RedactionIntegrationTest { AnalyzeRequest request = prepareStorage("scanned/VV-377031.pdf"); MemoryStats.printMemoryStats(); - AnalyzeResult result = reanalyzeService.analyze(request); + analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); + AnalyzeResult result = analyzeService.analyze(request); assertThat(result).isNotNull(); } @@ -547,7 +549,8 @@ public class RedactionIntegrationTest { ClassPathResource pdfFileResource = new ClassPathResource("files/Minimal Examples/merge_images.pdf"); AnalyzeRequest request = prepareStorage(pdfFileResource.getInputStream()); - AnalyzeResult result = reanalyzeService.analyze(request); + analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); + AnalyzeResult result = analyzeService.analyze(request); Map> duplicates = new HashMap<>(); @@ -573,7 +576,7 @@ public class RedactionIntegrationTest { fileOutputStream.write(annotateResponse.getDocument()); } long rstart = System.currentTimeMillis(); - reanalyzeService.reanalyze(request); + analyzeService.reanalyze(request); long rend = System.currentTimeMillis(); System.out.println("reanalysis analysis duration: " + (rend - rstart)); @@ -602,7 +605,11 @@ public class RedactionIntegrationTest { AnalyzeRequest request = prepareStorage(new FileInputStream((path))); System.out.println("Redacting file : " + path.getName()); - AnalyzeResult result = reanalyzeService.analyze(request); + analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); + + long fstart = System.currentTimeMillis(); + AnalyzeResult result = analyzeService.analyze(request); + System.out.println("analysis analysis duration: " + (System.currentTimeMillis() - fstart)); Map> duplicates = new HashMap<>(); @@ -620,7 +627,7 @@ public class RedactionIntegrationTest { when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(1L); long rstart = System.currentTimeMillis(); - reanalyzeService.reanalyze(request); + analyzeService.reanalyze(request); long rend = System.currentTimeMillis(); System.out.println("reanalysis analysis duration: " + (rend - rstart)); @@ -667,7 +674,8 @@ public class RedactionIntegrationTest { .value("true") .build())); - AnalyzeResult result = reanalyzeService.analyze(request); + analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); + AnalyzeResult result = analyzeService.analyze(request); var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); var text = redactionStorageService.getText(TEST_DOSSIER_ID, TEST_FILE_ID); @@ -741,7 +749,7 @@ public class RedactionIntegrationTest { request.setManualRedactions(manualRedactions); - AnalyzeResult reanalyzeResult = reanalyzeService.reanalyze(request); + AnalyzeResult reanalyzeResult = analyzeService.reanalyze(request); redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); @@ -766,7 +774,7 @@ public class RedactionIntegrationTest { when(dictionaryClient.getDictionaryForType(VERTEBRATE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) .thenReturn(getDictionaryResponse(VERTEBRATE, false)); - reanalyzeService.reanalyze(request); + analyzeService.reanalyze(request); redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); @@ -781,7 +789,8 @@ public class RedactionIntegrationTest { long start = System.currentTimeMillis(); AnalyzeRequest request = prepareStorage("files/Metolachlor/S-Metolachlor_RAR_02_Volume_2_2018-09-06.pdf"); - AnalyzeResult result = reanalyzeService.analyze(request); + analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); + AnalyzeResult result = analyzeService.analyze(request); AnnotateResponse annotateResponse = redactionController.annotate(AnnotateRequest.builder() .dossierId(TEST_DOSSIER_ID) @@ -842,7 +851,8 @@ public class RedactionIntegrationTest { AnalyzeRequest request = prepareStorage(pdfFileResource.getInputStream()); request.setManualRedactions(manualRedactions); - AnalyzeResult result = reanalyzeService.analyze(request); + analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); + AnalyzeResult result = analyzeService.analyze(request); manualRedactions.getEntriesToAdd().add(manualRedactionEntry); manualRedactions.setIdsToRemove(Set.of(IdRemoval.builder() @@ -855,7 +865,7 @@ public class RedactionIntegrationTest { .status(Status.APPROVED) .build())); - reanalyzeService.reanalyze(request); + analyzeService.reanalyze(request); var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); @@ -969,7 +979,8 @@ public class RedactionIntegrationTest { AnalyzeRequest request = prepareStorage(pdfFileResource.getInputStream()); - AnalyzeResult result = reanalyzeService.analyze(request); + analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); + AnalyzeResult result = analyzeService.analyze(request); var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); @@ -1017,7 +1028,8 @@ public class RedactionIntegrationTest { AnalyzeRequest request = prepareStorage(pdfFileResource.getInputStream()); - AnalyzeResult result = reanalyzeService.analyze(request); + analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); + AnalyzeResult result = analyzeService.analyze(request); AnnotateResponse annotateResponse = redactionController.annotate(AnnotateRequest.builder() .dossierId(TEST_DOSSIER_ID) diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionServiceTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionServiceTest.java deleted file mode 100644 index 1ab88196..00000000 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionServiceTest.java +++ /dev/null @@ -1,511 +0,0 @@ -package com.iqser.red.service.redaction.v1.server.redaction.service; - -import com.amazonaws.services.s3.AmazonS3; -import com.iqser.red.service.configuration.v1.api.model.*; -import com.iqser.red.service.configuration.v1.api.resource.DictionaryResource; -import com.iqser.red.service.redaction.v1.server.Application; -import com.iqser.red.service.redaction.v1.server.FileSystemBackedStorageService; -import com.iqser.red.service.redaction.v1.server.classification.model.Document; -import com.iqser.red.service.redaction.v1.server.client.DictionaryClient; -import com.iqser.red.service.redaction.v1.server.client.LegalBasisClient; -import com.iqser.red.service.redaction.v1.server.client.RulesClient; -import com.iqser.red.service.redaction.v1.server.redaction.model.Entity; -import com.iqser.red.service.redaction.v1.server.redaction.utils.EntitySearchUtils; -import com.iqser.red.service.redaction.v1.server.redaction.utils.ResourceLoader; -import com.iqser.red.service.redaction.v1.server.segmentation.PdfSegmentationService; -import com.iqser.red.storage.commons.service.StorageService; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.kie.api.KieServices; -import org.kie.api.builder.KieBuilder; -import org.kie.api.builder.KieFileSystem; -import org.kie.api.builder.KieModule; -import org.kie.api.runtime.KieContainer; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.Primary; -import org.springframework.core.io.ClassPathResource; -import org.springframework.test.context.junit4.SpringRunner; - -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.*; -import java.util.concurrent.atomic.AtomicLong; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.when; - -@RunWith(SpringRunner.class) -@SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@Import(EntityRedactionServiceTest.RedactionIntegrationTestConfiguration.class) -public class EntityRedactionServiceTest { - - private static final String DEFAULT_RULES = loadFromClassPath("drools/rules.drl"); - private static final String AUTHOR_CODE = "author"; - private static final String ADDRESS_CODE = "address"; - private static final String SPONSOR_CODE = "sponsor"; - - private static final AtomicLong DICTIONARY_VERSION = new AtomicLong(); - private static final AtomicLong RULES_VERSION = new AtomicLong(); - - @MockBean - private DictionaryClient dictionaryClient; - - @MockBean - private RulesClient rulesClient; - - @Autowired - private EntityRedactionService entityRedactionService; - - @Autowired - private PdfSegmentationService pdfSegmentationService; - - @Autowired - private DroolsExecutionService droolsExecutionService; - - @MockBean - private AmazonS3 amazonS3; - - @MockBean - private LegalBasisClient legalBasisClient; - - private final static String TEST_DOSSIER_TEMPLATE_ID = "123"; - - @Configuration - @EnableAutoConfiguration(exclude = {RabbitAutoConfiguration.class}) - public static class RedactionIntegrationTestConfiguration { - - @Bean - public KieContainer kieContainer() { - - KieServices kieServices = KieServices.Factory.get(); - - KieFileSystem kieFileSystem = kieServices.newKieFileSystem(); - InputStream input = new ByteArrayInputStream(DEFAULT_RULES.getBytes(StandardCharsets.UTF_8)); - kieFileSystem.write("src/test/resources/drools/rules.drl", kieServices.getResources() - .newInputStreamResource(input)); - KieBuilder kieBuilder = kieServices.newKieBuilder(kieFileSystem); - kieBuilder.buildAll(); - KieModule kieModule = kieBuilder.getKieModule(); - - return kieServices.newKieContainer(kieModule.getReleaseId()); - } - - - @Bean - @Primary - public StorageService inmemoryStorage() { - return new FileSystemBackedStorageService(); - } - - } - - - @Test - public void testNestedEntitiesRemoval() { - - Set entities = new HashSet<>(); - Entity nested = new Entity("nested", "fake type", 10, 16, "fake headline", 0, false, false); - Entity nesting = new Entity("nesting nested", "fake type", 2, 16, "fake headline", 0, false, false); - entities.add(nested); - entities.add(nesting); - EntitySearchUtils.removeEntitiesContainedInLarger(entities); - - assertThat(entities.size()).isEqualTo(1); - assertThat(entities).contains(nesting); - - } - - - @Test - public void testTableRedaction() throws IOException { - - ClassPathResource pdfFileResource = new ClassPathResource("files/Minimal Examples/Single Table.pdf"); - - DictionaryResponse dictionaryResponse = DictionaryResponse.builder() - .entries(toDictionaryEntry(Arrays.asList("Casey, H.W.", "O’Loughlin, C.K.", "Salamon, C.M.", "Smith, S.H."))) - .build(); - when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); - when(dictionaryClient.getDictionaryForType(AUTHOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); - DictionaryResponse addressResponse = DictionaryResponse.builder() - .entries(toDictionaryEntry(Collections.singletonList("Toxigenics, Inc., Decatur, IL 62526, USA"))) - .build(); - when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); - - DictionaryResponse sponsorResponse = DictionaryResponse.builder() - .entries(Collections.emptyList()) - .build(); - when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); - - Document classifiedDoc = pdfSegmentationService.parseDocument(pdfFileResource.getInputStream()); - entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId", null); - assertThat(classifiedDoc.getEntities()).hasSize(1); // one page - assertThat(classifiedDoc.getEntities().get(1)).hasSize(7);// 3 author cells, 1 address, 1 Y and 2 N entities - } - - - @Test - public void testNestedRedaction() throws IOException { - - ClassPathResource pdfFileResource = new ClassPathResource("files/Minimal Examples/nested_redaction.pdf"); - - DictionaryResponse dictionaryResponse = DictionaryResponse.builder() - .entries(toDictionaryEntry(Arrays.asList("Casey, H.W.", "O’Loughlin, C.K.", "Salamon, C.M.", "Smith, S.H."))) - .build(); - when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); - when(dictionaryClient.getDictionaryForType(AUTHOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); - DictionaryResponse addressResponse = DictionaryResponse.builder() - .entries(toDictionaryEntry(Collections.singletonList("Toxigenics, Inc., Decatur, IL 62526, USA"))) - .build(); - when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); - DictionaryResponse sponsorResponse = DictionaryResponse.builder() - .entries(Collections.emptyList()) - .build(); - when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); - - Document classifiedDoc = pdfSegmentationService.parseDocument(pdfFileResource.getInputStream()); - entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId", null); - assertThat(classifiedDoc.getEntities()).hasSize(1); // one page - assertThat(classifiedDoc.getEntities().get(1)).hasSize(7);// 3 author cells, 1 address, 1 Y and 2 N entities - } - - - @Test - public void testTrueNegativesInTable() throws IOException { - - ClassPathResource pdfFileResource = new ClassPathResource("files/Cyprodinil/40 Cyprodinil - EU AIR3 - LCA Section 1" + - " Supplement - Identity of the active substance - Reference list.pdf"); - when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); - DictionaryResponse dictionaryResponse = DictionaryResponse.builder() - .entries(toDictionaryEntry(new ArrayList<>(ResourceLoader.load("dictionaries/CBI_author.txt")))) - .build(); - when(dictionaryClient.getDictionaryForType(AUTHOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); - DictionaryResponse addressResponse = DictionaryResponse.builder() - .entries(toDictionaryEntry(new ArrayList<>(ResourceLoader.load("dictionaries/CBI_address.txt")))) - .build(); - when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); - DictionaryResponse sponsorResponse = DictionaryResponse.builder() - .entries(Collections.emptyList()) - .build(); - when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); - Document classifiedDoc = pdfSegmentationService.parseDocument(pdfFileResource.getInputStream()); - entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId", null); - assertThat(classifiedDoc.getEntities() - .entrySet() - .stream() - .noneMatch(entry -> entry.getValue().stream().anyMatch(e -> e.getMatchedRule() == 9))).isTrue(); - pdfFileResource = new ClassPathResource("files/Compounds/27 A8637C - EU AIR3 - MCP Section 1 - Identity of " + - "the plant protection product.pdf"); - classifiedDoc = pdfSegmentationService.parseDocument(pdfFileResource.getInputStream()); - entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId", null); - assertThat(classifiedDoc.getEntities() - .entrySet() - .stream() - .noneMatch(entry -> entry.getValue().stream().anyMatch(e -> e.getMatchedRule() == 9))).isTrue(); - } - - @Test - public void testFalsePositiveInWrongCell() throws IOException { - - ClassPathResource pdfFileResource = new ClassPathResource("files/Minimal Examples/Row With Ambiguous Redaction.pdf"); - when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); - DictionaryResponse dictionaryResponse = DictionaryResponse.builder() - .entries(toDictionaryEntry(new ArrayList<>(ResourceLoader.load("dictionaries/CBI_author.txt")))) - .build(); - when(dictionaryClient.getDictionaryForType(AUTHOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); - DictionaryResponse addressResponse = DictionaryResponse.builder() - .entries(toDictionaryEntry(new ArrayList<>(ResourceLoader.load("dictionaries/CBI_address.txt")))) - .build(); - when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); - DictionaryResponse sponsorResponse = DictionaryResponse.builder() - .entries(toDictionaryEntry(new ArrayList<>(ResourceLoader.load("dictionaries/CBI_sponsor.txt")))) - .build(); - when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); - Document classifiedDoc = pdfSegmentationService.parseDocument(pdfFileResource.getInputStream()); - entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId", null); - assertThat(classifiedDoc.getEntities()).hasSize(1); // one page - assertThat(classifiedDoc.getEntities().get(1).stream() - .filter(entity -> entity.getMatchedRule() == 9) - .count()).isEqualTo(10); - - } - - - @Test - public void testApplicantInTableRedaction() throws IOException { - - String tableRules = "package drools\n" + - "\n" + - "import com.iqser.red.service.redaction.v1.server.redaction.model.Section\n" + - "\n" + - "global Section section\n" + - "rule \"6: Redact contact information if applicant is found\"\n" + - " when\n" + - " eval(section.headlineContainsWord(\"applicant\") || section.getText().contains(\"Applicant\"));\n" + - " then\n" + - " section.redactLineAfter(\"Name:\", \"address\", 6,true, \"Applicant information was found\", \"Reg" + - " (EC) No 1107/2009 Art. 63 (2g)\");\n" + - " section.redactBetween(\"Address:\", \"Contact\", \"address\", 6,true, \"Applicant information was found\", \"Reg (EC) No 1107/2009 Art. 63 (2g)\");\n" + - " section.redactLineAfter(\"Contact point:\", \"address\", 6,true, \"Applicant information was found\", \"Reg (EC) No 1107/2009 Art. 63 (2g)\");\n" + - " section.redactLineAfter(\"Phone:\", \"address\", 6,true, \"Applicant information was found\", " + - "\"Reg (EC) No 1107/2009 Art. 63 (2g)\");\n" + - " section.redactLineAfter(\"Fax:\", \"address\", 6,true, \"Applicant information was found\", \"Reg " + - "(EC) No 1107/2009 Art. 63 (2g)\");\n" + - " section.redactLineAfter(\"Tel.:\", \"address\", 6,true, \"Applicant information was found\", \"Reg" + - " (EC) No 1107/2009 Art. 63 (2g)\");\n" + - " section.redactLineAfter(\"Tel:\", \"address\", 6,true, \"Applicant information was found\", \"Reg " + - "(EC) No 1107/2009 Art. 63 (2g)\");\n" + - " section.redactLineAfter(\"E-mail:\", \"address\", 6,true, \"Applicant information was found\", " + - "\"Reg (EC) No 1107/2009 Art. 63 (2g)\");\n" + - " section.redactLineAfter(\"Email:\", \"address\", 6,true, \"Applicant information was found\", " + - "\"Reg (EC) No 1107/2009 Art. 63 (2g)\");\n" + - " section.redactLineAfter(\"Contact:\", \"address\", 6,true, \"Applicant information was found\", " + - "\"Reg (EC) No 1107/2009 Art. 63 (2g)\");\n" + - " section.redactLineAfter(\"Telephone number:\", \"address\", 6,true, \"Applicant information was found\", \"Reg (EC) No 1107/2009 Art. 63 (2g)\");\n" + - " section.redactLineAfter(\"Fax number:\", \"address\", 6,true, \"Applicant information was found\"," + - " \"Reg (EC) No 1107/2009 Art. 63 (2g)\");\n" + - " section.redactLineAfter(\"Telephone:\", \"address\", 6,true, \"Applicant information was found\", " + - "\"Reg (EC) No 1107/2009 Art. 63 (2g)\");\n" + - " section.redactBetween(\"No:\", \"Fax\", \"address\", 6,true, \"Applicant information was found\", " + - "\"Reg (EC) No 1107/2009 Art. 63 (2g)\");\n" + - " section.redactBetween(\"Contact:\", \"Tel.:\", \"address\", 6,true, \"Applicant information was found\", \"Reg (EC) No 1107/2009 Art. 63 (2g)\");\n" + - " end"; - when(rulesClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(RULES_VERSION.incrementAndGet()); - when(rulesClient.getRules(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(new RulesResponse(tableRules)); - droolsExecutionService.updateRules(TEST_DOSSIER_TEMPLATE_ID); - - ClassPathResource pdfFileResource = new ClassPathResource("files/Minimal Examples/Applicant Producer Table.pdf"); - when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); - DictionaryResponse dictionaryResponse = DictionaryResponse.builder() - .entries(toDictionaryEntry(new ArrayList<>(ResourceLoader.load("dictionaries/CBI_author.txt")))) - .build(); - when(dictionaryClient.getDictionaryForType(AUTHOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); - DictionaryResponse addressResponse = DictionaryResponse.builder() - .entries(toDictionaryEntry(new ArrayList<>(ResourceLoader.load("dictionaries/CBI_address.txt")))) - .build(); - when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); - DictionaryResponse sponsorResponse = DictionaryResponse.builder() - .entries(Collections.emptyList()) - .build(); - when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); - Document classifiedDoc = pdfSegmentationService.parseDocument(pdfFileResource.getInputStream()); - entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId", null); - assertThat(classifiedDoc.getEntities()).hasSize(1); // one page - assertThat(classifiedDoc.getEntities().get(1).stream() - .filter(entity -> entity.getMatchedRule() == 6) - .count()).isEqualTo(13); - - } - - - @Test - public void testSponsorInCell() throws IOException { - - String tableRules = "package drools\n" + - "\n" + - "import com.iqser.red.service.redaction.v1.server.redaction.model.Section\n" + - "\n" + - "global Section section\n" + "rule \"11: Redact sponsor company\"\n" + " when\n" + " " + - "Section(searchText.toLowerCase().contains(\"batches produced at\"))\n" + " then\n" + " section" + - ".redactIfPrecededBy(\"batches produced at\", \"sponsor\", 11, \"Redacted because it represents a " + - "sponsor company\", \"Reg (EC) No 1107/2009 Art. 63 (2g)\");\n" + " end"; - when(rulesClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(RULES_VERSION.incrementAndGet()); - when(rulesClient.getRules(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(new RulesResponse(tableRules)); - droolsExecutionService.updateRules(TEST_DOSSIER_TEMPLATE_ID); - - ClassPathResource pdfFileResource = new ClassPathResource("files/Minimal Examples/batches_new_line.pdf"); - when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); - DictionaryResponse addressResponse = DictionaryResponse.builder() - .entries(Collections.emptyList()) - .build(); - when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); - DictionaryResponse authorResponse = DictionaryResponse.builder() - .entries(Collections.emptyList()) - .build(); - when(dictionaryClient.getDictionaryForType(AUTHOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(authorResponse); - DictionaryResponse dictionaryResponse = DictionaryResponse.builder() - .entries(toDictionaryEntry(new ArrayList<>(ResourceLoader.load("dictionaries/CBI_sponsor.txt")))) - .build(); - when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); - Document classifiedDoc = pdfSegmentationService.parseDocument(pdfFileResource.getInputStream()); - entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId", null); - assertThat(classifiedDoc.getEntities()).hasSize(1); // one page - assertThat(classifiedDoc.getEntities().get(1).stream() - .filter(entity -> entity.getMatchedRule() == 11) - .count()).isEqualTo(1); - - } - - - @Test - public void headerPropagation() throws IOException { - - ClassPathResource pdfFileResource = new ClassPathResource("files/Minimal Examples/Header Propagation.pdf"); - - DictionaryResponse dictionaryResponse = DictionaryResponse.builder() - .entries(toDictionaryEntry(Arrays.asList("Bissig R.", "Thanei P."))) - .build(); - - when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); - when(dictionaryClient.getDictionaryForType(AUTHOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); - DictionaryResponse addressResponse = DictionaryResponse.builder() - .entries(toDictionaryEntry(Collections.singletonList("Novartis Crop Protection AG, Basel, Switzerland"))) - .build(); - when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); - - DictionaryResponse sponsorResponse = DictionaryResponse.builder() - .entries(Collections.emptyList()) - .build(); - when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); - Document classifiedDoc = pdfSegmentationService.parseDocument(pdfFileResource.getInputStream()); - entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId", null); - assertThat(classifiedDoc.getEntities()).hasSize(2); // two pages - assertThat(classifiedDoc.getEntities().get(1).stream().filter(entity -> entity.getMatchedRule() == 9).count()).isEqualTo(8); - assertThat(classifiedDoc.getEntities().get(2).stream().filter(entity -> entity.getMatchedRule() == 9).count()).isEqualTo(5); // 2 names, 1 address, 2 Y - - pdfFileResource = new ClassPathResource("files/Minimal Examples/Header Propagation2.pdf"); - - dictionaryResponse = DictionaryResponse.builder() - .entries(toDictionaryEntry(Arrays.asList("Tribolet, R.", "Muir, G.", "Kühne-Thu, H.", "Close, C."))) - .build(); - - when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); - when(dictionaryClient.getDictionaryForType(AUTHOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); - addressResponse = DictionaryResponse.builder() - .entries(toDictionaryEntry(Collections.singletonList("Novartis Crop Protection AG, Basel, Switzerland"))) - .build(); - when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); - - classifiedDoc = pdfSegmentationService.parseDocument(pdfFileResource.getInputStream()); - entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId", null); - assertThat(classifiedDoc.getEntities()).hasSize(1); // one page - assertThat(classifiedDoc.getEntities().get(1).stream().filter(entity -> entity.getMatchedRule() == 9).count()).isEqualTo(3); - assertThat(classifiedDoc.getEntities().get(1).stream().filter(entity -> entity.getMatchedRule() == 8).count()).isEqualTo(9); - } - - - @Test - @Ignore - public void testNGuideline() throws IOException { - - ClassPathResource pdfFileResource = new ClassPathResource("files/Minimal Examples/Empty Tabular Data.pdf"); - - DictionaryResponse dictionaryResponse = DictionaryResponse.builder() - .entries(toDictionaryEntry(Collections.singletonList("Aldershof S."))) - .build(); - - when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); - when(dictionaryClient.getDictionaryForType(AUTHOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); - DictionaryResponse addressResponse = DictionaryResponse.builder() - .entries(toDictionaryEntry(Collections.singletonList("Novartis Crop Protection AG, Basel, Switzerland"))) - .build(); - when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); - - DictionaryResponse sponsorResponse = DictionaryResponse.builder() - .entries(Collections.emptyList()) - .build(); - when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); - Document classifiedDoc = pdfSegmentationService.parseDocument(pdfFileResource.getInputStream()); - entityRedactionService.processDocument(classifiedDoc, TEST_DOSSIER_TEMPLATE_ID, null, "dossierId", null); - assertThat(classifiedDoc.getEntities()).hasSize(1); // one page - assertThat(classifiedDoc.getEntities().get(1).stream().filter(entity -> entity.getMatchedRule() == 8).count()).isEqualTo(6); - } - - - @Before - public void stubRedaction() { - String tableRules = "package drools\n" + - "\n" + - "import com.iqser.red.service.redaction.v1.server.redaction.model.Section\n" + - "\n" + - "global Section section\n" + - "rule \"8: Not redacted because Vertebrate Study = N\"\n" + - " when\n" + - " Section(rowEquals(\"Vertebrate study Y/N\", \"N\") || rowEquals(\"Vertebrate study Y/N\", \"No\"))\n" + - " then\n" + - " section.redactNotCell(\"Author(s)\", 8, \"name\", false, \"Not redacted because row is not a vertebrate study\");\n" + - " section.redactNot(\"address\", 8, \"Not redacted because row is not a vertebrate study\");\n" + - " section.highlightCell(\"Vertebrate study Y/N\", 8, \"hint_only\");\n" + - " end\n" + - "rule \"9: Redact Authors and Addresses in Reference Table, if it is a Vertebrate study\"\n" + - " when\n" + - " Section(rowEquals(\"Vertebrate study Y/N\", \"Y\") || rowEquals(\"Vertebrate study Y/N\", " + - "\"Yes\"))\n" + - " then\n" + - " section.redactCell(\"Author(s)\", 9, \"name\", false, \"Redacted because row is a vertebrate study\", \"Reg (EC) No 1107/2009 Art. 63 (2g)\");\n" + - " section.redact(\"address\", 9, \"Redacted because row is a vertebrate sgitudy\", \"Reg (EC) No" + - " 1107/2009 Art. 63 (2g)\");\n" + - " section.highlightCell(\"Vertebrate study Y/N\", 9, \"must_redact\");\n" + - " end"; - when(rulesClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(RULES_VERSION.incrementAndGet()); - when(rulesClient.getRules(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(new RulesResponse(tableRules)); - TypeResponse typeResponse = TypeResponse.builder() - .types(Arrays.asList( - TypeResult.builder().dossierTemplateId(TEST_DOSSIER_TEMPLATE_ID).type(AUTHOR_CODE).hexColor("#ffff00").build(), - TypeResult.builder().dossierTemplateId(TEST_DOSSIER_TEMPLATE_ID).type(ADDRESS_CODE).hexColor("#ff00ff").build(), - TypeResult.builder().dossierTemplateId(TEST_DOSSIER_TEMPLATE_ID).type(SPONSOR_CODE).hexColor("#00ffff").build())) - .build(); - when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(DICTIONARY_VERSION.incrementAndGet()); - when(dictionaryClient.getAllTypes(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(typeResponse); - - // Default empty return to prevent NPEs - DictionaryResponse dictionaryResponse = DictionaryResponse.builder() - .build(); - when(dictionaryClient.getDictionaryForType(AUTHOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(dictionaryResponse); - DictionaryResponse addressResponse = DictionaryResponse.builder() - .build(); - when(dictionaryClient.getDictionaryForType(ADDRESS_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(addressResponse); - DictionaryResponse sponsorResponse = DictionaryResponse.builder() - .build(); - when(dictionaryClient.getDictionaryForType(SPONSOR_CODE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(sponsorResponse); - - Colors colors = new Colors(); - colors.setDefaultColor("#acfc00"); - colors.setNotRedacted("#cccccc"); - colors.setRequestAdd("#04b093"); - colors.setRequestRemove("#04b093"); - - when(dictionaryClient.getColors(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(colors); - } - - - private static String loadFromClassPath(String path) { - - URL resource = ResourceLoader.class.getClassLoader().getResource(path); - if (resource == null) { - throw new IllegalArgumentException("could not load classpath resource: drools/rules.drl"); - } - try (BufferedReader br = new BufferedReader(new InputStreamReader(resource.openStream(), StandardCharsets.UTF_8))) { - StringBuilder sb = new StringBuilder(); - String str; - while ((str = br.readLine()) != null) { - sb.append(str).append("\n"); - } - return sb.toString(); - } catch (IOException e) { - throw new IllegalArgumentException("could not load classpath resource: " + path, e); - } - } - - private List toDictionaryEntry(List entries) { - List dictionaryEntries = new ArrayList<>(); - entries.forEach(entry -> { - dictionaryEntries.add(new DictionaryEntry(entry, 1L, false)); - }); - return dictionaryEntries; - } - -} diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/utils/EntitySearchUtilsTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/utils/EntitySearchUtilsTest.java new file mode 100644 index 00000000..544b334e --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/utils/EntitySearchUtilsTest.java @@ -0,0 +1,29 @@ +package com.iqser.red.service.redaction.v1.server.redaction.utils; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.Test; + +import com.iqser.red.service.redaction.v1.server.redaction.model.Entity; + +public class EntitySearchUtilsTest { + + @Test + public void testNestedEntitiesRemoval() { + + Set entities = new HashSet<>(); + Entity nested = new Entity("nested", "fake type", 10, 16, "fake headline", 0, false, false); + Entity nesting = new Entity("nesting nested", "fake type", 2, 16, "fake headline", 0, false, false); + entities.add(nested); + entities.add(nesting); + EntitySearchUtils.removeEntitiesContainedInLarger(entities); + + assertThat(entities.size()).isEqualTo(1); + assertThat(entities).contains(nesting); + + } + +} From f1b3d129ee32d40a78fad038a69027a24d8ccdd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Tue, 7 Sep 2021 09:37:38 +0200 Subject: [PATCH 73/80] RED-1970: Call entity-redaction-service once per document --- .../redaction-service-server-v1/pom.xml | 2 +- .../client/EntityRecognitionClient.java | 3 +- ...ognitionResponse.java => NerEntities.java} | 4 +- .../queue/RedactionMessageReceiver.java | 5 ++ .../redaction/service/AnalyzeService.java | 19 +++++-- .../service/EntityRedactionService.java | 43 ++++++---------- .../redaction/service/NerAnalyserService.java | 49 +++++++++++++++++++ .../storage/RedactionStorageService.java | 20 ++++++++ .../src/test/resources/application.yml | 2 +- 9 files changed, 111 insertions(+), 36 deletions(-) rename redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/model/{EntityRecognitionResponse.java => NerEntities.java} (72%) create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/NerAnalyserService.java diff --git a/redaction-service-v1/redaction-service-server-v1/pom.xml b/redaction-service-v1/redaction-service-server-v1/pom.xml index 2bf40817..67e97d07 100644 --- a/redaction-service-v1/redaction-service-server-v1/pom.xml +++ b/redaction-service-v1/redaction-service-server-v1/pom.xml @@ -24,7 +24,7 @@ com.iqser.red.service file-management-service-api-v1 - 2.25.0 + 2.96.0 com.iqser.red.service diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/EntityRecognitionClient.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/EntityRecognitionClient.java index 589e6dc3..336b64f8 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/EntityRecognitionClient.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/EntityRecognitionClient.java @@ -9,10 +9,11 @@ import org.springframework.web.bind.annotation.PostMapping; import com.iqser.red.service.redaction.v1.server.client.model.EntityRecogintionEntity; import com.iqser.red.service.redaction.v1.server.client.model.EntityRecognitionRequest; +import com.iqser.red.service.redaction.v1.server.client.model.NerEntities; @FeignClient(name = "EntityRecognitionClient", url = "${entity-recognition-service.url}") public interface EntityRecognitionClient { @PostMapping(value = "/find_authors", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) - Map>> findAuthors(EntityRecognitionRequest entityRecognitionRequest); + NerEntities findAuthors(EntityRecognitionRequest entityRecognitionRequest); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/model/EntityRecognitionResponse.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/model/NerEntities.java similarity index 72% rename from redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/model/EntityRecognitionResponse.java rename to redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/model/NerEntities.java index af0d6beb..f45a55a5 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/model/EntityRecognitionResponse.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/model/NerEntities.java @@ -13,9 +13,9 @@ import lombok.NoArgsConstructor; @Builder @AllArgsConstructor @NoArgsConstructor -public class EntityRecognitionResponse { +public class NerEntities { @Builder.Default - private Map> result = new HashMap<>(); + private Map> result = new HashMap<>(); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/queue/RedactionMessageReceiver.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/queue/RedactionMessageReceiver.java index b4aed110..adfb6323 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/queue/RedactionMessageReceiver.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/queue/RedactionMessageReceiver.java @@ -7,6 +7,7 @@ import com.iqser.red.service.redaction.v1.model.AnalyzeResult; import com.iqser.red.service.redaction.v1.model.StructureAnalyzeRequest; import com.iqser.red.service.redaction.v1.server.client.FileStatusProcessingUpdateClient; import com.iqser.red.service.redaction.v1.server.redaction.service.AnalyzeService; +import com.iqser.red.service.redaction.v1.server.redaction.service.NerAnalyserService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -26,6 +27,7 @@ public class RedactionMessageReceiver { private final ObjectMapper objectMapper; private final AnalyzeService analyzeService; private final FileStatusProcessingUpdateClient fileStatusProcessingUpdateClient; + private final NerAnalyserService nerAnalyserService; @RabbitHandler @@ -43,6 +45,9 @@ public class RedactionMessageReceiver { // TODO Seperate stucture analysis by other queue analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(analyzeRequest.getDossierId(), analyzeRequest.getFileId())); + // TODO NerEntities should be computed and stored in entity-recognition-service, should be triggered by a seperate queue after structure analysis + nerAnalyserService.computeNerEntities(analyzeRequest.getDossierId(), analyzeRequest.getFileId()); + result = analyzeService.analyze(analyzeRequest); log.info("Successfully analyzed dossier {} file {} took: {}", analyzeRequest.getDossierId(), analyzeRequest.getFileId(), result .getDuration()); diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeService.java index 1e257ac6..c9dc03c8 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeService.java @@ -60,6 +60,7 @@ public class AnalyzeService { private final RedactionServiceSettings redactionServiceSettings; private final SectionTextBuilderService sectionTextBuilderService; private final SectionGridCreatorService sectionGridCreatorService; + private final NerAnalyserService nerAnalyserService; public void analyzeDocumentStructure(StructureAnalyzeRequest analyzeRequest) { @@ -81,7 +82,8 @@ public class AnalyzeService { List sectionTexts = sectionTextBuilderService.buildSectionText(classifiedDoc); sectionGridCreatorService.createSectionGrid(classifiedDoc, pageCount); - redactionStorageService.storeObject(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), FileType.TEXT, new Text(pageCount, sectionTexts)); + Text text = new Text(pageCount, sectionTexts); + redactionStorageService.storeObject(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), FileType.TEXT, text); redactionStorageService.storeObject(analyzeRequest.getDossierId(), analyzeRequest.getFileId(), FileType.SECTION_GRID, classifiedDoc .getSectionGrid()); @@ -94,6 +96,11 @@ public class AnalyzeService { long startTime = System.currentTimeMillis(); var text = redactionStorageService.getText(analyzeRequest.getDossierId(), analyzeRequest.getFileId()); + var nerEntities = redactionStorageService.getNerEntities(analyzeRequest.getDossierId(), analyzeRequest.getFileId()); + if(redactionServiceSettings.isEnableEntityRecognition() && nerEntities == null){ + nerAnalyserService.computeNerEntities(analyzeRequest.getDossierId(), analyzeRequest.getFileId()); + nerEntities = redactionStorageService.getNerEntities(analyzeRequest.getDossierId(), analyzeRequest.getFileId()); + } dictionaryService.updateDictionary(analyzeRequest.getDossierTemplateId(), analyzeRequest.getDossierId()); KieContainer kieContainer = droolsExecutionService.updateRules(analyzeRequest.getDossierTemplateId()); @@ -101,7 +108,7 @@ public class AnalyzeService { Dictionary dictionary = dictionaryService.getDeepCopyDictionary(analyzeRequest.getDossierTemplateId(), analyzeRequest .getDossierId()); - PageEntities pageEntities = entityRedactionService.findEntities(dictionary, text.getSectionTexts(), kieContainer, analyzeRequest); + PageEntities pageEntities = entityRedactionService.findEntities(dictionary, text.getSectionTexts(), kieContainer, analyzeRequest, nerEntities); dictionaryService.updateExternalDictionary(dictionary, analyzeRequest.getDossierTemplateId()); @@ -141,6 +148,12 @@ public class AnalyzeService { return finalizeAnalysis(analyzeRequest, startTime, redactionLog, text, dictionaryIncrement.getDictionaryVersion(), true); } + var nerEntities = redactionStorageService.getNerEntities(analyzeRequest.getDossierId(), analyzeRequest.getFileId()); + if(redactionServiceSettings.isEnableEntityRecognition() && nerEntities == null){ + nerAnalyserService.computeNerEntities(analyzeRequest.getDossierId(), analyzeRequest.getFileId()); + nerEntities = redactionStorageService.getNerEntities(analyzeRequest.getDossierId(), analyzeRequest.getFileId()); + } + List reanalysisSections = text.getSectionTexts() .stream() .filter(sectionText -> sectionsToReanalyse.contains(sectionText.getSectionNumber())) @@ -151,7 +164,7 @@ public class AnalyzeService { Dictionary dictionary = dictionaryService.getDeepCopyDictionary(analyzeRequest.getDossierTemplateId(), analyzeRequest .getDossierId()); - PageEntities pageEntities = entityRedactionService.findEntities(dictionary, reanalysisSections, kieContainer, analyzeRequest); + PageEntities pageEntities = entityRedactionService.findEntities(dictionary, reanalysisSections, kieContainer, analyzeRequest, nerEntities); var newRedactionLogEntries = redactionLogCreatorService.createRedactionLog(pageEntities, text.getNumberOfPages(), analyzeRequest .getDossierTemplateId()); diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java index 0c302dcc..7a2f95f7 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java @@ -1,5 +1,6 @@ package com.iqser.red.service.redaction.v1.server.redaction.service; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -18,9 +19,8 @@ import com.iqser.red.service.redaction.v1.model.ManualImageRecategorization; import com.iqser.red.service.redaction.v1.model.Status; import com.iqser.red.service.redaction.v1.server.classification.model.SectionText; import com.iqser.red.service.redaction.v1.server.client.EntityRecognitionClient; -import com.iqser.red.service.redaction.v1.server.client.model.EntityRecogintionEntity; import com.iqser.red.service.redaction.v1.server.client.model.EntityRecognitionRequest; -import com.iqser.red.service.redaction.v1.server.client.model.EntityRecognitionResponse; +import com.iqser.red.service.redaction.v1.server.client.model.NerEntities; import com.iqser.red.service.redaction.v1.server.client.model.EntityRecognitionSection; import com.iqser.red.service.redaction.v1.server.redaction.model.Dictionary; import com.iqser.red.service.redaction.v1.server.redaction.model.DictionaryModel; @@ -50,14 +50,14 @@ public class EntityRedactionService { public PageEntities findEntities(Dictionary dictionary, List sectionTexts, KieContainer kieContainer, - AnalyzeRequest analyzeRequest) { + AnalyzeRequest analyzeRequest, NerEntities nerEntities) { Map> imagesPerPage = new HashMap<>(); - Set entities = findEntities(sectionTexts, dictionary, kieContainer, analyzeRequest, false, null, imagesPerPage); + Set entities = findEntities(sectionTexts, dictionary, kieContainer, analyzeRequest, false, null, imagesPerPage, nerEntities); if (dictionary.hasLocalEntries()) { Map> hintsPerSectionNumber = getHintsPerSection(entities, dictionary); - Set foundByLocal = findEntities(sectionTexts, dictionary, kieContainer, analyzeRequest, true, hintsPerSectionNumber, imagesPerPage); + Set foundByLocal = findEntities(sectionTexts, dictionary, kieContainer, analyzeRequest, true, hintsPerSectionNumber, imagesPerPage, nerEntities); EntitySearchUtils.addEntitiesWithHigherRank(entities, foundByLocal, dictionary); EntitySearchUtils.removeEntitiesContainedInLarger(entities); } @@ -70,13 +70,13 @@ public class EntityRedactionService { public Set findEntities(List reanalysisSections, Dictionary dictionary, KieContainer kieContainer, AnalyzeRequest analyzeRequest, boolean local, Map> hintsPerSectionNumber, - Map> imagesPerPage) { + Map> imagesPerPage, NerEntities nerEntities) { List sectionSearchableTextPairs = new ArrayList<>(); for (SectionText reanalysisSection : reanalysisSections) { Set entities = findEntities(reanalysisSection.getSearchableText(), reanalysisSection.getHeadline(), reanalysisSection - .getSectionNumber(), dictionary, local); + .getSectionNumber(), dictionary, local, nerEntities); if (reanalysisSection.getCellStarts() != null && !reanalysisSection.getCellStarts().isEmpty()) { surroundingWordsService.addSurroundingText(entities, reanalysisSection.getSearchableText(), dictionary, reanalysisSection .getCellStarts()); @@ -124,7 +124,7 @@ public class EntityRedactionService { EntitySearchUtils.removeEntitiesContainedInLarger(analysedSection.getEntities()); entities.addAll(analysedSection.getEntities()); - if(!local) { + if (!local) { for (Image image : analysedSection.getImages()) { imagesPerPage.computeIfAbsent(image.getPage(), (a) -> new HashSet<>()).add(image); } @@ -198,7 +198,7 @@ public class EntityRedactionService { private Set findEntities(SearchableText searchableText, String headline, int sectionNumber, - Dictionary dictionary, boolean local) { + Dictionary dictionary, boolean local, NerEntities nerEntities) { Set found = new HashSet<>(); String searchableString = searchableText.toString(); @@ -217,35 +217,22 @@ public class EntityRedactionService { } } - if (redactionServiceSettings.isEnableEntityRecognition() && !local) { - found.addAll(getAiEntities(sectionNumber, searchableString, headline)); + if (!local) { + addNerEntities(found, sectionNumber, headline, nerEntities); } return EntitySearchUtils.clearAndFindPositions(found, searchableText, dictionary); } - private Set getAiEntities(int sectionNumber, String searchableString, String headline) { + private void addNerEntities(Set found, int sectionNumber, String headline, NerEntities nerEntities) { - Set found = new HashSet<>(); - - Map>> response = entityRecognitionClient.findAuthors(EntityRecognitionRequest - .builder() - .data(List.of(EntityRecognitionSection.builder() - .sectionNumber(sectionNumber) - .text(searchableString) - .build())) - .build()); - - EntityRecognitionResponse entityRecognitionResponse = new EntityRecognitionResponse(response.get("result:")); - - if (entityRecognitionResponse.getResult() != null && entityRecognitionResponse.getResult() - .containsKey(String.valueOf(sectionNumber))) { - entityRecognitionResponse.getResult().get(String.valueOf(sectionNumber)).forEach(res -> { + if (redactionServiceSettings.isEnableEntityRecognition() && nerEntities.getResult() + .containsKey(sectionNumber)) { + nerEntities.getResult().get(sectionNumber).forEach(res -> { found.add(new Entity(res.getValue(), res.getType(), res.getStartOffset(), res.getEndOffset(), headline, sectionNumber, false, false)); }); } - return found; } } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/NerAnalyserService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/NerAnalyserService.java new file mode 100644 index 00000000..df717214 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/NerAnalyserService.java @@ -0,0 +1,49 @@ +package com.iqser.red.service.redaction.v1.server.redaction.service; + +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; + +import com.iqser.red.service.file.management.v1.api.model.FileType; +import com.iqser.red.service.redaction.v1.server.client.EntityRecognitionClient; +import com.iqser.red.service.redaction.v1.server.client.model.EntityRecognitionRequest; +import com.iqser.red.service.redaction.v1.server.client.model.EntityRecognitionSection; +import com.iqser.red.service.redaction.v1.server.settings.RedactionServiceSettings; +import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class NerAnalyserService { + + private final RedactionStorageService redactionStorageService; + private final EntityRecognitionClient entityRecognitionClient; + private final RedactionServiceSettings redactionServiceSettings; + + public void computeNerEntities(String dossierId, String fileId) { + + if (redactionServiceSettings.isEnableEntityRecognition()) { + var text = redactionStorageService.getText(dossierId, fileId); + + long start = System.currentTimeMillis(); + + var nerRequest = EntityRecognitionRequest.builder() + .data(text.getSectionTexts() + .stream() + .map(sectionText -> new EntityRecognitionSection(sectionText.getSectionNumber(), sectionText + .getText())) + .collect(Collectors.toList())) + .build(); + + var nerResponse = entityRecognitionClient.findAuthors(nerRequest); + + log.info("Computing NER entities took: {} ms for dossierId {} and fileId {}", System.currentTimeMillis() - start, dossierId, fileId); + + redactionStorageService.storeObject(dossierId, fileId, FileType.NER_ENTITIES, nerResponse); + } + } + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/storage/RedactionStorageService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/storage/RedactionStorageService.java index 021beeff..503b3a84 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/storage/RedactionStorageService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/storage/RedactionStorageService.java @@ -5,6 +5,7 @@ import com.iqser.red.service.file.management.v1.api.model.FileType; import com.iqser.red.service.redaction.v1.model.RedactionLog; import com.iqser.red.service.redaction.v1.model.SectionGrid; import com.iqser.red.service.redaction.v1.server.classification.model.Text; +import com.iqser.red.service.redaction.v1.server.client.model.NerEntities; import com.iqser.red.storage.commons.exception.StorageObjectDoesNotExist; import com.iqser.red.storage.commons.service.StorageService; import lombok.Getter; @@ -73,6 +74,25 @@ public class RedactionStorageService { } + public NerEntities getNerEntities(String dossierId, String fileId) { + + InputStreamResource inputStreamResource; + try { + inputStreamResource = storageService.getObject(StorageIdUtils.getStorageId(dossierId, fileId, FileType.NER_ENTITIES)); + } catch (StorageObjectDoesNotExist e) { + log.debug("NER Entities not available."); + return null; + } + + try { + return objectMapper.readValue(inputStreamResource.getInputStream(), NerEntities.class); + } catch (IOException e) { + throw new RuntimeException("Could not convert NerEntities", e); + } + } + + + public SectionGrid getSectionGrid(String dossierId, String fileId) { var sectionGrid = storageService.getObject(StorageIdUtils.getStorageId(dossierId, fileId, FileType.SECTION_GRID)); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/resources/application.yml b/redaction-service-v1/redaction-service-server-v1/src/test/resources/application.yml index 72e05696..d8543d66 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/resources/application.yml +++ b/redaction-service-v1/redaction-service-server-v1/src/test/resources/application.yml @@ -1,7 +1,7 @@ configuration-service.url: "http://configuration-service-v1:8080" image-service.url: "http://image-service-v1:8080" file-management-service.url: "http://file-management-service-v1:8080" -entity-recognition-service.url: "http://entity-recognition-service-v1:8080" +entity-recognition-service.url: "localhost:8080" ribbon: ConnectTimeout: 600000 From 8de655d8848c05de3ce5d7a19ad0887d516bcb72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Tue, 7 Sep 2021 10:36:11 +0200 Subject: [PATCH 74/80] RED-1970: Base64 encode text in communication with entity-recogintion-service --- .../v1/server/redaction/service/EntityRedactionService.java | 3 ++- .../v1/server/redaction/service/NerAnalyserService.java | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java index 7a2f95f7..d57156bd 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java @@ -10,6 +10,7 @@ import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.StringUtils; import org.kie.api.runtime.KieContainer; import org.springframework.stereotype.Service; @@ -230,7 +231,7 @@ public class EntityRedactionService { if (redactionServiceSettings.isEnableEntityRecognition() && nerEntities.getResult() .containsKey(sectionNumber)) { nerEntities.getResult().get(sectionNumber).forEach(res -> { - found.add(new Entity(res.getValue(), res.getType(), res.getStartOffset(), res.getEndOffset(), headline, sectionNumber, false, false)); + found.add(new Entity(new String(Base64.decodeBase64(res.getValue().getBytes())), res.getType(), res.getStartOffset(), res.getEndOffset(), headline, sectionNumber, false, false)); }); } } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/NerAnalyserService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/NerAnalyserService.java index df717214..b7611b0d 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/NerAnalyserService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/NerAnalyserService.java @@ -2,6 +2,7 @@ package com.iqser.red.service.redaction.v1.server.redaction.service; import java.util.stream.Collectors; +import org.apache.commons.codec.binary.Base64; import org.springframework.stereotype.Service; import com.iqser.red.service.file.management.v1.api.model.FileType; @@ -33,8 +34,9 @@ public class NerAnalyserService { var nerRequest = EntityRecognitionRequest.builder() .data(text.getSectionTexts() .stream() - .map(sectionText -> new EntityRecognitionSection(sectionText.getSectionNumber(), sectionText - .getText())) + .map(sectionText -> new EntityRecognitionSection(sectionText.getSectionNumber(), new String(Base64 + .encodeBase64(sectionText + .getText().getBytes())))) .collect(Collectors.toList())) .build(); From 7a3924d96ed40c932cfe02fb410060d4e820f88f Mon Sep 17 00:00:00 2001 From: Timo Bejan Date: Tue, 7 Sep 2021 15:25:47 +0300 Subject: [PATCH 75/80] rule builder model --- .../service/redaction/v1/model/Argument.java | 15 ++ .../redaction/v1/model/ArgumentType.java | 7 + .../redaction/v1/model/RuleBuilderModel.java | 14 + .../redaction/v1/model/RuleElement.java | 18 ++ .../v1/resources/RuleBuilderResource.java | 12 + .../controller/RuleBuilderController.java | 21 ++ .../v1/server/redaction/model/Section.java | 248 ++++++++++++------ .../rulebuilder/RuleBuilderModelService.java | 36 +++ .../RuleBuilderModelServiceTest.java | 18 ++ 9 files changed, 315 insertions(+), 74 deletions(-) create mode 100644 redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/Argument.java create mode 100644 redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ArgumentType.java create mode 100644 redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RuleBuilderModel.java create mode 100644 redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RuleElement.java create mode 100644 redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/resources/RuleBuilderResource.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RuleBuilderController.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/rulebuilder/RuleBuilderModelService.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/rulebuilder/RuleBuilderModelServiceTest.java diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/Argument.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/Argument.java new file mode 100644 index 00000000..fc4ff45e --- /dev/null +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/Argument.java @@ -0,0 +1,15 @@ +package com.iqser.red.service.redaction.v1.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Argument { + + private String name; + private ArgumentType type; + +} diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ArgumentType.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ArgumentType.java new file mode 100644 index 00000000..2c05e253 --- /dev/null +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ArgumentType.java @@ -0,0 +1,7 @@ +package com.iqser.red.service.redaction.v1.model; + +public enum ArgumentType { + + INTEGER, BOOLEAN, STRING, FILE_ATTRIBUTE, REGEX, TYPE, RULE_NUMBER, LEGAL_BASIS + +} diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RuleBuilderModel.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RuleBuilderModel.java new file mode 100644 index 00000000..37664151 --- /dev/null +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RuleBuilderModel.java @@ -0,0 +1,14 @@ +package com.iqser.red.service.redaction.v1.model; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class RuleBuilderModel { + + private List whenClauses = new ArrayList<>(); + private List thenConditions = new ArrayList<>(); + +} diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RuleElement.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RuleElement.java new file mode 100644 index 00000000..21bd59b1 --- /dev/null +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RuleElement.java @@ -0,0 +1,18 @@ +package com.iqser.red.service.redaction.v1.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class RuleElement { + + private String conditionName; + private List arguments = new ArrayList<>(); + +} diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/resources/RuleBuilderResource.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/resources/RuleBuilderResource.java new file mode 100644 index 00000000..d65cdab5 --- /dev/null +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/resources/RuleBuilderResource.java @@ -0,0 +1,12 @@ +package com.iqser.red.service.redaction.v1.resources; + +import com.iqser.red.service.redaction.v1.model.RuleBuilderModel; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; + +public interface RuleBuilderResource { + + @PostMapping(value = "/rule-builder-model", produces = MediaType.APPLICATION_JSON_VALUE) + RuleBuilderModel getRuleBuilderModel(); + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RuleBuilderController.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RuleBuilderController.java new file mode 100644 index 00000000..c30f7363 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RuleBuilderController.java @@ -0,0 +1,21 @@ +package com.iqser.red.service.redaction.v1.server.controller; + + +import com.iqser.red.service.redaction.v1.model.RuleBuilderModel; +import com.iqser.red.service.redaction.v1.resources.RuleBuilderResource; +import com.iqser.red.service.redaction.v1.server.redaction.rulebuilder.RuleBuilderModelService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class RuleBuilderController implements RuleBuilderResource { + + private final RuleBuilderModelService ruleBuilderModelService; + + @Override + public RuleBuilderModel getRuleBuilderModel() { + return ruleBuilderModelService.getRuleBuilderModel(); + } + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Section.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Section.java index 17209efb..929e4110 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Section.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Section.java @@ -1,5 +1,6 @@ package com.iqser.red.service.redaction.v1.server.redaction.model; +import com.iqser.red.service.redaction.v1.model.ArgumentType; import com.iqser.red.service.redaction.v1.model.FileAttribute; import com.iqser.red.service.redaction.v1.server.classification.model.TextBlock; import com.iqser.red.service.redaction.v1.server.redaction.utils.EntitySearchUtils; @@ -9,13 +10,11 @@ import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -59,33 +58,45 @@ public class Section { private List fileAttributes = new ArrayList<>(); - public boolean fileAttributeByIdEquals(String id, String value){ + @WhenCondition + public boolean fileAttributeByIdEquals(@Argument(ArgumentType.FILE_ATTRIBUTE) String id, + @Argument(ArgumentType.STRING) String value) { return fileAttributes != null && fileAttributes.stream().filter(attribute -> id.equals(attribute.getId()) && value.equals(attribute.getValue())).findFirst().isPresent(); } - public boolean fileAttributeByPlaceholderEquals(String placeholder, String value){ + @WhenCondition + public boolean fileAttributeByPlaceholderEquals(@Argument(ArgumentType.FILE_ATTRIBUTE) String placeholder, + @Argument(ArgumentType.STRING) String value) { return fileAttributes != null && fileAttributes.stream().filter(attribute -> placeholder.equals(attribute.getPlaceholder()) && value.equals(attribute.getValue())).findFirst().isPresent(); } - public boolean fileAttributeByLabelEquals(String label, String value){ + @WhenCondition + public boolean fileAttributeByLabelEquals(@Argument(ArgumentType.FILE_ATTRIBUTE) String label, + @Argument(ArgumentType.STRING) String value) { return fileAttributes != null && fileAttributes.stream().filter(attribute -> label.equals(attribute.getLabel()) && value.equals(attribute.getValue())).findFirst().isPresent(); } - - public boolean fileAttributeByIdEqualsIgnoreCase(String id, String value){ + @WhenCondition + public boolean fileAttributeByIdEqualsIgnoreCase(@Argument(ArgumentType.FILE_ATTRIBUTE) String id, + @Argument(ArgumentType.STRING) String value) { return fileAttributes != null && fileAttributes.stream().filter(attribute -> id.equals(attribute.getId()) && value.equalsIgnoreCase(attribute.getValue())).findFirst().isPresent(); } - public boolean fileAttributeByPlaceholderEqualsIgnoreCase(String placeholder, String value){ + @WhenCondition + public boolean fileAttributeByPlaceholderEqualsIgnoreCase(@Argument(ArgumentType.FILE_ATTRIBUTE) String placeholder, + @Argument(ArgumentType.STRING) String value) { return fileAttributes != null && fileAttributes.stream().filter(attribute -> placeholder.equals(attribute.getPlaceholder()) && value.equalsIgnoreCase(attribute.getValue())).findFirst().isPresent(); } - public boolean fileAttributeByLabelEqualsIgnoreCase(String label, String value){ + @WhenCondition + public boolean fileAttributeByLabelEqualsIgnoreCase(@Argument(ArgumentType.FILE_ATTRIBUTE) String label, + @Argument(ArgumentType.STRING) String value) { return fileAttributes != null && fileAttributes.stream().filter(attribute -> label.equals(attribute.getLabel()) && value.equalsIgnoreCase(attribute.getValue())).findFirst().isPresent(); } - - public boolean rowEquals(String headerName, String value) { + @WhenCondition + public boolean rowEquals(@Argument(ArgumentType.STRING) String headerName, + @Argument(ArgumentType.STRING) String value) { String cleanHeaderName = headerName.replaceAll("\n", "").replaceAll(" ", "").replaceAll("-", ""); @@ -94,33 +105,36 @@ public class Section { .equals(value); } - - public boolean hasTableHeader(String headerName) { + @WhenCondition + public boolean hasTableHeader(@Argument(ArgumentType.STRING) String headerName) { String cleanHeaderName = headerName.replaceAll("\n", "").replaceAll(" ", "").replaceAll("-", ""); return tabularData != null && tabularData.containsKey(cleanHeaderName); } - - public boolean matchesType(String type) { + @WhenCondition + public boolean matchesType(@Argument(ArgumentType.TYPE) String type) { return entities.stream().anyMatch(entity -> entity.getType().equals(type)); } - - public boolean matchesImageType(String type) { + @WhenCondition + public boolean matchesImageType(@Argument(ArgumentType.TYPE) String type) { return images.stream().anyMatch(image -> image.getType().equals(type)); } - - public boolean headlineContainsWord(String word) { + @WhenCondition + public boolean headlineContainsWord(@Argument(ArgumentType.STRING) String word) { return StringUtils.containsIgnoreCase(headline, word); } - - public void expandByRegEx(String type, String pattern, boolean patternCaseInsensitive, int group) { + @ThenAction + public void expandByRegEx(@Argument(ArgumentType.TYPE) String type, + @Argument(ArgumentType.REGEX) String pattern, + @Argument(ArgumentType.BOOLEAN) boolean patternCaseInsensitive, + @Argument(ArgumentType.INTEGER) int group) { Pattern compiledPattern = Patterns.getCompiledPattern(pattern, patternCaseInsensitive); @@ -147,8 +161,11 @@ public class Section { EntitySearchUtils.removeEntitiesContainedInLarger(entities); } - - public void redactImage(String type, int ruleNumber, String reason, String legalBasis) { + @ThenAction + public void redactImage(@Argument(ArgumentType.TYPE) String type, + @Argument(ArgumentType.RULE_NUMBER) int ruleNumber, + @Argument(ArgumentType.STRING) String reason, + @Argument(ArgumentType.LEGAL_BASIS) String legalBasis) { images.forEach(image -> { if (image.getType().equals(type)) { @@ -160,8 +177,11 @@ public class Section { }); } - - public void redact(String type, int ruleNumber, String reason, String legalBasis) { + @ThenAction + public void redact(@Argument(ArgumentType.TYPE) String type, + @Argument(ArgumentType.RULE_NUMBER) int ruleNumber, + @Argument(ArgumentType.STRING) String reason, + @Argument(ArgumentType.LEGAL_BASIS) String legalBasis) { boolean hasRecommendationDictionary = dictionaryTypes.contains(RECOMMENDATION_PREFIX + type); @@ -176,8 +196,10 @@ public class Section { }); } - - public void redactNotImage(String type, int ruleNumber, String reason) { + @ThenAction + public void redactNotImage(@Argument(ArgumentType.TYPE) String type, + @Argument(ArgumentType.RULE_NUMBER) int ruleNumber, + @Argument(ArgumentType.STRING) String reason) { images.forEach(image -> { if (image.getType().equals(type)) { @@ -188,8 +210,10 @@ public class Section { }); } - - public void redactNot(String type, int ruleNumber, String reason) { + @ThenAction + public void redactNot(@Argument(ArgumentType.TYPE) String type, + @Argument(ArgumentType.RULE_NUMBER) int ruleNumber, + @Argument(ArgumentType.STRING) String reason) { boolean hasRecommendationDictionary = dictionaryTypes.contains(RECOMMENDATION_PREFIX + type); @@ -203,9 +227,12 @@ public class Section { }); } - - public void expandToHintAnnotationByRegEx(String type, String pattern, boolean patternCaseInsensitive, int group, - String asType) { + @ThenAction + public void expandToHintAnnotationByRegEx(@Argument(ArgumentType.TYPE) String type, + @Argument(ArgumentType.STRING) String pattern, + @Argument(ArgumentType.BOOLEAN) boolean patternCaseInsensitive, + @Argument(ArgumentType.INTEGER) int group, + @Argument(ArgumentType.TYPE) String asType) { Pattern compiledPattern = Patterns.getCompiledPattern(pattern, patternCaseInsensitive); @@ -230,8 +257,11 @@ public class Section { EntitySearchUtils.removeEntitiesContainedInLarger(entities); } - - public void addHintAnnotationByRegEx(String pattern, boolean patternCaseInsensitive, int group, String asType) { + @ThenAction + public void addHintAnnotationByRegEx(@Argument(ArgumentType.REGEX) String pattern, + @Argument(ArgumentType.BOOLEAN) boolean patternCaseInsensitive, + @Argument(ArgumentType.INTEGER) int group, + @Argument(ArgumentType.TYPE) String asType) { Pattern compiledPattern = Patterns.getCompiledPattern(pattern, patternCaseInsensitive); @@ -246,8 +276,12 @@ public class Section { } } - - public void redactIfPrecededBy(String prefix, String type, int ruleNumber, String reason, String legalBasis) { + @ThenAction + public void redactIfPrecededBy(@Argument(ArgumentType.STRING) String prefix, + @Argument(ArgumentType.TYPE) String type, + @Argument(ArgumentType.RULE_NUMBER) int ruleNumber, + @Argument(ArgumentType.STRING) String reason, + @Argument(ArgumentType.LEGAL_BASIS) String legalBasis) { entities.forEach(entity -> { if (entity.getType().equals(type) && searchText.indexOf(prefix + entity.getWord()) != 1) { @@ -259,23 +293,32 @@ public class Section { }); } - - public void addHintAnnotation(String value, String asType) { + @ThenAction + public void addHintAnnotation(@Argument(ArgumentType.STRING) String value, + @Argument(ArgumentType.TYPE) String asType) { Set found = findEntities(value.trim(), asType, true, false, 0, null, null); EntitySearchUtils.addEntitiesIgnoreRank(entities, found); } - - public void addRedaction(String value, String asType, int ruleNumber, String reason, String legalBasis) { + @ThenAction + public void addRedaction(@Argument(ArgumentType.STRING) String value, + @Argument(ArgumentType.TYPE) String asType, + @Argument(ArgumentType.RULE_NUMBER) int ruleNumber, + @Argument(ArgumentType.STRING) String reason, + @Argument(ArgumentType.LEGAL_BASIS) String legalBasis) { Set found = findEntities(value.trim(), asType, true, true, ruleNumber, reason, legalBasis); EntitySearchUtils.addEntitiesIgnoreRank(entities, found); } - - public void redactLineAfter(String start, String asType, int ruleNumber, boolean redactEverywhere, String reason, - String legalBasis) { + @ThenAction + public void redactLineAfter(@Argument(ArgumentType.STRING) String start, + @Argument(ArgumentType.TYPE) String asType, + @Argument(ArgumentType.RULE_NUMBER) int ruleNumber, + @Argument(ArgumentType.BOOLEAN) boolean redactEverywhere, + @Argument(ArgumentType.STRING) String reason, + @Argument(ArgumentType.LEGAL_BASIS) String legalBasis) { String[] values = StringUtils.substringsBetween(text, start, "\n"); @@ -293,8 +336,9 @@ public class Section { } } - - public void recommendLineAfter(String start, String asType) { + @ThenAction + public void recommendLineAfter(@Argument(ArgumentType.STRING) String start, + @Argument(ArgumentType.TYPE) String asType) { String[] values = StringUtils.substringsBetween(text, start, "\n"); @@ -317,9 +361,14 @@ public class Section { } } - - public void redactByRegEx(String pattern, boolean patternCaseInsensitive, int group, String asType, int ruleNumber, - String reason, String legalBasis) { + @ThenAction + public void redactByRegEx(@Argument(ArgumentType.REGEX) String pattern, + @Argument(ArgumentType.BOOLEAN) boolean patternCaseInsensitive, + @Argument(ArgumentType.INTEGER) int group, + @Argument(ArgumentType.TYPE) String asType, + @Argument(ArgumentType.RULE_NUMBER) int ruleNumber, + @Argument(ArgumentType.STRING) String reason, + @Argument(ArgumentType.LEGAL_BASIS) String legalBasis) { Pattern compiledPattern = Patterns.getCompiledPattern(pattern, patternCaseInsensitive); @@ -334,8 +383,11 @@ public class Section { } } - - public void addRecommendationByRegEx(String pattern, boolean patternCaseInsensitive, int group, String asType) { + @ThenAction + public void addRecommendationByRegEx(@Argument(ArgumentType.REGEX) String pattern, + @Argument(ArgumentType.BOOLEAN) boolean patternCaseInsensitive, + @Argument(ArgumentType.INTEGER) int group, + @Argument(ArgumentType.TYPE) String asType) { Pattern compiledPattern = Patterns.getCompiledPattern(pattern, patternCaseInsensitive); @@ -349,9 +401,14 @@ public class Section { } } - - public void redactAndRecommendByRegEx(String pattern, boolean patternCaseInsensitive, int group, String asType, - int ruleNumber, String reason, String legalBasis) { + @ThenAction + public void redactAndRecommendByRegEx(@Argument(ArgumentType.REGEX) String pattern, + @Argument(ArgumentType.BOOLEAN) boolean patternCaseInsensitive, + @Argument(ArgumentType.INTEGER) int group, + @Argument(ArgumentType.TYPE) String asType, + @Argument(ArgumentType.RULE_NUMBER) int ruleNumber, + @Argument(ArgumentType.STRING) String reason, + @Argument(ArgumentType.LEGAL_BASIS) String legalBasis) { Pattern compiledPattern = Patterns.getCompiledPattern(pattern, patternCaseInsensitive); @@ -366,9 +423,14 @@ public class Section { } } - - public void redactBetween(String start, String stop, String asType, int ruleNumber, boolean redactEverywhere, - String reason, String legalBasis) { + @ThenAction + public void redactBetween(@Argument(ArgumentType.STRING) String start, + @Argument(ArgumentType.STRING) String stop, + @Argument(ArgumentType.TYPE) String asType, + @Argument(ArgumentType.RULE_NUMBER) int ruleNumber, + @Argument(ArgumentType.BOOLEAN) boolean redactEverywhere, + @Argument(ArgumentType.STRING) String reason, + @Argument(ArgumentType.LEGAL_BASIS) String legalBasis) { String[] values = StringUtils.substringsBetween(searchText, start, stop); @@ -387,9 +449,14 @@ public class Section { } } - - public void redactLinesBetween(String start, String stop, String asType, int ruleNumber, boolean redactEverywhere, - String reason, String legalBasis) { + @ThenAction + public void redactLinesBetween(@Argument(ArgumentType.STRING) String start, + @Argument(ArgumentType.STRING) String stop, + @Argument(ArgumentType.TYPE) String asType, + @Argument(ArgumentType.RULE_NUMBER) int ruleNumber, + @Argument(ArgumentType.BOOLEAN) boolean redactEverywhere, + @Argument(ArgumentType.STRING) String reason, + @Argument(ArgumentType.LEGAL_BASIS) String legalBasis) { String[] values = StringUtils.substringsBetween(text, start, stop); @@ -416,29 +483,43 @@ public class Section { } } - - public void highlightCell(String cellHeader, int ruleNumber, String type) { + @ThenAction + public void highlightCell(@Argument(ArgumentType.STRING) String cellHeader, + @Argument(ArgumentType.RULE_NUMBER) int ruleNumber, + @Argument(ArgumentType.TYPE) String type) { annotateCell(cellHeader, ruleNumber, type, false, false, null, null); } - - public void redactCell(String cellHeader, int ruleNumber, String type, boolean addAsRecommendations, String reason, - String legalBasis) { + @ThenAction + public void redactCell(@Argument(ArgumentType.STRING) String cellHeader, + @Argument(ArgumentType.RULE_NUMBER) int ruleNumber, + @Argument(ArgumentType.TYPE) String type, + @Argument(ArgumentType.BOOLEAN) boolean addAsRecommendations, + @Argument(ArgumentType.STRING) String reason, + @Argument(ArgumentType.LEGAL_BASIS) String legalBasis) { annotateCell(cellHeader, ruleNumber, type, true, addAsRecommendations, reason, legalBasis); } - - public void redactNotCell(String cellHeader, int ruleNumber, String type, boolean addAsRecommendations, - String reason) { + @ThenAction + public void redactNotCell(@Argument(ArgumentType.STRING) String cellHeader, + @Argument(ArgumentType.RULE_NUMBER) int ruleNumber, + @Argument(ArgumentType.TYPE) String type, + @Argument(ArgumentType.BOOLEAN) boolean addAsRecommendations, + @Argument(ArgumentType.STRING) String reason) { annotateCell(cellHeader, ruleNumber, type, false, addAsRecommendations, reason, null); } - private Set findEntities(String value, String asType, boolean caseInsensitive, boolean redacted, - int ruleNumber, String reason, String legalBasis) { + private Set findEntities(@Argument(ArgumentType.STRING) String value, + @Argument(ArgumentType.TYPE) String asType, + @Argument(ArgumentType.BOOLEAN) boolean caseInsensitive, + @Argument(ArgumentType.BOOLEAN) boolean redacted, + @Argument(ArgumentType.RULE_NUMBER) int ruleNumber, + @Argument(ArgumentType.STRING) String reason, + @Argument(ArgumentType.LEGAL_BASIS) String legalBasis) { String text = caseInsensitive ? searchText.toLowerCase() : searchText; String searchValue = caseInsensitive ? value.toLowerCase() : value; @@ -507,6 +588,25 @@ public class Section { } } + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface WhenCondition { + + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface ThenAction { + + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.PARAMETER) + public @interface Argument { + + ArgumentType value() default ArgumentType.STRING; + } + } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/rulebuilder/RuleBuilderModelService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/rulebuilder/RuleBuilderModelService.java new file mode 100644 index 00000000..7df981f0 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/rulebuilder/RuleBuilderModelService.java @@ -0,0 +1,36 @@ +package com.iqser.red.service.redaction.v1.server.redaction.rulebuilder; + +import com.iqser.red.service.redaction.v1.model.Argument; +import com.iqser.red.service.redaction.v1.model.RuleBuilderModel; +import com.iqser.red.service.redaction.v1.model.RuleElement; +import com.iqser.red.service.redaction.v1.server.redaction.model.Section; +import org.springframework.stereotype.Service; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class RuleBuilderModelService { + + public RuleBuilderModel getRuleBuilderModel() { + + var whenConditions = Arrays.stream(Section.class.getDeclaredMethods()).filter(m -> m.isAnnotationPresent(Section.WhenCondition.class)).collect(Collectors.toList()); + var thenActions = Arrays.stream(Section.class.getDeclaredMethods()).filter(m -> m.isAnnotationPresent(Section.ThenAction.class)).collect(Collectors.toList()); + + RuleBuilderModel ruleBuilderModel = new RuleBuilderModel(); + + + ruleBuilderModel.setWhenClauses(whenConditions.stream().map(c -> new RuleElement(c.getName(), toArguments(c))).collect(Collectors.toList())); + ruleBuilderModel.setThenConditions(thenActions.stream().map(c -> new RuleElement(c.getName(), toArguments(c))).collect(Collectors.toList())); + + return ruleBuilderModel; + } + + private List toArguments(Method c) { + return Arrays.stream(c.getParameters()) + .map(parameter -> new Argument(parameter.getName(), parameter.getAnnotation(Section.Argument.class).value())) + .collect(Collectors.toList()); + } +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/rulebuilder/RuleBuilderModelServiceTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/rulebuilder/RuleBuilderModelServiceTest.java new file mode 100644 index 00000000..01baf6a4 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/rulebuilder/RuleBuilderModelServiceTest.java @@ -0,0 +1,18 @@ +package com.iqser.red.service.redaction.v1.server.redaction.rulebuilder; + +import com.iqser.red.service.redaction.v1.model.RuleBuilderModel; +import org.junit.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class RuleBuilderModelServiceTest { + + @Test + public void testRuleBuilderModelProvider() { + + RuleBuilderModel model = new RuleBuilderModelService().getRuleBuilderModel(); + + assertThat(model.getWhenClauses().size()).isGreaterThan(1); + assertThat(model.getThenConditions().size()).isGreaterThan(1); + } +} From 6a75fc74d6eabbb0d99bdbca4d6cefcbb17d224f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Tue, 7 Sep 2021 14:56:19 +0200 Subject: [PATCH 76/80] RED-1970: Ignore NER entities that are found over multiple table columns --- .../service/EntityRedactionService.java | 39 ++++++++++++++----- .../redaction/utils/EntitySearchUtils.java | 4 +- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java index d57156bd..752034da 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java @@ -1,6 +1,5 @@ package com.iqser.red.service.redaction.v1.server.redaction.service; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -20,9 +19,8 @@ import com.iqser.red.service.redaction.v1.model.ManualImageRecategorization; import com.iqser.red.service.redaction.v1.model.Status; import com.iqser.red.service.redaction.v1.server.classification.model.SectionText; import com.iqser.red.service.redaction.v1.server.client.EntityRecognitionClient; -import com.iqser.red.service.redaction.v1.server.client.model.EntityRecognitionRequest; +import com.iqser.red.service.redaction.v1.server.client.model.EntityRecogintionEntity; import com.iqser.red.service.redaction.v1.server.client.model.NerEntities; -import com.iqser.red.service.redaction.v1.server.client.model.EntityRecognitionSection; import com.iqser.red.service.redaction.v1.server.redaction.model.Dictionary; import com.iqser.red.service.redaction.v1.server.redaction.model.DictionaryModel; import com.iqser.red.service.redaction.v1.server.redaction.model.Entity; @@ -77,7 +75,8 @@ public class EntityRedactionService { for (SectionText reanalysisSection : reanalysisSections) { Set entities = findEntities(reanalysisSection.getSearchableText(), reanalysisSection.getHeadline(), reanalysisSection - .getSectionNumber(), dictionary, local, nerEntities); + .getSectionNumber(), dictionary, local, nerEntities, reanalysisSection.getCellStarts()); + if (reanalysisSection.getCellStarts() != null && !reanalysisSection.getCellStarts().isEmpty()) { surroundingWordsService.addSurroundingText(entities, reanalysisSection.getSearchableText(), dictionary, reanalysisSection .getCellStarts()); @@ -199,7 +198,8 @@ public class EntityRedactionService { private Set findEntities(SearchableText searchableText, String headline, int sectionNumber, - Dictionary dictionary, boolean local, NerEntities nerEntities) { + Dictionary dictionary, boolean local, NerEntities nerEntities, + List cellstarts) { Set found = new HashSet<>(); String searchableString = searchableText.toString(); @@ -210,30 +210,49 @@ public class EntityRedactionService { String lowercaseInputString = searchableString.toLowerCase(); for (DictionaryModel model : dictionary.getDictionaryModels()) { if (model.isCaseInsensitive()) { - found.addAll(EntitySearchUtils.find(lowercaseInputString, model.getValues(local), model.getType(), headline, sectionNumber, local, model + found.addAll(EntitySearchUtils.find(lowercaseInputString, model.getValues(local), model.getType(), headline, sectionNumber, !local, model .isDossierDictionary())); } else { - found.addAll(EntitySearchUtils.find(searchableString, model.getValues(local), model.getType(), headline, sectionNumber, local, model + found.addAll(EntitySearchUtils.find(searchableString, model.getValues(local), model.getType(), headline, sectionNumber, !local, model .isDossierDictionary())); } } if (!local) { - addNerEntities(found, sectionNumber, headline, nerEntities); + Map> nerValuesPerType = getNerValues(sectionNumber, nerEntities, cellstarts); + nerValuesPerType.entrySet().forEach(entry -> { + found.addAll(EntitySearchUtils.find(searchableString, entry.getValue(), entry.getKey(), headline, sectionNumber, false, false)); + }); } return EntitySearchUtils.clearAndFindPositions(found, searchableText, dictionary); } - private void addNerEntities(Set found, int sectionNumber, String headline, NerEntities nerEntities) { + private Map> getNerValues(int sectionNumber, NerEntities nerEntities, + List cellstarts) { + + Map> nerValuesPerType = new HashMap<>(); if (redactionServiceSettings.isEnableEntityRecognition() && nerEntities.getResult() .containsKey(sectionNumber)) { nerEntities.getResult().get(sectionNumber).forEach(res -> { - found.add(new Entity(new String(Base64.decodeBase64(res.getValue().getBytes())), res.getType(), res.getStartOffset(), res.getEndOffset(), headline, sectionNumber, false, false)); + if (cellstarts == null || cellstarts.isEmpty()) { + nerValuesPerType.computeIfAbsent(res.getType(), (a) -> new HashSet<>()).add(new String(Base64.decodeBase64(res.getValue().getBytes()))); + } else { + boolean intersectsCellStart = false; + for (Integer cellStart : cellstarts) { + if (res.getStartOffset() < cellStart && cellStart < res.getEndOffset()) { + intersectsCellStart = true; + } + } + if (!intersectsCellStart) { + nerValuesPerType.computeIfAbsent(res.getType(), (a) -> new HashSet<>()).add(new String(Base64.decodeBase64(res.getValue().getBytes()))); + } + } }); } + return nerValuesPerType; } } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/utils/EntitySearchUtils.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/utils/EntitySearchUtils.java index 0daca578..14bee48b 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/utils/EntitySearchUtils.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/utils/EntitySearchUtils.java @@ -47,7 +47,7 @@ public class EntitySearchUtils { public Set find(String inputString, Set values, String type, String headline, int sectionNumber, - boolean local, boolean isDossierDictionary) { + boolean isDictionaryEntry, boolean isDossierDictionary) { Set found = new HashSet<>(); @@ -67,7 +67,7 @@ public class EntitySearchUtils { if (startIndex > -1 && (startIndex == 0 || Character.isWhitespace(inputString.charAt(startIndex - 1)) || isSeparator(inputString .charAt(startIndex - 1))) && (stopIndex == inputString.length() || isSeparator(inputString.charAt(stopIndex)))) { - found.add(new Entity(inputString.substring(startIndex, stopIndex), type, startIndex, stopIndex, headline, sectionNumber, !local, isDossierDictionary)); + found.add(new Entity(inputString.substring(startIndex, stopIndex), type, startIndex, stopIndex, headline, sectionNumber, isDictionaryEntry, isDossierDictionary)); } } while (startIndex > -1); } From d89a41caca623eedbeee7bc9b058b605db8fc359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Wed, 8 Sep 2021 13:20:38 +0200 Subject: [PATCH 77/80] RED-2082: Added engines to redactionLog, to identify where a entry comes from --- .../service/redaction/v1/model/Engine.java | 5 ++++ .../redaction/v1/model/RedactionLogEntry.java | 8 ++++-- .../v1/server/redaction/model/Entity.java | 18 ++++++++++--- .../v1/server/redaction/model/Section.java | 5 ++-- .../service/EntityRedactionService.java | 25 ++++++++++--------- .../service/RedactionLogCreatorService.java | 1 + .../redaction/utils/EntitySearchUtils.java | 24 +++++++++++++++--- .../utils/EntitySearchUtilsTest.java | 5 ++-- 8 files changed, 67 insertions(+), 24 deletions(-) create mode 100644 redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/Engine.java diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/Engine.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/Engine.java new file mode 100644 index 00000000..8dfa1184 --- /dev/null +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/Engine.java @@ -0,0 +1,5 @@ +package com.iqser.red.service.redaction.v1.model; + +public enum Engine { + DICTIONARY, NER, RULE +} diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java index dc14eebc..e9431f21 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java @@ -7,9 +7,9 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; - - +import java.util.Set; @Data @Builder @@ -62,4 +62,8 @@ public class RedactionLogEntry { @Builder.Default private List changes = new ArrayList<>(); + private Set engines= new HashSet<>(); + + + } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Entity.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Entity.java index 2ae553db..ad426337 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Entity.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Entity.java @@ -1,17 +1,20 @@ package com.iqser.red.service.redaction.v1.server.redaction.model; +import com.iqser.red.service.redaction.v1.model.Engine; import com.iqser.red.service.redaction.v1.server.parsing.model.TextPositionSequence; + import lombok.Data; import lombok.EqualsAndHashCode; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; @Data @EqualsAndHashCode(onlyExplicitlyIncluded = true) public class Entity implements ReasonHolder { - private final String word; private final String type; private boolean redaction; @@ -39,8 +42,13 @@ public class Entity implements ReasonHolder { private boolean isDossierDictionaryEntry; + private Set engines = new HashSet<>(); - public Entity(String word, String type, boolean redaction, String redactionReason, List positionSequences, String headline, int matchedRule, int sectionNumber, String legalBasis, boolean isDictionaryEntry, String textBefore, String textAfter, Integer start, Integer end, boolean isDossierDictionaryEntry) { + + public Entity(String word, String type, boolean redaction, String redactionReason, + List positionSequences, String headline, int matchedRule, int sectionNumber, + String legalBasis, boolean isDictionaryEntry, String textBefore, String textAfter, Integer start, + Integer end, boolean isDossierDictionaryEntry, Set engines) { this.word = word; this.type = type; @@ -57,10 +65,12 @@ public class Entity implements ReasonHolder { this.start = start; this.end = end; this.isDossierDictionaryEntry = isDossierDictionaryEntry; + this.engines = engines; } - public Entity(String word, String type, Integer start, Integer end, String headline, int sectionNumber, boolean isDictionaryEntry, boolean isDossierDictionaryEntry) { + public Entity(String word, String type, Integer start, Integer end, String headline, int sectionNumber, + boolean isDictionaryEntry, boolean isDossierDictionaryEntry, Engine engine) { this.word = word; this.type = type; @@ -70,6 +80,8 @@ public class Entity implements ReasonHolder { this.sectionNumber = sectionNumber; this.isDictionaryEntry = isDictionaryEntry; this.isDossierDictionaryEntry = isDossierDictionaryEntry; + this.engines.add(engine); } + } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Section.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Section.java index 929e4110..6a1594f8 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Section.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Section.java @@ -1,6 +1,7 @@ package com.iqser.red.service.redaction.v1.server.redaction.model; import com.iqser.red.service.redaction.v1.model.ArgumentType; +import com.iqser.red.service.redaction.v1.model.Engine; import com.iqser.red.service.redaction.v1.model.FileAttribute; import com.iqser.red.service.redaction.v1.server.classification.model.TextBlock; import com.iqser.red.service.redaction.v1.server.redaction.utils.EntitySearchUtils; @@ -524,7 +525,7 @@ public class Section { String text = caseInsensitive ? searchText.toLowerCase() : searchText; String searchValue = caseInsensitive ? value.toLowerCase() : value; - Set found = EntitySearchUtils.find(text, Set.of(searchValue), asType, headline, sectionNumber, true, false); + Set found = EntitySearchUtils.find(text, Set.of(searchValue), asType, headline, sectionNumber, false, false, Engine.RULE); found.forEach(entity -> { if (redacted) { @@ -550,7 +551,7 @@ public class Section { } else { String word = value.toString(); - Entity entity = new Entity(word, type, value.getRowSpanStart(), value.getRowSpanStart() + word.length(), headline, sectionNumber, false, false); + Entity entity = new Entity(word, type, value.getRowSpanStart(), value.getRowSpanStart() + word.length(), headline, sectionNumber, false, false, Engine.RULE); entity.setRedaction(redact); entity.setMatchedRule(ruleNumber); entity.setRedactionReason(reason); diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java index 752034da..7b7e5547 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java @@ -15,11 +15,10 @@ import org.kie.api.runtime.KieContainer; import org.springframework.stereotype.Service; import com.iqser.red.service.redaction.v1.model.AnalyzeRequest; +import com.iqser.red.service.redaction.v1.model.Engine; import com.iqser.red.service.redaction.v1.model.ManualImageRecategorization; import com.iqser.red.service.redaction.v1.model.Status; import com.iqser.red.service.redaction.v1.server.classification.model.SectionText; -import com.iqser.red.service.redaction.v1.server.client.EntityRecognitionClient; -import com.iqser.red.service.redaction.v1.server.client.model.EntityRecogintionEntity; import com.iqser.red.service.redaction.v1.server.client.model.NerEntities; import com.iqser.red.service.redaction.v1.server.redaction.model.Dictionary; import com.iqser.red.service.redaction.v1.server.redaction.model.DictionaryModel; @@ -42,7 +41,6 @@ import lombok.extern.slf4j.Slf4j; @RequiredArgsConstructor public class EntityRedactionService { - private final EntityRecognitionClient entityRecognitionClient; private final RedactionServiceSettings redactionServiceSettings; private final DroolsExecutionService droolsExecutionService; private final SurroundingWordsService surroundingWordsService; @@ -151,7 +149,7 @@ public class EntityRedactionService { .add(new Entity(entity.getWord(), entity.getType(), entity.isRedaction(), entity.getRedactionReason(), entry .getValue(), entity.getHeadline(), entity.getMatchedRule(), entity.getSectionNumber(), entity .getLegalBasis(), entity.isDictionaryEntry(), entity.getTextBefore(), entity.getTextAfter(), entity - .getStart(), entity.getEnd(), entity.isDossierDictionaryEntry())); + .getStart(), entity.getEnd(), entity.isDossierDictionaryEntry(), entity.getEngines())); } } return entitiesPerPage; @@ -210,18 +208,19 @@ public class EntityRedactionService { String lowercaseInputString = searchableString.toLowerCase(); for (DictionaryModel model : dictionary.getDictionaryModels()) { if (model.isCaseInsensitive()) { - found.addAll(EntitySearchUtils.find(lowercaseInputString, model.getValues(local), model.getType(), headline, sectionNumber, !local, model - .isDossierDictionary())); + EntitySearchUtils.addOrAddEngine(found, EntitySearchUtils.find(lowercaseInputString, model.getValues(local), model + .getType(), headline, sectionNumber, !local, model.isDossierDictionary(), Engine.DICTIONARY)); } else { - found.addAll(EntitySearchUtils.find(searchableString, model.getValues(local), model.getType(), headline, sectionNumber, !local, model - .isDossierDictionary())); + EntitySearchUtils.addOrAddEngine(found, EntitySearchUtils.find(searchableString, model.getValues(local), model + .getType(), headline, sectionNumber, !local, model.isDossierDictionary(), Engine.DICTIONARY)); } } if (!local) { Map> nerValuesPerType = getNerValues(sectionNumber, nerEntities, cellstarts); nerValuesPerType.entrySet().forEach(entry -> { - found.addAll(EntitySearchUtils.find(searchableString, entry.getValue(), entry.getKey(), headline, sectionNumber, false, false)); + EntitySearchUtils.addOrAddEngine(found, EntitySearchUtils.find(searchableString, entry.getValue(), entry + .getKey(), headline, sectionNumber, false, false, Engine.NER)); }); } @@ -230,7 +229,7 @@ public class EntityRedactionService { private Map> getNerValues(int sectionNumber, NerEntities nerEntities, - List cellstarts) { + List cellstarts) { Map> nerValuesPerType = new HashMap<>(); @@ -238,7 +237,8 @@ public class EntityRedactionService { .containsKey(sectionNumber)) { nerEntities.getResult().get(sectionNumber).forEach(res -> { if (cellstarts == null || cellstarts.isEmpty()) { - nerValuesPerType.computeIfAbsent(res.getType(), (a) -> new HashSet<>()).add(new String(Base64.decodeBase64(res.getValue().getBytes()))); + nerValuesPerType.computeIfAbsent(res.getType(), (a) -> new HashSet<>()) + .add(new String(Base64.decodeBase64(res.getValue().getBytes()))); } else { boolean intersectsCellStart = false; for (Integer cellStart : cellstarts) { @@ -247,7 +247,8 @@ public class EntityRedactionService { } } if (!intersectsCellStart) { - nerValuesPerType.computeIfAbsent(res.getType(), (a) -> new HashSet<>()).add(new String(Base64.decodeBase64(res.getValue().getBytes()))); + nerValuesPerType.computeIfAbsent(res.getType(), (a) -> new HashSet<>()) + .add(new String(Base64.decodeBase64(res.getValue().getBytes()))); } } }); diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java index 0eaea701..7fefdbc1 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java @@ -177,6 +177,7 @@ public class RedactionLogCreatorService { .startOffset(entity.getStart()) .endOffset(entity.getEnd()) .isDossierDictionaryEntry(entity.isDossierDictionaryEntry()) + .engines(entity.getEngines()) .build(); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/utils/EntitySearchUtils.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/utils/EntitySearchUtils.java index 14bee48b..1cd9d6d0 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/utils/EntitySearchUtils.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/utils/EntitySearchUtils.java @@ -1,5 +1,6 @@ package com.iqser.red.service.redaction.v1.server.redaction.utils; +import com.iqser.red.service.redaction.v1.model.Engine; import com.iqser.red.service.redaction.v1.server.redaction.model.Dictionary; import com.iqser.red.service.redaction.v1.server.redaction.model.DictionaryIncrementValue; import com.iqser.red.service.redaction.v1.server.redaction.model.Entity; @@ -47,7 +48,7 @@ public class EntitySearchUtils { public Set find(String inputString, Set values, String type, String headline, int sectionNumber, - boolean isDictionaryEntry, boolean isDossierDictionary) { + boolean isDictionaryEntry, boolean isDossierDictionary, Engine engine) { Set found = new HashSet<>(); @@ -67,7 +68,7 @@ public class EntitySearchUtils { if (startIndex > -1 && (startIndex == 0 || Character.isWhitespace(inputString.charAt(startIndex - 1)) || isSeparator(inputString .charAt(startIndex - 1))) && (stopIndex == inputString.length() || isSeparator(inputString.charAt(stopIndex)))) { - found.add(new Entity(inputString.substring(startIndex, stopIndex), type, startIndex, stopIndex, headline, sectionNumber, isDictionaryEntry, isDossierDictionary)); + found.add(new Entity(inputString.substring(startIndex, stopIndex), type, startIndex, stopIndex, headline, sectionNumber, isDictionaryEntry, isDossierDictionary, engine)); } } while (startIndex > -1); } @@ -142,9 +143,13 @@ public class EntitySearchUtils { Entity existing = entities.stream().filter(entity -> entity.equals(found)).findFirst().get(); if (dictionary.getDictionaryRank(existing.getType()) <= dictionary.getDictionaryRank(found.getType())) { entities.remove(found); + entities.add(found); + } else { + existing.getEngines().addAll(found.getEngines()); } + } else { + entities.add(found); } - entities.add(found); } @@ -154,4 +159,17 @@ public class EntitySearchUtils { entities.addAll(found); } + + public void addOrAddEngine(Set existing, Set toBeAdded){ + + for(Entity toAdd: toBeAdded){ + if (existing.contains(toAdd)) { + Entity existingEntity = existing.stream().filter(entity -> entity.equals(toAdd)).findFirst().get(); + existingEntity.getEngines().addAll(toAdd.getEngines()); + } else { + existing.add(toAdd); + } + } + } + } diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/utils/EntitySearchUtilsTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/utils/EntitySearchUtilsTest.java index 544b334e..ae2fb019 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/utils/EntitySearchUtilsTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/utils/EntitySearchUtilsTest.java @@ -7,6 +7,7 @@ import java.util.Set; import org.junit.Test; +import com.iqser.red.service.redaction.v1.model.Engine; import com.iqser.red.service.redaction.v1.server.redaction.model.Entity; public class EntitySearchUtilsTest { @@ -15,8 +16,8 @@ public class EntitySearchUtilsTest { public void testNestedEntitiesRemoval() { Set entities = new HashSet<>(); - Entity nested = new Entity("nested", "fake type", 10, 16, "fake headline", 0, false, false); - Entity nesting = new Entity("nesting nested", "fake type", 2, 16, "fake headline", 0, false, false); + Entity nested = new Entity("nested", "fake type", 10, 16, "fake headline", 0, false, false, Engine.RULE); + Entity nesting = new Entity("nesting nested", "fake type", 2, 16, "fake headline", 0, false, false, Engine.RULE); entities.add(nested); entities.add(nesting); EntitySearchUtils.removeEntitiesContainedInLarger(entities); From 6f930b18133286289e330adbbbf390671b63d1b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Mon, 13 Sep 2021 12:43:26 +0200 Subject: [PATCH 78/80] RED-2125: Enabled possibility to reference annotations in rules --- .../redaction/v1/model/ArgumentType.java | 2 +- .../redaction/v1/model/RedactionLogEntry.java | 2 ++ .../v1/server/redaction/model/Entity.java | 5 +++- .../v1/server/redaction/model/Section.java | 24 +++++++++++++++++++ .../service/EntityRedactionService.java | 2 +- .../service/RedactionLogCreatorService.java | 4 ++++ .../src/test/resources/drools/rules.drl | 4 ++-- 7 files changed, 38 insertions(+), 5 deletions(-) diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ArgumentType.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ArgumentType.java index 2c05e253..baab0409 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ArgumentType.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ArgumentType.java @@ -2,6 +2,6 @@ package com.iqser.red.service.redaction.v1.model; public enum ArgumentType { - INTEGER, BOOLEAN, STRING, FILE_ATTRIBUTE, REGEX, TYPE, RULE_NUMBER, LEGAL_BASIS + INTEGER, BOOLEAN, STRING, FILE_ATTRIBUTE, REGEX, TYPE, RULE_NUMBER, LEGAL_BASIS, REFERENCE_TYPE } diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java index e9431f21..807b219c 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLogEntry.java @@ -64,6 +64,8 @@ public class RedactionLogEntry { private Set engines= new HashSet<>(); + private Set reference = new HashSet<>(); + } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Entity.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Entity.java index ad426337..cd90ab6b 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Entity.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Entity.java @@ -44,11 +44,13 @@ public class Entity implements ReasonHolder { private Set engines = new HashSet<>(); + private Set references = new HashSet<>(); + public Entity(String word, String type, boolean redaction, String redactionReason, List positionSequences, String headline, int matchedRule, int sectionNumber, String legalBasis, boolean isDictionaryEntry, String textBefore, String textAfter, Integer start, - Integer end, boolean isDossierDictionaryEntry, Set engines) { + Integer end, boolean isDossierDictionaryEntry, Set engines, Set references) { this.word = word; this.type = type; @@ -66,6 +68,7 @@ public class Entity implements ReasonHolder { this.end = end; this.isDossierDictionaryEntry = isDossierDictionaryEntry; this.engines = engines; + this.references = references; } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Section.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Section.java index 6a1594f8..9ba32611 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Section.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Section.java @@ -228,6 +228,30 @@ public class Section { }); } + + @ThenAction + public void redactNotAndReference(@Argument(ArgumentType.TYPE) String type, + @Argument(ArgumentType.REFERENCE_TYPE) String referenceType, + @Argument(ArgumentType.RULE_NUMBER) int ruleNumber, + @Argument(ArgumentType.STRING) String reason) { + + boolean hasRecommendationDictionary = dictionaryTypes.contains(RECOMMENDATION_PREFIX + type); + + Set references = entities.stream().filter(entity -> entity.getType().equals(referenceType)).collect(Collectors.toSet()); + + entities.forEach(entity -> { + if (entity.getType().equals(type) || hasRecommendationDictionary && entity.getType() + .equals(RECOMMENDATION_PREFIX + type)) { + entity.setRedaction(false); + entity.setMatchedRule(ruleNumber); + entity.setRedactionReason(reason); + entity.setReferences(references); + } + }); + } + + + @ThenAction public void expandToHintAnnotationByRegEx(@Argument(ArgumentType.TYPE) String type, @Argument(ArgumentType.STRING) String pattern, diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java index 7b7e5547..97b28158 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/EntityRedactionService.java @@ -149,7 +149,7 @@ public class EntityRedactionService { .add(new Entity(entity.getWord(), entity.getType(), entity.isRedaction(), entity.getRedactionReason(), entry .getValue(), entity.getHeadline(), entity.getMatchedRule(), entity.getSectionNumber(), entity .getLegalBasis(), entity.isDictionaryEntry(), entity.getTextBefore(), entity.getTextAfter(), entity - .getStart(), entity.getEnd(), entity.isDossierDictionaryEntry(), entity.getEngines())); + .getStart(), entity.getEnd(), entity.isDossierDictionaryEntry(), entity.getEngines(), entity.getReferences())); } } return entitiesPerPage; diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java index 7fefdbc1..2de87968 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/RedactionLogCreatorService.java @@ -159,6 +159,9 @@ public class RedactionLogCreatorService { private RedactionLogEntry createRedactionLogEntry(Entity entity, String dossierTemplateId) { + Set referenceIds = new HashSet<>(); + entity.getReferences().forEach(ref -> ref.getPositionSequences().forEach(pos -> referenceIds.add(pos.getId()))); + return RedactionLogEntry.builder() .color(getColor(entity.getType(), dossierTemplateId, entity.isRedaction())) .reason(entity.getRedactionReason()) @@ -178,6 +181,7 @@ public class RedactionLogCreatorService { .endOffset(entity.getEnd()) .isDossierDictionaryEntry(entity.isDossierDictionaryEntry()) .engines(entity.getEngines()) + .reference(referenceIds) .build(); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/rules.drl b/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/rules.drl index 853d7fac..bf9cb602 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/rules.drl +++ b/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/rules.drl @@ -56,8 +56,8 @@ rule "5: Do not redact Names and Addresses if no redaction Indicator is containe when Section(matchesType("vertebrate"), matchesType("published_information")) then - section.redactNot("CBI_author", 5, "Vertebrate and Published Information found"); - section.redactNot("CBI_address", 5, "Vertebrate and Published Information found"); + section.redactNotAndReference("CBI_author","published_information", 5, "Vertebrate and Published Information found"); + section.redactNotAndReference("CBI_address","published_information", 5, "Vertebrate and Published Information found"); end From ff97cba0c8935a44edf912310a459df879522c71 Mon Sep 17 00:00:00 2001 From: aoezyetimoglu Date: Wed, 15 Sep 2021 12:17:28 +0200 Subject: [PATCH 79/80] RED-2167: added computation of cellstarts for tables with two colums --- .../service/SectionTextBuilderService.java | 10 ++++++++++ .../v1/server/RedactionIntegrationTest.java | 8 ++++---- .../src/test/resources/files/S11.pdf | Bin 0 -> 135532 bytes .../src/test/resources/files/S16.pdf | Bin 0 -> 138352 bytes 4 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 redaction-service-v1/redaction-service-server-v1/src/test/resources/files/S11.pdf create mode 100644 redaction-service-v1/redaction-service-server-v1/src/test/resources/files/S16.pdf diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/SectionTextBuilderService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/SectionTextBuilderService.java index 8fe206d3..b0ae577c 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/SectionTextBuilderService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/SectionTextBuilderService.java @@ -142,7 +142,11 @@ public class SectionTextBuilderService { SearchableText entireTableText = new SearchableText(); SectionText sectionText = new SectionText(); + int start = 0; + List cellStarts = new ArrayList<>(); for (List row : table.getRows()) { + + for (Cell cell : row) { if (CollectionUtils.isEmpty(cell.getTextBlocks())) { continue; @@ -160,8 +164,14 @@ public class SectionTextBuilderService { for (TextBlock textBlock : cell.getTextBlocks()) { entireTableText.addAll(textBlock.getSequences()); } + + + cellStarts.add(start); + start = start + cell.toString().trim().length() + 1; } + } + sectionText.setCellStarts(cellStarts); sectionText.setText(entireTableText.toString()); sectionText.setHeadline(table.getHeadline()); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java index 6a4a5524..eb29141a 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java @@ -664,7 +664,7 @@ public class RedactionIntegrationTest { public void redactionTest() throws IOException { long start = System.currentTimeMillis(); - ClassPathResource pdfFileResource = new ClassPathResource("files/new/Single Study - Oral (Gavage) Mouse.pdf"); + ClassPathResource pdfFileResource = new ClassPathResource("files/S11.pdf"); AnalyzeRequest request = prepareStorage(pdfFileResource.getInputStream()); request.setExcludedPages(Set.of(1)); @@ -889,7 +889,7 @@ public class RedactionIntegrationTest { public void classificationTest() throws IOException { System.out.println("classificationTest"); - ClassPathResource pdfFileResource = new ClassPathResource("files/new/Single Study - Oral (Gavage) Mouse.pdf"); + ClassPathResource pdfFileResource = new ClassPathResource("files/S11.pdf"); AnalyzeRequest request = prepareStorage(pdfFileResource.getInputStream()); @@ -911,7 +911,7 @@ public class RedactionIntegrationTest { public void sectionsTest() throws IOException { System.out.println("sectionsTest"); - ClassPathResource pdfFileResource = new ClassPathResource("files/Fludioxonil/51 " + "Fludioxonil_RAR_02_Volume_2_2018-02-21.pdf"); + ClassPathResource pdfFileResource = new ClassPathResource("files/S11.pdf"); AnalyzeRequest request = prepareStorage(pdfFileResource.getInputStream()); @@ -933,7 +933,7 @@ public class RedactionIntegrationTest { public void htmlTablesTest() throws IOException { System.out.println("htmlTablesTest"); - ClassPathResource pdfFileResource = new ClassPathResource("files/Metolachlor/S-Metolachlor_RAR_02_Volume_2_2018-09-06.pdf"); + ClassPathResource pdfFileResource = new ClassPathResource("files/S11.pdf"); AnalyzeRequest request = prepareStorage(pdfFileResource.getInputStream()); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/resources/files/S11.pdf b/redaction-service-v1/redaction-service-server-v1/src/test/resources/files/S11.pdf new file mode 100644 index 0000000000000000000000000000000000000000..42d37883497cae24f5da03413edf5d2ddc4a670a GIT binary patch literal 135532 zcmce-1ymhtvnGtYyR&g;!^Yh$xCVE34-i}f!Civ8yCo3Z-2()72_7U!269f$mwRXC z&NuU~`>(ya`<1HddaJsgZk~5Rr6exN#LCQtj6juJ-?xB_zzSdiI2caqQ)=WJ%=YUN-rZscl4E6xjI0kN|1u(GpqvU0O>&;i9>rX8GVpMAA*v!eql zIXjrTnV9`E@Tb|AoBqokdsj1iR~G=sFHe9{&JJ#l&+8IX1&W)wTbY=tNQnYffojf1 z_AZV_&Sv%|o_{&}OHfb{C~szJWhCn0q4TWI0^s1`U}ggWxY=2mc|dy4(RtQz{T&_^ zGZzOpXA?7*XZsQ!u2QP5&ue*ZOQ~}60671?zbX$4fa_QL*|?;Gz3a1pBr7X``>%OP z5a7APe4l;i|}ehY8`KsP=QN7`brJjPBBS?we^pm!Hxt*tR^b9*oxHq5LEP{Os}O zyPwP?7T9&8McA&DaoMM6x2=XPcwG0$)Y~FDb^j)x@$_9vWjeB8O4?+gD|c}IT8UCZ zW;-TPa!>K(=IG}Aj^O=|qf46mM`O?WkHf232CJ)&?!I>i_HIvJPVKAA*GJ3WxYkS+ zvxP>o^aXugpF~aVRy7uP?gwHtH9Gwm%ip&pfy$S8_vGF0zz6j|TP+=X-P|h2>CG%O zO&S;o`d+zT8~gTKWQ3KseyDWVvv9uNYfJA6b8ga`m`GnVf6{Tkn;3{;7N(5gh{uE6 zXv%4>w+&8AQD2>B{?;KP4(+Vm8^)7RuO8Zsk8pkWp`#>B09zssahK)sBW+fl!$T7| zLwMe(1dqI@@~r0;-A`gXo9|A(8WS3XN;>{a6j2fh3%2Pw@gr^Vs#ZrI)VCTsdd_ww zx;)Y{v3a)S1}khwh>9dBJoN{UeZSvM0sSVLytLKq^t-FwX11v&D3MYt>c~@&(LevZ z-;xp%@J-b7{>jKknpUakaC5(mdJaDlCf22swO~fwIXH`orVZA2@hvinpmE(&tsH)8 z`H@Dx3r;<4foY_yX($7<)zE zNl=QX&Pg$lSeAk@50m~yD@9)4v8KAhnL25nou#a}byUl~^e1N2IvFU#Uk=9II9hH& zI!qS+dt{gPMInD$y|X))h3Mm=zm!%_$4*-&({AKg)9umIRav5h{s5mGrkKo@T?nM% zSd_N-9?a2R|48xtSAgWGh7KD+$;yObvdbA=-FS%d)!0KtrVj*z-NqWngU;AQ=c`-0 z4lo)>93L3_J(3mV);+!*`mVPf{Y5vIl8>Jam~Lz?s=g${W#vp6pd9c zAugM5(QswZUWO9ZGda}E_!*+&-rSx8>oR^AxrDfD+H%r=bo5prE8-7e6<6I0DOMQ9 z+uZK=FClAiOo!z`xsBmbz$CaYX0fu=fAEZ87USRQMo&Mi<@b!ihg~<)wImAx(EK=F zR=tV|V6$ODcd)RaF0x#_pSos zBT5{2qRpN3Y$fJ9Bz$#~$awz>h|ZB&)7^kbbr@$wXO?5?Au04eH#m=ge?Cv+F zGSg?^oDy2Mp7LqbaEs&#Ktq3eU`9omtgStM=iG2(fXdTsx^y6%n7qnr%UxYU$7YN1 z!1udSgJu|}nNnyZo?g~)e@hHX0e)dV>%D!3aT50?mS z*~gpJKEnmY<@5PHZyi!h}(a~2#rG`6+uFd z9vGWIw3Ub=!t(H)`w3&~)+X>P-qXc)|Jg#E0Q;>C-~glC!=j|K|L0mMEnOp1y$%6N{lb6l1%Q4s0y8| ze*;#m)B=+}e+jhMHXOH9&L{QvuSCevNvE}7U%_K6@I!kb9B7?MD%Bhx_6OmrML$gR z!HajgTA2xpJAYIw=4`cW$G;!N(n_jK(xMBqRG4%^d)*9K`lx`P(s!wfL{moyT1B{sbU?oMj8biq%~@(9hLAR;~Z^@cLZt`s6@g9Iq4$rK=W;OFR*{7?fi z`+UlJEdWvF|}mJ%BtW^Nu&?6UbH4YaFW-rrid-v$;B`PV)Z6iv*2d=0xmm z%3K_CJN^3u(ME-Ck=;ZpQ!K+geslr1HXMjyM17f)*$?j=b1m$xgH1-6B8?+_OdEl$ z`@!|YMHj-miK5$k6uDd9?Ip&=g-=awZ8~b+mq4HB65}9yY)AyUN zVbxw6>k?Vfr>kZ^i5IlkJc~DK^ea)jP>Z6%F|=^qr5O+F<$<2`gfD%ECF1(IPw3PL zY2meH?;H-v@j@z^2<3QHNxG~KES0z*%Uv0t#Q79a#Mf<*+o}Z7RM;Ru+EE8=Fl;4# zGraI9eCjQDf@X+XLN7xBPPTLi`}nYPNrZZZXc?1q#Laa*$02-8tE`E*8$0E2;kh1{ z4bw`1tLsxT`~F_?lpc4P27B6ky#^Pu<@pegsgZFu0O_ z!bIGY+ipXaR^;2&&vBN>zvNZs2nQjwnkjpU6u7=29-7$XJh}5D<21;~{Gzi}e?Vxr zA=t@A?@*dIZycU|WAn98RH)K-Wn;*>z}w>M9SN~OF#%rWuynhrb8|t?*n?4X{2tj6 zCSkIyElvqyQrnMXdjyT0JYu-;<8qSDk0P`JrkUwb_jBj<2~vp=vbBb#-(Y=1r#B_LgxzABL#HrZd>42xJRo`d1p?Q z5q{fb+J)CB6UdF~h*S603TTj@)j#sP8$WueZj=MXpu?o16-~nXt9MGYjk&x~J<5Wx zmoRA5^G0a3(s1pBlPDc)d#M$&!h5Gmxr>#8nY2;OeWoHW@Z{y=xhGvk@i0aa#qd%_ zJmO2FF=UJ*jX{d`%z8*eDV;8YEr5H#dd@n3@t#rDDL^Z{luBcb3f67%O3FEGR zwlK+x>a~vbbO!5(3h6iV7O#SY#Px(kanax%v8&*!c*7Cdh{!h<&q*GFH!Bh3uYo$=_M&%3=8}`>gpybahoCiaxu2_{K%T5*wpsw`bf_QST8mz ztR#C{QHu{HudU-fXzOmpMU_7!VUtdOe`Uvv9l>j|NbnsBD}soCqAXuIcQE%{485F5 z4TquSJ$>zP-iPU2X*k$%uiO#>R0+)Oe$hA@E}V$x4+3}zNEsjr3yz)L!l46Qwl?4z$log#C}CNc-O*<;Vp+LFIc#Y{a*ZLC3Kksx`~msv&IE}dZ;1;`zr0d01Y+qZdARp5rao3g0r`R-Jq#`Cfq znPw1T2 z24)-xEXtEdqIK4Ye%eaUzot)XSdNdj@xfDSBv{el>Ex};z2Oa;UtBXmsnwpOhBpNZ z@ukUoYZvd!7lPnL0%!RJ)Vy6{@UiWb-pHrPS?V?Yx6gUO!5Bp3>K1dXGgT!5=rg|K zUfE2seaRo*FNzl+lN=uAbs%o*Wdm1c!0!~ft2)Nx7A9{Mv z+kGfKc+-6Jfy$goUUc!|hjW+Vh5E`G8taP1`b+xs-w!LzN==oOPIfS?j0ECdebyx> zHTU0mn+lF^%oA#4w%G#bL_feyr5sv9w!4r|n~yJr|MF283gm(TJwHSJcw#cRsow3d zQbpHE$FD!QKfYBPKUmE;0c%=%08>2E=fU>L5LttRhjY4e>)=QRe=|Gzj@!wgHIb)% zFRkX#5t}CdzClzUUS3T?U-6s@=WHJrlDlHxSehuLrUBUz!Sl1WN7iZ5&>m>1tnJ7^ zf7Jy08$%?_ru+v)bfeLO3h*p;!yl9g-)<^c=B0%ZuI{aivrZjY8p?7S zy>skxsnj<#OHn9o=NDWJ;kQoub~aLV!T$G`PJRKu!6`RLtg6W8#h z9Y`cJqv$p&KC*Njop?-0eRW(}wvl{y5(@Ef7ph-gOGCpMw&+>JOb}8XD|eFI?4@!D zu0MTUL84-Ui4pnOPQHoho(lQFDTiFkr?!Wx$7vOkG!Yb>NOu!VN^$26)Vf9vd9 zs7#5HmckD!5pZ)@ny0Tn;%gVpzb-B5(Fnb}NAX52H>GJnX1MGhdNb6>I0#;%O8Ak1 zeYW8)H*^(=kpF})*L`on^;M*@wz&#P0td@@W9h^zyF+=!wL;pZjY(=!`q{pi=+ZdU zhj+G(2f-&w9hgfcvHCSfU#pR9maD*e$`CvYpuxgZX3i&UXNlb3SY7DN2CS2Ax&iYmE@kIp`e2@UXsHGZxYKzuS$Y8 z;HZ_G)7>`qHp7}8e>QEa`K_~m&(>bUHbG?XR}F{BZw74#^ln0d1i*ck<(=lUbJ3N; z>y-pj=K6)(a|5Ah-NiA&kC`JzzMkjD&cV7^u3G_OPlrPnd{f@v2!_;XYbgfyKH$o;{pgB#JCEc*ZM|Rj4|T` zBL!^aC-}}gd*fdu;%~C(H=p>zmHj4~MCE}B4$gK)wtsO_|L|%+6C(iYpX}5N{q;X9 ziHSUOk-ylie>DG8dSTuEQ8IEddm-e2BFI+q8;qu#0(O--v69*>?;AIlP%JxD_g1Gg5t>TxT z|5FrR+FtLK3=|7{F7y;PWfbvEjf4RiU!@=@QGMd?GJbU+7 z8eXjYD@s7kzug6KJd=k1Vw8U^|JP!503a@Iy+3CDWT!=cr%cVt&dh}Z#G+vK=J~AR zU}t3i|CTk@zp3tj(gxxH0sqPy=-)}(3#I)Z)Aq~af65y-59=R!V`2RxZ=5`Af98$z zf0H-vm%Kew!7MB+fM=CI^7g+9#UH6-|4*r7`6qGgpnoL}#KFn^J8>-kSBd+F(|=AJ z$IC;^&hk=ZEC6mcHUJkFH-MF!<@p(5`*m_y`_-GE=?FLVEg<3;wD z{IiW0J1+-2J3D~qnIh)`fdHJGT)&QA?Pp${gM;(e_)Gtn56{H>?>X+j+b{j!axeNX zZO&)im+_Z_=h^=6_RIK-oxfJ}m%q;|ewKLg{Lkx))bG{&_UmuG*sUu?fb^}qke^^6fOS$tj{`wIlIJ-2?XhU*2U|NDObTH#CUU%mhOj?eM? z6Dy`+~{+`ozU|Ge^l!^U4}W_`i8m*1~? z_`8OltL7OeUPk_J@#012`8nqLHa{pk! zOaHgr-?;IoonP+%Hu{&#zt1m=`c>@D@bdC-zrf7P^;c;<-|D6RQvARAf45&C^kw|F z-ak6e?){eg<>rekFE*Z!e-#b~=&w-z_nu#X;`fh(i}TN7{10Dfzu(*RUxUjON3{^tkde{X3%@35LZZwCL~;C=pV z`s=go-;3A(kEQ$Pcj^CrTbPZVi9KX-|b+u%L0)R$Pi#!JgqQx1I; zUsoTv(i|KUl9k#X2LJ%@81Mi-Q3$v|L?;m_Tt|GZua0Zxp+!NH{)qX}yv&f%KMqRr zxk1cql9~9X=83L9PfJf~ZpUsOf3{DwPuzUHS4yhya7(^vJ1RW{M50ZL^yWQq3mVK@F z*&`(vWf(|-)#7$iSpU|~`zQh?SX33Ay#KBpky(#+^a(xJ+kfwA{*$r(X}lSQIl0DrSQndl};FxCdN?811*UL!OGIsv3fRV}lg;G1hgwl_Vd*?zcWdtGMr zz|7Yymj=z`S6w)o;x4BbN#!7sD{@hT&k}oFUc(K$DW`f3=)-;zgN-Otjm{jn?ASi7 zX33wQ-iC+;?0;XkNOBd}fAgja^{K^b-<*Vw9$bU2i-KUp=YW$vQ%cDG#|9?J&Y_~g zF@|=2X8001g*sYqK6juStv!fCh6AZSl|P8PVBOt$XHtYV1N3Rq+qjpA3_)_-DcC3` z*K(KT;|IaQ0#pS%2XwTrfk8U)Bq1X^g5J{N+zCUSGcit>`@HB3<$Wi|ovmk-OJmo$ zP(Owwtf$5%-NB&-4tRspb9G@3kSu8!MYjO9!AdY)u(?E{gu(Ewu1LX>y#0#zq@P<} zqfO|&GmcdVT@UmIzsILfp39>{GmeBEhSMT5K(aHsV=4h#=T|xbYe7gCYqbb4RiANJXzyPkCyhaG)dCRQQvj#JEsFHx$CHy`8ct67A*)MFV< z!dY1xgYXM7xnPU_9)6l07gv&~N%KG_#BN%97iZ=xNYg&<`X zc$6P*s2ECu^y2cCoZ8OzI=l7tC#{Y`-tDPv$59(g6Z*(VWFYbio0M1%nIAlQ|$=k)0V%BL}F){ICP{puO(S^7dz z<}J-oL$xnBcykn#-2(8OM0jjP?~)%uRNKyeFQy(E$e}{*cdHR zma>uB?T#)wiVUvJQkB;54Wp{q*m(l<9Hw)zvR@$D-_}%u%i@IE9MG)bh{;^~BH8|C5xp;t4X;>z0xnPxJ$NNt<7gnvdwpchcJSa~8~-f$P# z2#KQX_+eQ^ttE`h6#bJ_@q`)sQa#(HLbD7H{D<9s14n}T)NxuC95`~aQEWr%OmIzA z;dhG5MV&UHG7mp`uYVvPx@RiKAP@SJM3*XlXE?5=ckBPvy)g`!{#{ zFl&hzQ$)0sKWN^Vk3ghvy&`8NIeHaKvgGX1(g%uaxU)6H?YeLVr+tR(nx^EoC%g(LArG*Jjak|khc%|6y zoOi7ZwfAQ!zo0z~QRaQ7qbAo@&B)jja}!$7)4>3c~A<8K^9Ga!(iYufRq~3bEudZiN6+;wO-iCKxcG`Csgh~jf|B1#7hkzm$lF4j z-z@iy|1Bcb=Y9GmHg)ORbnvLE$Re49O2)o2Sn|NvYrXZ7v#0?8f!-)Ia}mP=Jc>9D zY9J6-HMU}qdZcb>(-zdI>kR`okb5y|G|z2VpE7j_o>WwkU4%fzL>SLD+(%7iBaHjX z5r&;AgPL!+aMyk5J$TgkJW4~8S+@1K2G_R#(Qv!G2rVL-I9xlLyVxyCAdt#f2ZYwt#CfdcXA4b&X zOxYl(^6C`R)fh$BL;;M(tP!T!1n;m?d~Wy?Sra6^BCVKQto^xqEJr8(SJkLF&qq6y zV4(srMoC=pAVfyePN%%!pgP~synl5dJbD72R22z(>ktQ)O9~O>E)rQ-2N%%o$7fhK zebOO)h&=6*O({!Xmj=G7KR;z^E*N33qUrrM~9&>jnO6Hl#)92T84sB$UrPT`A4wQ|3~xgggG|DbfSo8A=ges>oJ(uSjg-=~a1wUb#)fdQ_ zol4V7V^627VW11*5*%p@qZR?M+}+SgF$?v6rDrlb@?Vi4^`I%81v{)7Jb~tKi9YG7 zmlJZS3=X%UM54?JBA*m;$-Htj7dD~QLH#6z9Wg?-3b8>O>_Aon#)&=8f;|@43Fa~2 zuV7Fl)H@nWLXt6?^fue9zjJy(`U^oq!)+l?%($7R&ver3Yof6nayg;vh1xpPuY7lC zY$W>@7;LPb;kdhkJn)axurw&Mvq?!r#?-G`Qd6&B>gHq;#D*hZ(bv1qfpS2TX$`OZ zYYRka)CZgyhs0q=n?S%xtIUQa=b_7z&~N3Tm8CF(bSa$qw*FiY-wnDGWfoYY zETsctaVu%~dU@MODhG~+yvzkYW$8>*bJ6w=D;cG=q)PU&M+mE}|nxEMr@jdAJQxVPu8Ndy<7ak7kyI7n0@VUx}(Yu&eJC!P}u$%g& zjG|x3b|BDFCW(TAbKj^#O%LW?9o{5l2 zE1~(}_DTf}1X7~&6H$g`91h-3Gp>bceFlzQ_;_6SSRLQw8VriSb=s~atgk}0+)%dM zfJN^<>FJ^NwyP)vw}=fx1DJDpF7!c4l@4`rpK=GZ(AH2lFyW9Q#J<8AwW?jHz&UgE zk}vYa8o(iBAIpV`NQ!PwP=f7i~Mba;Y!)fV;V2 zfnSGtLL57j;HLCRK@HQiF%a2~oi>urnWc4bUp}sV2o7~fm{VrDU`QOVvmM8eqOT)y z^Su1e~l|1>$ER2S1 zgrx=OZ{>m4;3CUZmPb%v1AdS`Wqmi&=bUs^Uk!47PM89t6r$dH3%DcW5kiem7`nrb^;S%-(7tp;;C#^bkE$P3SO-y_$fy@mxx2`s&wVq9r&%*QI0U`+4t~F(c8_HkY z&NZDy9D*FW>x=!G^1%<5@`iekG|bcjRF`<&3@K6I+mv6ESS8o=DW?^E6_kdqli) zLjpCE{RmS5cC%6jD%X~P3eXs&4Pl5pWQi9ar!_i9vo`=Jm5~G+XyL_gMP+%NM401$ zGQq|THT*_V)#`3I%ka3bN{JAwik{CH)AKAQZ%8=eZu%R&-$ z+Q>I2mnb3~LYxI>Au^|^#;*brR{g{Qi+hhBeWGuG-{@*Q+WlZ~e66!@Fu@*M!0Yms z?v<}vZhG!~Pi>U_(ezf=#NW3ALl?YMa??sZWPX29^n1zkK2rg{||% zl>nYG2Zu2FTkMV*tY;Fp_bjW@joohUXROjCO%wD_q-d}=iCE*Y{e(EBO$BFXpN2{8 zg3B2Jc{H?Kp!K0^(lbFiconM)i3uZa`NA>u8sF7ll<&A~tL}{DI9%+IwJK)Oa4|$8 z=;x|hV$p|)lBw1pzfeLkz;SECW3pvM zW>0@8iH=enV#B4hHr~RFhYKZOA+siP?W{`7mqF z2Szg(wJxXdO>P6p$2jOc2_vyE9la9^t zs98+kIGrcPo2cyRG>_?-B-;G6$YLAsJbw~FVu0+>X45>RDZKngUexsvQ>$g@1`?p{ zH;5W^3_NRqEErPb3{>Z!+^ld{j_$ zS90)17A_g#8#CC*Zhk}IIBKwdCJAM$PTFQQhD>%n!gg zo_M<_iKaW;2A+`fwCiC<_a=0#WJ$OLxX#`u*lN$QpIju}kQA?hM+IS_0s&2)XkHjx zmHL*7R}<*L0wWrG3Ya|tF1ySH2gjb_a{-=};bs9EMiYbMeluTXGNfR%Z*s<)N;BW` z_IXwg#cd8Ybso=G1%xlmUS+b)7tNT=>0UbUehPKh5F1GUa^sMGZ+R)st3gU!9uv)> z{DC%#<0;Ub|8u@e3JV21z^G#|w2K;zHN=WdU)um%)t<)T~)XNGVe5!6rMgNk$|hsQPzH_I^I;|R)*jzScX$sS6nX=xBP`J zOWU5l63p5!(L{hbn<>JCVt z1Y=Bup@t~KF}RiUlDlkU^|fH8C!Gt{IoMCIs~;TUwfUem0J_i6=BQ{6GA4c4TYN9H zZ(^iId{w5}u_(3^Y5wmSrtGO9T!?}=yOvNk8P4n_Q#|oBRHJtDz6&d+Z1waGv`_e? zXoW@Ad`3t;Fu!8?f$fF<2<=4}QVdEdB4oo95usN?YV49;$S0fxg1Z1aA2H@lNJ$U{ zej@lq3YjIdW{XY4%uK0Vxe)b}e}>Cy z533-19hBV`bN4Q)X3g9h1SH3Y@FUWDpB3!(h|?am8uUmIvV86rTb>4f5;}^_vc|0q zSKdM*5Qvg<${LoW88oKZxzj?R;SMbrDMUqr0mvLBGUs~ZVs5vKvkj?zAbGhy)VI&u z0RBx%YLaxyU=rZoo#5nAhyG?=r&Gu`oXDt%jV*oYe-0A3MIZ4`Ct{%xLRt6Lg)7g9<~t{7 z!(R&^?FVito;!I*0P@IuTk;DcA}QF08n;d6dnx~@x>`wll_P<#lm+im1;GJ178RuT8R~miJ4B_V4Vcss*wzi$ z&atG7D_ncTD%e+25z@Svg1hN^hA-immT-lv0gu}?5GTi9WeqK~{k6T$_`fV4`#^uj zap+#&_)NGKMhf=uIm9n)tv`Ex;_Rk|T0BA;6Cau35P%zJ-(nHhx=rAWxrlhcP3%`!L|=S zKG01_N`tjJf$v<^qJw{zvj>S8T#>hrU-qQknc-4ucW+xe_=`rVC#7^P*Cg9M@`>Gi zw@zjQ+!6X7r>D!tMA5@;mI6h&Xj&HKXP9gWc_)qM0-y-Z=@TbFk`ivofFn!L;55|#_!;b~c z|LE`55?T>>Uh<(Gar@XPbw+C6C6)-^NUI!U)Zk}M8xntK*|>jaBP$U%4-LESq~932tDL;td(Ky%XYdqW&A%>lH! zb(s>70D;$A1XC^88#v1~JWCkF+M1bGE0$AK;eqD1%PL2Ay|5|Km*_&4N6SzYqd@H&oUbpy8v-^IRT-eG?KCicAq{o;Y{ z-78sdm~1jAbocuG#7-hXD(8JE%9^kPShi1We%-Sd>ms}QkBl;%-5<755~b`(&uCd+ zv7$^_;n}DxY`5 zOx#Nyv!<%Pxi`Eyw>Pn46vW$trMa-X)D}9GjQS>)&6S+Iu3VX~JrUj<{oW5iS(FlcHTIswka7VRq#s&`0i-$1e0qDGrJ-w zk98jLP0CpP8;(elQ%Z*ooBjn(xsH;${mm{vkPSL}>H96lf*aEFE*w~0Z zmgaWnilsrpprcrvN=WwtK7n@xum@O%S1yv}Oj@Q{^DG}%U{-VmK7O|PFje^ka{(C|Cio%JSpaO}q^DT6*rT%1k0|9bpa_VA1rkrG!aK09BDlYfE9ddV`YU~@tltG{Uw*74J)v=yw*oE>% zaD2!!sqfy2+@?D?Hp~(}J{-w1Z`nXqgvi--tRRhc++fZ(H z)SkC=r6AU)C~DKc4)NQ^&$Yf3uZccI_`&ysxk(YsFg@OS+7F?Yw~nbjMUD_zWbNXp zq`eymM=6xuO$teb=4@RtLuVv=8=z9vX@ix8r9l;?D@X|ms+>wTRx^eR?{b93FzG`} zdM(0kY#IUwdj3~AwK(O|o4|G{GB=$DO@?$vY;CEH8^1zCA$%QhjM75YhPysP-;iwg z&Em{kE1HqVmKZNvJuq6cCD?nN27)l>c#p5eu7O5T#~?Lmoa&L^sW=@eFRu z4<^`xrAK>&>GSXfXkA=PB6SlWB!1EqZGD5H4qY7MLLeJ*Q7ifI#(pD4QEyHI4?#lm zd%_+t`aXEwDIC^F;-)j=>%J_j2gM!L9Z%1;jc-3%l6GPLI~V*nS~uF^bMl4?^o`w_ z-2(TKPC5{?G6?qxUrKVe^X4^ZRpcD2qk9Mp2Ql*yo%p3%Vj>-mR&G7H-1#oA@PuMm z-91nOaqF>VbXQkkohz#!dzEgSX1{PtPpFG0h&EUgV&}=j#)+ZEHEHAt35$-56177F zix-1Y7BcIHQl8M0KAAx2^<%t=&SuXffH0yr&#&p{V!G%pa|auN}+wYHJBOiKT_? zA{&+Ye}J-CGr(h5UB$14zjHoze_8qhGXit8Qk1Qg%_3+mV5x%JO1RQn0k#5r2ut?E z4*8-qBe4ZmSB%sXGGxdh4wKH1M_Vi;_;o(C%EhuzUbKsK9FK(DTj)rhIgtmpg8p$I zPc8N!mM;z-gxF`$1^x&yL93y5g0UDh!UvgnpxJ)n@h&e@ z2U}N^e&dHpr>B-$&Q$?gIwN7+DqDbs3F3C?hdiXa2cB}tP3}o3BO-$-x~fd4%MX*O zid<`TeoGQ5dU3vNnNN2GOS`EPJssS3UpITU&;6^f>weJLoXiSd-%h-a?kKIht}S1a z>3B_O#b`fGchX_oUjzhWy)s~af;cOM*fI>WbeP?Aj>PZ9XjSQ>Yar;QZ_sY!4Y7#e z?>7Ge!SO`^p%JHkA5AcXyMVl{MA=XwcZwXk8W15CiNTw^MKVQVL8z5y(ava;=_Rfl zO!Tn=NDI^=!m0$JHKLQ?EFD~ODlukIvYj#y`mwhAyJhRBa_DMV6 zxfPu$C>L^h%J*KyNy@5h&w)QALlBdLwxviO+V`=ri6it7BsY*Q9PG|bg!%&3&<8iy zp)BLJgeik^LKVSGP~1@iSLD+a+1bcz@F*ZUSQEwYhs!Yr3hya*O#`gK1IJjYGE7wH zpq0|8#|RGMG#3oHe56M&XA~YARE56HD)N1Q54MXNb;xL2!BFfN*JYW%Yy|m3+-Jo@ z;tlCFt00f8&_Hr+fKeG#yWz;R+2meI&?ncvwp^iuwf=hFgSR<#Y0(Fxa zeqF;?Rafw@vAM`HS4-M|e#EW*ah)F8ovv9wNwC9_qA2ezZon?@|M*rU{_(DJ>lItj zp3ZBXaQdXXVoPvB{$VJlYRyzh=)LCU_75Oi&*V0k>)wgZIgQOs#jqa!&9X3tnJUMwW$(W$&u}xokQo* zOwhV6y2hw!V|{0aQ_uQdJnQiFZb-3^E-CF5JBNdx&!P=OwU@J_;-Jl!mK_$QC^-_A zI~M)G`)LDyP$4r>R*(2}qrJNZqXUBPcG$Ps6k~qZuC<1Bm6)z{{#^26kVwJQvRI+7sZ+=? zfZD7dM1-W`(v!!;1__4(dIH&%>J#>wRUB6s#e*stibwS5V8TgcWe%jsXS$A!@%pvm zIQWF7Q^X|-o5)|0x#}cp%D`(J&=y0c-RQXvBTNY0UT5wa zl}>9iES!HQbbF8mDi0Jf?D=R-Xr9b%u#Ja!f7t_DY5=WgPqB~eZLcTq{Pfu4BCYJW z40!+fb_~(sdpjP&y!b$)Zd=J{VVd@a=7VO0(_{$0O_;#+LKJr>87F zEVI|qpPDM2uTyJpiVWPAA-E#jJ$%OEPjLN@2#wy{$hvz&5##95_YaBVn}51+GJADb z7HX&N*aTKw<9T^b7CwXRu_x>+m}iO2;e}#l=RgK~3P2fNkxXDCxRrjyDdO&l4xEw8 z`p)1isgyi4v2zZx(6qV=X)qES+;Qa-P>{qyK>XNAlsLe*yIzCg>>Kq4#hri*q3mK} z^O6>7c)w*CrmRgnS&Y7~c*>ZXF&QnrF`J3s?51C;eg!~MO0H{#v1)kR zZ3j61(v^vk(`gh-coZ183s?xjB!zFvi{9=r^2ojNSR{pWhUc2##e)ud_>-5SV0Rv* z=g{w}2Rk#6VFZOC1Tm%##&|QcD&~zctuw*zk=n8vVZ3384Zq+_-Up$_TH%rIN$5}b z4;84;VS6z&k@PO)fidGi1`aD@oFLzXy$Oj4jR`R$6J!kTbA+9>MW*S#U`l}q{Be7N ziDGd-X~?B%JKZI0(w^W2D`r^u0S9fx@cXOLBCv3n(oFM@1`*2nElu;$w<4pY866e#{vLYSlv@8p?<#pynA_FTFluc4tzEA$sdX_>=(e z$5A%8#xi@9PN`|u+kV@af4g*)VbaK~kB(5fw>mceczf#uty85Hu+lXb@PYRO>>0Z9EFpy>zuu)Xe<9Gi9GCRy!xRh5TV#NER*(1*}3G`)L%D7ss*B9;8tK>H`ORPx66e9Ib&0 zxxpYeTg!7LX3MW8_Fl0qeQapb#VsyZYd6{a)eF714t;v-#xbL>za3Xsb!;5dxsx0z z5-+-K$0ch+ftqDZ@3LBdXv!nqb9NZTRWoL+tj9z5q}$7?sz+~}GHY8s^_V7SKINu! zOMovu2aq+>KcG}qxKr6wmP@seid~S=IF+s{Q@is9>rKuru1#*m62lVXTEkl7Cc|U0 z$F+~xUa`IIQpv0+++uBV_ggpEZg5@i-p?M&Q^mszy=!EvwX0n>m=5aY)jESQ=s`1x z2ZJMK79s0Ki#?2Pu|<#0vF|_OeE|Z+5mQGQrS9w)L)$VYdpc_4q z%?)-<-=-S$^PXHqgo)|>b-5ft{{s%#1No#i&k;d^9#S* zzW$xk#+fhO)Bo=375{kT_vzjHU&mc9Y`K4?Lx{_{CFyADrQ26;JFqYP?vCC~m#tay zOFSy|0-kxeAs8>GveE_o*C+8`46DVN&VE3AfJQuxVl-M{T4-Mo+F6i_a0?8Jps>xb z-TJUeHdiMXJjm}?2p*k3;4aqdh`-Y1LJDJ%Tkr9Dh^IkOBF7zIrCn7x^69Lk@1rbK z-%gRsqc9&P;dW%^%_U~WTm>ivB<~Bimzc3s8tlrk6ga6_#9mAhJ1!98oko+vL}Zct zg8XPcBRf+fGONXAv0EG#Ru&A!_;3hEX%uimly7p=C*bT= zq!FSm^4;!kPjHmw$U`EI9CLN|(Q}->6>;j4XYC_?aIk*ZitNr>k}+VsuMt0#ONNUK z&*fBFtgjm`)Q1gpL(2_;ViL%nq0u6f)oap5OZE_KHRV0Xkz&_1tN3tO(>{N;siJQ3 zbAKuejr?(G(TJc^r{+|yaB~s6JnUK2Q?rvz55523eM5Dt?krDV+gm0i_NFI?EIPk^ zK6B+vO8|&2z5I^;Jo*E&V%XnDVSfv;Pi&X7N>w4_SC3P3GESxfW?;f>SQS=>HItZ8 zs!8hks@19+RJwHqTZ^A&pH{uhzO4G3{akgDJE>CXB=zPIJQlw{Jh`wiMIz$jd{0=f zzzVv_N{<2o>rE!)b(u$=CwPMXfI==06U`(onS{g7hj7T5D8|JIYxO#>j_4XZdgKL} z(2$qsaTJ*?g^?hM;0X8_L9@r@tIw@lhI%7bSed{&>$nZw&voZsgv4k$9b=M z>@)rJSW`4Z&3YR)yy~&}1LbF_FF%@5@~R8&5Lr@&q%8F|2HgjW&_K_5=vG(q$d@~RtmZa^TGA)(-~QUv znQz`wx}eJ1RvI98j32=(uT6ik?N^x>t4Co_dGjV0{@Um+F@r+%zx2ja>EAx^V)}#4 z7BhBs#KWNw=M9?1r9ZD5vFNEKo1emE_)%UlzMz&`EMT9?%)qZLxKT8=_`weVOVOk7 z`|b3U%1-gDt9CacJ8$1*XQ)(7k>Bt4mkFMz-(N?YO#oLX0*t=STerK8X%;+n0JG08 zr#;9|_aNuxyW|WOJaXDYz#!0VFBtBjKy?URVd_&0BYu&NpnM)z8u5fERefbp|+kjBDQt3?P^Q3v28;3?L>nC z@fiL(c+6kbI&yVRU$4k1?C!pk!v!?#gwWVn3le&$UZRayE(%%ZYin&Gd8x2I0416qs?X-h&FPsxW%X=6-H-dvluliL zCXXLRSzS!Np*{Zh%Vg5Wn9kc#M9(OYneIt`VdNA!TkQ;wjMdm+9o+EbhZerL zXU*PAM@^qt>$a#S8=NM0VfQ=aUAm;Krj>QTnnvK+2hhmO;Q@c7VkE`=J-nepfmK>{ zTs>ke9;;oZU5z%Pop`5q*T@upR+G~1ACVY&3K@5Sb}P0OkH8JuDeZ)*v zSS@myLgo}l;*mvq-pdod!MiID=Q-*}K0~HTy4jJH_kF`=)#pG-_4PCi9~#7BijD2X zv2NW}{7dY{VtY2%ojWZ620rVcAXn(-74?dG$&Hl^EGoV+bur7O6NAc%d@PqJ@!t?? zjsS9}Pg%*%Ov8VeH@0%bP(w@p%=B-{?5*R6rk+K?548aSYw$l7#jMkbVf5tNnYN*) zt}YTnAz7X^x(aWMrf*$b@e}Gc>wJy{=^1$2wDPc3&47q6SPe8025(0TG!a5u#Hu-X z4Zb4Ko6l|yYz;mdWX>@61b>#TK^rlyKoC)~?+x{bc7?c53Lg-8!51OG^H@O?q2Hl< zfl+pg)-&wy2$$rG`MdHNdTcPUJ7?FPI57lv5QHU^qRb8 zKFEhSy-{z{n^6#jlvT2`g=2A@C+yE*(OXAZATD?+{a`oUD7GMvTL8+BxLszq+l8vC z5GwNInLVC7gsVJm#*3Y(suE;QIOK5~4GM&+Yh1h&I~!C-)Ssw{+F2vnH}1R&X+kyB z>QY*)6*YO@NW7S~8E9Mav0`$#_-HXHc2w7-@YDexNY@lD+(4P4TN19o6|ogDYL>zV zNhdaTiAS-!fOjuKMirPF;JJRvY{=#=dc? z?GjsAV;@N%!~!)`5`I^J(*9rZLsG)+?O;OLI3O68kH9@QsawnhQXZU!x4eLFghU}{vkIRn?dL70dn-44vIU+WzLa)U1 zR8)&+X|eXI4v^)%A>u|Z+~LB=wH(;vDu2$j=eu7`7>L?_MF2ULUYm zy@a6BJ}i}Dv5kEFRWEJiU&3@3=m#w*lMrL3N~GuY)o6v#>+2W#eYc^l`mMrN-(IxW zr)34!7iII+epA#b<5QW7lBP;X9tHb~{_K!9W1ioNcexXM!mU75s)1;uzjb+JsYRP$*0z<2HO3*&*AZxJ$E5 z`zU!-^9uKh@@3C^_&u$buyTb=DO1_7jo36ct<|HP@0jntoLi$=?O5&Err&4Z=XuZd zokD$~PFIN-Yo%OiR6D%OIwf)dr$lrh7Y`)TE;5*L#)U?K2zsN}NQ~g8Q17vi`YWRT z8~`KzoRKyS4u7je-IK}GJ*mfee8>|vhm|2N>~PwhM5fmoLx3k&2wN18v&kT5(C9)~ z>n0Fmlgb)GP8OnAtRC*uFH>T%YcOy*J!##mkQr;aROW=JHr5imu~q|tq%vP74YlA9 zehmpnOWIl`l=f(A(eQC?*Xe^>AT1mOe@#w&LVno5BhD|EzGI>i!&u3aFk{25_-ET* zP2Z8e{nh*M-MHr9>`CjU?O4#(Ip>nQxmlX@vh?rL>6g-{{`nHt;$nQqgy-%_f0TaY z(N$$4cKi|Q)XV4zUj;(!Bf$Mm5YtBvAOW~vQ!7yJpQ)~$6vo@^Cv7K%fBD&{!i}&7 z{13k$ybzf`prxWb;3_tvVz*mnG7>O2cpvtC++*#x-fv~Bo8w^|c4gg%A}!MJ8q%TZ z(Gbm5q3}891614&`w{e9uUj%{vxhpvPJ(nKp63miopzhUMq~lAFOI!Vi2Rmd91FHQ zdi+gyBzH_l3&V!SS?Ah<^u|!e7;+z|1W19aZRY8N>`6G7et7HbFMNh|*I$1fnV-Iq z9$TL|z&?7<_0Rb2Wc$!Qa@+Q8H&YCDq;KW^3LIudVf-7hja4snEOajju|~CCWg4p= zYuc#2NxzBTWW33=(V}X_Evkk71)&|j2 zDJScmQ(+KI?~8W5p+X&jhwLNNgGFK1tbf|5-3kQxF$r zOxifuMGwmBPtrq#?3cc^AY8k%j5>ibYLzDJ14ea(LSexg2;n@9D+C@jL}4Qo{T6Qs zknh$)qES2bwh#`QAOZs5>*?Q__aV5uZ#a*#-EkF1X^tZQG|^ z(cYukaD!rGcweFdufsB2g6~K#O}~}?XL=pS9lAIDar)uMFI)Kt9*;-ePXF@`wL^ao zJQ+Ym_;RsvnsbG7yM<8%?1Ayl(eBay+3vZ1xe;+l#&f)kEr~C1t#PgK-xzqq^?KlF zT(QIYj`LskA00nBxwt};BJb{%xK_$g#)XVnOPLqUD@mjl1^nhfz`s6l6Kp1mx_z#G z|8f5bKg0Vw{73!FQ9t(EqHcd694dCD@E=860NkfwQLzcwUih8Q=l6rDt^ns1bD~m% zqI{G@KenY9B3d=U5SSS`DWK7GP|aUF@_^)hP*0Ga{z+dm57Cpr;nM@@P1#VrbXqu6 zpL<2tx1zh2zS6AirfOgEMeLHD1wI_E+<7+ zpFF;GY1@@}l;|p`N>59V@2b6N^Q7AzAdA!2pDRyV_g`_>oCa^YvdijaLS!-7G4#vw z4NLC6lUinrGsjpTNS0b$B-YwXrWdU7F)|%i>g6$6iCu4t73pJqfg$b}V!^_yXjN=M z!KQ*u(I+ZW(SsGH+S6x(V?_&^p|A2*k*X(3!H%9Gc)Wrads9GNqw;1TC+{TACoBaq zy&|kv>-BE6ThFf6uP(S-|A_i&^-FqLtU#}316*Y(6R5N(C*k4$I>X`V+1E5F%!@js za|G~z#R$E^3$71T?e&%xJL*!nc27==A0MP5;iU8viR|k!TWxor#Pswqk$!-fdsRmB zQa1NZjKHWFJqZ`^y}1X^*0o3i2Y3ebM+zhYwFkZT3~4DJCQ!{mQ30AW73Oi z>9&|z=@Vi4(kx&oAO1QqoL@}N`d+b`c|l#^`Rw}nYb+iy{^U1PE=d354ROVVC0=Kp zF%&BN@wVO#e6Z^O`1MB;D0cw=EqoQAtJ+^Id@OCWN zJM3HSj9uWpSnzgu*L$~mS?@s{MRxMb0ZE4QC+Q0}9s+ega|cO7^~3Kar(Gb2c?hw}rV0>D$&X_Zffu^BJdvwbo6( z3ooY_Dajn;9tMmQV@7T2CfL)qPsP)JMd;86HrS3+JWIQPk5#moTByC z^|k}LS0nF7zKrO&fJMjq1z$K&>YL%0zu@~mKpxfY(~)w8oF2saz0wPemUNW#mh39wN)+@5B1GO$P|y*@(W~Oa2cOyR zuO)k>FYBEQW(8MlSRna*5v5)g4u^Chbx09M`4Me^2W!Qr%-6(`-VYH)v817LLaA>B zD5w=C>L|)`ZemXOpM%|iG9uCd`-+alW&wv>&v+H4lP!$E(H?LiI@>=vCz zt5K*u8lM9kLNS%OS*)_jWrBhpUlX~!$ObHFvv4w5#O8pU!=jMOSPka@lg^^xIJr-& zL10HJ1Hcdai^f(2Y&Iu4h>MX8Un?2~jRS@SIp8a+~VW{C3G^aT+8qTzpz-orG*%c@x&p1hO2oVP#(spA4u>2} zOozknbSl)y1zh+i7WO;)I>~1A7=#G;{|Go!qxbM$C&;GKXo;s(WD8iNPGzniu&Y(z zQCuM|DgbVDW{R4Emzh}{8shfFSy6hr|iYSXAy zEFWz;m3QBE-5Xq4p*el(AeP~4={S8`<@=g<d zlPODymGW=G6=qp&dU}33k-jyQZf>m-$%N7IQmlHnrns!Bk=)jnXR#Ok;|~G8W)e^( z6AWp#{P+N~_|#qOl*dNNLLrjx312a^jF7FXCxP{XRdS!jwtDDl(l(>nT@WV{rRqSh z9RNyg#J7pB7{_YI@MH7Fc{k#Jjg%Es7u1Zy3-J8nb;3H|2eTH!drQ? z;}K7OZDXCMmcy)vLw0QEZ614A0C%CLwAxcs;?*OTlj5+Wk>?|MrRKc6QXIcohvN-` z&MZL3rEwvT*I@;BIu~{{0TpqxYM&$YpAB7$!jO7NOw>CZVr@-+m^kb*P9Zj>@Vhxm zif*9qxra{%sjYM3#L$ToS#pxTN}%MUqo?V^AK;z-a-Dwq^}nH%Y>dyT)8~G4&<)nA z^gXkecjy~+QWVjd8nIxsXV03tFt+AC*^`denXItNp2>p}$2gonEXOGA*i6;`tGzdY zZlg#Zhr6fMqmeX{Mz>^J8r`xi%km-Ljy<^?=Qa>R5-_>!B+g~WaS{?>6G*sOZo&@WNKzW2TN{ojk{xw~f6)6-L3 zRbACx)x*@Iq19DAnhRIAO|2SOGPOi+sd?)8w$hAizykErDZUY+z;5Uv45-%#o7IU<>@=bW5l} zAB)8b+|r^$VB0xW`6YRT#PVa_)0zBZ-kOt$C3ND0AbRM7oz75GgwC$rZmyMM#hzks zQ8X39POsA^M;FI+=g0qmZ;9^0ecoNszWDaczoLd(UvossEDW?q)w@eDB{Lmi_`29qCFi(m|1+c)I?=O(jFdZ-Ur=u}z z493zPuhSEYd%W>jtdPe}J{ILMc~uWcr~Uo{&TJ7%^gmAJ86N5EJt?1)}E z*aWZHT$XS5VJ1I9q^HYCPdlMUF+s^5Xd?E#x5-Ua<5GJdYlbEaOnzuH;cfU1{x<2L zK{tnA&7XtM;;+P4^0(nTq+NJB|FHCsZOD_}N+Z?;2k&;7B!iTNlyFQ0Fckt078(U+ zgHYX;xzJbW1-rxK5TU@C0EC>apb4gpCa00q!kNS_H?jLXEKX*Q&@F5buy6wC17N%hV|o7CNK(fVO3zGVi^jxzaEu zdqL&p`s*rDBgOdKiIux*;cxV}rnfdckbWxtdg!(E>y_`P4VC(_`U#E+p7E74JPU*? z!F82?fVL9Ecj4UO06u1#NGrUQ+wjq}}e8hbpOUEAOv>K=vr8jXUxwXwGzwFsQc z?&8=Q<`bUU&+1`$g+K=ORB>UdIF>3-HdNeI@lXZQRg9>ZTyc8EZ!2!9xVz$!ioaF- zz2ZQHsk;JJ)H{Wc&>^f6bevExOcK@!=Lt6nw+c@SZwLmHkWYFCIqd@Sn&Wbc)R
GAYAY*7UtT#Mr&;?1x0fR*Btv>jWA}Ng<8abtLP9xWNy9>H#*o0Ep zTG(_bALge(wN%Yjmt!Ro%XZ<2H8)_>z(H#ZI@09(i`_#a}{gP@u_b@-Wp+J4<89{2WG&ImCo*tD8?i0O! zF9&EC1@+3QsVj&WC8V>GK{6CG#cSi1fGq&TpdkRmkvdcz08)WD0F7ai)adF1THJtY zr)X})kZ2-I8(EP8bWSa%t=Y#a)sRzWd*^)f)z~Y|qN(<<1^$GM*K14r-1xiBZC%_4 zD?Li#$fCT0`1ty!<9nW4e%5s!i_vM$%MX+<8Pht;xVAnK@}mgM&2o}@l$u>h-2_Hn)M3;<+@gHRQ4v$Pjk7iO+yf!1X^`ug)Xi_bj)QTpD75!#t0NBrj7_vx2@o3Xx zLt;8Jjz!}%GKAv|L?Mdl!U^aXeF0#$$VLK3_;>>5nF9e35(0^z^fB`{9z8>eoQ}H< z-NE(I&BAT?R^b7iaHepUfm^RzZ(J|0k8Q?RMR~UDF%#Ofs9CjZ^%1(PMVGJCb^@VfpoU`t^jgwZ_UKiFIQx$L%Z=6(LF}|!SF^ZU~0|V<;^q+gp_Zv&AI&^g|h5x9!3ptrs3AWliZV$*WpL- zT~2QlU@=$ZJV9PySSYVHtTy%pu5xU3Y<2c=yPdlNyCY8qo{!l8bU6UB6!e3Aghu+| zKFH~y6LK)*AeOE7a~rgM?2ViBp%$IVYJrv%-3Mf6xvz=s2a<2FWvviyf!oO)dD~*2 z5gB1EkPEnia=liL=DP}0*?v9Lt6q1Otlk%`xkwH3X`=!6Z1IZDOi$k5!v>@SE2IV% zm(jMOb_E-ov_NGV+qfde_=lsh?iEAvFS%eH)%UmxDe|Z%yB^xN;MCVIy=!bu{bU30 z@yMm&?6mRK6U%0N;XQ3F%zNt5OSfG%t9Hzk_9maNV)9L!zNk-?P+plzcx5c%l>pJY z)+v#z&9|E$G(X_h+3nQ=2uK0WBc~05_ZB(uWJFb82>;v%Z{cMEocMr{+9ZmCiSFR4 z_&jSvai^YC1ysAQi2A~D-XexT3&D^TPKDfd2w`4EButiJUXBD`t(pD|*PYpt z;W9GOrxeOBii_Sl#AW@GPV$x1oZ-z0QHoQ-L#c1nvjghv)`5d;@ReA^e4ENLIi@KL z7nvQ=SR}^r_PFXRZXtR~JOPT#B#GHV#Ne=|6528o;gTZ8C7Et>x1&4UU9>&3C&PDJ z*4tNm)4*LoS(khe3kpU;#=K!74I&#Y_OhZ<7h9o&1Br2QMo&#ad$@JZpgM) zXj7TT8`0U8kIWyq3`N2rHxvwp6eWY8fP^+2bOYGz?t_K*<;NX*+y?_YMU#Q) zdnVGmZp9bQx;p{oAlli0E?w=}X|iMibD*O$@GCcFM8qANX!+lbY5@R^ z!b=E;rj(PE4CEbhl(X_cFY}UKSCk_9Ytkl}MbSEH;@b~BoAMb%)Ggm7(%ZC zkkpV~B=%dO_0@hNAzw9{?MYD!@usrbh4&dQ?@&P++sV zjMmc1ZENPyc;Lx{&FC${Nu^)}m;|3wj)_SoYg2LT9K*Tk%aix(9yIJo?kf2_`t=y2 zvBFTv*YfqjDOezEn=~Zl8o5QjP&ljTI>T-0+eVv|mguOEIq8)Es?$fEjY)GxWEy)O zA@)YaUfZa|6WK;3Ad|%9&6YO8Mz(K+H`~~Ubc)NV29*1&uM$OnhC@oGEQ|V3fg%zL zF1sqD9~<{uTNta^n`o{WNo#{Gu%*RY*LSF&iC%LZtSk5S=sDOU>tTkDZirWs#iJB* z6G^O1nNiR>N*=|HY6(g7qQovrP<2M^Ls)Ufv!$deE(@($Im>00P&`#kpC^;6#Y$l^ zTTB&ZYjIcc#l@(#xWAYyUNf1f%S@9Ze7nDa_P2DPjd>!nbf9g;kAy}Ju*)em-1uO_ zK#Bz(9H2V1rV~3AIh0M!Jdk1%K$~=X5H!;DZ-PE5iT!ex$YCu|ITx@@O|x3fwGpJK zR~K7fVjRvQkSnUoJ*txdbx3Ei%EJPd^=g@NMrlYLvbD1~cLS{7S?2I|JvxE!Nsp** zy!*weD;6Dp#_hl9pEdU6GZ(Ks`;>j#+a}btPOWNaO$S#k2-U9s-32#U^OvFPmzNb* z)wf@M9Ij7BOSlr{tm7{Tm6e@PS~A|J^o%{Tv}{Y~dCxSi>btUQ`As`Vmj3XCO|Gms zZo+7vE#RgaRWks(8Wwr7_d&oP`h16}mbuDhCRS!~Gsm@3PdPotyqk~nI;$CkL9uL> z!csAB-)4E#!sSEYh{~2e?k&X@PDJH!By5Pv=18OGHBFIr#bP!JvH=>3oDL;4vdN*09hY?|BPt!rX!2iMM@~x1NRlWdNh(c}aFis3x*dvS z%RcXbRtF3^o_BDP13IWFX5Ui+<&y0sT&AR(VxqB-eMpSe`b)7myIoQ<1yu%GnvB^SXx7eA(Z6;?# z*jnh$I2IZxVxiOyGx1Y0R@bV><`e;-i%IknP_tuB1 z#}1m>ePJOQ<%8}eXxHMbK)un5hbaS%%5LK!uK*|(@OGQgKygq&uBx`Uf|8=BMvfJEJqoFw z?EzF*$+iRvM%1mW93GY{nRID(y9ZwEfy@~6oT>y`1DqU~6W9{i9?)e1O@WIEuqUuD z!274{A%cf=+&68S7YXe)VvK7eP}2b|tl<|y>OaLZ!D)QttP>Sw)~x4BMi1&61J06B zcqzM}C?^iq59H6U)t^YWydCEItglNSNKVd*Btu@Fz0i)e>9G(D z6eXR>(hLmvt-6?ymP>$%up{sEgky5l9W_$29C-4{Cz}SI%#O*Tf zE&Z&NTVD!GiL&AvV}x%S`e66Yq!AkX;I9%(lub~y|v?t)e|>~u{aAghsVMMXu- zq?ZWOW{3^npWK(!C4HIFhamzXFa_?{ywg(ZaxdL+Z~wslg9qCN-j~#={S|F&(%&~( zI(X1Ku%8kFGuWt8@Xx&FfsF=u5BHcK1~adDwhYw3_Pd~iOB-R;YHbmR&BN;*TChDE zFOc5#bHaM;jnScE1^%vK*2V>IpGDN?L4VL+^k73_{%GO{i|KPtpxsA5C= z;2)pxh6%NKEuKbbt-qad@i3NEW1P4sp*;x@Mzd3nonwdF?a+z=4$N#jnMD-pt;+fDFd6I9m*PnGRk z?Az@qV{fuwY~N$wXUBG0zAT%i`L1+H*2b8V3fG@-=TK;jD*sk6hrpj=Z0BJXob8JdT5QyQ~-k)q>Rl8bGt`AVRyDhgH?SsO;?HUb`=x zj>?4uP@LhBQMn-!wnk+~B%&l>I4URlxYr+uDD|*9D%TTODT<7W%FU68KAf%!>7g#r zP`*GHSYR~j^q`qu%2|u@d}PS< z(ed#n-=n_geMs?L?BjeNhQdW9Y4TH={gi$*{d^iJ>5J1``a@u?maDnyqERE6^5_p| z=ZxGplG`$J`^Y^bQHCV_Bhg6TxRyTdxSb(7prxi94mx5ASi`_j-qxUUgSG}nSHYTS zY^Riy87$ySjr`KaHD*#pGiCXHlNs}+@q&06E(t(h?>FTIplHtUB(Q7`KjXfypGxgD|%F8yoGK@9ChCGZLwF?^3P#BM-*ZR?%iv^}itCXC`l)GHYF^_43xaE7J+W z=e7BEXUdk zc-iS=LQPGB#*Vxg<$_4rvGB6>O4KD!95l{v;+a@j%1KNtsQO|x;foZfqx#|p_nT@B zJWR70(utL=X^4rg7~|0!+^gu7Jmlgli57`og?sZkyVXLZYf83AAt|-Z`lwZa`326X zY*lr~IMEj)VI$EcneG^*x}z&XG|E&Y5();bR*TWM03)3~zYosd*-xFH4&ARz_g2ES z0Hk;$(7=@)lj@dE zs#{7<=%fmzRZeeDbD4BEVH=`NrZsJn<)o6drd6gjEi$cXmuXGAWH_-BtukGp4LgZM zJf~L1VJ5yO-XBMCO|6V;YGqtiC!^W8ulTS!$+Ssnm^zuF>jZ~si?O4&H6*!V1yLs( zhIuCaM5hd@I%RN3r?gU?GB~7DTG>nh)hVr1r?mcDrz9p&51r#^>!B+EL)82$g#1gI zeY|DUq!UkZN`xd6l^)5S%9}nRQ8}2<$aC$KaUBzDZyCH|DN`w9zWH#=%7)OXgQm_J z{ZGhH+g0@eLWgD$g3}c5pYvcsg!U5zOWX{B-V@gw1e0IU4b3*{lz1v@)j?feM4fG9 z**G<8V%g5x5m}lm(ZW=APozJ>frt{B6QLkhA~!|2h#GdF^wT&;b+(D+q-vV)5-nMi zk47_XxGzyzvw})awe!?@Nlo{s`>nE#V;fi}F>yEshG+<-90&$D-sx~TIG&H^7vvTA z3J{G>NKn750CXGd0pQj96Er#@0RzYqfDWT401B`tF>IgIR1uxZC&I9-5Z1!+aJ;lu z#NGUQalO>-+rVEeUMy|!J^)rk0X)AYpdKV|nQD-+XZXpU=Jp7C!zA zOpTlT-m^>E#!c{rN6dzyJJF=;bcnsuP$N79=-dYheC0(>kkn|QrNO2VX)pJy*q`?pM+O)g{eScD5(%lgZ> z(lVv2wXC~LS7z7L60=PZBU&NxX^e1aJqB>P8YE!l1&(>_T>1I~6Of6cb z6)hXSW}^i+98fI=%8);qgAT@vy#Ze=75B#zv0`sB0pkIZ6y>E8u&^MOn|n@aD>U^{ zrDy5>@kY^5hi`p@#wgfY0{&UksADmWsl_51%ThKEzrdy{-huu$4x%27X0u2e20CWXKz zyXdU~>=&Rgv|uCnGTPcg>KBU(OdFz;OMtD!HK+~+KUs_mrZC-sF&+wrgB)KJ=VLKb zVFEHDLv2lI8yXe%C^GdFU z*Wl|*ZpYh7`tYMA&y-j|Zh@gxCB;&W6ibIMFvQpl)!}7@>WU$SgE^hrhNh`)YWOwx z;E2lRN!u43bLwlSw4R>6AuQ!h9oXMkpo6Xz*;(Hl;`=h8#xR8=qg zVeCns7>vaVO2)xQ;2AeBiaSOx9Q@$56Y9&UW<3!Aw4C&M5+)R*DQ>DYIYmhoq+}R2 z(U`e>Ih)cneUfk3Ay=|fe?Yx#l~}GMobIfYf}2g3q&Sn$Y|C2xAOMoGUkXS`9=hBf z4+!5P2bh84c}n&(10@obla#Fd5o5Vk2{e##uAsWfx{#_CAjt>(Mr#`|?uN604$gky z5`DjZAKj82?p917Y4yk+&QlZ#tE3fXIzX2Baqr9zs{Y(gyF0rl47-Q6XMMA1iaK|x zS|~(p5KHpF0cpQFf6zepF`^l`PmAa%Ron%frC4W-KJKfHwVo$~2kUoW#r1}9j4z!`b_slOjyKbA?XswnI?+7Vp2rTUM_>{I2l5!Z9qCRd=v9u zAFDXPb@mLokj;FR^2)yJ^5widI-HUsm*l~`KDa~)_^xw*Ww+yaN^cLu?IulnD|S~F zo=9U?_uXf3^2bv^F&doQ@oIP~TT(Jvv+7xx&W|Z|gwJIQwB%W{Fq55{ot^E#nOq?OQ=J6?@tP{_?H6wXT)4y{OOAx~696#6RYKu<*kkr4Y2w2_RC2KNg=qC7E4 z;vu&PiZj$CG`J!pamUChc{fd;qKd&cJyg03-5Lt2bA0<01F0i)GP$3=3H5+uIjM!^ zq#~N{$kmmaxj)6Cn|x3y^b#Z&*hnJflY~Z}DrUE~*)C{0Kpzwc1guSgzC-Ws6rE~L zO3<7JtFnTu+=BoUMcYow;s7a#1Edso4i%wW!L+4@qBq#K-PVj^v^8ZEgOw!e4!?9G zyp&psJ~@^qO(U${zf;Z0s}hAXmL{qvIeSSl5FnN&+Gwor&=)(2F-#M(5;*g@5RBLm1(f2+Bo%sJq2i4Q-ajU11H6*f=s-=M2Jl+mwr@kM0hb zLhg_hPT$a6I-+6GHR(}T{&v#1e4E|vMUM?WcG05hXudD`=L@D!+1y%WDu=C`Hq{rE zHji6ebL{-3+hSI0gvz-1p|7~jx&hz=S1Xo_O&5zCOH3m0^}$`Fe{@bKa&6=wA2iX= zGb2-vp~GUL+csMiKQ`?W^YWoi2dr4e94>OWU2C0AheELLP$#a!*^Fb4qu+rXzC0>j z)Fn+~A{=Z`XRI37=LyIuXd2kxM&mo!s%8UZE(Pi~O0G!GGlvNlx->>NckFxT9cx?~ zSr<5F*UVFG#&y5Bca&~m@Xq-Ij~fR}iVrzV~;O0C5xvF@Upbr;pFx~OK=(R@dd`ZK9Hsu_1Q zSA0aR+!KXzbbFM`M7yJ0)G525tH??P%}!FWMYHWJnr&xMD`RrVK}@{QEihx*W9hdb zi!U-I=f=QB3AT1He7)djs|RElIdFK-9b!u`wSk5~vG27ors&DdeDUg{l&a-vz0S5$ zN>rlf3P%iX9DVk2Q%@~QG{Vyz$^59lu!dS>15r9JczWyjxo6%2S5nh!;EeXVfFo}z zJgChFIEWqgDe3(JxJk*gbAW?(V28TWz)a6fZ)@NIabMu`fSv}l?l4yd=!?e-paktPn15jS0I*ox8#!DeTU9d7F$6v_HanDW1s)|OfoYpWGm+bkbJy4N!sf`V`h ztM*F(1!D+6nTrz9>exjOYa{{eX4@=@`3;A`X8rf)1i6vufZZ{jUF3EOl5rKGiF zPKg0R&Th9k9Ka}7cPV5%+qG2EG_^z*ItMzF1v^tgcZP8-pQrvC-?x z%a$nArG>l`x2*)0cszNDRYAEArgtfGY~41__7F^iATSL1JQ>y-4jE9XK{2!%kinNO z>Eos|O3cMVtzhviZNmdv+Ejt@v$z&UnkxvTg-GQBT2@$o2{DTOY1mZiS4np$h>EUd zs6OUI2r=~`aA+U(DWF?rX!&qJ)J+3^{x8PiPfOzec8%*Op4Nt*QrWt zZHiV!cuE2r9KNP&t9Dm$C25%P2V`@`Z;{P)R*;5uR=t@BMxQ@V_C?b%Ia*a$Em!U4 zA(M_?VuN@ia#)U3X0mc+Nx(uxAxk5^n=)H7+)0@WGY@A_#($M{Yu&?j=%l&}>%O(3 z6RhW0Z>mH7W@{Z0p>=6(28-lRD`yi$`n@zfF1;bWHT_armxgw8Az>j5T`&fF;3kOA zGe3;Ib97`~w=W#qcB*5W9ox2Tvtv8yq+{D1t7F@?Z6_TkH+kOk+;h%1zCZ4$+I!8l zrhap-ReRK^TA=f#&bPP`eR6zq)p+2#T|KZHI|6Zo_kzk}52N#~)o@K4cg!@5VbRE) zw;oqNb!JK!cNDFhoi~}O-Lwn=$qw{VNt}kdyghN>O$y_;c85G%?APs7_R`n&+)g~z%V-}9GaNy{SlJa zj#j*BRc~CuQZq%ukD4P>Uwa=v%WSg|67u6nyVoN1PeQVv@TyL3$orm3(LhmCf6i<@ zLv_<0Ta(HLj+gv<X`GGNRyfxabo}7%HNHw69er5+D;Qq z4Ik63Rpj?AMkD67EhuzET{hGHnPGOhOvRDwCkcgBN<^`8 ziN^e#?UU#lQ^;?sRxIDIiwM-6mnnKorz(@n<9MBH=Wh&?2?q_=n!v5Su&qTQp%zWH zUSF2KcZgR6c;Z!xo}_RAOEWa?jgO@#^5q*lZ0x*>kRI+o?DJ*XQ97f2x8j%4Wj1G{ z-@`n?j8HSPg={=*KVXj69LKIpskzG2J<#E8Jc~P86n9}_)Wxf<8%m^zDxyi1cFLMe zO+G{&H>Iz!9wVRq88w!$Bbdsa-A@D3P8|!Z03#T6n@3nk2d$n@2(B$_XsFMNm~xPC zPW{pbSJNz7SI3|w3bk}H(!_zOfQg1EOBNVzZ72;wngv}5bV2^Ty{i7Knu;;_+-37m zcF6@5`oo@b_%MnKT+yECEU&CV6^|H&L@8CU2AH>aqDXbMTPXWPGR$OUMDeeUNgSIv?=1bB)V~>Ja*?& z6CN}4TRRfE1D#2ILGaGOBC4T*Ie0q05F$K-1G9KCoxHDTP^xrln71M`OnpIdUj0?PpsvHcrE&UG>k9)g|$?4?uOiPgMhU zI#}SiEpZhf?fm>e_m=Dy{a`cwl9|tBLr%D`+pG4OhZBK_nITLT)y%(S+4xAvq2HPb zq}7x7V|Jv z{W1w4QT6E=perMeYDHO%=;)j`v>#*GHG;A0ZK^)qum?c5F65AE- z=Rr(P+uJpZ<9HoGU2pcMpf2)ikXjCAvgr-)Q5kie5- z2=XZW=Q;h^sbUD18AMN)Z*IMs0TX4-m>iJ0Al&*1hA+Gi!NLyQj4pHZBr68NDzR-E zE(RNiIzK8*6XZ-cB}tlicLq@i-=79inlU3r?b68J_Kd>i?+b}uiTth54mUnl2=f6T zBtA`i19(b};Yx3`%Is0qCm{xoO?7FY5a~+f`68-+COOZ-{jJlaluy#xOd;N8_IHvI zFu5ENF#F113yWuS*M%pS#-=ajJPCoJ+Qox+ObRv0#7jUjxlxF5k9*#CX-c7W<9tra1K0-Y^cwg}tnsC_&JH#I8FOa($TZ0WA#6PaH{MUF;9<3V- zHbD6kU0&l%9;tP|liO+V=NA=3|&~=g8si@nhRT@GasQHkv>zCVy~x_p|LkLjRxY z#{(gd-|m!{--Vmd2S!~&4-(iCb}%P^zZHVG8trUWAD;qD&yrz%=tmsHZ4TBSt7?Ms ziFsl_Mf(^C+^*#-6nHkLMMqm8|7W$*)yi%pb(fDheS{Tta9w={vc;dz$DgVq z&r}e%d*7=zbGpAHElwI#XYI)NmO-FuA5WTA^H)YbcUn{IjoP`Y>NrQst!2?l;g2&PwvOo8#8Uhs$W|4VGVwJe9th<1xCmZXG zXfYE)BG3gu?KPp#PHTsi-(Ar5ZA71tdh?6gR)?6)ocV3eGexA$OsR-K{#9Gu3AS9mLVyPJ95DApv89;%7?Y*Lh>+NJX zYY#HGBt@X6|9YQdI+W;L013#;QslrHL$kosfZ&s4!?kod-fY4y1lYQouyCG!V~{zF zJIyQ?hJG?3$!#wr;Y-ri62G_vr(V{89Oe=~o+q&(CoPWMOs**tU7-8AihyrnNB>~#i+Q`$aAE^amIs!pMMeMKh_h-(vJPT^1Y9TD?C5XL$#)(fTvFUv>3d6KzG6Dt}VU&h3UqMp3^)AvYXr0u4=)|sQY@) zow&i%GvsY%{Ax)yiz$14X}G#EgvW|k!uPHDgp-ylyX}(dFUEPfHKxSs^5yy-<~=-x z>Pmx|izVyW;)&vB){Ns7rrkz=b#v0aJNfZ0Ab-TY+3ZcTHR&Vo;)6gc)XHx1-U%^3 zQ+MI!iF4)szELgvebjXxu@~4I^+jN@(XkN)P?49BNKbc!L+R!ozVk2MAu5V z3HZN%$80Xu*&59xs;#2(Gf&-1k)c5jiub!L5bzQfYTuG-1nemrmDDKcbT>Skj9Ho4N0yz#Fq z_$rs0D?KS{aD=4$F+%N>H@&2cX9uf2BC9*cSOrO=`@Qi8Cr5BX zz#t(PnK*kEuXC6>-JBpZFs?liGY|11m|{9|SQpG$7c`CEik#mYh$GV5!4i^mz1s}0 z;$?vat-!Mm%HLMm7kE(Urh`en1$egVJL@Ffp_y@-_SnM4M*4GYC|8;zZC=c~n!A|H zQ7+GJR2uK3q4UC=?82(J0H^L%8t)$#i?4-Sph-S0<@xQXc*e&MJbPwP>Lbjrc(N*^ z=}PB1GacJ%2tvQM0*gWnN-fl>;G}4K6?vrQJ)(LQ6{Ktck}QKX=78rz5ne#R`e_v z3pd5{+K#`QcTp6x_OePmvVR14Tx5!5iJUxKOjYs~wg+dvhA!7tfHC7)+#4HSEFPaO zrRpq}ostIC$TmhVX(S)hL1r*Wa$p<YCSwZNj&PZ0#ca+#kzsOXUh-36U2V zvO7i|Qsj-!?@sIf$0?!y6VEemlHDxd(9QYtI!R|bKVr?R?OQKRr`QM7Xyft6SGyRn z*bUF#8G+(6W71~D2kPkMG~dLH%`>@rht3BHrFTr0evCpkKlxjBH!o?8}h=Bk(<`za){wi-N{c~pHFTlzogv|wzdj`y-OU?jcJBjwca6Kc;XLz zEcZ{!6G!x~71mR6NbCb^)N7={9w9%B^V9K%h6DC4Gi%9CpE*bf}j_i>5?Pk25)^T#KJ-ci4h zWC3{MP~|8jfnEk8esp4xVnbnmG~xi+{xCn4D5$mwWPvKfpa~L%D5#JRaM3KFN&n>T zRWM>gfBNo+UxA|CC5FNbrC7+q=)?gU{bBkFj6~{`qF@rAq!J_1(x(Z8PmWT5*j64E z@*0&WSWE9mMCTBs5x`2(s>5OVuJ$R{1gayW@4;<7RwZi8HRlG6zo#N5OG)W)p60B z(IIg_!2Yj^vlZydot%U%E4XP5(=d7z2|i5@(I2UgaS%Fm9b-90!wqUIM~>GjAU{p^ zb!T%%`xmi=N857+lH(7j=qr=`aApYO!(_jQir?#A;o4U|nqElQ?PuM&er~=n$#a~! z7XSAktDgR$z!#23;x&waH)wiqWe~@?`4KvBAkGSs<;d4^y0sajJ=tpdT|4qU)oS{s z8RLz5F%Qv^_L*TZPtJ+^0OdXB zWCPlyUxlkAeeUPUL2MI0dgK!9CUWit&;)2KL1}GS{%%fv2jjTFf#Ass%)$z2>O?uR zgm|Nc-)BlQs=z=eobVJv>@h>ePi5Bqm!0@Y-?yMqL7l+Yo-*qoZ-713&b+t&uOgN8PY31 z2V%c>n#fBb;A97q?d62S<+;^>($^5@olVd$>*&>RA=J+3)x~YfOZ0e4T;$k^55T_T zy<10XLO_*9>WB(kLy{21wnI|-^X|QcDRSIBgVsO5|wX*v_PfUief2y0FAm~*Z8yXLMR#T>~UQQpx!<9Z-}o~9#wne$`% zrJz!7s`$w1j+Cyi-wn*Rhxpo@8zZK7_ga`6oiJ3ci+pFnAMDPM&4HIIpI6{_FyCmzoy)5j?k$(LuE@5&Rd@2s{o6+%!fhk30J$4V-f^=> zrj7tJz0R%l$Jkf%j-T11Kdz`{xTGIaUBfemB1NZ(Rzw<8$RYZwu`pr#iE7ko(CZ9; zE&-@^uxdmyNd833_F?Zx?BLbtXp_)kB%|PM`hy84!eS2>V4_d}b0i2-&cIqEkhSZC zi~xxymY`f6!8t=r<^MD%Mw-$%qS5C+%yIHV&J3LZCA-tQ(*op0PWN|3(UHHI38>{! z`=jdwRsy560IvgCADKEcSLgHP@Bu-#fUW~)9ML&r+UK@TuXp|YlWWrzcp>rtLf=BT zj_7qHa0eI?ipde<=k5d@3*D{@1`0Gl*nvj;k^1!(buXZex)(PRY_`$>Ay@M0)$AG>`o z8K}FWkq?v`AP>~!Mw|q7aRb%`I=MD9Y-QQncjLtJ_q0be?<>2uoC;dpcH!^t?Cu06 zzJ_Q6j_NVnVeU3>gSG=Px>|m;X28gfoCF_#^z!1x3Z?JE48ZV0?tqFKOx}{}0`tP| zfRpZ1+oJ9wX~VXIh`*kHv}6F+i>v}yy1sw(n2NCLV-GO8!e|4>i}a20MU3uk++FQv zX+yUIAKX26<4TFX?0xV@y`pP_9Rt+AAf<#8_QwREUhh6S*rU8NzcZWn`gZ#QlkXm0 z-|DmZ0Cxf%Y$abw>C@$&Ep~CgV)($04SHVdPDO9_Uiu@v(!bJI1AQJA1_66*_Q+ff zIJO_BD*W~ zq-g(LZz4Hx^GbskcHx%Q3x26<@!Dkzt|QhC%7^l~dQU z7uP1j#J1-nfqmt%?ri%O_pI!!tbNBZ@GR&SNGx35mLPH@)~hN@7=(SSImY;w;WXJw z7ipf9v%i!`_7P}uxX&~q9j1^Wl=ioQKCC81=`iSO4e1C8`@|F!r7cTFV$&{G!(esb zO7yaROZ!Z9`h~#LezF?qH9$0mSRih{!w!diJcmLC)Sob={?aYeYmRFU`$)F{H;~6n zw>&qZ$94{_4ET4z1m>T5g8(~BK9O=n?Gc~_NhhRI zbTOay=;6rKneapiF`wrM`i{i|;XXfk`t-=A9`Rg^5NY@cS~f3`BV6eSgd3YMFlLb1 z0XHj>%br{}L~TpOjg}Yx17S1dvS;;5_7xzz&9}q1U3^vdXm?~1*BPY(4W6RF^zJL>|d^kt%fnM1P4o zQBtIeVdfG=G)j6u^bXp!x|inNe)(Q(RIkF@2r zwC-_g8%jzW6N9^n@y$rzUfj@LTY`l~zSuzP(vb8kba;o;{xv_i3j>G+Xap}XP zi1AXoWXWFAbo-3W36ms>!13ZfWU(O9M8nc6V5#-cj25V-Gh{slG6@ptSn+HQWHu<$ zD`-p>@y0VnVG`+R@ofFlD+Q@|6b6of-aUTGcu*5*gal@2`EC;aBndw%GT`5-U?m19 zIiHp9WWXn>VCMQL`e5-uKczuf7@>0*pc&#M{1l`?qD)cr;o^aeJ_SB0ym%nm&o(m@ zeUNw{)KsvPK9VkDGT@?5@kDb-CUcWEFSy-JBkW0nK{r>N-Q`{EP+2oP50Kp{>?k2K zCWOp>jyvIjTyjPP8zVdqrroLKD8fSqpN!E*2ZG(frzpZzY(7^8AMLJ3hi|)sB2k1~ z*nD)RuT4SMyNUxbqgZ@&X5336CAX;6D8ce5Ci39VFgkv8B5Jv(Q!+#`T%rh1 zvH92-eDJy-YhtgBPtd^L_6%W3?vRRS7uB!-GH&hP3X6A#$O-M45X7Y zZE%NQo8s<{$79<~n6`0-ULR-=q~BuMO_;SUPq@_uULSxAq{Cv{(HXWa54$-r?v9g2 zu_rTaXb-qKaqf=O4WzF}v8yp`oPGPtT%#54!VI|v@+Uw?AJGk?4Q>ON(2u@5#+o2# znKvg)U0x*z*juLLN&1dff<4za{!aT2RDynI5*&}9J6mvu%m-gW?4~%@`L1g23chTc zOSSi*{PX)d{9z5wLM@K032VlLWeykjdq|T0)H7cFH^|J`z=<&dT&JlbJm&=@5}(w- z4nw4xjqtu@NFai)KtJMIfAvCPUZnisAt+)bC_DNbD?aN)e*M3foniBhi4%hM&ZLGQ z>0A+kH0jX4q$iPO$yxJaLI8ry&OX~!duP;!qi>msbvgIKbiX#|@VxuZL@W(}OsV-D z4>!JfBDWdVrqOh&r{_!-mC4? zhkp=4)oCHX!i3z~Q#{N(FFU`it2w?xT`E@aKaQ{tC?xlbFctZe_w4kN=Spf7{}8vUP6{{j1{!18JAOYy7x0`_nFS3sZPd~&`hjxWw9 z{V(_bYJ5WbN9(_MU!ML=!>2`^|B+b6PjmkymYbVi(7?$=)XvtKUfIIN#EF=RLDuBQ z=Uvgx#=w?d*u=@`vu6x!oxen!0G}oq0Ez&55nCfWV+&g|dI@6_TW1SrcUp0JB^N_y zcY70h<A$hlt9|ytKZ5@&iZ6fv;*@hVHgWvS4aL8?p`=$dF|+s_-R=|u#&(7# zl%J`xx3@O2`Ai*v;eXlr!tk#hfv?m%12{Mt=>=TP)GUmh%>gV7jPybV_TnZMX6DWS zHcsaM>LF(UEfXUfy{NT;nG=AS>7UpH1?}8EcQRyU;H=X3(mjsevo-t5 z0w>$2yZ=-eKNG~k$i&P0@6*FA5tkJCvFvv)_+||a z5Rx-!l)7pd3h49zhCc9VKmcxjKBxnlc2`Y26w4OH_mikqKjxDXX5wi9F|#qa-JE>E zmd(;tV1uNGkByg(kB^tmhkRSv%$19Z-%hfb--|D7__4kf1c!<6NMX3rJkDKkBami; zCeZkU^y(=}A@vXpnQ5;(=%|fs21ySs3T@1D5UZy!Sq(4^(Fb_F#FeS(4W?C;efwY% z%zZf2Tc0MK+5e$ zKH2Xe{he)Q@egMwv8&)Di-M}(8pK$Vns;5yqx~i--~Sf)iH_2PC1xd=ALKTN3Dx<> zfk9J1QTDw!yZq`6Gl;lGC`okU+v>Rj(nDePHb-XEk4DPcR*z;lLmH956IxV}coRzD zEF1_H-uo`qw7PrGLF2TWPt+4G406#Cnl619uHJ#SYGLI_ki}n|qFMj+3iU)j1=fMb zx@F4(e_{%<2MT>@Y$wQT?hC(X2GkLsKnyWL4x9lo1fBxC1#$#t4v_*^4Xlq-{}W_Q zAlrc;#l}4AOmeRK4GwzEdJi}S%!2;KGoiOq&{kY7$Nh+YtRNpx+%tzxD+$5IH|a5D zL5M46&X#Sh5$2x%jbo?`2(H#<2tpn57s$S<+k_DX=O#piM#3X(!D0^wncy?cmIsy< z?bfjT2Gns45DwOlpak**Ax*^N8h%`!f_Wep`%U&c0`n?=MmoP| zp%uIa9wmK@HNEEpAHAD#O2UESTT*l_A%Jp!5Vrh4XVS?ie)upO6f)WK)%*ZUd(J-J zjXR_jZ2m}`eEAUKppXCf(*ZR8^kGSp+`Dhj`ML;pAY^Pno;&}szT1RH7y5w<;zQ%G z*XarOG2ONNK8bz$DW#2&-0N%BMd1MUxV(v zgbyZ$e0{n13lKAY1Vs|anv{3H{HS*jkknX69e>SfkGW6+AwJQ!BcSiWyh4Zez!zkN zdq2h=QhW}ub)5@aXg;0p{&mC1BE9>8X5Wc;VI6@ zznkd06svj)mx~jUS%azOycXZ_dtK-FzQ|t7xZyzez25356bhb}yZJ5<>EVp;)q@0c zKObqU$Cuf2P{acGJ}-T0{iwX&n z@=!OWztez2@C`Ijj*!DG_X?{k@?-AqT;Y|=Z*&GA5va~%Riw^oYi%qo;;vyK;h@k| z4L}>49EIOAHZytl#@3Bl-FGfnwNZ^+HkOuQJOa;D%~z#Fcf_8of$OGdoY305hRaC57#ed6Cmay5kC~s^G!< zvIN$RwBV68?YSyt0Y&iAlHt|)7@6Jb1VB+sgipX84wmxVNkv7ZW?O?Zn8JYCVhv!0 zrJ}iV;lQ6k-TaVu+=@<{#z zYE<5?z~Y_h*=322NFMU5VPc-EZuvA?b)}6Kzv1t@@*J1WetzCLu=;cJQYet?b=#4R3PxlXL9!L*3eMCYxw5N3 zD1rV-6c)v-gAq9R?wd z1}kf(YC3i=6gIe~x^Yqq$vP#tGTX-BNZcdOeu}8=0FT*2wpozJU9bC%ma-M{F2z7b zQ6AblA9YhiPvyyk;*w{sqv;~7?6Y%?`{c*wKji|$AVC|q6#G0gW z!kPe(GFPwGcxT41b8CPpn5QghPDxvUjX9nJ0as?|H&podPcM-mk0LEKy74vOPLxbixFR;ZveMj`b%#1cLGXOs53F%xUP5EXj6@ZSG0M3KuMk&e z!u)t38k8JiKV?#W%mP#(?H#P!ZG9b`Tr5z$q2w!mRo86;O-B6&%=e(DUuR;eL(t&l zkWp-<2Z8=4C)2_b8cd8S@oH|hlEYqp?;!jsh$-|5&|j)R<93mN3#K0LbyT$igZTe0uBKBSs%1Kcoyhz;s4@-1PQDPWDTkB zA0)^B#Dv2f!zk7LYR}Zj{E)-arIp%B`U<5_y+tU{40tJv+q{2jQf{v9%Z|3PR2 z>aj!N)tjObcmvu!;u3$n%49+Op@N{_d+axGs8Wd|e>*Gytc(AIKztpvpfyxe9m(i` z%(QtJuooz#+0JRf}e{aD@}4jQxCa#*)9<9Hov7w((5LmNB}>qEeB<>3&5_M0Iq*%>5Cu(f?^LLUV4T2P zU!$>uC`m0|1g10Gpq zfTaU6ARKXqEOO?d9T7w+_|HJhLG%l1zX6>LX0*y<;#Al{ZWTVyDyHAM5Va>~pmH(A znCzOfU5pNJ)z)B@mQ$heAYsFWu-TxoKZs!%i2E09rpMY#wAW$L@HIVuZTA%Cg#0nezNd7 zRdMWUHPCYG?W*326Gzogg|nNf$So1^mN!!BzFeBRJD2dCRRyOFR_L-sGXh`HI-l`< z$3mw9fiilBUY?d48d115vzkx}iX4JS#cu~R_S#Prf~Xb9n+vIV^$G4BLP`tg-4d{y zP)7xy-Hob|_OVLwPMsC@7nm2`jd81;BEZ{^5Iby*f0YmJ;REP~PmqAq<^#zx?qc;P zx#T_{Ui)RY|C;R4y}$pO%hV~NeC6Ef1Eiyz!0|e0b0ctz72pSF&N(RW_pU)K)?7yTge;4BS*^m1*0?Cu%?=vq&rWIUdfa+Sr z9S>3S^hCGKn-A!t?2CZ1t0GA11qAO@;mv zb35o~-;VbtXoF7dHZz`a9lZuWl-w3ZVP)}baVO!%3-voSGX@}Xy!a0(m`hy=94a+O zX3BXKuJGuVfU90c?{N}dO}K1a2EO{;hOC(#F-*-f>@Lf%$8k+?a@J02Tt*s{U=UBG zmN=`rseK>KZK4SwN4RjpFz!2&7rct{hRo!g%HO1hH&H!CR*W)s`uNb3Cb48M?cQVIbrEQ|2Ij2g>>k(U_kUf!N61F;!f zpx;d1=fZI>0#PhTcDp)-HiO$gNf>KLJZyxl&MkR*<9_ZG-C+l9^QOkyS{9WoQM1X5 zi|>&o-Yf`Sm}CBtft)-obq1nNcj0$Q=HFyUX0&kfc1u@1Qk>(_T^d3qFhyv`L$5t% zbl5YKcJCW#G1I;w9tW6G2qN3Cg>lzo-)sq#E*qC(hm{sr{@L7>VDkW$EBMLprTYtq zR+g6T z68_n{Q1QOwb2ImjJ$ubxawmhKS_M7~@0BvDweaHI=D0+mZ2li3)z#0wxr_v7kLLkT zcUL*A{SF6)X;J%)RucT-{XrkU1TC3Swy0lJDE>k|}e;>}C zAlTXPrRlNPHT!lnRlDX0ot50W&i#SASXZ94m(Xj}IVv-q?Q~lrd#=V=S=kzo zK(^MRx>}pxI4ObfQ{7kDSDB_#&#ae}iU}1h8{5R){e#~f^HUXCzWhNoABbsDN5ym7 zZvMAQ8D+T!s-N3EPMz`}9E=K2QK9;*?{%@}1$0z@7XRpvM6>4S7HObmMdHQ$LXVY+ zJ&#?FEhbt^W2Ke1cAIco6I$V>=1%8c{yB{trU&juxD^Am31Tn(O^JfXOGeRBOEp&o7_L|BmxfHfivK(Z5VZ7gyN3$E zk3t?85`ljfs6+VzS+@ZKn9}c?UrwxYOT@;Xkeyq9Rr?=+J{eJ2pw_*t+`dqtVj$k1 za*`RlN#E?eEDXaWfD7ODC;)pxT+_A*{<6CucOc&(EWPNLIAx9qSP1aNN!~HyfFw5qd;7s(Wqh9luQYVuVGhHcstE6$k_@+; zikU`k!<*z65SIfq^Ft7>WO0N@Q&3-v?&u7?A$dnJqr91e6z~doFpRV3a&?F5M(E09 z1jd@?$lg(G`0;`E{>Ha4iCo&~V`728=eOI15SI}+ZTf)@*~QX?$`d8?sJ>&ogLtH| zB#zk=Z!6r`69zfO7&kd*?>!X!YEF|D@jd6S#~p!eH+WRakhC)SMd*y!G$skZuBkCY zG*DXD$`sojlKd{dGt3zfIvvEVDWSIA4xiIy&l1iQPCdbW*U&>x`^&U}wjXR5bd(SX z^O$Je=h;Impl^tIs7=6)TSkxWrih=5Xvh>%EyDRoP$(c-d!c^hALhm|bAZ|8Ko0bl zo8q5YR@(1Dq-=xS0lxch1UYyCIe(yd|8&}V{^b>8EjcE^o%Dz`Km^&F3nwGwG!0Xa zp%du?rtJ;f9`+tFi2`2FJ<-uuaVSfE0kZO}z#7$#tU2jtW(Q6OpM#}+*+p}|1YSqc zt90$$hGjR%z@fx5rd||YCNal#Y#;YOrn)a9; z9?~}+=g`yJ#H2t+C0GdHi{wvB>V9n5(hO(I!Ih_P0o~iDn}Zy%h2sg;6|l?S%$@WH zg=#ezQxmvU3!JkV_|fIUw-J*)NWZOi75Yf+fsClFQ4pjYhodNm5kGFfE+qTw$53%p zW-8*u4@B+Ol)y31oIDM-ZuA~j|J4zmkxsJFIGJBABYJ%7+;Pk^Og93bOI5C^cer*Z z2S0wLWU52%KIC#IAQG>GI^)8{WvR~6Ry_(LD??~E_O&isPaF6-Ov zo=6ER-mndO=#$!73G%%9`AFu)yb*GlSoxxF>v^^N2+$6_LT=g^AipMm zjCq6w5tPRIWlA_qBA|%CT=8vJf;bgn(XbYzlCCzc92|j15@Oik1|h!Zk$>KmVSVgv zEG88*r7^`$z2~4-fURf36tj-KW^kE(`NwiyMts?Bl6`9vrW>wa-{em6&BL|ab@t7n zaa%dG^*nrSqcL{ldgf!Y!y*Su3r z#7q7m4(umA4@Ag>{hhsx3}J*u1pV>hI8ErJINC6554cgdo&6LFu=vD(<5|{(W#cu* zm|vlbu)MNRkCM%KOzokJ552FEy{zJ|BTtT6m85AbOtlNQbi+r$b}ClXal-?N1Mt9^Pdd~PzPU`0W(?! zdkKzpD{?H2I%5a<;sBLc|N zw(v}#v>#@Mlw(&^OZjBIS&uL+RM+0hpl#_{O_Vk04WtB;W1#FVqC1RJomQxzC15t& znnTufT^Rn+-r${3FVl8cs#B||Q!hC&UQ1E$P>V4Jue(k8CCP8BM-wA`=i!YCI8(i# zXeU8BcW5m*Z`em{{+uB_*QEt~3X{&=2reixQrv0whNPNt>Ii8M*!5xq{ujpr2)2kr z7pktv(A{ozvB;3Q&IFCPuhd*axtUo({vjDNa#spBH0EsB?zTV<)=}IqEP)A3X&ipB zA`?!N0z3k66QVF}aC+fmz}sa*;>N%2$(T$?0;s;xW(1Z1_7+<2dBdtRMGm#cYMh8d zCjz($17GAwWw(>NZRG`e$|Cmepcv=3=@d0P7wG5&3}qdr z^N*0C*axJ!3t|~@7)8L34HW`^#i0uu#VQ(i{(;hi&;DF`JQCPj;@G74SO5rWC3PbA zJGH7wzQ)8%41)b>QOXoD(6%Tfb%o-~tcMDxTC=#Prm_#Z)h{hu)8Kfs2jR4(T|yOu zai}tFhsf6Z>;WNK2^{WzB|moKG`cw-^*!lxGSn$`oV{)d)F4VWLeDP+nrkOUd=w03 z;RU9!_sRjL1__yjzvB#y@Iwz{0@z0LAOzwjNYcj^KShTvkJqA9%OtAW(%wpsEei&p z$Jt(ZE(%Y(k6(Yg?3L}Yh#h`MDJ>vD+`3p$=rkoYWui~3c-bjMt?3gVxdGj$+`pbs zG;+|j4Q>d!-idO-kTMYw=`Me|-rOp`Z#L{t2^izrx%3aqfez2Uy*1c?D;Gwj;#X;s zy#vv~zTxOpduzICva`F`&Fy^^xHs6YzSO_i{`E}9DI^{>jHc8*4cYG>&J54ED`|Z% zDb&ssqJ>Zk7YTFP4Q_{Afqu`)8RQ2Zt&(c8+d)fdH8LpJks^0vv{yG%|L3zQb9sDTf4K#Ux5YZZZ! zNQa?DcoL~zlY74LST={5mJ4$&D-jA$yhn`#$7T;%?*4!Z5NU}3AI+;oQ!4{p6&A$ zvSSb6++kp~$98ucW!w$#`&Myq?QjWi4)#3-pRjX(rEC4Qp?*+Jjqsa zU9YJ?mBJ6nqm<+0BIR}*UweyuiJ96*xdYP-!3$%PUZV7^n+w^3S3y#)+iBaB2w2 zvu4b|wk#PwOK4bKty_h~j6SE*=K1TLtXTnFaJ36lR(oy6uqSwF^AsZJfS+P(aZ7p=j8=%K%x-x5WuPcg7oGzTm@ZK~{Q36vk(UmKpe0b;^ zKq$C-HOaqh9G{H1Oq~f&7O#TZ%N|aNQB3$^XHGxRFE)5rW$(%v&Kz)69Z+?x(VNwaAkxJ$Im?Q&1=@DUxI?dTu_^nJ=bNmAT*kmNgAx zgr3Ow_ z8wrsEQj;1|9ENoo->h5&xUys_DD1SrP{<|y+BJag_2gMcX?;NgcbD2L9&(9e{FAEBW% zP6S@iNbT%K4c`{ZTa=$`5(t^{PHUWv$gnfpP`hz8DYSAlzquYJC##ERpUlAp{30QW zii*m>2443)7!%(^D<_&gVv4(ZD(GWgLaX^`UH_Gwn2?kqH`Cyx+uj2b5S5UmnVc~5 zd!>z@kmwUoG!)7je_ybFoN>Qa}6Ox~kXhRa+{JA-__$>TEdMO+(ABbOp# zb~8hPoHkP%Kp6`UEnhvUb_#ZyE!xd_l49U@6{DKta*K-`T6#0oxYuTzGxRgLSeSE+ z3IP;jZO7yQcp}WhoxtDd=D`tmki(TTjI+O@hJ_`&sQUOL_+i>S#VJ!PAXTX5CJZjf z<;6FP1jmdCdd?kb_(sZ(Ty6JPa$EE1P>6^g&B&n+iBiUt*vZvCXOF8^f5If|boudy+_qW`3% zh0joeMk*s)xKZmWK+PC3E}qte|HR$#?QHG|YAa7le99c+mLLITpd$kva#sxV100t| zkw#;UVV!7DSdm}!6Vc!9@AD7&S-+kZYunHYaQ3#NJ}^Bkz9ZNHq#flZYyhp@UriYv zRfk~LO~x7C3{M8(3Am+%m-+0rbeqG*@F{+sPvtjTOcuhsS$!vR^43nAs)dwM4XF(5 z>cloVLjB*@9RA*IMbH(HZXpGqXvP%4&uxEj z!vnYes`G^#u70tuyt`zzBR`Z;o?TH}JfVo}`V==#uiE`m{Hf352k-s(`M<|M?Z3BU z<$hfL=>w}m?y)oC4+4))!-C_1Vn+0RF;z6ToBPZ|W)_)6Gg%8xDA87>;Hz*I_+EWr zg)>rG4rwni_-mxYSAlN_ZVLWF)MA~EC9MWyl?a=k< zSe?amgk<1Tqr?qFrUr;(JQ<}SY94V#*pz}YC zC%IE1^Wvnv%&3(4tiEdYXAhm-va-xUd_Iz1u%7((o*b9MN#!sX=C}{$=)g(El1@mu-s8EQh>dGDkR>YXFs1`Wub_=EkAF&)^z~aT*1~E}PVt5<)oBT(O zDQzh$QZ}WqDKRV)Q|Om!R^$?0Juar*)#oBE%TfFdtTPQ3#nd*kf@~t&$#dj5IZ6IP zJcH#l_N{XG4w?a+1weok>*K~QUVOW@S#1}8Z< zL`XJydZT5V1?pS0ku3tZ@kQ;6)bU-}3LHy1zl^&Wp`>Lf^N#L)u(c7HyAXCcW?$R6 zA>4soyRKQV+wZd+e_+q=LX-A>GY)qypHpweT>PvLSL6F0zhUp1?!zy?wPW$(A03FF zE)xpqE0Y&YWK z^7`^f9+V$wlWl^z$H>Q`QY^xKMaPN=`09AvAu;8-6p~^IQ-$;C1qMmn8KxT>M5lx^ z)DjB}udEzy9j5NLq=QQGgATt^$MPPx*PY?^xmlj`Y5jgBD3>6c*NGiENV!#+omi>J z=R=*?sY$2WMu?1hhdDPukk;5r)D1>$iKKIYPezGZ?$J#OtQAT7NcKJ~J}+H|l(VuW z75z6qJg54|#-6Ub;(xkre$Z{P>aR2Tau$8xYjp;8PIWczo^V6^154ORx81w6aqd04 z3JzW$z2ULi%=BD2SIMh(EpKcnOV6ruD6hP^aq*_TREN7@wH^lUDv<{LTFf$PunyH} zL>(jQaE=T>CHI?!kJI(y4+TqospSY4oY_qx?Jv*BcMslOo|9J#UCN^k=qSc*%3Sr+3@%S zJ+eM3$M{Q-V=orH?Ox)A1@g^7f%Lhtn?6KTxJ02(AcDnn1mBB4lPGqo71X@YN~t4> z_NfaVswB{$^{JNGBF_(SKrTcxNkt=SR^pXprRzp~Be~JVgT|qX2J}qLyk=3gj9p<^ z;OOGI(z(_aFtTKB87tY-{6#j_sPIxt8Z+yr#+S5U#RHq?+&pd7y7epaz1GZN!<04s zyS85a3}(5>PaMqLb!%+d!Jf?08HM(M;4bRlbp6{Ec`~A-(%B5W=m+UEqbzhz%vqyY ztGrfwqvAc^XFi^1@J41myWY6j#75*B^X%%ndi9|xsAGvm)!jNuv*a@%qY5YEg7c_)m>qKrX zq%xHLPHZ>0I*~^W5xQ`slryR_aGI8po!9HsM$s1;GD^aj`v2!C4$7a|dGn(WFU#0* z=hoL3Z+Lxc$Fp}~-Pg-TUN?-d4^NzP+pQb@bGRiwP2-P#cH8`+=o7a;F>es3AH);l z%_Fs2X0(4;9em`zec!rZ6--W?V)no)P@(4!BQ`NKm|`2pN#FHA!Xn2UlcT6cqNZKb zr+F2>Lf*yik|7O{6ss`Oh#H1)ELfCxi&loDG7MoE4JVE-=03taApRo^VjjcyAM8_M zl|{`RA)g|K{6SPBR$xW8iS1)K_8IaCQjgM|dda6Gb$y2JpaEexkhD*?Xg9t%D!hu- z+-m-2?q;4H6|Vj*)okHvjUalvO=y_j?~snR*D3ZWQ+4$9z`jd^eP4Dbp=R1g%G3# z(8U5O7K@2k++N&QJXFjUd!K;{2G&3htN~@nB${@Z7?Y*w2>$b^Vv!EQB&9NZ>P*r? zrvi_u*9Age7zjE%dLt`O^SL=MnB%haPMn*X(~0s8up@d{*diU&91j@tI#E!cmlX9; zS*QCWRW{v}YyOMe`AeMki;uueDd!Ksq;u5U9%_C4@kf5Xd|$MD^85YIFQ2^*7hET< zU9_mDxS(W4)3&RZ-{>Dt_TAh!`{w5cR!-iv?AED^y0^c!u4C2Q{&$vdY`p5)wT(qf zg7H7p?`gl`f%S7HlrIGxG!0h5V;~bIl!?`1`1;KExL$r{b^@?lN*J;+J zTW8_?>*_~AEVGVTbTkIYznIBIsM;1IrmXD0{RV z=&0MDHkpvdOf`@d>kc7#)NR1WZ+ z^bmOzzKc~7Wt6lhnCR(LX;Sl-jv6xrv=gQbN^(e2P0Mc_dNccno3_8c==zr*z4opT zUVi9V5;jz?o6>SiOI2I`_v}8h24_6A@OOs>wm#mw@9anMbvG;}hi{zP@!@rSyMA@; z>|E+~gV(%+iGn^dq3V9dLce27*DNmCVd(?sPed{`D0H2e2F`TRjRKa9tV z=8HDB?|J>=l9zQadWc$MOGy(H>VsxA9an0MnJPLRq8a>rox%yueThw|(7B!N9yjCu zxy_P6U47|=2e=24=RE?W3<{?tK1r;?-IolQ`8Q`9*g87+EC2D3gXtHOV;8^u_MeYC zFv>(n*(=z4z;^rFp;88$keMf=N>#-oxeSX6O%di$Fc^?VWgB3@uwgPq70;#e4B2HwFtZ@@*e2;_vnvYajlpya=4OP!ENGt=zkxFF^f2n zBRe?U#IC2> zT50H(j#GoJ-NRPP$cWVpNEjXld&exDgB(Fsr7YnJ%3T9%Wl7|1c{m&B(-)08`FfU?F1VGLEv4!~Y8` zM3jZR#6otIh1>-e5@2CK#JCc#kiH3FF~<@eZp4J zKO5hLcU}?Cp&Xr;_?Q{PEkhoZk3K!1kn2K(P~)a7R9qAcRfZZvZJ`Z{t%^PBJ=TY9 z3cp;f(K3RO;oMFZ>$E9K`eS23M(BIj+)XJsC58_kP}W<3u`y=4X!DBMD)3s7KUda& zG9ByEarzd&aJBK1+)xZ(eaJ7maznW!_bAbzAZ*8_gYF0CCg1yhA|0i6cVBq(`&9(2 z_SVt!Z&NvvPQFd&k2*n#TTOPk+U8T4HR(uUm8D~~Ntur2c8E|?XM?^5jn|F6CG=qY z!tQxNpCnZ`81cO9VpaHZwya=EBx6hGoCT45cTU|4dwXB5ns)cxb@nwL%d!IDRAJ5v>u&L*X|>K*ZVSuS@6szx*M@5H5h(cY@h zs~9k|?1ClD-ej3&Uu%?M0cCxP{#Z3M`>Q&(xoL0OUgL?`tgFd2%{9Hv%({^agWuoC zE

);`A+uU59$n*(#=AUR7Nku11a13#zLKVpXg)r?Dzb*cuy(G1a0%7>CC#K{dDr za)+vQ^?p<>vmXWFwjdib{=noLN*wj37&A>Qk=5rHl_;k#X3Gi+X3bXB=TussblE~S z(Z<-Uv&+kL6MH6-iI1gJxI7_`=xOq>9?PuRG5j7d@Je$Gmu?+Gr>%L#^GIId75~ii=2w*Ta&uR0A7!p_DK|Zchd&77Qp{$Qsh5(^hIbjK1@f zPGOCvQW-VWM;}r`-+}wNo;6kWfNSC#ckPM)>fj&ZtN-vC?s^N$@Z+m1=EnW;H~$=8 z^3m7$xwFUdlpjBQ?zYKO4EGGwj=$z753ahRrbTeS&@iRDY0UWCik{nDWfPfa(h!V?DeVb23!#*4oGB!27j7%Z>&pX2*K#6SER%ke81?~5Ngd?NK za~w$|x=FfvVWOka-J+W#%t~!`EY&R*mN?c5 zJ?s{3uWpO5#c-SBR_6n{2Za0e4>%6%4huiA9(KH@dsX;(`m2uL=-w6nr2AC()bWk( zYvG&pZydP_U4xA{!Mg(@p>&7CuFxuN3Zva*Gs=m~CQs9++S0Cb=meMGu-iR)Aywa{ z$Mhe3(8kCsqMkTX32`{n_rSIx5iW)gh-$f@W73R9xm+Q)$MCnJLI?MdJz7y8BcZ`Y z2X@5B=c3jnYMZpDwT$*r*Roz|ud!HR&6=(B$b_EJQc;Ka41Ez9p|A4kNy(O0Eo?Mf zxQ#Cc%t#o`E=&c-R!)x_uV>;^ZD3( zcWxWjkc^SvQV-UH5c>)E?G6+`Wn`lmnv3Q-ZbP>^ZVTUU{bA;Q>;BBotbfe>Lr`6Y z)@QB@|Df>x@Scn(!tYw&&3rdY$yUV39|m=cODd>T*gZuQi@#4Z6@^82E<_ebQK9I~ zg2hX6__< z0SOhe@Z5~{imr;DioOcDHDoPpLa>)wGaOl#Aj=cRVRAHvZ_W5Y#(QCzt3s@3s+dpa zGwobE-!5wpt>sr)SJ}E8t20(*uFtxezr}WoV|%!#;?>~$!9Qhun_+2@>zp=)+aow_ zMz=Sd0k%voDh@a^7*BRtZkWmUWEB@HjM-Txlab_SQAye1$9^jI6~z*&rl@DIvZ{z? z2W#pjET%&F*|%+k4NR=2CX;u-uVa-$AxM$%^|fk0%+Fv!jbSb?qG z^hXB;Ox|ZU2@SV^I*w2YT{$cX*vh242OyaaqxOP6X(`b6JFu}-1&h2{W(Ss8Z5A8h zd4C4Dp<#cP*&oJ1SwR?k9sV#=gbTt+LtK6s`O-aMh1&2&VFJ0xMmx9+EG1k z^TNZkd?ntM%<}KKbA9HFL)R{PNGFq3-Zp7R;AC!$9jw>8?(I1STdGyXEsKYbAG|I8*2!4>iyhsMr5*!!wUgFs9f-)DHB*E=kYU zjmV7|u|;K5rQaglE4(dmYlXF`TZH>k?oWHg_DcF&g50b(q^3Iu;(t1K41*yY7t zUV28)c-a!@n4*tyUJJUUd1fma^o} zY{C{?n@(jN+#U95pm-RUh?QdTV*9oB2SSgV_lJ&#hKl90EnR$^o=-pUT(Sq!g{atI?hes zj)$eM14_e8Qn7ux{fP_~%emCa`F;CZJEH_GsH(xK-8F&}9q zPHH8lX(di-J&tsVlj%}ny2Q!!UHSjC=*~zdBJ^|MQ_{B!!k=3&FfaKz7-eB#lzSuR zF|d$+YS|sYddf0jVm|SD^~uj~!)X`oOXv9q_4whx&uY~8d^oeN_U{@cxatKXM?y3G zW{uJbLc#o11HAgL~yx>~Z78PSI$+tF@ zbd}uBKFJQ1uxjQO)v*$0f*kl_{+q`@x#lgB_Mbs%|G|S`0XB#=1wY6&nav(PE0@t` zc@#L{bf_ui9TJfpJSDQjqt`bXb{L4x&}bl3cWyEy46K2r+%SLwJ@vGtKx5=Fd(r?;@TDx5cl0{r=gD2JU@r(z+F8DJeDvwz!#n)7HH4KhAG_D@!?!M~bo>29 zzN_OM<0{VmznH zAOb$7YAnjg$RA7c?Mi|wgOX?Y1~`x?6ZZB*cd2Vm9(>B)a z1qygGGHm0aW+nXziqO4MQU=nGQAWKqxRR>iyj7Y!S2TuMS3U!F$MB&;x-`h}25T8x%oqC^Yg+XolZ1(koRJj2s*#NC(evU8x3mkpCDCz2vo;Y00ihe8}j5 zLSFQlIHt~k+fv$6$pTZCX|sBt?wF4=m~qG_`iNDYBviUYBBR+Z7%c>oP^y?pno@CU zj8Pu2WN8%X_E_RuY2J`H^E9OgO^9wv6T3&D2+2kHcKL4kQ*!P(`AK;Ks76M~^hYsO zB9l>~pVfB~tp1@4UyKwCx`%#5J-Jh@l2Ovy4c6l*jfRI?yDKBf?^e#+j)K*uR9n^d zF<7OvsVrj<>|+s0&(Uwax>LTR9RJ;Gx6$|iqtty}BA~hEk=0i&x43g%;Y?G8Ehy2G z%an*Xa@GCMZflJcSe!XmlvK}Tc3q&U2lUmuK+_r$6=T@UU zOP*66(a5hx*ud81g9w(8S!5yUA-9P;$o=A==4tJb@R6FgHNP&@7*v?i5}t5{ThW%n zU1&e<)4o|KS0VZdEvI%W9GV>D!$C!*qET@>db#L5^hJ?Qp|Yq#xR`{+YO$$qFMgQp z5f74s%4qc)=(p$uev7=r{0aRDpT=J*PpiMsn2lj$QBh&2XePcN-J{uAcyEzn^qq%0 zsPi}?j=I`3lole|5MnaTMvKkNo8{SlTjrQdLec8&7vx zw(ej8q{+52j{@ic&+NB7`+X zRa{*NRTZ%n8cgUvQYw>ayR^?~iPkS;Wiq4DvfmsTGbSr5b8JOLcD8?irrBiTc`lQ1 z^2pt+HWUi7Jsj@hFc%|bqFU56X-JO-M>SXzBj1R*L7ik#>7>0tC+!eA4{Y{S8b>#K zX-zwJ)z&?OBht}=<^0jYdD9Avs+IJEMCsMjsq??JqPDT{-IdG$g<69^t4n|3vV|Us z0UkXPgB|+2)3DJDeJ=y<4(eW6(<~~2;k>F~bzTkI+R_@JUlu7#%tlS6(v@0X7)uNt z6v{uk5Bs|-fCb_VSmd|ks>cX4mKlr^R z_*z+fhp)`FnpsBOS0Bz1*PYux!;4VSQrcE#=hi()Hb z()e3~^0;+CXkyfC87|Y)Bw+s6t0RBTr!tzYSwB2?Y!kMVF<9^p#kZ zWk*#Or7FeCRqy2K2FgOkZJh%f_-61+ScD?vaOI`GF4n_>oZh_On1BtxvpwThNO3RD4nCqrkPF1Um{8ZE`pR19ka zMEWNxSPYagf#?Ok^rybw#9Cd-n6*>k0o;F=1kLK-dU|?SBk~ig%~kS=IY?HahJZY$ zO_EQ_pc++``l;p8W@@9fmAX>8QF<6XiC#d5q#vWdA?c4a0?liX!n*ag1yGKPKK;bA zVW~!g$sAL2tU|tb7z33StU~udqY04au&;_`ROr5;*`=!Fk*c)l#*43tRi8Hwn5+0Q z$Mxnfhg?-!b2MrGtmzq`iW*`tP?gV;D&v0-WyB;CKqJgNfEe@!z*6Fq&j*tI7_b14 zdgk?ndWp?-wf(>IY zm=#xv+p-_&_QhrnOdAS7u8TkEOxi4tJQYB49=SN&^B8eY6s&pVqwz}O-FeDuaObJH zFDv^wjKG+)I7BK6>rRqp>>!CQl0!@->1o8HTkAL0Kd8S~zxCf=`tQS^-hJ;i&%X56 z-Jixv@7DUy>p!kvj_yUhXu)gGEZg-&{oD0}&)$hzP#?PB={v!;3!bMe(OGinh9gt~ zKk9wmm15y~`MSV$!JE?~g)axW9rAOD=hMFm{3`g%1nWzQg>VG;ae^8bgr)7mZ^=>yE~$ zkdkW&rEBs2I3163?v}L2E$v7R#M|fCikGdyszDf&pLmZU7FK%jCyY>t z*g>eQ9x^$KMgbkO{{w|39+%k^NY5ER3x~cLOs*==yny+$D}ebk`qSZ`jsNP^*3rJo z`Jv12UwYe`_5u2)`t@TG%%8g=*D+T^Z^eNdA3JVaA_!j_+i?F1CtyW=t8oX$3OALc zX0*&Aw1N9FVptnmk9(J*?dT3PPJM;{f&I6X$za#0a}cwRXYMyl(#N$T?@5ac75)~_ zgZu&kKnCi3p0_gDSZ~Bxbkb*Hv5n4U=_UrHwKSdf_lj1Dab1v1GRfvZN4LX(L^s*ytsxaOkJEUM`#Wl83aOQ-L@tNu#;cbAT@ zxp5b|3t^E3-HvhZ#=~3hx%bnDzJ2Yk<<%GM6OUOG<0a2t(%ttd6!_DOmCG>(rl~SbYX=fCfUQb^cUKHawQG>G{KmVNlmIts10ZsGc7u8s+NC^@e;PflK9~G<{&d34 zDz~9~61SxuN<50bNk5j@pMNR;M(L0F(|M~zd61u$(iqEHy9?c=OA}WVg%+L;29Y}w zut#H*nhsD{54T|r9IQuijn1iwM2to*nnsCl(F)DAWF93B1~1$$CvpSaFvpCO1PChd ztzdZ)eL=Igr9+`0ZMWI58p%r}RAGZ2s<5cPLPhsS>3%E}(a}R<2huu5I*xZR9c7+i zHc#FIPcWO0dE5lE-DJ>BFx&k|7pe&6Tf^~)nvWOTQEM2l3b9-}6;G6uwsu4{Aq(sM zqq>id@K=?jUSU1xz6Yxl+UjWj~_ z?PrsS+LPDcGMTL3bjJn@{U!9o=XdUPf_-7^^luDCOeebN^_oxmat_&%oi;O+-H@g- z#%w?8r%jITNwioy-qF?JX99-J@@C&=|KI(x%T%}FI`ehb>ulTYBjJ(ANO7dJ zo4>=n%eu>Un|)XI3BwcZqGWBimaUagd#D_$fNM8zP{K+ilgYQwNAu~Lq2w!tOOaA^ zUU^<+sdZ_~8uL2qdU0K5T{aX!5jxNwsRTOK$ZLFS{DW;5v|muZpmIUS#!ee!HfNmX zKqhWhx_dIE?onyfxjXRy_dxN%(i6q0^mkic$xd~j>~@{YcLk`ebYMUFDWcI%)bI>z z*2+Vz!9ZwhBoGciAA*sx?;#f^9=#Tu%VM!*EiE=f(nK0Ij%Kh1lx}6>Y48{$EgUN& z1XV01QCt&?j+Y$t+YY2S_B-BoFpf!j*K?6?g|i~$IN}2$dkg5L!XFE#F>BM7RJ6iR z@diT`ltKwJJVW8zXfahqi%FmXkj$a%HcTu>PeE$p=*(!fnAK$u!nYdHhM?}-bUnXm z%_qGfByO8-kk*bwiF2lt=A5aW%GiOaIq`}s;Y}fLX{XGVTs9?Q7H79*T2u+MIljmO z%yL9u6Yn;As?C z3Db?N0X;3!(H^FosY!`sGwuH1w?tF26H7|NZaWz2cAG?kPvU@Ari`RrPN; zboDJ?=9bI03RKOBZcGtcOcb+%DeZ?)8Uh}2%9(vnktc8o{U%vQ- z;oCO0U78Dj|GInDeDPl^zDN-ue?FFtCWvhGC#`!U+DLBw8~*AvNxfj-*c3@R*!H;4Cy4dLhMH03${p=Jgu=pppagRlOqvSmnj zpq?56f)~aF)?9%L95CC<_yuQAh`kMm9DEr{Ggt7^-1ROIb-)ad#2@O#PN*T>FO$^W zZgc1mF~o3jr{Qb%q}ds{Y|TqpA}`K-7sT^#Y)+S#b4k&-vi`j_iSEviPc=nxgT>~& z`T{f`-m3ZZJH}`5TP>hlk5DD7cC-}BB@)$^Ag#8>6D+6MZgwTR!){RHYN{>XmRb~F zlzKG9Wm44?Jy5#Ne3Sj5)Jv(qB-vh@ZYqvM0={Uhg&2&TU<8a~)g1FFT1{I`Em%kU z(?NJ^xbrSi;7AAFUQrweNp zv4loS`$`j~cS?p*L?ItjAsO@Y$yPP)I=M2L>r>}VGjQ%&&wA(hb7WzpYRr&CGHYtAzF6Z~s%Am(4tJM9Bm_j%K={rcUKvIs(a z%4-t7(8@iR-~O{znB#!CdO>8{%r9U2&DS3K{NSHy>6UXdEHYOlt#+#tFaMYHapYu_dER?Oe!)Kx{VQjD()TU@w~dF{ zBb@Om5D@)S?5_8fLw5HEUF&WkZ*HF}2KQRM`xd=F{%9$A~4!Xel}zJsveS4BJ7<7F7g~ z-_zn@Je(CCF<`@doZ}-luxZzZ#F>|0ei=-)dYr%JqKqg&+1+BmZsYO65pOsUnLNEq zb9gvb;W-XVLoO^07+DqoxZ?3b0f2}di&+%Sv8Io`$ospJNArw(PI?TU_eyTfJ>Z^j zpL81)_pp1!J?1vJC+QCkE77k+A(|2sk0IZwp%X)t+^{&1Og-QoxRpuge-EJ4Q%=wP z5s3zCHlslDO@b^{?V40IfE7Wk^1QQ(#l|-dJF7z438Qa3W3M)~-a*JxbwiOC6o(2i z$y5bOE@AF~P$NgRV*KZ&Y9&)osdYn2^evlDw_Mm&K!Zptb}uqoj4M@ZwDppYKWq5H zMpq>ASrg8vc5^)>Et@a-7 zbeJl|<)%Ty2GipvHpyoBoH=btJJbG5pe3DZ?O?0^a%l;>kXvD18d$?_;5P7s!UoF* z|Ax|<)+^X8+|}mG{g(%>YTs_y&Ti+n3pbc=vfSjqA#h9Z2IczV?S^~!yMlKX?=02@w^fLQaNkP+|K*ow zbKC-2&J+ytCX*1va#uLaQ!GXVr{5KDri+v${E-@3VVxOIeVywiHJm2Z8I|DEZ4 zbKsEQ{5-8te)KoZtXT#u$1RNI@$Q~U`pRc@M1!PQpLjwRXHF3%B|G!(tX|Tv?lyB| z3Fe*Y@VCst#s3`{=dL-?*3096oY+h9Y#M)<{)4Fk;~XHUE1mXOYhhR^CA^BrlDS4T zTs1?3$Q^5-V*?{~_CQe5nMx(~e{%*Kcp9&8w^YsvyR!8=()AzLk0t8Y(l!&R@azZgQGYMoE*S7eUR<$Osjz0 zV$u<%3WlVwi%!x$p5>_;{v#p#z-GixW)WQ6;XqcODcOx~pgt2dN)bpnve~Oe(Nz`u zw5-<0^Z{V(iNwp>sT&+O#zwL?<{yeZ7=H{sCO#Q`GWKNrvHX+87ve9dFC?GuI$V2I zd^zy4@@jRe?-$Z9l)su!_64P)s7Ns-kxdth#d%^$Dk(kDj#MeTgu-fipVC+AJKkq_ zC6BJl-(0*sySr#ukR7xPMj2Dw7x&Ds^{wzPNU@TufD(mEqK`%&Ef^XY7&G|$w2UKJ zpdD1SU(-B)gIgkLt@WHyU^eO&+wOpcGL)suabTLySo^3gx95D{=wW zui&es@@b5OweCRGhztRvPm%+^WDKra><=a&&RSx{_+~CWhux&Q&0(AfQHcsnm|+N9dr^2Xp;V$W?!hRR<@H6k*+mV43~W)D>NH3wTZAq7|&R&VB?&0c5!bkjU}3JR~9Sq+_fnrezc?3}cRj!uN;aAK!ZKT00~m4I1@ z;ybS2IP-jW&>b*wrFZKm@>0jjdZb;QKe7~Q^*>+pz{T{n13jhVe`;}B?1iQ1H`PSP z#`Ebv*3Uh-8S`jlHmOdp*Rcd$Q2$bQ%B{38s%jMd8#bXYpAws4l7h5Vdxs z-EPtCKzPXRJ@*R0dnd;GCy8G1i-SfANkl?_qE^vL7s?m<211a=S!33tI<=fdUrtEi9e+R_#mR5h1NTv%U$ zzJ6gLnDbGj<_%>4FQfy}2PF-1Nb&o<$muV4*}KGr#^v@q47;t*nIMzOq@r`FMd(~% zi{T<}*m<2{lpAs0VYrnWb3REuDLiU@iJC+&36oZrUBu95WEj?AWFgolW)fUj{xONX zfG9jlN?>VPJ1>~y4hID>y&Pn~=p`xlvIf>)bk>~xPR8kIQ$!@*%ljnfj_6fG#M`k7 zOZ8Z(e0PYH5L-p!sAqb`cN?Ym^b$O}V^JTOV+P2A0Dq7YEfB;3HcVmi^k1IQy~LPi zL?QKniB?cFrke#G`s8*xH@3SH)jpnJF0G$?^{3k1$)IuN$Lz?{Z2dvi@Op9ZMzk8g z$tUVp(!<89D31D^CTmbU(8L&wNro0VHi_}qszrQXkOq0s3+))yP3`a>}F;t`655`vVSp*oB- zy3DX-shk!6+^d(#!WY8VFi0Q2Wr)`ouK~E;Zg8WQjGxBm_GqpV<8dQhG9o+P@e9UI zBlxjcsyWKr7Z0_hm+{F?H)J;Z~vN@ZrXv=xq-m7 zlVjVf9BH{caStjcd`h963L7%HQrV)J@I@_JCmjL1;%ko;X8=Tgo;`+&c!My^sj0WJ*`wmmk3pbhK-+2#-qlV^ zt2>aSM5<%8CrpP0M#@UJP`9+-LEUY>yTXPfPq$bbs~JqemBy9qB4tr*Ww%z_9pVKW zr%yBOuqaAR)#E; z1&au#swHi%T8MA0yCPyug9deD*up5J*={jddS!6dXUx@pd9%D#W{UDonU+6`nF>5{ zsaNZz@nerbA+~%4zqd(dvF0$C3sZTNA6BV$tHn|-;}`!4{sQdU_7`B1HcEwGv{flJ zqK>KKszFmvs`QwOR1pqRU!WIYWN~9Gh*aH^=rS!FC{|lJ%~n;o0d9+1;rq;#qg?|?F~}In*`?VhW)HXPo5q{n4h6-qZG)3HHk=3S&9(LR{W#|5Zpx4 z9JWnwHjl=i(D;KmbykT8p_ocB|uDj4y&aWHKM9#_H zUjN~(Juc4L+x3-&_o1O)Uv9X%bTh{M(;uHc!5lH(OL?dS`k9XT&xDPD!6M+Y(Pg1z z5(Fb#zz_?FZUHcZA<_Xnx4`jMH~>>XtiRr3k-e0GHaS6ibGS4UKFsB&0;L3IGBmNZhjx~Pom|o5ypzRQo=$q_e_7L~`2gvBCS2dpF3 zajRhruO_SvWd-MX33sQg$E}^_U2`k7i$U({{n=L^eZ$%7I1^LW@AQfT*N~3&G!|MFG&tZTknH!^ngXm0E zGYngLU_wf5cQ`y}oRTc+x&5Jk_}SO*etO{1b#_G#w%Cv}-+oPX(?9;>mP#c}e{|$e z|8nZfW8K}%p|39Wi}8_}^vu7vwZHn(#EStJW;Kg33@yjpBZ~fffH$Bf|DgS6rTh^d zzi5rfNL}@Vnk~EEI2vhunP8YovcYbsjI7+CM!6Y7&@9a?R=rjS8V~~&kj6^aH zuWiYfsW^N+uXVkV4m#HuRvTBdYq$-84MFZQ<96d1H5NUH)%WAdJJfHDrcSg3t&`UV zH^+zN;ox?8GQwRsR;_IxR_vork=Xif2*G!@T0wc zVhfJYU(lV6ki|MQx@~6L(1~pXtg_kK^z=4Dx=%ob38tO%e?cxw)>)UJIm;TL*Ga2 z+ixJ-o>j?W(!D(rUVhQnzWMQj!VdiO3s0Xg7%_#4P zC?(_}1j}Pqh;sW5g&d7X|Dx%h9~sV2g3m*e4=`#-m5WHk@Z{7?HhQhoMXC71 z<#G&>IDm2$@q!u>##>mvm?FRlX+4d1d%lFvq7*a3z^TEJ5Yj@!AvzQ>7 zil?WZU9zNH1QaBiaWx+(qc~X z&XlE33Og6NLn%*RFdSMK!KaxGIX8K0ksQk9B8)jgnWGj31PsYN>>2ay_b?I9xQF(9 zkQrdX{GKk9p?z*igF*atW-)MjB6%!n&zD5K7fy~cRO0nY9Q4AVbY zFt+?F@BHQaJNhw&^k=Qek+(-ZfxNkXvcUFUT->l|)5O)AE?YeBP%bLO+|q#PH1EOu&H@DsC>v=SIh^j2 zMj;5^qA5y*DVl8l6lD&m4i0jEX$bN;2xoJIy5i$F!{q7L$r-rwqvrq{ z46SA}Aq*fvlMF&|YYq{x#_YIwZ0Za%4!NP!)&#{gbfnGlkZ+h8cD>FReM%6EX~C*T3sxhfK*WOOWj+EdB0@kcT_(e; zTMFd>>oaX|Ug+8E-6(JJbBHmqoQby>-OJeB^gZk@%U$B_p>NPn%ZHplr{A>yQanZf z7vq$MxnX_;KizKAcez*XCppaNIO}aRV*)&3F`g{%FfFE+nEE4Y=ryK`=uvvNbGPq7 z=QmB?6ejsYrU~H(^zZ0*ET;q)f1JZ=^l^^f1}(fh2*sM1G+m-TPjuI9bXVY@I|=ScuzV|wR_N3&rZ)C592xI za*aVk+Bi>__&xmFJR|ZNkDr8};NRg{{wbT=KMHdU?QsnX)rZG12p15K;H`J`x;w< zz9r~eCbFg3OjWn&tNIj2)vAyHXOullN*^2?bh6$v)5Fk&7I@rhG^t0m{1R>1vTNh* z`H1^R4?Oz8pAJ9t%FHhGgi-We+_Cyry63g)uDkdK*Y4jS^yUYM`|(rV8xmdG=de7~ zPf^T`#(StN%{Q=H%@cl=*MRNhiC!JZBGJYo-j+c;iHDVJkn1J^g4!g4`g&k2`@EiU zstJ5T4SOldp0Q7&zyXPcI!4n|;#BR}v^cFZr6~}nkBYCrzoR5Wuz|=&C_6ca!l!DX z44c4L;xmXKB4Pmp5s|COZM>nG2{w{(y!}feU)yZ?T$2U81uOjX$ByYS)B$b&-O7XR z2b0V~W}#)N?{?;Pi}4`?D&}`a$JudiFTdCHi1>(OB5x8|Os6)tY|hd_-gYp|e=&v* zhPg?G*CO%o-tbFdI_yZOUX&fc%5SM9BRN=}6GRN)lj!_s_h5y0lK$ubYROI_QM0Bq zNU}S`FWT)W0l@m~@Nk*5-Q9XytLbf`m9&~C7%h+65PPZKwlq(TLe-~kh zg`_#gPzch9SK48Ultyhhn4Ag(Can3n+ui7~pgMo`%v+iCIlco2HXPb^#fI*3*xSB5 z5=j=c;D^l0na9RrxkMtp@FIHS(%#+Qxqe~3D_n_Q<8-!O_WC(Xp;FPj`eNo+SSIhG zmQjPu{o3ax&%pi32Rj%lFK(i@w`^ZcQ!Q+PJ^yaSQ0wg9w6*j48XnQ=5)&dS}zgO##2+ZWk-Aj>snC5gE)zedB1r}Nkc9b=N~W^1)S%VOlDW25-LJl+GU_CH zp8}re=7wMl-EJm(XR4?8ou$=EabY3oJU-ccP zg!x?kf1e6WUU zttDoFp$8ZQX%jRUYoGOn%B00tW#H#QOoq;L@QlnA8#g_VZoqn-@XT&-+mTo&tW}K8 zkj`{=^n{q*rsv*f^+HM8#1o`g!L&HtUI;NcP~m*f+4Srj7hbvwY|06%CLRO{$xRI zNv>1Z1W~Xj9HdrMRw>jusHRZ!jI|&&kY5#~&d;t<&_a1}5UowE52AJJL)`&7H-s4We_4=LMNXMT^?&e<_x_x4d}p>GtwNzd`3_?i;-GsAgnMUcX>umyV1bb|tGWhr$^c zqscmQ{{yi!D&+{}3}D477C{#z6&}DDqw9l2XcdpX(k33w>LbUnf_QX}Zn&W= z0ZLZL8vkOdj4uk8bfg5reg(pE1;UnvhNX)PHEdsUS0{>+;V2o7lHn+Rnv;au;Ld*@ zgdMo^$#bwTpYQ5yaBaf08~tM-bm6Datz*Pz0ao&2m)24dx`wgbY*+2cv99qh!$j9q z*YPeU%c6m<;jR%F(z=ks%bBoalCf)!SUwX@EsqJAu(&)P&4iPaj7=-VE2%ro)U{y4u>fCPwc;pouw{Tms!IAn2_>yQ->tO? zOk`oH^~&Y$>f(C$ytqrYNBr)h4N1m(XD+&N(Ygz?r|REcugF2jWD(Cr3%_zBCO9WzhY9>Fuyi&q@R7c&*38=jdXR01=C5Gf#ICE#w25=%yuq@+ZhGGoFV zgwS#mBt#MyGiZSgr}Iw^!_g+I+@$!p^B)Hr==M$%-CjRLuuTDTh_^qwHM$eC;n-G; z!!QvVkz|S14i{i!tP@LvuVbQqY)E`-s9_<|Ju$~HN?@9qg=uwWF~O=39Eg%NT<745 z6^%|`pWe}Yot^Aj4NP78SQ>tmqLgTibHd|&qy^zjlPM9m5`3`I0QOdb5Ab9+Ezjds3Om|i^P{B8zK*NcV#CT#~;$*_8BnA?+2CD=x%(k{N zX}h}hHeb@)xJp{B;PaO;f;gAQteLQc(IVyRQ^L`O7N5mAjvu8;Q85eWl!S2;GF6G= zbOG_7YW7-%xyoX(`m70A%T{ILH|ppvkIQI4M#J)mJTC8(Ps&F5K>WZr^hx7jA~V1N z%zCGFF%@%OsJb;f{S;~hk!;h=!gC9>&%$*_$Ly3;7_KudJv}Wgy*;1xwf5B)EGPs_ zT-YB>+mOq6FYM@TY3Zp)XOwkS49Nc8wdhk{&M7{7VuYekUtC{|_89kIsLi0G4LWa5 zJBbqHjKKFhbr1+N=`_IeohEp`p*eN<)L~nIVJp_&>*N&N`H-A~JO55jiNGl)I3+@{ znH0dQC9Q!mFP-rOej-xE>0@BE6JI~p0LyH)37FYeuC;J z%MN76vrpKb2<^+V3f>sYG9n&1o@M-eI;Hfb!s&%RcqDeM({F0=1(b}1^GqU}W)&&Q z!r@=G?qI0nt{}UDU>>ZK3Zp2*Iyiay z&sbvw+u(tg?7u|eo0HVYh6f=_Lea^zsi*MQ7fC)|t)}DI1bZxSkigsY=pYo+Ys}>% zWgAnI>6KA}VVgaq2JMj`3flr8<)EhiLgrKbdw`ka!t%8L9o(d|y}els9%HZW+q9uI z>JK~6{S)|F2#{ouqo@pzk+Td%JF0)5Z@*(iY^L^&wRV8L{+lXmKKs!1;d`yL8OI_PAciM zSad%b^B;#z7Apa7=0BbhSTY4xwjo5!WT&R4&N##xeEJnkH6eJkwbY3J0-AKhUX4^~ zi4_t4f1l~g!nk?d^056O$HUS?k-gPt1)=Jz`ZtT49h)Opi(4IABM;N255m(Cdd&1W z+bhf~_V?-c?bD7wN_@>xlWUPKrB+>R9~G{*^F_KvR8%EdtahO;k#mb{(fQ&U#Sjb>xkgDK!bh8CnZo;@Bu)dVVf`7Up-4LT-v;aOw*ao4Q z&~e=P8=>QcLbdCRTtmbfkY|oz1~lvN!|X;|E7}q5kep7@7x9OE1uUwhVghXn3m~ba z;vK1CUnShJkSbDUOm`AW#HAowiD22VglHE+G?Wa9IFTVm+XYdSg-(j{PNEOBm9phE zvsqv<S2xdbI%&qm2veE0HwTyzmmz zFyN)_B;@gcxoU>SQMK!hNpB#);TcC2%?Vl5gOVMcdYy4SD3RlwM5k$QR%S-c84=q$ z&q!N9Nc!JjZsDUzbnp4s^nLKbMX^%CH^06h8A#WE=PRtL7Z%6eX1h)CyIUMcG~WBk zwx2JQEEZRYRusC>^Je|uZi*IdLL!0OPH#K9tbTm3OGb%=!|aWo$DFfwNx%^YN;3~j zSawWl+~^BU`O1sM3nE{+EG*(0_Blc`5TO~!0%CT<7yf&qVOW|Xlm!GW;tHQU{p&-- zJ8pat69*pZl2cBo80Vbzi$l)LTiLc*^PMh}90d#B9IYYcBvO)#IE%n7PElOLa;Mwu z2or&u(uE$2PP|A*7kVrfujeemzeapUKvSL@_fC0FdKoVqBLm2JhCA#!XxCmQf_EYPbe{FCP?;z1O2Z;^V>mZ-jv36ytN$*SWLE-b2NWdn1 z<5HF!m2K@@+&pS=^Qgt7iWwZWxNk`xIj)ZYN*@7~zLhTcwUy2DRyO&|%H{=d=Wm)1 zj$J9hWmaa%=~;4mwv)tI!9XV|2-yj-Q6#IdGYA*%BuX$G+DViBq-a{FgIvQwuHk@y z7X2DZsp0?po_-Cbh4@A%PybRg!=Vb@*#8NJQSg^~e8sj!OF^JgmaJK;!7;_PsDJI& zwL8}`YuB+$T4gn7=6Z8RJ+h++9BpU_3sy5z@YiH$vl7=?-3F+GJ>{sFCG9IjOPDoU zYrXjL`08fP$gNqsj+0xLI0(c$6yiWtvP22alCf-OAKB_7TYW3>bNv1}-KDyr6U+!O z+^O3WNasJv-p?OA#-n)9)Sf%RU(^@A}_hKns?+(vM2*-W@J$yg7@`!eChxTDRFuT8D$TN7Tp zkk53k(yEy>PjTv!b?f27s5y&Z=2(M~Te7&dBzuKHEPaZOM6{%!5oH3V`a~7Y-kB+6 z6J4cFG}1ZINq54SXVvg=(E#coyO$%G{uL^gZOrq0#b zM@YV+qd!Z6ZASxJr2OUW>uz>mcJGR1+oB$; z&@r#x>+Feo1w$aUu5#5%ns#?DuD7nNnvKz1e@A6?-q*UK-cxJy6FE0!N3JaW;TC(c zWy|IpR;*ateRF;LI>mz(e6JXH44}J43R-2UIa^;rw0_K$&c|b|S}509cW>+nBocw1 zwdlh8b4_{JLQ%|rV@lRe&r-4qp=2c@54Y;2RCv254kV!fTk%jL!xNI!s6a{R0q-H^ zwMJI5}q_qLI!=jf`DP+XOfx zLj~zX2?&X)m1t(IZC0491Inw$bbFg-O4t)^oIj^Wi4}`_4OJ2=Ry|uZoREQiIA_F0^ zGejPgL})b0MFxlo9FfUX%Kukm8PWWtH%F+LNvEsuvX;ka&iYfZKEmBOtHgH*h&ojh?O z9=F?Vg4dJa#(6ZsBRfCB@8y5W8+b6h1v2fSL`x(ykQs*R(qozN%tVHvGGc}%MYBv8 z;WNW!-T0RMzZ>5q*~hR3)yH^)$jHh@e-n-MNezZbq5-kJ{jZicn7m-l;29yH9j*A< z7gt=Zcx>j@bLu@#tz9tmt@_M%vkef@wYb$DY2xAZ_g1Xyy}7<)eZ)s>ZuWlknOnAf zz8)I#gfONp*@D(Qy3|iB7no?DV2)rsvr{43(!jJJ76bL*Mk3DXI-Ll?&=$V|a0hlm zM{~lUfgEP=s=QfLDV;OwA#A$U;Y@Ia3HHNremFS*Fy%M62*kN8B9Y)kBEA`j8~|N| zA#AbeK@x-&0dQenL^a*8i=;6(dfao^^D_FO>1gOp6D$2rKub-FJnP-JqkBxd?QaIS zh}Kpykl={D5%h}thkja%pk;jXZX`0rMi$He{TLJt=s2tg48w*I!?z#2Y;=>Th* zWiM87X5#`NE*K26*%cG%)hi|j&fEBmCA{pJh+*0J8#lZNk-G@ z+-9#r`HXng(3zWmBoY|I8iqQwV1wqkp1z*FQMl1|qjZDkdinYwKQ!2wh}RSp9n}C{ z+~BA&>y8>QnKh={&?n05@PfD2B{du_G8m|*^Q-k=@B99#dxjzQ_OJEj%<~x5QdE_WHDE2%L*nh* zgcfMP9RMB?r;k#YjFZq{r$WN*74U5|h?u6#xo_w;ctu-Qy3-qusChQj& z;RA&xCcJ>63_#fGSQJ6Hryk;Ij1G50Krc~dNV1L|%M0WXEU?~L3oK)U`DFA7$dqD* zv+`;=X-SsU3fHEfq7|>&O&tnqH5Zh>n@EKC5itldYP)-OT5|%2sipwa?wRA0FzN&o zlCBzp4vqZDC42hsx?tO#BTp^wNVj>bE9#1`Gv#!P@vy9-vdMPM>MirnyFlAeDkhlf z=<7Q^b@gpOpMH3!+n%q#e_?wVN~)S$w=fqCmSo$``cqru-5bum^vKV)ohwV=r&w5D zZlEwMho~(2c>|XHNkS@hZ_tswYkdk6oFVBXr=-!Ktcm80EdAVfI+|AA6_RiC{ILg zMNSRmX)QmN|7Ln$-ca(FqP3Q;Y`>`awP=6K((DHNfPXMN5Z&0aIlEQ7$bV6EYs<~z zHvi7>w&>36?f!qre%1aJ|5w9bi9XQsNcIWOWB#Xu|CBxA`3}B=UuHkdew=Ml^4F@@ zruR7Sci!)s%5$roD8}2c2AyhX&;i*Vjxce52Ek*+)sW0_tSu0rA`u&aU6G2QaYPTJ zF|;2sh$s5~2gz2^J>aHaa{tu*M>pdpkpS)mxn}GnR2!Pf4vp&5Hx!A2Jzbjtc$Ave zu;oPB=}mZ(3YB)^mFkHrDCL3>%BJlXVu81fc4Z+PID1AFZ_LTjh@PZ~{N2eonv>Fk zJ(#&lZeLz+b9RMX@}@hN-Tp7g_5JE_vb%C?YD;Zo-#4!9xq#XK@ueHuf~qQ-t622C zy8l0a{610@C775gqJPHx`a9n}GSyD$9%K4B3|VP(s2MBSLTDr#@j6mOI8DhB)KKN; zYQB-CTpMXhq!FM-BtjxW6!QoXVIxH2B?lsrkv%>z=E_tOgPpBEwKcUf#iY`lY+*35 zItCiYl)5}&)(w9`bJ@C9TR-zg!K4x+)rJ^qal$_Q8l0A8U( z;;kZ)mdqRrr+AdaTMf<1kO6HgR^RMxbemnHHT&lcoYsOix1b16IifG#5l^L*zGOJH zkP^%-4woV#gFFVF2+@KpgA7A)SR>rbB8^3CA<}|cC`Td^i704H8CPgZ5wV6ir5sm` z%5dhf*$g0E#~VGdZM0E*addjf(a1ri=D3_jv3!Rar@NyW7v9vmyt9tfvkZPKuieqP zw48{qcT4Vk$!R@jem%Q5<`az8xIdB-kek{6lbh)W!Se z1wnwr@UrFf3HlcpURn+12E3%&3B0sxAf3^O_(~D+l_Gn<&!;Rfo{HL=Eox8kh89m)Dj4aOR!+Z+Pg-F_R~c_I(nhs~TNp-L!q;zo%e@3UmTvvamrR_iF5i zaX!d+TvE^z@?`?K2TczO4=eY@?@O4atTmlmp6hSl+8qX;UJNeHtCs22;9$geqCK-eGgBhjzmHwQa?FM42SOp+3DtJz>rk;WKC zKcEW5mR5>_m+Jql&nH6yhs#rh7fZh;AtXU4(jOtxjB#n^5jz^TkJ!iUjC~SyXug!+ zSBUV4&+Sd2VUnE9K&CdGdLAiM8&aNG(L@fY^G1}T8Hj>C9YsTf)gqQF51dB03n5V_ zPLYYjNkRBI1>mq$DquY=VKuv~R&%o)+@M|$ZfNwZig38w$lWd|CTp3>4(b(8)0v1O zI#@2^hz1dx;e!-R??`cJV-Ph7JLq0e``}~lBk?20$7#dRAQ~k_-!^Ezy=X7Jm)UE6 z$U5#G_m2n0gAc~;kMGS}NP&cHBr{}#W-i8yiMw+TCmzljhXz4tcBBa7Vsmg&WX?)d$9@ZS!8w->ma#nMV8a)Rj{aSVj z|Ad5pLaOEz3C=mG+3ol+dzBHb_;;*ufs>kK#lK_4$KXYFkmc`F^v?c6dd?mcxtpC{ z-Ro`Uv%p+oj`n8KR3bIEz#TmvO@8Kr#p{&F<}d#E#p~Bx9d&!H(P;3Iix#c_RQ=ca z{KGeOtZH|Nl7-n{fAzjAm*>0EnZlBbzkbVuVZo1<+;i`F)kPPMcURYMd%$bA$(V+@ zPXCGSHGG!}pqXX_aY&Oe4GR$$y4gYuFc!BH8J(o-B#g}2jFxgX{pwD57kWW6GoQ12 zTm}dorVxuc+05}{#p$CB9`;r<3-1hl@_F_6L9%ww=?B2;NyMpU#|QEpaZikxk=Y(V z?kikqnG2Dh(=aIF-!=!3kthd7VoWg-E@*V><|CHeDdB(4riahz31roT?;+vc+mHf`z6n$?>!&4H-vWkQNN|%y4kYX z`e)h5%5FI&Gak3>^D_umuE0LTxJ!+@WlA)SSW5+N+VfNQ+eF5?#1(jrGK1eQr(ot; zD266NbcjO8U@#_J15PyNL{Ln3!a3zU?tI6|I){T#?{2CFjp-hSP7Og|rs`}b5?2yphdapprpsS{y*(YB??}bR78W+N ze4+E&yfvRo1N7WHywu znK_8eKLn~LFHps|vJgG4i|`*dLo@XGlHSVBm7~^kdddRB3ueVE+Y+i5Usb=Pxgiq^ zx(|$a#S}mBi1~?#c2% zC!<$z*1-+~UGjT7&z93*_K}%hGTpsCogyhx4|hH*sX+)xAZT|4?fxJoIs#%S*qj`P z)V*#x;`F?zCLCh9jjKc((CGmGE_1Ox>WQRm^*`jd-@Itmwp_4tDe4=nWv^LL-N<}t z<`;WOHq`gV&KbOC3_aM_7C`FE!(#&-D{1cBPFe+z9fr|q45JGD-Da&f6Xlm!GPkn> zFA830=0BdHAc$i6!w+jk%n^!bY-+8tU<&XiQ!I+lGrNfK*X3j#ME`S0EKNo*ek-J- zz;%vg&-~Xzjf-y`6G>K`W|CG58{|zs#z#u$S7N}0KIN{sd@g_76ceHjMM}tuPw{t~ zszSE}(_VD@m-EX^3x!4UBHuFq75rEE2TlLze<-jw_9XQL|ETHfzOVbA2z-Y>WI8My zmY?%I?|&gM75jz!k?@iHu|L1pgkmJdY`9F?Y^&aeGkUvZiQcACdK-`Ht;0cDP4flI z_SntTC>o_lj5jNvGv4ml6Ek)5WuYur1Fx`C(Kr0uox*N;myhX`mddnKc7>f(KnYWl z;0R+ZxI@dC{60nY`AT|?lYrl!F!8v@bF9%|;IT;Nl(5)Dv3{RfoXh-Gg+44BzG*H)F z|Az{RdfByYO^JYjzFryekf3<1HB7wTdk(jzgc-u}-oa9e0Ab03v&t)2Ou52iBiU%c++&9O4dmR<2+{e?{Zh$kI!v@u^&lS;f) zXK8C!$Y!#eRn_1KFaG2YjIpCAnt0HSPoFRz#MqK!jy13)84WvZbPkMsHp-Ngc|%%_ zuyz)}yjCj~^~!0@vT04DUJ`T9g(NyMNK^{4mWf$`*Vl5=WT4XI;yZFEM_sQX)qH&# zrOo=)bGdvpTF5u-(xgn=Pz^#s$&cybIOMGco{>lykYKIiNnr)kp{A6==E4=Gk;41x z`{}=^e@R>5fCJ77Iqua!q#P|2GFv)AK3^md7Yha<8A|4o)#O_533BffALtMzw03xR`^r*F|T zSxqQB$D6oBChf{((ij@lREamac;1Ai6Q4UFs4hWJGY!b`fz<01DpX@iCmlIJQL^fAYoiYa@mu$%WobPCg zV?Soi-DVUoR2%Zh2EM1MJujFS%iEoIIPZl#W8Q+IxYpFie^K`N`}&XKq-Ukh+Zg4? z2)yh)>PJty`IV}9=OHX9`{MRZ<^%L&9@1YjhfgP z6eRjN$)uVbv62lq3?F&C+?$hBP4-Vnb15(U%9T?|(r4xkJeT+Gz*kuVuci*KMtNdL zO9>SAe?fL^to9^uJ8d3&rpw)bjC@-WOiYuqvaD_2eJoAvKS-sCWmaK(6@b2v5@t9( zrt6Gqw%J>rZ72dPD#EQb7A4xH;e3fB6s0*dK@*8+_8I$i-q&n2Zgh?{lTFr537Z@n zvp1H%Y-(5ss`60hOpi1hLhp^M%F|~B%EFDH5(wz?;$OmnpznSS5*!Q zv`9p^JIN*5Va@0FbXzv^rY9*Wr@TCBwy0{Emv2Pp%PxB|a-E6a+G<>lp+H6YsOMM;z~W0uco3`MhC(P);Ml#^3j zrn<_?RDAMYljQz$Xz2^T&V#xWmCm<*=cM6cweVrJ7Y# zG4Jn8B1z$_UXrjc=p#NZS5^me?Kp7axlD*Mz4$6alP)5oQ zNxyLR-yAemdiYxgq(9($C%O3~)s*6>vBr|^!i?QMJn;(aj-G@X#(&c^a)YWB zoT}og2!?sNHeTB22W*vDK^q_NxvfScXJnhUp`H~lSVatfJCPmahXdmrJRjd}i7N?v zom$|~REA`0r5yoq;NgimEA4h$r2~(`n453W#HB)#7C8A(=;rS}K9423D=QRtWp1&n zG7k@jlEl|uVqr;@c`*kb?#fbrV9g$0J08v*W?(7*q>t zQrDjNu6-C^5i>B39!JSk8vpGSwbO!p3TNezoK&1Ki1)lME|?sGr24yGx0A16AG@+lM>6rNi7u@vWsQOWMWRm zr}#|;E`PQumxy`rOzbJWW;aSQ$^E%&Je^2sRg_oHx^KF4G zrl0w5C^!T!l)o!yqE1p!P?(=@CPGv9arM5iR9Ek_m7ve>FU~i+FhOBKfl%xV@-a)k z-()Z=g&6u!(@-*X6<-*qOu*LWL^3N=ZRH-1-^aCuVrPp15v}xth{^L#DNHK&aajUG5?OZVA5W!Q&vQgg-*l#;*a zW^n{x4IxfDnt}wqW$u?^CR%u@#oT=JXZC^^-j=k4S?ukX5iot8xYS?Ps znsASy*I?Z0Ay&_7Ye_`|%(6AOX|lnSkZ7A{m}Yy+@Pf^#_sU#CJxSCiB{20gM$TcR zfh}MXD^}$?8|kr;`L<4*WGj};P}7e>Qg|nKbFajyEqraak9Mc=*IJ=iDP#N1X%<6* z%@(2t7o`SjkqkDXCU~s87DEFgtk|l`_W3qqD<+cJc1Rip31E;$#R@42Z^hdRH&{ur zHD>LyQma4WsqswpP>&k6L_s2wFUfPOHiU3{gvYt9OTLTL@us8pqkL~v@nt>t2HU8A z`-^k8jprTlYP-&Vr3XUvLw5mA(uCYkIPpTvgi@XsqbC-ySdp;B_*p24uT_8jKvJb4 z*Tu;{9&l9}x}2Qce85?0@VYtq!2vgtHbMT-5U`L#|Ar|0Xh}?KF?wLVN@7hiJYpvvkrx)^{&u76zBVi4&T&MOGnd>Keac{O0j=_xQu(Ul>ovr!`I}kf>DmfV2bq3rgpvmC-2X7xrUV2j-su zdg1!Pxx05)r0`5*-eC`r!!KfI3v#hzSKmk_t~2&{Pn`hYt%Wm7ZeD zzycn)GG?c~o!*v;4Zdxtg3w`#^C6l8GWj*AG-oeRH$SC#YNI(Z*9Oxz({2+Dnu<*^ z6fHZwH{Y&J5c>N0&nf-dGNF9pv07}Y;34i*;xHnW$dF1ftEnVOLeH)^Q^93GFFw?r zkMd#Ymrb6*AFy$(BiEn5Z~un%`}b{Vy5NGQxffo*mhsd5>o@Fg6S_ zT1>a$eT5&uJ9zK_$W~zxm7KB9>e_FYfPH|)Ga>LtsUUVH7*rPp3VxAA}< z00tO);vMz^TMm3Hnp&7FWykiF$1Ig4c$DFhi$@V2{3pB!DiPY@s`Q45VaminONAlB zoNEn3IYe0?9EeD^Tw7Q&IprkTZ%T_qBNgW)u5l|YGdEM}^mPWL`GEzF1s>YqXz(9tm!io)TFK$Pr81(!#RQ0(J?ddDfID3sgFgXOgR>XO=E z!LUFDsc5g>P)G{fH=qXF9vDw-HF)MyqW))>t=@WRK0#R;s0CCyBLc{;ay} z@nvLU#`qD$VROxZ07;oaJSL=kMx@4NjVXidoC%qeM({OkJtUQ)=eBrzAeo`3>1RJQ zFi@bapVU6sLGjm(gIxFEZm_&-ml~XImt@Gy2yv4(Ay0-ua=V;{veJ+$MEzUI=sWGdlZ_SFTm9w(LZ27@~`yS{W*m>v> znb!LNnSOBO-2+eEbN9eA+qYv`_mE+CzjR^Ku8JDV(Sp?`M-ylGKtrV&krikf!iL?<(&q-|C{X!$59S7o|<;i90|SL_cK`aBhd zkwhu9B`ar3R(5s;cKzsCsl z3bIkjhmDRyk`)Df9eH(M`mD40j13j6qxR#x{~{YyBOeGs6yJax`*(f6n8}G(Ce`Ad z$$oYl8a=bd6GMjVcrP8?#sZoW(>+z$csDor`zcqaCtDk6@^v9$O^Ylfh<<<^dK00&q{03WwVKH$K6Y&hsa%N8)=pK=m zIPh$6;2B3&PH5n{;WKs3sXB|e3dao;PR}=l zLL@1FH2I{zL%M7rtu8MKXB_!s$?qoTy2UOPpBUUyRM=atjM-3$(QGYC44(${lK%VY zMUqdqwEV6v<&W!PK2qP;q-mifGf|2T{3)Z2+gm2H`FgoZ!vd+BRf_R&BC1cm#+;T4 zHMZVKoDf?t6WPZ{k=P5Sw@s4iVq0v!yVEVX8@-Klv|o4X`@e1)tf#5IFVe(6KKb5Y zmOg}*p3( zi)1-u;t`1p__zG>A+{i4TY{8uNa3bYF+N#h21I0=BuR&G#u2NELaZXnjj;UI-b?77 zc|wA76Te5B^gdcGJkuL^p(+1|9j?cw=qRBAX@Cr@{&4lhhi>#F-t?wTs2D8baTJSD&4w`Aq zlu%9qB9VefpBN32_cNIpWPURizUep+W?(W7L^%hjgxa)3dTH%JX$!(>GMYvwAaIKG zBI;lUQpXMJNq+){M-Y{Wx>z=tlyU>b3S_cP{+wGs=#n?cbOCysQNxsja!jW3gVGtm z$QLo6M9R%(o6&A8HquVxdgCS(zH=?J-qiFYjtCBZ;v-Uo<#;tj`ux8Be!myx0Y}pB z6T4RHuR;wv!MD&@nMI<+ZITc=!=!JYHy)( z+WC~&Xn@vGx|QytQYT$cH&NP&-d0L&G(xA+`8dxR#4%q&*d-ujo7l^Qc99KaGoe6Y zWC5XktCI#o$y`+jRshNu^L!`Bm731*J#?`r{tIvui5ua?X@xu8XDIphd3xZxNrYo- zcx7Pt83R>|$z|lqZznb(8(cCl&M=O@hlMvEfGYmMZIr*Ein(mjuBzQtlD&$gnnKj< zGZWd)-}w35Qj_ZbU=L7%0_779tjQ%!FEjy~xc)nw+c;WI22HD=>qjaGj>MaUp*fX%?* zipss3AzCrc*v0D8KECQfeCjE_4C}M&Yf{DP2X04D{}fET$}1>HEE`0R z4+i|D)Sz_JJ}=HWa=X(yE^p16I}~F^qTlJah3At$uU4k>V<&XqLZ~;#CA;IAFWF-f3GZcpX7dl>rI{p&IK`lbU+|h>rIS zc~PK$vE+_hm?|4GvSj^uG#NK*ve{&{7CQ4rP8v3*{bK3N))@9QOJP#p$ok>6%PwKd z^TI77GOY==k%h%$d#1PS$j+{sJ1Qk1!9Jp(WJ34!wjF%ls-u8Rcm(x8`nbfrtvC>-0Fcw*bO?TIn5ZQItwn%K6jiS1;fiEaKnbI$wS_ulp2 zb=OMot}b*}Rae(u*-t;+z^hVxLKUwJ%*ioAbJWNT)L?KfMJ)!4paRMqBKLmYfmuYJ zds|awJY9Q9p%Q!r{TCw=)`p?ai5&Cz7jq3>(@o0+&-4s=+}RDK6Q20)X7;Le7PV{sCic#`R(~`{4O`oHMx-CM+r#KMIXp*{jUa4Nt3UDu+e1iiebB`M#c|ZtMks zE~ZL1B&e+L08c@@i?WSwnSR8pA{^W9h^D>Ax#6JZqmeJYo{bc@Q?aJ+Sd zKdAtIPQeq@A|}pXnekg3m`ytUkr$3)0jyV|uO`fvlvwT|c{G^*rF$#!;!X(UyTCyl z^mnQs3UnCz?Sx_Jf=x+@6S57L_>uKb&ltt71x(P>9(y)6*Tou9KcvS9hE3Hr2d9S(&sMORRlP3^n z+tsaOj$d6kF%_Z?)=%SX%Y9S6U%%A$L@-e%O`C5R{is-4_EOBm&TOTB>-vsxzDv{u zVZJkjGl1c+VBa6V;O#vrh(m)%!or(YJ&c&br9)acSQxKFMQ#$RV@#=VOfzdGY2oSb zGDYUqDmz=#Yx`S)9*^|L508)^@T&?)?LwSiuNoKY)?{TtE6oju5mOzghtF0Ft=9QX zulmXzHhvgFP+2nTZ0f1D6jSvl8mk=zz3ZGA))gvHxD>KjUMt7C$fw_)wQ%;&LQ#!| zA4Igy>L=tl$5LCnz5nL)>cD5V3(5wTH}DPbL92~tC*S(oMLvt;&UCFW!pE)O_~$w( zk{69y0~;oTwn*8;-wrI1zo$LCO& zuN~PJSk(a%8vgke2hpZy>8zpN8J!~PQP)3`lU?0i-T5D8bDMjq2dA~RH%?jIoXb^o zH!OarHEWVO7(c!4lb>d?tflxp3Ce72+H{kLf^i%TCJXd|Ejm{~=>~q-sTe_34ky_!NR}Jv zWnh2jOXI&wBagvmiXVPO3dk8o*E#Q?IU@VBF{w-5KJDVKQ->v_RQLU!18T%v11}%? zOCH6w)j~r2eoq>WDT%UCV4dtr2m@w?Q30V*pJO6IoTvawfwLOuD*rV-dW%M+ z)51bXomvICR!f=Wx08VPS&2C*8+ZJV_*2xJt!s%hW;59|Yl}V~LY&sVT-uF^@U;y2 zYfN`j*X3VQsHG>Hq~^)%qmE`_a?zr{=8}CJPKfEX>-X~N_9OtC4_vL&B&u;JS}7_Y z*0W`q>3q)CVmFEHCUDQ1H~FVIYNBWkn1)?_hK=(6u-u&-U!yg(v*OwCgmPQsMn^YdKL;ZY9S@15?QP8sga06 zLjQPDTY zBiWaxDW02E-umxY%bb_XyBNd)yd3R9nYEGL*wQ@T2`RUkB+6)MSr?*t;z((pIcXQK z6)1>_hj#!kmc%8XA}U_$2On;vwewpf zoc?JeQv#LPpwR$->k;a=u#(l#-gaCBa4n6}^tWkK4BJL`BmXXm{2~AJ(8GL<%-(MP zvlBvhb-VOtFS=_gVi}SL;7(v z$sIA8rjmN{vZyUmDu7rAJyDY^WC`TKcG22_T%38VqNt;94wl_f*h|GAEEu_MO-OrP z(nnKnX4p*ehFP|)WbLa==fbMNMs{4PomO8;6ZNU# z*O#oV7CP5h`G5jM-tKF@9nXoa?98NVgX8svU!bnw`A3CE-%Vp+d6O67H?r#KFOmZ? zU~*`W={)id!|j;b#_Xi+9Y=!;J%1%q)AS!x+kRD4^4=f8W%Z5eWFXUg_}$X;7)gGb z^?1A2_nfADZXPD9wemO;HJ>*6F&jO-ahq2D9^LM9a91C8PP9J3$MQ0rNk1vW>uA32 z&EaaO{AFY5qch`ACPsF%>1O=X`_r$F8Cvz~R+G7I=g{s8^woD^*R>B;*TWZ^nN;QT z4=vY*ebeIg4^W$_59Eh6<@2N${KHLa&Y5HGeYsXZ^Z%^gEt{|ZMf#?L`cWMrZ*%{? z?l$iG!{X0kKV%M#5~f^C>D0f*WsR8-;-4ZjXO6`*qSt}EI50<{0LSgT3YHQ_?Yv60 zLBd%pc1CZpTr~$~72@io2|^rs%n~e>oHrLjXpUK8)h73ATW^BPItclS1;!0)!5lS8 ze}l`K^zPJ$CJmEfu@IY7kh$PYtT@*gIfu|JKMWC86Zw`xFvAIsRkL3qpY@*#UU=3F z)1o2dr2DrdWUZC%rInBno z<&1bX<|c|_SB}pUnu7=*#!2|r=iY#OSLa58ZTvDfmU#I==v*qiv(W3=3K^gf5tbyt zsT5uFMU$~2RPL}RA97JJB@8RjbVI_i8*6t>B!WQ~3;ygdfB}9tS??WBope%@(Q0ts za@w{xDca3DI1Ik;3*1t~w=B%1C{pk%s_xk+@z|1l@VQINjhe5!T>4iZsxLmNb;nyx zfz4jf6+@mo&T74I_88^2D1k_WO48Hdgc#Dcfue4WsMR;<_X3--Mf9Ssb-I;{iOy_2 zSn5i^nPsRCjp)A)dltd=rf}j&tGW+xSz_HzEOUn7rjB z;)6;Ye6vsDW&m6S^$ps1d2+?gn>G8U>B>Ph^3O=ln9>^-EjsCaSn=Rg`v7irPdV@5 z;~YYt5a6&MUtL3eu0?jusWsj`sT6@~a7zsmDJ#^}N;GF$p^xE{g-Q2SQMc~$L7j%M zhOEQpuoImf=ql98QzW%^Fg#7@$lJPx_CIeNkBF|Dl+)Ve*-kioE4hd9u)EH0>g`(x5)? zP`3LWyHy(Ebn~EKjV9lu{Lt7wd${1XNcx&LOgd6}EHGMH zEATE0H;ebAO*7DttG%78J;QUAi@UFP=IpH3+*&&Mz5+(hQfFbcv#61DeYq*mS;bE3 zWOch?5^Vyvw@IbgMYdB$helT(ky%8cZ!1R{I zwV}DHAKkgR^7xbjZqiI^er^RfbA5edc6A1orj&*{YonE`CC^>Q?yhW6Z%U~ne=y6f z&|RgbywOusOJ3em+U_@3$1L_&ZtrWej#XN@wl;1pRr+J-CTFlEI%};4Ckb7+H7@kV zYMb5)-mKo_CKa7CbwNjQ*aM4xut)|pReS^pNa~AIv+o$=*1uqzs?H8HpJTtYmDDzq=&{$+CNk2?aKl}R0r=DESd^PwQI z*LxDk)FhgibxYP3$|r+AxC{JDu)$YAlifz^^BzOde&^}j=u^$2m`ymLV8AiKu*rb( zCMB~GmD%U6fPcf2Rhl`uwBfVjtiJQ)&ug!(_JOVz{#~fNE zt+k!@qwmGgFR&nbSTDKf^m97s=#`$OJ%n&hN#qX4^Nf(k1Hvw*fo6oO=)R?~TDU8f z4OaoE;bVuAA!J{sm=DLR@TOg6J`B15>fc-~ST>Mp)oH=f;C*1u2FPRl$nt3v=5BHf zaKAV7%se!?*)bgP`>pdj2VQgW_)3HORN+Fx*BE$1vcJ&YE?PWwvg_-pcRlF7y&}KS zJ$4Wa&sz%{kyttN%3+p8sqBoKA2QzR4Rfs7v00QZVOD`lmZ}u_3#Jv0gETudIna?% zp8UA!E!Hm4hN2u3+h>wcVWVajR(O)(gb+>Ukp@mukDwh?uz6a(W)xW%o+noL$@h~F zxF2w;KMqwTUOA+NUyX5xcsyKbW(@Oqz?b&Wo&h|st6+}MYOJ@yfk3tNyYrYbMb0u(=2;z=H z47V%3(nmgL;v?$Y7!nukyJW~d8<_7#`Hiq!E!r~omyGwS z4&ila%!nx$QO-%2*H2;x8Sy=7{}FrK`087{o5q;&@6JYDGx1Luhm5{US5KXE+W zBDzNY8rY2*o@#$UiKIw_O%zc^n5WJvk^}~iJUE1`cnZJ>3yatT1j7NKGyrrIu_L2K zQse_?~VN9XOK&0AiV z!n{z#NwmaC#KlR}#7X4ENxq902oNZh5GxfBqbwqaABKH72}^hrtc% z#&IB0T~PqMDfjc8N%{>}>X~uuD8zDp>tw$1SUMm*#NKzh)N&yu9-Df$Gf#P`%=gO| z`3Lf>HuRIr>%&XuO3!dBLcBRi29Ysq!tetq(N6~JwwnLU4)VgzPSLvHr z6_RgIrB`E^Q7`JnRVw_*^XgG2bEUoudHlm#w<&IsF+*CnHXWiDgIc#f9isRHTDSTg zqB#Jh2A~44h<1pMypYKBmGnH4@*`0yk9{#@w6IxZ`U!r!t-J=*uV^Xa2_iIGE!uFSo^| zM5=qY;TGf87HP=`U;a)h^Eoh=Vp9~Pr65HVVKbqeWHaJKG+iyEKr+D%a1Rb1WCW#c z+&d=y5#Sx=1&%w-Ob=eUPUPavC}rEh(q{0%%cu65DN})}pxydg*b3NT(<5{^-#|(q zLf-Iq+F9-^FW<$BH3}VC>mkzy**4U#m)NNAnNZbufqO|7dw?^$p410Nar!qCD{MgIO`TAk2Tzgx$nKRYY=1sH7=SPD&& z?2_z)Wlr@@%mw;bu|hotb*Z^S*M9po6y2aXV+wLZE^}Av2j9V23FTy?loNO6%>#B? zIC>**!s15UW%izx_BiO2d`XpjCP>4b;2bZx2d;!RDZUe3r2!YNo;WZMwt}jZQk{b; zPb2Ps8}sCh2X)|@5;y7Py(PvjXl;lKF{iA|#h8vfB)wJg;OtDdDO#Grv8!@{?Tp@( zyeM2dl55L2FNCz4yoY*Y^$y^Z&B^c1r=L?h%DA1n<)t}6eWrZN#4~KIh>JF2wPL~P zAF8{uBI1sgH8!nDJC$L}p*4_Rc32!$t!+I0+LWC&MQ4OjAAQQ?0^S~BZNP85RCls6 zwH$r==M=+=y8XvS@3}trGM)=R_XwU5Vr~B^lxG5Ozph@lA$_gd^2w>SOOkgFpMFmL z?kUcF7F?a6s*y_5FS!*`Pvh=PGSe{)hnP%qj1f@>5xV$adnMYGv2m7rGuN1{k~Ya& z`zpzu$#2P^Wd&np8nOym7752{bMtVlS`#X;3kHqaxK*m3X25)bM4CZUZ9&OE`HI-!Y1#5qki?yFx==8k zsk(4vZSgi>%$-rX5a=FgoiN|p!fimPJJEA=K{~T_5!pP5JCRQTL5#7T+dQk)D|wOY zDnQg3x(Q0>f!~R^?gY63Ea`-?0gjhxdjU+}nX`$=m+8n4x+NcB>XAgkKyh zOywQH2X20=@k|4D7 zI8OM*41aHS>d-V!ygT>10N=fzHbsxF0#tBCx{5F|&Y+lJ>YzFtQAP5C5M#NavqIlc zt-Zn+Sq-$p5LvCH!emhmC6e*nGE0db^D-Rf&_N9+L*|6xO>P3M5$w+hT9Zh9Fs%^g z6pO`PE&dQ3n*yVc(?K1fzty2u8}83&p#$Zs2wloA{Z1ZjJOrFQs#ciE6!FpgZ4gef z;ccN-)vDmgz1C~(7Ij?$dOzts^j2`Tu?+`yZHpDd70?uodyV_v{p7&pK)c`wVG8Y1 zrXRg3Ws*r}&bSD%Yw;>t@ zX#H|Zky$~s=9wDdY`%q-_HUCp2XTS;R4taU+SPN3;evK6443czM!#dZV_7bfQz`Ca zyMw!fYf;H6oeWx=XI^0bopS8u%(q@er-adqdyL`?yYHgR&rmYim!w>Deq{5PQ zFat%K|9zJFQ2uD@aOz0)S=s|*LmY8d_*QYRY52(Y=;rp}7VR0^JMulaYw9Ur$QWY#YmL)SXb6}cu1&u{dXSub{Y5@zA?q8StV57db4S`%sFvt=+ddxh!q@+!tM{A;{JN$ z5WrN4v`|J>iA}G7n3rjkpjFJbDCAPYt&CA2FS|QFI(BkyS}kBJuRMn7Sh68p5s+Xb zGD;^g!bW47ATYu}Rgy-(9;4X%LA3Wna)i1*P7R$fnK?cC2JK|3XfB>BXvjzcW(s6jCZ|^gM(X+$6 zXVE>nhHP)81^AjmXJ^s6xx{R*FoRD`qVcSexVdC+uRH~qVnAhEqw};YTpkhi7SjgE zYk}v`p?kH7TplU)7F!3%r$F=R&^9^9Q7=f$13gdVAQKjmjl4`FXfpT(cKyfg3$@pIX3psx??_a;F0 zCOllYGA2#x$ri^oHE^_gRqGb4aJggO5;g|U4cnLb?t!TFVr#zFlP}Mmaz3Mk(ATT2 zWSmZ&^4=rAB;guKyt(z8BS9M|29`$bD=Bn5%Dg1YkT-zs$2BK;z=oPS>lQ2)^e|Y2A?)3@;w!w z^0%d8AA4Ry=!?l7{SU|Wt^S)C;Q_74rl$$HVQc% z9*NT7o%xi2J`L`eXIs!3A2uMAm?;jNK}KwHY7V&!!QLxp#*Ya|vT#@76$LtzXO~~h zBi*TqJ}eUYzS|dw*@N};_Eq;G5-9>p#Lc^`v<3L#PXsx_eyQ+C<4@>0z!Bf^zc5z& z;F~6TZJ@?Cncs*!1`e7?K89|QxMe(6%V8;mJr3QnBXy%OADHggqQns0ISO{-6mQ}b z>^1V-b*KA((2MG|Ho?Of+ZZ`IIT-6({{?LgEZ||7*a;X2{z6&=3|a*2>>N4-%vuCY ztPDB?tXc#t41hL=76At{^Cyjfk%NJN832D4Rt5$FRyLMT-oNe7nhC)37yn87f0R$& zzx;qcKKuDBEG#SpoJ@=a>`Y7qY;5d*%U_t0k${zz?Jxeb{Z|JL&VTp-G>(7aPuhR@ zKIK1QHURw-|5-Qz{r?C4!~^=#Az)+C`WIl+`qUbL0hs)$%BKncUH^~S0Q~=`|1bRM zp8wP2|ET%jZuzU?zperFtwX@b@Xu%fh>_`^asRGclhnc7PNM=YI+S!wQgQWoH9G?4LBi*o+K-k59us%U>lw{lLfx z7#`rUPYB?Nze+H%0EC%8hi2jgxaqHVm>E9#m;kD?eh$sT!Aii&%%-FBUs1*JpQz&D zp%*i^ax!+H7qilLG8Qp5v^6rOmoc_6buuGhVP@vz`%gr=WqK>G#pE-|%093?z&b(7ist%m8t*HF&IJZgMz8Q3D zZ&XDx3^p1os!M62sH9&X6_duwmZ53>JxX*^7U~$V^jp0PBYTCbrjR6!F&(tE&b68%4SwhT!8#3HXQHJ3$D0ux5Bjl-(RtIs1**~{j7{dW5x{oJa#zwlC{(lOd0_eQiZ=|kta*3hZ;@ebPh zm8|UlKc8^^*USs)I~t4G+BngFGq*N&BxYieGj;`hD%o1=+t3KvS{c!c7&{sQvXZ`y z(p&oc_PH6_ylHc6W3#wwAOpu_a(; z`e$4K_M5G^q=>A(-T$adFQWE$k^R-@(?|z%J11KQz*_r9=0COoWIr|jRGsOsVSfea z)c|Y!FXR7a%IA3hQC8l;$k+kkAd3Guh>~8(*wh@bnA|A@jcg5!DFObnv$Ha`2KbMF z;eYGrQ-}ZbBlzimCjt&mMtVUPQ#Es=&t=KPPA{x)Ct+-EYUV`1#?JolG&vE_GBL7! zE@V@{OfmgcRY=IzO^bk*m6hW!k$~wlvj9YN{!#%;-x!drS^hEl(^5HoYrqWt-@@UO zL+zho*%=te0*UyZxgo6JTI^Wx$N6exm;;0{)lB z>DB)=1Muzizd7Mkr~f$N|DK_k*_jzR{!LeaZxKL~^S?9JX(y!Hm*FQ49_Dt>&eOXp zYERdM^LYhZiE=5lFau(JBN5g?2xtaO?KtWQwiW}se)wjRujFIEbTBUKfK8nPF=v=x zQP(7C)+8Aut(+b@>s|Bq{(N`u`nY+!e#;5fDC*|szM0fAeE{o&BE%)3Io4vZU0kP+ zq^=4{t|myP_k%#Sq!WjN%*E?4SXAL%q%6Ytxe4REH9#3WxOi?UC+N*L-;SYOyHi$N z&Fl?)r-nrJY&VHk@3Hv(y4ez$YSSOd-N|RlSdHd#;u-j7omfyy_dQL_`X&_w77$M0vKqSGA|cc~HSv?1@fksK-oohr5W_f$O- zU5mQzxr2rM&D?J5ncDDf%iEVAe3u!VsAInv1C=LjBg^i;8^NXz<>Rw?`9_JS-7^bX z=hCiXmB8C8feP|aiPRtrk|_ty_c26{I8K*uH#w3r2vB4(b5DC2416Vo5u3CR(vQuT zumI8#y-=FPDQ;qYLelnkU*9pJri*{MB1HcEjSAeAe>A%IM5LkurhN`^a=>(&u1C-F-mKXhV>Bjkckf}AODaA+fC(_g|AhlqPMe`*7H>pxjjU~Ka!*h01_ zr3>?p(}_ps`qCst9;VhH5btq?4UKF1B#vfGZXjcNLagH$+X$l)iuE{T?sbHZ101y~(7oSbdO!sK z{2{0(r7-bvgn6o6kgrI^???8YGj;5HDUf-i$)Rs+WyCmFcaVJ<3r8t zN~i^iE(rEFpn7Kq>0)=j+~N)WFxTV%=r*mw=nJ+Z)Q0+)^iw>}1tE&18=3ga`LDqL ztNM5m=ni~yo%-zjlUE!xG5mb|f{rf)JL&QQIT9i_Tih4epM}Cth$5g&k@r|6C6}%N zLMPNEHzyBsHBvkdChd^eF%XRRgx^0qL#mMPCEgN2`=ZDv z$3y$7_h|VlG{%y{V1hPz=PEXs}O>Ff`e@$Wgx@7-P)gF}%; z5qHkSmX>AZRMcZq(h=fc(9rk(h!VGQuKZR|fO_=aq={YIcUrbqja)Hex%9|V%~z#F zbHEyA0?Y3}|Jk^P0qZ)vT*Khu#8(MhpR2#f3BSRJ&(fHUhZ6bi)zgYg*@7j7osFTLSHF{}9aNQOn`=jP5Y`Qn& z`k7^TW0iJIZDBiB$XcZ;E41v*wnM&;6HX9}Wb;gxHWl7V;?niME-uEWB7YZg_MKv)U>iP{fCZ!%3=bH|GPd4gZV9zv zH;guO+v@FBLgP)G$5-Dw%yjx6+=6*V;mUxp2B^Ervs0 zeeMc4NBN2O$-UiO7`wLJ`B_FVpSPLeLMG>1LXWxm`W|!Ypp#f1CP7%LZ49ExEMZSN zl{r&2igl(kv>p-{<2qUfEO#A4k=60$h}xWshi??@ReA=?mbu{Q2@Lwq-$*RSV18_Kx{e#v6$8qL z)TTtuIVWYwrzyP4px}{7Q9!o@jQo?YH01|&!E+{_~Ycc+7k>|^Pgbf4%dz&*8zl^nfgFOFWi-k?+1iufV{w{eTYnb7g^N>L(*EQD~u_%6Btcs9fT1) z{)fdE8`^;hBv%0*IsL+!FQm^97{7!O=_|9ub;E*@u;wRbzt0X8e4C;IqK08X-s!LS zT3bOCL-&B^Cz$$)j~v-65O;n&MM?2~FIr+q7JiF4r+VPTZyQp~DE#K3T)W)E{2_9N z1RcTHJ_95Ykj%98vk?i}?;_$)Y%ZdK%U!wCAWx5Ppcm2$0h1YIfqGda66GuV8yPk3 zdt}Qd!GPS2Wk=mx(5$&rh@NOdwLs;)eO~yl`K=kFLz${ z_&gVkK)z8L(GEXfpiSVZLS88kxb_lyd~lWub8gn$^GHOd2Yl_7@0ulq5bOI@1~fg6$nMz5=6R{v^oj}`$_cD zpami^4#H;om8h7yBl-m{JBrizjDov}$L~f!o|wns?IT^ck#1a}Pru-TXFmY8eoZ_# zw(a;skls)fA{|uwU3E*zbFbi6(TKt8AEZrN>5dLED2e!b*!cnIm&eAlm zTlSHmO&o)LXORCgtOEG6sZElN+?^%dpr!spv5+%mK?WgS@FTnkdQVXwc2Qm$5*Co{ z#nMsaU1;jj?1bS?8_LDaxS9I18MIdZZn7WCAH_6~^4Rvei0`@JNwG(WtLsOwq=*R{-h1xi693+AlTbN|kWZ$Sjvn1=m zFr%rRlpI<56J!dutf=onvQ(wZY2$fPVV1jKmeXOj6H&~WBVOEz6+X0!)HM|uk3(8O z4d94iJ+qr-xJJ< zzxqjd`K!Z>c7;rJx6zDNS9+1ja;0%y)2C99Fgf@-Vo;QYGFK9T1Z`ss&p0Q6QYm80 zg|wgA8}LXOeqeurG%j3B!+A8++SlTxE1FT&D&9Iikr*D^N?@442Q*$#oB@#)&!VFx zhEvJ4;yi=vKw_=7$#ylBIOprjUWR-s@zd~i_o(Rro0_xzpiIY?>cI@glfmE?Jo$lB z857F-R#mR^FLNRLpiB0L3r6-UMuztfV_mQyXP zCAyR3yOC!jEhBCtrs@s_U3v`T(`IG(B-EfT^IZ1}=j^M0a6Y(-sjDJUSI@o&MQ zVRmc-AI&b^;sydh1(~K5WclJuo`A=%LCur#d$RB)rrv>uf>A_(O3cg7 z?HXc2|69>s9&iRY1X5(K{{Y2r*xQrm+prL!G`@oSDMl&?9pW> z+zP@H8^zHmwwVrb8FZayZwMRqmmx4t%2>)848}|3!V|a zV!;T6es_Xs@P#t-#h5-vG|PcDyMQq>1#3tIS-k@0`i5{#NP9K? zs^|7fo+KPP_&t}w9sOFQqY^Lop*SbzA(!iGfQen%A$5)UMyojl8*gDAF8+m~y-fTxsTw9#6_@P$B;6g9=jtTIM&MP)9A^DC< zq`JbZCZ-Ye;gih}EB%ghu+BTC4jfS9-6sm72`5!AAFwu46ZF|&hk|pg5*cjwZynKj z1wRmGa|dbDf?0OSEN*^_b!cIWMu|rNRnXx<3$rmrx2`_EQW*WX`r5--750Y*ER}kS zhalyA%8>(VE*BK~YNQ@$FM4V!n1-6K8=MKF6P3((jAtS2xIhwX2N`B?n3(SZRz3Pa znz&2>({}3*Zh*aSiS%~l?&if{5Xf++@07n)&O>?A$|ip^Y0u8xJGA=aiua}=(6c1& z7tzLt-4W;u@f*-hb|N6cwvK9Xm=5}2HBVH|(2h52jm-^)+U3`kO`V%UWFPd9wVlmk z2NbaW4eo>3hfR!dkLD0mCj?iJORi@)%)^5biwpSQC!J~_loe)c+d6nxZk`TwcXVSv z5$oJB#K=4`dW7f#Jr5?j$GNE;QkvUk(yt2Rn>&@q3Rt5u=qt@+vgR2*-i>Uj$AP$x z%^}BgAr9*>ZD=XDMeM!FLSgM7dkV~b^SCCQW_Fz$en%bAP;cKb+fYz(aD&HXJ_rC!oun+v7{@R zwRuc7{Jq(2M>@I7MGGZvttn4dt%0V5JJ;}_!_n_KUZs7L6T6wRt^JwCvpy4HyaR4HFwuQUF{`kHg^derRJu8R^-yWRo zj?`|Lp-NXX{A}{#)t*u@o=FzcZaSqQaHDZ!KppHpQA;XRZ~HJ3gMW0MyFna$uu+n4 zY%zR}V6W@wRlrdvn!2#Q!vFhuZ1X0&@%MN=RrN=FZ&h)i%B-pE;amK<#{NzixCiwd7*|jU1=2JFTxUv6-n1 zG0RW`lDV3Xun*`NNOF-5mj^@UQQdd7U02)zdE+)WGd8Gbv*1o+edbp!1pUDXYC@ox z6ngpH)$DV1Wb626U_J08RS95%pEA1+rL1$^AUf`BBKhX(4Lqy!we6LagIzT%;9_3< zjqUkazZ6;0ee_r&w$o}XMyH#lLb^0fXrG+d@K?Jm4IxNiKPU!v6N34H7T*-ZhI{*_ z3?PnU7ICW$Ay#9Wa3PoFV|*N5QB4Rw%-*lOiins&pG)N6n0Y*PuylM#tz!(#8)w^& zMD%7Aa?A0=`W;FQgM%=kj$DowDd4{FI(pt(>ASbxNF60Rom|(mz~MSQqN#~}7>+bJ zG!xUa>qgid*z5^W+))moB6Z8e$Yvd&6BD*lIA=2YS0EG_k_Ao6>g{`npNrDx(cO|^ zM$@(cITN6-#e)`jpkYI{)Dt2&St6uH?J98B`Q?6qF6QZ&fjy(zh%N=1WQXAa<=cU{ z9l{?XG=RoRy&>zwZVtlE)Z0BMtBb6d@!(}~g^>NcpZ9Tt5Mni>UX5|iEgdpMLs!b_ zf%-MW<%IMGQ8?}7gx?G*W-gvMlQ8eY0@@i^yeq9_!vaAoxb*X7n)uM&38Z=$lqTg^ z%-D=gGh}}`$T1a1N&ctQIq3f8{IAmdFvv%7z4>a1Bx_mkXpvjeNM%?*HklHzD$+y6 z21M_uvz91xA+8!;Qy{u3!S>Xzsk>$kz>6+O-=kd_+vay`(qUL2E5SY5U=We`AAldM zE#R5@o~87QaAtN7{{Xg%nPOI=$s1QmEEbdb1$@h;u<$3}2(l662OY-^IWevn_ThmL zd*^%y^Btqn=L?O`5eYo}@(elI3wz~UkiFi6hNzRX8rfGPggk^9SoHJPjJzFY`p=o^_zng{eK(&O$S#4TLKNy?$lE#IjcwM~CF;S%H9I&EpWepj#= zaK#SUEf|+ySN~U419&z=e7at=n!55Up!X!VFafUAm2tZB#rflT>k-6;+2DiTtjOu; zLz4!4T)}Q}U$MYtXjk;Uth*N6Er|E4Uwcr@(P_QwzamaMPwN3!owJy3Bam-N;z3r- z*+(egXzQ5qX@a)M_1vL6BQY-r*XtF;yU|3kpiT`hZPPyDy<%>LKm1m^QhXJEg?}fn zP1G9Q*h6{(=M;)z!XxWrVMb)Ot@XJVzK49OAe6rz++(M7rb?%hw3g(Jd!}HeI19j` z=)z+KcBU2cu6d?C#JeTf_CoZ)dBZ+DPj$)?*y_W3^)nWbMMzZyg5|*Z(|cU>8=(Wn zcEI)b@D=r=vKL4WS(upwP!$vxHqHn)LJwtiZOr+@g24!)ChbW++F3TfOZ%a*eLb(({mlWbm1$N2;G7HWn zgcrW{!*1v$vEP>KW%XtIBj!7tFQlaqNe)Y#Vojcl1N2dVC0Uwq6?w)8UW<>Uk1!um zH~AJbkD*?N4BK3v_E3c>8xx}m;tk%7CEGY#P5#Q@X*uAj-Sx~mrVSZ;=-L2pAMX(E z!REG{9p#EnowzadN{Yje4(&|@wj+qAxohF;*nLV-wrORYalVI4_I}sj(#t}1;KdRk zi*Fzry&aP0LsQu=*Qskz%&D;1P66)OJZ=-{2M^=&Bk)kQ6G{&faR#~z;2ta*>fuQZ;p3?E19y6-!M8vjf~RyJu;sT$v7dqfU=`Hz2OV_5z*3sP} zNOaLS@hhBRl;FEv!m~kry~X9~NyeXgtE(}7-kBLQ*ND18n0vr&{FfW-6Jgi9-ElX!<3@s< z^_DMUZ6LHk&W}nt@Fh|gwu-3=hfA(8+2cVksI-OXmTqBtSFHD zmLYBx3H7%7x5-Ud2N$4&TgKaWbr32`pfSoGR-F#v$}KC+T@l~WO%29dWDKHl zBchUy37E+S*I1n71zu%`N$(Y+Ib}H~N__~;9rX!MF66p&ac4GjWIwIU$p4-^qPU9} z@I7G^HA;wuFvdGvkc#sKMGPT0$}^l{>Ni9|5N!+QO8#r<>d#f^*U{CcpPJtgjo(>F zh$CJhx(UIcUFr`TVzD@A9S$M)3?UCg+1r(Td*-jV1rKx|c*1-8GWY0`M9)tjC>W&} z5N6CV4V7(2SK~$Ntk{Z&c!t77LYu$f4yki>1~V-lX6cgK9~`mj!q1S@NPg?0=w}N3 z6G(hdFP!eo1?i&VM0vXDytw6id-pIZ-)bm|lxCIf;<+BRS=Z z7OhFE_RzF88JHPP7ms|ljwXlU>0#QS@wvdP7WI^{^sF+gMKZoY9j2R=3llfDeHqUR6&bo@UCG8KR6wjs&ZsqmcNthRf`^4&MRjO=(_`Fs13(EN{fg z+$;qJhG~^6<~#=vX1nf!C@KvpWE;$IIVDGDO*udsrQqBA(!v4So+WaBnZ;6DO=Q!R z7j3a=tGxh%IPowsNdkfGg{{wr+5VN367)XrjsP;9)b^q`UxTGzFWh>Ac#Lbfhy^zl zKOMP)N9@O2e)4V)lmhw>(A9j+Jm)Cfhp#^PRyyA8>FpKDR6tUfktzVvG*)3e^UIATddlJ$y>UnpF0!$!qT73CG>Wu@8S>@fcCL1dxRBX402pB8U_>E$~%KY#dktIpvtsO);P zRclilc(p^rM(%j&m0LHx^uirxo!zNdJGB;*#-TCr8oP#D{QV5>@z)=I?9G>VJn?p! z*4DfGwzfR0R%0m~zu~EIrFQku!dD-8?5($UJn}}V#(Kr}$48ggG-_Mvn7bbsU20L& zBb~{~`Q%QZ7b9vHbJpqh>q!NBAGurk6nR3);lqg09M+n&8V$e}ne}q|`ho$}-yl1qRUb0QO zlRT|tVRj~%9Y{G|?#s-|FpoZ|ff~?u+aCm*A@Tlj>8q~lTP%X+~40};2UrI`WhWFGSm1B7VFBQ$;80{?B zi4L!=#m8!Kt^BwOt0-GJw1!eugYFv;BO(;p24oeh%dA7XW{K&l%bJIfr~|d@JzRS@ z1a15B3QD(xa43NrMP-EuwTCNk#fzXXeW-v$(0KG7E&+~k@TZ)E%cArj1iO=`5;e-7 zi1yNm>X&s5I==e@Uuz#+#t(I!;7<FO6+In#T8J0{knYA#LtWCI7Jc>(UQS@9raXlTfe*4G)h%A&&h>b%;qHRck#J2Wm@w9;v+ z&W*Nc6(+CWs?av(<<{h{F34N#cGOs_M%U!muQ1q|HB{k)xW!2@EG*@Ql)Hen4C?a8`tF#8XdyUC$_<6tw_<5q?p9IS# zdV0$^Mx$cKSzwDqH7@b$vA)6n)7vN!b)6g<1oAmKhz*j^rUWFhv2gCnVDx4c+F3f+wzJZo?=ic|=+Gc0&x%KohNM2()Q4XLd9;$-NUeggsT0gzZUMDa@L18IbjiH3kTTaM zx4SGpOURz9lq>Kmg(rbkaU+)vS-(tcv?Umo7(!w&T#CeK0VE~mkkpJU6~PkPK{1tY z)Cauag;7Y|W-Zo=CQGT-k@wwS=)zA$mw}zo)FIjeVlcZjKnDrXK>~D;r2*VaC3q(Q zN`D>`21c!k?D23j>v*SNFXw` zMx{||6dG1$v6`(!=CC{ME;}P5;Ow%Pkwv1pQ6kGS1ZJbK91^$5HXG+~koa|;*;vqI z&qj7Dl%g0)lBS_aBonR&=Ig~~xsFKOMeUr5@-#1TJgrkQ1X*jdrZo(+w<@sw&WmS1 zG&0X0tt&aYV&&UKP06=dRoGD(b%dN|ePuzhBO)U^-;7uVjo7|BYMl!?V}6$W4AL)kMWBbV~6J;?!a4+#N}PRpp|;tPJ)<4_4R}F zv{N)GM60%5A=-41IP{Qct0ejZBs9&)&?`VlbP7OH?&769?l#m*uqOELE<=^E z3ab3;RA`|tzrPF_%St6&Nd!c#Oc@w7Ak9md_S3(|OCP-9((M<8%0J$6*)!c^=OurF zLzg#10zoU@iwm|a+_YUgkm!DL%*`7PB=;JlEfkOb)Mv~_z+)605xsJ~O}`+zI(oC^ zX6yZ?d#q0zcUljc)cNjuH!&-40)z2@5dGo}`PB_d+>I2-Pu@ad@=N4I3Rn*4I|;8w z3*_XN`$Qe*)FN|&nD+QE=2QpqJxGn6`?At#24S$@@Gi>bb4e~03EVyhWvqyuOV?9Xo4^(CAM$-nRduORl^+`F1k-lZjQ0QNM?O>7ucV z2FO!_zv{}`DXZ?*KKay7mXF(1R`uj{? zn`p1s;u4G*#1gqu#VNEJ#45B}wK{=kh&;k(*a9_pBMP+^v*-{$4Ss-%@S;Y6IfX`x z6ug2c4lzm?SdP0zdm&rT53G37&iX!CDr6uSIYRq-Dovf8Ot{!ZsQ?(gy#aOkj zAj*Qw*X#9Z+?a5Qp~ND9d+4w58%oGcYp%H_IheG}!W(glS$KNuk>pWabcEP|LRvs0 z^mF4;0Jn<;IvG}~9I6~JideJC;)|2wu-M5mOnQ|8SY-gD zKSlvwQUJ<5Mg%%Nfxp>j^f50o1ONm57_rU--Y%;2Ca;N@KGtYSg1pv`-%+4LM1}&$ zgTE72snf^4o-p=IRQj62x|BwFy>&_Ab7)+`9w1#0*f&s z@Eb!pl%c#Taa^)jVonyQ79iLRQ?eN*m2v>3dEBg#Zi#jdcF~)}tNxWFN^_j1ax<$3Z3 z*c104gp_#u-f);loEH@R9$p*x7(Dz>`;jx(o*=L9O}qY-an)wJ5oSlu@J0ZiN;bzS zYR`c9FC63tPe`;#DN<6#1+I|F6b!pv9v6`rL%MKC6_|~)49?ler-zcD4$sCelXo`q zYav0yiZvREUVlC60$l=*ijK(7*s|1K0+u%hF?ejoG!l8LUzx*uKUyo~c^Vq;dvnPf z%ddTJ)kpZQ4?d%PCYVs{Gi!Fs=y&3x zP03jFkr4rxCS*sN(h{1nUiv`{wG0a&+bz>drq>z5cZr2jRF6tE%oI&~XpUyN=0?PY zG+J%3Js3n9y*)G{fGn<%9mJ%}Sd4=~YHnDx#byA(K_*bF9ii6-aWN}9=P_}a)|Jua zt^|WbkELkD$0F0t>|hwiS(-LGKh_sTM5VSgLLqdUl~_p$R_Q3A1WV1&!6?-$AUjSD zMWtD%C!Id(S*I(AuG3w6hKHp_W0hRT*RIp?FTYIP($0+UY&~naSA@?x7xH1B18CFA z$fQ$+jQ$}9H!uDBd#``I_U=b6`Qr7%@ASSD3RXtOHeI?f->WtIiaHC&%q7W%d#`-_ zvsX4>@p$vq4_vnX$o{_WyA;LOjJ=_yY}V*84<=u8+k{)jT)MV$ao1ts`g+*i{gNG@ zg9b#Gig7X#hPhAqlro`wO~W=TIGeyJY~E~qNaAz_-rk>`jVM}0jh;hV+dIg?JBWjF zx6$N`2p_B8!4zlMk$Z;8(|$$92ud^g`8;0i3@eTPkTz@xxtwmN$H~Y-VV_Qzjk0*h zY#dfXDWLJr#!e$7K~?rSd>(;F869`oK$TZyJW@%Q8!cwS;>=kLwVy3KW%v4>U#xT0 zO)J{|_R?Q2UG?5|ZzmVwTvfzg=*TH{XE#R2WV_wryFa|$=lJC3w;a2AeKPsPZ<1FH zlJz|o?R#)qt~FY-Gx=v=bLn*S3w)Y64Bnaz6(2wj>em}hrLr+dt{G!g>zOgiycaFl z;;^4NC_Fhde1-y^z&ZY#>0Dp#V#$GO-ParGFPF~@?ggfeybI2mDBaQYN7P@>U2=i#e$2Ont zXW9iZ!7s8)nDx@x^;l8G$`L11VmW9|CbSQM!o}%RV>8SKJC*cD zD5A*$28<#-9ctfw9_^*x^Ql3cW-uTkliwm;Ic)0>2We#1-|&kY&co21Vz83y07?8` z;72Y)=i`StiyxKFSSqvGoN`R0ZxxirQev|W&#Pr#^C>_QWGDaUxH18z$}rAln}2wY zX*=D|Zb~*yPLAA!yYVG}t7}t#U~U87J%9?(O`=imvbo4sEmr=_iFcT>S%wcX4kX~_ zeYt9S*q)cb&7u{B`7oi0FWeVq!?z$AFC#MArS*CWJ@uaB9@g`ZB0~ZTdy$O?N9Q?Q zjS4e|N0M1YPts0y^$r22qWYoF>$@asrN*)l9=4XH)6O#ln-N|znfzQT{ydjB7Wx;A zDi66$(Wc_c@o()v{N{pt=GGgVF1@q~B0jiu$PB> zncaBVX#k;T^_NQjaU9}eAgBB&h>OI#0=(K-Kc8R8uM*brw+PQ@_w(|5wf$Oxg91ST zLGY_|YLCk1w|i`A5aLAPQCbZaj}>GG3Rss5dfq1>Kkp}gf%x+cyxCyj1tR!Kj!tjZ z>GWi!4(n8F4A^hr^{iF!8+0H^Y=S-z%mKc`_yjNVdInqrl}f45Te0;Zz5%wPKooo` zM^U&3aK9sbG<-ZP3(=hr!|l+rB^(ROH(vqd(#v<9l*pqCJQImr>T8|U3myV%{EQ;( z0`E>zDhf(CIv^Y%E6U*G#=x+h;+KZ^WMw*JnPHAsjPnlfJ#Rz z!XbAsn62}9l!1WW=P?I_qEHqSLMO(n znndLVh1hQo(nNqZA)kvBf+FK39BqLHxxpYeTT3!EX7kS{^jy6)d3r7caSE+uYS5p>he+v!p5zn({=~>}^JI#q{aRYw^$n z$+qJ1ic#yP%-mW_HPEEgr=WBe17CU$AZx0xU#Ti}C(^krlWQRtJE5R)DqZEKHs>wY zP0r1(4Q|C^!(!uV!)oIO!;`Y7v`^SxwY}+5$v}oRSsUDa)|+j&xNdUqXAfnm3d0M$ zt7I#+D_ysk4(jCf~*ZE9+XR#km<5*TUlxG>q1EBTL|ynN@%tq1lcf4!||!raG&*YlWdMo?(?8PK;iS~0)o3huOopnmkTM3`EI?( z>mi;xMUfo0gEw+*-pIXai>8;dP;DDUE-xvca2qo7<{~p=E(eqXlJ|w%ia=k{+F)mf zr3RzXG-5BMh#f0Ld8g52FcDc!c5Zegn~|NZ5t-Fuv)C;T3o8qTqI@`nBQz15kYtJl6G-L^c&S9tYxvIoXv|2*X5i2sxLb=684Mgw^WddU`XWrZyq_b96eoehzX>F>? zCo}(c_ufM@?tF3M$Q!5gCRfRm9al|mm_Irc@>v!#*DNT_4mD0nCXQ_W%Y!qW8aDOA zCsV^J{j%-28FLS;$@5AU2x7m7{TP8Kh=W$vp(MVNqLQAHEhSIc-Z#H*``q?-n{u^k zh2@%p4a}WpZiDJR=04S3mZzAfR5G8r#Uhrpm#k(u6{Av-5_)WKH~WC{arS4*on}sh zk$jRy^QOWh_xU{bKp;A4#E4JxJW<&sjNjxuGQZE08xXJzY2;dD;Vr~sjhZc1#wNE} z`;7(m5jnZIK%=qe61zes*UKl$Nj)T+<-6oZcm8v`@ zpdPE{WSmR|%)o#VrV6XWnu$!SYNC3+YNhHHm2OS$mV&+PUezn?E2_`g&sC?mQ!15C zQg0rg#}WvHC*|cONRGHD+Y{C+u!3%~(xX7YdXorwQ|6Ip`8>gZppeVML^DxKCgSjm zAslkX3UC3!TD{JzBf2_|9(h3~)MaIP9QkHTUQUqY;2iL!gJzwliZ+B$PAEt$iu{7- zFaZN=B$m_H>hy4d?oaK>TB`MG2c-&@c9aJf7Xu(o`zUPGyXAdX z3{Cs_)yY|u9fx;`mXlX+xXGo5J(`j_E(>y(qY`|%XjSoCkO}E>ukx<<-W0qkbX)HF zh)T%lD$T#>t_am#O;A`MU!Y#4UKKpRyuc=8`-A(#`y;Amp*2^GtjoP6!fgw0jXWuP zO#YPm7oj(E@xy3$*F;swKi;c&0`A)CFiKG0GeL(bTrG0d9T)H zw+FbgD5EV4D3O6T5JMf#a+cBil^VXZ%$VaSEqe}6fxTLSkEcI@pw^3C=~WV?WW6XQ zg%&+k+m?AHPB#bqBn%Pp!@7;WW=X$lK~xa8P(fVmi^%k9Kz}GZ2trsM(g?~B()syD z?DOh*Sp;gTY;6eXecDE($d&XRh+AsHo;h>Fy^_GC+bx8Hp!CSF&Kurg3w?KNG0^i& zy47Vo@@LN3E4Z6OP05oF-}lDU8E@Y<;<9pU%LswoHLiwN-jMua>(5g!RkUJIdGjXa z{le%jGJ`@4y!_U)$zMM5Qu4!%7BhCX7luP2&Koq1O@3Zov+&u)8=u9+cqgwIms>?G z7O+ocX5iN*TrV1%0^kRLrRY%v0(N>DYo~bDRk-Vsowx6>GgPW3$Zz)tihZ6)AW$uN z$^cg_0*t=eTfM8AY4mxj0cKx7PDhZR89~m=cgPv+^T_E4g26|(J$HD7Txo<{Z|<&K zMgS=dFyf^`iO*9N5CZ<3CPZ1co;u2r++4feMyji-6biXQK#hDOX{;;Omtgoe6Xfo- zsHMAwh%N0cJ6d8bY>O}b7Ou{KcntsAdCXtiJaT15UoXoj?5^HZ!xc2_gwWJj1rmCw zR-%n)COxMp31XhgV*H;mu!TB^bc6l>q2*uiAVvSm+2j|rtXJb>*5;}_@^W6S05w`3 zs!f;4t;rex%IfKIvJdy2t)0HAx=?18L9uf#ReI8>V;PQFE zDp&hFhJau+5P}s>z02z&t~#~!$%egEs6Vd4MJiF%qZ&}LGa(@=htE$tT`o`QaX;?y z5BN!uU-ZxP_xWQ`kx6(5L`NmuM~CrUPx*z)MKD|ba|BX!k*|LYJ%F~*k6=JrGpy8_ z`jj~dD~+IOqAI<}Ff1=Y-V^PD#iA#e_{;RlFbeS@5~@=@tj6jDp53440eZ*t`;8N$ zn-56hg+Db|Rmg*!m*x$o8we9=;^$6NcJ4ty=3A(#Xvy+?jcmz37DvUXTg4W4xX zjZ6*n2XabBQrthl8%h;erBxTIYsTO)+9leRXdT*)w`+HdOyK7=3GM!x*vQk!xC69X zfvunh*J-B|POVvlFDpdOLpcW{JVK$%4&w{nzCmkUoW;dj~kkL4h28f z0tBqVe_t52P9ugDIz@3<^E9dEs;By3eP zAmVdZ0!@U$+tC6|gwSTOd^TQ%uNHc;*)77B;Lad(mbu3V(rgXdh;a!)M9ID<)EC+j z;z9|0K;(V?90EL#6+{vG6?zaDWtV6@%l?jVQMQ=9Bb%Ye1{1n6cJ0ZNLtrP-w=lIQ zyK3ocREqy-=HP|-uh`lqnX91gv?V``KL7n0bdW~L_*USg`5T`flO?op5qL8v&cCK(NsP=>d87z9MDGL<(JY@l} zo7Rg>$m8|_<>$CvX1Cjg%F7YT_hgwpo-BmRJ#NN}ov6GFWKKBbaT^T^geodsyc0X? zR7cdu)I{y9la%XkWqc0y0o=w15;iDk>9rYQG<( zYXaxpEPV$m30L5X=&~p^OJReg6C1n4BWW(loZ2YX6+}5;3k78mS{VLeDYa9(mQlAI z^#Tw4=XwhZp#5wap0JjmyUWZ`+(UBG@@`md(eltg>_gvj+G$h-*Apr8~h>4zID1~uC2H}6vE!pLiHug^vg=JLsSxaQlD~LVbA@zH*Pe7 z{`p5-TSYT*m8Ob!yY;-=_6APCmiU@bhjP)_Xj4=~t2- zR{$CGI*dIwKUf-aL~K@tUWw_cs20!EV(qoS09*rv#QV*rWtnvA{nWdx1(VW||2uH$R3eR&=KGN!vgKiop8m>4ycAw93J zK+AkR{ytxy{|>Z8zs0x3zX$E{Ygr%bkFeS5fGOgX@rl$WaZ?#&JHfu9KYHiQnCG|P z9qt$(b1M*)Y9QL^FD>`-3bU)8r@xC<8twH+p)=K^MC#j2O>eH(CsJSZ0}SLJ#&x#( z^Z}jplR``#OF3|}j#vzo4APWAl^y*ovJ5B5gF;a+9=N#1A2_{uaf>hM?dkAD8|%37 zr}vXlYogU86jBQlyMNfqEjzle&BxJ^Mz3kRPO?+;D)*}L70>(leXW+Ta)nGOQ`xYM*fch+)uWv6 znD4%dTcug)Sn1iS-)G1o&C@HctXJ()z^lUkg`hdg0(SQ+BN4yWBo zWO}VJ1bA|VutfnSn+!?@jV^?>ZUQMbsjMO7WFd)0YvDTc(l8pm9s`%tlh!>7nX!sX zq)v)zV->L*t27WvBK2k5Pz4U*SCDbErma#!ZMU}S%opRGXAW+Gv~UpoH97J7vcm=* zaRIsXy&jbq#xkCS85?fK_iTMFc~|nz*B-+6x>$zGc=Ie#4rZZn}}oPp+rO)@Ki}k3MkIa{)WKZ)hL6eyeetWvBfa z-{avo^lzB7R?0)ZzH<%^vNAcJ$?cTq>Y)&p*o`I#DU~XfM$KttyvnGuHt=JOx9HcK zG^;eL_^Yg|0xLtCR9o$@;8#@2NxBzQ7(~+tqFrw&Rp&rLe?<0`nn7y8Wh*l<)Zgq6 z<(0}4xO@*YLRQ8l@I29=_WlYfWYdf~M|RHT{v{p5=LDh_3G`i8o7p*#gL9-;nOb9M zPTJIw*fQF6in8VYb71yT5Eo@k+Bw)s56Wv#(L;p#^jlaEu3c$H9ZwmxTod*KqdG#N zu+J)laF)gu0*@M!uo0>Oi#G(wcWWWjsGWLS2nS7&00Ho|^xxU{A-Jn|xQx=@Q}J5f zP-TIr2QD>1L}^a!;-qh5E%aDDTlzVI-$zq?4kOa@UFO|_!*}N7Z(VgT`O(-@$#?O6 zxEfdCdy|KgOZLp3eD%d!@4M*gwr8-RjyTm^}<`OH-)2xifz{Sod2}{;P}DG6)H3d^6Nbk*GdJ-xKI$QDD#4O zC5hC0Az&7Sz*=DwY$l4h{jR>iiNMJK!w1>}M+3~!01nt9?tl;u6}S@k_o7Vz_bHfP zU;?)H{mSnT1i(~Rfb)tuQK>-@K0+d&*b)p8t(ss6%#4f_&}iDJ<}Vm|K=MAQCrD5K zq~BZ&(UZX8(*x;E*-))?S~ygjc}3Q{tgDK?(yZ#DYG3k2?2?@YMB>ZNGdnFIM>r>x zXO0x&Y$qg9YknclwTBCl({=XESXw@$KakAHN=zU=kRC&0Gg4&bsS{f+Z@C7yimu%97^4Lq99IdGY;sQ_E~o>Nx8M$x?;$#VUKzwA@vGMyA6`y*w%_ zvg>Wpe0`M9H53MX(O_PAq&#|A?uOhAk*7-&k%OhCsxxPTV?+y@t}pkNlk%rWfE_*E z=kfZy*qZ?AYR#ICoV=4bpSI*i^@^}wt=GHNZauqFzcTlJ{S)fF>X-GhXs%w(3S8L; zMkuo=C*tA1dB)*s>DM#~%!@jsvj+ITv_`M+g6jiKd%PnG9MuV2wL7E5PYhC#a7y}t zUi$Tzt*Wb6VtRU*NI$gAyegw*shIip%g3l0JqhK8ql?rF^=s6t^|$2S9KA>X6ZIkW z8|pXoT3~4Dd#GOUmQ3mQW9c7J*`)WItn?{7eQ72bN`}9}4QFS5qmnr*T>%EP2h3j0-EG@SIi07oJYKmo z2l)uESD+sHnG24H0Y&=Qw$8!ZvCrOa-(qL%KHiIc-gfU=?-noXJ%}U7PJY%e$#DJ@ zec{GKr0!?tAZe&}_+82H%Mx&7Bqu&|lKvl0+9kI}FmRfnZ$fi;<~&y)t8>Ph$Fr4%DYspH(^fRO^sh>zIyjsUT%vI7y}YT-8B z?ZPhMZ6PHnr9Ujh@R%n&(*sV?TI*Wd0o`jkALM+QqvHgNjt}_!VPS-SdO&{I|D8Z~ z>h|eKi9${f;sRdjg;vB@;6Z_4`ta0lvtbxhvnZ$pFT}Uji)VUMUgEv>|6%VX$!GW-aBshsfSYlQK2_X5es&0+Nl92EH|NqWA?^u0Qb-Sm!y6V=w zx9+XFRh7#rxsF_KZd(rL1iFlk(>JG59i_1J?Ci+Er@GF7=__kg>*SCsxY8p6DH1L* zSdEE9+!8m%g)B&xn4=QWR>Fp)m<84d2`GwL52XpEHGQOm>T^&-k=M*bP52}es>OeF zfQ(oNOe*Pf+)tx38hy3rELeT>v_v3tJbbyheyYvfa`>fz1?R8yD|7JFc(|c|XzA|t z(-&=jjZ2+0o%E6M^77Ekp`j0cw==6edoOpxy7~xYTR0JWVJEW&1A9P}P-TNZmfs(R zRl4h4oD_w19$4>L=DF81=+U`79+%hWqg#OlfR6}vmnC2p1yeu_`-nlP3?8{$sqyIf zkU)p8IlVsPAuQ=}VV*DX_z2{433^^9VjofITmr`Wuvr8|k2FLHKRl9|l#hBmey|so z0}nh;v4=#3JX;__(HDuX3~yO=)Lc*deba`9yljnTnh~-(ibcICJ@s@Di}n_nn&=kh z_u~JW%gfgsUoMg?P4r)tJdb!of-O(%7Gm>J0Ye&z*z{?2!6LET6KteP^bT{a?h8Cbu~|pH^k#^YrtNy zVyKgwyK1sOST5R_b#6cMXFVNduHblO*A~M+Q`{u)GT*)4LGQD^cYN=p^z~k-FY|yn zr~xx7PpRyvT0v~iN~wy@^!8S5s=BIbTh&07;pgy$%D;k7z>!M4*09#sUvh!reBU;3 zw`%}A2@GCeiV$eFsvb-VwN>GQ} zm59J3nK+Xv$pTC!%O;|Binku2& z{foq=G*yITx?t2~6!eBFN;0iR_!tqeDXs}9MBud$SUR$Aw`9vp)MtlVvTHQ;Fyirh zhx|T`f(iqnBSh3ksmT5fLK414QKM)p4Vc1G{~u^<^v$Q87tujeI$+TRJJTB3m4*!F ze5gbtJEE_2u2}>6*3gx*8s!y;j}4JS2kFe%beKA1?r^A-Sgn6Qk$3X-!zUz%2ZpbV z504#Rqi|CvW-Fla<+}39))wyS2|<@P^Z6g6Qr!$fl_(Y$w|sd!TK?5m-ORh&_;{R4 z1`=lut>(C^*3Tf?3pDD(F3DTrUN^OBxE5EQ$GPg^Uf|KQo z;J?Q3soGTCWVi$_EsX*0Os;k8pcJkB9e`@6GLP`LNaIl$=3d4q2fgY0b*8 zNGE6cTozUY<&dQ!Xbo9fB#?zI5^vE5cwf*T@LiRJ$!tj=S>MtSsK-zjz`zT=k|*Fz zgowFNS5X_N%gI)t!z@hrS|q6?Sm6u?D`56=3(Pi$EY1+gxFQ=0N){;K0+jHz5-P%{ z)c=g5Tiu-p5+s(nv|{!7l=`}4g7bNKEGVsm@MVpXN-v=E+#@4{RM$CtciKpsruvLn&$qzxK?kEw1S#+A8zgmnEOI#lnITi=zdG>|S-OX)c6r z&1H`bts_-fsblh_6Niv3!d1H%Dxw{=Og$QzUDcz7aDC^DnxPdl%Jr7IXI|*cWnHa9 zpUakV`KZSPy8@v-<4hoKCL}5#h zOqLe*0dd$0+Lnja{S|mkW^BfzGgP!^hA-uRURqS${l%vXEU`$8lyThfrSN4|#@UMV z>!q@j4#*&|nFmrbwUeVIv(Sz3srgGSCbeL8@Bq9*HGiq@J-Sr3!V1+Sn*e|>53eqi z2T%Q1bHZQKg~bJyXtad%#3vv51pNs;2`WGnH(7DnG?ZJ0^Q3RiEftEL;a}uKqFH4R;G- zl^$+tzEO8m=9UT_XzplUL*o?Rv}&0n5t=2XIEWObz9W~RgYEurs|JXhpJ3;=Z{9G z5Ei3w!6Xz3y#jLD1>`j+Ha<1Z8kgf- z{9ORDh8Dv{!{Y{Bh(sJPNR*o3ql%<7Z))b0<^|1M^F1!;qKkc$l8%y=BSo+%4Qi!Y zuC@{@(RjWGe~7spR&WPifOXh6rfxQ2?TQOjqDxOZwC3=d^b?)L$U5BF*+&OEKBJaz z3*DWD{7#vfIztT|J}9ZSa3Aw(8`<OtT4UNgLFH?Ex^~+|g_{o*?m)E8OcCSlLmsCucTGf7j z+nTYZ*AHK(gr&H5^0+D2!TO2E)zp?pi|CkZ`p7|D5#d7*NW$Y3tG!KN%MYN}XN$?6 zLHNE>6isYK`e4Fjvh-Q4lF0*rL>F)B{dVd#vvW!{bDpVd9Fy-z?N4#Jl#=R5^`^F^ z22%S{`jmz6txxuGzEYcAkzh_zqz-AHv|qxKuVnffm3i3^5>YI7`NDJph?w#r7Vq@P zbiv$QYFO82rOv)|nk7xCNmE+V)TpGNjfnGubXX*fED}*8I<*x|ah(~*V+p#RiQ^4K zy@~50Nf;1)L14DXMgm9qL=yVV!5|0=!Q|K4n0XJ6J5hKUdSO`|)v(;vz0Sbm#DbjnT$Q z4fh{-wWEPb{^>^!qEmxm9S(>*zdF<0Yj~-Zj=4)5~WI|ozGz0JP$hk;<)}-1g6(@e= zJ!=E>KlAw2JFb~mKYn^wi_cdz?Y7MyHKxlcugoC4GJ)_)kmyinDA60t_n7yXA93sK z_F4f1r6A{#GX}wXha7x5swyLde;$H&@G=2TdPGQX7Ku$ycLP^^o(IN%yFbhIX18UztW2~ch4PEyqOXo{ z*?vhU`O53g^%jIE#VO&T^k?ebA$6?k&|$WmLSp+obgC>WN8=oCPpF>X z7NT_|lAzQ~g19YA%m-^asZAOYE-7VPlI=D3I(j3$r32Z0S-#h@(Z1fZG1{9tGjpNm z(##F!>)kh(-R1sC+1@hCChH|Oj<#awT*lp5mAkVVcV{*3hHOKSPL+8)E?lh+-;4-S zwWqIksGD+VOJT>~Su2b$d5B^>O3f+%w#Bw%>BUMX#A(bA5vTX*OZ;gi`8+L8YW|9*cu^HBRL6jA}$ePWvS& z`NA3pvbleC5l-i4=`fy570t_S0p1)!dt-bcZ1T&{({JaA;aNS;<1) zA;&l?5A-rG>2;-PqT8lyl35h(MBt|-s~D5@Pe))(<_XUh$we(iXa`YEDii0qV8(KO z8A(F=V`G*s-zMgC_RTEH5{nixkJOy6DDMrXA_|EkSEaJQf0LLsuKyXt|}LN(;1pG^mTRZwW)`7dki~MyURa}eLCJ~tTI&d^?YM! zIu;1qrVJ^$PHvYk7cMBh*>HE}?r|oiJvKIMPI)DO8uT$|bIP0*na1uX#NMpf>zkEC zGT*EOWfHi&`CKz>X1kqu^UZ@ur?{NzHVA>*>qRk;<&cuC$fH42tcZkyE3VJ#CnN&a zcE)Pc z$I4^5vF%}rK9tx)397Dzg9t0mL_SBd;_}d%m-Ad+2`AEJ^m{USTBekw@?}(EwwCpj zZ7D+?W&6vxveTv!b(v{Wgl`Wv(e{=Obuzy~77lf;`HIlUA@(?}E|NdoG?Zq`2o6yl zTGNT0iX6_T=N?Kk37`$3?E%enHk+W2O5%W=CvsR@USC*dPnU_*YmSN_O?|Z3Y!Tyd zwr;+vw$h`z3{dxSw!(hYGJCz6rjk(_QoC&JF2UUj8+TPWygiRk=KC^ZYMUQ;amJdZ zv(LTfoc;4A%s+4W+6zxVFwi-{SI5?oytxPPM?TzXs%MUG*(ud|+{SSo)bCA?G4JLRyv}L{5l|+ZrHE9<+jm$V zw{S%eIAXG8kb6V1MUpW&5{(#QvN;+pj>+Lc?)S=~Xh}>ii$-A)c@=mU>-3Ru*kUmo z1=#=%rA~(uZf$WW6DH;z%9v`0GLHP$H;|iL1qqTRBuHmS5Q&k1P`5*oZ21=)(CUC8 z#|sWlqI=^JQ_Q}v9LnVb?9s*ini^%5ajo3=3%^#fIh^R(@)c)e|8p|1LjDDY2C+ce?Cvr{Ew zh7*-OyFFc0GPTU=XdF&7`fSN&qObq;xN%(=3aP5R)Q2yeGH>4V<>Q9+%|U1RSiF)wP?VE~ z8;6P()#_p~F5+3lMF>dncH|esZE%S6_9jpP{zwhT50#+Xn+H{Jac*%%f3CmcQs;TO z^C|{%0~PzK4pf=gfd-<*mIoD51y>f6E2xQUiF)6STyM8lh&)9ioiZbsqsVj(BBVH} zZ%3#oSELkm6fG#~E!tFMC>liiU5Rv>;o0+Dcz(Am%{*i+Ql1dk*L4Qriiz&A0c|AuS7=Gm{V8@JUmbap9o=e4xV zoBO=YcikCht{am|ltE6Ce4a3Ggb1c5;1zRQD0W-s4mW*OcX8|dzOIR7t(lA{xrC_A zRuXrP>G2%m#^zhH`Z$q=_}5zpB-u{Qr3bPD3@YVc`k~rRw)oD z4hAg{awL?E7L#mr7kgxPDkjH>+~r8FF3Bmoc zV_2n@N7a2KP@uBkh`v37QN+@FOl(68V0m&s$T=5a7rTa|z|ssKUs?EOESu+ZB!}YfuW!zNg%>aP$*65d5Y{f4^x~Fdm7`2RFB-!gkS)2ZEB%TZ z91}R14I@iS)fRlyi)RrtC^H$(O2w70K#4%k#KAGpb|bEA5nRXAl+Y<@;cP8!8zp+3 zAT6Vg>e5v_)Z4snNWKDB9AjrmC&5Y7=Ne+3BYm7j6um9*yZ$EN#^<=%Rq!0UOr_p& zPO&47hK9E^W_j0Z`8E*sLpwe1*sUzPqGtAcK9dExw0>qrq=|YHp1Gfw<~S#sE3r%@syeXh9;oV39hu!izCOff`#N`FE4gKaif)ckeI}^A z7vcuI9MNJIMf~^_(vcya1SdN1*;Fcmc)F|or!8!r059?GdsWx zej1rs`uCy(5nE8sd*7~KUtsEs<%k~4OSI--aP!?HzF}vd5XdUyV`GYhsmGFF8)j(u z$QruZv3|W(J$94uitD+07EpTn!c&*WnY^jkFWatnotK;k3axm-hTvm6emuAwgLl(j zs^3E}v*Pv2M>ANUHfUOEA$0!)PsYuRX^^7e;0#a3Y}~xrTXcE{Pqth* zZ2X0o-71qlkmS?J^U0NMqyD)=l;$mrBZYJ-O z(Jh}*1fM~pyg%DL4kb#sCz&cfMxYuU6JCzX=H5G& z@^jcAs@fjh)A!u0?Ra(++3Oi>A4pZwziW?yz_IDtJ~Jkg5FfdvJ5uw$DF?qq9T0;; zAPHJch`|{CC<=G6rY61Uo@W~ML1c2oG;Yi);0bWOv0TC$Od$c6VOn72{tS3B(>A*xcSt$YxG!Ph8M znqb*Xf_A15OLHo6lG%9iN4!v}k0{=S75Q_Xa2nfwR0(E)Fi>|_hSGpcJW$b(3PCDz zDe%&}#gybU(0V4vGqPW-wFik1-}P%tP;4|AW#UOfTQxjiH&M3s(u9|^RUnR< zvP^o-_9~{Fr~ZAd!{<6D*@}qWUzn0^zdm%k)$g^Pv*X;*@!QFlosZne-?Vnvv|qF_ zlRtBff62Lyi`xU~3VqP$%<~@%hJ@^C*C=K`z5#U|Lg@Y^_izzFtFoHTi!fiu`}y0M zA{p)4B*TTOvfuDHi6UdwvThIvFD~4~SoafYn*9dPHFp22tDh`oNTw)a(+?I|Tff)> z-tXG74DYF6F1-O$YA}s@{yzq;QrCwqnK=&ZWOe{H;V3&0pwpi{ro0`z*Ladf%~Dom zgptAQ?JQmWTg8bK;psc1wd&KUof1NH2tvu~XvJ41YdqH)i&0)OZRe|3{I+_r)Y8_= zsr&RiHIIoG1nV(($roN{u@?f5?)OeR*z&1kgI^0%r^T7rJUOYy`k>-uH)m91Xb49< zxdq@_bf378yh8tiexziDOKZ~X zYpK(wye%Y*Rn8aGO>|<&7hAJ3g5uagZlgT7Qj%#s)KrL_Wh~24#4p*;wK8V5IZmOQ zvikX%@<2(kwN|$DL)(C}&Q=RZY41j=-J7TDdB+Z(caXf2Uo~AGMYq_7l8T z%7^>x$G*o{?wkgs_vr}8O+ODg(L!3w1=0OVjg6n>v=~H1gd-{=U9Xgd(GVQYA^U6Pqm_-l@Fh{Z%k!vOQ}rPNcw-iz`mNalh@O ziFwE~V3>#$B_YbPsxd&zbE}_T@%B=ns7ESZ+nPK;Ss1d}QH`mk4!KES`29nkKET`t z0fqQ}m|^m39|cUzfuLS;e9;mVK%w@Sf*vI?m|a}aU{4&QIY!pZ(|8AIX>l!GCAvPj z9Q;x={|Zc|Em^9n(j;R)bD??g?Cq(dk)lezeIK*%HH91F2Uy4+W?_ODRN{McoCIwO z-aGhXzo-axE=#3R7^|7%F0O;O17C`mn+0)18!00Z%S>l5DJ?}o>07?~G0_|yth4KH zyM*_yX!oLjA*cU1sd_iKyqHWdU3=9xvheF$Drfb8shig%X1APnQ!Pk!Tk2`^JI8c` z>!XJ9qw zlgZ&+u=Z`y0_Noz=Y58BdQhT|;~ZeC#p$r9Bbq(Wi#NTZ;G$ee_SbM4;jew)GUJH9 zB?KlDHnUxyg%eM`FEm#sVndj+$e`J+o}9M<;K`6;yoXB60nvyt~X?EN!LHo;HC8--rnf;q!zbvLHb<|qqQ9qF1w z^VS;Wr?hRt>1Zx|THbuslPCcW5Zc!F>Q!7Ery+*@k9CRyHBrXalh#~Orb`+#BdLUr zXqcx$NF$oyO^UVRdvUUK?ePuTpfkSW@o-(nG~rLh5OOx_p;uF;F7l_uS0{llgjZOi z?d~p!^0ODUs%A^_lx35b;D4Qrlele+;F%>;Wo-P6RaT&rR}`cFo`*|>!`)&u_2cry z-|FiDtFx_|(8NX#1gg~*1d3r;o1A6`uC3~$D~kOD%8`5u(%ALLwJS6UIw~~Z+rtCH z^{m}P(hB!{^|6LVv&OwMaXWP5wlOFk5#a+REf~WzHz|gnJnq{VCQVrH!XhK1mdX>S zEkBdKxvap3)QF@<+jsvUOc5+xMCsDLQAie7C;nZNl!1zPJ&6UBgR8xIoSi@NnWhjc+C? zC%&Cw*0fnaMAswg=z+GmmOU1{zN}Ak19fG672Pt;vPadO9Ay3GW9!J#t+-tnz0~5d zLE=cm67`$d;!uKIaIT!cG`906`P|aW>B)zK_Y4n_bChoj@Ep8|@kB$<+BQml31z>1 zq_Mh8UU4>cDJE7;o}ESJ(;8{>_MNc{@3%S8_dY*626*YlWNvRdw3}Ya2Rk+$4+yj- z)jHw1{Sb214>EC1ds?{?=Z1EsY8WJkK#<_{nM-pq7 z(zQMU($`hmSDrb;;0*sNVm>l!8NVn;wuR5iURLxKdSHFFIZNJI`R*4e7n76DTjQ;f zydKjU(XdZpJq`2uBK87i2Ns^AWo6g?O@VP@OpkeDTQh}pvD9`UCrKfuaZo=x8KqOB zi5%Ix6wIOZgzFaTa3o}6jWW6<0Sy`*)j6uL%R0KJCfgSRPoP?(7_xcyaTO29}1xN(h(?2WhS|21>U zI}LmnN2^5kIP}Ni%(wZy@f6ybfbf}!B?XjW9%4U79dY8|w3Q)9t;(1h9zI3mU%RBEnQXhx3zs2 zHsK0K^C_7{9ZAk&*twHUZ)f>(;NKVXLdOy25)(RSpwmB7E1(^&1lFtyJ#WAt3()kDwu=!$@(x=F;(`Ke31 z&fISEqd((A7RX(fjy9||KAwJhmj!d~v8*&;Bi6!)H;pb?$!j$r_JK$0D2WZBh|ON^ zgHmDufytO29jpCKOHfspx2LTf)OW2+M?v10CCjF-n;dthNu&pq0{N_5i zM}+|okiI{0hI@9@WrQlUYf{3bIXxqgH)Dp-IW)6Ihi7*LkkCWuq91IBS95k`$8@uw zEU-H>&$1C=se~HX3;mhZaF7gYT3yBhRk)78Jg$O4+TfvTvSPw1d zczh@r=5J{Vu0jiCzzy@UXYNT!&H2QaGD&yueXn^>tZ_?zNivd?HfeGudPgH%iK*k; zuHl+K>KJ4gSFfHoYdxw-;0*jU>Zocv!D`Ix*l8IGsps#_I$KS13Vx$>tG?JrMjJB= zwtn$KCK!(<))n(}lJgRG))4p>9kZ9pIukB5-U-VDe0pQO&PLo6IQ!?$5K10BE)Bak3xwAd{qZW_07_PB&8C$=lxmkN=O zQ3rJulRR|=jnP^@{pFGQM-zKSka=5F#vWbh3P?J1ggF1 z-&*eJE%(E-6$*o&H`&M!`)S<{UaRmJMUB}dDElo{#?q71(`BSAo!*I_Zf)&jVt!Y? zSri>*EQT4;m&=9#&?Sa%gl~kq&<(MD+>1(1+P39bdT)6M&NcA#qRAvhcpQCxu*cnD zU`$c%J4JH6agB{{&h+VRglSERI%xOa8Iy*&sL_7zv?f&~*Nw7Q!Q}MyuLe4Ia-*R%IqeC-{8%1 z|KP^4X!)Gz`6Me~v*~^*VIF%&^FTS}gyG?nb;s0i5>o!&80qI;cx9v?4j&E-W(j5# zd&!sSyDH)BOce6Hy|G<}9pYVpS*W^m#IGKto;EV<1oS#Q4<`Z^OR=jenRG4Fh|tHdNiD zk~F4C=nIrwlQy{t4b#+=yP(cv*opnZ>CR4sEM+}=j3FRG3o`7M1X=}i(UHY&ARWiA z0Ksh+BzDL~;=`PgUbpo5t{iYssHsdY2+Qwxzu0yB~4sv2%v0U0IWp z?;$ZRF%ALB5H1298L7sB1VRR)U}-Z1RNe~j?X*&m{%n0AT~Uq4eK=^^uxBl_TDwxv zT7a2FuI%@@ChND+VW-t26p1yE{pqIHybxn5QbHf7IH`tO(H^R(g^^vNHN*q;xE>rP zlh~bfn^egyZ0H%{3EN6KQG|GHt}1H z-d_7)Sqsu??;7>8(Oe9ArqWf=S>{Uehc}+Z`>;*TV%AF0yd9nshZ`1Q3)SC*4!xB) zt|AZ^Y+09nbi(l%?4!YNZz52n(HZQEP*f3Q8m#wG(2UT1^$I~JuW_7dl3~p*$Sy>1 zUqNtldA1x*)^k~P`O#W4p2l^X>2x;LKWpDzbR zewUXhv98IDp5yPr-3L`5Ta1V61B@9UK!%Fcb^pW+Nyfh39@cnl&qW;k`<)d0{&225 z=#Y+t*#fxAC^%-e*rBo#rt?5ofiM>|CE&@9%#+ba>od1aVu{a@zq^*XYqb93Ta_({ zM}4o1R$(p0%lTLjRaOL446eP{dS!{9IkPZ9i$c;xI`w!X= zJ6H{9?P*Nh3Jn3`2aacri9`ck-!GKeKMre`ma|~YQ5ql3{e~XUT3m5=aHwATRHaz} zXbzByVRV}isEBu6?`aVhi0!$g_iPkF#p0RifHFaG{JgU$FVO^2R{VzY`HPnBR9>Rv z7aR99BD_<@@p=>459wlS28+d6N8F7<&Nx)NdQA$m$aP|n@XmaFdqON~2?7%`@rt<-86&t;mNJtdSvKCsm{fA1prp4SpaHU4F_ zlZUCc=%LZl&Uy}uO@_yI*I3PYq$fu>eW`Ee^C;i%L!!cvIPW)hvl5vZEN%&gyPBO< zo}?Gs2;^{N4bg<*&CHH=uu=p<-d%u#)_t5)_<-ctNf0JMyZ$(P>yF|DZG}#oR=6u__ zexTgvrZ?p`No&+`9XvsNIZIhf9YBD!m+?{ur|Ixc{Lvh>7N%Giv&1v?Ypfai?D@#H z{T^!yGbly?4|h__N$H-dD`RFy_Qa&F zVVccODFTrP2^4&rc;N=(#mi6-u4z&CVbg+v7OpK(Y!_dAZQ?`hNyVw3*zFunN4JUC zTEO~R+vUOJssBb>)$u;p55M!+H@s`K(Vk)t4X1ChRuekmyowUszOY?{1p@`wStwoc zQ1z&rw>r=^OMHHx?twTMHtNI|yS%uEzr%4M4j zsW*^v`Mzx&2Vr_}(zA*L0 z$YlVUb~EbJQKATXkazKtL54Q%AD@1>7CeFaD`Z

E=Uh+u%HTuOQZrpY^bxirwY( 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 literal 0 HcmV?d00001 diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/resources/files/S16.pdf b/redaction-service-v1/redaction-service-server-v1/src/test/resources/files/S16.pdf new file mode 100644 index 0000000000000000000000000000000000000000..013951cca729092adfb4717e848a1ed835019bb5 GIT binary patch literal 138352 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ŚZSFx> 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 literal 0 HcmV?d00001 From 146b0f5c8eb1fe8d8c530b940049e40a6eebc80a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20Eifl=C3=A4nder?= Date: Wed, 22 Sep 2021 15:11:14 +0200 Subject: [PATCH 80/80] RED-2228: Use persistence-service instead of configuration/file-management-service --- .../redaction-service-api-v1/pom.xml | 10 +- .../redaction/v1/model/RedactionLog.java | 5 +- .../redaction-service-server-v1/pom.xml | 15 --- .../v1/server/client/DictionaryClient.java | 5 +- .../FileStatusProcessingUpdateClient.java | 5 +- .../v1/server/client/LegalBasisClient.java | 5 +- .../v1/server/client/RulesClient.java | 5 +- .../controller/RedactionController.java | 4 +- .../redaction/model/DictionaryModel.java | 3 +- .../redaction/service/AnalyzeService.java | 2 +- .../redaction/service/DictionaryService.java | 35 +++--- .../service/DroolsExecutionService.java | 7 +- .../redaction/service/NerAnalyserService.java | 2 +- .../storage/RedactionStorageService.java | 2 +- .../src/main/resources/application.yml | 3 +- .../v1/server/RedactionIntegrationTest.java | 104 +++++++++--------- .../src/test/resources/application.yml | 3 +- 17 files changed, 98 insertions(+), 117 deletions(-) diff --git a/redaction-service-v1/redaction-service-api-v1/pom.xml b/redaction-service-v1/redaction-service-api-v1/pom.xml index e4b7cd36..79cd421c 100644 --- a/redaction-service-v1/redaction-service-api-v1/pom.xml +++ b/redaction-service-v1/redaction-service-api-v1/pom.xml @@ -19,14 +19,8 @@ com.iqser.red.service - configuration-service-api-v1 - 2.11.0 - - - com.iqser.red.service - file-management-service-api-v1 - - + persistence-service-api-v1 + 0.4.0 diff --git a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLog.java b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLog.java index 942e36a6..2e404431 100644 --- a/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLog.java +++ b/redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/RedactionLog.java @@ -1,11 +1,12 @@ package com.iqser.red.service.redaction.v1.model; -import com.iqser.red.service.configuration.v1.api.model.LegalBasisMapping; import lombok.AllArgsConstructor; import lombok.Data; import java.util.List; +import com.iqser.red.service.persistence.service.v1.api.model.data.configuration.LegalBasis; + @Data @AllArgsConstructor public class RedactionLog { @@ -18,7 +19,7 @@ public class RedactionLog { private long analysisVersion; private List redactionLogEntry; - private List legalBasis; + private List legalBasis; private long dictionaryVersion = -1; private long dossierDictionaryVersion = -1; diff --git a/redaction-service-v1/redaction-service-server-v1/pom.xml b/redaction-service-v1/redaction-service-server-v1/pom.xml index 67e97d07..52a51cb7 100644 --- a/redaction-service-v1/redaction-service-server-v1/pom.xml +++ b/redaction-service-v1/redaction-service-server-v1/pom.xml @@ -21,21 +21,6 @@ redaction-service-api-v1 ${project.version} - - com.iqser.red.service - file-management-service-api-v1 - 2.96.0 - - - com.iqser.red.service - redaction-service-api-v1 - - - com.iqser.red.service - configuration-service-api-v1 - - - org.drools drools-core diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/DictionaryClient.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/DictionaryClient.java index 422c97bb..2f8d8154 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/DictionaryClient.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/DictionaryClient.java @@ -1,8 +1,9 @@ package com.iqser.red.service.redaction.v1.server.client; -import com.iqser.red.service.configuration.v1.api.resource.DictionaryResource; import org.springframework.cloud.openfeign.FeignClient; -@FeignClient(name = "DictionaryResource", url = "${configuration-service.url}") +import com.iqser.red.service.persistence.service.v1.api.resources.DictionaryResource; + +@FeignClient(name = "DictionaryResource", url = "${persistence-service.url}") public interface DictionaryClient extends DictionaryResource { } \ No newline at end of file diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/FileStatusProcessingUpdateClient.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/FileStatusProcessingUpdateClient.java index 41182a31..dde47a5e 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/FileStatusProcessingUpdateClient.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/FileStatusProcessingUpdateClient.java @@ -1,9 +1,10 @@ package com.iqser.red.service.redaction.v1.server.client; -import com.iqser.red.service.file.management.v1.api.resources.FileStatusProcessingUpdateResource; import org.springframework.cloud.openfeign.FeignClient; -@FeignClient(name = "FileStatusProcessingUpdateResource", url = "${file-management-service.url}") +import com.iqser.red.service.persistence.service.v1.api.resources.FileStatusProcessingUpdateResource; + +@FeignClient(name = "FileStatusProcessingUpdateResource", url = "${persistence-service.url}") public interface FileStatusProcessingUpdateClient extends FileStatusProcessingUpdateResource { } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/LegalBasisClient.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/LegalBasisClient.java index 1a68ab35..f51ca689 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/LegalBasisClient.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/LegalBasisClient.java @@ -1,8 +1,9 @@ package com.iqser.red.service.redaction.v1.server.client; -import com.iqser.red.service.configuration.v1.api.resource.LegalBasisMappingResource; import org.springframework.cloud.openfeign.FeignClient; -@FeignClient(name = "LegalBasisMappingResource", url = "${configuration-service.url}") +import com.iqser.red.service.persistence.service.v1.api.resources.LegalBasisMappingResource; + +@FeignClient(name = "LegalBasisMappingResource", url = "${persistence-service.url}") public interface LegalBasisClient extends LegalBasisMappingResource { } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/RulesClient.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/RulesClient.java index 35ffba79..5e8fb5c2 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/RulesClient.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/client/RulesClient.java @@ -1,8 +1,9 @@ package com.iqser.red.service.redaction.v1.server.client; -import com.iqser.red.service.configuration.v1.api.resource.RulesResource; import org.springframework.cloud.openfeign.FeignClient; -@FeignClient(name = "RulesResource", url = "${configuration-service.url}") +import com.iqser.red.service.persistence.service.v1.api.resources.RulesResource; + +@FeignClient(name = "RulesResource", url = "${persistence-service.url}") public interface RulesClient extends RulesResource { } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java index 2ea691d7..c47022d0 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/controller/RedactionController.java @@ -1,6 +1,6 @@ package com.iqser.red.service.redaction.v1.server.controller; -import com.iqser.red.service.file.management.v1.api.model.FileType; +import com.iqser.red.service.persistence.service.v1.api.model.FileType; import com.iqser.red.service.redaction.v1.model.*; import com.iqser.red.service.redaction.v1.resources.RedactionResource; import com.iqser.red.service.redaction.v1.server.classification.model.Document; @@ -11,7 +11,6 @@ import com.iqser.red.service.redaction.v1.server.redaction.service.DictionarySer import com.iqser.red.service.redaction.v1.server.redaction.service.DroolsExecutionService; import com.iqser.red.service.redaction.v1.server.redaction.service.RedactionLogMergeService; import com.iqser.red.service.redaction.v1.server.segmentation.PdfSegmentationService; -import com.iqser.red.service.redaction.v1.server.settings.RedactionServiceSettings; import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService; import com.iqser.red.service.redaction.v1.server.tableextraction.model.AbstractTextContainer; import com.iqser.red.service.redaction.v1.server.tableextraction.model.Table; @@ -20,7 +19,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.pdfbox.io.MemoryUsageSetting; import org.apache.pdfbox.pdmodel.PDDocument; -import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/DictionaryModel.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/DictionaryModel.java index c3a80b96..e6c0eb09 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/DictionaryModel.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/DictionaryModel.java @@ -1,7 +1,6 @@ package com.iqser.red.service.redaction.v1.server.redaction.model; -import com.iqser.red.service.configuration.v1.api.model.DictionaryEntry; import lombok.AllArgsConstructor; import lombok.Data; @@ -9,6 +8,8 @@ import java.io.Serializable; import java.util.Set; import java.util.stream.Collectors; +import com.iqser.red.service.persistence.service.v1.api.model.data.configuration.DictionaryEntry; + @Data @AllArgsConstructor public class DictionaryModel implements Serializable { diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeService.java index c9dc03c8..85672d3e 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/AnalyzeService.java @@ -12,7 +12,7 @@ import org.kie.api.runtime.KieContainer; import org.springframework.stereotype.Service; import org.springframework.web.bind.annotation.RequestBody; -import com.iqser.red.service.file.management.v1.api.model.FileType; +import com.iqser.red.service.persistence.service.v1.api.model.FileType; import com.iqser.red.service.redaction.v1.model.AnalyzeRequest; import com.iqser.red.service.redaction.v1.model.AnalyzeResult; import com.iqser.red.service.redaction.v1.model.IdRemoval; diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java index 9631d5b8..2e0d641d 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DictionaryService.java @@ -1,9 +1,7 @@ package com.iqser.red.service.redaction.v1.server.redaction.service; -import com.iqser.red.service.configuration.v1.api.model.Colors; -import com.iqser.red.service.configuration.v1.api.model.DictionaryEntry; -import com.iqser.red.service.configuration.v1.api.model.TypeResponse; -import com.iqser.red.service.configuration.v1.api.model.TypeResult; +import com.iqser.red.service.persistence.service.v1.api.model.data.configuration.Colors; +import com.iqser.red.service.persistence.service.v1.api.model.data.configuration.DictionaryEntry; import com.iqser.red.service.redaction.v1.server.client.DictionaryClient; import com.iqser.red.service.redaction.v1.server.redaction.model.Dictionary; import com.iqser.red.service.redaction.v1.server.redaction.model.*; @@ -18,7 +16,6 @@ import java.awt.Color; import java.util.*; import java.util.stream.Collectors; -import static com.iqser.red.service.configuration.v1.api.resource.DictionaryResource.GLOBAL_DOSSIER; @Slf4j @Service @@ -34,13 +31,13 @@ public class DictionaryService { public DictionaryVersion updateDictionary(String dossierTemplateId, String dossierId) { log.info("Updating dictionary data for dossierTemplate {} and dossier {}", dossierTemplateId, dossierId); - long dossierTemplateDictionaryVersion = dictionaryClient.getVersion(dossierTemplateId, GLOBAL_DOSSIER); + long dossierTemplateDictionaryVersion = dictionaryClient.getVersion(dossierTemplateId); var dossierTemplateDictionary = dictionariesByDossierTemplate.get(dossierTemplateId); if (dossierTemplateDictionary == null || dossierTemplateDictionaryVersion > dossierTemplateDictionary.getDictionaryVersion()) { - updateDictionaryEntry(dossierTemplateId, dossierTemplateDictionaryVersion, GLOBAL_DOSSIER); + updateDictionaryEntry(dossierTemplateId, dossierTemplateDictionaryVersion, null); } - long dossierDictionaryVersion = dictionaryClient.getVersion(dossierTemplateId, dossierId); + long dossierDictionaryVersion = dictionaryClient.getVersionForDossier(dossierId); var dossierDictionary = dictionariesByDossier.get(dossierId); if (dossierDictionary == null || dossierDictionaryVersion > dossierDictionary.getDictionaryVersion()) { updateDictionaryEntry(dossierTemplateId, dossierDictionaryVersion, dossierId); @@ -84,13 +81,13 @@ public class DictionaryService { try { DictionaryRepresentation dictionaryRepresentation = new DictionaryRepresentation(); - TypeResponse typeResponse = dictionaryClient.getAllTypes(dossierTemplateId, dossierId); - if (typeResponse != null && CollectionUtils.isNotEmpty(typeResponse.getTypes())) { + var typeResponse = dossierId == null ? dictionaryClient.getAllTypesForDossierTemplate(dossierTemplateId) : dictionaryClient.getAllTypesForDossier(dossierId); + if (typeResponse != null && CollectionUtils.isNotEmpty(typeResponse)) { - List dictionary = typeResponse.getTypes() + List dictionary = typeResponse .stream() .map(t -> new DictionaryModel(t.getType(), t.getRank(), convertColor(t.getHexColor()), t.isCaseInsensitive(), t - .isHint(), t.isRecommendation(), convertEntries(t, dossierId), new HashSet<>(), !dossierId.equals(GLOBAL_DOSSIER))) + .isHint(), t.isRecommendation(), convertEntries(t.getId()), new HashSet<>(), dossierId != null)) .sorted(Comparator.comparingInt(DictionaryModel::getRank).reversed()) .collect(Collectors.toList()); @@ -106,7 +103,7 @@ public class DictionaryService { dictionaryRepresentation.setDictionaryVersion(version); dictionaryRepresentation.setDictionary(dictionary); - if(dossierId.equals(GLOBAL_DOSSIER)) { + if(dossierId == null) { dictionariesByDossierTemplate.put(dossierTemplateId, dictionaryRepresentation); } else { dictionariesByDossier.put(dossierId, dictionaryRepresentation); @@ -123,8 +120,8 @@ public class DictionaryService { dictionary.getDictionaryModels().forEach(dm -> { if (dm.isRecommendation() && !dm.getLocalEntries().isEmpty()) { - dictionaryClient.addEntries(dm.getType(), dossierTemplateId, new ArrayList<>(dm.getLocalEntries()), false, GLOBAL_DOSSIER); - long externalVersion = dictionaryClient.getVersion(dossierTemplateId, GLOBAL_DOSSIER); + dictionaryClient.addEntries(dm.getType(), new ArrayList<>(dm.getLocalEntries()), false); + long externalVersion = dictionaryClient.getVersion(dossierTemplateId); if (externalVersion == dictionary.getVersion().getDossierTemplateVersion() + 1) { dictionary.getVersion().setDossierTemplateVersion(externalVersion); } @@ -133,12 +130,14 @@ public class DictionaryService { } - private Set convertEntries(TypeResult t, String dossierId) { + private Set convertEntries(String typeId) { - Set entries = new HashSet<>(dictionaryClient.getDictionaryForType(t.getType(), t.getDossierTemplateId(), dossierId) + var type = dictionaryClient.getDictionaryForType(typeId); + + Set entries = new HashSet<>(type .getEntries()); - if (t.isCaseInsensitive()) { + if (type.isCaseInsensitive()) { entries.forEach(entry -> entry.setValue(entry.getValue().toLowerCase(Locale.ROOT))); } return entries; diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DroolsExecutionService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DroolsExecutionService.java index 0ac53bc6..39893057 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DroolsExecutionService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/DroolsExecutionService.java @@ -1,6 +1,5 @@ package com.iqser.red.service.redaction.v1.server.redaction.service; -import com.iqser.red.service.configuration.v1.api.model.RulesResponse; import com.iqser.red.service.redaction.v1.server.client.RulesClient; import com.iqser.red.service.redaction.v1.server.exception.RulesValidationException; import com.iqser.red.service.redaction.v1.server.redaction.model.Section; @@ -76,13 +75,13 @@ public class DroolsExecutionService { try { - RulesResponse rules = rulesClient.getRules(dossierTemplateId); - if (rules == null || StringUtils.isEmpty(rules.getRules())) { + var rules = rulesClient.getRules(dossierTemplateId); + if (rules == null || StringUtils.isEmpty(rules.getValue())) { throw new RuntimeException("Rules cannot be empty."); } KieServices kieServices = KieServices.Factory.get(); - KieModule kieModule = getKieModule(dossierTemplateId, rules.getRules(), kieServices); + KieModule kieModule = getKieModule(dossierTemplateId, rules.getValue(), kieServices); var container = kieContainers.get(dossierTemplateId); if (container != null) { diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/NerAnalyserService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/NerAnalyserService.java index b7611b0d..32769c78 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/NerAnalyserService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/NerAnalyserService.java @@ -5,7 +5,7 @@ import java.util.stream.Collectors; import org.apache.commons.codec.binary.Base64; import org.springframework.stereotype.Service; -import com.iqser.red.service.file.management.v1.api.model.FileType; +import com.iqser.red.service.persistence.service.v1.api.model.FileType; import com.iqser.red.service.redaction.v1.server.client.EntityRecognitionClient; import com.iqser.red.service.redaction.v1.server.client.model.EntityRecognitionRequest; import com.iqser.red.service.redaction.v1.server.client.model.EntityRecognitionSection; diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/storage/RedactionStorageService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/storage/RedactionStorageService.java index 503b3a84..0ff294b0 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/storage/RedactionStorageService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/storage/RedactionStorageService.java @@ -1,7 +1,7 @@ package com.iqser.red.service.redaction.v1.server.storage; import com.fasterxml.jackson.databind.ObjectMapper; -import com.iqser.red.service.file.management.v1.api.model.FileType; +import com.iqser.red.service.persistence.service.v1.api.model.FileType; import com.iqser.red.service.redaction.v1.model.RedactionLog; import com.iqser.red.service.redaction.v1.model.SectionGrid; import com.iqser.red.service.redaction.v1.server.classification.model.Text; diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/resources/application.yml b/redaction-service-v1/redaction-service-server-v1/src/main/resources/application.yml index d8aa5ace..4d0ce6fe 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/resources/application.yml +++ b/redaction-service-v1/redaction-service-server-v1/src/main/resources/application.yml @@ -1,8 +1,7 @@ info: description: Redaction Service Server V1 -configuration-service.url: "http://configuration-service-v1:8080" -file-management-service.url: "http://file-management-service-v1:8080" +persistence-service.url: "http://persistence-service-v1:8080" image-service.url: "http://image-service-v1:8080" entity-recognition-service.url: "http://entity-recognition-service-v1:8080" diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java index eb29141a..8aa8e862 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java @@ -2,9 +2,11 @@ package com.iqser.red.service.redaction.v1.server; import com.amazonaws.services.s3.AmazonS3; import com.fasterxml.jackson.databind.ObjectMapper; -import com.iqser.red.service.configuration.v1.api.model.*; -import com.iqser.red.service.configuration.v1.api.resource.DictionaryResource; -import com.iqser.red.service.file.management.v1.api.model.FileType; +import com.iqser.red.service.persistence.service.v1.api.model.FileType; +import com.iqser.red.service.persistence.service.v1.api.model.JSONPrimitive; +import com.iqser.red.service.persistence.service.v1.api.model.data.configuration.Colors; +import com.iqser.red.service.persistence.service.v1.api.model.data.configuration.DictionaryEntry; +import com.iqser.red.service.persistence.service.v1.api.model.data.configuration.Type; import com.iqser.red.service.redaction.v1.model.*; import com.iqser.red.service.redaction.v1.server.classification.model.SectionText; import com.iqser.red.service.redaction.v1.server.client.DictionaryClient; @@ -176,69 +178,64 @@ public class RedactionIntegrationTest { @Before public void stubClients() { - //Testkommentar when(rulesClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(0L); - when(rulesClient.getRules(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(new RulesResponse(RULES)); + when(rulesClient.getRules(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(JSONPrimitive.of(RULES)); loadDictionaryForTest(); loadTypeForTest(); - when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(0L); - when(dictionaryClient.getAllTypes(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(TypeResponse - .builder() - .types(getTypeResponse()) - .build()); + when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(0L); + when(dictionaryClient.getAllTypesForDossierTemplate(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(getTypeResponse()); - when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, TEST_DOSSIER_ID)).thenReturn(0L); - when(dictionaryClient.getAllTypes(TEST_DOSSIER_TEMPLATE_ID, TEST_DOSSIER_ID)).thenReturn(TypeResponse.builder() - .types(List.of(TypeResult.builder() - .type(DOSSIER_REDACTIONS) - .dossierTemplateId(TEST_DOSSIER_TEMPLATE_ID) + when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(0L); + when(dictionaryClient.getAllTypesForDossier(TEST_DOSSIER_ID)).thenReturn(List.of(Type.builder() + .id(DOSSIER_REDACTIONS +":"+ TEST_DOSSIER_TEMPLATE_ID) + .type(DOSSIER_REDACTIONS) + .dossierTemplateId(TEST_DOSSIER_ID) .hexColor("#ffe187") .isHint(hintTypeMap.get(DOSSIER_REDACTIONS)) .isCaseInsensitive(caseInSensitiveMap.get(DOSSIER_REDACTIONS)) .isRecommendation(recommendationTypeMap.get(DOSSIER_REDACTIONS)) .rank(rankTypeMap.get(DOSSIER_REDACTIONS)) - .build())) - .build()); + .build())); - when(dictionaryClient.getDictionaryForType(VERTEBRATE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + when(dictionaryClient.getDictionaryForType(VERTEBRATE + ":" + TEST_DOSSIER_TEMPLATE_ID)) .thenReturn(getDictionaryResponse(VERTEBRATE, false)); - when(dictionaryClient.getDictionaryForType(ADDRESS, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + when(dictionaryClient.getDictionaryForType(ADDRESS+ ":" + TEST_DOSSIER_TEMPLATE_ID)) .thenReturn(getDictionaryResponse(ADDRESS, false)); - when(dictionaryClient.getDictionaryForType(AUTHOR, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + when(dictionaryClient.getDictionaryForType(AUTHOR+ ":" + TEST_DOSSIER_TEMPLATE_ID)) .thenReturn(getDictionaryResponse(AUTHOR, false)); - when(dictionaryClient.getDictionaryForType(SPONSOR, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + when(dictionaryClient.getDictionaryForType(SPONSOR+ ":" + TEST_DOSSIER_TEMPLATE_ID)) .thenReturn(getDictionaryResponse(SPONSOR, false)); - when(dictionaryClient.getDictionaryForType(NO_REDACTION_INDICATOR, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + when(dictionaryClient.getDictionaryForType(NO_REDACTION_INDICATOR+ ":" + TEST_DOSSIER_TEMPLATE_ID)) .thenReturn(getDictionaryResponse(NO_REDACTION_INDICATOR, false)); - when(dictionaryClient.getDictionaryForType(REDACTION_INDICATOR, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + when(dictionaryClient.getDictionaryForType(REDACTION_INDICATOR+ ":" + TEST_DOSSIER_TEMPLATE_ID)) .thenReturn(getDictionaryResponse(REDACTION_INDICATOR, false)); - when(dictionaryClient.getDictionaryForType(HINT_ONLY, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + when(dictionaryClient.getDictionaryForType(HINT_ONLY+ ":" + TEST_DOSSIER_TEMPLATE_ID)) .thenReturn(getDictionaryResponse(HINT_ONLY, false)); - when(dictionaryClient.getDictionaryForType(MUST_REDACT, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + when(dictionaryClient.getDictionaryForType(MUST_REDACT+ ":" + TEST_DOSSIER_TEMPLATE_ID)) .thenReturn(getDictionaryResponse(MUST_REDACT, false)); - when(dictionaryClient.getDictionaryForType(PUBLISHED_INFORMATION, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + when(dictionaryClient.getDictionaryForType(PUBLISHED_INFORMATION+ ":" + TEST_DOSSIER_TEMPLATE_ID)) .thenReturn(getDictionaryResponse(PUBLISHED_INFORMATION, false)); - when(dictionaryClient.getDictionaryForType(TEST_METHOD, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + when(dictionaryClient.getDictionaryForType(TEST_METHOD+ ":" + TEST_DOSSIER_TEMPLATE_ID)) .thenReturn(getDictionaryResponse(TEST_METHOD, false)); - when(dictionaryClient.getDictionaryForType(PII, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(PII, false)); - when(dictionaryClient.getDictionaryForType(RECOMMENDATION_AUTHOR, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + when(dictionaryClient.getDictionaryForType(PII+ ":" + TEST_DOSSIER_TEMPLATE_ID)).thenReturn(getDictionaryResponse(PII, false)); + when(dictionaryClient.getDictionaryForType(RECOMMENDATION_AUTHOR+ ":" + TEST_DOSSIER_TEMPLATE_ID)) .thenReturn(getDictionaryResponse(RECOMMENDATION_AUTHOR, false)); - when(dictionaryClient.getDictionaryForType(RECOMMENDATION_ADDRESS, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + when(dictionaryClient.getDictionaryForType(RECOMMENDATION_ADDRESS+ ":" + TEST_DOSSIER_TEMPLATE_ID)) .thenReturn(getDictionaryResponse(RECOMMENDATION_ADDRESS, false)); - when(dictionaryClient.getDictionaryForType(FALSE_POSITIVE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + when(dictionaryClient.getDictionaryForType(FALSE_POSITIVE+ ":" + TEST_DOSSIER_TEMPLATE_ID)) .thenReturn(getDictionaryResponse(FALSE_POSITIVE, false)); - when(dictionaryClient.getDictionaryForType(PURITY, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + when(dictionaryClient.getDictionaryForType(PURITY+ ":" + TEST_DOSSIER_TEMPLATE_ID)) .thenReturn(getDictionaryResponse(PURITY, false)); - when(dictionaryClient.getDictionaryForType(IMAGE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(IMAGE, false)); - when(dictionaryClient.getDictionaryForType(OCR, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(OCR, false)); - when(dictionaryClient.getDictionaryForType(LOGO, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(getDictionaryResponse(LOGO, false)); - when(dictionaryClient.getDictionaryForType(SIGNATURE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + when(dictionaryClient.getDictionaryForType(IMAGE+ ":" + TEST_DOSSIER_TEMPLATE_ID)).thenReturn(getDictionaryResponse(IMAGE, false)); + when(dictionaryClient.getDictionaryForType(OCR+ ":" + TEST_DOSSIER_TEMPLATE_ID)).thenReturn(getDictionaryResponse(OCR, false)); + when(dictionaryClient.getDictionaryForType(LOGO+ ":" + TEST_DOSSIER_TEMPLATE_ID)).thenReturn(getDictionaryResponse(LOGO, false)); + when(dictionaryClient.getDictionaryForType(SIGNATURE+ ":" + TEST_DOSSIER_TEMPLATE_ID)) .thenReturn(getDictionaryResponse(SIGNATURE, false)); - when(dictionaryClient.getDictionaryForType(FORMULA, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + when(dictionaryClient.getDictionaryForType(FORMULA+ ":" + TEST_DOSSIER_TEMPLATE_ID)) .thenReturn(getDictionaryResponse(FORMULA, false)); - when(dictionaryClient.getDictionaryForType(DOSSIER_REDACTIONS, TEST_DOSSIER_TEMPLATE_ID, TEST_DOSSIER_ID)).thenReturn(getDictionaryResponse(DOSSIER_REDACTIONS, true)); + when(dictionaryClient.getDictionaryForType(DOSSIER_REDACTIONS+ ":" + TEST_DOSSIER_TEMPLATE_ID)).thenReturn(getDictionaryResponse(DOSSIER_REDACTIONS, true)); when(dictionaryClient.getColors(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(colors); } @@ -477,11 +474,12 @@ public class RedactionIntegrationTest { } - private List getTypeResponse() { + private List getTypeResponse() { return typeColorMap.entrySet() .stream() - .map(typeColor -> TypeResult.builder() + .map(typeColor -> Type.builder() + .id(typeColor.getKey() + ":" + TEST_DOSSIER_TEMPLATE_ID) .type(typeColor.getKey()) .dossierTemplateId(TEST_DOSSIER_TEMPLATE_ID) .hexColor(typeColor.getValue()) @@ -495,9 +493,10 @@ public class RedactionIntegrationTest { } - private DictionaryResponse getDictionaryResponse(String type, boolean isDossierDictionary) { + private Type getDictionaryResponse(String type, boolean isDossierDictionary) { - return DictionaryResponse.builder() + return Type.builder() + .id(type + ":" +TEST_DOSSIER_TEMPLATE_ID) .hexColor(typeColorMap.get(type)) .entries(isDossierDictionary ? toDictionaryEntry(dossierDictionary.get(type)) : toDictionaryEntry(dictionary .get(type))) @@ -513,8 +512,11 @@ public class RedactionIntegrationTest { List dictionaryEntries = new ArrayList<>(); entries.forEach(entry -> { - dictionaryEntries.add(new DictionaryEntry(entry, reanlysisVersions.containsKey(entry) ? reanlysisVersions.get(entry) : 0L, deleted - .contains(entry) ? true : false)); + dictionaryEntries.add(DictionaryEntry.builder() + .value(entry) + .version(reanlysisVersions.containsKey(entry) ? reanlysisVersions.get(entry) : 0L) + .deleted(deleted + .contains(entry) ? true : false).build()); }); return dictionaryEntries; } @@ -566,7 +568,7 @@ public class RedactionIntegrationTest { }); dictionary.get(AUTHOR).add("Drinking water"); - when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(1L); + when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(1L); AnnotateResponse annotateResponse = redactionController.annotate(AnnotateRequest.builder() .dossierId(TEST_DOSSIER_ID) @@ -625,7 +627,7 @@ public class RedactionIntegrationTest { }); dictionary.get(AUTHOR).add("Drinking water"); - when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(1L); + when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(1L); long rstart = System.currentTimeMillis(); analyzeService.reanalyze(request); @@ -730,12 +732,12 @@ public class RedactionIntegrationTest { reanlysisVersions.put("mouse", 3L); - when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(3L); + when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(3L); - when(dictionaryClient.getDictionaryForType(VERTEBRATE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + when(dictionaryClient.getDictionaryForType(VERTEBRATE)) .thenReturn(getDictionaryResponse(VERTEBRATE, false)); - when(dictionaryClient.getDictionaryForType(FALSE_POSITIVE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + when(dictionaryClient.getDictionaryForType(FALSE_POSITIVE)) .thenReturn(getDictionaryResponse(FALSE_POSITIVE, false)); start = System.currentTimeMillis(); @@ -770,9 +772,9 @@ public class RedactionIntegrationTest { deleted.remove("mouse"); reanlysisVersions.put("mouse", 4L); - when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)).thenReturn(4L); + when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(4L); - when(dictionaryClient.getDictionaryForType(VERTEBRATE, TEST_DOSSIER_TEMPLATE_ID, DictionaryResource.GLOBAL_DOSSIER)) + when(dictionaryClient.getDictionaryForType(VERTEBRATE)) .thenReturn(getDictionaryResponse(VERTEBRATE, false)); analyzeService.reanalyze(request); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/resources/application.yml b/redaction-service-v1/redaction-service-server-v1/src/test/resources/application.yml index d8543d66..6cf7c053 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/resources/application.yml +++ b/redaction-service-v1/redaction-service-server-v1/src/test/resources/application.yml @@ -1,6 +1,5 @@ -configuration-service.url: "http://configuration-service-v1:8080" image-service.url: "http://image-service-v1:8080" -file-management-service.url: "http://file-management-service-v1:8080" +persistence-service.url: "http://persistence-service-v1:8080" entity-recognition-service.url: "localhost:8080" ribbon: