From 459af9e33b08cb83fe909ecace8587a0fee2e729 Mon Sep 17 00:00:00 2001 From: Thomas Beyer Date: Mon, 27 Mar 2023 09:05:13 +0200 Subject: [PATCH 1/8] RED-6411 - remove false positives before calling EntitySearchUtils.addOrAddEngine() --- .../server/redaction/service/entityredaction/EntityFinder.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/entityredaction/EntityFinder.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/entityredaction/EntityFinder.java index 0cf2d378..86c390f0 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/entityredaction/EntityFinder.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/service/entityredaction/EntityFinder.java @@ -66,7 +66,7 @@ class EntityFinder { !local, model.isDossierDictionary(), local ? Engine.RULE : Engine.DICTIONARY, - local ? EntityType.RECOMMENDATION : EntityType.ENTITY)); + local ? EntityType.RECOMMENDATION : EntityType.ENTITY)).stream().filter(e -> !e.isFalsePositive()).collect(Collectors.toSet()); EntitySearchUtils.addOrAddEngine(found, entities); } From f03523ca8f49767b22d551023866dc9948c06e9f Mon Sep 17 00:00:00 2001 From: deiflaender Date: Mon, 27 Mar 2023 09:37:48 +0200 Subject: [PATCH 2/8] RED-6224: Multitenancy for rules cache --- .../server/redaction/model/TenantRules.java | 15 ++++ .../service/DroolsExecutionService.java | 69 +++++++++++++++---- 2 files changed, 70 insertions(+), 14 deletions(-) create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/TenantRules.java diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/TenantRules.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/TenantRules.java new file mode 100644 index 00000000..eff0a617 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/TenantRules.java @@ -0,0 +1,15 @@ +package com.iqser.red.service.redaction.v1.server.redaction.model; + +import java.util.HashMap; +import java.util.Map; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class TenantRules { + + private Map rulesVersionPerDossierTemplateId = new HashMap<>(); + +} 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 6905fb63..eaaa9b63 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,11 +1,13 @@ package com.iqser.red.service.redaction.v1.server.redaction.service; -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; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; -import io.micrometer.core.annotation.Timed; -import lombok.RequiredArgsConstructor; +import javax.annotation.PostConstruct; import org.apache.commons.lang3.StringUtils; import org.kie.api.KieServices; @@ -16,11 +18,19 @@ import org.kie.api.runtime.KieContainer; import org.kie.api.runtime.KieSession; import org.springframework.stereotype.Service; -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.Map; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +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.multitenancy.TenantContext; +import com.iqser.red.service.redaction.v1.server.redaction.model.Section; +import com.iqser.red.service.redaction.v1.server.redaction.model.TenantRules; +import com.iqser.red.service.redaction.v1.server.settings.RedactionServiceSettings; + +import io.micrometer.core.annotation.Timed; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; @Service @RequiredArgsConstructor @@ -30,7 +40,24 @@ public class DroolsExecutionService { private final Map kieContainers = new HashMap<>(); - private final Map rulesVersionPerDossierTemplateId = new HashMap<>(); + private final RedactionServiceSettings settings; + + private LoadingCache tenantRulesCache; + + + @PostConstruct + protected void createCache() { + + tenantRulesCache = CacheBuilder.newBuilder() + .maximumSize(settings.getDictionaryCacheMaximumSize()) + .expireAfterAccess(settings.getDictionaryCacheExpireAfterAccessDays(), TimeUnit.DAYS) + .build(new CacheLoader<>() { + public TenantRules load(String key) { + + return new TenantRules(); + } + }); + } public KieContainer getKieContainer(String dossierTemplateId) { @@ -61,13 +88,13 @@ public class DroolsExecutionService { public KieContainer updateRules(String dossierTemplateId) { long version = rulesClient.getVersion(dossierTemplateId); - Long rulesVersion = rulesVersionPerDossierTemplateId.get(dossierTemplateId); + Long rulesVersion = getVersionForDossierTemplate(dossierTemplateId); if (rulesVersion == null) { rulesVersion = -1L; } if (version > rulesVersion) { - rulesVersionPerDossierTemplateId.put(dossierTemplateId, version); + setRulesVersionForDossierTemplate(dossierTemplateId, version); return createOrUpdateKieContainer(dossierTemplateId); } return getKieContainer(dossierTemplateId); @@ -126,11 +153,25 @@ public class DroolsExecutionService { public long getRulesVersion(String dossierTemplateId) { - Long rulesVersion = rulesVersionPerDossierTemplateId.get(dossierTemplateId); + Long rulesVersion = getVersionForDossierTemplate(dossierTemplateId); if (rulesVersion == null) { return -1; } return rulesVersion; } + + @SneakyThrows + private Long getVersionForDossierTemplate(String dossierTemplateId) { + + return tenantRulesCache.get(TenantContext.getTenantId()).getRulesVersionPerDossierTemplateId().get(dossierTemplateId); + } + + + @SneakyThrows + private void setRulesVersionForDossierTemplate(String dossierTemplateId, long version) { + + tenantRulesCache.get(TenantContext.getTenantId()).getRulesVersionPerDossierTemplateId().put(dossierTemplateId, version); + } + } From 7644afc3aa766e2e642060dd0b0f5c225ab8241f Mon Sep 17 00:00:00 2001 From: Thomas Beyer Date: Mon, 27 Mar 2023 12:49:33 +0200 Subject: [PATCH 3/8] RED-6411 - extend logic when replacing entities with higher rank --- .../server/redaction/utils/EntitySearchUtils.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 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 8bd7f515..ff22105f 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 @@ -273,14 +273,14 @@ public final class EntitySearchUtils { existing.setLegalBasis(found.getLegalBasis()); existing.setMatchedRule(found.getMatchedRule()); existing.setRedactionReason(found.getRedactionReason()); - if (existing.getEntityType().equals(EntityType.RECOMMENDATION) && found.getEntityType().equals(EntityType.ENTITY) || existing.getEntityType() - .equals(EntityType.ENTITY) && found.getEntityType().equals(EntityType.RECOMMENDATION)) { + if (isOneARecommendationAndTheOtherEntity(found, existing)) { existing.setEntityType(EntityType.ENTITY); if (found.isRedaction()) { existing.setRedaction(true); } } - } else if (dictionary.getDictionaryRank(existing.getType()) <= dictionary.getDictionaryRank(found.getType())) { + } else if (dictionary.getDictionaryRank(existing.getType()) <= dictionary.getDictionaryRank(found.getType()) && + !(isOneARecommendationAndTheOtherEntity(found, existing) && existing.isRedaction() && found.isRedaction()) ) { entities.remove(found); entities.add(found); } @@ -290,6 +290,13 @@ public final class EntitySearchUtils { } + private boolean isOneARecommendationAndTheOtherEntity (Entity found, Entity existing) { + + return existing.getEntityType().equals(EntityType.RECOMMENDATION) && found.getEntityType().equals(EntityType.ENTITY) || existing.getEntityType() + .equals(EntityType.ENTITY) && found.getEntityType().equals(EntityType.RECOMMENDATION); + } + + public void addEntitiesIgnoreRank(Set entities, Set found) { // HashSet keeps old value but we want the new. entities.removeAll(found); From 157b20a1de505cb2930c56987822b76ef153e078 Mon Sep 17 00:00:00 2001 From: Thomas Beyer Date: Tue, 28 Mar 2023 10:16:17 +0200 Subject: [PATCH 4/8] RED-6411 - add test for this case (with the current rules-file) and move/abstract logic from the RedactionIntegrationTest into AbstractRedactionIntegrationTest --- .../AbstractRedactionIntegrationTest.java | 481 ++++++++++++++++++ .../v1/server/RedactionIntegrationTest.java | 457 +---------------- .../v1/server/RedactionIntegrationTestV2.java | 152 ++++++ .../src/test/resources/drools/rules_v2.drl | 341 +++++++++++++ .../test/resources/files/new/simplified2.pdf | Bin 0 -> 69833 bytes 5 files changed, 980 insertions(+), 451 deletions(-) create mode 100644 redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/AbstractRedactionIntegrationTest.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTestV2.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/rules_v2.drl create mode 100644 redaction-service-v1/redaction-service-server-v1/src/test/resources/files/new/simplified2.pdf diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/AbstractRedactionIntegrationTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/AbstractRedactionIntegrationTest.java new file mode 100644 index 00000000..70202114 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/AbstractRedactionIntegrationTest.java @@ -0,0 +1,481 @@ +package com.iqser.red.service.redaction.v1.server; + +import static org.mockito.Mockito.when; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Collections; +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.junit.jupiter.api.AfterEach; +import org.mockito.stubbing.Answer; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.core.io.ClassPathResource; + +import com.amazonaws.services.s3.AmazonS3; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeRequest; +import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.configuration.Colors; +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.DictionaryEntry; +import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.type.Type; +import com.iqser.red.service.redaction.v1.server.annotate.AnnotationService; +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.controller.RedactionController; +import com.iqser.red.service.redaction.v1.server.multitenancy.TenantContext; +import com.iqser.red.service.redaction.v1.server.redaction.service.ManualRedactionSurroundingTextService; +import com.iqser.red.service.redaction.v1.server.redaction.service.analyze.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; +import com.iqser.red.storage.commons.service.StorageService; + +import lombok.SneakyThrows; + +public abstract class AbstractRedactionIntegrationTest { + + protected static final String VERTEBRATE = "vertebrate"; + protected static final String ADDRESS = "CBI_address"; + protected static final String AUTHOR = "CBI_author"; + protected static final String SPONSOR = "CBI_sponsor"; + protected static final String NO_REDACTION_INDICATOR = "no_redaction_indicator"; + protected static final String REDACTION_INDICATOR = "redaction_indicator"; + protected static final String HINT_ONLY = "hint_only"; + protected static final String MUST_REDACT = "must_redact"; + protected static final String PUBLISHED_INFORMATION = "published_information"; + protected static final String TEST_METHOD = "test_method"; + protected static final String PURITY = "purity"; + protected static final String IMAGE = "image"; + protected static final String LOGO = "logo"; + protected static final String SIGNATURE = "signature"; + protected static final String FORMULA = "formula"; + protected static final String OCR = "ocr"; + protected static final String DOSSIER_REDACTIONS = "dossier_redactions"; + protected static final String IMPORTED_REDACTION = "imported_redaction"; + protected static final String PII = "PII"; + protected static final String ROTATE_SIMPLE = "RotateSimple"; + + @Autowired + protected RedactionController redactionController; + + @Autowired + protected AnnotationService annotationService; + + @Autowired + protected AnalyzeService analyzeService; + + @Autowired + protected ObjectMapper objectMapper; + + @Autowired + protected RedactionStorageService redactionStorageService; + + @Autowired + protected StorageService storageService; + + @Autowired + protected ManualRedactionSurroundingTextService manualRedactionSurroundingTextService; + + @MockBean + protected AmazonS3 amazonS3; + + @MockBean + protected RabbitTemplate rabbitTemplate; + + @MockBean + protected LegalBasisClient legalBasisClient; + + protected final Map> dictionary = new HashMap<>(); + protected final Map> dossierDictionary = new HashMap<>(); + protected final Map> falsePositive = new HashMap<>(); + protected final Map> falseRecommendation = new HashMap<>(); + protected final Map typeColorMap = new HashMap<>(); + protected final Map hintTypeMap = new HashMap<>(); + protected final Map caseInSensitiveMap = new HashMap<>(); + protected final Map recommendationTypeMap = new HashMap<>(); + protected final Map rankTypeMap = new HashMap<>(); + protected final Colors colors = new Colors(); + protected final Map reanlysisVersions = new HashMap<>(); + protected final Set deleted = new HashSet<>(); + + protected final static String TEST_DOSSIER_TEMPLATE_ID = "123"; + protected final static String TEST_DOSSIER_ID = "123"; + protected final static String TEST_FILE_ID = "123"; + + @MockBean + protected RulesClient rulesClient; + + @MockBean + protected DictionaryClient dictionaryClient; + + + @AfterEach + public void cleanupStorage() { + + if (this.storageService instanceof FileSystemBackedStorageService) { + ((FileSystemBackedStorageService) this.storageService).clearStorage(); + } + } + + + protected void mockDictionaryCalls(Long version) { + + when(dictionaryClient.getDictionaryForType(VERTEBRATE + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(VERTEBRATE, + false)); + when(dictionaryClient.getDictionaryForType(ADDRESS + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(ADDRESS, false)); + when(dictionaryClient.getDictionaryForType(AUTHOR + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(AUTHOR, false)); + when(dictionaryClient.getDictionaryForType(SPONSOR + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(SPONSOR, false)); + when(dictionaryClient.getDictionaryForType(NO_REDACTION_INDICATOR + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse( + NO_REDACTION_INDICATOR, + false)); + when(dictionaryClient.getDictionaryForType(REDACTION_INDICATOR + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse( + REDACTION_INDICATOR, + false)); + when(dictionaryClient.getDictionaryForType(HINT_ONLY + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(HINT_ONLY, false)); + when(dictionaryClient.getDictionaryForType(MUST_REDACT + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(MUST_REDACT, + false)); + when(dictionaryClient.getDictionaryForType(PUBLISHED_INFORMATION + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse( + PUBLISHED_INFORMATION, + false)); + when(dictionaryClient.getDictionaryForType(TEST_METHOD + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(TEST_METHOD, + false)); + when(dictionaryClient.getDictionaryForType(PII + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(PII, false)); + when(dictionaryClient.getDictionaryForType(PURITY + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(PURITY, false)); + when(dictionaryClient.getDictionaryForType(IMAGE + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(IMAGE, false)); + when(dictionaryClient.getDictionaryForType(OCR + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(OCR, false)); + when(dictionaryClient.getDictionaryForType(LOGO + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(LOGO, false)); + when(dictionaryClient.getDictionaryForType(SIGNATURE + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(SIGNATURE, false)); + when(dictionaryClient.getDictionaryForType(FORMULA + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(FORMULA, false)); + when(dictionaryClient.getDictionaryForType(ROTATE_SIMPLE + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(ROTATE_SIMPLE, + false)); + when(dictionaryClient.getDictionaryForType(DOSSIER_REDACTIONS + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse( + DOSSIER_REDACTIONS, + true)); + when(dictionaryClient.getDictionaryForType(IMPORTED_REDACTION + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse( + IMPORTED_REDACTION, + true)); + + } + + + protected void loadDictionaryForTest() { + + dictionary.computeIfAbsent(AUTHOR, v -> new ArrayList<>()) + .addAll(ResourceLoader.load("dictionaries/CBI_author.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); + dictionary.computeIfAbsent(SPONSOR, v -> new ArrayList<>()) + .addAll(ResourceLoader.load("dictionaries/CBI_sponsor.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); + dictionary.computeIfAbsent(VERTEBRATE, v -> new ArrayList<>()) + .addAll(ResourceLoader.load("dictionaries/vertebrate.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); + dictionary.computeIfAbsent(ADDRESS, v -> new ArrayList<>()) + .addAll(ResourceLoader.load("dictionaries/CBI_address.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); + dictionary.computeIfAbsent(NO_REDACTION_INDICATOR, v -> new ArrayList<>()) + .addAll(ResourceLoader.load("dictionaries/no_redaction_indicator.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); + dictionary.computeIfAbsent(REDACTION_INDICATOR, v -> new ArrayList<>()) + .addAll(ResourceLoader.load("dictionaries/redaction_indicator.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); + dictionary.computeIfAbsent(HINT_ONLY, v -> new ArrayList<>()) + .addAll(ResourceLoader.load("dictionaries/hint_only.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); + dictionary.computeIfAbsent(MUST_REDACT, v -> new ArrayList<>()) + .addAll(ResourceLoader.load("dictionaries/must_redact.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); + dictionary.computeIfAbsent(PUBLISHED_INFORMATION, v -> new ArrayList<>()) + .addAll(ResourceLoader.load("dictionaries/published_information.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); + dictionary.computeIfAbsent(TEST_METHOD, v -> new ArrayList<>()) + .addAll(ResourceLoader.load("dictionaries/test_method.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); + dictionary.computeIfAbsent(PII, v -> new ArrayList<>()) + .addAll(ResourceLoader.load("dictionaries/PII.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); + dictionary.computeIfAbsent(PURITY, v -> new ArrayList<>()) + .addAll(ResourceLoader.load("dictionaries/purity.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); + dictionary.computeIfAbsent(IMAGE, v -> new ArrayList<>()) + .addAll(ResourceLoader.load("dictionaries/empty.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); + dictionary.computeIfAbsent(OCR, v -> new ArrayList<>()) + .addAll(ResourceLoader.load("dictionaries/empty.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); + dictionary.computeIfAbsent(LOGO, v -> new ArrayList<>()) + .addAll(ResourceLoader.load("dictionaries/empty.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); + dictionary.computeIfAbsent(SIGNATURE, v -> new ArrayList<>()) + .addAll(ResourceLoader.load("dictionaries/empty.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); + dictionary.computeIfAbsent(FORMULA, v -> new ArrayList<>()) + .addAll(ResourceLoader.load("dictionaries/empty.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); + dossierDictionary.computeIfAbsent(DOSSIER_REDACTIONS, v -> new ArrayList<>()) + .addAll(ResourceLoader.load("dictionaries/dossier_redactions.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); + dossierDictionary.put(IMPORTED_REDACTION, new ArrayList<>()); + + falsePositive.computeIfAbsent(PII, v -> new ArrayList<>()) + .addAll(ResourceLoader.load("dictionaries/PII_false_positive.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); + + } + + + protected void loadOnlyDictionaryForSimpleFile() { + + dictionary.clear(); + dictionary.computeIfAbsent(ROTATE_SIMPLE, v -> new ArrayList<>()) + .addAll(ResourceLoader.load("dictionaries/RotateTestFileSimple.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); + } + + + protected 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); + } + } + + + protected List getPathsRecursively(File path) { + + List result = new ArrayList<>(); + if (path == null || path.listFiles() == null) { + return result; + } + for (File f : path.listFiles()) { + if (f.isFile()) { + result.add(f); + } else { + result.addAll(getPathsRecursively(f)); + } + } + return result; + + } + + + protected void loadTypeForTest() { + + typeColorMap.put(VERTEBRATE, "#ff85f7"); + typeColorMap.put(ADDRESS, "#ffe187"); + typeColorMap.put(AUTHOR, "#ffe187"); + typeColorMap.put(SPONSOR, "#85ebff"); + typeColorMap.put(NO_REDACTION_INDICATOR, "#be85ff"); + typeColorMap.put(REDACTION_INDICATOR, "#caff85"); + typeColorMap.put(HINT_ONLY, "#abc0c4"); + typeColorMap.put(MUST_REDACT, "#fab4c0"); + typeColorMap.put(PUBLISHED_INFORMATION, "#85ebff"); + typeColorMap.put(TEST_METHOD, "#91fae8"); + typeColorMap.put(PII, "#66ccff"); + typeColorMap.put(PURITY, "#ffe187"); + typeColorMap.put(IMAGE, "#fcc5fb"); + typeColorMap.put(OCR, "#fcc5fb"); + typeColorMap.put(LOGO, "#ffe187"); + typeColorMap.put(FORMULA, "#ffe187"); + typeColorMap.put(SIGNATURE, "#ffe187"); + typeColorMap.put(IMPORTED_REDACTION, "#fcfbe6"); + typeColorMap.put(ROTATE_SIMPLE, "#66ccff"); + + hintTypeMap.put(VERTEBRATE, true); + hintTypeMap.put(ADDRESS, false); + hintTypeMap.put(AUTHOR, false); + hintTypeMap.put(SPONSOR, false); + hintTypeMap.put(NO_REDACTION_INDICATOR, true); + hintTypeMap.put(REDACTION_INDICATOR, true); + hintTypeMap.put(HINT_ONLY, true); + hintTypeMap.put(MUST_REDACT, true); + hintTypeMap.put(PUBLISHED_INFORMATION, true); + hintTypeMap.put(TEST_METHOD, true); + hintTypeMap.put(PII, false); + hintTypeMap.put(PURITY, false); + hintTypeMap.put(IMAGE, true); + hintTypeMap.put(OCR, true); + hintTypeMap.put(FORMULA, false); + hintTypeMap.put(LOGO, false); + hintTypeMap.put(SIGNATURE, false); + hintTypeMap.put(DOSSIER_REDACTIONS, false); + hintTypeMap.put(IMPORTED_REDACTION, false); + hintTypeMap.put(ROTATE_SIMPLE, false); + + caseInSensitiveMap.put(VERTEBRATE, true); + caseInSensitiveMap.put(ADDRESS, false); + caseInSensitiveMap.put(AUTHOR, false); + caseInSensitiveMap.put(SPONSOR, false); + caseInSensitiveMap.put(NO_REDACTION_INDICATOR, true); + caseInSensitiveMap.put(REDACTION_INDICATOR, true); + caseInSensitiveMap.put(HINT_ONLY, true); + caseInSensitiveMap.put(MUST_REDACT, true); + caseInSensitiveMap.put(PUBLISHED_INFORMATION, true); + caseInSensitiveMap.put(TEST_METHOD, false); + caseInSensitiveMap.put(PII, false); + caseInSensitiveMap.put(PURITY, false); + caseInSensitiveMap.put(IMAGE, true); + caseInSensitiveMap.put(OCR, true); + caseInSensitiveMap.put(SIGNATURE, true); + caseInSensitiveMap.put(LOGO, true); + caseInSensitiveMap.put(FORMULA, true); + caseInSensitiveMap.put(DOSSIER_REDACTIONS, false); + caseInSensitiveMap.put(IMPORTED_REDACTION, false); + caseInSensitiveMap.put(ROTATE_SIMPLE, true); + + recommendationTypeMap.put(VERTEBRATE, false); + recommendationTypeMap.put(ADDRESS, false); + recommendationTypeMap.put(AUTHOR, false); + recommendationTypeMap.put(SPONSOR, false); + recommendationTypeMap.put(NO_REDACTION_INDICATOR, false); + recommendationTypeMap.put(REDACTION_INDICATOR, false); + recommendationTypeMap.put(HINT_ONLY, false); + recommendationTypeMap.put(MUST_REDACT, false); + recommendationTypeMap.put(PUBLISHED_INFORMATION, false); + recommendationTypeMap.put(TEST_METHOD, false); + recommendationTypeMap.put(PII, false); + recommendationTypeMap.put(PURITY, false); + recommendationTypeMap.put(IMAGE, false); + recommendationTypeMap.put(OCR, false); + recommendationTypeMap.put(FORMULA, false); + recommendationTypeMap.put(SIGNATURE, false); + recommendationTypeMap.put(LOGO, false); + recommendationTypeMap.put(DOSSIER_REDACTIONS, false); + recommendationTypeMap.put(IMPORTED_REDACTION, false); + recommendationTypeMap.put(ROTATE_SIMPLE, false); + + rankTypeMap.put(PURITY, 155); + rankTypeMap.put(PII, 150); + rankTypeMap.put(ADDRESS, 140); + rankTypeMap.put(AUTHOR, 130); + rankTypeMap.put(SPONSOR, 120); + rankTypeMap.put(VERTEBRATE, 110); + rankTypeMap.put(MUST_REDACT, 100); + rankTypeMap.put(REDACTION_INDICATOR, 90); + rankTypeMap.put(NO_REDACTION_INDICATOR, 80); + rankTypeMap.put(PUBLISHED_INFORMATION, 70); + rankTypeMap.put(TEST_METHOD, 60); + rankTypeMap.put(HINT_ONLY, 50); + rankTypeMap.put(IMAGE, 30); + rankTypeMap.put(OCR, 29); + rankTypeMap.put(LOGO, 28); + rankTypeMap.put(SIGNATURE, 27); + rankTypeMap.put(FORMULA, 26); + rankTypeMap.put(DOSSIER_REDACTIONS, 200); + rankTypeMap.put(IMPORTED_REDACTION, 200); + rankTypeMap.put(ROTATE_SIMPLE, 150); + + colors.setSkippedColor("#cccccc"); + colors.setRequestAddColor("#04b093"); + colors.setRequestRemoveColor("#04b093"); + } + + + @SneakyThrows + protected void loadNerForTest() { + + ClassPathResource responseJson = new ClassPathResource("files/ner_response.json"); + storageService.storeObject(TenantContext.getTenantId(), + RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.NER_ENTITIES), + responseJson.getInputStream()); + } + + + protected List getTypeResponse() { + + return typeColorMap.entrySet() + .stream() + .map(typeColor -> Type.builder() + .id(typeColor.getKey() + ":" + TEST_DOSSIER_TEMPLATE_ID) + .type(typeColor.getKey()) + .dossierTemplateId(TEST_DOSSIER_TEMPLATE_ID) + .hexColor(typeColor.getValue()) + .isHint(hintTypeMap.get(typeColor.getKey())) + .isCaseInsensitive(caseInSensitiveMap.get(typeColor.getKey())) + .isRecommendation(recommendationTypeMap.get(typeColor.getKey())) + .rank(rankTypeMap.get(typeColor.getKey())) + .build()) + + .collect(Collectors.toList()); + } + + + protected Type getDictionaryResponse(String type, boolean isDossierDictionary) { + + return Type.builder() + .id(type + ":" + TEST_DOSSIER_TEMPLATE_ID) + .hexColor(typeColorMap.get(type)) + .entries(isDossierDictionary ? toDictionaryEntry(dossierDictionary.get(type)) : toDictionaryEntry(dictionary.get(type))) + .falsePositiveEntries(falsePositive.containsKey(type) ? toDictionaryEntry(falsePositive.get(type)) : new ArrayList<>()) + .falseRecommendationEntries(falseRecommendation.containsKey(type) ? toDictionaryEntry(falseRecommendation.get(type)) : new ArrayList<>()) + .isHint(hintTypeMap.get(type)) + .isCaseInsensitive(caseInSensitiveMap.get(type)) + .isRecommendation(recommendationTypeMap.get(type)) + .rank(rankTypeMap.get(type)) + .build(); + } + + + private String cleanDictionaryEntry(String entry) { + + return TextNormalizationUtilities.removeHyphenLineBreaks(entry).replaceAll("\\n", " "); + } + + + private List toDictionaryEntry(List entries) { + + if (entries == null) { + entries = Collections.emptyList(); + } + + List dictionaryEntries = new ArrayList<>(); + entries.forEach(entry -> { + dictionaryEntries.add(DictionaryEntry.builder().value(entry).version(reanlysisVersions.getOrDefault(entry, 0L)).deleted(deleted.contains(entry)).build()); + }); + return dictionaryEntries; + } + + + @SneakyThrows + protected AnalyzeRequest prepareStorage(String file) { + + return prepareStorage(file, "files/cv_service_empty_response.json"); + } + + + @SneakyThrows + protected AnalyzeRequest prepareStorage(String file, String cvServiceResponseFile) { + + ClassPathResource pdfFileResource = new ClassPathResource(file); + ClassPathResource cvServiceResponseFileResource = new ClassPathResource(cvServiceResponseFile); + + return prepareStorage(pdfFileResource.getInputStream(), cvServiceResponseFileResource.getInputStream()); + } + + + @SneakyThrows + protected AnalyzeRequest prepareStorage(InputStream fileStream, InputStream cvServiceResponseFileStream) { + + AnalyzeRequest request = AnalyzeRequest.builder() + .dossierTemplateId(TEST_DOSSIER_TEMPLATE_ID) + .dossierId(TEST_DOSSIER_ID) + .fileId(TEST_FILE_ID) + .lastProcessed(OffsetDateTime.now()) + .build(); + + storageService.storeObject(TenantContext.getTenantId(), + RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.TABLES), + cvServiceResponseFileStream); + storageService.storeObject(TenantContext.getTenantId(), RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.ORIGIN), fileStream); + + return request; + + } + +} 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 e5d61ddb..599179c9 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 @@ -3,28 +3,23 @@ package com.iqser.red.service.redaction.v1.server; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; -import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.net.URL; import java.nio.charset.StandardCharsets; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -34,13 +29,9 @@ 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.mockito.stubbing.Answer; -import org.springframework.amqp.rabbit.core.RabbitTemplate; -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; @@ -48,9 +39,7 @@ import org.springframework.context.annotation.Primary; import org.springframework.core.io.ClassPathResource; import org.springframework.test.context.junit.jupiter.SpringExtension; -import com.amazonaws.services.s3.AmazonS3; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeRequest; import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeResult; import com.iqser.red.service.persistence.service.v1.api.shared.model.FileAttribute; @@ -67,24 +56,14 @@ import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations 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.configuration.Colors; 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.DictionaryEntry; 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.service.redaction.v1.model.StructureAnalyzeRequest; 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.annotate.AnnotationService; import com.iqser.red.service.redaction.v1.server.classification.model.SectionText; -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.controller.RedactionController; import com.iqser.red.service.redaction.v1.server.multitenancy.TenantContext; -import com.iqser.red.service.redaction.v1.server.redaction.service.ManualRedactionSurroundingTextService; -import com.iqser.red.service.redaction.v1.server.redaction.service.analyze.AnalyzeService; import com.iqser.red.service.redaction.v1.server.redaction.utils.OsUtils; -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.StorageAutoConfiguration; import com.iqser.red.storage.commons.service.StorageService; @@ -94,82 +73,9 @@ import lombok.SneakyThrows; @ExtendWith(SpringExtension.class) @SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Import(RedactionIntegrationTest.RedactionIntegrationTestConfiguration.class) -public class RedactionIntegrationTest { +public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { private static final String RULES = loadFromClassPath("drools/rules.drl"); - private static final String VERTEBRATE = "vertebrate"; - private static final String ADDRESS = "CBI_address"; - private static final String AUTHOR = "CBI_author"; - private static final String SPONSOR = "CBI_sponsor"; - private static final String NO_REDACTION_INDICATOR = "no_redaction_indicator"; - private static final String REDACTION_INDICATOR = "redaction_indicator"; - private static final String HINT_ONLY = "hint_only"; - private static final String MUST_REDACT = "must_redact"; - private static final String PUBLISHED_INFORMATION = "published_information"; - private static final String TEST_METHOD = "test_method"; - private static final String PURITY = "purity"; - private static final String IMAGE = "image"; - private static final String LOGO = "logo"; - private static final String SIGNATURE = "signature"; - private static final String FORMULA = "formula"; - private static final String OCR = "ocr"; - private static final String DOSSIER_REDACTIONS = "dossier_redactions"; - private static final String IMPORTED_REDACTION = "imported_redaction"; - private static final String PII = "PII"; - private static final String ROTATE_SIMPLE = "RotateSimple"; - - @Autowired - private RedactionController redactionController; - - @Autowired - private AnnotationService annotationService; - - @Autowired - private AnalyzeService analyzeService; - - @Autowired - private ObjectMapper objectMapper; - - @MockBean - private RulesClient rulesClient; - - @MockBean - private DictionaryClient dictionaryClient; - - @Autowired - private RedactionStorageService redactionStorageService; - - @Autowired - private StorageService storageService; - - @Autowired - private ManualRedactionSurroundingTextService manualRedactionSurroundingTextService; - - @MockBean - private AmazonS3 amazonS3; - - @MockBean - private RabbitTemplate rabbitTemplate; - - @MockBean - private LegalBasisClient legalBasisClient; - - private final Map> dictionary = new HashMap<>(); - private final Map> dossierDictionary = new HashMap<>(); - private final Map> falsePositive = new HashMap<>(); - private final Map> falseRecommendation = new HashMap<>(); - private final Map typeColorMap = new HashMap<>(); - private final Map hintTypeMap = new HashMap<>(); - private final Map caseInSensitiveMap = new HashMap<>(); - private final Map recommendationTypeMap = new HashMap<>(); - 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"; - private final static String TEST_FILE_ID = "123"; @Configuration @EnableAutoConfiguration(exclude = {RabbitAutoConfiguration.class, StorageAutoConfiguration.class}) @@ -201,15 +107,6 @@ public class RedactionIntegrationTest { } - @AfterEach - public void cleanupStorage() { - - if (this.storageService instanceof FileSystemBackedStorageService) { - ((FileSystemBackedStorageService) this.storageService).clearStorage(); - } - } - - @BeforeEach public void stubClients() { @@ -243,46 +140,6 @@ public class RedactionIntegrationTest { } - private void mockDictionaryCalls(Long version) { - - when(dictionaryClient.getDictionaryForType(VERTEBRATE + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(VERTEBRATE, - false)); - when(dictionaryClient.getDictionaryForType(ADDRESS + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(ADDRESS, false)); - when(dictionaryClient.getDictionaryForType(AUTHOR + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(AUTHOR, false)); - when(dictionaryClient.getDictionaryForType(SPONSOR + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(SPONSOR, false)); - when(dictionaryClient.getDictionaryForType(NO_REDACTION_INDICATOR + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse( - NO_REDACTION_INDICATOR, - false)); - when(dictionaryClient.getDictionaryForType(REDACTION_INDICATOR + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse( - REDACTION_INDICATOR, - false)); - when(dictionaryClient.getDictionaryForType(HINT_ONLY + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(HINT_ONLY, false)); - when(dictionaryClient.getDictionaryForType(MUST_REDACT + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(MUST_REDACT, - false)); - when(dictionaryClient.getDictionaryForType(PUBLISHED_INFORMATION + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse( - PUBLISHED_INFORMATION, - false)); - when(dictionaryClient.getDictionaryForType(TEST_METHOD + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(TEST_METHOD, - false)); - when(dictionaryClient.getDictionaryForType(PII + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(PII, false)); - when(dictionaryClient.getDictionaryForType(PURITY + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(PURITY, false)); - when(dictionaryClient.getDictionaryForType(IMAGE + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(IMAGE, false)); - when(dictionaryClient.getDictionaryForType(OCR + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(OCR, false)); - when(dictionaryClient.getDictionaryForType(LOGO + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(LOGO, false)); - when(dictionaryClient.getDictionaryForType(SIGNATURE + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(SIGNATURE, false)); - when(dictionaryClient.getDictionaryForType(FORMULA + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(FORMULA, false)); - when(dictionaryClient.getDictionaryForType(ROTATE_SIMPLE + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(ROTATE_SIMPLE, - false)); - when(dictionaryClient.getDictionaryForType(DOSSIER_REDACTIONS + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse( - DOSSIER_REDACTIONS, - true)); - when(dictionaryClient.getDictionaryForType(IMPORTED_REDACTION + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse( - IMPORTED_REDACTION, - true)); - - } - - @Test public void test270Rotated() { @@ -644,7 +501,9 @@ public class RedactionIntegrationTest { String outputFileName = OsUtils.getTemporaryDirectory() + "/Annotated.pdf"; ClassPathResource responseJson = new ClassPathResource("files/crafted_document.NER_ENTITIES.json"); - storageService.storeObject(TenantContext.getTenantId(), RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.NER_ENTITIES), responseJson.getInputStream()); + storageService.storeObject(TenantContext.getTenantId(), + RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.NER_ENTITIES), + responseJson.getInputStream()); long start = System.currentTimeMillis(); AnalyzeRequest request = prepareStorage(fileName); @@ -1102,10 +961,6 @@ public class RedactionIntegrationTest { } - - - - @Test public void phantomCellsDocumentTest() throws IOException { @@ -1326,272 +1181,6 @@ public class RedactionIntegrationTest { } - private void loadDictionaryForTest() { - - dictionary.computeIfAbsent(AUTHOR, v -> new ArrayList<>()) - .addAll(ResourceLoader.load("dictionaries/CBI_author.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dictionary.computeIfAbsent(SPONSOR, v -> new ArrayList<>()) - .addAll(ResourceLoader.load("dictionaries/CBI_sponsor.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dictionary.computeIfAbsent(VERTEBRATE, v -> new ArrayList<>()) - .addAll(ResourceLoader.load("dictionaries/vertebrate.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dictionary.computeIfAbsent(ADDRESS, v -> new ArrayList<>()) - .addAll(ResourceLoader.load("dictionaries/CBI_address.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dictionary.computeIfAbsent(NO_REDACTION_INDICATOR, v -> new ArrayList<>()) - .addAll(ResourceLoader.load("dictionaries/no_redaction_indicator.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dictionary.computeIfAbsent(REDACTION_INDICATOR, v -> new ArrayList<>()) - .addAll(ResourceLoader.load("dictionaries/redaction_indicator.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dictionary.computeIfAbsent(HINT_ONLY, v -> new ArrayList<>()) - .addAll(ResourceLoader.load("dictionaries/hint_only.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dictionary.computeIfAbsent(MUST_REDACT, v -> new ArrayList<>()) - .addAll(ResourceLoader.load("dictionaries/must_redact.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dictionary.computeIfAbsent(PUBLISHED_INFORMATION, v -> new ArrayList<>()) - .addAll(ResourceLoader.load("dictionaries/published_information.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dictionary.computeIfAbsent(TEST_METHOD, v -> new ArrayList<>()) - .addAll(ResourceLoader.load("dictionaries/test_method.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dictionary.computeIfAbsent(PII, v -> new ArrayList<>()) - .addAll(ResourceLoader.load("dictionaries/PII.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dictionary.computeIfAbsent(PURITY, v -> new ArrayList<>()) - .addAll(ResourceLoader.load("dictionaries/purity.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dictionary.computeIfAbsent(IMAGE, v -> new ArrayList<>()) - .addAll(ResourceLoader.load("dictionaries/empty.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dictionary.computeIfAbsent(OCR, v -> new ArrayList<>()) - .addAll(ResourceLoader.load("dictionaries/empty.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dictionary.computeIfAbsent(LOGO, v -> new ArrayList<>()) - .addAll(ResourceLoader.load("dictionaries/empty.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dictionary.computeIfAbsent(SIGNATURE, v -> new ArrayList<>()) - .addAll(ResourceLoader.load("dictionaries/empty.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dictionary.computeIfAbsent(FORMULA, v -> new ArrayList<>()) - .addAll(ResourceLoader.load("dictionaries/empty.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dossierDictionary.computeIfAbsent(DOSSIER_REDACTIONS, v -> new ArrayList<>()) - .addAll(ResourceLoader.load("dictionaries/dossier_redactions.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dossierDictionary.put(IMPORTED_REDACTION, new ArrayList<>()); - - falsePositive.computeIfAbsent(PII, v -> new ArrayList<>()) - .addAll(ResourceLoader.load("dictionaries/PII_false_positive.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - - } - - - private void loadOnlyDictionaryForSimpleFile() { - - dictionary.clear(); - dictionary.computeIfAbsent(ROTATE_SIMPLE, v -> new ArrayList<>()) - .addAll(ResourceLoader.load("dictionaries/RotateTestFileSimple.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - } - - - 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 getPathsRecursively(File path) { - - List result = new ArrayList<>(); - if (path == null || path.listFiles() == null) { - return result; - } - for (File f : path.listFiles()) { - if (f.isFile()) { - result.add(f); - } else { - result.addAll(getPathsRecursively(f)); - } - } - return result; - - } - - - private void loadTypeForTest() { - - typeColorMap.put(VERTEBRATE, "#ff85f7"); - typeColorMap.put(ADDRESS, "#ffe187"); - typeColorMap.put(AUTHOR, "#ffe187"); - typeColorMap.put(SPONSOR, "#85ebff"); - typeColorMap.put(NO_REDACTION_INDICATOR, "#be85ff"); - typeColorMap.put(REDACTION_INDICATOR, "#caff85"); - typeColorMap.put(HINT_ONLY, "#abc0c4"); - typeColorMap.put(MUST_REDACT, "#fab4c0"); - typeColorMap.put(PUBLISHED_INFORMATION, "#85ebff"); - typeColorMap.put(TEST_METHOD, "#91fae8"); - typeColorMap.put(PII, "#66ccff"); - typeColorMap.put(PURITY, "#ffe187"); - typeColorMap.put(IMAGE, "#fcc5fb"); - typeColorMap.put(OCR, "#fcc5fb"); - typeColorMap.put(LOGO, "#ffe187"); - typeColorMap.put(FORMULA, "#ffe187"); - typeColorMap.put(SIGNATURE, "#ffe187"); - typeColorMap.put(IMPORTED_REDACTION, "#fcfbe6"); - typeColorMap.put(ROTATE_SIMPLE, "#66ccff"); - - hintTypeMap.put(VERTEBRATE, true); - hintTypeMap.put(ADDRESS, false); - hintTypeMap.put(AUTHOR, false); - hintTypeMap.put(SPONSOR, false); - hintTypeMap.put(NO_REDACTION_INDICATOR, true); - hintTypeMap.put(REDACTION_INDICATOR, true); - hintTypeMap.put(HINT_ONLY, true); - hintTypeMap.put(MUST_REDACT, true); - hintTypeMap.put(PUBLISHED_INFORMATION, true); - hintTypeMap.put(TEST_METHOD, true); - hintTypeMap.put(PII, false); - hintTypeMap.put(PURITY, false); - hintTypeMap.put(IMAGE, true); - hintTypeMap.put(OCR, true); - hintTypeMap.put(FORMULA, false); - hintTypeMap.put(LOGO, false); - hintTypeMap.put(SIGNATURE, false); - hintTypeMap.put(DOSSIER_REDACTIONS, false); - hintTypeMap.put(IMPORTED_REDACTION, false); - hintTypeMap.put(ROTATE_SIMPLE, false); - - caseInSensitiveMap.put(VERTEBRATE, true); - caseInSensitiveMap.put(ADDRESS, false); - caseInSensitiveMap.put(AUTHOR, false); - caseInSensitiveMap.put(SPONSOR, false); - caseInSensitiveMap.put(NO_REDACTION_INDICATOR, true); - caseInSensitiveMap.put(REDACTION_INDICATOR, true); - caseInSensitiveMap.put(HINT_ONLY, true); - caseInSensitiveMap.put(MUST_REDACT, true); - caseInSensitiveMap.put(PUBLISHED_INFORMATION, true); - caseInSensitiveMap.put(TEST_METHOD, false); - caseInSensitiveMap.put(PII, false); - caseInSensitiveMap.put(PURITY, false); - caseInSensitiveMap.put(IMAGE, true); - caseInSensitiveMap.put(OCR, true); - caseInSensitiveMap.put(SIGNATURE, true); - caseInSensitiveMap.put(LOGO, true); - caseInSensitiveMap.put(FORMULA, true); - caseInSensitiveMap.put(DOSSIER_REDACTIONS, false); - caseInSensitiveMap.put(IMPORTED_REDACTION, false); - caseInSensitiveMap.put(ROTATE_SIMPLE, true); - - recommendationTypeMap.put(VERTEBRATE, false); - recommendationTypeMap.put(ADDRESS, false); - recommendationTypeMap.put(AUTHOR, false); - recommendationTypeMap.put(SPONSOR, false); - recommendationTypeMap.put(NO_REDACTION_INDICATOR, false); - recommendationTypeMap.put(REDACTION_INDICATOR, false); - recommendationTypeMap.put(HINT_ONLY, false); - recommendationTypeMap.put(MUST_REDACT, false); - recommendationTypeMap.put(PUBLISHED_INFORMATION, false); - recommendationTypeMap.put(TEST_METHOD, false); - recommendationTypeMap.put(PII, false); - recommendationTypeMap.put(PURITY, false); - recommendationTypeMap.put(IMAGE, false); - recommendationTypeMap.put(OCR, false); - recommendationTypeMap.put(FORMULA, false); - recommendationTypeMap.put(SIGNATURE, false); - recommendationTypeMap.put(LOGO, false); - recommendationTypeMap.put(DOSSIER_REDACTIONS, false); - recommendationTypeMap.put(IMPORTED_REDACTION, false); - recommendationTypeMap.put(ROTATE_SIMPLE, false); - - rankTypeMap.put(PURITY, 155); - rankTypeMap.put(PII, 150); - rankTypeMap.put(ADDRESS, 140); - rankTypeMap.put(AUTHOR, 130); - rankTypeMap.put(SPONSOR, 120); - rankTypeMap.put(VERTEBRATE, 110); - rankTypeMap.put(MUST_REDACT, 100); - rankTypeMap.put(REDACTION_INDICATOR, 90); - rankTypeMap.put(NO_REDACTION_INDICATOR, 80); - rankTypeMap.put(PUBLISHED_INFORMATION, 70); - rankTypeMap.put(TEST_METHOD, 60); - rankTypeMap.put(HINT_ONLY, 50); - rankTypeMap.put(IMAGE, 30); - rankTypeMap.put(OCR, 29); - rankTypeMap.put(LOGO, 28); - rankTypeMap.put(SIGNATURE, 27); - rankTypeMap.put(FORMULA, 26); - rankTypeMap.put(DOSSIER_REDACTIONS, 200); - rankTypeMap.put(IMPORTED_REDACTION, 200); - rankTypeMap.put(ROTATE_SIMPLE, 150); - - colors.setSkippedColor("#cccccc"); - colors.setRequestAddColor("#04b093"); - colors.setRequestRemoveColor("#04b093"); - } - - - @SneakyThrows - private void loadNerForTest() { - - ClassPathResource responseJson = new ClassPathResource("files/ner_response.json"); - storageService.storeObject(TenantContext.getTenantId(), RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.NER_ENTITIES), responseJson.getInputStream()); - } - - - private List getTypeResponse() { - - return typeColorMap.entrySet() - .stream() - .map(typeColor -> Type.builder() - .id(typeColor.getKey() + ":" + TEST_DOSSIER_TEMPLATE_ID) - .type(typeColor.getKey()) - .dossierTemplateId(TEST_DOSSIER_TEMPLATE_ID) - .hexColor(typeColor.getValue()) - .isHint(hintTypeMap.get(typeColor.getKey())) - .isCaseInsensitive(caseInSensitiveMap.get(typeColor.getKey())) - .isRecommendation(recommendationTypeMap.get(typeColor.getKey())) - .rank(rankTypeMap.get(typeColor.getKey())) - .build()) - - .collect(Collectors.toList()); - } - - - private Type getDictionaryResponse(String type, boolean isDossierDictionary) { - - return Type.builder() - .id(type + ":" + TEST_DOSSIER_TEMPLATE_ID) - .hexColor(typeColorMap.get(type)) - .entries(isDossierDictionary ? toDictionaryEntry(dossierDictionary.get(type)) : toDictionaryEntry(dictionary.get(type))) - .falsePositiveEntries(falsePositive.containsKey(type) ? toDictionaryEntry(falsePositive.get(type)) : new ArrayList<>()) - .falseRecommendationEntries(falseRecommendation.containsKey(type) ? toDictionaryEntry(falseRecommendation.get(type)) : new ArrayList<>()) - .isHint(hintTypeMap.get(type)) - .isCaseInsensitive(caseInSensitiveMap.get(type)) - .isRecommendation(recommendationTypeMap.get(type)) - .rank(rankTypeMap.get(type)) - .build(); - } - - - private String cleanDictionaryEntry(String entry) { - - return TextNormalizationUtilities.removeHyphenLineBreaks(entry).replaceAll("\\n", " "); - } - - - private List toDictionaryEntry(List entries) { - - if (entries == null) { - entries = Collections.emptyList(); - } - - List dictionaryEntries = new ArrayList<>(); - entries.forEach(entry -> { - dictionaryEntries.add(DictionaryEntry.builder().value(entry).version(reanlysisVersions.getOrDefault(entry, 0L)).deleted(deleted.contains(entry)).build()); - }); - return dictionaryEntries; - } - - @Test public void testImportedRedactions() throws IOException { @@ -1599,7 +1188,8 @@ public class RedactionIntegrationTest { ClassPathResource importedRedactions = new ClassPathResource("files/ImportedRedactions/RotateTestFile_without_highlights.IMPORTED_REDACTIONS.json"); AnalyzeRequest request = prepareStorage("files/ImportedRedactions/RotateTestFile_without_highlights.pdf"); - storageService.storeObject(TenantContext.getTenantId(), RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.IMPORTED_REDACTIONS), + storageService.storeObject(TenantContext.getTenantId(), + RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.IMPORTED_REDACTIONS), importedRedactions.getInputStream()); analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); @@ -1659,39 +1249,4 @@ public class RedactionIntegrationTest { assertThat(values).contains("Mr. Tambourine Man"); } - - @SneakyThrows - private AnalyzeRequest prepareStorage(InputStream fileStream, InputStream cvServiceResponseFileStream) { - - AnalyzeRequest request = AnalyzeRequest.builder() - .dossierTemplateId(TEST_DOSSIER_TEMPLATE_ID) - .dossierId(TEST_DOSSIER_ID) - .fileId(TEST_FILE_ID) - .lastProcessed(OffsetDateTime.now()) - .build(); - - storageService.storeObject(TenantContext.getTenantId(), RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.TABLES), cvServiceResponseFileStream); - storageService.storeObject(TenantContext.getTenantId(), RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.ORIGIN), fileStream); - - return request; - - } - - - @SneakyThrows - private AnalyzeRequest prepareStorage(String file) { - - return prepareStorage(file, "files/cv_service_empty_response.json"); - } - - - @SneakyThrows - private AnalyzeRequest prepareStorage(String file, String cvServiceResponseFile) { - - ClassPathResource pdfFileResource = new ClassPathResource(file); - ClassPathResource cvServiceResponseFileResource = new ClassPathResource(cvServiceResponseFile); - - return prepareStorage(pdfFileResource.getInputStream(), cvServiceResponseFileResource.getInputStream()); - } - } diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTestV2.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTestV2.java new file mode 100644 index 00000000..5ed34a17 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTestV2.java @@ -0,0 +1,152 @@ +package com.iqser.red.service.redaction.v1.server; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +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.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +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.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.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.Engine; +import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogEntry; +import com.iqser.red.service.redaction.v1.model.StructureAnalyzeRequest; +import com.iqser.red.service.redaction.v1.server.multitenancy.TenantContext; +import com.iqser.red.storage.commons.StorageAutoConfiguration; +import com.iqser.red.storage.commons.service.StorageService; + +import lombok.SneakyThrows; + +@ExtendWith(SpringExtension.class) +@SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Import(RedactionIntegrationTest.RedactionIntegrationTestConfiguration.class) +public class RedactionIntegrationTestV2 extends AbstractRedactionIntegrationTest { + + private static final String RULES = loadFromClassPath("drools/rules_v2.drl"); + + @Configuration + @EnableAutoConfiguration(exclude = {RabbitAutoConfiguration.class, StorageAutoConfiguration.class}) + public static class RedactionIntegrationTestConfiguration { + + @Bean + public KieContainer kieContainer() { + + KieServices kieServices = KieServices.Factory.get(); + + KieFileSystem kieFileSystem = kieServices.newKieFileSystem(); + InputStream input = new ByteArrayInputStream(RULES.getBytes(StandardCharsets.UTF_8)); + kieFileSystem.write("src/test/resources/drools/rules_v2", 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(); + } + + } + + + @BeforeEach + public void stubClients() { + + TenantContext.setTenantId("redaction"); + + when(rulesClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(0L); + when(rulesClient.getRules(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(JSONPrimitive.of(RULES)); + + loadDictionaryForTest(); + loadTypeForTest(); + loadNerForTest(); + when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(0L); + when(dictionaryClient.getAllTypesForDossierTemplate(TEST_DOSSIER_TEMPLATE_ID, false)).thenReturn(getTypeResponse()); + + when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(0L); + when(dictionaryClient.getAllTypesForDossier(TEST_DOSSIER_ID, false)).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())); + + mockDictionaryCalls(null); + mockDictionaryCalls(0L); + + when(dictionaryClient.getColors(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(colors); + } + + + /** + * The case in this test: One term, 'Dr. Alan Miller', is found by PII-Rule and is in the PII-dictionary + * as well as in the PII-false-positive-list - and in the CBI-author dictionary. + * It gets redacted, as the PII-finding is false positive and so the CBI-author entry is effective + * independent of the entity-rank + */ + @Test + @SneakyThrows + public void testTermIsInTwoDictionariesAndInOneFalsePositive() { + + AnalyzeRequest request = prepareStorage("files/new/simplified2.pdf"); + + dictionary.clear(); + dictionary.computeIfAbsent(PII, v -> new ArrayList<>()).add("Dr. Alan Miller"); + dictionary.computeIfAbsent(AUTHOR, v -> new ArrayList<>()).add("Dr. Alan Miller"); + + falsePositive.clear(); + falsePositive.computeIfAbsent(PII, v -> new ArrayList<>()).add("Dr. Alan Miller COMPLETION DATE:"); + + analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); + analyzeService.analyze(request); + + var redactionLog = redactionStorageService.getRedactionLog(TEST_DOSSIER_ID, TEST_FILE_ID); + + assertThat(redactionLog.getRedactionLogEntry().size()).isEqualTo(1); + + RedactionLogEntry redactionLogEntry = redactionLog.getRedactionLogEntry().get(0); + + assertThat(redactionLogEntry.getType()).isEqualTo("CBI_author"); + assertThat(redactionLogEntry.getValue()).isEqualTo("Dr. Alan Miller"); + assertThat(redactionLogEntry.isRedacted()).isEqualTo(true); + assertThat(redactionLogEntry.isRecommendation()).isEqualTo(false); + assertThat(redactionLogEntry.isFalsePositive()).isEqualTo(false); + assertThat(redactionLogEntry.isExcluded()).isEqualTo(false); + assertThat(redactionLogEntry.isDictionaryEntry()).isEqualTo(true); + + assertThat(redactionLogEntry.getEngines().size()).isEqualTo(1); + assertThat(redactionLogEntry.getEngines().contains(Engine.DICTIONARY)).isEqualTo(true); + + } + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/rules_v2.drl b/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/rules_v2.drl new file mode 100644 index 00000000..44e24b98 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/rules_v2.drl @@ -0,0 +1,341 @@ +package drools + +import com.iqser.red.service.redaction.v1.server.redaction.model.Section + +global Section section + + +// --------------------------------------- AI rules ------------------------------------------------------------------- + +rule "0: Add CBI_author from ai" + when + Section(aiMatchesType("CBI_author")) + then + section.addAiEntities("CBI_author", "CBI_author"); + end + +rule "0: Combine address parts from ai to CBI_address (org is mandatory)" + when + Section(aiMatchesType("ORG")) + then + section.combineAiTypes("ORG", "STREET,POSTAL,COUNTRY,CARDINAL,CITY,STATE", 20, "CBI_address", 3, false); + end + +rule "0: Combine address parts from ai to CBI_address (street is mandatory)" + when + Section(aiMatchesType("STREET")) + then + section.combineAiTypes("STREET", "ORG,POSTAL,COUNTRY,CARDINAL,CITY,STATE", 20, "CBI_address", 3, false); + end + +rule "0: Combine address parts from ai to CBI_address (city is mandatory)" + when + Section(aiMatchesType("CITY")) + then + section.combineAiTypes("CITY", "ORG,STREET,POSTAL,COUNTRY,CARDINAL,STATE", 20, "CBI_address", 3, false); + end + +/* Syngenta specific laboratory recommendation */ +rule "0: Recommend CTL/BL laboratory that start with BL or CTL" + when + Section(searchText.contains("CT") || searchText.contains("BL")) + then + /* Regular expression: ((\b((([Cc]T(([1ILli\/])| L|~P))|(BL))[\. ]?([\dA-Ziltphz~\/.:!]| ?[\(',][Ppi](\(e)?|([\(-?']\/))+( ?[\(\/\dA-Znasieg]+)?)\b( ?\/? ?\d+)?)|(\bCT[L1i]\b)) */ + section.addRecommendationByRegEx("((\\b((([Cc]T(([1ILli\\/])| L|~P))|(BL))[\\. ]?([\\dA-Ziltphz~\\/.:!]| ?[\\(',][Ppi](\\(e)?|([\\(-?']\\/))+( ?[\\(\\/\\dA-Znasieg]+)?)\\b( ?\\/? ?\\d+)?)|(\\bCT[L1i]\\b))", true, 0, "CBI_address"); + end + + +// --------------------------------------- CBI rules ------------------------------------------------------------------- + +rule "1: Redact CBI Authors (Non vertebrate study)" + when + Section(!fileAttributeByLabelEqualsIgnoreCase("Vertebrate Study","Yes") && matchesType("CBI_author")) + then + section.redact("CBI_author", 1, "Author found", "Article 39(e)(3) of Regulation (EC) No 178/2002"); + end + +rule "2: Redact CBI Authors (Vertebrate study)" + when + Section(fileAttributeByLabelEqualsIgnoreCase("Vertebrate Study","Yes") && matchesType("CBI_author")) + then + section.redact("CBI_author", 2, "Author found", "Article 39(e)(2) of Regulation (EC) No 178/2002"); + end + + +rule "3: Redact not CBI Address (Non vertebrate study)" + when + Section(!fileAttributeByLabelEqualsIgnoreCase("Vertebrate Study","Yes") && matchesType("CBI_address")) + then + section.redactNot("CBI_address", 3, "Address found for non vertebrate study"); + section.ignoreRecommendations("CBI_address"); + end + +rule "4: Redact CBI Address (Vertebrate study)" + when + Section(fileAttributeByLabelEqualsIgnoreCase("Vertebrate Study","Yes") && matchesType("CBI_address")) + then + section.redact("CBI_address", 4, "Address found", "Article 39(e)(2) of Regulation (EC) No 178/2002"); + end + + +rule "5: Do not redact genitive CBI_author" + when + Section(matchesType("CBI_author")) + then + section.expandToFalsePositiveByRegEx("CBI_author", "['’’'ʼˈ´`‘′ʻ’']s", false, 0); + end + + +rule "6: Redact Author(s) cells in Tables with Author(s) header (Non vertebrate study)" + when + Section(!fileAttributeByLabelEqualsIgnoreCase("Vertebrate Study","Yes") && hasTableHeader("Author(s)") && !hasTableHeader("Vertebrate study Y/N")) + then + section.redactCell("Author(s)", 6, "CBI_author", false, "Author found", "Article 39(e)(3) of Regulation (EC) No 178/2002"); + end + +rule "7: Redact Author(s) cells in Tables with Author(s) header (Vertebrate study)" + when + Section(fileAttributeByLabelEqualsIgnoreCase("Vertebrate Study","Yes") && hasTableHeader("Author(s)") && !hasTableHeader("Vertebrate study Y/N")) + then + section.redactCell("Author(s)", 7, "CBI_author", false, "Author found", "Article 39(e)(2) of Regulation (EC) No 178/2002"); + end + + +rule "8: Redact Author cells in Tables with Author header (Non vertebrate study)" + when + Section(!fileAttributeByLabelEqualsIgnoreCase("Vertebrate Study","Yes") && hasTableHeader("Author") && !hasTableHeader("Vertebrate study Y/N")) + then + section.redactCell("Author", 8, "CBI_author", false, "Author found", "Article 39(e)(3) of Regulation (EC) No 178/2002"); + end + +rule "9: Redact Author cells in Tables with Author header (Vertebrate study)" + when + Section(fileAttributeByLabelEqualsIgnoreCase("Vertebrate Study","Yes") && hasTableHeader("Author") && !hasTableHeader("Vertebrate study Y/N")) + then + section.redactCell("Author", 9, "CBI_author", false, "Author found", "Article 39(e)(2) of Regulation (EC) No 178/2002"); + end + + +rule "10: Redact and recommand Authors in Tables with Vertebrate study Y/N header (Non vertebrate study)" + when + Section(!fileAttributeByLabelEqualsIgnoreCase("Vertebrate Study","Yes") && (rowEquals("Vertebrate study Y/N", "Y") || rowEquals("Vertebrate study Y/N", "Yes") || rowEquals("Vertebrate study Y/N", "N") || rowEquals("Vertebrate study Y/N", "No"))) + then + section.redactCell("Author(s)", 10, "CBI_author", true, "Author found", "Article 39(e)(3) of Regulation (EC) No 178/2002"); + end + +rule "11: Redact and recommand Authors in Tables with Vertebrate study Y/N header (Vertebrate study)" + when + Section(fileAttributeByLabelEqualsIgnoreCase("Vertebrate Study","Yes") && (rowEquals("Vertebrate study Y/N", "Y") || rowEquals("Vertebrate study Y/N", "Yes") || rowEquals("Vertebrate study Y/N", "N") || rowEquals("Vertebrate study Y/N", "No"))) + then + section.redactCell("Author(s)", 11, "CBI_author", true, "Author found", "Article 39(e)(2) of Regulation (EC) No 178/2002"); + end + +rule "14: Redact and add recommendation for et al. author (Non vertebrate study)" + when + Section(!fileAttributeByLabelEqualsIgnoreCase("Vertebrate Study","Yes") && searchText.contains("et al")) + then + section.redactAndRecommendByRegEx("\\b([A-ZÄÖÜ][^\\s\\.,]+( [A-ZÄÖÜ]{1,2}\\.?)?( ?[A-ZÄÖÜ]\\.?)?) et al\\.?", false, 1, "CBI_author", 14, "Author found", "Article 39(e)(3) of Regulation (EC) No 178/2002"); + end + +rule "15: Redact and add recommendation for et al. author (Vertebrate study)" + when + Section(fileAttributeByLabelEqualsIgnoreCase("Vertebrate Study","Yes") && searchText.contains("et al")) + then + section.redactAndRecommendByRegEx("\\b([A-ZÄÖÜ][^\\s\\.,]+( [A-ZÄÖÜ]{1,2}\\.?)?( ?[A-ZÄÖÜ]\\.?)?) et al\\.?", false, 1, "CBI_author", 15, "Author found", "Article 39(e)(2) of Regulation (EC) No 178/2002"); + end + + +rule "16: Add recommendation for Addresses in Test Organism sections" + when + Section(fileAttributeByLabelEqualsIgnoreCase("Vertebrate Study","Yes") && searchText.contains("Species:") && searchText.contains("Source:")) + then + section.recommendLineAfter("Source:", "CBI_address"); + end + +rule "17: Add recommendation for Addresses in Test Animals sections" + when + Section(fileAttributeByLabelEqualsIgnoreCase("Vertebrate Study","Yes") && searchText.contains("Species") && searchText.contains("Source")) + then + section.recommendLineAfter("Source", "CBI_address"); + end + + +rule "18: Do not redact Names and Addresses if Published Information found" + when + Section(matchesType("published_information")) + then + section.redactNotAndReference("CBI_author","published_information", 18, "Published Information found"); + section.redactNotAndReference("CBI_address","published_information", 18, "Published Information found"); + end + + +// --------------------------------------- PII rules ------------------------------------------------------------------- + + +rule "19: Redacted PII Personal Identification Information (Non vertebrate study)" + when + Section(!fileAttributeByLabelEqualsIgnoreCase("Vertebrate Study","Yes") && matchesType("PII")) + then + section.redact("PII", 19, "Personal information found", "Article 39(e)(3) of Regulation (EC) No 178/2002"); + end + +rule "20: Redacted PII Personal Identification Information (Vertebrate study)" + when + Section(fileAttributeByLabelEqualsIgnoreCase("Vertebrate Study","Yes") && matchesType("PII")) + then + section.redact("PII", 20, "Personal information found", "Article 39(e)(2) of Regulation (EC) No 178/2002"); + end + + +rule "21: Redact Emails by RegEx (Non vertebrate study)" + when + Section(!fileAttributeByLabelEqualsIgnoreCase("Vertebrate Study","Yes") && searchText.contains("@")) + then + section.redactByRegEx("\\b([A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z\\-]{1,23}[A-Za-z])\\b", true, 1, "PII", 21, "Personal information found", "Article 39(e)(3) of Regulation (EC) No 178/2002"); + end + +rule "22: Redact Emails by RegEx (Vertebrate study)" + when + Section(fileAttributeByLabelEqualsIgnoreCase("Vertebrate Study","Yes") && searchText.contains("@")) + then + section.redactByRegEx("\\b([A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z\\-]{1,23}[A-Za-z])\\b", true, 1, "PII", 22, "Personal information found", "Article 39(e)(2) of Regulation (EC) No 178/2002"); + end + + +rule "25: Redact Phone and Fax by RegEx (Non vertebrate study)" + when + Section(!fileAttributeByLabelEqualsIgnoreCase("Vertebrate Study","Yes") && ( + text.contains("Contact") + || text.contains("Telephone") + || text.contains("Phone") + || text.contains("Fax") + || text.contains("Tel") + || text.contains("Ter") + || text.contains("Mobile") + || text.contains("Fel") + || text.contains("Fer") + )) + then + section.redactByRegEx("\\b(contact|telephone|phone|fax|tel|ter|mobile|fel|fer)[a-zA-Z\\s]{0,10}[:.\\s]{0,3}([\\+\\d\\(][\\s\\d\\(\\)\\-\\/\\.]{4,100}\\d)\\b", true, 2, "PII", 25, "Personal information found", "Article 39(e)(3) of Regulation (EC) No 178/2002"); + end + +rule "26: Redact Phone and Fax by RegEx (Vertebrate study)" + when + Section(fileAttributeByLabelEqualsIgnoreCase("Vertebrate Study","Yes") && ( + text.contains("Contact") + || text.contains("Telephone") + || text.contains("Phone") + || text.contains("Fax") + || text.contains("Tel") + || text.contains("Ter") + || text.contains("Mobile") + || text.contains("Fel") + || text.contains("Fer") + )) + then + section.redactByRegEx("\\b(contact|telephone|phone|fax|tel|ter|mobile|fel|fer)[a-zA-Z\\s]{0,10}[:.\\s]{0,3}([\\+\\d\\(][\\s\\d\\(\\)\\-\\/\\.]{4,100}\\d)\\b", true, 2, "PII", 26, "Personal information found", "Article 39(e)(2) of Regulation (EC) No 178/2002"); + end + + +rule "27: Redact AUTHOR(S) (Non vertebrate study)" + when + Section(!fileAttributeByLabelEqualsIgnoreCase("Vertebrate Study","Yes") + && searchText.contains("AUTHOR(S):") + && searchText.contains("COMPLETION DATE:") + && !searchText.contains("STUDY COMPLETION DATE:") + ) + then + section.redactLinesBetween("AUTHOR(S):", "COMPLETION DATE:", "PII", 27, true, "Author found", "Article 39(e)(3) of Regulation (EC) No 178/2002"); + end + +rule "28: Redact AUTHOR(S) (Vertebrate study)" + when + Section(fileAttributeByLabelEqualsIgnoreCase("Vertebrate Study","Yes") + && searchText.contains("AUTHOR(S):") + && searchText.contains("COMPLETION DATE:") + && !searchText.contains("STUDY COMPLETION DATE:") + ) + then + section.redactLinesBetween("AUTHOR(S):", "COMPLETION DATE:", "PII", 28, true, "AUTHOR(S) was found", "Article 39(e)(2) of Regulation (EC) No 178/2002"); + end + + +rule "29: Redact AUTHOR(S) (Non vertebrate study)" + when + Section(!fileAttributeByLabelEqualsIgnoreCase("Vertebrate Study","Yes") + && searchText.contains("AUTHOR(S):") + && searchText.contains("STUDY COMPLETION DATE:") + ) + then + section.redactLinesBetween("AUTHOR(S):", "STUDY COMPLETION DATE:", "PII", 29, true, "AUTHOR(S) was found", "Article 39(e)(3) of Regulation (EC) No 178/2002"); + end + +rule "30: Redact AUTHOR(S) (Vertebrate study)" + when + Section(fileAttributeByLabelEqualsIgnoreCase("Vertebrate Study","Yes") + && searchText.contains("AUTHOR(S):") + && searchText.contains("STUDY COMPLETION DATE:") + ) + then + section.redactLinesBetween("AUTHOR(S):", "STUDY COMPLETION DATE:", "PII", 30, true, "AUTHOR(S) was found", "Article 39(e)(2) of Regulation (EC) No 178/2002"); + end + + +rule "31: Redact PERFORMING LABORATORY (Non vertebrate study)" + when + Section(!fileAttributeByLabelEqualsIgnoreCase("Vertebrate Study","Yes") + && searchText.contains("PERFORMING LABORATORY:") + ) + then + section.redactBetween("PERFORMING LABORATORY:", "LABORATORY PROJECT ID:", "CBI_address", 31, true, "PERFORMING LABORATORY was found", "Article 39(e)(3) of Regulation (EC) No 178/2002"); + section.redactNot("CBI_address", 31, "Performing laboratory found for non vertebrate study"); + end + +rule "32: Redact PERFORMING LABORATORY (Vertebrate study)" + when + Section(fileAttributeByLabelEqualsIgnoreCase("Vertebrate Study","Yes") + && searchText.contains("PERFORMING LABORATORY:")) + then + section.redactBetween("PERFORMING LABORATORY:", "LABORATORY PROJECT ID:", "CBI_address", 32, true, "PERFORMING LABORATORY was found", "Article 39(e)(2) of Regulation (EC) No 178/2002"); + end + + +// --------------------------------------- other rules ------------------------------------------------------------------- + +rule "33: Purity Hint" + when + Section(searchText.toLowerCase().contains("purity")) + then + section.addHintAnnotationByRegEx("(purity ?( of|\\(.{1,20}\\))?( ?:)?) .{0,5}[\\d\\.]+( .{0,4}\\.)? ?%", true, 1, "hint_only"); + end + + +rule "34: Ignore dossier_redaction entries if confidentiality is not 'confidential'" + when + Section(!fileAttributeByLabelEqualsIgnoreCase("Confidentiality","confidential") && matchesType("dossier_redaction")); + then + section.ignore("dossier_redaction"); + end + + +rule "35: Redact signatures (Non vertebrate study)" + when + Section(!fileAttributeByLabelEqualsIgnoreCase("Vertebrate Study","Yes") && matchesImageType("signature")) + then + section.redactImage("signature", 35, "Signature found", "Article 39(e)(3) of Regulation (EC) No 178/2002"); + end + +rule "36: Redact signatures (Vertebrate study)" + when + Section(fileAttributeByLabelEqualsIgnoreCase("Vertebrate Study","Yes") && matchesImageType("signature")) + then + section.redactImage("signature", 36, "Signature found", "Article 39(e)(2) of Regulation (EC) No 178/2002"); + end + + +rule "43: Redact Logos (Vertebrate study)" + when + Section(fileAttributeByLabelEqualsIgnoreCase("Vertebrate Study","Yes") && matchesImageType("logo")) + then + section.redactImage("logo", 43, "Logo found", "Article 39(e)(2) of Regulation (EC) No 178/2002"); + end diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/resources/files/new/simplified2.pdf b/redaction-service-v1/redaction-service-server-v1/src/test/resources/files/new/simplified2.pdf new file mode 100644 index 0000000000000000000000000000000000000000..32eac69fc43c06d1991b29522d6b6e9df3543801 GIT binary patch literal 69833 zcmdRVWmH_twk`yB*Wm6n?(Xic4K(iV+DNcK2=4AKAq01V2Y1&%a0rs{NcP$1?0xUK z_r5pY`|}u!UbSk?S##E$-~6ho$7pI*Noi&_79JEt>gJa}6ht;M0GX4SEefKbAgiX2 zGl*3J=nk}ZvSL*QT7lfi*k4|&vnm1|t>`R3%#u>{WULzQt{&#@nyw&_x|5ST8T%h1 zN^n*!}Z1Ni=)y`j_$v1Y-C)&-$}ERvHc+>%|XV-`9~io85`fP*2`3+yxnCq z+=1?(7bO`DHUQbn^=r(RHXGT?^@o(ItCP6~$lZYTCDg2%Aa8e8Ifs{6ivMww_~Rz` z%kh`kSl9r?oxBZRQ~_jMd|WIX>}0&0Y%F~2Mr1G3b##Am+>PuHPh?!3Je*%F|IX6y z2UZPMO;@0!oAa*;n)|RyXs}9xJZ;QD>N4UlqiX`q++IZf$k!i1a3cf!HD134{xe?E zHumlyS5|5Jm-$J8%$+PitcoB(`y7WFaV8#BjfoN@%2#?JB`4l=)a!r;GR z(joSr$3aw#yo-nE;gqIBFix_j^6!|Rn?doPopUwNxylKqvfnsnXr@5t#WuEeF2RUD zYj>M@NJRJCcrsl?(O{VIFcSI@)gV!T?;ZnY$I}O=+h1u740vt9w0L>o+!v#Hz_`pn zo)njkei`k%1}o>SspN8?$<=N?oMB%!U?WCB9~a^$<4lmk0xim**yx&aRI)J&j43jt z$ZXX<4(|*%%Yar9e2*JeA7mZh>C5uHd!!N-A{O1&7QhAfB%*N$mCbP?j{#&g5t!bc z#>B`@l#p*Atb+9;^)9EYTr~)nh4p+R3M~1DG2_TQ=q&^~ii^U(43Ul6n{#v2E6^=T zk4g>1VwNL;9THnvt4l;DyWWg#ezKE)&3bW4p#KS>S^0JEVq^~0H`>|d3s`EIH8`dENsnlmSjOO{?#mzln?mK_J9y za{jKWKXdnIf&Ie!pNapCr(dPOs^MYg{)+;B^#EAKfo`DR-Qp6GQc_Zk569gxEM*vC3JzK-9+FhgtRodv4}027r$4ztr5wxL=H@vuZkNIokY!6B+04 z+WIU0eo6dx!+?zEm#`7pzc}Dm$Dc0#x32u@wS=7HFT1~-XaA!}etZAYt?48qC#eK< z{-?97k~%N73jnabywvbisYA zKRr}&wE(%k#Dnh7c)Sp$I>^fACGb9UVlS))`o)|c&d&BAhhJp$w=(=?;?IHpnT>zB z@SCTUfes+nf0v1ug0*rZ<6vi%d*N*xb1_FNdk`6bRqR(T+{t*j0RPB~I~gOyXQ~n{gsgZ>dBvw z`oHHQw%>&KJJU28!Z4p`y!^~Wbr>pyc_~7Jj_A= zuN?a;TseNh=Wi%bF|*ZhcVJb1IjQ`y-jK0zyj0D9y`ksk{x#sAn?mobVofEL9RPX< z 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 z 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 literal 0 HcmV?d00001 From 6e5b64ca94e6a9076249ce660a05e5f3c679f4e9 Mon Sep 17 00:00:00 2001 From: Thomas Beyer Date: Tue, 28 Mar 2023 10:35:27 +0200 Subject: [PATCH 5/8] RED-6411 - fix import of configuration --- .../service/redaction/v1/server/RedactionIntegrationTestV2.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTestV2.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTestV2.java index 5ed34a17..f7f88f99 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTestV2.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTestV2.java @@ -40,7 +40,7 @@ import lombok.SneakyThrows; @ExtendWith(SpringExtension.class) @SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@Import(RedactionIntegrationTest.RedactionIntegrationTestConfiguration.class) +@Import(RedactionIntegrationTestV2.RedactionIntegrationTestConfiguration.class) public class RedactionIntegrationTestV2 extends AbstractRedactionIntegrationTest { private static final String RULES = loadFromClassPath("drools/rules_v2.drl"); From 4e76c007b8e301ffd2f84de226591d5cf2915244 Mon Sep 17 00:00:00 2001 From: Thomas Beyer Date: Tue, 28 Mar 2023 10:41:37 +0200 Subject: [PATCH 6/8] RED-6411 - rename testclass because of regexp --- ...onTestV2.java => RedactionIntegrationWithRulesV2Test.java} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/{RedactionIntegrationTestV2.java => RedactionIntegrationWithRulesV2Test.java} (97%) diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTestV2.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationWithRulesV2Test.java similarity index 97% rename from redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTestV2.java rename to redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationWithRulesV2Test.java index f7f88f99..7b3d6919 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTestV2.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationWithRulesV2Test.java @@ -40,8 +40,8 @@ import lombok.SneakyThrows; @ExtendWith(SpringExtension.class) @SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@Import(RedactionIntegrationTestV2.RedactionIntegrationTestConfiguration.class) -public class RedactionIntegrationTestV2 extends AbstractRedactionIntegrationTest { +@Import(RedactionIntegrationWithRulesV2Test.RedactionIntegrationTestConfiguration.class) +public class RedactionIntegrationWithRulesV2Test extends AbstractRedactionIntegrationTest { private static final String RULES = loadFromClassPath("drools/rules_v2.drl"); From 710747d00b4b6d92d739d72f8564e60da7014c9a Mon Sep 17 00:00:00 2001 From: Thomas Beyer Date: Tue, 28 Mar 2023 11:28:43 +0200 Subject: [PATCH 7/8] RED-6411 - optimize method --- .../v1/server/redaction/utils/EntitySearchUtils.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 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 ff22105f..20e8e9f0 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 @@ -289,14 +289,14 @@ public final class EntitySearchUtils { } } + private boolean isOneARecommendationAndTheOtherEntity(Entity entityOne, Entity entityTwo) { - private boolean isOneARecommendationAndTheOtherEntity (Entity found, Entity existing) { - - return existing.getEntityType().equals(EntityType.RECOMMENDATION) && found.getEntityType().equals(EntityType.ENTITY) || existing.getEntityType() - .equals(EntityType.ENTITY) && found.getEntityType().equals(EntityType.RECOMMENDATION); + var entityTypeOne = entityOne.getEntityType(); + var entityTypeTwo = entityTwo.getEntityType(); + return entityTypeTwo.equals(EntityType.RECOMMENDATION) && entityTypeOne.equals(EntityType.ENTITY) + || entityTypeTwo.equals(EntityType.ENTITY) && entityTypeOne.equals(EntityType.RECOMMENDATION); } - public void addEntitiesIgnoreRank(Set entities, Set found) { // HashSet keeps old value but we want the new. entities.removeAll(found); From 2a56187294c125931d4b998ee1f812ea426e2b58 Mon Sep 17 00:00:00 2001 From: Thomas Beyer Date: Wed, 29 Mar 2023 09:35:55 +0200 Subject: [PATCH 8/8] RED-6411 - optimize code --- .../AbstractRedactionIntegrationTest.java | 379 +++++++++--------- .../v1/server/RedactionIntegrationTest.java | 121 +++--- ...t.java => RedactionIntegrationTestV2.java} | 31 +- 3 files changed, 254 insertions(+), 277 deletions(-) rename redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/{RedactionIntegrationWithRulesV2Test.java => RedactionIntegrationTestV2.java} (85%) diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/AbstractRedactionIntegrationTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/AbstractRedactionIntegrationTest.java index 70202114..4cdf09c0 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/AbstractRedactionIntegrationTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/AbstractRedactionIntegrationTest.java @@ -2,13 +2,10 @@ package com.iqser.red.service.redaction.v1.server; import static org.mockito.Mockito.when; -import java.io.BufferedReader; import java.io.File; -import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.net.URL; -import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.Collections; @@ -50,26 +47,48 @@ import lombok.SneakyThrows; public abstract class AbstractRedactionIntegrationTest { - protected static final String VERTEBRATE = "vertebrate"; - protected static final String ADDRESS = "CBI_address"; - protected static final String AUTHOR = "CBI_author"; - protected static final String SPONSOR = "CBI_sponsor"; + protected static final String VERTEBRATE_INDICATOR = "vertebrate"; + protected static final String DICTIONARY_ADDRESS = "CBI_address"; + protected static final String DICTIONARY_AUTHOR = "CBI_author"; + protected static final String DICTIONARY_SPONSOR = "CBI_sponsor"; + protected static final String DICTIONARY_PII = "PII"; protected static final String NO_REDACTION_INDICATOR = "no_redaction_indicator"; protected static final String REDACTION_INDICATOR = "redaction_indicator"; - protected static final String HINT_ONLY = "hint_only"; - protected static final String MUST_REDACT = "must_redact"; - protected static final String PUBLISHED_INFORMATION = "published_information"; - protected static final String TEST_METHOD = "test_method"; - protected static final String PURITY = "purity"; - protected static final String IMAGE = "image"; - protected static final String LOGO = "logo"; - protected static final String SIGNATURE = "signature"; - protected static final String FORMULA = "formula"; - protected static final String OCR = "ocr"; - protected static final String DOSSIER_REDACTIONS = "dossier_redactions"; - protected static final String IMPORTED_REDACTION = "imported_redaction"; - protected static final String PII = "PII"; - protected static final String ROTATE_SIMPLE = "RotateSimple"; + protected static final String HINT_ONLY_INDICATOR = "hint_only"; + protected static final String MUST_REDACT_INDICATOR = "must_redact"; + protected static final String PUBLISHED_INFORMATION_INDICATOR = "published_information"; + protected static final String TEST_METHOD_INDICATOR = "test_method"; + protected static final String PURITY_INDICATOR = "purity"; + protected static final String IMAGE_INDICATOR = "image"; + protected static final String LOGO_INDICATOR = "logo"; + protected static final String SIGNATURE_INDICATOR = "signature"; + protected static final String FORMULA_INDICATOR = "formula"; + protected static final String OCR_INDICATOR = "ocr"; + protected static final String DOSSIER_REDACTIONS_INDICATOR = "dossier_redactions"; + protected static final String IMPORTED_REDACTION_INDICATOR = "imported_redaction"; + protected static final String ROTATE_SIMPLE_INDICATOR = "RotateSimple"; + + protected final static String TEST_DOSSIER_TEMPLATE_ID = "123"; + public static final String IMPORTED_REDACTION_TYPE_ID = IMPORTED_REDACTION_INDICATOR + ":" + TEST_DOSSIER_TEMPLATE_ID; + public static final String DOSSIER_REDACTIONS_TYPE_ID = DOSSIER_REDACTIONS_INDICATOR + ":" + TEST_DOSSIER_TEMPLATE_ID; + public static final String ROTATE_SIMPLE_TYPE_ID = ROTATE_SIMPLE_INDICATOR + ":" + TEST_DOSSIER_TEMPLATE_ID; + public static final String FORMULA_TYPE_ID = FORMULA_INDICATOR + ":" + TEST_DOSSIER_TEMPLATE_ID; + public static final String SIGNATURE_TYPE_ID = SIGNATURE_INDICATOR + ":" + TEST_DOSSIER_TEMPLATE_ID; + public static final String LOGO_TYPE_ID = LOGO_INDICATOR + ":" + TEST_DOSSIER_TEMPLATE_ID; + public static final String OCR_TYPE_ID = OCR_INDICATOR + ":" + TEST_DOSSIER_TEMPLATE_ID; + public static final String IMAGE_TYPE_ID = IMAGE_INDICATOR + ":" + TEST_DOSSIER_TEMPLATE_ID; + public static final String PURITY_TYPE_ID = PURITY_INDICATOR + ":" + TEST_DOSSIER_TEMPLATE_ID; + public static final String PII_TYPE_ID = DICTIONARY_PII + ":" + TEST_DOSSIER_TEMPLATE_ID; + public static final String TEST_METHOD_TYPE_ID = TEST_METHOD_INDICATOR + ":" + TEST_DOSSIER_TEMPLATE_ID; + public static final String PUBLISHED_INFORMATION_TYPE_ID = PUBLISHED_INFORMATION_INDICATOR + ":" + TEST_DOSSIER_TEMPLATE_ID; + public static final String MUST_REDACT_TYPE_ID = MUST_REDACT_INDICATOR + ":" + TEST_DOSSIER_TEMPLATE_ID; + public static final String HINT_ONLY_TYPE_ID = HINT_ONLY_INDICATOR + ":" + TEST_DOSSIER_TEMPLATE_ID; + public static final String REDACTION_TYPE_ID = REDACTION_INDICATOR + ":" + TEST_DOSSIER_TEMPLATE_ID; + public static final String NO_REDACTION_TYPE_ID = NO_REDACTION_INDICATOR + ":" + TEST_DOSSIER_TEMPLATE_ID; + public static final String SPONSOR_TYPE_ID = DICTIONARY_SPONSOR + ":" + TEST_DOSSIER_TEMPLATE_ID; + public static final String AUTHOR_TYPE_ID = DICTIONARY_AUTHOR + ":" + TEST_DOSSIER_TEMPLATE_ID; + public static final String ADDRESS_TYPE_ID = DICTIONARY_ADDRESS + ":" + TEST_DOSSIER_TEMPLATE_ID; + public static final String VERTEBRATE_TYPE_ID = VERTEBRATE_INDICATOR + ":" + TEST_DOSSIER_TEMPLATE_ID; @Autowired protected RedactionController redactionController; @@ -114,7 +133,6 @@ public abstract class AbstractRedactionIntegrationTest { protected final Map reanlysisVersions = new HashMap<>(); protected final Set deleted = new HashSet<>(); - protected final static String TEST_DOSSIER_TEMPLATE_ID = "123"; protected final static String TEST_DOSSIER_ID = "123"; protected final static String TEST_FILE_ID = "123"; @@ -136,85 +154,71 @@ public abstract class AbstractRedactionIntegrationTest { protected void mockDictionaryCalls(Long version) { - when(dictionaryClient.getDictionaryForType(VERTEBRATE + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(VERTEBRATE, - false)); - when(dictionaryClient.getDictionaryForType(ADDRESS + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(ADDRESS, false)); - when(dictionaryClient.getDictionaryForType(AUTHOR + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(AUTHOR, false)); - when(dictionaryClient.getDictionaryForType(SPONSOR + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(SPONSOR, false)); - when(dictionaryClient.getDictionaryForType(NO_REDACTION_INDICATOR + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse( - NO_REDACTION_INDICATOR, - false)); - when(dictionaryClient.getDictionaryForType(REDACTION_INDICATOR + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse( - REDACTION_INDICATOR, - false)); - when(dictionaryClient.getDictionaryForType(HINT_ONLY + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(HINT_ONLY, false)); - when(dictionaryClient.getDictionaryForType(MUST_REDACT + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(MUST_REDACT, - false)); - when(dictionaryClient.getDictionaryForType(PUBLISHED_INFORMATION + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse( - PUBLISHED_INFORMATION, - false)); - when(dictionaryClient.getDictionaryForType(TEST_METHOD + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(TEST_METHOD, - false)); - when(dictionaryClient.getDictionaryForType(PII + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(PII, false)); - when(dictionaryClient.getDictionaryForType(PURITY + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(PURITY, false)); - when(dictionaryClient.getDictionaryForType(IMAGE + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(IMAGE, false)); - when(dictionaryClient.getDictionaryForType(OCR + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(OCR, false)); - when(dictionaryClient.getDictionaryForType(LOGO + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(LOGO, false)); - when(dictionaryClient.getDictionaryForType(SIGNATURE + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(SIGNATURE, false)); - when(dictionaryClient.getDictionaryForType(FORMULA + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(FORMULA, false)); - when(dictionaryClient.getDictionaryForType(ROTATE_SIMPLE + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse(ROTATE_SIMPLE, - false)); - when(dictionaryClient.getDictionaryForType(DOSSIER_REDACTIONS + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse( - DOSSIER_REDACTIONS, - true)); - when(dictionaryClient.getDictionaryForType(IMPORTED_REDACTION + ":" + TEST_DOSSIER_TEMPLATE_ID, version)).then((Answer) invocation -> getDictionaryResponse( - IMPORTED_REDACTION, - true)); + when(dictionaryClient.getDictionaryForType(VERTEBRATE_TYPE_ID, version)).then((Answer) invocation -> getDictionaryResponse(VERTEBRATE_INDICATOR, false)); + when(dictionaryClient.getDictionaryForType(ADDRESS_TYPE_ID, version)).then((Answer) invocation -> getDictionaryResponse(DICTIONARY_ADDRESS, false)); + when(dictionaryClient.getDictionaryForType(AUTHOR_TYPE_ID, version)).then((Answer) invocation -> getDictionaryResponse(DICTIONARY_AUTHOR, false)); + when(dictionaryClient.getDictionaryForType(SPONSOR_TYPE_ID, version)).then((Answer) invocation -> getDictionaryResponse(DICTIONARY_SPONSOR, false)); + when(dictionaryClient.getDictionaryForType(NO_REDACTION_TYPE_ID, version)).then((Answer) invocation -> getDictionaryResponse(NO_REDACTION_INDICATOR, false)); + when(dictionaryClient.getDictionaryForType(REDACTION_TYPE_ID, version)).then((Answer) invocation -> getDictionaryResponse(REDACTION_INDICATOR, false)); + when(dictionaryClient.getDictionaryForType(HINT_ONLY_TYPE_ID, version)).then((Answer) invocation -> getDictionaryResponse(HINT_ONLY_INDICATOR, false)); + when(dictionaryClient.getDictionaryForType(MUST_REDACT_TYPE_ID, version)).then((Answer) invocation -> getDictionaryResponse(MUST_REDACT_INDICATOR, false)); + when(dictionaryClient.getDictionaryForType(PUBLISHED_INFORMATION_TYPE_ID, version)).then((Answer) invocation -> getDictionaryResponse(PUBLISHED_INFORMATION_INDICATOR, false)); + when(dictionaryClient.getDictionaryForType(TEST_METHOD_TYPE_ID, version)).then((Answer) invocation -> getDictionaryResponse(TEST_METHOD_INDICATOR, false)); + when(dictionaryClient.getDictionaryForType(PII_TYPE_ID, version)).then((Answer) invocation -> getDictionaryResponse(DICTIONARY_PII, false)); + when(dictionaryClient.getDictionaryForType(PURITY_TYPE_ID, version)).then((Answer) invocation -> getDictionaryResponse(PURITY_INDICATOR, false)); + when(dictionaryClient.getDictionaryForType(IMAGE_TYPE_ID, version)).then((Answer) invocation -> getDictionaryResponse(IMAGE_INDICATOR, false)); + when(dictionaryClient.getDictionaryForType(OCR_TYPE_ID, version)).then((Answer) invocation -> getDictionaryResponse(OCR_INDICATOR, false)); + when(dictionaryClient.getDictionaryForType(LOGO_TYPE_ID, version)).then((Answer) invocation -> getDictionaryResponse(LOGO_INDICATOR, false)); + when(dictionaryClient.getDictionaryForType(SIGNATURE_TYPE_ID, version)).then((Answer) invocation -> getDictionaryResponse(SIGNATURE_INDICATOR, false)); + when(dictionaryClient.getDictionaryForType(FORMULA_TYPE_ID, version)).then((Answer) invocation -> getDictionaryResponse(FORMULA_INDICATOR, false)); + when(dictionaryClient.getDictionaryForType(ROTATE_SIMPLE_TYPE_ID, version)).then((Answer) invocation -> getDictionaryResponse(ROTATE_SIMPLE_INDICATOR, false)); + when(dictionaryClient.getDictionaryForType(DOSSIER_REDACTIONS_TYPE_ID, version)).then((Answer) invocation -> getDictionaryResponse(DOSSIER_REDACTIONS_INDICATOR,true)); + when(dictionaryClient.getDictionaryForType(IMPORTED_REDACTION_TYPE_ID, version)).then((Answer) invocation -> getDictionaryResponse(IMPORTED_REDACTION_INDICATOR,true)); } protected void loadDictionaryForTest() { - dictionary.computeIfAbsent(AUTHOR, v -> new ArrayList<>()) + dictionary.computeIfAbsent(DICTIONARY_AUTHOR, v -> new ArrayList<>()) .addAll(ResourceLoader.load("dictionaries/CBI_author.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dictionary.computeIfAbsent(SPONSOR, v -> new ArrayList<>()) + dictionary.computeIfAbsent(DICTIONARY_SPONSOR, v -> new ArrayList<>()) .addAll(ResourceLoader.load("dictionaries/CBI_sponsor.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dictionary.computeIfAbsent(VERTEBRATE, v -> new ArrayList<>()) + dictionary.computeIfAbsent(VERTEBRATE_INDICATOR, v -> new ArrayList<>()) .addAll(ResourceLoader.load("dictionaries/vertebrate.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dictionary.computeIfAbsent(ADDRESS, v -> new ArrayList<>()) + dictionary.computeIfAbsent(DICTIONARY_ADDRESS, v -> new ArrayList<>()) .addAll(ResourceLoader.load("dictionaries/CBI_address.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); dictionary.computeIfAbsent(NO_REDACTION_INDICATOR, v -> new ArrayList<>()) .addAll(ResourceLoader.load("dictionaries/no_redaction_indicator.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); dictionary.computeIfAbsent(REDACTION_INDICATOR, v -> new ArrayList<>()) .addAll(ResourceLoader.load("dictionaries/redaction_indicator.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dictionary.computeIfAbsent(HINT_ONLY, v -> new ArrayList<>()) + dictionary.computeIfAbsent(HINT_ONLY_INDICATOR, v -> new ArrayList<>()) .addAll(ResourceLoader.load("dictionaries/hint_only.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dictionary.computeIfAbsent(MUST_REDACT, v -> new ArrayList<>()) + dictionary.computeIfAbsent(MUST_REDACT_INDICATOR, v -> new ArrayList<>()) .addAll(ResourceLoader.load("dictionaries/must_redact.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dictionary.computeIfAbsent(PUBLISHED_INFORMATION, v -> new ArrayList<>()) + dictionary.computeIfAbsent(PUBLISHED_INFORMATION_INDICATOR, v -> new ArrayList<>()) .addAll(ResourceLoader.load("dictionaries/published_information.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dictionary.computeIfAbsent(TEST_METHOD, v -> new ArrayList<>()) + dictionary.computeIfAbsent(TEST_METHOD_INDICATOR, v -> new ArrayList<>()) .addAll(ResourceLoader.load("dictionaries/test_method.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dictionary.computeIfAbsent(PII, v -> new ArrayList<>()) + dictionary.computeIfAbsent(DICTIONARY_PII, v -> new ArrayList<>()) .addAll(ResourceLoader.load("dictionaries/PII.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dictionary.computeIfAbsent(PURITY, v -> new ArrayList<>()) + dictionary.computeIfAbsent(PURITY_INDICATOR, v -> new ArrayList<>()) .addAll(ResourceLoader.load("dictionaries/purity.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dictionary.computeIfAbsent(IMAGE, v -> new ArrayList<>()) + dictionary.computeIfAbsent(IMAGE_INDICATOR, v -> new ArrayList<>()) .addAll(ResourceLoader.load("dictionaries/empty.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dictionary.computeIfAbsent(OCR, v -> new ArrayList<>()) + dictionary.computeIfAbsent(OCR_INDICATOR, v -> new ArrayList<>()) .addAll(ResourceLoader.load("dictionaries/empty.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dictionary.computeIfAbsent(LOGO, v -> new ArrayList<>()) + dictionary.computeIfAbsent(LOGO_INDICATOR, v -> new ArrayList<>()) .addAll(ResourceLoader.load("dictionaries/empty.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dictionary.computeIfAbsent(SIGNATURE, v -> new ArrayList<>()) + dictionary.computeIfAbsent(SIGNATURE_INDICATOR, v -> new ArrayList<>()) .addAll(ResourceLoader.load("dictionaries/empty.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dictionary.computeIfAbsent(FORMULA, v -> new ArrayList<>()) + dictionary.computeIfAbsent(FORMULA_INDICATOR, v -> new ArrayList<>()) .addAll(ResourceLoader.load("dictionaries/empty.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dossierDictionary.computeIfAbsent(DOSSIER_REDACTIONS, v -> new ArrayList<>()) + dossierDictionary.computeIfAbsent(DOSSIER_REDACTIONS_INDICATOR, v -> new ArrayList<>()) .addAll(ResourceLoader.load("dictionaries/dossier_redactions.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); - dossierDictionary.put(IMPORTED_REDACTION, new ArrayList<>()); + dossierDictionary.put(IMPORTED_REDACTION_INDICATOR, new ArrayList<>()); - falsePositive.computeIfAbsent(PII, v -> new ArrayList<>()) + falsePositive.computeIfAbsent(DICTIONARY_PII, v -> new ArrayList<>()) .addAll(ResourceLoader.load("dictionaries/PII_false_positive.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); } @@ -223,153 +227,128 @@ public abstract class AbstractRedactionIntegrationTest { protected void loadOnlyDictionaryForSimpleFile() { dictionary.clear(); - dictionary.computeIfAbsent(ROTATE_SIMPLE, v -> new ArrayList<>()) + dictionary.computeIfAbsent(ROTATE_SIMPLE_INDICATOR, v -> new ArrayList<>()) .addAll(ResourceLoader.load("dictionaries/RotateTestFileSimple.txt").stream().map(this::cleanDictionaryEntry).collect(Collectors.toSet())); } + @SneakyThrows protected 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); - } - } - - - protected List getPathsRecursively(File path) { - - List result = new ArrayList<>(); - if (path == null || path.listFiles() == null) { - return result; - } - for (File f : path.listFiles()) { - if (f.isFile()) { - result.add(f); - } else { - result.addAll(getPathsRecursively(f)); - } - } - return result; - + List stringList = Files.readAllLines(new File(resource.getPath()).toPath()); + return String.join("\n", stringList); } protected void loadTypeForTest() { - typeColorMap.put(VERTEBRATE, "#ff85f7"); - typeColorMap.put(ADDRESS, "#ffe187"); - typeColorMap.put(AUTHOR, "#ffe187"); - typeColorMap.put(SPONSOR, "#85ebff"); + typeColorMap.put(VERTEBRATE_INDICATOR, "#ff85f7"); + typeColorMap.put(DICTIONARY_ADDRESS, "#ffe187"); + typeColorMap.put(DICTIONARY_AUTHOR, "#ffe187"); + typeColorMap.put(DICTIONARY_SPONSOR, "#85ebff"); typeColorMap.put(NO_REDACTION_INDICATOR, "#be85ff"); typeColorMap.put(REDACTION_INDICATOR, "#caff85"); - typeColorMap.put(HINT_ONLY, "#abc0c4"); - typeColorMap.put(MUST_REDACT, "#fab4c0"); - typeColorMap.put(PUBLISHED_INFORMATION, "#85ebff"); - typeColorMap.put(TEST_METHOD, "#91fae8"); - typeColorMap.put(PII, "#66ccff"); - typeColorMap.put(PURITY, "#ffe187"); - typeColorMap.put(IMAGE, "#fcc5fb"); - typeColorMap.put(OCR, "#fcc5fb"); - typeColorMap.put(LOGO, "#ffe187"); - typeColorMap.put(FORMULA, "#ffe187"); - typeColorMap.put(SIGNATURE, "#ffe187"); - typeColorMap.put(IMPORTED_REDACTION, "#fcfbe6"); - typeColorMap.put(ROTATE_SIMPLE, "#66ccff"); + typeColorMap.put(HINT_ONLY_INDICATOR, "#abc0c4"); + typeColorMap.put(MUST_REDACT_INDICATOR, "#fab4c0"); + typeColorMap.put(PUBLISHED_INFORMATION_INDICATOR, "#85ebff"); + typeColorMap.put(TEST_METHOD_INDICATOR, "#91fae8"); + typeColorMap.put(DICTIONARY_PII, "#66ccff"); + typeColorMap.put(PURITY_INDICATOR, "#ffe187"); + typeColorMap.put(IMAGE_INDICATOR, "#fcc5fb"); + typeColorMap.put(OCR_INDICATOR, "#fcc5fb"); + typeColorMap.put(LOGO_INDICATOR, "#ffe187"); + typeColorMap.put(FORMULA_INDICATOR, "#ffe187"); + typeColorMap.put(SIGNATURE_INDICATOR, "#ffe187"); + typeColorMap.put(IMPORTED_REDACTION_INDICATOR, "#fcfbe6"); + typeColorMap.put(ROTATE_SIMPLE_INDICATOR, "#66ccff"); - hintTypeMap.put(VERTEBRATE, true); - hintTypeMap.put(ADDRESS, false); - hintTypeMap.put(AUTHOR, false); - hintTypeMap.put(SPONSOR, false); + hintTypeMap.put(VERTEBRATE_INDICATOR, true); + hintTypeMap.put(DICTIONARY_ADDRESS, false); + hintTypeMap.put(DICTIONARY_AUTHOR, false); + hintTypeMap.put(DICTIONARY_SPONSOR, false); hintTypeMap.put(NO_REDACTION_INDICATOR, true); hintTypeMap.put(REDACTION_INDICATOR, true); - hintTypeMap.put(HINT_ONLY, true); - hintTypeMap.put(MUST_REDACT, true); - hintTypeMap.put(PUBLISHED_INFORMATION, true); - hintTypeMap.put(TEST_METHOD, true); - hintTypeMap.put(PII, false); - hintTypeMap.put(PURITY, false); - hintTypeMap.put(IMAGE, true); - hintTypeMap.put(OCR, true); - hintTypeMap.put(FORMULA, false); - hintTypeMap.put(LOGO, false); - hintTypeMap.put(SIGNATURE, false); - hintTypeMap.put(DOSSIER_REDACTIONS, false); - hintTypeMap.put(IMPORTED_REDACTION, false); - hintTypeMap.put(ROTATE_SIMPLE, false); + hintTypeMap.put(HINT_ONLY_INDICATOR, true); + hintTypeMap.put(MUST_REDACT_INDICATOR, true); + hintTypeMap.put(PUBLISHED_INFORMATION_INDICATOR, true); + hintTypeMap.put(TEST_METHOD_INDICATOR, true); + hintTypeMap.put(DICTIONARY_PII, false); + hintTypeMap.put(PURITY_INDICATOR, false); + hintTypeMap.put(IMAGE_INDICATOR, true); + hintTypeMap.put(OCR_INDICATOR, true); + hintTypeMap.put(FORMULA_INDICATOR, false); + hintTypeMap.put(LOGO_INDICATOR, false); + hintTypeMap.put(SIGNATURE_INDICATOR, false); + hintTypeMap.put(DOSSIER_REDACTIONS_INDICATOR, false); + hintTypeMap.put(IMPORTED_REDACTION_INDICATOR, false); + hintTypeMap.put(ROTATE_SIMPLE_INDICATOR, false); - caseInSensitiveMap.put(VERTEBRATE, true); - caseInSensitiveMap.put(ADDRESS, false); - caseInSensitiveMap.put(AUTHOR, false); - caseInSensitiveMap.put(SPONSOR, false); + caseInSensitiveMap.put(VERTEBRATE_INDICATOR, true); + caseInSensitiveMap.put(DICTIONARY_ADDRESS, false); + caseInSensitiveMap.put(DICTIONARY_AUTHOR, false); + caseInSensitiveMap.put(DICTIONARY_SPONSOR, false); caseInSensitiveMap.put(NO_REDACTION_INDICATOR, true); caseInSensitiveMap.put(REDACTION_INDICATOR, true); - caseInSensitiveMap.put(HINT_ONLY, true); - caseInSensitiveMap.put(MUST_REDACT, true); - caseInSensitiveMap.put(PUBLISHED_INFORMATION, true); - caseInSensitiveMap.put(TEST_METHOD, false); - caseInSensitiveMap.put(PII, false); - caseInSensitiveMap.put(PURITY, false); - caseInSensitiveMap.put(IMAGE, true); - caseInSensitiveMap.put(OCR, true); - caseInSensitiveMap.put(SIGNATURE, true); - caseInSensitiveMap.put(LOGO, true); - caseInSensitiveMap.put(FORMULA, true); - caseInSensitiveMap.put(DOSSIER_REDACTIONS, false); - caseInSensitiveMap.put(IMPORTED_REDACTION, false); - caseInSensitiveMap.put(ROTATE_SIMPLE, true); + caseInSensitiveMap.put(HINT_ONLY_INDICATOR, true); + caseInSensitiveMap.put(MUST_REDACT_INDICATOR, true); + caseInSensitiveMap.put(PUBLISHED_INFORMATION_INDICATOR, true); + caseInSensitiveMap.put(TEST_METHOD_INDICATOR, false); + caseInSensitiveMap.put(DICTIONARY_PII, false); + caseInSensitiveMap.put(PURITY_INDICATOR, false); + caseInSensitiveMap.put(IMAGE_INDICATOR, true); + caseInSensitiveMap.put(OCR_INDICATOR, true); + caseInSensitiveMap.put(SIGNATURE_INDICATOR, true); + caseInSensitiveMap.put(LOGO_INDICATOR, true); + caseInSensitiveMap.put(FORMULA_INDICATOR, true); + caseInSensitiveMap.put(DOSSIER_REDACTIONS_INDICATOR, false); + caseInSensitiveMap.put(IMPORTED_REDACTION_INDICATOR, false); + caseInSensitiveMap.put(ROTATE_SIMPLE_INDICATOR, true); - recommendationTypeMap.put(VERTEBRATE, false); - recommendationTypeMap.put(ADDRESS, false); - recommendationTypeMap.put(AUTHOR, false); - recommendationTypeMap.put(SPONSOR, false); + recommendationTypeMap.put(VERTEBRATE_INDICATOR, false); + recommendationTypeMap.put(DICTIONARY_ADDRESS, false); + recommendationTypeMap.put(DICTIONARY_AUTHOR, false); + recommendationTypeMap.put(DICTIONARY_SPONSOR, false); recommendationTypeMap.put(NO_REDACTION_INDICATOR, false); recommendationTypeMap.put(REDACTION_INDICATOR, false); - recommendationTypeMap.put(HINT_ONLY, false); - recommendationTypeMap.put(MUST_REDACT, false); - recommendationTypeMap.put(PUBLISHED_INFORMATION, false); - recommendationTypeMap.put(TEST_METHOD, false); - recommendationTypeMap.put(PII, false); - recommendationTypeMap.put(PURITY, false); - recommendationTypeMap.put(IMAGE, false); - recommendationTypeMap.put(OCR, false); - recommendationTypeMap.put(FORMULA, false); - recommendationTypeMap.put(SIGNATURE, false); - recommendationTypeMap.put(LOGO, false); - recommendationTypeMap.put(DOSSIER_REDACTIONS, false); - recommendationTypeMap.put(IMPORTED_REDACTION, false); - recommendationTypeMap.put(ROTATE_SIMPLE, false); + recommendationTypeMap.put(HINT_ONLY_INDICATOR, false); + recommendationTypeMap.put(MUST_REDACT_INDICATOR, false); + recommendationTypeMap.put(PUBLISHED_INFORMATION_INDICATOR, false); + recommendationTypeMap.put(TEST_METHOD_INDICATOR, false); + recommendationTypeMap.put(DICTIONARY_PII, false); + recommendationTypeMap.put(PURITY_INDICATOR, false); + recommendationTypeMap.put(IMAGE_INDICATOR, false); + recommendationTypeMap.put(OCR_INDICATOR, false); + recommendationTypeMap.put(FORMULA_INDICATOR, false); + recommendationTypeMap.put(SIGNATURE_INDICATOR, false); + recommendationTypeMap.put(LOGO_INDICATOR, false); + recommendationTypeMap.put(DOSSIER_REDACTIONS_INDICATOR, false); + recommendationTypeMap.put(IMPORTED_REDACTION_INDICATOR, false); + recommendationTypeMap.put(ROTATE_SIMPLE_INDICATOR, false); - rankTypeMap.put(PURITY, 155); - rankTypeMap.put(PII, 150); - rankTypeMap.put(ADDRESS, 140); - rankTypeMap.put(AUTHOR, 130); - rankTypeMap.put(SPONSOR, 120); - rankTypeMap.put(VERTEBRATE, 110); - rankTypeMap.put(MUST_REDACT, 100); + rankTypeMap.put(PURITY_INDICATOR, 155); + rankTypeMap.put(DICTIONARY_PII, 150); + rankTypeMap.put(DICTIONARY_ADDRESS, 140); + rankTypeMap.put(DICTIONARY_AUTHOR, 130); + rankTypeMap.put(DICTIONARY_SPONSOR, 120); + rankTypeMap.put(VERTEBRATE_INDICATOR, 110); + rankTypeMap.put(MUST_REDACT_INDICATOR, 100); rankTypeMap.put(REDACTION_INDICATOR, 90); rankTypeMap.put(NO_REDACTION_INDICATOR, 80); - rankTypeMap.put(PUBLISHED_INFORMATION, 70); - rankTypeMap.put(TEST_METHOD, 60); - rankTypeMap.put(HINT_ONLY, 50); - rankTypeMap.put(IMAGE, 30); - rankTypeMap.put(OCR, 29); - rankTypeMap.put(LOGO, 28); - rankTypeMap.put(SIGNATURE, 27); - rankTypeMap.put(FORMULA, 26); - rankTypeMap.put(DOSSIER_REDACTIONS, 200); - rankTypeMap.put(IMPORTED_REDACTION, 200); - rankTypeMap.put(ROTATE_SIMPLE, 150); + rankTypeMap.put(PUBLISHED_INFORMATION_INDICATOR, 70); + rankTypeMap.put(TEST_METHOD_INDICATOR, 60); + rankTypeMap.put(HINT_ONLY_INDICATOR, 50); + rankTypeMap.put(IMAGE_INDICATOR, 30); + rankTypeMap.put(OCR_INDICATOR, 29); + rankTypeMap.put(LOGO_INDICATOR, 28); + rankTypeMap.put(SIGNATURE_INDICATOR, 27); + rankTypeMap.put(FORMULA_INDICATOR, 26); + rankTypeMap.put(DOSSIER_REDACTIONS_INDICATOR, 200); + rankTypeMap.put(IMPORTED_REDACTION_INDICATOR, 200); + rankTypeMap.put(ROTATE_SIMPLE_INDICATOR, 150); colors.setSkippedColor("#cccccc"); colors.setRequestAddColor("#04b093"); @@ -433,17 +412,19 @@ public abstract class AbstractRedactionIntegrationTest { if (entries == null) { entries = Collections.emptyList(); } + return entries.stream().map(this::toDictionaryEntry).collect(Collectors.toList()); - List dictionaryEntries = new ArrayList<>(); - entries.forEach(entry -> { - dictionaryEntries.add(DictionaryEntry.builder().value(entry).version(reanlysisVersions.getOrDefault(entry, 0L)).deleted(deleted.contains(entry)).build()); - }); - return dictionaryEntries; + } + + + private DictionaryEntry toDictionaryEntry(String entry) { + + return DictionaryEntry.builder().value(entry).version(reanlysisVersions.getOrDefault(entry, 0L)).deleted(deleted.contains(entry)).build(); } @SneakyThrows - protected AnalyzeRequest prepareStorage(String file) { + protected AnalyzeRequest uploadFileToStorage(String file) { return prepareStorage(file, "files/cv_service_empty_response.json"); } 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 599179c9..b5b33dca 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 @@ -4,12 +4,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; import java.io.ByteArrayInputStream; -import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.net.URI; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.ArrayList; @@ -79,7 +82,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { @Configuration @EnableAutoConfiguration(exclude = {RabbitAutoConfiguration.class, StorageAutoConfiguration.class}) - public static class RedactionIntegrationTestConfiguration { + static class RedactionIntegrationTestConfiguration { @Bean public KieContainer kieContainer() { @@ -123,18 +126,17 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(0L); when(dictionaryClient.getAllTypesForDossier(TEST_DOSSIER_ID, false)).thenReturn(List.of(Type.builder() - .id(DOSSIER_REDACTIONS + ":" + TEST_DOSSIER_TEMPLATE_ID) - .type(DOSSIER_REDACTIONS) + .id(DOSSIER_REDACTIONS_INDICATOR + ":" + TEST_DOSSIER_TEMPLATE_ID) + .type(DOSSIER_REDACTIONS_INDICATOR) .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)) + .isHint(hintTypeMap.get(DOSSIER_REDACTIONS_INDICATOR)) + .isCaseInsensitive(caseInSensitiveMap.get(DOSSIER_REDACTIONS_INDICATOR)) + .isRecommendation(recommendationTypeMap.get(DOSSIER_REDACTIONS_INDICATOR)) + .rank(rankTypeMap.get(DOSSIER_REDACTIONS_INDICATOR)) .build())); mockDictionaryCalls(null); - mockDictionaryCalls(0L); when(dictionaryClient.getColors(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(colors); } @@ -143,7 +145,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { @Test public void test270Rotated() { - AnalyzeRequest request = prepareStorage("files/Minimal Examples/270Rotated.pdf"); + AnalyzeRequest request = uploadFileToStorage("files/Minimal Examples/270Rotated.pdf"); analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); AnalyzeResult result = analyzeService.analyze(request); assertThat(result).isNotNull(); @@ -154,7 +156,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { @Disabled public void testLargeScannedFileOOM() { - AnalyzeRequest request = prepareStorage("scanned/VV-377031.pdf"); + AnalyzeRequest request = uploadFileToStorage("scanned/VV-377031.pdf"); analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); AnalyzeResult result = analyzeService.analyze(request); assertThat(result).isNotNull(); @@ -166,7 +168,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { long start = System.currentTimeMillis(); - AnalyzeRequest request = prepareStorage("files/Minimal Examples/merge_images.pdf"); + AnalyzeRequest request = uploadFileToStorage("files/Minimal Examples/merge_images.pdf"); analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); AnalyzeResult result = analyzeService.analyze(request); @@ -180,7 +182,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { duplicates.forEach((key, value) -> assertThat(value.size()).isEqualTo(1)); - dictionary.get(AUTHOR).add("Drinking water"); + dictionary.get(DICTIONARY_AUTHOR).add("Drinking water"); when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(1L); AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build()); @@ -206,7 +208,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { // F. Lastname, J. Doe, M. Mustermann // Lastname M., Doe J., Mustermann M. - AnalyzeRequest request = prepareStorage("files/Minimal Examples/ExpansionTest.pdf"); + AnalyzeRequest request = uploadFileToStorage("files/Minimal Examples/ExpansionTest.pdf"); analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); AnalyzeResult result = analyzeService.analyze(request); @@ -222,7 +224,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { @Test public void titleExtraction() throws IOException { - AnalyzeRequest request = prepareStorage("files/new/APN3_Clean_6.1 (6.4.3.01-02)_Apple_211029.pdf"); + AnalyzeRequest request = uploadFileToStorage("files/new/APN3_Clean_6.1 (6.4.3.01-02)_Apple_211029.pdf"); analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); AnalyzeResult result = analyzeService.analyze(request); @@ -245,7 +247,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { @Test public void testAddFileAttribute() { - AnalyzeRequest request = prepareStorage("files/RSS/01 - CGA100251 - Acute Oral Toxicity (Up and Down Procedure) - Rat (1).pdf"); + AnalyzeRequest request = uploadFileToStorage("files/RSS/01 - CGA100251 - Acute Oral Toxicity (Up and Down Procedure) - Rat (1).pdf"); analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); AnalyzeResult result = analyzeService.analyze(request); @@ -262,7 +264,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { System.out.println("testIgnoreHint"); - AnalyzeRequest request = prepareStorage("files/new/test-ignore-hint.pdf"); + AnalyzeRequest request = uploadFileToStorage("files/new/test-ignore-hint.pdf"); analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); analyzeService.analyze(request); @@ -300,15 +302,11 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { System.out.println("noExceptionShouldBeThrownForAnyFiles"); ClassLoader loader = getClass().getClassLoader(); URL url = loader.getResource("files"); - File[] files = new File(url.getPath()).listFiles(); - List input = new ArrayList<>(); - for (File file : files) { - input.addAll(getPathsRecursively(file)); - } - for (File path : input) { + Path path = Paths.get(URI.create(url.toString())); - AnalyzeRequest request = prepareStorage(path.getPath()); - System.out.println("Redacting file : " + path.getName()); + Files.walk(path).forEach(currentPath -> { + AnalyzeRequest request = uploadFileToStorage(currentPath.toString()); + System.out.println("Redacting file : " + currentPath.getFileName()); analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); long fstart = System.currentTimeMillis(); @@ -327,7 +325,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { assertThat(entry.getValue().size()).isEqualTo(1); }); - dictionary.get(AUTHOR).add("Drinking water"); + dictionary.get(DICTIONARY_AUTHOR).add("Drinking water"); when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(1L); long rstart = System.currentTimeMillis(); @@ -335,8 +333,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { long rend = System.currentTimeMillis(); System.out.println("reanalysis analysis duration: " + (rend - rstart)); - - } + }); long end = System.currentTimeMillis(); @@ -352,7 +349,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { String outputFileName = OsUtils.getTemporaryDirectory() + "/AnnotatedRedactionTestSeparatedRedaction.pdf"; long start = System.currentTimeMillis(); - AnalyzeRequest request = prepareStorage(fileName); + AnalyzeRequest request = uploadFileToStorage(fileName); request.setExcludedPages(Set.of(1)); request.setFileAttributes(List.of(FileAttribute.builder() @@ -396,10 +393,10 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { } assertThat(correctFound).isEqualTo(redactionLog.getRedactionLogEntry().size()); - dictionary.get(AUTHOR).add("properties"); + dictionary.get(DICTIONARY_AUTHOR).add("properties"); reanlysisVersions.put("properties", 1L); - dictionary.get(AUTHOR).add("physical"); + dictionary.get(DICTIONARY_AUTHOR).add("physical"); reanlysisVersions.put("physical", 2L); deleted.add("David Chubb"); @@ -409,7 +406,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(3L); - when(dictionaryClient.getDictionaryForType(VERTEBRATE, null)).thenReturn(getDictionaryResponse(VERTEBRATE, false)); + when(dictionaryClient.getDictionaryForType(VERTEBRATE_INDICATOR, null)).thenReturn(getDictionaryResponse(VERTEBRATE_INDICATOR, false)); start = System.currentTimeMillis(); @@ -442,7 +439,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(4L); - when(dictionaryClient.getDictionaryForType(VERTEBRATE, null)).thenReturn(getDictionaryResponse(VERTEBRATE, false)); + when(dictionaryClient.getDictionaryForType(VERTEBRATE_INDICATOR, null)).thenReturn(getDictionaryResponse(VERTEBRATE_INDICATOR, false)); analyzeService.reanalyze(request); @@ -458,19 +455,19 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { String fileName = "files/new/test1S1T1.pdf"; String outputFileName = OsUtils.getTemporaryDirectory() + "/Annotated.pdf"; - AnalyzeRequest request = prepareStorage(fileName); + AnalyzeRequest request = uploadFileToStorage(fileName); analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); analyzeService.analyze(request); - dictionary.get(AUTHOR).add("report"); + dictionary.get(DICTIONARY_AUTHOR).add("report"); reanlysisVersions.put("report", 2L); when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(2L); mockDictionaryCalls(0L); analyzeService.reanalyze(request); - dictionary.get(AUTHOR).add("assessment report"); + dictionary.get(DICTIONARY_AUTHOR).add("assessment report"); reanlysisVersions.put("assessment report", 3L); when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(3L); mockDictionaryCalls(2L); @@ -506,7 +503,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { responseJson.getInputStream()); long start = System.currentTimeMillis(); - AnalyzeRequest request = prepareStorage(fileName); + AnalyzeRequest request = uploadFileToStorage(fileName); request.setExcludedPages(Set.of(1)); request.setFileAttributes(List.of(FileAttribute.builder() @@ -550,11 +547,11 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { } assertThat(correctFound).isEqualTo(redactionLog.getRedactionLogEntry().size()); - dictionary.get(AUTHOR).add("properties"); + dictionary.get(DICTIONARY_AUTHOR).add("properties"); reanlysisVersions.put("properties", 1L); mockDictionaryCalls(0L); - dictionary.get(AUTHOR).add("physical"); + dictionary.get(DICTIONARY_AUTHOR).add("physical"); reanlysisVersions.put("physical", 2L); deleted.add("David Chubb"); @@ -566,7 +563,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(3L); - when(dictionaryClient.getDictionaryForType(VERTEBRATE, null)).thenReturn(getDictionaryResponse(VERTEBRATE, false)); + when(dictionaryClient.getDictionaryForType(VERTEBRATE_INDICATOR, null)).thenReturn(getDictionaryResponse(VERTEBRATE_INDICATOR, false)); start = System.currentTimeMillis(); @@ -600,7 +597,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(4L); - when(dictionaryClient.getDictionaryForType(VERTEBRATE, null)).thenReturn(getDictionaryResponse(VERTEBRATE, false)); + when(dictionaryClient.getDictionaryForType(VERTEBRATE_INDICATOR, null)).thenReturn(getDictionaryResponse(VERTEBRATE_INDICATOR, false)); analyzeService.reanalyze(request); @@ -623,7 +620,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { }; List types = objectMapper.readValue(typeResource.getInputStream(), typeRefForTypes); - AnalyzeRequest request = prepareStorage("files/new/PublishedInformationTest.pdf"); + AnalyzeRequest request = uploadFileToStorage("files/new/PublishedInformationTest.pdf"); analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); ManualRedactions manualRedactions = new ManualRedactions(); manualRedactions.getIdsToRemove() @@ -681,7 +678,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { System.out.println("testTableRedaction"); long start = System.currentTimeMillis(); - AnalyzeRequest request = prepareStorage("files/Metolachlor/S-Metolachlor_RAR_02_Volume_2_2018-09-06.pdf"); + AnalyzeRequest request = uploadFileToStorage("files/Metolachlor/S-Metolachlor_RAR_02_Volume_2_2018-09-06.pdf"); analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); AnalyzeResult result = analyzeService.analyze(request); @@ -750,7 +747,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { long start = System.currentTimeMillis(); - AnalyzeRequest request = prepareStorage("files/new/unicodeProblem.pdf"); + AnalyzeRequest request = uploadFileToStorage("files/new/unicodeProblem.pdf"); analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); AnalyzeResult result = analyzeService.analyze(request); @@ -772,7 +769,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { System.out.println("testTableRedaction"); long start = System.currentTimeMillis(); - AnalyzeRequest request = prepareStorage("files/new/RotateTestFile.pdf"); + AnalyzeRequest request = uploadFileToStorage("files/new/RotateTestFile.pdf"); analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); AnalyzeResult result = analyzeService.analyze(request); @@ -798,7 +795,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { System.out.println("testTableRedaction"); long start = System.currentTimeMillis(); - AnalyzeRequest request = prepareStorage("files/new/RotateTestFileSimple.pdf"); + AnalyzeRequest request = uploadFileToStorage("files/new/RotateTestFileSimple.pdf"); analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); AnalyzeResult result = analyzeService.analyze(request); @@ -820,7 +817,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { System.out.println("testTableHeader"); long start = System.currentTimeMillis(); - AnalyzeRequest request = prepareStorage("files/Minimal Examples/NoHeaderTable.pdf"); + AnalyzeRequest request = uploadFileToStorage("files/Minimal Examples/NoHeaderTable.pdf"); analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); AnalyzeResult result = analyzeService.analyze(request); @@ -855,7 +852,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { long start = System.currentTimeMillis(); - AnalyzeRequest request = prepareStorage("files/new/S157.pdf"); + AnalyzeRequest request = uploadFileToStorage("files/new/S157.pdf"); ManualRedactions manualRedactions = new ManualRedactions(); @@ -931,7 +928,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { manualRedactionEntry.setPositions(List.of(Rectangle.builder().topLeftX(375.61096f).topLeftY(241.282f).width(7.648041f).height(43.72262f).page(1).build(), Rectangle.builder().topLeftX(384.83517f).topLeftY(241.282f).width(7.648041f).height(17.043358f).page(1).build())); - AnalyzeRequest request = prepareStorage(pdfFile); + AnalyzeRequest request = uploadFileToStorage(pdfFile); request.setManualRedactions(manualRedactions); analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); AnalyzeResult result = analyzeService.analyze(request); @@ -964,7 +961,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { @Test public void phantomCellsDocumentTest() throws IOException { - AnalyzeRequest request = prepareStorage("files/Minimal Examples/Phantom Cells.pdf"); + AnalyzeRequest request = uploadFileToStorage("files/Minimal Examples/Phantom Cells.pdf"); analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); AnalyzeResult result = analyzeService.analyze(request); @@ -984,7 +981,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { long start = System.currentTimeMillis(); - AnalyzeRequest request = prepareStorage("files/Minimal Examples/sponsor_companies.pdf"); + AnalyzeRequest request = uploadFileToStorage("files/Minimal Examples/sponsor_companies.pdf"); analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); AnalyzeResult result = analyzeService.analyze(request); @@ -1046,7 +1043,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { // manualRedactions.getEntriesToAdd().add(manualRedactionEntry); - AnalyzeRequest request = prepareStorage(pdfFile); + AnalyzeRequest request = uploadFileToStorage(pdfFile); request.setManualRedactions(manualRedactions); analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); AnalyzeResult result = analyzeService.analyze(request); @@ -1102,7 +1099,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { System.out.println("expandByRegex"); long start = System.currentTimeMillis(); - AnalyzeRequest request = prepareStorage("files/Metolachlor/S-Metolachlor_RAR_02_Volume_2_2018-09-06.pdf"); + AnalyzeRequest request = uploadFileToStorage("files/Metolachlor/S-Metolachlor_RAR_02_Volume_2_2018-09-06.pdf"); analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); AnalyzeResult result = analyzeService.analyze(request); @@ -1157,7 +1154,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { manualRedactions.getEntriesToAdd().add(manualRedactionEntry2); manualRedactions.getEntriesToAdd().add(manualRedactionEntry3); - AnalyzeRequest request = prepareStorage(pdfFile); + AnalyzeRequest request = uploadFileToStorage(pdfFile); request.setManualRedactions(manualRedactions); analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); AnalyzeResult result = analyzeService.analyze(request); @@ -1187,7 +1184,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { String outputFileName = OsUtils.getTemporaryDirectory() + "/Annotated.pdf"; ClassPathResource importedRedactions = new ClassPathResource("files/ImportedRedactions/RotateTestFile_without_highlights.IMPORTED_REDACTIONS.json"); - AnalyzeRequest request = prepareStorage("files/ImportedRedactions/RotateTestFile_without_highlights.pdf"); + AnalyzeRequest request = uploadFileToStorage("files/ImportedRedactions/RotateTestFile_without_highlights.pdf"); storageService.storeObject(TenantContext.getTenantId(), RedactionStorageService.StorageIdUtils.getStorageId(TEST_DOSSIER_ID, TEST_FILE_ID, FileType.IMPORTED_REDACTIONS), importedRedactions.getInputStream()); @@ -1220,17 +1217,17 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { @Test public void testExpandByPrefixRegEx() throws IOException { - assertThat(dictionary.get(AUTHOR)).contains("Robinson"); - assertThat(dictionary.get(AUTHOR)).doesNotContain("Mrs. Robinson"); - assertThat(dictionary.get(AUTHOR)).contains("Bojangles"); - assertThat(dictionary.get(AUTHOR)).doesNotContain("Mr. Bojangles"); - assertThat(dictionary.get(AUTHOR)).contains("Tambourine Man"); - assertThat(dictionary.get(AUTHOR)).doesNotContain("Mr. Tambourine Man"); + assertThat(dictionary.get(DICTIONARY_AUTHOR)).contains("Robinson"); + assertThat(dictionary.get(DICTIONARY_AUTHOR)).doesNotContain("Mrs. Robinson"); + assertThat(dictionary.get(DICTIONARY_AUTHOR)).contains("Bojangles"); + assertThat(dictionary.get(DICTIONARY_AUTHOR)).doesNotContain("Mr. Bojangles"); + assertThat(dictionary.get(DICTIONARY_AUTHOR)).contains("Tambourine Man"); + assertThat(dictionary.get(DICTIONARY_AUTHOR)).doesNotContain("Mr. Tambourine Man"); String fileName = "files/mr-mrs.pdf"; String outputFileName = OsUtils.getTemporaryDirectory() + "/Annotated.pdf"; - AnalyzeRequest request = prepareStorage(fileName); + AnalyzeRequest request = uploadFileToStorage(fileName); analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); analyzeService.analyze(request); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationWithRulesV2Test.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTestV2.java similarity index 85% rename from redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationWithRulesV2Test.java rename to redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTestV2.java index 7b3d6919..b8f4d592 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationWithRulesV2Test.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/RedactionIntegrationTestV2.java @@ -6,7 +6,7 @@ import static org.mockito.Mockito.when; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -40,14 +40,14 @@ import lombok.SneakyThrows; @ExtendWith(SpringExtension.class) @SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@Import(RedactionIntegrationWithRulesV2Test.RedactionIntegrationTestConfiguration.class) -public class RedactionIntegrationWithRulesV2Test extends AbstractRedactionIntegrationTest { +@Import(RedactionIntegrationTestV2.RedactionIntegrationTestConfiguration.class) +public class RedactionIntegrationTestV2 extends AbstractRedactionIntegrationTest { private static final String RULES = loadFromClassPath("drools/rules_v2.drl"); @Configuration @EnableAutoConfiguration(exclude = {RabbitAutoConfiguration.class, StorageAutoConfiguration.class}) - public static class RedactionIntegrationTestConfiguration { + static class RedactionIntegrationTestConfiguration { @Bean public KieContainer kieContainer() { @@ -91,18 +91,17 @@ public class RedactionIntegrationWithRulesV2Test extends AbstractRedactionIntegr when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(0L); when(dictionaryClient.getAllTypesForDossier(TEST_DOSSIER_ID, false)).thenReturn(List.of(Type.builder() - .id(DOSSIER_REDACTIONS + ":" + TEST_DOSSIER_TEMPLATE_ID) - .type(DOSSIER_REDACTIONS) + .id(DOSSIER_REDACTIONS_INDICATOR + ":" + TEST_DOSSIER_TEMPLATE_ID) + .type(DOSSIER_REDACTIONS_INDICATOR) .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)) + .isHint(hintTypeMap.get(DOSSIER_REDACTIONS_INDICATOR)) + .isCaseInsensitive(caseInSensitiveMap.get(DOSSIER_REDACTIONS_INDICATOR)) + .isRecommendation(recommendationTypeMap.get(DOSSIER_REDACTIONS_INDICATOR)) + .rank(rankTypeMap.get(DOSSIER_REDACTIONS_INDICATOR)) .build())); mockDictionaryCalls(null); - mockDictionaryCalls(0L); when(dictionaryClient.getColors(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(colors); } @@ -118,14 +117,14 @@ public class RedactionIntegrationWithRulesV2Test extends AbstractRedactionIntegr @SneakyThrows public void testTermIsInTwoDictionariesAndInOneFalsePositive() { - AnalyzeRequest request = prepareStorage("files/new/simplified2.pdf"); + AnalyzeRequest request = uploadFileToStorage("files/new/simplified2.pdf"); dictionary.clear(); - dictionary.computeIfAbsent(PII, v -> new ArrayList<>()).add("Dr. Alan Miller"); - dictionary.computeIfAbsent(AUTHOR, v -> new ArrayList<>()).add("Dr. Alan Miller"); + dictionary.put(DICTIONARY_PII, Arrays.asList("Dr. Alan Miller")); + dictionary.put(DICTIONARY_AUTHOR, Arrays.asList("Dr. Alan Miller")); falsePositive.clear(); - falsePositive.computeIfAbsent(PII, v -> new ArrayList<>()).add("Dr. Alan Miller COMPLETION DATE:"); + falsePositive.put(DICTIONARY_PII, Arrays.asList("Dr. Alan Miller COMPLETION DATE:")); analyzeService.analyzeDocumentStructure(new StructureAnalyzeRequest(request.getDossierId(), request.getFileId())); analyzeService.analyze(request); @@ -136,7 +135,7 @@ public class RedactionIntegrationWithRulesV2Test extends AbstractRedactionIntegr RedactionLogEntry redactionLogEntry = redactionLog.getRedactionLogEntry().get(0); - assertThat(redactionLogEntry.getType()).isEqualTo("CBI_author"); + assertThat(redactionLogEntry.getType()).isEqualTo(DICTIONARY_AUTHOR); assertThat(redactionLogEntry.getValue()).isEqualTo("Dr. Alan Miller"); assertThat(redactionLogEntry.isRedacted()).isEqualTo(true); assertThat(redactionLogEntry.isRecommendation()).isEqualTo(false);