From 37627aa79eca2764b2336dd03ce36dd15a5d7c3c Mon Sep 17 00:00:00 2001 From: Kilian Schuettler Date: Thu, 12 Oct 2023 11:41:59 +0200 Subject: [PATCH 1/6] RED-7631: entity log to replace redactionlog * replaced redactionLog in all tests * refactored out sectiongrid Signed-off-by: Kilian Schuettler --- .../v1/server/service/AnalyzeService.java | 1 - .../service/EntityChangeLogService.java | 19 +- .../service/EntityLogCreatorService.java | 38 +-- .../storage/RedactionStorageService.java | 7 + .../utils/RectangleTransformations.java | 15 +- .../v1/server/DocumineFloraTest.java | 8 +- .../v1/server/RedactionIntegrationTest.java | 90 +++---- .../v1/server/RedactionIntegrationV2Test.java | 28 ++- .../v1/server/annotate/AnnotationService.java | 220 ++++++------------ .../src/test/resources/dictionaries/PII.txt | 2 +- .../dictionaries/PII_false_positive.txt | 2 +- .../{402Study.pdf => 402Study-ocred.pdf} | Bin 2328655 -> 2380311 bytes 12 files changed, 183 insertions(+), 247 deletions(-) rename redaction-service-v1/redaction-service-server-v1/src/test/resources/files/Documine/Flora/{402Study.pdf => 402Study-ocred.pdf} (97%) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/AnalyzeService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/AnalyzeService.java index 6abf510e..48fa20cc 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/AnalyzeService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/AnalyzeService.java @@ -71,7 +71,6 @@ public class AnalyzeService { ComponentLogCreatorService componentLogCreatorService; RedactionStorageService redactionStorageService; RedactionChangeLogService redactionChangeLogService; - EntityChangeLogService entityChangeLogService; LegalBasisClient legalBasisClient; RedactionServiceSettings redactionServiceSettings; ImportedRedactionService importedRedactionService; diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/EntityChangeLogService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/EntityChangeLogService.java index 039b9290..64397765 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/EntityChangeLogService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/EntityChangeLogService.java @@ -26,9 +26,14 @@ public class EntityChangeLogService { @Timed("redactmanager_computeChanges") public boolean computeChanges(List previousEntityLogEntries, List newEntityLogEntries, int analysisNumber) { + var now = OffsetDateTime.now(); + if (previousEntityLogEntries.isEmpty()) { + newEntityLogEntries.forEach(entry -> entry.getChanges().add(new Change(analysisNumber, ChangeType.ADDED, now))); + return true; + } + boolean hasChanges = false; - var now = OffsetDateTime.now(); for (EntityLogEntry entityLogEntry : newEntityLogEntries) { Optional optionalPreviousEntity = previousEntityLogEntries.stream().filter(entry -> entry.getId().equals(entityLogEntry.getId())).findAny(); if (optionalPreviousEntity.isEmpty()) { @@ -46,11 +51,21 @@ public class EntityChangeLogService { } } } + addRemovedEntriesAsRemoved(previousEntityLogEntries, newEntityLogEntries, analysisNumber, now); + return hasChanges; + } + + + private static void addRemovedEntriesAsRemoved(List previousEntityLogEntries, + List newEntityLogEntries, + int analysisNumber, + OffsetDateTime now) { + Set existingIds = newEntityLogEntries.stream().map(EntityLogEntry::getId).collect(Collectors.toSet()); List removedEntries = previousEntityLogEntries.stream().filter(entry -> !existingIds.contains(entry.getId())).toList(); removedEntries.forEach(entry -> entry.getChanges().add(new Change(analysisNumber, ChangeType.REMOVED, now))); removedEntries.forEach(entry -> entry.setState(EntryState.REMOVED)); - return hasChanges; + newEntityLogEntries.addAll(removedEntries); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/EntityLogCreatorService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/EntityLogCreatorService.java index e915cc0f..8a2bbfcf 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/EntityLogCreatorService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/EntityLogCreatorService.java @@ -1,6 +1,5 @@ package com.iqser.red.service.redaction.v1.server.service; -import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -12,8 +11,6 @@ import java.util.stream.Collectors; import org.springframework.stereotype.Service; import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeRequest; -import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Change; -import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.ChangeType; import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLog; import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLogChanges; import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLogEntry; @@ -33,21 +30,26 @@ import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntit import com.iqser.red.service.redaction.v1.server.model.document.nodes.Document; import com.iqser.red.service.redaction.v1.server.model.document.nodes.Image; import com.iqser.red.service.redaction.v1.server.model.document.nodes.ImageType; +import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService; +import lombok.AccessLevel; import lombok.RequiredArgsConstructor; +import lombok.experimental.FieldDefaults; import lombok.extern.slf4j.Slf4j; @Service @Slf4j @RequiredArgsConstructor +@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) public class EntityLogCreatorService { - private final DictionaryService dictionaryService; - private final ManualChangeFactory manualChangeFactory; - private final ImportedRedactionService importedRedactionService; - private final RedactionServiceSettings redactionServiceSettings; - private final LegalBasisClient legalBasisClient; - private final EntityChangeLogService entityChangeLogService; + DictionaryService dictionaryService; + ManualChangeFactory manualChangeFactory; + ImportedRedactionService importedRedactionService; + RedactionServiceSettings redactionServiceSettings; + LegalBasisClient legalBasisClient; + EntityChangeLogService entityChangeLogService; + RedactionStorageService redactionStorageService; private static boolean notFalsePositiveOrFalseRecommendation(TextEntity textEntity) { @@ -65,7 +67,6 @@ public class EntityLogCreatorService { List entityLogEntries = createEntityLogEntries(document, analyzeRequest.getDossierTemplateId(), notFoundManualRedactionEntries); List legalBasis = legalBasisClient.getLegalBasisMapping(analyzeRequest.getDossierTemplateId()); - EntityLog entityLog = new EntityLog(redactionServiceSettings.getAnalysisVersion(), analyzeRequest.getAnalysisNumber(), entityLogEntries, @@ -82,13 +83,23 @@ public class EntityLogCreatorService { true); entityLog.setEntityLogEntry(importedRedactionFilteredEntries); - var now = OffsetDateTime.now(); - entityLogEntries.forEach(entry -> entry.getChanges().add(new Change(analyzeRequest.getAnalysisNumber(), ChangeType.ADDED, now))); + List previousExistingEntityLogEntries = getPreviousEntityLogEntries(analyzeRequest.getDossierId(), analyzeRequest.getFileId()); + entityChangeLogService.computeChanges(previousExistingEntityLogEntries, entityLogEntries, analyzeRequest.getAnalysisNumber()); excludeExcludedPages(entityLog, analyzeRequest.getExcludedPages()); return entityLog; } + private List getPreviousEntityLogEntries(String dossierId, String fileId) { + + if (redactionStorageService.entityLogExists(dossierId, fileId)) { + return redactionStorageService.getEntityLog(dossierId, fileId).getEntityLogEntry(); + } else { + return Collections.emptyList(); + } + } + + public EntityLogChanges updateVersionsAndReturnChanges(EntityLog entityLog, DictionaryVersion dictionaryVersion, String dossierTemplateId, boolean hasChanges) { List legalBasis = legalBasisClient.getLegalBasisMapping(dossierTemplateId); @@ -207,8 +218,7 @@ public class EntityLogCreatorService { boolean isHint = isHint(manualEntity.getEntityType()); return EntityLogEntry.builder().id(manualEntity.getId()).color(getColor(type, dossierTemplateId, manualEntity.applied(), isHint)) .reason(manualEntity.buildReasonWithManualChangeDescriptions()) - .legalBasis(manualEntity.legalBasis()) - .value(manualEntity.value()).type(type).state(buildEntryState(manualEntity)).entryType(buildEntryType(manualEntity)) + .legalBasis(manualEntity.legalBasis()).value(manualEntity.value()).type(type).state(buildEntryState(manualEntity)).entryType(buildEntryType(manualEntity)) .section(manualEntity.getManualOverwrite().getSection().orElse(manualEntity.getSection())) .containingNodeId(Collections.emptyList()) .closestHeadline("") 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 8f4ddbd1..a5648475 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 @@ -86,6 +86,7 @@ public class RedactionStorageService { } + @Deprecated(forRemoval = true) @Timed("redactmanager_getRedactionLog") public RedactionLog getRedactionLog(String dossierId, String fileId) { @@ -160,6 +161,12 @@ public class RedactionStorageService { } + public boolean entityLogExists(String dossierId, String fileId) { + + return storageService.objectExists(TenantContext.getTenantId(), StorageIdUtils.getStorageId(dossierId, fileId, FileType.COMPONENT_LOG)); + } + + @RequiredArgsConstructor public enum StorageType { PARSED_DOCUMENT(".json"); diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/utils/RectangleTransformations.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/utils/RectangleTransformations.java index b0880c33..41edfba8 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/utils/RectangleTransformations.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/utils/RectangleTransformations.java @@ -12,7 +12,7 @@ import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collector; -import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Rectangle; +import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Position; import com.iqser.red.service.redaction.v1.server.model.document.textblock.AtomicTextBlock; import lombok.AllArgsConstructor; @@ -26,18 +26,9 @@ public class RectangleTransformations { } - public static Rectangle2D rectangleBBox(List rectangles) { + public static Rectangle2D rectangleBBox(List positions) { - return rectangles.stream().map(RectangleTransformations::toRectangle2D).collect(new Rectangle2DBBoxCollector()); - } - - - public static Rectangle2D toRectangle2D(Rectangle redactionLogRectangle) { - - return new Rectangle2D.Double(redactionLogRectangle.getTopLeft().getX(), - redactionLogRectangle.getTopLeft().getY() + redactionLogRectangle.getHeight(), - redactionLogRectangle.getWidth(), - -redactionLogRectangle.getHeight()); + return positions.stream().map(Position::toRectangle2D).collect(new Rectangle2DBBoxCollector()); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/DocumineFloraTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/DocumineFloraTest.java index 237f58a8..fb8531b4 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/DocumineFloraTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/DocumineFloraTest.java @@ -48,7 +48,7 @@ public class DocumineFloraTest extends AbstractRedactionIntegrationTest { // @Disabled public void titleExtraction() throws IOException { - AnalyzeRequest request = uploadFileToStorage("files/Documine/Flora/A8591B/15-Curacron_ToxicidadeAgudaOral.pdf"); + AnalyzeRequest request = uploadFileToStorage("files/Documine/Flora/402Study-ocred.pdf"); // AnalyzeRequest request = prepareStorage("files/Documine/Flora/ProblemDocs/SOLICITA_VICTRATO-GOLD-II_Item 21_Mutacao_Genica (1).pdf", // "files/Documine/Flora/ProblemDocs/SOLICITA_VICTRATO-GOLD-II_Item 21_Mutacao_Genica (1).TABLES.json"); @@ -58,7 +58,7 @@ public class DocumineFloraTest extends AbstractRedactionIntegrationTest { System.out.println("Finished structure analysis"); AnalyzeResult result = analyzeService.analyze(request); System.out.println("Finished analysis"); - var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); + var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID); var componentLog = redactionStorageService.getComponentLog(TEST_DOSSIER_ID, TEST_FILE_ID); AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build()); @@ -84,7 +84,7 @@ public class DocumineFloraTest extends AbstractRedactionIntegrationTest { System.out.println("Finished structure analysis"); AnalyzeResult result = analyzeService.analyze(request); System.out.println("Finished analysis"); - var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); + var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID); AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build()); @@ -111,7 +111,7 @@ public class DocumineFloraTest extends AbstractRedactionIntegrationTest { System.out.println("Finished structure analysis"); AnalyzeResult result = analyzeService.analyze(request); System.out.println("Finished analysis"); - var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); + var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID); AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().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/RedactionIntegrationTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTest.java index 79933126..3ed2941b 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 @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; @@ -44,6 +45,9 @@ import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeRequ import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeResult; import com.iqser.red.service.persistence.service.v1.api.shared.model.FileAttribute; import com.iqser.red.service.persistence.service.v1.api.shared.model.RuleFileType; +import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLogEntry; +import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntryState; +import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntryType; import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.AnnotationStatus; import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.Comment; import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.ManualRedactions; @@ -59,7 +63,6 @@ import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemp import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.FileType; import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.type.Type; import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Point; -import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogEntry; import com.iqser.red.service.redaction.v1.server.annotate.AnnotateRequest; import com.iqser.red.service.redaction.v1.server.annotate.AnnotateResponse; import com.iqser.red.service.redaction.v1.server.model.document.TextRange; @@ -161,11 +164,11 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request); AnalyzeResult result = analyzeService.analyze(request); - Map> duplicates = new HashMap<>(); + Map> duplicates = new HashMap<>(); - var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); + var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID); - redactionLog.getRedactionLogEntry().forEach(entry -> { + entityLog.getEntityLogEntry().forEach(entry -> { duplicates.computeIfAbsent(entry.getId(), v -> new ArrayList<>()).add(entry); }); @@ -202,9 +205,9 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request); AnalyzeResult result = analyzeService.analyze(request); - var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); + var redactionLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID); - var values = redactionLog.getRedactionLogEntry().stream().map(RedactionLogEntry::getValue).collect(Collectors.toList()); + var values = redactionLog.getEntityLogEntry().stream().map(EntityLogEntry::getValue).collect(Collectors.toList()); assertThat(values).containsExactlyInAnyOrder("Lastname M.", "Doe", "Doe J.", "M. Mustermann", "Mustermann M.", "F. Lastname"); } @@ -213,13 +216,13 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { @Test public void titleExtraction() throws IOException { - AnalyzeRequest request = uploadFileToStorage("files/new/crafted document.pdf"); + AnalyzeRequest request = uploadFileToStorage("files/Metolachlor/S-Metolachlor_RAR_02_Volume_2_2018-09-06.pdf"); System.out.println("Start Full integration test"); analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request); System.out.println("Finished structure analysis"); AnalyzeResult result = analyzeService.analyze(request); System.out.println("Finished analysis"); - var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); + var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID); AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build()); @@ -257,7 +260,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request); analyzeService.analyze(request); - var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); + var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID); var toRemove = IdRemoval.builder() .annotationId("c630599611e6e3db314518374bcf70f7") @@ -272,13 +275,13 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { request.setManualRedactions(manualRedactions); analyzeService.reanalyze(request); - var mergedRedactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); + var mergedEntityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID); - var cbiAddressBeforeHintRemoval = redactionLog.getRedactionLogEntry().stream().filter(re -> re.getType().equalsIgnoreCase("CBI_Address")).findAny().get(); - assertThat(cbiAddressBeforeHintRemoval.isRedacted()).isFalse(); + var cbiAddressBeforeHintRemoval = entityLog.getEntityLogEntry().stream().filter(re -> re.getType().equalsIgnoreCase("CBI_Address")).findAny().get(); + assertThat(cbiAddressBeforeHintRemoval.getState().equals(EntryState.APPLIED)).isFalse(); - var cbiAddressAfterHintRemoval = mergedRedactionLog.getRedactionLogEntry().stream().filter(re -> re.getType().equalsIgnoreCase("CBI_Address")).findAny().get(); - assertThat(cbiAddressAfterHintRemoval.isRedacted()).isTrue(); + var cbiAddressAfterHintRemoval = mergedEntityLog.getEntityLogEntry().stream().filter(re -> re.getType().equalsIgnoreCase("CBI_Address")).findAny().get(); + assertThat(cbiAddressAfterHintRemoval.getState().equals(EntryState.APPLIED)).isTrue(); } @@ -303,11 +306,11 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { AnalyzeResult result = analyzeService.analyze(request); System.out.println("analysis analysis duration: " + (System.currentTimeMillis() - fstart)); - Map> duplicates = new HashMap<>(); + Map> duplicates = new HashMap<>(); - var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); + var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID); - redactionLog.getRedactionLogEntry().forEach(entry -> { + entityLog.getEntityLogEntry().forEach(entry -> { duplicates.computeIfAbsent(entry.getId(), v -> new ArrayList<>()).add(entry); }); @@ -352,7 +355,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request); AnalyzeResult result = analyzeService.analyze(request); - var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); + var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID); var documentGraph = DocumentGraphMapper.toDocumentGraph(redactionStorageService.getDocumentData(TEST_DOSSIER_ID, TEST_FILE_ID)); long end = System.currentTimeMillis(); @@ -365,15 +368,15 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { int correctFound = 0; loop: - for (RedactionLogEntry redactionLogEntry : redactionLog.getRedactionLogEntry()) { + for (EntityLogEntry entityLogEntry : entityLog.getEntityLogEntry()) { for (Section section : documentGraph.getMainSections()) { - if (redactionLogEntry.isImage()) { + if (entityLogEntry.getEntryType().equals(EntryType.IMAGE)) { correctFound++; continue loop; } - if (redactionLogEntry.getSectionNumber() == section.getTreeId().get(0)) { - String value = section.getTextBlock().subSequence(new TextRange(redactionLogEntry.getStartOffset(), redactionLogEntry.getEndOffset())).toString(); - if (redactionLogEntry.getValue().equalsIgnoreCase(value)) { + if (Objects.equals(entityLogEntry.getContainingNodeId().get(0), section.getTreeId().get(0))) { + String value = section.getTextBlock().subSequence(new TextRange(entityLogEntry.getStartOffset(), entityLogEntry.getEndOffset())).toString(); + if (entityLogEntry.getValue().equalsIgnoreCase(value)) { correctFound++; } else { throw new RuntimeException("WTF"); @@ -414,7 +417,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { AnalyzeResult reanalyzeResult = analyzeService.reanalyze(request); - redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); + entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID); end = System.currentTimeMillis(); System.out.println("reanalysis analysis duration: " + (end - start)); @@ -434,7 +437,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { analyzeService.reanalyze(request); - redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); + entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID); System.out.println("Output file:" + outputFileName); } @@ -468,9 +471,9 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { mockDictionaryCalls(3L); analyzeService.reanalyze(request); - var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); + var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID); - var changes = redactionLog.getRedactionLogEntry().stream().filter(entry -> entry.getValue() != null && entry.getValue().equals("report")).findFirst().get().getChanges(); + var changes = entityLog.getEntityLogEntry().stream().filter(entry -> entry.getValue() != null && entry.getValue().equals("report")).findFirst().get().getChanges(); assertThat(changes.size()).isEqualTo(2); @@ -507,7 +510,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request); AnalyzeResult result = analyzeService.analyze(request); - var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); + var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID); var documentGraph = DocumentGraphMapper.toDocumentGraph(redactionStorageService.getDocumentData(TEST_DOSSIER_ID, TEST_FILE_ID)); long end = System.currentTimeMillis(); @@ -518,13 +521,12 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { fileOutputStream.write(objectMapper.writeValueAsBytes(redactionStorageService.getDocumentData(TEST_DOSSIER_ID, TEST_FILE_ID))); } - List valuesInDocument = redactionLog.getRedactionLogEntry() - .stream() - .filter(e -> !e.isImage()) + List valuesInDocument = entityLog.getEntityLogEntry() + .stream().filter(e -> !e.getEntryType().equals(EntryType.IMAGE)) .map(redactionLogEntry -> new TextRange(redactionLogEntry.getStartOffset(), redactionLogEntry.getEndOffset())) .map(boundary -> documentGraph.getTextBlock().subSequence(boundary).toString()) .toList(); - List valuesInRedactionLog = redactionLog.getRedactionLogEntry().stream().filter(e -> !e.isImage()).map(RedactionLogEntry::getValue).toList(); + List valuesInRedactionLog = entityLog.getEntityLogEntry().stream().filter(e -> !e.getEntryType().equals(EntryType.IMAGE)).map(EntityLogEntry::getValue).toList(); assertEquals(valuesInRedactionLog, valuesInDocument); @@ -562,7 +564,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { AnalyzeResult reanalyzeResult = analyzeService.reanalyze(request); - redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); + entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID); end = System.currentTimeMillis(); System.out.println("reanalysis analysis duration: " + (end - start)); @@ -583,7 +585,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { analyzeService.reanalyze(request); - redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); + entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID); System.out.println("Output file:" + outputFileName); } @@ -810,8 +812,8 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { fileOutputStream.write(annotateResponse.getDocument()); } - var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); - assertThat(redactionLog.getRedactionLogEntry().size()).isEqualTo(5); + var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID); + assertThat(entityLog.getEntityLogEntry().size()).isEqualTo(5); long end = System.currentTimeMillis(); @@ -857,7 +859,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request); AnalyzeResult result = analyzeService.analyze(request); - var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); + var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID); AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder() .manualRedactions(manualRedactions) @@ -885,10 +887,10 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request); AnalyzeResult result = analyzeService.analyze(request); - var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); + var redactionLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID); - redactionLog.getRedactionLogEntry().forEach(entry -> { - if (!entry.isHint()) { + redactionLog.getEntityLogEntry().forEach(entry -> { + if (!entry.getEntryType().equals(EntryType.HINT)) { assertThat(entry.getReason()).isEqualTo("Not redacted because it's row does not belong to a vertebrate study"); } }); @@ -1048,7 +1050,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request); AnalyzeResult result = analyzeService.analyze(request); - var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); + var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID); AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build()); @@ -1056,7 +1058,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { fileOutputStream.write(annotateResponse.getDocument()); } - redactionLog.getRedactionLogEntry().forEach(entry -> { + entityLog.getEntityLogEntry().forEach(entry -> { if (entry.getValue() == null) { return; } @@ -1094,8 +1096,8 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { fileOutputStream.write(annotateResponse.getDocument()); } - var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); - var values = redactionLog.getRedactionLogEntry().stream().map(RedactionLogEntry::getValue).collect(Collectors.toList()); + var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID); + var values = entityLog.getEntityLogEntry().stream().map(EntityLogEntry::getValue).collect(Collectors.toList()); assertThat(values).contains("Mrs. Robinson"); assertThat(values).contains("Mr. Bojangles"); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationV2Test.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationV2Test.java index 45909ba4..5aff0602 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationV2Test.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationV2Test.java @@ -22,9 +22,11 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeRequest; import com.iqser.red.service.persistence.service.v1.api.shared.model.RuleFileType; +import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Engine; +import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLogEntry; +import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntryState; import com.iqser.red.service.persistence.service.v1.api.shared.model.common.JSONPrimitive; import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.type.Type; -import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogEntry; import com.iqser.red.storage.commons.StorageAutoConfiguration; import com.iqser.red.storage.commons.service.StorageService; import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsingType; @@ -120,22 +122,20 @@ public class RedactionIntegrationV2Test extends AbstractRedactionIntegrationTest analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request); analyzeService.analyze(request); - var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); + var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID); - assertThat(redactionLog.getRedactionLogEntry().size()).isEqualTo(1); + assertThat(entityLog.getEntityLogEntry().size()).isEqualTo(1); - RedactionLogEntry redactionLogEntry = redactionLog.getRedactionLogEntry().get(0); + EntityLogEntry redactionLogEntry = entityLog.getEntityLogEntry().get(0); assertThat(redactionLogEntry.getType()).isEqualTo(DICTIONARY_AUTHOR); assertThat(redactionLogEntry.getValue()).isEqualTo(entryAuthorAndPIIDictionary); - assertThat(redactionLogEntry.isRedacted()).isEqualTo(true); - assertThat(redactionLogEntry.isRecommendation()).isEqualTo(false); - assertThat(redactionLogEntry.isFalsePositive()).isEqualTo(false); + assertThat(redactionLogEntry.getState()).isEqualTo(EntryState.APPLIED); assertThat(redactionLogEntry.isExcluded()).isEqualTo(false); assertThat(redactionLogEntry.isDictionaryEntry()).isEqualTo(true); assertThat(redactionLogEntry.getEngines().size()).isEqualTo(1); - assertThat(redactionLogEntry.getEngines().contains(com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Engine.DICTIONARY)).isEqualTo(true); + assertThat(redactionLogEntry.getEngines().contains(Engine.DICTIONARY)).isEqualTo(true); } @@ -158,22 +158,20 @@ public class RedactionIntegrationV2Test extends AbstractRedactionIntegrationTest analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request); analyzeService.analyze(request); - var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); + var redactionLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID); - assertThat(redactionLog.getRedactionLogEntry().size()).isEqualTo(1); + assertThat(redactionLog.getEntityLogEntry().size()).isEqualTo(1); - RedactionLogEntry redactionLogEntry = redactionLog.getRedactionLogEntry().get(0); + EntityLogEntry redactionLogEntry = redactionLog.getEntityLogEntry().get(0); assertThat(redactionLogEntry.getType()).isEqualTo(DICTIONARY_AUTHOR); assertThat(redactionLogEntry.getValue()).isEqualTo(entryAuthorDictionary); - assertThat(redactionLogEntry.isRedacted()).isEqualTo(true); - assertThat(redactionLogEntry.isRecommendation()).isEqualTo(false); - assertThat(redactionLogEntry.isFalsePositive()).isEqualTo(false); + assertThat(redactionLogEntry.getState()).isEqualTo(EntryState.APPLIED); assertThat(redactionLogEntry.isExcluded()).isEqualTo(false); assertThat(redactionLogEntry.isDictionaryEntry()).isEqualTo(true); assertThat(redactionLogEntry.getEngines().size()).isEqualTo(1); - assertThat(redactionLogEntry.getEngines().contains(com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Engine.DICTIONARY)).isEqualTo(true); + assertThat(redactionLogEntry.getEngines().contains(Engine.DICTIONARY)).isEqualTo(true); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/annotate/AnnotationService.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/annotate/AnnotationService.java index 98875593..c98aedda 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/annotate/AnnotationService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/annotate/AnnotationService.java @@ -1,11 +1,9 @@ package com.iqser.red.service.redaction.v1.server.annotate; -import java.awt.Color; import java.awt.geom.Rectangle2D; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; -import java.util.GregorianCalendar; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -15,26 +13,20 @@ import java.util.stream.Stream; import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; -import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.apache.pdfbox.pdmodel.font.PDType1Font; -import org.apache.pdfbox.pdmodel.font.Standard14Fonts; import org.apache.pdfbox.pdmodel.graphics.color.PDColor; import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceRGB; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationHighlight; -import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationText; import org.springframework.stereotype.Service; import com.google.common.primitives.Floats; +import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLog; +import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLogEntry; +import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntryState; +import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntryType; +import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Position; import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.FileType; -import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Rectangle; -import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLog; -import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogComment; -import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogEntry; -import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.section.CellRectangle; -import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.section.SectionGrid; -import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.section.SectionRectangle; import com.iqser.red.service.redaction.v1.server.service.DictionaryService; import com.iqser.red.service.redaction.v1.server.storage.RedactionStorageService; import com.iqser.red.service.redaction.v1.server.utils.RectangleTransformations; @@ -67,17 +59,15 @@ public class AnnotationService { public AnnotateResponse annotate(AnnotateRequest annotateRequest) { var storedObjectFile = redactionStorageService.getStoredObjectFile(RedactionStorageService.StorageIdUtils.getStorageId(annotateRequest.getDossierId(), - annotateRequest.getFileId(), - FileType.ORIGIN)); + annotateRequest.getFileId(), FileType.VIEWER_DOCUMENT)); - var redactionLog = redactionStorageService.getRedactionLog(annotateRequest.getDossierId(), annotateRequest.getFileId()); - var sectionsGrid = redactionStorageService.getSectionGrid(annotateRequest.getDossierId(), annotateRequest.getFileId()); + var entityLog = redactionStorageService.getEntityLog(annotateRequest.getDossierId(), annotateRequest.getFileId()); try (PDDocument pdDocument = Loader.loadPDF(storedObjectFile)) { pdDocument.setAllSecurityToBeRemoved(true); dictionaryService.updateDictionary(annotateRequest.getDossierTemplateId(), annotateRequest.getDossierId()); - annotate(pdDocument, redactionLog, sectionsGrid); + annotate(pdDocument, entityLog); try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { pdDocument.save(byteArrayOutputStream); @@ -90,20 +80,15 @@ public class AnnotationService { } - private void annotate(PDDocument document, RedactionLog redactionLog, SectionGrid sectionGrid) throws IOException { + private void annotate(PDDocument document, EntityLog entityLog) throws IOException { - Map> redactionLogPerPage = convertRedactionLog(redactionLog); + Map> redactionLogPerPage = groupingByPageNumber(entityLog); for (int page = 1; page <= document.getNumberOfPages(); page++) { PDPage pdPage = document.getPage(page - 1); - List sectionRectangles = sectionGrid.getRectanglesPerPage().get(page); - if (sectionRectangles != null && !sectionRectangles.isEmpty()) { - drawSectionGrid(document, pdPage, sectionRectangles); - } - - List logEntries = redactionLogPerPage.get(page); + List logEntries = redactionLogPerPage.get(page); if (logEntries != null && !logEntries.isEmpty()) { addAnnotations(logEntries, pdPage, page); } @@ -111,19 +96,67 @@ public class AnnotationService { } - private void addAnnotations(List logEntries, PDPage pdPage, int page) throws IOException { + private Map> groupingByPageNumber(EntityLog redactionLog) { + + Map> redactionLogPerPage = new HashMap<>(); + if (redactionLog == null) { + return redactionLogPerPage; + } + for (EntityLogEntry entry : redactionLog.getEntityLogEntry()) { + int page = 0; + for (Position position : entry.getPositions()) { + if (position.getPageNumber() != page) { + redactionLogPerPage.computeIfAbsent(position.getPageNumber(), x -> new ArrayList<>()).add(entry); + page = position.getPageNumber(); + } + } + } + return redactionLogPerPage; + } + + + private void addAnnotations(List logEntries, PDPage pdPage, int page) throws IOException { List annotations = pdPage.getAnnotations(); - for (RedactionLogEntry entry : logEntries) { - if (entry.lastChangeIsRemoved()) { + for (EntityLogEntry entry : logEntries) { + if (entry.getState().equals(EntryState.REMOVED)) { continue; } annotations.addAll(createAnnotation(entry, page, pdPage.getRotation(), pdPage.getCropBox())); } } - public static PDRectangle toPDRectangleBBox(List rectangles) { + + private List createAnnotation(EntityLogEntry redactionLogEntry, int page, int rotation, PDRectangle cropBox) { + + List annotations = new ArrayList<>(); + + List rectangles = redactionLogEntry.getPositions().stream().filter(pos -> pos.getPageNumber() == page).collect(Collectors.toList()); + + if (rectangles.isEmpty()) { + return annotations; + } + + PDAnnotationHighlight annotation = new PDAnnotationHighlight(); + annotation.constructAppearances(); + PDRectangle pdRectangle = toPDRectangleBBox(rectangles); + annotation.setRectangle(pdRectangle); + annotation.setQuadPoints(Floats.toArray(toQuadPoints(rectangles))); + if (!(redactionLogEntry.getEntryType().equals(EntryType.HINT) || redactionLogEntry.getState().equals(EntryState.IGNORED))) { + annotation.setContents(redactionLogEntry.getValue() + " " + createAnnotationContent(redactionLogEntry)); + } + annotation.setTitlePopup(redactionLogEntry.getId()); + annotation.setAnnotationName(redactionLogEntry.getId()); + annotation.setColor(new PDColor(redactionLogEntry.getColor(), PDDeviceRGB.INSTANCE)); + annotation.setNoRotate(false); + annotations.add(annotation); + + return annotations; + } + + + public static PDRectangle toPDRectangleBBox(List rectangles) { Rectangle2D rectangle2D = RectangleTransformations.rectangleBBox(rectangles); @@ -135,69 +168,12 @@ public class AnnotationService { return annotationPosition; } - private List createAnnotation(RedactionLogEntry redactionLogEntry, int page, int rotation, PDRectangle cropBox) { - List annotations = new ArrayList<>(); + public static List toQuadPoints(List rectangles) { - List rectangles = redactionLogEntry.getPositions().stream().filter(pos -> pos.getPage() == page).collect(Collectors.toList()); - - if (rectangles.isEmpty()) { - return annotations; - } - - PDAnnotationHighlight annotation = new PDAnnotationHighlight(); - annotation.constructAppearances(); - PDRectangle pdRectangle = toPDRectangleBBox(rectangles); - annotation.setRectangle(pdRectangle); - annotation.setQuadPoints(Floats.toArray(toQuadPoints(rectangles))); - if (!redactionLogEntry.isHint()) { - annotation.setContents(redactionLogEntry.getValue() + " " + createAnnotationContent(redactionLogEntry)); - } - annotation.setTitlePopup(redactionLogEntry.getId()); - annotation.setAnnotationName(redactionLogEntry.getId()); - annotation.setColor(new PDColor(redactionLogEntry.getColor(), PDDeviceRGB.INSTANCE)); - annotation.setNoRotate(false); - annotations.add(annotation); - - if (redactionLogEntry.getComments() != null) { - for (RedactionLogComment comment : redactionLogEntry.getComments()) { - PDAnnotationText txtAnnot = new PDAnnotationText(); - txtAnnot.setAnnotationName(String.valueOf(comment.getId())); - txtAnnot.setInReplyTo(annotation); // Reference to highlight annotation - txtAnnot.setName(PDAnnotationText.NAME_COMMENT); - txtAnnot.setCreationDate(GregorianCalendar.from(comment.getDate().toZonedDateTime())); - txtAnnot.setTitlePopup(comment.getUser()); - txtAnnot.setContents(comment.getText()); - txtAnnot.setRectangle(pdRectangle); - annotations.add(txtAnnot); - } - } - - return annotations; + return rectangles.stream().map(Position::toRectangle2D).flatMap(AnnotationService::toQuadPoints).toList(); } - - private String createAnnotationContent(RedactionLogEntry redactionLogEntry) { - - return redactionLogEntry.getType() + " \nRule " + redactionLogEntry.getMatchedRule() + " matched\n\n" + redactionLogEntry.getReason() + "\n\nLegal basis:" + redactionLogEntry.getLegalBasis() + "\n\nIn section: \"" + redactionLogEntry.getSection() + "\""; - } - - - public static List toQuadPoints(List rectangles) { - - return rectangles.stream().map(AnnotationService::toRectangle2D).flatMap(AnnotationService::toQuadPoints).toList(); - } - - - private static Rectangle2D toRectangle2D(Rectangle redactionLogRectangle) { - - return new Rectangle2D.Double(redactionLogRectangle.getTopLeft().getX(), - redactionLogRectangle.getTopLeft().getY(), - redactionLogRectangle.getWidth(), - redactionLogRectangle.getHeight()); - } - - public static Stream toQuadPoints(Rectangle2D rectangle) { double x1 = rectangle.getMinX(); @@ -215,71 +191,9 @@ public class AnnotationService { } - private void drawSectionGrid(PDDocument document, PDPage pdPage, List sectionRectangles) throws IOException { + private String createAnnotationContent(EntityLogEntry redactionLogEntry) { - PDPageContentStream contentStream = new PDPageContentStream(document, pdPage, PDPageContentStream.AppendMode.APPEND, true); - for (SectionRectangle sectionRectangle : sectionRectangles) { - drawSectionRectangle(contentStream, sectionRectangle); - drawSectionPartNumberText(contentStream, sectionRectangle); - drawTableCells(contentStream, sectionRectangle); - } - contentStream.close(); - } - - - private void drawSectionRectangle(PDPageContentStream contentStream, SectionRectangle sectionRectangle) throws IOException { - - contentStream.setStrokingColor(Color.LIGHT_GRAY); - contentStream.setLineWidth(0.5f); - contentStream.addRect(sectionRectangle.getTopLeft().getX(), sectionRectangle.getTopLeft().getY(), sectionRectangle.getWidth(), sectionRectangle.getHeight()); - contentStream.stroke(); - } - - - private void drawSectionPartNumberText(PDPageContentStream contentStream, SectionRectangle sectionRectangle) throws IOException { - - contentStream.beginText(); - contentStream.setNonStrokingColor(Color.DARK_GRAY); - contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.TIMES_ROMAN), 8f); - if (sectionRectangle.getTableCells() == null) { - contentStream.newLineAtOffset(sectionRectangle.getTopLeft().getX(), sectionRectangle.getTopLeft().getY() + sectionRectangle.getHeight()); - } else { - contentStream.newLineAtOffset(sectionRectangle.getTopLeft().getX(), sectionRectangle.getTopLeft().getY()); - } - contentStream.showText(sectionRectangle.getPart() + "/" + sectionRectangle.getNumberOfParts()); - contentStream.endText(); - } - - - private void drawTableCells(PDPageContentStream contentStream, SectionRectangle sectionRectangle) throws IOException { - - if (sectionRectangle.getTableCells() != null) { - for (CellRectangle cell : sectionRectangle.getTableCells()) { - contentStream.setLineWidth(0.5f); - contentStream.setStrokingColor(Color.CYAN); - contentStream.addRect(cell.getTopLeft().getX(), cell.getTopLeft().getY(), cell.getWidth(), cell.getHeight()); - contentStream.stroke(); - } - } - } - - - private Map> convertRedactionLog(RedactionLog redactionLog) { - - Map> redactionLogPerPage = new HashMap<>(); - if (redactionLog == null) { - return redactionLogPerPage; - } - for (RedactionLogEntry entry : redactionLog.getRedactionLogEntry()) { - int page = 0; - for (Rectangle position : entry.getPositions()) { - if (position.getPage() != page) { - redactionLogPerPage.computeIfAbsent(position.getPage(), x -> new ArrayList<>()).add(entry); - page = position.getPage(); - } - } - } - return redactionLogPerPage; + return redactionLogEntry.getType() + " \nRule " + redactionLogEntry.getMatchedRule() + " matched\n\n" + redactionLogEntry.getReason() + "\n\nLegal basis:" + redactionLogEntry.getLegalBasis() + "\n\nIn section: \"" + redactionLogEntry.getSection() + "\""; } } diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/resources/dictionaries/PII.txt b/redaction-service-v1/redaction-service-server-v1/src/test/resources/dictionaries/PII.txt index 1a480892..cbcbfe3d 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/resources/dictionaries/PII.txt +++ b/redaction-service-v1/redaction-service-server-v1/src/test/resources/dictionaries/PII.txt @@ -29,4 +29,4 @@ J.B. RASCLE 青森植 サンプル量 供試試料 (無処理 区) -材料 +材料 \ No newline at end of file diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/resources/dictionaries/PII_false_positive.txt b/redaction-service-v1/redaction-service-server-v1/src/test/resources/dictionaries/PII_false_positive.txt index cd9fab48..492ca143 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/resources/dictionaries/PII_false_positive.txt +++ b/redaction-service-v1/redaction-service-server-v1/src/test/resources/dictionaries/PII_false_positive.txt @@ -1 +1 @@ -C. J. Alfred Xinyi \ No newline at end of file +C. J. Alfred XinyiP \ No newline at end of file diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/resources/files/Documine/Flora/402Study.pdf b/redaction-service-v1/redaction-service-server-v1/src/test/resources/files/Documine/Flora/402Study-ocred.pdf similarity index 97% rename from redaction-service-v1/redaction-service-server-v1/src/test/resources/files/Documine/Flora/402Study.pdf rename to redaction-service-v1/redaction-service-server-v1/src/test/resources/files/Documine/Flora/402Study-ocred.pdf index 289b46cfde062e60b7c7bb6d8030c2e130d49cf8..6ad1b53f615d91068758365e161f981838c9b18f 100644 GIT binary patch delta 59188 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 delta 7280 zcmc&(3s4m29gh!;T`|E2V2Hwp0mMhx_X{0HJdhxq;vH=iF=T}WPtQ9%7Nj(t)$wr? zC03m#r!!_^F%6TL)v)TJKx3K=I!CWk=(fL*GUJdWV1$rxVdao03f z%kUcZ+hebns(n9dN_)z8s%Q9EpQS4yF{xB`)uAPX zNHA<(9%xXY7FKvS9ZUCbIyUk4yW`PK1CzaXW<%HUjVj4VQbSwl>#^pYJv4P-=#aP} z;Pe^G#ft6Yt}aZesHn)!_`4_3v*vHFm3BXWEB-`xX=1B&WcRJ^*IG}E-DvAN+&T8f z>M05PUf!QDCw|LMhn%3EIR0eU)S2T7uTL_;QNaejzSy;~9$~7~GKk@9;)Zb$A z{K)(9q5I*FMwhR?DeV~(j#!pt=@3V2_N?DCsB_QY(Jz^BB!uqp-Eed%#L?{3-(j+^ zXHLebd?IwIekO*Y7@i0b<8@XIhGU4|SvAx<&3AH6#u$T}NASUgmJO)QY#7!{hRwZ_3Q42~!LZLPxx6$Nm?SHQ}W^^<3-7L$8C8JXs=l-T5u4GB!l<~=*UAUA}kbb|J$n;c?%s;~8h!uh&&?XuB@FRTlq7$Jt%qugYyn{dbTTaiDmi z1(G)K(wQ{U+nMwf98dc@lMsE3yS9Ey$IkV0Do^eGWbT;dmr~l}nhtiY+*<5Cf3hWUa>|(N+okOK*N+t4 z{B(Lp{-1nbl*niOU$1*E|4c>cr1IUhPVVyWlXG)ZZ)5hTG(k}VMwiG^f^J4Tx)QuQ zx<X@;0&bRG~-Oqv;E8&MUj+V&v@JSjf3MEI8yxW`=0$}wHaS}e2CyH zEXl#3Wpv3^pU2fLR+3kmxMWuQt8ByJm1`!rT$MGa(wl$vQsKs|P3zP1?BwGq#KBlV7nOVWmFyMHqtMTndj@+5Cu@_0BV zz>(^YOHP|oWkMIN$A<{I`fy3k)FqEax@6bQ`G43@Hqn=5Q}WL1LH=-PtW~pUL;F0Jfrhnk~4r zhz2Nx+hA7&+aFcyf6)8_1|TKDdGr^nT;q^txtqZvlpa};$q`vm^k|5et6I}FkO*~vJV3=zI6(Em z5TfW9g%CEGF%atUD`@R80jm9=LmeSW!2UA|V_y(eU5g}&X;C1lm>@|#?A#A!Y>;Z5 z{D78fKuH5KXu=O|s-$M)x~-}KV}*@<3{{ePz<6|&u|cX4{%AnS0W#<~506qabRA^? zBx3?n4ZJ-eqtbs&i;bhkPM*JN3D5i)1rqoR6UWpG?qP~sz+@= zbyTBEK~_-WsA&L;P+GMC%^!efFz_|BHl?K^8_;644QSB}erNz-HP)NIg2ov#AyY>Q zovOmj7d%$GFVIZR1~i@6`qjO2Cs(oP*XrU~U zB01f42ud9+bLj{q%^tZHBLI9OPlO)8u`JX?EeOpalF$QO+8jkwjL5>n4Rh#6ii5ku z2gcukAo_(S!WR?pWj>1{H+u99SyF(R#4v~CI5FZ7iem|rwM_UK@LLQhNJcJZ2wq^I z^BWFkQ~E9^`l1((n>N6M+)ILM2lGl+BbR(ax@!pFi(jj z8{AM=6UX!6mm#4XjSSJSpGt}>$suPpOu~bC_-oi4k`s_48s*TAZ1@GyS_)-|a5s^Q!dlUWEFXPAj*C(g1Z<#M@kkD*>_J}$+8r%2ava>|b}N9^q6&r= gyK3F(02M_r%wr9TAJVV$rvLx| -- 2.47.2 From b78f6d2d0f2b23028b24601935f09f01012def7d Mon Sep 17 00:00:00 2001 From: Kilian Schuettler Date: Thu, 12 Oct 2023 12:30:43 +0200 Subject: [PATCH 2/6] DM-483: endless loops with false positive * fix byNerEntity always creating a new Entity and therefore enabling endless loops Signed-off-by: Kilian Schuettler --- .../document/EntityCreationService.java | 149 ++++++++++-------- .../v1/server/DocumineFloraTest.java | 8 + 2 files changed, 92 insertions(+), 65 deletions(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/EntityCreationService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/EntityCreationService.java index cff4eb6d..b772288c 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/EntityCreationService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/EntityCreationService.java @@ -36,6 +36,7 @@ import com.iqser.red.service.redaction.v1.server.utils.RedactionSearchUtility; import com.iqser.red.service.redaction.v1.server.model.NerEntities; import com.iqser.red.service.redaction.v1.server.model.dictionary.SearchImplementation; import com.iqser.red.service.redaction.v1.server.utils.IdBuilder; +import com.iqser.red.service.redaction.v1.server.utils.exception.NotFoundException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -250,20 +251,66 @@ public class EntityCreationService { public Stream bySearchImplementation(SearchImplementation searchImplementation, String type, EntityType entityType, SemanticNode node) { - return searchImplementation.getBoundaries(node.getTextBlock(), node.getTextRange()) - .stream().filter(boundary -> isValidEntityTextRange(node.getTextBlock(), boundary)) + return searchImplementation.getBoundaries(node.getTextBlock(), node.getTextRange()).stream().filter(boundary -> isValidEntityTextRange(node.getTextBlock(), boundary)) .map(bounds -> byTextRange(bounds, type, entityType, node)) .filter(Optional::isPresent) .map(Optional::get); } + /** + * Creates a redaction entity based on the given boundary, type, entity type, and semantic node. + * If the document already contains an equal redaction entity, then the original Entity is returned. + * Also inserts the Entity into the kieSession. + * + * @param textRange The boundary of the redaction entity. + * @param type The type of the redaction entity. + * @param entityType The entity type of the redaction entity. + * @param node The semantic node to associate with the redaction entity. + * @return An Optional containing the redaction entity, or the previous entity if the entity already exists. + */ + public Optional byTextRange(TextRange textRange, String type, EntityType entityType, SemanticNode node) { + + return byTextRangeWithEngine(textRange, type, entityType, node, Set.of(Engine.RULE)); + } + + + /** + * Creates a redaction entity based on the given boundary, type, entity type, and semantic node. + * If the document already contains an equal redaction entity, then the original Entity is returned. + * Also inserts the Entity into the kieSession. + * + * @param textRange The boundary of the redaction entity. + * @param type The type of the redaction entity. + * @param entityType The entity type of the redaction entity. + * @param node The semantic node to associate with the redaction entity. + * @return An Optional containing the redaction entity, or the previous entity if the entity already exists. + */ + public Optional byTextRangeWithEngine(TextRange textRange, String type, EntityType entityType, SemanticNode node, Set engines) { + + if (!node.getTextRange().contains(textRange)) { + throw new IllegalArgumentException(String.format("%s is not in the %s of the provided semantic node %s", textRange, node.getTextRange(), node)); + } + TextRange trimmedTextRange = textRange.trim(node.getTextBlock()); + TextEntity entity = TextEntity.initialEntityNode(trimmedTextRange, type, entityType); + if (node.getEntities().contains(entity)) { + return node.getEntities().stream().filter(entity::equals).peek(e -> e.addEngines(engines)).findAny(); + } + addEntityToGraph(entity, node); + entity.addEngines(engines); + insertToKieSession(entity); + return Optional.of(entity); + } + + public Stream lineAfterStrings(List strings, String type, EntityType entityType, SemanticNode node) { TextBlock textBlock = node.getTextBlock(); SearchImplementation searchImplementation = new SearchImplementation(strings, false); return searchImplementation.getBoundaries(textBlock, node.getTextRange()) - .stream().map(boundary -> toLineAfterTextRange(textBlock, boundary)).filter(boundary -> isValidEntityTextRange(textBlock, boundary)) + .stream() + .map(boundary -> toLineAfterTextRange(textBlock, boundary)) + .filter(boundary -> isValidEntityTextRange(textBlock, boundary)) .map(boundary -> byTextRange(boundary, type, entityType, node)) .filter(Optional::isPresent) .map(Optional::get); @@ -275,29 +322,9 @@ public class EntityCreationService { TextBlock textBlock = node.getTextBlock(); SearchImplementation searchImplementation = new SearchImplementation(strings, true); return searchImplementation.getBoundaries(textBlock, node.getTextRange()) - .stream().map(boundary -> toLineAfterTextRange(textBlock, boundary)).filter(boundary -> isValidEntityTextRange(textBlock, boundary)) - .map(boundary -> byTextRange(boundary, type, entityType, node)) - .filter(Optional::isPresent) - .map(Optional::get); - } - - - public Stream lineAfterString(String string, String type, EntityType entityType, SemanticNode node) { - - TextBlock textBlock = node.getTextBlock(); - return RedactionSearchUtility.findTextRangesByString(string, textBlock) - .stream().map(boundary -> toLineAfterTextRange(textBlock, boundary)).filter(boundary -> isValidEntityTextRange(textBlock, boundary)) - .map(boundary -> byTextRange(boundary, type, entityType, node)) - .filter(Optional::isPresent) - .map(Optional::get); - } - - - public Stream lineAfterStringIgnoreCase(String string, String type, EntityType entityType, SemanticNode node) { - - TextBlock textBlock = node.getTextBlock(); - return RedactionSearchUtility.findTextRangesByStringIgnoreCase(string, textBlock) - .stream().map(boundary -> toLineAfterTextRange(textBlock, boundary)).filter(boundary -> isValidEntityTextRange(textBlock, boundary)) + .stream() + .map(boundary -> toLineAfterTextRange(textBlock, boundary)) + .filter(boundary -> isValidEntityTextRange(textBlock, boundary)) .map(boundary -> byTextRange(boundary, type, entityType, node)) .filter(Optional::isPresent) .map(Optional::get); @@ -520,31 +547,29 @@ public class EntityCreationService { } - /** - * Creates a redaction entity based on the given boundary, type, entity type, and semantic node. - * If the document already contains an equal redaction entity, then the original Entity is returned. - * Also inserts the Entity into the kieSession. - * - * @param textRange The boundary of the redaction entity. - * @param type The type of the redaction entity. - * @param entityType The entity type of the redaction entity. - * @param node The semantic node to associate with the redaction entity. - * @return An Optional containing the redaction entity, or the previous entity if the entity already exists. - */ - public Optional byTextRange(TextRange textRange, String type, EntityType entityType, SemanticNode node) { + public Stream lineAfterString(String string, String type, EntityType entityType, SemanticNode node) { - if (!node.getTextRange().contains(textRange)) { - throw new IllegalArgumentException(String.format("%s is not in the %s of the provided semantic node %s", textRange, node.getTextRange(), node)); - } - TextRange trimmedTextRange = textRange.trim(node.getTextBlock()); - TextEntity entity = TextEntity.initialEntityNode(trimmedTextRange, type, entityType); - if (node.getEntities().contains(entity)) { - return node.getEntities().stream().filter(entity::equals).peek(e -> e.addEngine(Engine.RULE)).findAny(); - } - addEntityToGraph(entity, node); - entity.addEngine(Engine.RULE); - insertToKieSession(entity); - return Optional.of(entity); + TextBlock textBlock = node.getTextBlock(); + return RedactionSearchUtility.findTextRangesByString(string, textBlock) + .stream() + .map(boundary -> toLineAfterTextRange(textBlock, boundary)) + .filter(boundary -> isValidEntityTextRange(textBlock, boundary)) + .map(boundary -> byTextRange(boundary, type, entityType, node)) + .filter(Optional::isPresent) + .map(Optional::get); + } + + + public Stream lineAfterStringIgnoreCase(String string, String type, EntityType entityType, SemanticNode node) { + + TextBlock textBlock = node.getTextBlock(); + return RedactionSearchUtility.findTextRangesByStringIgnoreCase(string, textBlock) + .stream() + .map(boundary -> toLineAfterTextRange(textBlock, boundary)) + .filter(boundary -> isValidEntityTextRange(textBlock, boundary)) + .map(boundary -> byTextRange(boundary, type, entityType, node)) + .filter(Optional::isPresent) + .map(Optional::get); } @@ -600,15 +625,14 @@ public class EntityCreationService { return newEntity; } + public TextEntity copyEntityWithoutRules(TextEntity entity, String type, EntityType entityType, SemanticNode node) { - TextEntity newEntity = TextEntity.initialEntityNode(entity.getTextRange(), type, entityType); - newEntity.addEngines(entity.getEngines()); + TextEntity newEntity = byTextRangeWithEngine(entity.getTextRange(), type, entityType, node, entity.getEngines()).orElseThrow(() -> new NotFoundException( + "No entity present!")); newEntity.getManualOverwrite().addChanges(entity.getManualOverwrite().getManualChangeLog()); newEntity.setDictionaryEntry(entity.isDictionaryEntry()); newEntity.setDossierDictionaryEntry(entity.isDossierDictionaryEntry()); - addEntityToGraph(newEntity, node); - insertToKieSession(newEntity); return newEntity; } @@ -623,27 +647,22 @@ public class EntityCreationService { public TextEntity byNerEntity(NerEntities.NerEntity nerEntity, EntityType entityType, SemanticNode semanticNode) { - var entity = forceByTextRange(nerEntity.textRange(), nerEntity.type(), entityType, semanticNode); - entity.addEngine(Engine.NER); - insertToKieSession(entity); - return entity; + return byTextRangeWithEngine(nerEntity.textRange(), nerEntity.type(), entityType, semanticNode, Engine.NER).orElseThrow(() -> new NotFoundException("No entity present!")); } public TextEntity byNerEntity(NerEntities.NerEntity nerEntity, String type, EntityType entityType, SemanticNode semanticNode) { - var entity = forceByTextRange(nerEntity.textRange(), type, entityType, semanticNode); - entity.addEngine(Engine.NER); - insertToKieSession(entity); - return entity; + return byTextRangeWithEngine(nerEntity.textRange(), type, entityType, semanticNode, Engine.NER).orElseThrow(() -> new NotFoundException("No entity present!")); } public Stream combineNerEntitiesToCbiAddressDefaults(NerEntities nerEntities, String type, EntityType entityType, SemanticNode semanticNode) { - return NerEntitiesAdapter.combineNerEntitiesToCbiAddressDefaults(nerEntities).map(boundary -> forceByTextRange(boundary, type, entityType, semanticNode)) - .peek(entity -> entity.addEngine(Engine.NER)) - .peek(this::insertToKieSession); + return NerEntitiesAdapter.combineNerEntitiesToCbiAddressDefaults(nerEntities) + .map(boundary -> byTextRangeWithEngine(boundary, type, entityType, semanticNode, Engine.NER)) + .filter(Optional::isPresent) + .map(Optional::get); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/DocumineFloraTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/DocumineFloraTest.java index fb8531b4..1550be69 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/DocumineFloraTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/DocumineFloraTest.java @@ -44,6 +44,13 @@ public class DocumineFloraTest extends AbstractRedactionIntegrationTest { private static final String COMPONENT_RULES = loadFromClassPath("drools/documine_flora_components.drl"); + @BeforeEach + public void setUpDictionaries() { + + loadDictionaryForTest(); + mockDictionaryCalls(0L); + } + @Test // @Disabled public void titleExtraction() throws IOException { @@ -55,6 +62,7 @@ public class DocumineFloraTest extends AbstractRedactionIntegrationTest { System.out.println("Start Full integration test"); analyzeDocumentStructure(LayoutParsingType.DOCUMINE, request); + System.out.println("Finished structure analysis"); AnalyzeResult result = analyzeService.analyze(request); System.out.println("Finished analysis"); -- 2.47.2 From 92ea6fdb9930fc1f095936a89355740c47a52530 Mon Sep 17 00:00:00 2001 From: Kilian Schuettler Date: Thu, 12 Oct 2023 12:37:16 +0200 Subject: [PATCH 3/6] DM-483: endless loops with false positive * fix byNerEntity always creating a new Entity and therefore enabling endless loops Signed-off-by: Kilian Schuettler --- .../v1/server/service/document/EntityCreationService.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/EntityCreationService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/EntityCreationService.java index b772288c..89b448c5 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/EntityCreationService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/EntityCreationService.java @@ -647,20 +647,21 @@ public class EntityCreationService { public TextEntity byNerEntity(NerEntities.NerEntity nerEntity, EntityType entityType, SemanticNode semanticNode) { - return byTextRangeWithEngine(nerEntity.textRange(), nerEntity.type(), entityType, semanticNode, Engine.NER).orElseThrow(() -> new NotFoundException("No entity present!")); + return byTextRangeWithEngine(nerEntity.textRange(), nerEntity.type(), entityType, semanticNode, Set.of(Engine.NER)).orElseThrow(() -> new NotFoundException( + "No entity present!")); } public TextEntity byNerEntity(NerEntities.NerEntity nerEntity, String type, EntityType entityType, SemanticNode semanticNode) { - return byTextRangeWithEngine(nerEntity.textRange(), type, entityType, semanticNode, Engine.NER).orElseThrow(() -> new NotFoundException("No entity present!")); + return byTextRangeWithEngine(nerEntity.textRange(), type, entityType, semanticNode, Set.of(Engine.NER)).orElseThrow(() -> new NotFoundException("No entity present!")); } public Stream combineNerEntitiesToCbiAddressDefaults(NerEntities nerEntities, String type, EntityType entityType, SemanticNode semanticNode) { return NerEntitiesAdapter.combineNerEntitiesToCbiAddressDefaults(nerEntities) - .map(boundary -> byTextRangeWithEngine(boundary, type, entityType, semanticNode, Engine.NER)) + .map(boundary -> byTextRangeWithEngine(boundary, type, entityType, semanticNode, Set.of(Engine.NER))) .filter(Optional::isPresent) .map(Optional::get); } -- 2.47.2 From 5cf8a06761f9989744e978f894f92d3ea88a79be Mon Sep 17 00:00:00 2001 From: Andrei Isvoran Date: Thu, 12 Oct 2023 15:14:56 +0300 Subject: [PATCH 4/6] DM-483 - Prevent endless loops * cleanups and renaming redaction log to entity log --- .../v1/server/storage/RedactionStorageService.java | 2 +- .../redaction/v1/server/DocumineFloraTest.java | 2 +- .../v1/server/annotate/AnnotationService.java | 12 ++++++------ .../resources/dictionaries/PII_false_positive.txt | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) 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 a5648475..3fff6ff7 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 @@ -105,7 +105,7 @@ public class RedactionStorageService { try { return storageService.readJSONObject(TenantContext.getTenantId(), StorageIdUtils.getStorageId(dossierId, fileId, FileType.ENTITY_LOG), EntityLog.class); } catch (StorageObjectDoesNotExist e) { - log.debug("RedactionLog not available."); + log.debug("EntityLog not available."); return null; } diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/DocumineFloraTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/DocumineFloraTest.java index 1550be69..c89a69fe 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/DocumineFloraTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/DocumineFloraTest.java @@ -112,7 +112,7 @@ public class DocumineFloraTest extends AbstractRedactionIntegrationTest { // Fix In BodyTextFrameService destroys header detection in files/new/SYNGENTA_EFSA_sanitisation_GFL_v1_moreSections.pdf // TODO unify logic - AnalyzeRequest request = uploadFileToStorage("files/Documine/Flora/402Study.pdf"); + AnalyzeRequest request = uploadFileToStorage("files/Documine/Flora/402Study-ocred.pdf"); System.out.println("Start Full integration test"); analyzeDocumentStructure(LayoutParsingType.DOCUMINE, request); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/annotate/AnnotationService.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/annotate/AnnotationService.java index c98aedda..10ab391e 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/annotate/AnnotationService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/annotate/AnnotationService.java @@ -82,13 +82,13 @@ public class AnnotationService { private void annotate(PDDocument document, EntityLog entityLog) throws IOException { - Map> redactionLogPerPage = groupingByPageNumber(entityLog); + Map> entityLogPerPage = groupingByPageNumber(entityLog); for (int page = 1; page <= document.getNumberOfPages(); page++) { PDPage pdPage = document.getPage(page - 1); - List logEntries = redactionLogPerPage.get(page); + List logEntries = entityLogPerPage.get(page); if (logEntries != null && !logEntries.isEmpty()) { addAnnotations(logEntries, pdPage, page); } @@ -98,20 +98,20 @@ public class AnnotationService { private Map> groupingByPageNumber(EntityLog redactionLog) { - Map> redactionLogPerPage = new HashMap<>(); + Map> entityLogPerPage = new HashMap<>(); if (redactionLog == null) { - return redactionLogPerPage; + return entityLogPerPage; } for (EntityLogEntry entry : redactionLog.getEntityLogEntry()) { int page = 0; for (Position position : entry.getPositions()) { if (position.getPageNumber() != page) { - redactionLogPerPage.computeIfAbsent(position.getPageNumber(), x -> new ArrayList<>()).add(entry); + entityLogPerPage.computeIfAbsent(position.getPageNumber(), x -> new ArrayList<>()).add(entry); page = position.getPageNumber(); } } } - return redactionLogPerPage; + return entityLogPerPage; } diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/resources/dictionaries/PII_false_positive.txt b/redaction-service-v1/redaction-service-server-v1/src/test/resources/dictionaries/PII_false_positive.txt index 492ca143..cd9fab48 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/resources/dictionaries/PII_false_positive.txt +++ b/redaction-service-v1/redaction-service-server-v1/src/test/resources/dictionaries/PII_false_positive.txt @@ -1 +1 @@ -C. J. Alfred XinyiP \ No newline at end of file +C. J. Alfred Xinyi \ No newline at end of file -- 2.47.2 From 700383cfbd479d9d483df5e24f4adb1e3ac8cc5f Mon Sep 17 00:00:00 2001 From: Andrei Isvoran Date: Thu, 12 Oct 2023 15:15:37 +0300 Subject: [PATCH 5/6] DM-483 - Prevent endless loops * cleanups and renaming redaction log to entity log --- .../redaction/v1/server/annotate/AnnotationService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/annotate/AnnotationService.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/annotate/AnnotationService.java index 10ab391e..c46aaf75 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/annotate/AnnotationService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/annotate/AnnotationService.java @@ -96,13 +96,13 @@ public class AnnotationService { } - private Map> groupingByPageNumber(EntityLog redactionLog) { + private Map> groupingByPageNumber(EntityLog entityLog) { Map> entityLogPerPage = new HashMap<>(); - if (redactionLog == null) { + if (entityLog == null) { return entityLogPerPage; } - for (EntityLogEntry entry : redactionLog.getEntityLogEntry()) { + for (EntityLogEntry entry : entityLog.getEntityLogEntry()) { int page = 0; for (Position position : entry.getPositions()) { if (position.getPageNumber() != page) { -- 2.47.2 From 90cb7c4dc9b2118cce12579c108098ac5437f4f3 Mon Sep 17 00:00:00 2001 From: Andrei Isvoran Date: Thu, 12 Oct 2023 17:23:30 +0300 Subject: [PATCH 6/6] DM-483 - Prevent endless loops * Check entryType is not null when creating annotation --- .../v1/server/annotate/AnnotationService.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/annotate/AnnotationService.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/annotate/AnnotationService.java index c46aaf75..3a767021 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/annotate/AnnotationService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/annotate/AnnotationService.java @@ -123,16 +123,16 @@ public class AnnotationService { if (entry.getState().equals(EntryState.REMOVED)) { continue; } - annotations.addAll(createAnnotation(entry, page, pdPage.getRotation(), pdPage.getCropBox())); + annotations.addAll(createAnnotation(entry, page)); } } - private List createAnnotation(EntityLogEntry redactionLogEntry, int page, int rotation, PDRectangle cropBox) { + private List createAnnotation(EntityLogEntry entityLogEntry, int page) { List annotations = new ArrayList<>(); - List rectangles = redactionLogEntry.getPositions().stream().filter(pos -> pos.getPageNumber() == page).collect(Collectors.toList()); + List rectangles = entityLogEntry.getPositions().stream().filter(pos -> pos.getPageNumber() == page).collect(Collectors.toList()); if (rectangles.isEmpty()) { return annotations; @@ -143,12 +143,12 @@ public class AnnotationService { PDRectangle pdRectangle = toPDRectangleBBox(rectangles); annotation.setRectangle(pdRectangle); annotation.setQuadPoints(Floats.toArray(toQuadPoints(rectangles))); - if (!(redactionLogEntry.getEntryType().equals(EntryType.HINT) || redactionLogEntry.getState().equals(EntryState.IGNORED))) { - annotation.setContents(redactionLogEntry.getValue() + " " + createAnnotationContent(redactionLogEntry)); + if (!(entityLogEntry.getEntryType() == null || entityLogEntry.getEntryType().equals(EntryType.HINT) || entityLogEntry.getState().equals(EntryState.IGNORED))) { + annotation.setContents(entityLogEntry.getValue() + " " + createAnnotationContent(entityLogEntry)); } - annotation.setTitlePopup(redactionLogEntry.getId()); - annotation.setAnnotationName(redactionLogEntry.getId()); - annotation.setColor(new PDColor(redactionLogEntry.getColor(), PDDeviceRGB.INSTANCE)); + annotation.setTitlePopup(entityLogEntry.getId()); + annotation.setAnnotationName(entityLogEntry.getId()); + annotation.setColor(new PDColor(entityLogEntry.getColor(), PDDeviceRGB.INSTANCE)); annotation.setNoRotate(false); annotations.add(annotation); -- 2.47.2