From a7111b0e24661d3d28d4e664e5f67704021b0a7f Mon Sep 17 00:00:00 2001 From: Andrei Isvoran Date: Thu, 1 Feb 2024 11:30:47 +0100 Subject: [PATCH] RED-8049 - Change TextEntity id to be calculated based on position --- .../v1/server/model/ClosestEntity.java | 4 +- .../v1/server/model/ManualEntity.java | 19 ++ .../v1/server/model/MigrationEntity.java | 2 +- .../server/model/dictionary/Dictionary.java | 6 +- .../model/document/entity/TextEntity.java | 49 ++++- .../server/model/document/nodes/Document.java | 12 +- .../server/model/document/nodes/Footer.java | 7 +- .../server/model/document/nodes/Header.java | 7 +- .../server/model/document/nodes/Headline.java | 5 +- .../v1/server/model/document/nodes/Image.java | 2 + .../v1/server/model/document/nodes/Page.java | 22 +- .../model/document/nodes/Paragraph.java | 5 +- .../server/model/document/nodes/Section.java | 6 +- .../model/document/nodes/SemanticNode.java | 48 +++- .../v1/server/model/document/nodes/Table.java | 10 +- .../model/document/nodes/TableCell.java | 4 +- .../textblock/ConcatenatedTextBlock.java | 15 +- .../service/EntityLogCreatorService.java | 47 ++-- .../ManualChangesApplicationService.java | 154 ++++++------- .../service/RedactionLogCreatorService.java | 4 +- .../service/UnprocessedChangesService.java | 120 +++------- .../document/EntityCreationService.java | 207 +++++++++--------- .../document/EntityCreationUtility.java | 91 ++++++++ .../document/EntityFindingUtility.java | 27 ++- .../document/IntersectingNodeVisitor.java | 31 +++ .../document/ManualEntityCreationService.java | 56 +++-- .../document/ManualRedactionEntryService.java | 2 +- .../server/service/document/NodeVisitor.java | 8 + .../drools/EntityDroolsExecutionService.java | 5 +- .../v1/server/RedactionIntegrationTest.java | 32 ++- .../document/entity/TextEntityTest.java | 13 +- ...cumentIEntityInsertionIntegrationTest.java | 21 +- .../DocumentPerformanceIntegrationTest.java | 25 ++- .../ManualChangesEnd2EndTest.java | 83 +++++-- .../ManualChangesIntegrationTest.java | 4 +- .../manualchanges/ManualChangesUnitTest.java | 2 +- .../adapter/NerEntitiesAdapterTest.java | 4 +- .../v1/server/rules/RulesIntegrationTest.java | 3 +- .../UnprocessedChangesServiceTest.java | 10 +- .../resources/drools/acceptance_rules.drl | 67 +++--- .../drools/all_redact_manager_rules.drl | 93 ++++---- .../test/resources/drools/documine_flora.drl | 37 ++-- .../drools/manual_redaction_rules.drl | 15 +- .../src/test/resources/drools/rules.drl | 63 +++--- .../src/test/resources/drools/rules_v2.drl | 49 ++--- .../src/test/resources/drools/table_demo.drl | 39 ++-- .../src/test/resources/drools/test_rules.drl | 43 ++-- .../files/Minimal Examples/Meto1_Page22.pdf | Bin 0 -> 196360 bytes .../EFSA_sanitisation_GFL_v1/rules.drl | 49 ++--- .../resources/all_redact_manager_rules.drl | 99 +++++---- .../src/main/resources/all_rules_documine.drl | 37 ++-- .../EFSA_sanitisation_GFL_v1/rules.txt | 2 +- .../test/resources/dev/Basf-Demo/rules.txt | 2 +- .../src/test/resources/dev/Flora/rules.txt | 2 +- 54 files changed, 988 insertions(+), 781 deletions(-) create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/EntityCreationUtility.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/IntersectingNodeVisitor.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/NodeVisitor.java create mode 100644 redaction-service-v1/redaction-service-server-v1/src/test/resources/files/Minimal Examples/Meto1_Page22.pdf diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/ClosestEntity.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/ClosestEntity.java index 275f7e83..99511a8f 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/ClosestEntity.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/ClosestEntity.java @@ -1,6 +1,6 @@ package com.iqser.red.service.redaction.v1.server.model; -import com.iqser.red.service.redaction.v1.server.model.document.TextRange; +import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity; import lombok.Builder; import lombok.Getter; @@ -12,6 +12,6 @@ import lombok.Setter; public class ClosestEntity { private double distance; - private TextRange textRange; + private TextEntity textEntity; } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/ManualEntity.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/ManualEntity.java index 5b47e232..d33f588e 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/ManualEntity.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/ManualEntity.java @@ -2,10 +2,12 @@ package com.iqser.red.service.redaction.v1.server.model; import java.util.List; import java.util.PriorityQueue; +import java.util.UUID; import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLogEntry; import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntryType; import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualRedactionEntry; +import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualResizeRedaction; import com.iqser.red.service.redaction.v1.server.model.document.TextRange; import com.iqser.red.service.redaction.v1.server.model.document.entity.EntityType; import com.iqser.red.service.redaction.v1.server.model.document.entity.IEntity; @@ -69,6 +71,7 @@ public class ManualEntity implements IEntity { .build(); } + public static ManualEntity fromEntityLogEntry(EntityLogEntry entityLogEntry) { List rectangleWithPages = entityLogEntry.getPositions().stream().map(RectangleWithPage::fromEntityLogPosition).toList(); @@ -90,6 +93,19 @@ public class ManualEntity implements IEntity { } + public static ManualEntity fromManualResizeRedaction(ManualResizeRedaction manualResizeRedaction) { + + List rectangleWithPages = manualResizeRedaction.getPositions().stream().map(RectangleWithPage::fromAnnotationRectangle).toList(); + return ManualEntity.builder() + .id(UUID.randomUUID().toString()) + .value(manualResizeRedaction.getValue()) + .entityPosition(rectangleWithPages) + .entityType(EntityType.ENTITY) + .manualOverwrite(new ManualChangeOverwrite(EntityType.ENTITY)) + .build(); + } + + @Override public TextRange getTextRange() { @@ -103,6 +119,7 @@ public class ManualEntity implements IEntity { return getManualOverwrite().getType().orElse(type); } + private static EntityType getEntityType(EntryType entryType) { switch (entryType) { @@ -124,7 +141,9 @@ public class ManualEntity implements IEntity { } } + private EntityType getEntityType(boolean isHint) { + return isHint ? EntityType.HINT : EntityType.ENTITY; } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/MigrationEntity.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/MigrationEntity.java index 48c3beea..dd208f56 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/MigrationEntity.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/MigrationEntity.java @@ -307,7 +307,7 @@ public final class MigrationEntity { .reason(entity.buildReasonWithManualChangeDescriptions()) .legalBasis(entity.legalBasis()) .value(entity.getManualOverwrite().getValue().orElse(entity.getMatchedRule().isWriteValueWithLineBreaks() ? entity.getValueWithLineBreaks() : entity.getValue())) - .type(entity.getType()) + .type(entity.type()) .section(redactionLogEntry.getSection()) .textAfter(redactionLogEntry.getTextAfter()) .textBefore(redactionLogEntry.getTextBefore()) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/dictionary/Dictionary.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/dictionary/Dictionary.java index 6cd61fca..fad8c114 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/dictionary/Dictionary.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/dictionary/Dictionary.java @@ -128,19 +128,19 @@ public class Dictionary { public void recommendEverywhere(TextEntity textEntity) { - addLocalDictionaryEntry(textEntity.getType(), textEntity.getValue(), textEntity.getMatchedRuleList(), false); + addLocalDictionaryEntry(textEntity.type(), textEntity.getValue(), textEntity.getMatchedRuleList(), false); } public void recommendEverywhereWithLastNameSeparately(TextEntity textEntity) { - addLocalDictionaryEntry(textEntity.getType(), textEntity.getValue(), textEntity.getMatchedRuleList(), true); + addLocalDictionaryEntry(textEntity.type(), textEntity.getValue(), textEntity.getMatchedRuleList(), true); } public void addMultipleAuthorsAsRecommendation(TextEntity textEntity) { - splitIntoAuthorNames(textEntity).forEach(authorName -> addLocalDictionaryEntry(textEntity.getType(), authorName, textEntity.getMatchedRuleList(), true)); + splitIntoAuthorNames(textEntity).forEach(authorName -> addLocalDictionaryEntry(textEntity.type(), authorName, textEntity.getMatchedRuleList(), true)); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/entity/TextEntity.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/entity/TextEntity.java index a0a0c5c9..a649889d 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/entity/TextEntity.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/entity/TextEntity.java @@ -1,6 +1,7 @@ package com.iqser.red.service.redaction.v1.server.model.document.entity; import java.awt.geom.Rectangle2D; +import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashSet; @@ -33,13 +34,15 @@ public class TextEntity implements IEntity { // primary key @EqualsAndHashCode.Include - final TextRange textRange; - @EqualsAndHashCode.Include - final String type; - @EqualsAndHashCode.Include - final EntityType entityType; + final String id; // primary key end + TextRange textRange; + @Builder.Default + List duplicateTextRanges = new ArrayList<>(); + String type; // TODO: make final once ManualChangesApplicatioService recategorize is deleted + final EntityType entityType; + @Builder.Default final PriorityQueue matchedRuleList = new PriorityQueue<>(); final ManualChangeOverwrite manualOverwrite; @@ -62,12 +65,30 @@ public class TextEntity implements IEntity { SemanticNode deepestFullyContainingNode; - public static TextEntity initialEntityNode(TextRange textRange, String type, EntityType entityType) { + public static TextEntity initialEntityNode(TextRange textRange, String type, EntityType entityType, SemanticNode node) { - return TextEntity.builder().type(type).entityType(entityType).textRange(textRange).manualOverwrite(new ManualChangeOverwrite(entityType)).build(); + return TextEntity.builder().id(buildId(node, textRange, type, entityType)).type(type).entityType(entityType).textRange(textRange).manualOverwrite(new ManualChangeOverwrite(entityType)).build(); } + public static TextEntity initialEntityNode(TextRange textRange, String type, EntityType entityType, String id) { + + return TextEntity.builder().id(id).type(type).entityType(entityType).textRange(textRange).manualOverwrite(new ManualChangeOverwrite(entityType)).build(); + } + + + private static String buildId(SemanticNode node, TextRange textRange, String type, EntityType entityType) { + + Map> rectanglesPerLinePerPage = node.getPositionsPerPage(textRange); + return IdBuilder.buildId(rectanglesPerLinePerPage.keySet(), rectanglesPerLinePerPage.values().stream().flatMap(Collection::stream).toList(), type, entityType.name()); + } + + + public void addTextRange(TextRange textRange) { + + duplicateTextRanges.add(textRange); + } + public boolean occursInNodeOfType(Class clazz) { return intersectingNodes.stream().anyMatch(clazz::isInstance); @@ -82,13 +103,13 @@ public class TextEntity implements IEntity { public boolean isType(String type) { - return this.type.equals(type); + return type().equals(type); } public boolean isAnyType(List types) { - return types.contains(type); + return types.contains(type()); } @@ -125,7 +146,6 @@ public class TextEntity implements IEntity { .min(Comparator.comparingInt(Page::getNumber)) .orElseThrow(() -> new RuntimeException("No Positions found on any page!")); - String id = IdBuilder.buildId(pages, rectanglesPerLinePerPage.values().stream().flatMap(Collection::stream).toList(), type, entityType.name()); positionsOnPagePerPage = rectanglesPerLinePerPage.entrySet().stream().map(entry -> buildPositionOnPage(firstPage, id, entry)).toList(); } return positionsOnPagePerPage; @@ -193,7 +213,7 @@ public class TextEntity implements IEntity { }); sb.delete(sb.length() - 2, sb.length()); sb.append("], type = \""); - sb.append(type); + sb.append(type()); sb.append("\", EntityType."); sb.append(entityType); sb.append("]"); @@ -207,4 +227,11 @@ public class TextEntity implements IEntity { return getManualOverwrite().getType().orElse(type); } + + @Override + public String value() { + + return getManualOverwrite().getValue().orElse(getMatchedRule().isWriteValueWithLineBreaks() ? getValueWithLineBreaks() : value); + } + } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Document.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Document.java index 4201f353..79484363 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Document.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Document.java @@ -19,6 +19,7 @@ import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.experimental.FieldDefaults; @@ -27,8 +28,12 @@ import lombok.experimental.FieldDefaults; @AllArgsConstructor @NoArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) +@EqualsAndHashCode(onlyExplicitlyIncluded = true) public class Document implements GenericSemanticNode { + @EqualsAndHashCode.Include + List treeId = Collections.emptyList(); + Set pages; DocumentTree documentTree; Integer numberOfPages; @@ -67,13 +72,6 @@ public class Document implements GenericSemanticNode { } - @Override - public List getTreeId() { - - return Collections.emptyList(); - } - - @Override public void setTreeId(List tocId) { diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Footer.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Footer.java index 0d71164f..c61bff64 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Footer.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Footer.java @@ -23,22 +23,21 @@ import lombok.experimental.FieldDefaults; @AllArgsConstructor @NoArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) +@EqualsAndHashCode(onlyExplicitlyIncluded = true) public class Footer implements GenericSemanticNode { - @Builder.Default final static SectionIdentifier sectionIdentifier = SectionIdentifier.empty(); + @EqualsAndHashCode.Include List treeId; + TextBlock leafTextBlock; - @EqualsAndHashCode.Exclude DocumentTree documentTree; @Builder.Default - @EqualsAndHashCode.Exclude Set entities = new HashSet<>(); - @EqualsAndHashCode.Exclude Map bBoxCache; diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Header.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Header.java index 0575e37d..270a4960 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Header.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Header.java @@ -23,22 +23,21 @@ import lombok.experimental.FieldDefaults; @AllArgsConstructor @NoArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) +@EqualsAndHashCode(onlyExplicitlyIncluded = true) public class Header implements GenericSemanticNode { - @Builder.Default final static SectionIdentifier sectionIdentifier = SectionIdentifier.empty(); + @EqualsAndHashCode.Include List treeId; + TextBlock leafTextBlock; - @EqualsAndHashCode.Exclude DocumentTree documentTree; @Builder.Default - @EqualsAndHashCode.Exclude Set entities = new HashSet<>(); - @EqualsAndHashCode.Exclude Map bBoxCache; diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Headline.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Headline.java index 31006ddf..a2c748e9 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Headline.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Headline.java @@ -24,20 +24,19 @@ import lombok.experimental.FieldDefaults; @AllArgsConstructor @NoArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) +@EqualsAndHashCode(onlyExplicitlyIncluded = true) public class Headline implements GenericSemanticNode { + @EqualsAndHashCode.Include List treeId; TextBlock leafTextBlock; SectionIdentifier sectionIdentifier; - @EqualsAndHashCode.Exclude DocumentTree documentTree; @Builder.Default - @EqualsAndHashCode.Exclude Set entities = new HashSet<>(); - @EqualsAndHashCode.Exclude Map bBoxCache; diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Image.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Image.java index 2f0c8a2e..e591af11 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Image.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Image.java @@ -32,8 +32,10 @@ import lombok.experimental.FieldDefaults; @AllArgsConstructor @NoArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) +@EqualsAndHashCode(onlyExplicitlyIncluded = true) public class Image implements GenericSemanticNode, IEntity { + @EqualsAndHashCode.Include List treeId; String id; diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Page.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Page.java index 18aee858..f741bf62 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Page.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Page.java @@ -23,26 +23,23 @@ import lombok.experimental.FieldDefaults; @NoArgsConstructor @AllArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) +@EqualsAndHashCode(onlyExplicitlyIncluded = true) public class Page { + @EqualsAndHashCode.Include Integer number; Integer height; Integer width; Integer rotation; - @EqualsAndHashCode.Exclude List mainBody; - @EqualsAndHashCode.Exclude Header header; - @EqualsAndHashCode.Exclude Footer footer; @Builder.Default - @EqualsAndHashCode.Exclude Set entities = new HashSet<>(); @Builder.Default - @EqualsAndHashCode.Exclude Set images = new HashSet<>(); @@ -57,19 +54,4 @@ public class Page { return String.valueOf(number); } - - - @Override - public int hashCode() { - - return number; - } - - - @Override - public boolean equals(Object o) { - - return o instanceof Page && o.hashCode() == this.hashCode(); - } - } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Paragraph.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Paragraph.java index 4d252731..db5c38aa 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Paragraph.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Paragraph.java @@ -21,19 +21,18 @@ import lombok.experimental.FieldDefaults; @Builder @AllArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) +@EqualsAndHashCode(onlyExplicitlyIncluded = true) public class Paragraph implements GenericSemanticNode { + @EqualsAndHashCode.Include List treeId; TextBlock leafTextBlock; - @EqualsAndHashCode.Exclude DocumentTree documentTree; @Builder.Default - @EqualsAndHashCode.Exclude Set entities = new HashSet<>(); - @EqualsAndHashCode.Exclude Map bBoxCache; diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Section.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Section.java index 06c7cbe2..58259feb 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Section.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Section.java @@ -24,19 +24,17 @@ import lombok.extern.slf4j.Slf4j; @Builder @AllArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) +@EqualsAndHashCode(onlyExplicitlyIncluded = true) public class Section implements GenericSemanticNode { - + @EqualsAndHashCode.Include List treeId; TextBlock textBlock; - @EqualsAndHashCode.Exclude DocumentTree documentTree; @Builder.Default - @EqualsAndHashCode.Exclude Set entities = new HashSet<>(); - @EqualsAndHashCode.Exclude Map bBoxCache; diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/SemanticNode.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/SemanticNode.java index af423530..d874ea24 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/SemanticNode.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/SemanticNode.java @@ -19,6 +19,7 @@ import com.iqser.red.service.redaction.v1.server.model.document.TextRange; import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity; import com.iqser.red.service.redaction.v1.server.model.document.textblock.AtomicTextBlock; import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlock; +import com.iqser.red.service.redaction.v1.server.service.document.NodeVisitor; import com.iqser.red.service.redaction.v1.server.utils.RectangleTransformations; import com.iqser.red.service.redaction.v1.server.utils.RedactionSearchUtility; @@ -80,8 +81,8 @@ public interface SemanticNode { */ default Set getPages(TextRange textRange) { - if (!getTextRange().contains(textRange)) { - throw new IllegalArgumentException(format("%s which was used to query for pages is not contained in the %s of this node!", textRange, getTextRange())); + if (!getTextRange().intersects(textRange)) { + throw new IllegalArgumentException(format("%s which was used to query for pages is not intersected in the %s of this node!", textRange, getTextRange())); } return getTextBlock().getPages(textRange); } @@ -247,7 +248,7 @@ public interface SemanticNode { */ default boolean hasEntitiesOfType(String type) { - return getEntities().stream().filter(TextEntity::active).anyMatch(redactionEntity -> redactionEntity.getType().equals(type)); + return getEntities().stream().filter(TextEntity::active).anyMatch(redactionEntity -> redactionEntity.type().equals(type)); } @@ -260,7 +261,7 @@ public interface SemanticNode { */ default boolean hasEntitiesOfAnyType(String... types) { - return getEntities().stream().filter(TextEntity::active).anyMatch(redactionEntity -> Arrays.stream(types).anyMatch(type -> redactionEntity.getType().equals(type))); + return getEntities().stream().filter(TextEntity::active).anyMatch(redactionEntity -> Arrays.stream(types).anyMatch(type -> redactionEntity.type().equals(type))); } @@ -273,7 +274,7 @@ public interface SemanticNode { */ default boolean hasEntitiesOfAllTypes(String... types) { - return getEntities().stream().filter(TextEntity::active).map(TextEntity::getType).collect(Collectors.toUnmodifiableSet()).containsAll(Arrays.stream(types).toList()); + return getEntities().stream().filter(TextEntity::active).map(TextEntity::type).collect(Collectors.toUnmodifiableSet()).containsAll(Arrays.stream(types).toList()); } @@ -286,7 +287,7 @@ public interface SemanticNode { */ default List getEntitiesOfType(String type) { - return getEntities().stream().filter(TextEntity::active).filter(redactionEntity -> redactionEntity.getType().equals(type)).toList(); + return getEntities().stream().filter(TextEntity::active).filter(redactionEntity -> redactionEntity.type().equals(type)).toList(); } @@ -627,6 +628,27 @@ public interface SemanticNode { } + /** + * For a given TextRange this function returns a List of rectangle around the text in the range. + * These Rectangles are split either by a new line or by a large gap in the current line. + * This is mainly used to find the positions of TextEntities + * + * @param textRange A TextRange to calculate the positions for. + * @return A Map, where the keys are the pages and the values are a list of rectangles describing the position of words + */ + default Map> getPositionsPerPage(TextRange textRange) { + + if (isLeaf()) { + return getTextBlock().getPositionsPerPage(textRange); + } + Optional containingChildNode = streamChildren().filter(child -> child.getTextRange().contains(textRange)).findFirst(); + if (containingChildNode.isEmpty()) { + return getTextBlock().getPositionsPerPage(textRange); + } + return containingChildNode.get().getPositionsPerPage(textRange); + } + + /** * If this Node is a Leaf it will calculate the boundingBox of its LeafTextBlock, otherwise it will calculate the Union of the BoundingBoxes of all its Children. * If called on the Document, it will return the cropbox of each page @@ -693,4 +715,18 @@ public interface SemanticNode { return bBoxPerPage; } + + /** + * Accepts a {@link NodeVisitor} and initiates a depth-first traversal of the semantic tree rooted at this node. + * The visitor's {@link NodeVisitor#visit(SemanticNode)} method is invoked for each node encountered during the traversal. + * + * @param visitor The {@link NodeVisitor} to accept and apply during the traversal. + * @see NodeVisitor + */ + default void accept(NodeVisitor visitor) { + + visitor.visit(this); + streamChildren().forEach(childNode -> childNode.accept(visitor)); + } + } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Table.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Table.java index 09320581..100f3fe9 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Table.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/Table.java @@ -28,8 +28,10 @@ import lombok.experimental.FieldDefaults; @Builder @AllArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) +@EqualsAndHashCode(onlyExplicitlyIncluded = true) public class Table implements SemanticNode { + @EqualsAndHashCode.Include List treeId; DocumentTree documentTree; @@ -39,10 +41,8 @@ public class Table implements SemanticNode { TextBlock textBlock; @Builder.Default - @EqualsAndHashCode.Exclude Set entities = new HashSet<>(); - @EqualsAndHashCode.Exclude Map bBoxCache; @@ -128,7 +128,7 @@ public class Table implements SemanticNode { List rowsWithEntityOfType = getEntities().stream() .filter(TextEntity::active) - .filter(redactionEntity -> types.stream().anyMatch(type -> type.equals(redactionEntity.getType()))) + .filter(redactionEntity -> types.stream().anyMatch(type -> type.equals(redactionEntity.type()))) .map(TextEntity::getIntersectingNodes) .filter(node -> node instanceof TableCell) .map(node -> (TableCell) node) @@ -153,7 +153,7 @@ public class Table implements SemanticNode { .filter(rowNumber -> streamRow(rowNumber).map(TableCell::getEntities) .flatMap(Collection::stream) .filter(TextEntity::active) - .noneMatch(entity -> types.contains(entity.getType()))) + .noneMatch(entity -> types.contains(entity.type()))) .flatMap(this::streamRow) .map(TableCell::getEntities) .flatMap(Collection::stream); @@ -196,7 +196,7 @@ public class Table implements SemanticNode { */ public Stream streamTableCellsWhichContainType(String type) { - return streamTableCells().filter(tableCell -> tableCell.getEntities().stream().filter(TextEntity::active).anyMatch(entity -> entity.getType().equals(type))); + return streamTableCells().filter(tableCell -> tableCell.getEntities().stream().filter(TextEntity::active).anyMatch(entity -> entity.type().equals(type))); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/TableCell.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/TableCell.java index 6165c618..24658449 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/TableCell.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/nodes/TableCell.java @@ -23,8 +23,10 @@ import lombok.experimental.FieldDefaults; @Builder @AllArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) +@EqualsAndHashCode(onlyExplicitlyIncluded = true) public class TableCell implements GenericSemanticNode { + @EqualsAndHashCode.Include List treeId; int row; int col; @@ -36,11 +38,9 @@ public class TableCell implements GenericSemanticNode { TextBlock textBlock; - @EqualsAndHashCode.Exclude DocumentTree documentTree; @Builder.Default - @EqualsAndHashCode.Exclude Set entities = new HashSet<>(); diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/textblock/ConcatenatedTextBlock.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/textblock/ConcatenatedTextBlock.java index 57334d5f..9f372ed4 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/textblock/ConcatenatedTextBlock.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/textblock/ConcatenatedTextBlock.java @@ -71,7 +71,16 @@ public class ConcatenatedTextBlock implements TextBlock { private List getAllAtomicTextBlocksPartiallyInStringBoundary(TextRange textRange) { - return atomicTextBlocks.stream().filter(tb -> tb.getTextRange().intersects(textRange)).toList(); + List intersectingAtomicTextBlocks = new LinkedList<>(); + for (AtomicTextBlock atomicTextBlock : atomicTextBlocks) { + if (atomicTextBlock.getTextRange().start() > textRange.end()) { + break; // early stop, following TextBlocks will never intersect + } + if (atomicTextBlock.getTextRange().intersects(textRange)) { + intersectingAtomicTextBlocks.add(atomicTextBlock); + } + } + return intersectingAtomicTextBlocks; } @@ -122,13 +131,13 @@ public class ConcatenatedTextBlock implements TextBlock { } - @Override public Rectangle2D getPosition(int stringIdx) { return getAtomicTextBlockByStringIndex(stringIdx).getPosition(stringIdx); } + public TextRange getLineTextRange(int lineNumber) { if (atomicTextBlocks.size() == 1) { @@ -144,10 +153,10 @@ public class ConcatenatedTextBlock implements TextBlock { return new TextRange(textRange.start(), textRange.start()); } + @Override public List getPositions(TextRange stringTextRange) { - List textBlocks = getAllAtomicTextBlocksPartiallyInStringBoundary(stringTextRange); if (textBlocks.isEmpty()) { diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/EntityLogCreatorService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/EntityLogCreatorService.java index d0271272..5cf13382 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/EntityLogCreatorService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/EntityLogCreatorService.java @@ -142,44 +142,39 @@ public class EntityLogCreatorService { private List createEntityLogEntries(Document document, String dossierTemplateId, List notFoundManualRedactionEntries) { List entries = new ArrayList<>(); - Set processedIds = new HashSet<>(); document.getEntities() .stream() .filter(entity -> !entity.getValue().isEmpty()) .filter(EntityLogCreatorService::notFalsePositiveOrFalseRecommendation) .filter(entity -> !entity.removed()) - .forEach(entityNode -> entries.addAll(toEntityLogEntries(entityNode, dossierTemplateId, processedIds))); + .forEach(entityNode -> entries.addAll(toEntityLogEntries(entityNode))); document.streamAllImages().filter(entity -> !entity.removed()).forEach(imageNode -> entries.add(createEntityLogEntry(imageNode, dossierTemplateId))); - notFoundManualRedactionEntries.stream().filter(entity -> !entity.removed()).forEach(manualEntity -> entries.add(createEntityLogEntry(manualEntity, dossierTemplateId))); + notFoundManualRedactionEntries.stream().filter(entity -> !entity.removed()).forEach(manualEntity -> entries.add(createEntityLogEntry(manualEntity))); return entries; } - private List toEntityLogEntries(TextEntity textEntity, String dossierTemplateId, Set processedIds) { + private List toEntityLogEntries(TextEntity textEntity) { - List redactionLogEntities = new ArrayList<>(); + List entityLogEntries = new ArrayList<>(); + // split entity into multiple entries if it occurs on multiple pages, since FE can't handle multi page entities for (PositionOnPage positionOnPage : textEntity.getPositionsOnPagePerPage()) { - // Duplicates should be removed. They might exist due to table extraction duplicating cells spanning multiple columns/rows. - if (processedIds.contains(positionOnPage.getId())) { - continue; - } - processedIds.add(positionOnPage.getId()); - - EntityLogEntry entityLogEntries = createEntityLogEntry(textEntity, dossierTemplateId); - entityLogEntries.setId(positionOnPage.getId()); + EntityLogEntry entityLogEntry = createEntityLogEntry(textEntity); List rectanglesPerLine = positionOnPage.getRectanglePerLine() .stream() .map(rectangle2D -> new Position(rectangle2D, positionOnPage.getPage().getNumber())) .toList(); - entityLogEntries.setPositions(rectanglesPerLine); - redactionLogEntities.add(entityLogEntries); + // set the ID from the positions, since it might contain a "-" with the page number if the entity is split across multiple pages + entityLogEntry.setId(positionOnPage.getId()); + entityLogEntry.setPositions(rectanglesPerLine); + entityLogEntries.add(entityLogEntry); } - return redactionLogEntities; + return entityLogEntries; } @@ -190,8 +185,6 @@ public class EntityLogCreatorService { return EntityLogEntry.builder() .id(image.getId()) .value(image.value()) - .color(getColor(imageType, dossierTemplateId, image.applied(), isHint)) - .value(image.value()) .type(imageType) .reason(image.buildReasonWithManualChangeDescriptions()) .legalBasis(image.legalBasis()) @@ -210,13 +203,12 @@ public class EntityLogCreatorService { } - private EntityLogEntry createEntityLogEntry(ManualEntity manualEntity, String dossierTemplateId) { + private EntityLogEntry createEntityLogEntry(ManualEntity manualEntity) { String type = manualEntity.getManualOverwrite().getType().orElse(manualEntity.getType()); boolean isHint = isHint(manualEntity.getEntityType()); return EntityLogEntry.builder() .id(manualEntity.getId()) - .color(getColor(type, dossierTemplateId, manualEntity.applied(), isHint)) .reason(manualEntity.buildReasonWithManualChangeDescriptions()) .legalBasis(manualEntity.legalBasis()) .value(manualEntity.value()) @@ -246,17 +238,16 @@ public class EntityLogCreatorService { } - private EntityLogEntry createEntityLogEntry(TextEntity entity, String dossierTemplateId) { + private EntityLogEntry createEntityLogEntry(TextEntity entity) { Set referenceIds = new HashSet<>(); entity.references().stream().filter(TextEntity::active).forEach(ref -> ref.getPositionsOnPagePerPage().forEach(pos -> referenceIds.add(pos.getId()))); boolean isHint = isHint(entity.getEntityType()); return EntityLogEntry.builder() - .color(getColor(entity.getType(), dossierTemplateId, entity.applied(), isHint)) .reason(entity.buildReasonWithManualChangeDescriptions()) .legalBasis(entity.legalBasis()) .value(entity.getManualOverwrite().getValue().orElse(entity.getMatchedRule().isWriteValueWithLineBreaks() ? entity.getValueWithLineBreaks() : entity.getValue())) - .type(entity.getType()) + .type(entity.type()) .section(entity.getManualOverwrite().getSection().orElse(entity.getDeepestFullyContainingNode().toString())) .containingNodeId(entity.getDeepestFullyContainingNode().getTreeId()) .closestHeadline(entity.getDeepestFullyContainingNode().getHeadline().getTextBlock().getSearchText()) @@ -282,16 +273,6 @@ public class EntityLogCreatorService { } - private float[] getColor(String type, String dossierTemplateId, boolean isApplied, boolean isHint) { - - if (!isApplied && !isHint) { - return dictionaryService.getNotRedactedColor(dossierTemplateId); - } - return dictionaryService.getColor(type, dossierTemplateId); - - } - - private EntryState buildEntryState(IEntity entity) { if (entity.applied() && entity.active()) { diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/ManualChangesApplicationService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/ManualChangesApplicationService.java index 55bf7620..c0f9c402 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/ManualChangesApplicationService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/ManualChangesApplicationService.java @@ -1,54 +1,70 @@ package com.iqser.red.service.redaction.v1.server.service; import java.awt.geom.Rectangle2D; +import java.util.ArrayList; +import java.util.Collection; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; -import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.Rectangle; +import org.springframework.stereotype.Service; + +import com.google.common.collect.Sets; import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualRecategorization; import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualResizeRedaction; -import com.iqser.red.service.redaction.v1.server.model.ClosestEntity; -import com.iqser.red.service.redaction.v1.server.model.dictionary.SearchImplementation; -import com.iqser.red.service.redaction.v1.server.model.document.TextRange; -import com.iqser.red.service.redaction.v1.server.model.document.entity.EntityType; +import com.iqser.red.service.redaction.v1.server.model.ManualEntity; import com.iqser.red.service.redaction.v1.server.model.document.entity.IEntity; import com.iqser.red.service.redaction.v1.server.model.document.entity.PositionOnPage; import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity; import com.iqser.red.service.redaction.v1.server.model.document.nodes.Image; import com.iqser.red.service.redaction.v1.server.model.document.nodes.ImageType; +import com.iqser.red.service.redaction.v1.server.model.document.nodes.Page; import com.iqser.red.service.redaction.v1.server.model.document.nodes.SemanticNode; -import com.iqser.red.service.redaction.v1.server.service.document.EntityCreationService; +import com.iqser.red.service.redaction.v1.server.service.document.EntityFindingUtility; import com.iqser.red.service.redaction.v1.server.utils.RectangleTransformations; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; +@Service @RequiredArgsConstructor @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) public class ManualChangesApplicationService { - EntityCreationService entityCreationService; + static double MATCH_THRESHOLD = 10; - public void recategorize(IEntity IEntityToBeReCategorized, ManualRecategorization manualRecategorization) { + EntityFindingUtility entityFindingUtility; - if (IEntityToBeReCategorized instanceof Image image) { + + @Deprecated + public void recategorize(IEntity entityToBeReCategorized, ManualRecategorization manualRecategorization) { + + entityToBeReCategorized.getMatchedRuleList().clear(); + entityToBeReCategorized.getManualOverwrite().addChange(manualRecategorization); + + if (entityToBeReCategorized instanceof Image image) { image.setImageType(ImageType.fromString(manualRecategorization.getType())); return; } - // need to create a new entity and copy over all values, since type is part of the primary key for entities and should never be changed! - if (IEntityToBeReCategorized instanceof TextEntity textEntity) { - TextEntity recategorizedEntity = entityCreationService.copyEntityWithoutRules(textEntity, manualRecategorization.getType(), textEntity.getEntityType(), textEntity.getDeepestFullyContainingNode()); - recategorizedEntity.setPositionsOnPagePerPage(textEntity.getPositionsOnPagePerPage()); - recategorizedEntity.getManualOverwrite().addChange(manualRecategorization); - textEntity.remove("FINAL.0.0", "removed by manual recategorization"); + + if (entityToBeReCategorized instanceof TextEntity textEntity) { + textEntity.setType(manualRecategorization.getType()); } } + public void resize(TextEntity entityToBeResized, ManualResizeRedaction manualResizeRedaction) { + + resizeEntityAndReinsert(entityToBeResized, manualResizeRedaction); + } + + + @Deprecated public void resizeEntityAndReinsert(TextEntity entityToBeResized, ManualResizeRedaction manualResizeRedaction) { PositionOnPage positionOnPageToBeResized = entityToBeResized.getPositionsOnPagePerPage() @@ -57,96 +73,62 @@ public class ManualChangesApplicationService { .findFirst() .orElseThrow(() -> new NoSuchElementException("No redaction position with matching annotation id found!")); - positionOnPageToBeResized.setRectanglePerLine(manualResizeRedaction.getPositions().stream().map(ManualChangesApplicationService::toRectangle2D).collect(Collectors.toList())); + positionOnPageToBeResized.setRectanglePerLine(manualResizeRedaction.getPositions() + .stream() + .map(ManualChangesApplicationService::toRectangle2D) + .collect(Collectors.toList())); - String value = manualResizeRedaction.getValue(); - int newStartOffset = -1; SemanticNode node = entityToBeResized.getDeepestFullyContainingNode(); - ClosestEntity closestEntity = ClosestEntity.builder().distance(100).textRange(null).build(); - + ManualEntity searchEntity = ManualEntity.fromManualResizeRedaction(manualResizeRedaction); // Loop through nodes starting from the deepest fully containing node all the way to the document node while (node != null) { - if (node.containsString(value)) { - SearchImplementation searchImplementation = new SearchImplementation(value, false); - List textRanges = searchImplementation.getBoundaries(node.getTextBlock(), node.getTextRange()); - for (TextRange textRange : textRanges) { - SemanticNode finalNode = node; + Map> possibleEntities = entityFindingUtility.findAllPossibleEntitiesAndGroupByValue(node, List.of(searchEntity)); + Optional closestEntity = entityFindingUtility.findClosestEntityAndReturnEmptyIfNotFound(searchEntity, possibleEntities, MATCH_THRESHOLD); - List tempEntities = searchImplementation.getBoundaries(node.getTextBlock(), textRange) - .stream() - .map(boundary -> entityCreationService.forceByTextRange(boundary, "temp", EntityType.ENTITY, finalNode)) - .collect(Collectors.toList()); - - // If a value appears multiple times in a section after resizing, we need to make sure we select the surrounding text for the correct one. - determineCorrectEntity(manualResizeRedaction, textRange, tempEntities, closestEntity); - - // Remove all temp entities from the graph - tempEntities.forEach(TextEntity::removeFromGraph); - } - break; + if (closestEntity.isPresent()) { + copyValuesFromClosestEntity(entityToBeResized, manualResizeRedaction, closestEntity.get()); + possibleEntities.values().stream().flatMap(Collection::stream).forEach(TextEntity::removeFromGraph); + return; } - // If the current node is the document node then it does not have a parent, meaning we could not find the value anywhere. + possibleEntities.values().stream().flatMap(Collection::stream).forEach(TextEntity::removeFromGraph); + if (node.hasParent()) { node = node.getParent(); } else { break; } } + } - if (closestEntity.getTextRange() != null) { - newStartOffset = closestEntity.getTextRange().start(); - } - // need to reinsert the entity, due to the boundary having changed. - if (newStartOffset > -1) { - removeAndUpdateAndReInsertEntity(entityToBeResized, manualResizeRedaction, newStartOffset); - } + private static void copyValuesFromClosestEntity(TextEntity entityToBeResized, ManualResizeRedaction manualResizeRedaction, TextEntity closestEntity) { + + Set currentIntersectingNodes = new HashSet<>(entityToBeResized.getIntersectingNodes()); + Set newIntersectingNodes = new HashSet<>(closestEntity.getIntersectingNodes()); + + Sets.difference(currentIntersectingNodes, newIntersectingNodes).forEach(removedNode -> removedNode.getEntities().remove(entityToBeResized)); + Sets.difference(newIntersectingNodes, currentIntersectingNodes).forEach(addedNode -> addedNode.getEntities().add(entityToBeResized)); + + Set currentIntersectingPages = new HashSet<>(entityToBeResized.getPages()); + Set newIntersectingPages = new HashSet<>(closestEntity.getPages()); + + Sets.difference(currentIntersectingPages, newIntersectingPages).forEach(removedPage -> removedPage.getEntities().remove(entityToBeResized)); + Sets.difference(newIntersectingPages, currentIntersectingPages).forEach(addedPage -> addedPage.getEntities().add(entityToBeResized)); + + entityToBeResized.setDeepestFullyContainingNode(closestEntity.getDeepestFullyContainingNode()); + entityToBeResized.setIntersectingNodes(new ArrayList<>(newIntersectingNodes)); + entityToBeResized.setTextRange(closestEntity.getTextRange()); + entityToBeResized.setTextAfter(closestEntity.getTextAfter()); + entityToBeResized.setTextBefore(closestEntity.getTextBefore()); + entityToBeResized.setDuplicateTextRanges(new ArrayList<>(closestEntity.getDuplicateTextRanges())); + entityToBeResized.setValue(closestEntity.getValue()); + entityToBeResized.setPages(newIntersectingPages); entityToBeResized.getManualOverwrite().addChange(manualResizeRedaction); } - public static void determineCorrectEntity(ManualResizeRedaction manualResizeRedaction, - TextRange textRange, - List tempEntities, - ClosestEntity closestEntity) { - - double currentDistance = calculateClosest(manualResizeRedaction.getPositions().get(0), tempEntities.get(0).getPositionsOnPagePerPage()); - if (currentDistance < closestEntity.getDistance()) { - closestEntity.setDistance(currentDistance); - closestEntity.setTextRange(textRange); - } - } - - - private static double calculateClosest(Rectangle position, List positionOnPages) { - - Rectangle2D rectangle2D = positionOnPages.get(0).getRectanglePerLine().get(0); - double difference = 0; - difference += position.getTopLeftX() > rectangle2D.getX() ? (position.getTopLeftX() - rectangle2D.getX()) : (rectangle2D.getX() - position.getTopLeftX()); - difference += position.getTopLeftY() > rectangle2D.getY() ? (position.getTopLeftY() - rectangle2D.getY()) : (rectangle2D.getY() - position.getTopLeftY()); - difference += position.getWidth() > rectangle2D.getWidth() ? (position.getWidth() - rectangle2D.getWidth()) : (rectangle2D.getWidth() - position.getWidth()); - difference += position.getHeight() > rectangle2D.getHeight() ? (position.getHeight() - rectangle2D.getHeight()) : (rectangle2D.getHeight() - position.getHeight()); - return difference; - } - - - private void removeAndUpdateAndReInsertEntity(TextEntity entityToBeResized, ManualResizeRedaction manualResizeRedaction, int newStartOffset) { - - SemanticNode nodeToInsertInto = entityToBeResized.getDeepestFullyContainingNode().getDocumentTree().getRoot().getNode(); - entityToBeResized.getIntersectingNodes().forEach(node -> node.getEntities().remove(this)); - entityToBeResized.getPages().forEach(page -> page.getEntities().remove(this)); - entityToBeResized.setIntersectingNodes(new LinkedList<>()); - entityToBeResized.setDeepestFullyContainingNode(null); - entityToBeResized.setPages(new HashSet<>()); - entityToBeResized.getTextRange().setStart(newStartOffset); - entityToBeResized.getTextRange().setEnd(newStartOffset + manualResizeRedaction.getValue().length()); - - entityCreationService.addEntityToGraph(entityToBeResized, nodeToInsertInto); - } - - public void resizeImage(Image image, ManualResizeRedaction manualResizeRedaction) { if (manualResizeRedaction.getPositions().isEmpty() || manualResizeRedaction.getPositions() == null) { diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/RedactionLogCreatorService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/RedactionLogCreatorService.java index 968e16c4..596167ea 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/RedactionLogCreatorService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/RedactionLogCreatorService.java @@ -100,11 +100,11 @@ public class RedactionLogCreatorService { int sectionNumber = entity.getDeepestFullyContainingNode().getTreeId().isEmpty() ? 0 : entity.getDeepestFullyContainingNode().getTreeId().get(0); boolean isHint = isHint(entity.getEntityType()); return RedactionLogEntry.builder() - .color(getColor(entity.getType(), dossierTemplateId, entity.applied(), isHint)) + .color(getColor(entity.type(), dossierTemplateId, entity.applied(), isHint)) .reason(entity.buildReasonWithManualChangeDescriptions()) .legalBasis(entity.legalBasis()) .value(entity.getManualOverwrite().getValue().orElse(entity.getMatchedRule().isWriteValueWithLineBreaks() ? entity.getValueWithLineBreaks() : entity.getValue())) - .type(entity.getType()) + .type(entity.type()) .redacted(entity.applied()) .isHint(isHint) .isRecommendation(entity.getEntityType().equals(EntityType.RECOMMENDATION)) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/UnprocessedChangesService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/UnprocessedChangesService.java index d9aa03ea..dc1c49aa 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/UnprocessedChangesService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/UnprocessedChangesService.java @@ -3,10 +3,8 @@ package com.iqser.red.service.redaction.v1.server.service; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.NoSuchElementException; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -23,11 +21,8 @@ import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations import com.iqser.red.service.redaction.v1.model.AnalyzeResponse; import com.iqser.red.service.redaction.v1.model.QueueNames; import com.iqser.red.service.redaction.v1.model.UnprocessedManualEntity; -import com.iqser.red.service.redaction.v1.server.model.ClosestEntity; import com.iqser.red.service.redaction.v1.server.model.ManualEntity; -import com.iqser.red.service.redaction.v1.server.model.dictionary.SearchImplementation; import com.iqser.red.service.redaction.v1.server.model.document.TextRange; -import com.iqser.red.service.redaction.v1.server.model.document.entity.EntityType; import com.iqser.red.service.redaction.v1.server.model.document.entity.PositionOnPage; import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity; import com.iqser.red.service.redaction.v1.server.model.document.nodes.Document; @@ -62,6 +57,8 @@ public class UnprocessedChangesService { final EntityEnrichmentService entityEnrichmentService; final ManualEntityCreationService manualEntityCreationService; final DictionaryService dictionaryService; + final ManualChangesApplicationService manualChangesApplicationService; + EntityCreationService entityCreationService; @@ -145,8 +142,21 @@ public class UnprocessedChangesService { continue; } - TextEntity correctEntity = createCorrectEntity(manualEntity, document, optionalTextEntity.get().getTextRange()); - resizeEntityAndReinsert(correctEntity, manualResizeRedactions.stream().filter(manualResizeRedaction -> manualResizeRedaction.getAnnotationId().equals(manualEntity.getId())).findFirst()); + TextEntity correctEntity = createCorrectEntity(manualEntity, optionalTextEntity.get()); + Optional optionalManualResizeRedaction = manualResizeRedactions.stream() + .filter(manualResizeRedaction -> manualResizeRedaction.getAnnotationId().equals(manualEntity.getId())) + .findFirst(); + if (optionalManualResizeRedaction.isPresent()) { + ManualResizeRedaction manualResizeRedaction = optionalManualResizeRedaction.get(); + manualChangesApplicationService.resizeEntityAndReinsert(correctEntity, manualResizeRedaction); + + // If the entity's value is not the same as the manual resize request's value it means we didn't find it anywhere and we want to remove it + // from the graph, so it does not get processed and sent back to persistence-service to update its value. + if (!correctEntity.getValue().equals(manualResizeRedaction.getValue())) { + correctEntity.removeFromGraph(); + } + } + } // remove all temp entities from the graph @@ -154,101 +164,27 @@ public class UnprocessedChangesService { } - public void resizeEntityAndReinsert(TextEntity entityToBeResized, Optional optionalManualResizeRedaction) { + private TextEntity createCorrectEntity(ManualEntity manualEntity, TextEntity closestEntity) { - if (optionalManualResizeRedaction.isEmpty()) { - return; - } + TextEntity correctEntity = TextEntity.initialEntityNode(closestEntity.getTextRange(), manualEntity.type(), manualEntity.getEntityType(), manualEntity.getId()); - ManualResizeRedaction manualResizeRedaction = optionalManualResizeRedaction.get(); + correctEntity.setDeepestFullyContainingNode(closestEntity.getDeepestFullyContainingNode()); + correctEntity.setIntersectingNodes(new ArrayList<>(closestEntity.getIntersectingNodes())); + correctEntity.setDuplicateTextRanges(new ArrayList<>(closestEntity.getDuplicateTextRanges())); + correctEntity.setPages(new HashSet<>(closestEntity.getPages())); - PositionOnPage positionOnPageToBeResized = entityToBeResized.getPositionsOnPagePerPage() - .stream() - .filter(redactionPosition -> redactionPosition.getId().equals(manualResizeRedaction.getAnnotationId())) - .findFirst() - .orElseThrow(() -> new NoSuchElementException("No redaction position with matching annotation id found!")); + correctEntity.setValue(closestEntity.getValue()); + correctEntity.setTextAfter(closestEntity.getTextAfter()); + correctEntity.setTextBefore(closestEntity.getTextBefore()); - positionOnPageToBeResized.setRectanglePerLine(manualResizeRedaction.getPositions().stream().map(ManualChangesApplicationService::toRectangle2D).collect(Collectors.toList())); - - String value = manualResizeRedaction.getValue(); - int newStartOffset = -1; - SemanticNode node = entityToBeResized.getDeepestFullyContainingNode(); - ClosestEntity closestEntity = ClosestEntity.builder().distance(100).textRange(null).build(); - - // Loop through nodes starting from the deepest fully containing node all the way to the document node - while (node != null) { - if (node.containsString(value)) { - SearchImplementation searchImplementation = new SearchImplementation(value, false); - List textRanges = searchImplementation.getBoundaries(node.getTextBlock(), node.getTextRange()); - - for (TextRange textRange : textRanges) { - SemanticNode finalNode = node; - - List tempEntities = searchImplementation.getBoundaries(node.getTextBlock(), textRange) - .stream() - .map(boundary -> entityCreationService.forceByTextRange(boundary, "temp", EntityType.ENTITY, finalNode)) - .collect(Collectors.toList()); - - // If a value appears multiple times in a section after resizing, we need to make sure we select the surrounding text for the correct one. - ManualChangesApplicationService.determineCorrectEntity(manualResizeRedaction, textRange, tempEntities, closestEntity); - - // Remove all temp entities from the graph - tempEntities.forEach(TextEntity::removeFromGraph); - } - break; - } - - // If the current node is the document node then it does not have a parent, meaning we could not find the value anywhere. - if (node.hasParent()) { - node = node.getParent(); - } else { - break; - } - } - - if (closestEntity.getTextRange() != null) { - newStartOffset = closestEntity.getTextRange().start(); - } - - // need to reinsert the entity, due to the boundary having changed. - removeAndUpdateAndReInsertEntity(entityToBeResized, manualResizeRedaction, newStartOffset); - entityToBeResized.getManualOverwrite().addChange(manualResizeRedaction); - } - - - private void removeAndUpdateAndReInsertEntity(TextEntity entityToBeResized, ManualResizeRedaction manualResizeRedaction, int newStartOffset) { - - SemanticNode nodeToInsertInto = entityToBeResized.getDeepestFullyContainingNode().getDocumentTree().getRoot().getNode(); - entityToBeResized.removeFromGraph(); - entityToBeResized.getIntersectingNodes().forEach(node -> node.getEntities().remove(this)); - entityToBeResized.getPages().forEach(page -> page.getEntities().remove(this)); - entityToBeResized.setIntersectingNodes(new LinkedList<>()); - entityToBeResized.setDeepestFullyContainingNode(null); - entityToBeResized.setPages(new HashSet<>()); - entityToBeResized.getTextRange().setStart(newStartOffset); - entityToBeResized.getTextRange().setEnd(newStartOffset + manualResizeRedaction.getValue().length()); - // Don't insert into the graph if newStartOffset is -1 because it means nothing was found. - if (newStartOffset > -1) { - entityCreationService.addEntityToGraph(entityToBeResized, nodeToInsertInto); - } - } - - - private TextEntity createCorrectEntity(ManualEntity manualEntity, SemanticNode node, TextRange closestTextRange) { - - TextEntity correctEntity = entityCreationService.forceByTextRange(closestTextRange, manualEntity.getType(), manualEntity.getEntityType(), node); + correctEntity.getIntersectingNodes().forEach(n -> n.getEntities().add(correctEntity)); + correctEntity.getPages().forEach(page -> page.getEntities().add(correctEntity)); correctEntity.addMatchedRules(manualEntity.getMatchedRuleList()); correctEntity.setDictionaryEntry(manualEntity.isDictionaryEntry()); correctEntity.setDossierDictionaryEntry(manualEntity.isDossierDictionaryEntry()); correctEntity.getManualOverwrite().addChanges(manualEntity.getManualOverwrite().getManualChangeLog()); - List redactionPositionsWithIdOfManualOnPage = new ArrayList<>(correctEntity.getPositionsOnPagePerPage().size()); - for (PositionOnPage positionOnPage : correctEntity.getPositionsOnPagePerPage()) { - redactionPositionsWithIdOfManualOnPage.add(new PositionOnPage(manualEntity.getId(), positionOnPage.getPage(), positionOnPage.getRectanglePerLine())); - } - correctEntity.setPositionsOnPagePerPage(redactionPositionsWithIdOfManualOnPage); - return correctEntity; } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/EntityCreationService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/EntityCreationService.java index bdfa11d5..4a9d31d4 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/EntityCreationService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/EntityCreationService.java @@ -1,5 +1,6 @@ package com.iqser.red.service.redaction.v1.server.service.document; +import static com.iqser.red.service.redaction.v1.server.service.document.EntityCreationUtility.*; import static com.iqser.red.service.redaction.v1.server.utils.SeparatorUtils.boundaryIsSurroundedBySeparators; import java.util.Collection; @@ -18,24 +19,21 @@ import org.kie.api.runtime.KieSession; import com.google.common.base.Functions; import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Engine; -import com.iqser.red.service.redaction.v1.server.model.document.TextRange; +import com.iqser.red.service.redaction.v1.server.model.NerEntities; +import com.iqser.red.service.redaction.v1.server.model.dictionary.SearchImplementation; import com.iqser.red.service.redaction.v1.server.model.document.ConsecutiveBoundaryCollector; import com.iqser.red.service.redaction.v1.server.model.document.DocumentTree; +import com.iqser.red.service.redaction.v1.server.model.document.TextRange; import com.iqser.red.service.redaction.v1.server.model.document.entity.EntityType; import com.iqser.red.service.redaction.v1.server.model.document.entity.ManualChangeOverwrite; import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity; -import com.iqser.red.service.redaction.v1.server.model.document.entity.PositionOnPage; import com.iqser.red.service.redaction.v1.server.model.document.nodes.NodeType; -import com.iqser.red.service.redaction.v1.server.model.document.nodes.Page; import com.iqser.red.service.redaction.v1.server.model.document.nodes.SemanticNode; import com.iqser.red.service.redaction.v1.server.model.document.nodes.Table; import com.iqser.red.service.redaction.v1.server.model.document.nodes.TableCell; import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlock; import com.iqser.red.service.redaction.v1.server.utils.RectangleTransformations; import com.iqser.red.service.redaction.v1.server.utils.RedactionSearchUtility; -import com.iqser.red.service.redaction.v1.server.model.NerEntities; -import com.iqser.red.service.redaction.v1.server.model.dictionary.SearchImplementation; -import com.iqser.red.service.redaction.v1.server.utils.IdBuilder; import com.iqser.red.service.redaction.v1.server.utils.exception.NotFoundException; import lombok.RequiredArgsConstructor; @@ -203,6 +201,7 @@ public class EntityCreationService { return betweenTextRanges(startTextRanges, stopTextRanges, type, entityType, node); } + public Stream shortestBetweenAnyStringIgnoreCase(List starts, List stops, String type, EntityType entityType, SemanticNode node, int limit) { checkIfBothStartAndEndAreEmpty(starts, stops); @@ -233,9 +232,10 @@ public class EntityCreationService { return betweenTextRanges(startBoundaries, stopBoundaries, type, entityType, node); } + public Stream betweenTextRanges(List startBoundaries, List stopBoundaries, String type, EntityType entityType, SemanticNode node) { - return betweenTextRanges(startBoundaries, stopBoundaries, type, entityType, node,0); + return betweenTextRanges(startBoundaries, stopBoundaries, type, entityType, node, 0); } @@ -282,22 +282,11 @@ public class EntityCreationService { } - private void checkIfBothStartAndEndAreEmpty(String start, String end) { - checkIfBothStartAndEndAreEmpty(List.of(start), List.of(end)); - } - - - private void checkIfBothStartAndEndAreEmpty(List start, List end) { - if ((start == null || start.isEmpty()) && (end == null || end.isEmpty())) { - throw new IllegalArgumentException("Start and end values are empty!"); - } - } - - public Stream bySearchImplementation(SearchImplementation searchImplementation, String type, EntityType entityType, SemanticNode node) { return searchImplementation.getBoundaries(node.getTextBlock(), node.getTextRange()) - .stream().filter(boundary -> isValidEntityTextRange(node.getTextBlock(), boundary)) + .stream() + .filter(boundary -> isValidEntityTextRange(node.getTextBlock(), boundary)) .map(bounds -> byTextRange(bounds, type, entityType, node)) .filter(Optional::isPresent) .map(Optional::get); @@ -309,7 +298,9 @@ public class EntityCreationService { TextBlock textBlock = node.getTextBlock(); SearchImplementation searchImplementation = new SearchImplementation(strings, false); return searchImplementation.getBoundaries(textBlock, node.getTextRange()) - .stream().map(boundary -> toLineAfterTextRange(textBlock, boundary)).filter(boundary -> isValidEntityTextRange(textBlock, boundary)) + .stream() + .map(boundary -> toLineAfterTextRange(textBlock, boundary)) + .filter(boundary -> isValidEntityTextRange(textBlock, boundary)) .map(boundary -> byTextRange(boundary, type, entityType, node)) .filter(Optional::isPresent) .map(Optional::get); @@ -321,7 +312,9 @@ public class EntityCreationService { TextBlock textBlock = node.getTextBlock(); SearchImplementation searchImplementation = new SearchImplementation(strings, true); return searchImplementation.getBoundaries(textBlock, node.getTextRange()) - .stream().map(boundary -> toLineAfterTextRange(textBlock, boundary)).filter(boundary -> isValidEntityTextRange(textBlock, boundary)) + .stream() + .map(boundary -> toLineAfterTextRange(textBlock, boundary)) + .filter(boundary -> isValidEntityTextRange(textBlock, boundary)) .map(boundary -> byTextRange(boundary, type, entityType, node)) .filter(Optional::isPresent) .map(Optional::get); @@ -332,7 +325,9 @@ public class EntityCreationService { TextBlock textBlock = node.getTextBlock(); return RedactionSearchUtility.findTextRangesByString(string, textBlock) - .stream().map(boundary -> toLineAfterTextRange(textBlock, boundary)).filter(boundary -> isValidEntityTextRange(textBlock, boundary)) + .stream() + .map(boundary -> toLineAfterTextRange(textBlock, boundary)) + .filter(boundary -> isValidEntityTextRange(textBlock, boundary)) .map(boundary -> byTextRange(boundary, type, entityType, node)) .filter(Optional::isPresent) .map(Optional::get); @@ -343,7 +338,9 @@ public class EntityCreationService { TextBlock textBlock = node.getTextBlock(); return RedactionSearchUtility.findTextRangesByStringIgnoreCase(string, textBlock) - .stream().map(boundary -> toLineAfterTextRange(textBlock, boundary)).filter(boundary -> isValidEntityTextRange(textBlock, boundary)) + .stream() + .map(boundary -> toLineAfterTextRange(textBlock, boundary)) + .filter(boundary -> isValidEntityTextRange(textBlock, boundary)) .map(boundary -> byTextRange(boundary, type, entityType, node)) .filter(Optional::isPresent) .map(Optional::get); @@ -352,7 +349,8 @@ public class EntityCreationService { public Stream lineAfterStringAcrossColumns(String string, String type, EntityType entityType, Table tableNode) { - return tableNode.streamTableCells().flatMap(tableCell -> lineAfterBoundariesAcrossColumns(RedactionSearchUtility.findTextRangesByString(string, tableCell.getTextBlock()), + return tableNode.streamTableCells() + .flatMap(tableCell -> lineAfterBoundariesAcrossColumns(RedactionSearchUtility.findTextRangesByString(string, tableCell.getTextBlock()), tableCell, type, entityType, @@ -545,7 +543,7 @@ public class EntityCreationService { public Optional byPrefixExpansionRegex(TextEntity entity, String regexPattern) { int expandedStart = RedactionSearchUtility.getExpandedStartByRegex(entity, regexPattern); - return byTextRange(new TextRange(expandedStart, entity.getTextRange().end()), entity.getType(), entity.getEntityType(), entity.getDeepestFullyContainingNode()); + return byTextRange(new TextRange(expandedStart, entity.getTextRange().end()), entity.type(), entity.getEntityType(), entity.getDeepestFullyContainingNode()); } @@ -553,18 +551,10 @@ public class EntityCreationService { int expandedEnd = RedactionSearchUtility.getExpandedEndByRegex(entity, regexPattern); expandedEnd = truncateEndIfLineBreakIsBetween(entity.getTextRange().end(), expandedEnd, entity.getDeepestFullyContainingNode().getTextBlock()); - return byTextRange(new TextRange(entity.getTextRange().start(), expandedEnd), entity.getType(), entity.getEntityType(), entity.getDeepestFullyContainingNode()); + return byTextRange(new TextRange(entity.getTextRange().start(), expandedEnd), entity.type(), entity.getEntityType(), entity.getDeepestFullyContainingNode()); } - private int truncateEndIfLineBreakIsBetween(int end, int expandedEnd, TextBlock textBlock) { - - if (textBlock.getNextLinebreak(end) < expandedEnd) { - return end; - } - return expandedEnd; - } - /** * Creates a redaction entity based on the given boundary, type, entity type, and semantic node. * If the document already contains an equal redaction entity, then the original Entity is returned. @@ -581,6 +571,7 @@ public class EntityCreationService { return byTextRangeWithEngine(textRange, type, entityType, node, Set.of(Engine.RULE)); } + /** * Creates a redaction entity based on the given boundary, type, entity type, and semantic node. * If the document already contains an equal redaction entity, then the original Entity is returned. @@ -599,9 +590,21 @@ public class EntityCreationService { throw new IllegalArgumentException(String.format("%s is not in the %s of the provided semantic node %s", textRange, node.getTextRange(), node)); } TextRange trimmedTextRange = textRange.trim(node.getTextBlock()); - TextEntity entity = TextEntity.initialEntityNode(trimmedTextRange, type, entityType); + TextEntity entity = TextEntity.initialEntityNode(trimmedTextRange, type, entityType, node); if (node.getEntities().contains(entity)) { - return node.getEntities().stream().filter(entity::equals).peek(e -> e.addEngines(engines)).findAny(); + Optional optionalTextEntity = node.getEntities().stream().filter(e -> e.equals(entity) && e.type().equals(type)).peek(e -> e.addEngines(engines)).findAny(); + if (optionalTextEntity.isEmpty()) { + return optionalTextEntity; // Entity has been recategorized and should not be created at all. + } + TextEntity existingEntity = optionalTextEntity.get(); + if (existingEntity.getTextRange().equals(textRange)) { + return optionalTextEntity; // exactly the same entity, return directly + } + if (!existingEntity.resized()) { + addDuplicateEntityToGraph(existingEntity, textRange, node); + return optionalTextEntity; // If Entity has not been resized, insert as duplicate at appropriate position + } + return Optional.empty(); // Entity has been resized, if there are duplicates they should be treated there } addEntityToGraph(entity, node); entity.addEngines(engines); @@ -610,10 +613,23 @@ public class EntityCreationService { } + // Do not use anymore. This might not work correctly due to duplicate textranges not being taken into account here. + @Deprecated(forRemoval = true) public TextEntity forceByTextRange(TextRange textRange, String type, EntityType entityType, SemanticNode node) { TextRange trimmedTextRange = textRange.trim(node.getTextBlock()); - TextEntity entity = TextEntity.initialEntityNode(trimmedTextRange, type, entityType); + TextEntity entity = TextEntity.initialEntityNode(trimmedTextRange, type, entityType, node); + addEntityToGraph(entity, node); + return entity; + } + + + // Do not use anymore. This might not work correctly due to duplicate textranges not being taken into account here. + @Deprecated(forRemoval = true) + public TextEntity forceByTextRange(TextRange textRange, String type, EntityType entityType, SemanticNode node, String id) { + + TextRange trimmedTextRange = textRange.trim(node.getTextBlock()); + TextEntity entity = TextEntity.initialEntityNode(trimmedTextRange, type, entityType, id); addEntityToGraph(entity, node); return entity; } @@ -631,7 +647,7 @@ public class EntityCreationService { return entitiesToMerge.get(0); } - TextEntity mergedEntity = TextEntity.initialEntityNode(TextRange.merge(entitiesToMerge.stream().map(TextEntity::getTextRange).toList()), type, entityType); + TextEntity mergedEntity = TextEntity.initialEntityNode(TextRange.merge(entitiesToMerge.stream().map(TextEntity::getTextRange).toList()), type, entityType, node); mergedEntity.addEngines(entitiesToMerge.stream().flatMap(entityNode -> entityNode.getEngines().stream()).collect(Collectors.toSet())); entitiesToMerge.stream().map(TextEntity::getMatchedRuleList).flatMap(Collection::stream).forEach(matchedRule -> mergedEntity.getMatchedRuleList().add(matchedRule)); entitiesToMerge.stream() @@ -662,6 +678,7 @@ public class EntityCreationService { return newEntity; } + public TextEntity copyEntityWithoutRules(TextEntity entity, String type, EntityType entityType, SemanticNode node) { TextEntity newEntity = byTextRangeWithEngine(entity.getTextRange(), type, entityType, node, entity.getEngines()).orElseThrow(() -> new NotFoundException( @@ -683,7 +700,8 @@ public class EntityCreationService { public TextEntity byNerEntity(NerEntities.NerEntity nerEntity, EntityType entityType, SemanticNode semanticNode) { - return byTextRangeWithEngine(nerEntity.textRange(), nerEntity.type(), entityType, semanticNode, Set.of(Engine.NER)).orElseThrow(() -> new NotFoundException("No entity present!")); + return byTextRangeWithEngine(nerEntity.textRange(), nerEntity.type(), entityType, semanticNode, Set.of(Engine.NER)).orElseThrow(() -> new NotFoundException( + "No entity present!")); } @@ -693,6 +711,18 @@ public class EntityCreationService { } + public Optional optionalByNerEntity(NerEntities.NerEntity nerEntity, EntityType entityType, SemanticNode semanticNode) { + + return byTextRangeWithEngine(nerEntity.textRange(), nerEntity.type(), entityType, semanticNode, Set.of(Engine.NER)); + } + + + public Optional optionalByNerEntity(NerEntities.NerEntity nerEntity, String type, EntityType entityType, SemanticNode semanticNode) { + + return byTextRangeWithEngine(nerEntity.textRange(), type, entityType, semanticNode, Set.of(Engine.NER)); + } + + public Stream combineNerEntitiesToCbiAddressDefaults(NerEntities nerEntities, String type, EntityType entityType, SemanticNode semanticNode) { return NerEntitiesAdapter.combineNerEntitiesToCbiAddressDefaults(nerEntities) @@ -702,21 +732,6 @@ public class EntityCreationService { } - public TextEntity byTableCellAsHighlight(TableCell tableCell, String type, EntityType entityType) { - - TextEntity highlightEntity = TextEntity.initialEntityNode(new TextRange(tableCell.getTextRange().start(), tableCell.getTextRange().start()), type, entityType); - - String positionId = IdBuilder.buildId(tableCell.getBBox().keySet(), tableCell.getBBox().values().stream().toList(), type, entityType.name()); - highlightEntity.setPositionsOnPagePerPage(tableCell.getBBox() - .entrySet() - .stream() - .map(entry -> new PositionOnPage(positionId, entry.getKey(), List.of(entry.getValue()))) - .toList()); - addEntityToGraph(highlightEntity, tableCell); - return highlightEntity; - } - - public boolean isValidEntityTextRange(TextBlock textBlock, TextRange textRange) { return textRange.length() > 0 && boundaryIsSurroundedBySeparators(textBlock, textRange); @@ -727,9 +742,17 @@ public class EntityCreationService { DocumentTree documentTree = node.getDocumentTree(); try { - addEntityToGraph(entity, documentTree); - entity.addIntersectingNode(documentTree.getRoot().getNode()); - documentTree.getRoot().getNode().getEntities().add(entity); + if (node.getEntities().contains(entity)) { + // If entity already exists and it has a different text range, we add the text range to the list of duplicated text ranges + node.getEntities().stream()// + .filter(e -> e.equals(entity))// + .filter(e -> !e.getTextRange().equals(entity.getTextRange()))// + .findAny()// + .ifPresent(entityToDuplicate -> addDuplicateEntityToGraph(entityToDuplicate, entity.getTextRange(), node)); + } else { + entity.addIntersectingNode(documentTree.getRoot().getNode()); + addEntityToGraph(entity, documentTree); + } } catch (NoSuchElementException e) { entity.setDeepestFullyContainingNode(documentTree.getRoot().getNode()); entityEnrichmentService.enrichEntity(entity, entity.getDeepestFullyContainingNode().getTextBlock()); @@ -740,6 +763,33 @@ public class EntityCreationService { } + private void addDuplicateEntityToGraph(TextEntity entityToDuplicate, TextRange newTextRange, SemanticNode node) { + + entityToDuplicate.addTextRange(newTextRange); + + SemanticNode deepestSharedNode = entityToDuplicate.getIntersectingNodes() + .stream() + .sorted(Comparator.comparingInt(n -> -n.getTreeId().size())) + .filter(intersectingNode -> entityToDuplicate.getDuplicateTextRanges().stream().allMatch(tr -> intersectingNode.getTextRange().contains(tr)) && // + intersectingNode.getTextRange().contains(entityToDuplicate.getTextRange())) + .findFirst() + .orElse(node.getDocumentTree().getRoot().getNode()); + + entityToDuplicate.setDeepestFullyContainingNode(deepestSharedNode); + + Set additionalIntersectingNodes = findIntersectingSubNodes(deepestSharedNode, newTextRange); + + additionalIntersectingNodes.forEach(additionalIntersectingNode -> { + if (entityToDuplicate.getIntersectingNodes().contains(additionalIntersectingNode)) { + return; + } + additionalIntersectingNode.getEntities().add(entityToDuplicate); + additionalIntersectingNode.getPages(newTextRange).forEach(page -> page.getEntities().add(entityToDuplicate)); + entityToDuplicate.addIntersectingNode(additionalIntersectingNode); + }); + } + + private void addEntityToGraph(TextEntity entity, DocumentTree documentTree) { SemanticNode containingNode = documentTree.childNodes(Collections.emptyList()) @@ -757,45 +807,4 @@ public class EntityCreationService { } - private static void addToPages(TextEntity entity) { - - Set pages = entity.getDeepestFullyContainingNode().getPages(entity.getTextRange()); - entity.getPages().addAll(pages); - pages.forEach(page -> page.getEntities().add(entity)); - } - - - private static void addEntityToNodeEntitySets(TextEntity entity) { - - entity.getIntersectingNodes().forEach(node -> node.getEntities().add(entity)); - } - - - private static boolean allEntitiesIntersectAndHaveSameTypes(List entitiesToMerge) { - - if (entitiesToMerge.isEmpty()) { - return true; - } - TextEntity previousEntity = entitiesToMerge.get(0); - for (TextEntity textEntity : entitiesToMerge.subList(1, entitiesToMerge.size())) { - boolean typeMatches = textEntity.getType().equals(previousEntity.getType()); - boolean entityTypeMatches = textEntity.getEntityType().equals(previousEntity.getEntityType()); - boolean intersects = textEntity.intersects(previousEntity); - if (!typeMatches || !entityTypeMatches || !intersects) { - return false; - } - } - return true; - } - - - private static TextRange toLineAfterTextRange(TextBlock textBlock, TextRange textRange) { - - if (textBlock.getTextRange().end() == textRange.end()) { - return new TextRange(textRange.end(), textRange.end()); - } - - return new TextRange(textRange.end(), textBlock.getNextLinebreak(textRange.end())).trim(textBlock); - } - } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/EntityCreationUtility.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/EntityCreationUtility.java new file mode 100644 index 00000000..25f1b0d1 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/EntityCreationUtility.java @@ -0,0 +1,91 @@ +package com.iqser.red.service.redaction.v1.server.service.document; + +import java.util.List; +import java.util.Set; + +import com.iqser.red.service.redaction.v1.server.model.document.TextRange; +import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity; +import com.iqser.red.service.redaction.v1.server.model.document.nodes.Page; +import com.iqser.red.service.redaction.v1.server.model.document.nodes.SemanticNode; +import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlock; + +public class EntityCreationUtility { + + + public static void checkIfBothStartAndEndAreEmpty(String start, String end) { + + checkIfBothStartAndEndAreEmpty(List.of(start), List.of(end)); + } + + + public static void checkIfBothStartAndEndAreEmpty(List start, List end) { + + if ((start == null || start.isEmpty()) && (end == null || end.isEmpty())) { + throw new IllegalArgumentException("Start and end values are empty!"); + } + } + + + public static int truncateEndIfLineBreakIsBetween(int end, int expandedEnd, TextBlock textBlock) { + + if (textBlock.getNextLinebreak(end) < expandedEnd) { + return end; + } + return expandedEnd; + } + + + public static Set findIntersectingSubNodes(SemanticNode initialIntersectingNode, TextRange textRange) { + + IntersectingNodeVisitor visitor = new IntersectingNodeVisitor(textRange); + + if (initialIntersectingNode.getTextRange().intersects(textRange)) { + initialIntersectingNode.accept(visitor); + } + + return visitor.getIntersectingNodes(); + } + + + public static void addToPages(TextEntity entity) { + + Set pages = entity.getDeepestFullyContainingNode().getPages(entity.getTextRange()); + entity.getPages().addAll(pages); + pages.forEach(page -> page.getEntities().add(entity)); + } + + + public static void addEntityToNodeEntitySets(TextEntity entity) { + + entity.getIntersectingNodes().forEach(node -> node.getEntities().add(entity)); + } + + + public static boolean allEntitiesIntersectAndHaveSameTypes(List entitiesToMerge) { + + if (entitiesToMerge.isEmpty()) { + return true; + } + TextEntity previousEntity = entitiesToMerge.get(0); + for (TextEntity textEntity : entitiesToMerge.subList(1, entitiesToMerge.size())) { + boolean typeMatches = textEntity.type().equals(previousEntity.type()); + boolean entityTypeMatches = textEntity.getEntityType().equals(previousEntity.getEntityType()); + boolean intersects = textEntity.intersects(previousEntity); + if (!typeMatches || !entityTypeMatches || !intersects) { + return false; + } + } + return true; + } + + + public static TextRange toLineAfterTextRange(TextBlock textBlock, TextRange textRange) { + + if (textBlock.getTextRange().end() == textRange.end()) { + return new TextRange(textRange.end(), textRange.end()); + } + + return new TextRange(textRange.end(), textBlock.getNextLinebreak(textRange.end())).trim(textBlock); + } + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/EntityFindingUtility.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/EntityFindingUtility.java index 62da7a73..61f94301 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/EntityFindingUtility.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/EntityFindingUtility.java @@ -5,6 +5,7 @@ import static java.util.stream.Collectors.groupingBy; import java.awt.geom.Rectangle2D; import java.util.Collection; +import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Locale; @@ -17,6 +18,7 @@ import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import com.iqser.red.service.redaction.v1.server.model.ClosestEntity; import com.iqser.red.service.redaction.v1.server.model.ManualEntity; import com.iqser.red.service.redaction.v1.server.model.RectangleWithPage; import com.iqser.red.service.redaction.v1.server.model.dictionary.SearchImplementation; @@ -58,28 +60,28 @@ public class EntityFindingUtility { return Optional.empty(); } - Optional optionalClosestEntity = possibleEntities.stream() + Optional optionalClosestEntity = possibleEntities.stream() .filter(entity -> pagesMatch(entity, manualEntity.getEntityPosition())) - .min(Comparator.comparingDouble(entity -> calculateMinDistance(manualEntity.getEntityPosition(), entity))); + .map(entity -> ClosestEntity.builder().distance(calculateMinDistance(manualEntity.getEntityPosition(), entity)).textEntity(entity).build()) + .min(Comparator.comparingDouble(ClosestEntity::getDistance)); if (optionalClosestEntity.isEmpty()) { log.warn("No Entity with value {} found on page {}", manualEntity.getValue(), manualEntity.getEntityPosition()); return Optional.empty(); } - TextEntity closestEntity = optionalClosestEntity.get(); - double distance = calculateMinDistance(manualEntity.getEntityPosition(), closestEntity); - if (distance > matchThreshold) { + ClosestEntity closestEntity = optionalClosestEntity.get(); + if (closestEntity.getDistance() > matchThreshold) { log.warn("For entity {} on page {} with positions {} distance to closest found entity is {} and therefore higher than the threshold of {}", manualEntity.getValue(), manualEntity.getEntityPosition().get(0).pageNumber(), manualEntity.getEntityPosition().stream().map(RectangleWithPage::rectangle2D).toList(), - distance, + closestEntity.getDistance(), matchThreshold); return Optional.empty(); } - return Optional.of(closestEntity); + return Optional.of(closestEntity.getTextEntity()); } @@ -97,7 +99,7 @@ public class EntityFindingUtility { } - private double calculateMinDistance(List originalPositions, TextEntity entity) { + public static double calculateMinDistance(List originalPositions, TextEntity entity) { if (originalPositions.size() != countRectangles(entity)) { return Double.MAX_VALUE; @@ -115,7 +117,7 @@ public class EntityFindingUtility { } - private double calculateMinDistancePerRectangle(TextEntity entity, int pageNumber, Rectangle2D originalRectangle) { + private static double calculateMinDistancePerRectangle(TextEntity entity, int pageNumber, Rectangle2D originalRectangle) { return entity.getPositionsOnPagePerPage() .stream() @@ -128,7 +130,7 @@ public class EntityFindingUtility { } - public double calculateDistance(Rectangle2D rectangle1, Rectangle2D rectangle2) { + public static double calculateDistance(Rectangle2D rectangle1, Rectangle2D rectangle2) { // mirrored coordinates safe comparison double minX1 = Math.min(rectangle1.getMinX(), rectangle1.getMaxX()); @@ -163,7 +165,10 @@ public class EntityFindingUtility { return searchImplementation.getBoundaries(node.getTextBlock(), node.getTextRange()) .stream() - .map(boundary -> entityCreationService.forceByTextRange(boundary, "temp", EntityType.ENTITY, node)) + .map(boundary -> entityCreationService.byTextRangeWithEngine(boundary, "temp", EntityType.ENTITY, node, Collections.emptySet())) + .filter(Optional::isPresent) + .map(Optional::get) + .distinct() .collect(groupingBy(entity -> entity.getValue().toLowerCase(Locale.ROOT))); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/IntersectingNodeVisitor.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/IntersectingNodeVisitor.java new file mode 100644 index 00000000..6b5ac081 --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/IntersectingNodeVisitor.java @@ -0,0 +1,31 @@ +package com.iqser.red.service.redaction.v1.server.service.document; + +import java.util.HashSet; +import java.util.Set; + +import com.iqser.red.service.redaction.v1.server.model.document.TextRange; +import com.iqser.red.service.redaction.v1.server.model.document.nodes.SemanticNode; + +import lombok.Getter; + +public class IntersectingNodeVisitor implements NodeVisitor { + + @Getter + private Set intersectingNodes; + private final TextRange textRange; + + public IntersectingNodeVisitor(TextRange textRange) { + + this.textRange = textRange; + this.intersectingNodes = new HashSet<>(); + } + + @Override + public void visit(SemanticNode node) { + + if (node.getTextRange().intersects(textRange)) { + intersectingNodes.add(node); + } + } + +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/ManualEntityCreationService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/ManualEntityCreationService.java index 74c8ec89..6b0379f1 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/ManualEntityCreationService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/ManualEntityCreationService.java @@ -2,6 +2,7 @@ package com.iqser.red.service.redaction.v1.server.service.document; import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -46,17 +47,24 @@ public class ManualEntityCreationService { } - public List createRedactionEntitiesIfFoundAndReturnNotFoundEntries(ManualRedactions manualRedactions, - SemanticNode node, - String dossierTemplateId) { + public List createRedactionEntitiesIfFoundAndReturnNotFoundEntries(ManualRedactions manualRedactions, SemanticNode node, String dossierTemplateId) { Set idRemovals = manualRedactions.getIdsToRemove(); - List manualEntities = manualRedactions.getEntriesToAdd().stream() - .filter(manualRedactionEntry -> !(idRemovals.stream().map(BaseAnnotation::getAnnotationId).toList().contains(manualRedactionEntry.getAnnotationId()) && - manualRedactionEntry.getRequestDate().isBefore(idRemovals.stream().filter(idRemoval -> idRemoval.getAnnotationId().equals(manualRedactionEntry.getAnnotationId())).findFirst().get().getRequestDate()))) + List manualEntities = manualRedactions.getEntriesToAdd() + .stream() + .filter(manualRedactionEntry -> !(idRemovals.stream() + .map(BaseAnnotation::getAnnotationId) + .toList() + .contains(manualRedactionEntry.getAnnotationId()) && manualRedactionEntry.getRequestDate() + .isBefore(idRemovals.stream() + .filter(idRemoval -> idRemoval.getAnnotationId().equals(manualRedactionEntry.getAnnotationId())) + .findFirst() + .get() + .getRequestDate()))) .filter(manualRedactionEntry -> !(manualRedactionEntry.isAddToDictionary() || manualRedactionEntry.isAddToDossierDictionary())) .map(manualRedactionEntry -> ManualEntity.fromManualRedactionEntry(manualRedactionEntry, - dictionaryService.isHint(manualRedactionEntry.getType(), dossierTemplateId))).peek(manualEntity -> { + dictionaryService.isHint(manualRedactionEntry.getType(), dossierTemplateId))) + .peek(manualEntity -> { if (manualEntity.getEntityType().equals(EntityType.HINT)) { manualEntity.skip("MAN.5.1", "manual hint is skipped by default"); } else { @@ -75,12 +83,12 @@ public class ManualEntityCreationService { List notFoundManualEntities = new LinkedList<>(); for (ManualEntity manualEntity : manualEntities) { - Optional optionalRedactionEntity = entityFindingUtility.findClosestEntityAndReturnEmptyIfNotFound(manualEntity, tempEntitiesByValue, MATCH_THRESHOLD); - if (optionalRedactionEntity.isEmpty()) { + Optional optionalClosestEntity = entityFindingUtility.findClosestEntityAndReturnEmptyIfNotFound(manualEntity, tempEntitiesByValue, MATCH_THRESHOLD); + if (optionalClosestEntity.isEmpty()) { notFoundManualEntities.add(manualEntity); continue; } - createCorrectEntity(manualEntity, node, optionalRedactionEntity.get().getTextRange()); + createCorrectEntity(manualEntity, optionalClosestEntity.get()); } tempEntitiesByValue.values().stream().flatMap(Collection::stream).forEach(TextEntity::removeFromGraph); return notFoundManualEntities; @@ -90,25 +98,29 @@ public class ManualEntityCreationService { /** * Deletes the temp Entity and creates a RedactionEntity with correct values, based on the given parameters. * - * @param manualEntity The entity identifier for the RedactionEntity. - * @param node The SemanticNode associated with the RedactionEntity. - * @param closestTextRange The closest Boundary to the RedactionEntity. + * @param manualEntity The entity identifier for the RedactionEntity. + * @param closestEntity The closest Boundary to the RedactionEntity. */ - private void createCorrectEntity(ManualEntity manualEntity, SemanticNode node, TextRange closestTextRange) { + private void createCorrectEntity(ManualEntity manualEntity, TextEntity closestEntity) { - TextEntity correctEntity = entityCreationService.forceByTextRange(closestTextRange, manualEntity.getType(), manualEntity.getEntityType(), node); + TextEntity correctEntity = TextEntity.initialEntityNode(closestEntity.getTextRange(), manualEntity.type(), manualEntity.getEntityType(), manualEntity.getId()); + + correctEntity.setDeepestFullyContainingNode(closestEntity.getDeepestFullyContainingNode()); + correctEntity.setIntersectingNodes(new ArrayList<>(closestEntity.getIntersectingNodes())); + correctEntity.setDuplicateTextRanges(new ArrayList<>(closestEntity.getDuplicateTextRanges())); + correctEntity.setPages(new HashSet<>(closestEntity.getPages())); + + correctEntity.setValue(closestEntity.getValue()); + correctEntity.setTextAfter(closestEntity.getTextAfter()); + correctEntity.setTextBefore(closestEntity.getTextBefore()); + + correctEntity.getIntersectingNodes().forEach(n -> n.getEntities().add(correctEntity)); + correctEntity.getPages().forEach(page -> page.getEntities().add(correctEntity)); correctEntity.addMatchedRules(manualEntity.getMatchedRuleList()); correctEntity.setDictionaryEntry(manualEntity.isDictionaryEntry()); correctEntity.setDossierDictionaryEntry(manualEntity.isDossierDictionaryEntry()); correctEntity.getManualOverwrite().addChanges(manualEntity.getManualOverwrite().getManualChangeLog()); - - // AnnotationIds must match the IDs in the add requests, or comments break. Maybe think about migrating IDs on the fly! - List redactionPositionsWithIdOfManualOnPage = new ArrayList<>(correctEntity.getPositionsOnPagePerPage().size()); - for (PositionOnPage positionOnPage : correctEntity.getPositionsOnPagePerPage()) { - redactionPositionsWithIdOfManualOnPage.add(new PositionOnPage(manualEntity.getId(), positionOnPage.getPage(), positionOnPage.getRectanglePerLine())); - } - correctEntity.setPositionsOnPagePerPage(redactionPositionsWithIdOfManualOnPage); } } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/ManualRedactionEntryService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/ManualRedactionEntryService.java index 985d58b1..efdc7d24 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/ManualRedactionEntryService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/ManualRedactionEntryService.java @@ -10,8 +10,8 @@ import org.springframework.stereotype.Service; import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeRequest; import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.ManualRedactions; import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.BaseAnnotation; -import com.iqser.red.service.redaction.v1.server.model.document.nodes.Document; import com.iqser.red.service.redaction.v1.server.model.ManualEntity; +import com.iqser.red.service.redaction.v1.server.model.document.nodes.Document; import io.micrometer.observation.annotation.Observed; import lombok.RequiredArgsConstructor; diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/NodeVisitor.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/NodeVisitor.java new file mode 100644 index 00000000..a027c47a --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/document/NodeVisitor.java @@ -0,0 +1,8 @@ +package com.iqser.red.service.redaction.v1.server.service.document; + +import com.iqser.red.service.redaction.v1.server.model.document.nodes.SemanticNode; + +public interface NodeVisitor { + + void visit(SemanticNode node); +} diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/drools/EntityDroolsExecutionService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/drools/EntityDroolsExecutionService.java index 17f809f0..88268f3a 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/drools/EntityDroolsExecutionService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/service/drools/EntityDroolsExecutionService.java @@ -42,7 +42,7 @@ public class EntityDroolsExecutionService { EntityEnrichmentService entityEnrichmentService; ObservationRegistry observationRegistry; - + ManualChangesApplicationService manualChangesApplicationService; RedactionServiceSettings settings; @@ -73,7 +73,6 @@ public class EntityDroolsExecutionService { KieSession kieSession = kieContainer.newKieSession(); EntityCreationService entityCreationService = new EntityCreationService(entityEnrichmentService, kieSession); - ManualChangesApplicationService manualChangesApplicationService = new ManualChangesApplicationService(entityCreationService); kieSession.setGlobal("document", document); kieSession.setGlobal("entityCreationService", entityCreationService); @@ -88,7 +87,7 @@ public class EntityDroolsExecutionService { fileAttributes.stream().filter(f -> f.getValue() != null).forEach(kieSession::insert); if (manualRedactions != null) { - manualRedactions.getResizeRedactions().stream().filter(manualResizeRedaction -> !manualResizeRedaction.getUpdateDictionary()).forEach(kieSession::insert); + manualRedactions.getResizeRedactions().forEach(kieSession::insert); manualRedactions.getRecategorizations().forEach(kieSession::insert); manualRedactions.getEntriesToAdd().forEach(kieSession::insert); manualRedactions.getForceRedactions().forEach(kieSession::insert); 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 a7f3237b..e10a911b 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 @@ -293,7 +293,7 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { @Test public void titleExtraction() throws IOException { - AnalyzeRequest request = uploadFileToStorage("files/migration/def8f960580f088b975ba806dfae1f87.ORIGIN.pdf"); + AnalyzeRequest request = uploadFileToStorage("files/Metolachlor/S-Metolachlor_RAR_01_Volume_1_2018-09-06.pdf"); System.out.println("Start Full integration test"); analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request); System.out.println("Finished structure analysis"); @@ -1331,6 +1331,36 @@ public class RedactionIntegrationTest extends AbstractRedactionIntegrationTest { assertEquals(entityLog.getEntityLogEntry().stream().filter(entityLogEntry -> entityLogEntry.getId().equals(manualAddId2)).findFirst().get().getState(), EntryState.REMOVED); } + @Test + @SneakyThrows + public void testResizeWithUpdateDictionaryTrue() { + + String pdfFile = "files/new/crafted document.pdf"; + + AnalyzeRequest request = uploadFileToStorage(pdfFile); + analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request); + analyzeService.analyze(request); + + var entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID); + var david = entityLog.getEntityLogEntry().stream().filter(e -> e.getValue().equals("David")).findFirst().get(); + + request.setManualRedactions(ManualRedactions.builder() + .resizeRedactions(Set.of(ManualResizeRedaction.builder() + .updateDictionary(true) + .annotationId(david.getId()) + .requestDate(OffsetDateTime.now()) + .value("David Ksenia") + .positions(List.of(Rectangle.builder().topLeftX(56.8f).topLeftY(293.564f).width(65.592f).height(15.408f).page(1).build())) + .addToAllDossiers(false) + .build())) + .build()); + analyzeService.reanalyze(request); + entityLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID); + var resizedEntity = entityLog.getEntityLogEntry().stream().filter(e -> e.getId().equals(david.getId())).findFirst().get(); + assertEquals(resizedEntity.getState(), EntryState.APPLIED); + assertEquals(resizedEntity.getValue(), "David Ksenia"); + } + private IdRemoval getIdRemoval(String id) { diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/entity/TextEntityTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/entity/TextEntityTest.java index c6e30b9a..95174da1 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/entity/TextEntityTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/entity/TextEntityTest.java @@ -5,16 +5,18 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; -import com.iqser.red.service.redaction.v1.server.model.document.TextRange; +import com.iqser.red.service.redaction.v1.server.model.ManualEntity; import com.iqser.red.service.redaction.v1.server.model.document.entity.EntityType; -import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity; public class TextEntityTest { @Test public void testMatchedRule() { - TextEntity entity = TextEntity.initialEntityNode(new TextRange(1, 100), "PII", EntityType.ENTITY); + ManualEntity entity = ManualEntity.builder() + .type("PII") + .entityType(EntityType.ENTITY) + .build(); entity.skip("CBI.1.0", ""); entity.skip("CBI.2.0", ""); entity.skip("CBI.3.0", ""); @@ -29,7 +31,10 @@ public class TextEntityTest { @Test public void testMatchedRuleWithNonsense() { - TextEntity entity = TextEntity.initialEntityNode(new TextRange(1, 100), "PII", EntityType.ENTITY); + ManualEntity entity = ManualEntity.builder() + .type("PII") + .entityType(EntityType.ENTITY) + .build(); assertThrows(IllegalArgumentException.class, () -> { entity.skip("", ""); }); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/DocumentIEntityInsertionIntegrationTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/DocumentIEntityInsertionIntegrationTest.java index 38a52644..739eb54c 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/DocumentIEntityInsertionIntegrationTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/DocumentIEntityInsertionIntegrationTest.java @@ -49,6 +49,21 @@ public class DocumentIEntityInsertionIntegrationTest extends BuildDocumentIntegr entityCreationService = new EntityCreationService(entityEnrichmentService, kieSession); } + @Test + public void assertEntitiesAreDuplicatedWithTheirTableCell() { + + Document document = buildGraph("files/Minimal Examples/Meto1_Page22.pdf"); + List entities = entityCreationService.byString("Surface Water", "test", EntityType.ENTITY, document).toList(); + assertEquals(3, entities.size()); + assertEquals(1, entities.stream().distinct().count()); + assertEquals(2, entities.get(0).getDuplicateTextRanges().size()); + + var node = entities.get(0).getDeepestFullyContainingNode(); + + assertTrue(node.getTextRange().contains(entities.get(0).getTextRange())); + assertTrue(entities.get(0).getDuplicateTextRanges().stream().allMatch(tr -> node.getTextRange().contains(tr))); + } + @Test public void assertCollectAllEntitiesWorks() { @@ -82,7 +97,7 @@ public class DocumentIEntityInsertionIntegrationTest extends BuildDocumentIntegr assert start != -1; TextRange textRange = new TextRange(start, start + searchTerm.length()); - TextEntity textEntity = TextEntity.initialEntityNode(textRange, "123", EntityType.ENTITY); + TextEntity textEntity = TextEntity.initialEntityNode(textRange, "123", EntityType.ENTITY, document); entityCreationService.addEntityToGraph(textEntity, document); return textEntity; } @@ -241,7 +256,7 @@ public class DocumentIEntityInsertionIntegrationTest extends BuildDocumentIntegr assert start != -1; TextRange textRange = new TextRange(start, start + searchTerm.length()); - TextEntity textEntity = TextEntity.initialEntityNode(textRange, "123", EntityType.ENTITY); + TextEntity textEntity = TextEntity.initialEntityNode(textRange, "123", EntityType.ENTITY, document); entityCreationService.addEntityToGraph(textEntity, document); assertEquals("2.6.1 Summary of ", textEntity.getTextBefore()); @@ -299,7 +314,7 @@ public class DocumentIEntityInsertionIntegrationTest extends BuildDocumentIntegr assert start != -1; TextRange textRange = new TextRange(start, start + searchTerm.length()); - TextEntity textEntity = TextEntity.initialEntityNode(textRange, "123", EntityType.ENTITY); + TextEntity textEntity = TextEntity.initialEntityNode(textRange, "123", EntityType.ENTITY, document); entityCreationService.addEntityToGraph(textEntity, document); Page pageNode = document.getPages().stream().filter(page -> page.getNumber() == pageNumber).findFirst().orElseThrow(); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/DocumentPerformanceIntegrationTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/DocumentPerformanceIntegrationTest.java index 0bd06cfb..e3e6a69c 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/DocumentPerformanceIntegrationTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/document/graph/DocumentPerformanceIntegrationTest.java @@ -1,7 +1,7 @@ package com.iqser.red.service.redaction.v1.server.document.graph; import static com.iqser.red.service.redaction.v1.server.utils.SeparatorUtils.boundaryIsSurroundedBySeparators; -import static org.mockito.Mockito.doThrow; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.when; import java.awt.Color; @@ -34,6 +34,10 @@ import com.iqser.red.service.persistence.service.v1.api.shared.model.RuleFileTyp import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.ManualRedactions; 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.redaction.v1.server.model.NerEntities; +import com.iqser.red.service.redaction.v1.server.model.dictionary.Dictionary; +import com.iqser.red.service.redaction.v1.server.model.dictionary.DictionaryModel; +import com.iqser.red.service.redaction.v1.server.model.dictionary.SearchImplementation; import com.iqser.red.service.redaction.v1.server.model.document.entity.EntityType; import com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity; import com.iqser.red.service.redaction.v1.server.model.document.nodes.Document; @@ -41,16 +45,11 @@ import com.iqser.red.service.redaction.v1.server.model.document.nodes.Page; import com.iqser.red.service.redaction.v1.server.model.document.nodes.Section; import com.iqser.red.service.redaction.v1.server.model.document.nodes.SemanticNode; import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlock; +import com.iqser.red.service.redaction.v1.server.service.DictionaryService; import com.iqser.red.service.redaction.v1.server.service.document.EntityCreationService; import com.iqser.red.service.redaction.v1.server.service.document.EntityEnrichmentService; -import com.iqser.red.service.redaction.v1.server.utils.ExceptionProvider; -import com.iqser.red.service.redaction.v1.server.utils.PdfVisualisationUtility; -import com.iqser.red.service.redaction.v1.server.model.NerEntities; -import com.iqser.red.service.redaction.v1.server.model.dictionary.Dictionary; -import com.iqser.red.service.redaction.v1.server.model.dictionary.DictionaryModel; -import com.iqser.red.service.redaction.v1.server.model.dictionary.SearchImplementation; -import com.iqser.red.service.redaction.v1.server.service.DictionaryService; import com.iqser.red.service.redaction.v1.server.service.drools.EntityDroolsExecutionService; +import com.iqser.red.service.redaction.v1.server.utils.PdfVisualisationUtility; import com.knecon.fforesight.tenantcommons.TenantContext; import lombok.SneakyThrows; @@ -245,8 +244,12 @@ public class DocumentPerformanceIntegrationTest extends BuildDocumentIntegration System.out.printf("%d Searches took %s s, average %.2f ms\n", numberOfRuns, ((float) totalSearchTime / 1000), totalSearchTime / numberOfRuns); System.out.printf("%d Insertions took %s s, average %.2f ms\n", numberOfRuns, ((float) totalInsertTime / 1000), totalInsertTime / numberOfRuns); System.out.printf("Found %d entities and saved %d\n", foundEntities.size(), document.getEntities().size()); - assert document.getEntities().size() == foundEntities.size(); - + for (TextEntity entity : document.getEntities()) { + var foundEntity = foundEntities.stream().filter(f -> f.getId().equals(entity.getId())).findFirst().get(); + assertTrue(foundEntity.getTextRange().equals(entity.getTextRange()) || foundEntity.getDuplicateTextRanges().contains(entity.getTextRange())); + } + assert document.getEntities().stream().mapToInt(e -> e.getDuplicateTextRanges().size() + 1).sum() == foundEntities.size(); + assert foundEntities.stream().map(TextEntity::getId).distinct().count() == document.getEntities().size(); drawAllEntities(filename, document); } @@ -300,7 +303,7 @@ public class DocumentPerformanceIntegrationTest extends BuildDocumentIntegration searchImplementation.getBoundaries(textBlock, textBlock.getTextRange()) .stream() .filter(boundary -> boundaryIsSurroundedBySeparators(textBlock, boundary)) - .map(bounds -> TextEntity.initialEntityNode(bounds, type, entityType)) + .map(bounds -> TextEntity.initialEntityNode(bounds, type, entityType, document)) .forEach(foundEntities::add); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/manualchanges/ManualChangesEnd2EndTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/manualchanges/ManualChangesEnd2EndTest.java index f642b81a..128849f7 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/manualchanges/ManualChangesEnd2EndTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/manualchanges/ManualChangesEnd2EndTest.java @@ -4,11 +4,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.mockito.Mockito.when; +import static org.wildfly.common.Assert.assertTrue; import java.awt.geom.Rectangle2D; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.nio.file.Path; import java.nio.file.Paths; import java.time.OffsetDateTime; import java.util.Comparator; @@ -121,6 +123,7 @@ public class ManualChangesEnd2EndTest extends AbstractRedactionIntegrationTest { loadNerForTest(); when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(0L); when(dictionaryClient.getAllTypesForDossierTemplate(TEST_DOSSIER_TEMPLATE_ID, false)).thenReturn(getTypeResponse()); + when(dictionaryClient.getAllTypesForDossierTemplate(TEST_DOSSIER_TEMPLATE_ID, true)).thenReturn(getTypeResponse()); when(dictionaryClient.getVersion(TEST_DOSSIER_TEMPLATE_ID)).thenReturn(0L); when(dictionaryClient.getAllTypesForDossier(TEST_DOSSIER_ID, false)).thenReturn(List.of(Type.builder() @@ -147,7 +150,7 @@ public class ManualChangesEnd2EndTest extends AbstractRedactionIntegrationTest { String filePath = "files/new/crafted document.pdf"; AnalyzeRequest request = uploadFileToStorage(filePath); analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request); - AnalyzeResult result = analyzeService.analyze(request); + analyzeService.analyze(request); String testEntityValue1 = "Desiree"; String testEntityValue2 = "Melanie"; @@ -157,7 +160,7 @@ public class ManualChangesEnd2EndTest extends AbstractRedactionIntegrationTest { Document document = DocumentGraphMapper.toDocumentGraph(redactionStorageService.getDocumentData(TEST_DOSSIER_ID, TEST_FILE_ID)); String expandedEntityKeyword = "Lorem ipsum dolor sit amet, consectetur adipiscing elit Desiree et al sed do eiusmod tempor incididunt ut labore et dolore magna aliqua Melanie et al. Reference No 12345 Lorem ipsum."; - TextEntity expandedEntity = entityCreationService.byString(expandedEntityKeyword, "PII", EntityType.ENTITY, document).findFirst().get(); + entityCreationService.byString(expandedEntityKeyword, "PII", EntityType.ENTITY, document).findFirst().get(); String idToResize = redactionLog.getEntityLogEntry() .stream() @@ -165,24 +168,18 @@ public class ManualChangesEnd2EndTest extends AbstractRedactionIntegrationTest { .max(Comparator.comparingInt(EntityLogEntry::getStartOffset)) .get() .getId(); - List resizedPositions = expandedEntity.getPositionsOnPagePerPage() - .get(0) - .getRectanglePerLine() - .stream() - .map(rectangle2D -> toAnnotationRectangle(rectangle2D, 3)) - .toList(); - ManualResizeRedaction manualResizeRedaction = ManualResizeRedaction.builder() - .annotationId(idToResize) - .value(expandedEntityKeyword) - .positions(resizedPositions) - .requestDate(OffsetDateTime.now()) - .updateDictionary(false) - .build(); - manualResizeRedaction.setUpdateDictionary(false); ManualRedactions manualRedactions = new ManualRedactions(); - manualRedactions.getResizeRedactions().add(manualResizeRedaction); + manualRedactions.getResizeRedactions().add(ManualResizeRedaction.builder() + .annotationId(idToResize) + .value(expandedEntityKeyword) + .positions(List.of(Rectangle.builder().topLeftX(56.8f).topLeftY(454.664f).height(15.408f).width(493.62f).page(3).build(), + Rectangle.builder().topLeftX(56.8f).topLeftY(440.864f).height(15.408f).width(396f).page(3).build())) + .addToAllDossiers(false) + .updateDictionary(false) + .requestDate(OffsetDateTime.now()) + .build()); request.setManualRedactions(manualRedactions); - AnalyzeResult reanalyzeResult = analyzeService.reanalyze(request); + analyzeService.reanalyze(request); redactionLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID); String annotatedFileName = Paths.get(filePath).getFileName().toString().replace(".pdf", "_annotated2.pdf"); @@ -225,7 +222,7 @@ public class ManualChangesEnd2EndTest extends AbstractRedactionIntegrationTest { .annotationId("675eba69b0c2917de55462c817adaa05") .fileId("fileId") .legalBasis("Something") - .build())); + .build())); ManualRedactionEntry manualRedactionEntry = new ManualRedactionEntry(); manualRedactionEntry.setAnnotationId(manualAddId); @@ -248,7 +245,7 @@ public class ManualChangesEnd2EndTest extends AbstractRedactionIntegrationTest { .annotationId("675eba69b0c2917de55462c817adaa05") .fileId("fileId") .legalBasis("Manual Legal Basis Change") - .requestDate(OffsetDateTime.now()) + .requestDate(OffsetDateTime.now()) .build()))); analyzeService.reanalyze(request); @@ -267,6 +264,45 @@ public class ManualChangesEnd2EndTest extends AbstractRedactionIntegrationTest { } + @Test + public void testManualRedactionIsDuplicatedWithTableCells() throws IOException { + + System.out.println("testManualRedaction"); + long start = System.currentTimeMillis(); + String pdfFile = "files/Minimal Examples/Meto1_Page22.pdf"; + + ManualRedactions manualRedactions = new ManualRedactions(); + + String manualAddId = UUID.randomUUID().toString(); + + ManualRedactionEntry manualRedactionEntry = new ManualRedactionEntry(); + manualRedactionEntry.setAnnotationId(manualAddId); + manualRedactionEntry.setFileId("fileId"); + manualRedactionEntry.setType("name"); + manualRedactionEntry.setValue("Surface Water"); + manualRedactionEntry.setReason("Dictionary Request"); + manualRedactionEntry.setPositions(List.of(Rectangle.builder().topLeftX(76.584f).topLeftY(506.56238f).width(57.4329f).height(9.065156f).page(1).build())); + manualRedactions.setEntriesToAdd(Set.of(manualRedactionEntry)); + AnalyzeRequest request = uploadFileToStorage(pdfFile); + request.setManualRedactions(manualRedactions); + analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request); + AnalyzeResult result = analyzeService.analyze(request); + + var redactionLog = redactionStorageService.getEntityLog(TEST_DOSSIER_ID, TEST_FILE_ID); + AnnotateResponse annotateResponse = annotationService.annotate(AnnotateRequest.builder().dossierId(TEST_DOSSIER_ID).fileId(TEST_FILE_ID).build()); + + try (FileOutputStream fileOutputStream = new FileOutputStream(OsUtils.getTemporaryDirectory() + "/" + Path.of(pdfFile).getFileName() + "MANUAL_REDACTION_TEST.pdf")) { + fileOutputStream.write(annotateResponse.getDocument()); + } + long end = System.currentTimeMillis(); + var optionalEntry = redactionLog.getEntityLogEntry().stream().filter(entityLogEntry -> entityLogEntry.getId().equals(manualAddId)).findAny(); + assertTrue(optionalEntry.isPresent()); + assertEquals(2, optionalEntry.get().getContainingNodeId().size()); // 2 is the depth of the table instead of the table cell + System.out.println("duration: " + (end - start)); + System.out.println("numberOfPages: " + result.getNumberOfPages()); + } + + @Test public void testReCategorizeToVertebrateChangesCbiAuthor() { @@ -296,7 +332,7 @@ public class ManualChangesEnd2EndTest extends AbstractRedactionIntegrationTest { ManualRecategorization recategorization = ManualRecategorization.builder() .requestDate(OffsetDateTime.now()) - .type("vertebrate") + .type("vertebrate") .annotationId(oxfordUniversityPress.getId()) .fileId(TEST_FILE_ID) .build(); @@ -345,18 +381,19 @@ public class ManualChangesEnd2EndTest extends AbstractRedactionIntegrationTest { manualRedactions.setEntriesToAdd(Set.of(ManualRedactionEntry.builder() .annotationId(annotationId) .requestDate(OffsetDateTime.now()) + .type("manual") .value("Expand to Hint Clarissa’s Donut ← not added to Dict, should be not annotated Simpson's Tower ← added to Authors-Dict, should be annotated") .positions(List.of(// new Rectangle(new Point(56.8f, 496.27f), 61.25f, 12.83f, 2), // new Rectangle(new Point(56.8f, 482.26f), 303.804f, 15.408f, 2), // new Rectangle(new Point(56.8f, 468.464f), 314.496f, 15.408f, 2))) // - .build())); + .build())); ManualResizeRedaction manualResizeRedaction = ManualResizeRedaction.builder() .annotationId(annotationId) .requestDate(OffsetDateTime.now()) .value("Expand to Hint") .positions(List.of(new Rectangle(new Point(56.8f, 496.27f), 61.25f, 12.83f, 2))) - .updateDictionary(false) + .updateDictionary(false) .build(); manualRedactions.setResizeRedactions(Set.of(manualResizeRedaction)); request.setManualRedactions(manualRedactions); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/manualchanges/ManualChangesIntegrationTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/manualchanges/ManualChangesIntegrationTest.java index 4d59e8d0..860f4d1b 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/manualchanges/ManualChangesIntegrationTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/manualchanges/ManualChangesIntegrationTest.java @@ -7,12 +7,14 @@ import static org.wildfly.common.Assert.assertFalse; import java.awt.geom.Rectangle2D; import java.time.OffsetDateTime; import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; +import com.google.common.collect.Sets; import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.Rectangle; import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.IdRemoval; import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualForceRedaction; @@ -50,7 +52,7 @@ public class ManualChangesIntegrationTest extends RulesIntegrationTest { assertEquals(biggerEntity.getTextRange(), entity.getTextRange()); assertEquals(biggerEntity.getDeepestFullyContainingNode(), entity.getDeepestFullyContainingNode()); - assertEquals(biggerEntity.getIntersectingNodes(), entity.getIntersectingNodes()); + assertTrue(Sets.difference(new HashSet<>(biggerEntity.getIntersectingNodes()), new HashSet<>(entity.getIntersectingNodes())).isEmpty()); assertEquals(biggerEntity.getPages(), entity.getPages()); assertEquals(biggerEntity.getValue(), entity.getValue()); assertEquals(initialId, entity.getPositionsOnPagePerPage().get(0).getId()); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/manualchanges/ManualChangesUnitTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/manualchanges/ManualChangesUnitTest.java index c681b6fb..85d8311c 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/manualchanges/ManualChangesUnitTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/manualchanges/ManualChangesUnitTest.java @@ -119,7 +119,7 @@ public class ManualChangesUnitTest extends BuildDocumentIntegrationTest { entity.getManualOverwrite().addChange(imageRecategorizationRequest); assertTrue(entity.getManualOverwrite().getRecategorized().isPresent()); assertTrue(entity.getManualOverwrite().getRecategorized().get()); - assertEquals("type", entity.getManualOverwrite().getType().orElse(entity.getType())); + assertEquals("type", entity.getManualOverwrite().getType().orElse(entity.type())); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/adapter/NerEntitiesAdapterTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/adapter/NerEntitiesAdapterTest.java index c5ae5d41..c507b417 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/adapter/NerEntitiesAdapterTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/redaction/adapter/NerEntitiesAdapterTest.java @@ -167,14 +167,14 @@ class NerEntitiesAdapterTest extends BuildDocumentIntegrationTest { private List getPositionsFromEntityOfType(String type, List entities) { - return getPositionsFromEntities(entities.stream().filter(e -> e.getType().equals(type))); + return getPositionsFromEntities(entities.stream().filter(e -> e.type().equals(type))); } private List getPositionsFromEntityNotOfType(List types, List entities) { - return getPositionsFromEntities(entities.stream().filter(e -> types.stream().noneMatch(type -> e.getType().equals(type)))); + return getPositionsFromEntities(entities.stream().filter(e -> types.stream().noneMatch(type -> e.type().equals(type)))); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/rules/RulesIntegrationTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/rules/RulesIntegrationTest.java index 8ce45581..b6cbf355 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/rules/RulesIntegrationTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/rules/RulesIntegrationTest.java @@ -28,6 +28,8 @@ public class RulesIntegrationTest extends BuildDocumentIntegrationTest { @Autowired protected EntityEnrichmentService entityEnrichmentService; + @Autowired + protected ManualChangesApplicationService manualChangesApplicationService; protected EntityCreationService entityCreationService; protected KieSession kieSession; @@ -72,7 +74,6 @@ public class RulesIntegrationTest extends BuildDocumentIntegrationTest { kieSession = kieContainer.newKieSession(); entityCreationService = new EntityCreationService(entityEnrichmentService, kieSession); - ManualChangesApplicationService manualChangesApplicationService = new ManualChangesApplicationService(entityCreationService); kieSession.setGlobal("manualChangesApplicationService", manualChangesApplicationService); kieSession.setGlobal("entityCreationService", entityCreationService); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/service/document/UnprocessedChangesServiceTest.java b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/service/document/UnprocessedChangesServiceTest.java index b787d489..8eeba13c 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/service/document/UnprocessedChangesServiceTest.java +++ b/redaction-service-v1/redaction-service-server-v1/src/test/java/com/iqser/red/service/redaction/v1/server/service/document/UnprocessedChangesServiceTest.java @@ -174,7 +174,7 @@ public class UnprocessedChangesServiceTest extends AbstractRedactionIntegrationT analyzeService.reanalyze(request); - List positions = List.of(Rectangle.builder().topLeftX(286.1072f).topLeftY(266.18945f).width(98.7528f).height(10.048125f).build()); + List positions = List.of(Rectangle.builder().topLeftX(286.1072f).topLeftY(266.18945f).width(98.7528f).height(10.048125f).page(1).build()); ManualResizeRedaction manualResizeRedaction = prepareManualSizeRedaction(aoelId, positions, "was above the AOEL"); request.setManualRedactions(ManualRedactions.builder().resizeRedactions(Set.of(manualResizeRedaction)).build()); @@ -221,7 +221,7 @@ public class UnprocessedChangesServiceTest extends AbstractRedactionIntegrationT analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request); analyzeService.analyze(request); - List positions = List.of(Rectangle.builder().topLeftX(369.024f).topLeftY(240.8695f).width(29.32224f).height(10.048125f).build()); + List positions = List.of(Rectangle.builder().topLeftX(369.024f).topLeftY(240.8695f).width(29.32224f).height(10.048125f).page(1).build()); List positions2 = List.of(Rectangle.builder().topLeftX(129.86f).topLeftY(505.7295f).width(80.144233125f).height(10.048125f).page(1).build()); List positions3 = List.of(Rectangle.builder().topLeftX(70.944f).topLeftY(291.5095f).width(107.01071999999994f).height(10.048125f).page(1).build()); ManualResizeRedaction manualResizeRedaction = prepareManualSizeRedaction(aoelId, positions, "AOEL"); @@ -285,7 +285,7 @@ public class UnprocessedChangesServiceTest extends AbstractRedactionIntegrationT analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request); analyzeService.analyze(request); - List positions = List.of(Rectangle.builder().topLeftX(369.024f).topLeftY(240.8695f).width(29.32224f).height(10.048125f).build()); + List positions = List.of(Rectangle.builder().topLeftX(369.024f).topLeftY(240.8695f).width(29.32224f).height(10.048125f).page(1).build()); ManualResizeRedaction manualResizeRedaction = prepareManualSizeRedaction(aoelId, positions, "AOEL"); request.setManualRedactions(ManualRedactions.builder().resizeRedactions(Set.of(manualResizeRedaction)).build()); @@ -323,7 +323,7 @@ public class UnprocessedChangesServiceTest extends AbstractRedactionIntegrationT analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request); analyzeService.analyze(request); - List positions = List.of(Rectangle.builder().topLeftX(149.94624f).topLeftY(417.1695f).width(37.23792f).height(10.048125f).build()); + List positions = List.of(Rectangle.builder().topLeftX(149.94624f).topLeftY(417.1695f).width(37.23792f).height(10.048125f).page(1).build()); ManualResizeRedaction manualResizeRedaction = prepareManualSizeRedaction(aoelId, positions, "AAOEL"); request.setManualRedactions(ManualRedactions.builder().resizeRedactions(Set.of(manualResizeRedaction)).build()); @@ -361,7 +361,7 @@ public class UnprocessedChangesServiceTest extends AbstractRedactionIntegrationT analyzeDocumentStructure(LayoutParsingType.REDACT_MANAGER, request); analyzeService.analyze(request); - List positions = List.of(Rectangle.builder().topLeftX(293.024f).topLeftY(240.8695f).width(29.32224f).height(10.048125f).build()); + List positions = List.of(Rectangle.builder().topLeftX(293.024f).topLeftY(240.8695f).width(29.32224f).height(10.048125f).page(1).build()); ManualResizeRedaction manualResizeRedaction = prepareManualSizeRedaction(aoelId, positions, "Does Not Exist"); request.setManualRedactions(ManualRedactions.builder().resizeRedactions(Set.of(manualResizeRedaction)).build()); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/acceptance_rules.drl b/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/acceptance_rules.drl index 857f3060..7c19f205 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/acceptance_rules.drl +++ b/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/acceptance_rules.drl @@ -84,7 +84,7 @@ rule "SYN.1.0: Recommend CTL/BL laboratory that start with BL or CTL" rule "CBI.0.0: Redact CBI Authors (non vertebrate Study)" when not FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $entity: TextEntity(type == "CBI_author", dictionaryEntry) + $entity: TextEntity(type() == "CBI_author", dictionaryEntry) then $entity.redact("CBI.0.0", "Author found", "Article 39(e)(3) of Regulation (EC) No 178/2002"); end @@ -92,7 +92,7 @@ rule "CBI.0.0: Redact CBI Authors (non vertebrate Study)" rule "CBI.0.1: Redact CBI Authors (vertebrate Study)" when FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $entity: TextEntity(type == "CBI_author", dictionaryEntry) + $entity: TextEntity(type() == "CBI_author", dictionaryEntry) then $entity.redact("CBI.0.1", "Author found", "Article 39(e)(2) of Regulation (EC) No 178/2002"); end @@ -102,7 +102,7 @@ rule "CBI.0.1: Redact CBI Authors (vertebrate Study)" rule "CBI.1.0: Do not redact CBI Address (non vertebrate Study)" when not FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $entity: TextEntity(type == "CBI_address", dictionaryEntry) + $entity: TextEntity(type() == "CBI_address", dictionaryEntry) then $entity.skip("CBI.1.0", "Address found for Non Vertebrate Study"); end @@ -110,7 +110,7 @@ rule "CBI.1.0: Do not redact CBI Address (non vertebrate Study)" rule "CBI.1.1: Redact CBI Address (vertebrate Study)" when FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $entity: TextEntity(type == "CBI_address", dictionaryEntry) + $entity: TextEntity(type() == "CBI_address", dictionaryEntry) then $entity.redact("CBI.1.1", "Address found", "Article 39(e)(2) of Regulation (EC) No 178/2002"); end @@ -119,7 +119,7 @@ rule "CBI.1.1: Redact CBI Address (vertebrate Study)" // Rule unit: CBI.2 rule "CBI.2.0: Do not redact genitive CBI Author" when - $entity: TextEntity(type == "CBI_author", anyMatch(textAfter, "['’’'ʼˈ´`‘′ʻ’']s")) + $entity: TextEntity(type() == "CBI_author", anyMatch(textAfter, "['’’'ʼˈ´`‘′ʻ’']s")) then entityCreationService.byTextRange($entity.getTextRange(), "CBI_author", EntityType.FALSE_POSITIVE, document) .ifPresent(falsePositive -> falsePositive.skip("CBI.2.0", "Genitive Author found")); @@ -148,7 +148,7 @@ rule "CBI.7.1: Do not redact Names and Addresses if published information found $table: Table(hasEntitiesOfType("published_information"), hasEntitiesOfType("CBI_author") || hasEntitiesOfType("CBI_address")) $cellsWithPublishedInformation: TableCell() from $table.streamTableCellsWhichContainType("published_information").toList() $tableCell: TableCell(row == $cellsWithPublishedInformation.row) from $table.streamTableCells().toList() - $authorOrAddress: TextEntity(type == "CBI_author" || type == "CBI_address", active()) from $tableCell.getEntities() + $authorOrAddress: TextEntity(type() == "CBI_author" || type() == "CBI_address", active()) from $tableCell.getEntities() then $authorOrAddress.skipWithReferences("CBI.7.1", "Published Information found in row", $table.getEntitiesOfTypeInSameRow("published_information", $authorOrAddress)); end @@ -301,7 +301,7 @@ rule "CBI.20.1: Redact between \"PERFORMING LABORATORY\" and \"LABORATORY PROJEC rule "PII.0.0: Redact all PII (non vertebrate study)" when not FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $pii: TextEntity(type == "PII", dictionaryEntry) + $pii: TextEntity(type() == "PII", dictionaryEntry) then $pii.redact("PII.0.0", "Personal Information found", "Article 39(e)(3) of Regulation (EC) No 178/2002"); end @@ -309,7 +309,7 @@ rule "PII.0.0: Redact all PII (non vertebrate study)" rule "PII.0.1: Redact all PII (vertebrate study)" when FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $pii: TextEntity(type == "PII", dictionaryEntry) + $pii: TextEntity(type() == "PII", dictionaryEntry) then $pii.redact("PII.0.1", "Personal Information found", "Article 39(e)(2) of Regulation (EC) No 178/2002"); end @@ -625,7 +625,7 @@ rule "PII.11.0: Redact On behalf of Sequani Ltd.:" rule "PII.12.0: Expand PII entities with salutation prefix" when not FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $entityToExpand: TextEntity(type == "PII", anyMatch(textBefore, "\\b(Mrs?|Ms|Miss|Sir|Madame?|Mme)\\s?\\.?\\s*")) + $entityToExpand: TextEntity(type() == "PII", anyMatch(textBefore, "\\b(Mrs?|Ms|Miss|Sir|Madame?|Mme)\\s?\\.?\\s*")) then entityCreationService.byPrefixExpansionRegex($entityToExpand, "\\b(Mrs?|Ms|Miss|Sir|Madame?|Mme)\\s?\\.?\\s*") .ifPresent(expandedEntity -> expandedEntity.apply("PII.12.0", "Expanded PII with salutation prefix", "Article 39(e)(3) of Regulation (EC) No 178/2002")); @@ -634,7 +634,7 @@ rule "PII.12.0: Expand PII entities with salutation prefix" rule "PII.12.1: Expand PII entities with salutation prefix" when FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $entityToExpand: TextEntity(type == "PII", anyMatch(textBefore, "\\b(Mrs?|Ms|Miss|Sir|Madame?|Mme)\\s?\\.?\\s*")) + $entityToExpand: TextEntity(type() == "PII", anyMatch(textBefore, "\\b(Mrs?|Ms|Miss|Sir|Madame?|Mme)\\s?\\.?\\s*")) then entityCreationService.byPrefixExpansionRegex($entityToExpand, "\\b(Mrs?|Ms|Miss|Sir|Madame?|Mme)\\s?\\.?\\s*") .ifPresent(expandedEntity -> expandedEntity.apply("PII.12.1", "Expanded PII with salutation prefix", "Article 39(e)(2) of Regulation (EC) No 178/2002")); @@ -693,7 +693,7 @@ rule "ETC.3.1: Redact logos (vertebrate study)" rule "ETC.5.0: Ignore dossier_redaction entries if confidentiality is not 'confidential'" when not FileAttribute(label == "Confidentiality", value == "confidential") - $dossierRedaction: TextEntity(type == "dossier_redaction") + $dossierRedaction: TextEntity(type() == "dossier_redaction") then $dossierRedaction.ignore("ETC.5.0", "Ignore dossier redactions, when not confidential"); $dossierRedaction.getIntersectingNodes().forEach(node -> update(node)); @@ -709,7 +709,7 @@ rule "AI.0.0: Add all NER Entities of type CBI_author" nerEntities: NerEntities(hasEntitiesOfType("CBI_author")) then nerEntities.streamEntitiesOfType("CBI_author") - .forEach(nerEntity -> entityCreationService.byNerEntity(nerEntity, EntityType.RECOMMENDATION, document)); + .forEach(nerEntity -> entityCreationService.optionalByNerEntity(nerEntity, EntityType.RECOMMENDATION, document)); end @@ -733,7 +733,7 @@ rule "MAN.0.0: Apply manual resize redaction" not ManualResizeRedaction(annotationId == $id, requestDate.isBefore($requestDate)) $entityToBeResized: TextEntity(matchesAnnotationId($id)) then - manualChangesApplicationService.resizeEntityAndReinsert($entityToBeResized, $resizeRedaction); + manualChangesApplicationService.resize($entityToBeResized, $resizeRedaction); retract($resizeRedaction); update($entityToBeResized); $entityToBeResized.getIntersectingNodes().forEach(node -> update(node)); @@ -811,13 +811,12 @@ rule "MAN.3.0: Apply entity recategorization" when $recategorization: ManualRecategorization($id: annotationId, $type: type, $requestDate: requestDate) not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate)) - $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type != $type) + $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type() != $type) then $entityToBeRecategorized.getIntersectingNodes().forEach(node -> update(node)); - manualChangesApplicationService.recategorize($entityToBeRecategorized, $recategorization); + $entityToBeRecategorized.getManualOverwrite().addChange($recategorization); + update($entityToBeRecategorized); retract($recategorization); - // Entity is copied and inserted, so the old entity needs to be retracted to avoid duplication. - retract($entityToBeRecategorized); end rule "MAN.3.1: Apply entity recategorization of same type" @@ -825,7 +824,7 @@ rule "MAN.3.1: Apply entity recategorization of same type" when $recategorization: ManualRecategorization($id: annotationId, $type: type, $requestDate: requestDate) not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate)) - $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type == $type) + $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type() == $type) then $entityToBeRecategorized.getManualOverwrite().addChange($recategorization); retract($recategorization); @@ -883,8 +882,8 @@ rule "MAN.4.1: Apply legal basis change" rule "X.0.0: Remove Entity contained by Entity of same type" salience 65 when - $larger: TextEntity($type: type, $entityType: entityType, active()) - $contained: TextEntity(containedBy($larger), type == $type, entityType == $entityType, this != $larger, !hasManualChanges(), active()) + $larger: TextEntity($type: type(), $entityType: entityType, active()) + $contained: TextEntity(containedBy($larger), type() == $type, entityType == $entityType, this != $larger, !hasManualChanges(), active()) then $contained.remove("X.0.0", "remove Entity contained by Entity of same type"); retract($contained); @@ -895,8 +894,8 @@ rule "X.0.0: Remove Entity contained by Entity of same type" rule "X.1.0: Merge intersecting Entities of same type" salience 64 when - $first: TextEntity($type: type, $entityType: entityType, !resized(), active()) - $second: TextEntity(intersects($first), type == $type, entityType == $entityType, this != $first, !hasManualChanges(), active()) + $first: TextEntity($type: type(), $entityType: entityType, !resized(), active()) + $second: TextEntity(intersects($first), type() == $type, entityType == $entityType, this != $first, !hasManualChanges(), active()) then TextEntity mergedEntity = entityCreationService.mergeEntitiesOfSameType(List.of($first, $second), $type, $entityType, document); $first.remove("X.1.0", "merge intersecting Entities of same type"); @@ -911,8 +910,8 @@ rule "X.1.0: Merge intersecting Entities of same type" rule "X.2.0: Remove Entity of type ENTITY when contained by FALSE_POSITIVE" salience 64 when - $falsePositive: TextEntity($type: type, entityType == EntityType.FALSE_POSITIVE, active()) - $entity: TextEntity(containedBy($falsePositive), type == $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), !hasManualChanges(), active()) + $falsePositive: TextEntity($type: type(), entityType == EntityType.FALSE_POSITIVE, active()) + $entity: TextEntity(containedBy($falsePositive), type() == $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), !hasManualChanges(), active()) then $entity.getIntersectingNodes().forEach(node -> update(node)); $entity.remove("X.2.0", "remove Entity of type ENTITY when contained by FALSE_POSITIVE"); @@ -924,8 +923,8 @@ rule "X.2.0: Remove Entity of type ENTITY when contained by FALSE_POSITIVE" rule "X.3.0: Remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION" salience 64 when - $falseRecommendation: TextEntity($type: type, entityType == EntityType.FALSE_RECOMMENDATION, active()) - $recommendation: TextEntity(containedBy($falseRecommendation), type == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) + $falseRecommendation: TextEntity($type: type(), entityType == EntityType.FALSE_RECOMMENDATION, active()) + $recommendation: TextEntity(containedBy($falseRecommendation), type() == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) then $recommendation.remove("X.3.0", "remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION"); retract($recommendation); @@ -936,8 +935,8 @@ rule "X.3.0: Remove Entity of type RECOMMENDATION when contained by FALSE_RECOMM rule "X.4.0: Remove Entity of type RECOMMENDATION when text range equals ENTITY with same type" salience 256 when - $entity: TextEntity($type: type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) - $recommendation: TextEntity(getTextRange().equals($entity.getTextRange()), type == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) + $entity: TextEntity($type: type(), (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) + $recommendation: TextEntity(getTextRange().equals($entity.getTextRange()), type() == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) then $entity.addEngines($recommendation.getEngines()); $recommendation.remove("X.4.0", "remove Entity of type RECOMMENDATION when text range equals ENTITY with same type"); @@ -959,8 +958,8 @@ rule "X.5.0: Remove Entity of type RECOMMENDATION when intersected by ENTITY" rule "X.5.1: Remove Entity of type RECOMMENDATION when contained by RECOMMENDATION" salience 256 when - $entity: TextEntity($type: type, entityType == EntityType.RECOMMENDATION, active()) - $recommendation: TextEntity(containedBy($entity), type != $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) + $entity: TextEntity($type: type(), entityType == EntityType.RECOMMENDATION, active()) + $recommendation: TextEntity(containedBy($entity), type() != $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) then $recommendation.remove("X.5.1", "remove Entity of type RECOMMENDATION when contained by RECOMMENDATION"); retract($recommendation); @@ -971,8 +970,8 @@ rule "X.5.1: Remove Entity of type RECOMMENDATION when contained by RECOMMENDATI rule "X.6.0: Remove Entity of lower rank, when contained by by entity of type ENTITY" salience 32 when - $higherRank: TextEntity($type: type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) - $lowerRank: TextEntity(containedBy($higherRank), type != $type, dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), !hasManualChanges(), active()) + $higherRank: TextEntity($type: type(), (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) + $lowerRank: TextEntity(containedBy($higherRank), type() != $type, dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), !hasManualChanges(), active()) then $lowerRank.getIntersectingNodes().forEach(node -> update(node)); $lowerRank.remove("X.6.0", "remove Entity of lower rank, when contained by entity of type ENTITY"); @@ -982,8 +981,8 @@ rule "X.6.0: Remove Entity of lower rank, when contained by by entity of type EN rule "X.6.1: remove Entity of higher rank, when intersected by entity of type ENTITY and length of lower rank Entity is bigger than the higher rank Entity" salience 32 when - $higherRank: TextEntity($type: type, $value: value, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active(), !hasManualChanges()) - $lowerRank: TextEntity(intersects($higherRank), type != $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), active(), $lowerRank.getValue().length() > $value.length()) + $higherRank: TextEntity($type: type(), $value: value, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active(), !hasManualChanges()) + $lowerRank: TextEntity(intersects($higherRank), type() != $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), active(), $lowerRank.getValue().length() > $value.length()) then $higherRank.getIntersectingNodes().forEach(node -> update(node)); $higherRank.remove("X.6.1", "remove Entity of higher rank, when intersected by entity of type ENTITY and length of lower rank Entity is bigger than the higher rank Entity"); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/all_redact_manager_rules.drl b/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/all_redact_manager_rules.drl index 256a327a..156f3f09 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/all_redact_manager_rules.drl +++ b/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/all_redact_manager_rules.drl @@ -97,7 +97,7 @@ rule "SYN.1.0: Recommend CTL/BL laboratory that start with BL or CTL" rule "CBI.0.0: Redact CBI Authors (non vertebrate Study)" when not FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $entity: TextEntity(type == "CBI_author", dictionaryEntry) + $entity: TextEntity(type() == "CBI_author", dictionaryEntry) then $entity.redact("CBI.0.0", "Author found", "Article 39(e)(3) of Regulation (EC) No 178/2002"); end @@ -105,7 +105,7 @@ rule "CBI.0.0: Redact CBI Authors (non vertebrate Study)" rule "CBI.0.1: Redact CBI Authors (vertebrate Study)" when FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $entity: TextEntity(type == "CBI_author", dictionaryEntry) + $entity: TextEntity(type() == "CBI_author", dictionaryEntry) then $entity.redact("CBI.0.1", "Author found", "Article 39(e)(2) of Regulation (EC) No 178/2002"); end @@ -115,7 +115,7 @@ rule "CBI.0.1: Redact CBI Authors (vertebrate Study)" rule "CBI.1.0: Do not redact CBI Address (non vertebrate Study)" when not FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $entity: TextEntity(type == "CBI_address", dictionaryEntry) + $entity: TextEntity(type() == "CBI_address", dictionaryEntry) then $entity.skip("CBI.1.0", "Address found for Non Vertebrate Study"); end @@ -123,7 +123,7 @@ rule "CBI.1.0: Do not redact CBI Address (non vertebrate Study)" rule "CBI.1.1: Redact CBI Address (vertebrate Study)" when FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $entity: TextEntity(type == "CBI_address", dictionaryEntry) + $entity: TextEntity(type() == "CBI_address", dictionaryEntry) then $entity.redact("CBI.1.1", "Address found", "Article 39(e)(2) of Regulation (EC) No 178/2002"); end @@ -132,7 +132,7 @@ rule "CBI.1.1: Redact CBI Address (vertebrate Study)" // Rule unit: CBI.2 rule "CBI.2.0: Do not redact genitive CBI Author" when - $entity: TextEntity(type == "CBI_author", anyMatch(textAfter, "['’’'ʼˈ´`‘′ʻ’']s")) + $entity: TextEntity(type() == "CBI_author", anyMatch(textAfter, "['’’'ʼˈ´`‘′ʻ’']s")) then entityCreationService.byTextRange($entity.getTextRange(), "CBI_author", EntityType.FALSE_POSITIVE, document) .ifPresent(falsePositive -> falsePositive.skip("CBI.2.0", "Genitive Author found")); @@ -331,7 +331,7 @@ rule "CBI.7.1: Do not redact Names and Addresses if published information found $table: Table(hasEntitiesOfType("published_information"), hasEntitiesOfType("CBI_author") || hasEntitiesOfType("CBI_address")) $cellsWithPublishedInformation: TableCell() from $table.streamTableCellsWhichContainType("published_information").toList() $tableCell: TableCell(row == $cellsWithPublishedInformation.row) from $table.streamTableCells().toList() - $authorOrAddress: TextEntity(type == "CBI_author" || type == "CBI_address", active()) from $tableCell.getEntities() + $authorOrAddress: TextEntity(type() == "CBI_author" || type() == "CBI_address", active()) from $tableCell.getEntities() then $authorOrAddress.skipWithReferences("CBI.7.1", "Published Information found in row", $table.getEntitiesOfTypeInSameRow("published_information", $authorOrAddress)); end @@ -493,7 +493,7 @@ rule "CBI.12.2: Skip TableCell with header 'Author' or 'Author(s)' and header 'V rule "CBI.13.0: Ignore CBI Address recommendations" when not FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $entity: TextEntity(type == "CBI_address", entityType == EntityType.RECOMMENDATION) + $entity: TextEntity(type() == "CBI_address", entityType == EntityType.RECOMMENDATION) then $entity.ignore("CBI.13.0", "Ignore CBI Address Recommendations"); retract($entity) @@ -503,7 +503,7 @@ rule "CBI.13.0: Ignore CBI Address recommendations" // Rule unit: CBI.14 rule "CBI.14.0: Redact CBI_sponsor entities if preceded by \"batches produced at\"" when - $sponsorEntity: TextEntity(type == "CBI_sponsor", textBefore.contains("batches produced at")) + $sponsorEntity: TextEntity(type() == "CBI_sponsor", textBefore.contains("batches produced at")) then $sponsorEntity.redact("CBI.14.0", "Redacted because it represents a sponsor company", "Reg (EC) No 1107/2009 Art. 63 (2g)"); end @@ -606,7 +606,7 @@ rule "CBI.17.1: Add recommendation for Addresses in Test Organism sections, with rule "CBI.18.0: Expand CBI_author entities with firstname initials" no-loop true when - $entityToExpand: TextEntity(type == "CBI_author", + $entityToExpand: TextEntity(type() == "CBI_author", value.matches("[^\\s]+"), textAfter.startsWith(" "), anyMatch(textAfter, "(,? [A-Z]\\.?( ?[A-Z]\\.?)?( ?[A-Z]\\.?)?\\b\\.?)") @@ -624,7 +624,7 @@ rule "CBI.18.0: Expand CBI_author entities with firstname initials" // Rule unit: CBI.19 rule "CBI.19.0: Expand CBI_author entities with salutation prefix" when - $entityToExpand: TextEntity(type == "CBI_author", anyMatch(textBefore, "\\b(Mrs?|Ms|Miss|Sir|Madame?|Mme)\\s?\\.?\\s*")) + $entityToExpand: TextEntity(type() == "CBI_author", anyMatch(textBefore, "\\b(Mrs?|Ms|Miss|Sir|Madame?|Mme)\\s?\\.?\\s*")) then entityCreationService.byPrefixExpansionRegex($entityToExpand, "\\b(Mrs?|Ms|Miss|Sir|Madame?|Mme)\\s?\\.?\\s*") .ifPresent(expandedEntity -> { @@ -668,7 +668,7 @@ rule "CBI.21.0: Redact short Authors section (non vertebrate study)" when not FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") $section: Section(containsAnyStringIgnoreCase("author(s)", "author", "authors"), length() < 50, getTreeId().get(0) <= 20) //TODO: evaluate the reason of this rule - not TextEntity(type == "CBI_author", engines contains Engine.NER) from $section.getEntities() + not TextEntity(type() == "CBI_author", engines contains Engine.NER) from $section.getEntities() then entityCreationService.byRegexIgnoreCase("(?<=author\\(?s\\)?\\s\\n?)([\\p{Lu}\\p{L} ]{5,15}(,|\\n)?){1,3}", "CBI_author", EntityType.ENTITY, $section) .forEach(entity -> { @@ -680,7 +680,7 @@ rule "CBI.21.1: Redact short Authors section (vertebrate study)" when FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") $section: Section(containsAnyStringIgnoreCase("author(s)", "author", "authors"), length() < 50, getTreeId().get(0) <= 20) //TODO: evaluate the reason of this rule - not TextEntity(type == "CBI_author", engines contains Engine.NER) from $section.getEntities() + not TextEntity(type() == "CBI_author", engines contains Engine.NER) from $section.getEntities() then entityCreationService.byRegexIgnoreCase("(?<=author\\(?s\\)?\\s\\n?)([\\p{Lu}\\p{L} ]{5,15}(,|\\n)?){1,3}", "CBI_author", EntityType.ENTITY, $section) .forEach(entity -> { @@ -707,7 +707,7 @@ rule "CBI.22.0: Redact Addresses in Reference Tables for vertebrate studies in n rule "PII.0.0: Redact all PII (non vertebrate study)" when not FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $pii: TextEntity(type == "PII", dictionaryEntry) + $pii: TextEntity(type() == "PII", dictionaryEntry) then $pii.redact("PII.0.0", "Personal Information found", "Article 39(e)(3) of Regulation (EC) No 178/2002"); end @@ -715,7 +715,7 @@ rule "PII.0.0: Redact all PII (non vertebrate study)" rule "PII.0.1: Redact all PII (vertebrate study)" when FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $pii: TextEntity(type == "PII", dictionaryEntry) + $pii: TextEntity(type() == "PII", dictionaryEntry) then $pii.redact("PII.0.1", "Personal Information found", "Article 39(e)(2) of Regulation (EC) No 178/2002"); end @@ -1047,7 +1047,7 @@ rule "PII.11.0: Redact On behalf of Sequani Ltd.:" rule "PII.12.0: Expand PII entities with salutation prefix" when not FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $entityToExpand: TextEntity(type == "PII", anyMatch(textBefore, "\\b(Mrs?|Ms|Miss|Sir|Madame?|Mme)\\s?\\.?\\s*")) + $entityToExpand: TextEntity(type() == "PII", anyMatch(textBefore, "\\b(Mrs?|Ms|Miss|Sir|Madame?|Mme)\\s?\\.?\\s*")) then entityCreationService.byPrefixExpansionRegex($entityToExpand, "\\b(Mrs?|Ms|Miss|Sir|Madame?|Mme)\\s?\\.?\\s*") .ifPresent(expandedEntity -> expandedEntity.apply("PII.12.0", "Expanded PII with salutation prefix", "Article 39(e)(3) of Regulation (EC) No 178/2002")); @@ -1134,21 +1134,21 @@ rule "ETC.3.1: Redact logos (vertebrate study)" // Rule unit: ETC.4 rule "ETC.4.0: Redact dossier dictionary entries" when - $dossierRedaction: TextEntity(type == "dossier_redaction") + $dossierRedaction: TextEntity(type() == "dossier_redaction") then $dossierRedaction.redact("ETC.4.0", "Specification of impurity found", "Article 39(e)(3) of Regulation (EC) No 178/2002"); end rule "ETC.4.1: Redact dossier dictionary entries" when - $dossierRedaction: TextEntity(type == "dossier_redaction") + $dossierRedaction: TextEntity(type() == "dossier_redaction") then $dossierRedaction.redact("ETC.4.1", "Dossier Redaction found", "Article 39(1)(2) of Regulation (EC) No 178/2002"); end rule "ETC.4.2: Redact dossier dictionary entries" when - $dossierRedaction: TextEntity(type == "dossier_redaction") + $dossierRedaction: TextEntity(type() == "dossier_redaction") then $dossierRedaction.redact("ETC.4.2", "Dossier redaction found", "Article 63(2)(a) of Regulation (EC) No 1107/2009 (making reference to Article 39 of Regulation EC No 178/2002)"); end @@ -1158,7 +1158,7 @@ rule "ETC.4.2: Redact dossier dictionary entries" rule "ETC.5.0: Ignore dossier_redaction entries if confidentiality is not 'confidential'" when not FileAttribute(label == "Confidentiality", value == "confidential") - $dossierRedaction: TextEntity(type == "dossier_redaction") + $dossierRedaction: TextEntity(type() == "dossier_redaction") then $dossierRedaction.ignore("ETC.5.0", "Ignore dossier redactions, when not confidential"); $dossierRedaction.getIntersectingNodes().forEach(node -> update(node)); @@ -1212,7 +1212,7 @@ rule "ETC.8.1: Redact formulas (non vertebrate study)" rule "ETC.9.0: Redact skipped impurities" when FileAttribute(label == "Redact Skipped Impurities", value soundslike "Yes" || value.toLowerCase() == "y") - $skippedImpurities: TextEntity(type == "skipped_impurities") + $skippedImpurities: TextEntity(type() == "skipped_impurities") then $skippedImpurities.redact("ETC.9.0", "Occasional Impurity found", "Article 63(2)(b) of Regulation (EC) No 1107/2009"); end @@ -1220,7 +1220,7 @@ rule "ETC.9.0: Redact skipped impurities" rule "ETC.9.1: Redact impurities" when FileAttribute(label == "Redact Impurities", value soundslike "Yes" || value.toLowerCase() == "y") - $skippedImpurities: TextEntity(type == "impurities") + $skippedImpurities: TextEntity(type() == "impurities") then $skippedImpurities.redact("ETC.9.1", "Impurity found", "Article 63(2)(b) of Regulation (EC) No 1107/2009"); end @@ -1229,7 +1229,7 @@ rule "ETC.9.1: Redact impurities" // Rule unit: ETC.10 rule "ETC.10.0: Redact Product Composition Information" when - $compositionInformation: TextEntity(type == "product_composition") + $compositionInformation: TextEntity(type() == "product_composition") then $compositionInformation.redact("ETC.10.0", "Product Composition Information found", "Article 63(2)(d) of Regulation (EC) No 1107/2009"); end @@ -1256,7 +1256,7 @@ rule "AI.0.0: Add all NER Entities of type CBI_author" nerEntities: NerEntities(hasEntitiesOfType("CBI_author")) then nerEntities.streamEntitiesOfType("CBI_author") - .forEach(nerEntity -> entityCreationService.byNerEntity(nerEntity, EntityType.RECOMMENDATION, document)); + .forEach(nerEntity -> entityCreationService.optionalByNerEntity(nerEntity, EntityType.RECOMMENDATION, document)); end @@ -1278,7 +1278,7 @@ rule "AI.2.0: Add all NER Entities of any type except CBI_author" then nerEntities.getNerEntityList().stream() .filter(nerEntity -> !nerEntity.type().equals("CBI_author")) - .forEach(nerEntity -> entityCreationService.byNerEntity(nerEntity, nerEntity.type().toLowerCase(), EntityType.RECOMMENDATION, document)); + .forEach(nerEntity -> entityCreationService.optionalByNerEntity(nerEntity, nerEntity.type().toLowerCase(), EntityType.RECOMMENDATION, document)); end @@ -1289,7 +1289,7 @@ rule "AI.3.0: Recommend authors from AI as PII" nerEntities: NerEntities(hasEntitiesOfType("CBI_author")) then nerEntities.streamEntitiesOfType("CBI_author") - .forEach(nerEntity -> entityCreationService.byNerEntity(nerEntity, "PII", EntityType.RECOMMENDATION, document)); + .forEach(nerEntity -> entityCreationService.optionalByNerEntity(nerEntity, "PII", EntityType.RECOMMENDATION, document)); end @@ -1303,7 +1303,7 @@ rule "MAN.0.0: Apply manual resize redaction" not ManualResizeRedaction(annotationId == $id, requestDate.isBefore($requestDate)) $entityToBeResized: TextEntity(matchesAnnotationId($id)) then - manualChangesApplicationService.resizeEntityAndReinsert($entityToBeResized, $resizeRedaction); + manualChangesApplicationService.resize($entityToBeResized, $resizeRedaction); retract($resizeRedaction); update($entityToBeResized); $entityToBeResized.getIntersectingNodes().forEach(node -> update(node)); @@ -1381,13 +1381,12 @@ rule "MAN.3.0: Apply entity recategorization" when $recategorization: ManualRecategorization($id: annotationId, $type: type, $requestDate: requestDate) not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate)) - $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type != $type) + $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type() != $type) then $entityToBeRecategorized.getIntersectingNodes().forEach(node -> update(node)); - manualChangesApplicationService.recategorize($entityToBeRecategorized, $recategorization); + $entityToBeRecategorized.getManualOverwrite().addChange($recategorization); + update($entityToBeRecategorized); retract($recategorization); - // Entity is copied and inserted, so the old entity needs to be retracted to avoid duplication. - retract($entityToBeRecategorized); end rule "MAN.3.1: Apply entity recategorization of same type" @@ -1395,7 +1394,7 @@ rule "MAN.3.1: Apply entity recategorization of same type" when $recategorization: ManualRecategorization($id: annotationId, $type: type, $requestDate: requestDate) not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate)) - $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type == $type) + $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type() == $type) then $entityToBeRecategorized.getManualOverwrite().addChange($recategorization); retract($recategorization); @@ -1453,8 +1452,8 @@ rule "MAN.4.1: Apply legal basis change" rule "X.0.0: Remove Entity contained by Entity of same type" salience 65 when - $larger: TextEntity($type: type, $entityType: entityType, active()) - $contained: TextEntity(containedBy($larger), type == $type, entityType == $entityType, this != $larger, !hasManualChanges(), active()) + $larger: TextEntity($type: type(), $entityType: entityType, active()) + $contained: TextEntity(containedBy($larger), type() == $type, entityType == $entityType, this != $larger, !hasManualChanges(), active()) then $contained.remove("X.0.0", "remove Entity contained by Entity of same type"); retract($contained); @@ -1465,8 +1464,8 @@ rule "X.0.0: Remove Entity contained by Entity of same type" rule "X.1.0: Merge intersecting Entities of same type" salience 64 when - $first: TextEntity($type: type, $entityType: entityType, !resized(), active()) - $second: TextEntity(intersects($first), type == $type, entityType == $entityType, this != $first, !hasManualChanges(), active()) + $first: TextEntity($type: type(), $entityType: entityType, !resized(), active()) + $second: TextEntity(intersects($first), type() == $type, entityType == $entityType, this != $first, !hasManualChanges(), active()) then TextEntity mergedEntity = entityCreationService.mergeEntitiesOfSameType(List.of($first, $second), $type, $entityType, document); $first.remove("X.1.0", "merge intersecting Entities of same type"); @@ -1481,8 +1480,8 @@ rule "X.1.0: Merge intersecting Entities of same type" rule "X.2.0: Remove Entity of type ENTITY when contained by FALSE_POSITIVE" salience 64 when - $falsePositive: TextEntity($type: type, entityType == EntityType.FALSE_POSITIVE, active()) - $entity: TextEntity(containedBy($falsePositive), type == $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), !hasManualChanges(), active()) + $falsePositive: TextEntity($type: type(), entityType == EntityType.FALSE_POSITIVE, active()) + $entity: TextEntity(containedBy($falsePositive), type() == $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), !hasManualChanges(), active()) then $entity.getIntersectingNodes().forEach(node -> update(node)); $entity.remove("X.2.0", "remove Entity of type ENTITY when contained by FALSE_POSITIVE"); @@ -1494,8 +1493,8 @@ rule "X.2.0: Remove Entity of type ENTITY when contained by FALSE_POSITIVE" rule "X.3.0: Remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION" salience 64 when - $falseRecommendation: TextEntity($type: type, entityType == EntityType.FALSE_RECOMMENDATION, active()) - $recommendation: TextEntity(containedBy($falseRecommendation), type == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) + $falseRecommendation: TextEntity($type: type(), entityType == EntityType.FALSE_RECOMMENDATION, active()) + $recommendation: TextEntity(containedBy($falseRecommendation), type() == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) then $recommendation.remove("X.3.0", "remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION"); retract($recommendation); @@ -1506,8 +1505,8 @@ rule "X.3.0: Remove Entity of type RECOMMENDATION when contained by FALSE_RECOMM rule "X.4.0: Remove Entity of type RECOMMENDATION when text range equals ENTITY with same type" salience 256 when - $entity: TextEntity($type: type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) - $recommendation: TextEntity(getTextRange().equals($entity.getTextRange()), type == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) + $entity: TextEntity($type: type(), (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) + $recommendation: TextEntity(getTextRange().equals($entity.getTextRange()), type() == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) then $entity.addEngines($recommendation.getEngines()); $recommendation.remove("X.4.0", "remove Entity of type RECOMMENDATION when text range equals ENTITY with same type"); @@ -1529,8 +1528,8 @@ rule "X.5.0: Remove Entity of type RECOMMENDATION when intersected by ENTITY" rule "X.5.1: Remove Entity of type RECOMMENDATION when contained by RECOMMENDATION" salience 256 when - $entity: TextEntity($type: type, entityType == EntityType.RECOMMENDATION, active()) - $recommendation: TextEntity(containedBy($entity), type != $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) + $entity: TextEntity($type: type(), entityType == EntityType.RECOMMENDATION, active()) + $recommendation: TextEntity(containedBy($entity), type() != $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) then $recommendation.remove("X.5.1", "remove Entity of type RECOMMENDATION when contained by RECOMMENDATION"); retract($recommendation); @@ -1541,8 +1540,8 @@ rule "X.5.1: Remove Entity of type RECOMMENDATION when contained by RECOMMENDATI rule "X.6.0: Remove Entity of lower rank, when contained by by entity of type ENTITY" salience 32 when - $higherRank: TextEntity($type: type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) - $lowerRank: TextEntity(containedBy($higherRank), type != $type, dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), !hasManualChanges(), active()) + $higherRank: TextEntity($type: type(), (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) + $lowerRank: TextEntity(containedBy($higherRank), type() != $type, dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), !hasManualChanges(), active()) then $lowerRank.getIntersectingNodes().forEach(node -> update(node)); $lowerRank.remove("X.6.0", "remove Entity of lower rank, when contained by entity of type ENTITY"); @@ -1552,8 +1551,8 @@ rule "X.6.0: Remove Entity of lower rank, when contained by by entity of type EN rule "X.6.1: remove Entity of higher rank, when intersected by entity of type ENTITY and length of lower rank Entity is bigger than the higher rank Entity" salience 32 when - $higherRank: TextEntity($type: type, $value: value, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active(), !hasManualChanges()) - $lowerRank: TextEntity(intersects($higherRank), type != $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), active(), $lowerRank.getValue().length() > $value.length()) + $higherRank: TextEntity($type: type(), $value: value, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active(), !hasManualChanges()) + $lowerRank: TextEntity(intersects($higherRank), type() != $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), active(), $lowerRank.getValue().length() > $value.length()) then $higherRank.getIntersectingNodes().forEach(node -> update(node)); $higherRank.remove("X.6.1", "remove Entity of higher rank, when intersected by entity of type ENTITY and length of lower rank Entity is bigger than the higher rank Entity"); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/documine_flora.drl b/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/documine_flora.drl index 72c1cf67..9cf983bb 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/documine_flora.drl +++ b/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/documine_flora.drl @@ -466,7 +466,9 @@ rule "DOC.7.1: Performing Laboratory (Country)" then nerEntities.streamEntitiesOfType("COUNTRY") .filter(nerEntity -> $section.getTextRange().contains(nerEntity.textRange())) - .map(nerEntity -> entityCreationService.byNerEntity(nerEntity, "laboratory_country", EntityType.ENTITY, $section)) + .map(nerEntity -> entityCreationService.optionalByNerEntity(nerEntity, "laboratory_country", EntityType.ENTITY, $section)) + .flatMap(Optional::stream) + .collect(Collectors.toList()) .forEach(entity -> { entity.apply("DOC.7.1", "Performing Laboratory found", "n-a"); }); @@ -475,7 +477,7 @@ rule "DOC.7.1: Performing Laboratory (Country)" rule "DOC.7.2: Performing Laboratory (Country & Name) from dict" when $section: Section(containsString("PERFORMING LABORATORY:") || (containsString("PERFORMING") && containsString("LABORATORY:"))) - $countryOrNameFromDictionary: TextEntity(type == "laboratory_country" || type == "laboratory_name", $type: type, isDictionaryEntry()) from $section.getEntities() + $countryOrNameFromDictionary: TextEntity(type() == "laboratory_country" || type() == "laboratory_name", $type: type, isDictionaryEntry()) from $section.getEntities() then $countryOrNameFromDictionary.apply("DOC.7.2", "Performing " + $type + " dictionary entry found."); end @@ -1161,7 +1163,7 @@ rule "MAN.0.0: Apply manual resize redaction" not ManualResizeRedaction(annotationId == $id, requestDate.isBefore($requestDate)) $entityToBeResized: TextEntity(matchesAnnotationId($id)) then - manualChangesApplicationService.resizeEntityAndReinsert($entityToBeResized, $resizeRedaction); + manualChangesApplicationService.resize($entityToBeResized, $resizeRedaction); retract($resizeRedaction); update($entityToBeResized); $entityToBeResized.getIntersectingNodes().forEach(node -> update(node)); @@ -1239,13 +1241,12 @@ rule "MAN.3.0: Apply entity recategorization" when $recategorization: ManualRecategorization($id: annotationId, $type: type, $requestDate: requestDate) not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate)) - $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type != $type) + $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type() != $type) then $entityToBeRecategorized.getIntersectingNodes().forEach(node -> update(node)); - manualChangesApplicationService.recategorize($entityToBeRecategorized, $recategorization); + $entityToBeRecategorized.getManualOverwrite().addChange($recategorization); + update($entityToBeRecategorized); retract($recategorization); - // Entity is copied and inserted, so the old entity needs to be retracted to avoid duplication. - retract($entityToBeRecategorized); end rule "MAN.3.1: Apply entity recategorization of same type" @@ -1253,7 +1254,7 @@ rule "MAN.3.1: Apply entity recategorization of same type" when $recategorization: ManualRecategorization($id: annotationId, $type: type, $requestDate: requestDate) not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate)) - $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type == $type) + $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type() == $type) then $entityToBeRecategorized.getManualOverwrite().addChange($recategorization); retract($recategorization); @@ -1311,8 +1312,8 @@ rule "MAN.4.1: Apply legal basis change" rule "X.0.0: Remove Entity contained by Entity of same type" salience 65 when - $larger: TextEntity($type: type, $entityType: entityType, active()) - $contained: TextEntity(containedBy($larger), type == $type, entityType == $entityType, this != $larger, !hasManualChanges(), active()) + $larger: TextEntity($type: type(), $entityType: entityType, active()) + $contained: TextEntity(containedBy($larger), type() == $type, entityType == $entityType, this != $larger, !hasManualChanges(), active()) then $contained.remove("X.0.0", "remove Entity contained by Entity of same type"); retract($contained); @@ -1323,8 +1324,8 @@ rule "X.0.0: Remove Entity contained by Entity of same type" rule "X.2.0: Remove Entity of type ENTITY when contained by FALSE_POSITIVE" salience 64 when - $falsePositive: TextEntity($type: type, entityType == EntityType.FALSE_POSITIVE, active()) - $entity: TextEntity(containedBy($falsePositive), type == $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), !hasManualChanges(), active()) + $falsePositive: TextEntity($type: type(), entityType == EntityType.FALSE_POSITIVE, active()) + $entity: TextEntity(containedBy($falsePositive), type() == $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), !hasManualChanges(), active()) then $entity.getIntersectingNodes().forEach(node -> update(node)); $entity.remove("X.2.0", "remove Entity of type ENTITY when contained by FALSE_POSITIVE"); @@ -1336,8 +1337,8 @@ rule "X.2.0: Remove Entity of type ENTITY when contained by FALSE_POSITIVE" rule "X.3.0: Remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION" salience 64 when - $falseRecommendation: TextEntity($type: type, entityType == EntityType.FALSE_RECOMMENDATION, active()) - $recommendation: TextEntity(containedBy($falseRecommendation), type == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) + $falseRecommendation: TextEntity($type: type(), entityType == EntityType.FALSE_RECOMMENDATION, active()) + $recommendation: TextEntity(containedBy($falseRecommendation), type() == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) then $recommendation.remove("X.3.0", "remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION"); retract($recommendation); @@ -1348,8 +1349,8 @@ rule "X.3.0: Remove Entity of type RECOMMENDATION when contained by FALSE_RECOMM rule "X.4.0: Remove Entity of type RECOMMENDATION when text range equals ENTITY with same type" salience 256 when - $entity: TextEntity($type: type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) - $recommendation: TextEntity(getTextRange().equals($entity.getTextRange()), type == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) + $entity: TextEntity($type: type(), (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) + $recommendation: TextEntity(getTextRange().equals($entity.getTextRange()), type() == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) then $entity.addEngines($recommendation.getEngines()); $recommendation.remove("X.4.0", "remove Entity of type RECOMMENDATION when text range equals ENTITY with same type"); @@ -1371,8 +1372,8 @@ rule "X.5.0: Remove Entity of type RECOMMENDATION when intersected by ENTITY" rule "X.5.1: Remove Entity of type RECOMMENDATION when contained by RECOMMENDATION" salience 256 when - $entity: TextEntity($type: type, entityType == EntityType.RECOMMENDATION, active()) - $recommendation: TextEntity(containedBy($entity), type != $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) + $entity: TextEntity($type: type(), entityType == EntityType.RECOMMENDATION, active()) + $recommendation: TextEntity(containedBy($entity), type() != $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) then $recommendation.remove("X.5.1", "remove Entity of type RECOMMENDATION when contained by RECOMMENDATION"); retract($recommendation); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/manual_redaction_rules.drl b/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/manual_redaction_rules.drl index c0750bcc..50272f51 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/manual_redaction_rules.drl +++ b/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/manual_redaction_rules.drl @@ -75,7 +75,7 @@ rule "MAN.0.0: Apply manual resize redaction" not ManualResizeRedaction(annotationId == $id, requestDate.isBefore($requestDate)) $entityToBeResized: TextEntity(matchesAnnotationId($id)) then - manualChangesApplicationService.resizeEntityAndReinsert($entityToBeResized, $resizeRedaction); + manualChangesApplicationService.resize($entityToBeResized, $resizeRedaction); retract($resizeRedaction); update($entityToBeResized); $entityToBeResized.getIntersectingNodes().forEach(node -> update(node)); @@ -153,13 +153,12 @@ rule "MAN.3.0: Apply entity recategorization" when $recategorization: ManualRecategorization($id: annotationId, $type: type, $requestDate: requestDate) not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate)) - $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type != $type) + $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type() != $type) then $entityToBeRecategorized.getIntersectingNodes().forEach(node -> update(node)); - manualChangesApplicationService.recategorize($entityToBeRecategorized, $recategorization); + $entityToBeRecategorized.getManualOverwrite().addChange($recategorization); + update($entityToBeRecategorized); retract($recategorization); - // Entity is copied and inserted, so the old entity needs to be retracted to avoid duplication. - retract($entityToBeRecategorized); end rule "MAN.3.1: Apply entity recategorization of same type" @@ -167,7 +166,7 @@ rule "MAN.3.1: Apply entity recategorization of same type" when $recategorization: ManualRecategorization($id: annotationId, $type: type, $requestDate: requestDate) not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate)) - $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type == $type) + $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type() == $type) then $entityToBeRecategorized.getManualOverwrite().addChange($recategorization); retract($recategorization); @@ -225,8 +224,8 @@ rule "MAN.4.1: Apply legal basis change" rule "X.5.1: Remove Entity of type RECOMMENDATION when contained by RECOMMENDATION" salience 256 when - $entity: TextEntity($type: type, entityType == EntityType.RECOMMENDATION, active()) - $recommendation: TextEntity(containedBy($entity), type != $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) + $entity: TextEntity($type: type(), entityType == EntityType.RECOMMENDATION, active()) + $recommendation: TextEntity(containedBy($entity), type() != $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) then $recommendation.remove("X.5.1", "remove Entity of type RECOMMENDATION when contained by RECOMMENDATION"); retract($recommendation); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/rules.drl b/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/rules.drl index bba749d1..3e5a6bb5 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/rules.drl +++ b/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/rules.drl @@ -339,7 +339,7 @@ rule "CBI.12.2: Skip TableCell with header 'Author' or 'Author(s)' and header 'V // Rule unit: CBI.14 rule "CBI.14.0: Redact CBI_sponsor entities if preceded by \"batches produced at\"" when - $sponsorEntity: TextEntity(type == "CBI_sponsor", textBefore.contains("batches produced at")) + $sponsorEntity: TextEntity(type() == "CBI_sponsor", textBefore.contains("batches produced at")) then $sponsorEntity.redact("CBI.14.0", "Redacted because it represents a sponsor company", "Reg (EC) No 1107/2009 Art. 63 (2g)"); end @@ -442,7 +442,7 @@ rule "CBI.17.1: Add recommendation for Addresses in Test Organism sections, with rule "CBI.18.0: Expand CBI_author entities with firstname initials" no-loop true when - $entityToExpand: TextEntity(type == "CBI_author", + $entityToExpand: TextEntity(type() == "CBI_author", value.matches("[^\\s]+"), textAfter.startsWith(" "), anyMatch(textAfter, "(,? [A-Z]\\.?( ?[A-Z]\\.?)?( ?[A-Z]\\.?)?\\b\\.?)") @@ -460,7 +460,7 @@ rule "CBI.18.0: Expand CBI_author entities with firstname initials" // Rule unit: CBI.19 rule "CBI.19.0: Expand CBI_author entities with salutation prefix" when - $entityToExpand: TextEntity(type == "CBI_author", anyMatch(textBefore, "\\b(Mrs?|Ms|Miss|Sir|Madame?|Mme)\\s?\\.?\\s*")) + $entityToExpand: TextEntity(type() == "CBI_author", anyMatch(textBefore, "\\b(Mrs?|Ms|Miss|Sir|Madame?|Mme)\\s?\\.?\\s*")) then entityCreationService.byPrefixExpansionRegex($entityToExpand, "\\b(Mrs?|Ms|Miss|Sir|Madame?|Mme)\\s?\\.?\\s*") .ifPresent(expandedEntity -> { @@ -505,7 +505,7 @@ rule "CBI.20.1: Redact between \"PERFORMING LABORATORY\" and \"LABORATORY PROJEC rule "PII.0.0: Redact all PII (non vertebrate study)" when not FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $pii: TextEntity(type == "PII", dictionaryEntry) + $pii: TextEntity(type() == "PII", dictionaryEntry) then $pii.redact("PII.0.0", "Personal Information found", "Article 39(e)(3) of Regulation (EC) No 178/2002"); end @@ -513,7 +513,7 @@ rule "PII.0.0: Redact all PII (non vertebrate study)" rule "PII.0.1: Redact all PII (vertebrate study)" when FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $pii: TextEntity(type == "PII", dictionaryEntry) + $pii: TextEntity(type() == "PII", dictionaryEntry) then $pii.redact("PII.0.1", "Personal Information found", "Article 39(e)(2) of Regulation (EC) No 178/2002"); end @@ -733,7 +733,7 @@ rule "PII.11.0: Redact On behalf of Sequani Ltd.:" rule "PII.12.0: Expand PII entities with salutation prefix" when not FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $entityToExpand: TextEntity(type == "PII", anyMatch(textBefore, "\\b(Mrs?|Ms|Miss|Sir|Madame?|Mme)\\s?\\.?\\s*")) + $entityToExpand: TextEntity(type() == "PII", anyMatch(textBefore, "\\b(Mrs?|Ms|Miss|Sir|Madame?|Mme)\\s?\\.?\\s*")) then entityCreationService.byPrefixExpansionRegex($entityToExpand, "\\b(Mrs?|Ms|Miss|Sir|Madame?|Mme)\\s?\\.?\\s*") .ifPresent(expandedEntity -> expandedEntity.apply("PII.12.0", "Expanded PII with salutation prefix", "Article 39(e)(3) of Regulation (EC) No 178/2002")); @@ -742,7 +742,7 @@ rule "PII.12.0: Expand PII entities with salutation prefix" rule "PII.12.1: Expand PII entities with salutation prefix" when FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $entityToExpand: TextEntity(type == "PII", anyMatch(textBefore, "\\b(Mrs?|Ms|Miss|Sir|Madame?|Mme)\\s?\\.?\\s*")) + $entityToExpand: TextEntity(type() == "PII", anyMatch(textBefore, "\\b(Mrs?|Ms|Miss|Sir|Madame?|Mme)\\s?\\.?\\s*")) then entityCreationService.byPrefixExpansionRegex($entityToExpand, "\\b(Mrs?|Ms|Miss|Sir|Madame?|Mme)\\s?\\.?\\s*") .ifPresent(expandedEntity -> expandedEntity.apply("PII.12.1", "Expanded PII with salutation prefix", "Article 39(e)(2) of Regulation (EC) No 178/2002")); @@ -800,7 +800,7 @@ rule "ETC.3.1: Redact logos (vertebrate study)" // Rule unit: ETC.4 rule "ETC.4.0: Redact dossier dictionary entries" when - $dossierRedaction: TextEntity(type == "dossier_redaction") + $dossierRedaction: TextEntity(type() == "dossier_redaction") then $dossierRedaction.redact("ETC.4.0", "Specification of impurity found", "Article 39(e)(3) of Regulation (EC) No 178/2002"); end @@ -810,7 +810,7 @@ rule "ETC.4.0: Redact dossier dictionary entries" rule "ETC.5.0: Ignore dossier_redaction entries if confidentiality is not 'confidential'" when not FileAttribute(label == "Confidentiality", value == "confidential") - $dossierRedaction: TextEntity(type == "dossier_redaction") + $dossierRedaction: TextEntity(type() == "dossier_redaction") then $dossierRedaction.ignore("ETC.5.0", "Ignore dossier redactions, when not confidential"); $dossierRedaction.getIntersectingNodes().forEach(node -> update(node)); @@ -869,7 +869,7 @@ rule "AI.0.0: Add all NER Entities of type CBI_author" nerEntities: NerEntities(hasEntitiesOfType("CBI_author")) then nerEntities.streamEntitiesOfType("CBI_author") - .forEach(nerEntity -> entityCreationService.byNerEntity(nerEntity, EntityType.RECOMMENDATION, document)); + .forEach(nerEntity -> entityCreationService.optionalByNerEntity(nerEntity, EntityType.RECOMMENDATION, document)); end @@ -893,7 +893,7 @@ rule "MAN.0.0: Apply manual resize redaction" not ManualResizeRedaction(annotationId == $id, requestDate.isBefore($requestDate)) $entityToBeResized: TextEntity(matchesAnnotationId($id)) then - manualChangesApplicationService.resizeEntityAndReinsert($entityToBeResized, $resizeRedaction); + manualChangesApplicationService.resize($entityToBeResized, $resizeRedaction); retract($resizeRedaction); update($entityToBeResized); $entityToBeResized.getIntersectingNodes().forEach(node -> update(node)); @@ -971,13 +971,12 @@ rule "MAN.3.0: Apply entity recategorization" when $recategorization: ManualRecategorization($id: annotationId, $type: type, $requestDate: requestDate) not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate)) - $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type != $type) + $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type() != $type) then $entityToBeRecategorized.getIntersectingNodes().forEach(node -> update(node)); - manualChangesApplicationService.recategorize($entityToBeRecategorized, $recategorization); + $entityToBeRecategorized.getManualOverwrite().addChange($recategorization); + update($entityToBeRecategorized); retract($recategorization); - // Entity is copied and inserted, so the old entity needs to be retracted to avoid duplication. - retract($entityToBeRecategorized); end rule "MAN.3.1: Apply entity recategorization of same type" @@ -985,7 +984,7 @@ rule "MAN.3.1: Apply entity recategorization of same type" when $recategorization: ManualRecategorization($id: annotationId, $type: type, $requestDate: requestDate) not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate)) - $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type == $type) + $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type() == $type) then $entityToBeRecategorized.getManualOverwrite().addChange($recategorization); retract($recategorization); @@ -1043,8 +1042,8 @@ rule "MAN.4.1: Apply legal basis change" rule "X.0.0: Remove Entity contained by Entity of same type" salience 65 when - $larger: TextEntity($type: type, $entityType: entityType, active()) - $contained: TextEntity(containedBy($larger), type == $type, entityType == $entityType, this != $larger, !hasManualChanges(), active()) + $larger: TextEntity($type: type(), $entityType: entityType, active()) + $contained: TextEntity(containedBy($larger), type() == $type, entityType == $entityType, this != $larger, !hasManualChanges(), active()) then $contained.remove("X.0.0", "remove Entity contained by Entity of same type"); retract($contained); @@ -1055,8 +1054,8 @@ rule "X.0.0: Remove Entity contained by Entity of same type" rule "X.1.0: Merge intersecting Entities of same type" salience 64 when - $first: TextEntity($type: type, $entityType: entityType, !resized(), active()) - $second: TextEntity(intersects($first), type == $type, entityType == $entityType, this != $first, !hasManualChanges(), active()) + $first: TextEntity($type: type(), $entityType: entityType, !resized(), active()) + $second: TextEntity(intersects($first), type() == $type, entityType == $entityType, this != $first, !hasManualChanges(), active()) then TextEntity mergedEntity = entityCreationService.mergeEntitiesOfSameType(List.of($first, $second), $type, $entityType, document); $first.remove("X.1.0", "merge intersecting Entities of same type"); @@ -1071,8 +1070,8 @@ rule "X.1.0: Merge intersecting Entities of same type" rule "X.2.0: Remove Entity of type ENTITY when contained by FALSE_POSITIVE" salience 64 when - $falsePositive: TextEntity($type: type, entityType == EntityType.FALSE_POSITIVE, active()) - $entity: TextEntity(containedBy($falsePositive), type == $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), !hasManualChanges(), active()) + $falsePositive: TextEntity($type: type(), entityType == EntityType.FALSE_POSITIVE, active()) + $entity: TextEntity(containedBy($falsePositive), type() == $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), !hasManualChanges(), active()) then $entity.getIntersectingNodes().forEach(node -> update(node)); $entity.remove("X.2.0", "remove Entity of type ENTITY when contained by FALSE_POSITIVE"); @@ -1084,8 +1083,8 @@ rule "X.2.0: Remove Entity of type ENTITY when contained by FALSE_POSITIVE" rule "X.3.0: Remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION" salience 64 when - $falseRecommendation: TextEntity($type: type, entityType == EntityType.FALSE_RECOMMENDATION, active()) - $recommendation: TextEntity(containedBy($falseRecommendation), type == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) + $falseRecommendation: TextEntity($type: type(), entityType == EntityType.FALSE_RECOMMENDATION, active()) + $recommendation: TextEntity(containedBy($falseRecommendation), type() == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) then $recommendation.remove("X.3.0", "remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION"); retract($recommendation); @@ -1096,8 +1095,8 @@ rule "X.3.0: Remove Entity of type RECOMMENDATION when contained by FALSE_RECOMM rule "X.4.0: Remove Entity of type RECOMMENDATION when text range equals ENTITY with same type" salience 256 when - $entity: TextEntity($type: type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) - $recommendation: TextEntity(getTextRange().equals($entity.getTextRange()), type == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) + $entity: TextEntity($type: type(), (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) + $recommendation: TextEntity(getTextRange().equals($entity.getTextRange()), type() == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) then $entity.addEngines($recommendation.getEngines()); $recommendation.remove("X.4.0", "remove Entity of type RECOMMENDATION when text range equals ENTITY with same type"); @@ -1119,8 +1118,8 @@ rule "X.5.0: Remove Entity of type RECOMMENDATION when intersected by ENTITY" rule "X.5.1: Remove Entity of type RECOMMENDATION when contained by RECOMMENDATION" salience 256 when - $entity: TextEntity($type: type, entityType == EntityType.RECOMMENDATION, active()) - $recommendation: TextEntity(containedBy($entity), type != $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) + $entity: TextEntity($type: type(), entityType == EntityType.RECOMMENDATION, active()) + $recommendation: TextEntity(containedBy($entity), type() != $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) then $recommendation.remove("X.5.1", "remove Entity of type RECOMMENDATION when contained by RECOMMENDATION"); retract($recommendation); @@ -1131,8 +1130,8 @@ rule "X.5.1: Remove Entity of type RECOMMENDATION when contained by RECOMMENDATI rule "X.6.0: Remove Entity of lower rank, when contained by by entity of type ENTITY" salience 32 when - $higherRank: TextEntity($type: type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) - $lowerRank: TextEntity(containedBy($higherRank), type != $type, dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), !hasManualChanges(), active()) + $higherRank: TextEntity($type: type(), (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) + $lowerRank: TextEntity(containedBy($higherRank), type() != $type, dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), !hasManualChanges(), active()) then $lowerRank.getIntersectingNodes().forEach(node -> update(node)); $lowerRank.remove("X.6.0", "remove Entity of lower rank, when contained by entity of type ENTITY"); @@ -1142,8 +1141,8 @@ rule "X.6.0: Remove Entity of lower rank, when contained by by entity of type EN rule "X.6.1: remove Entity of higher rank, when intersected by entity of type ENTITY and length of lower rank Entity is bigger than the higher rank Entity" salience 32 when - $higherRank: TextEntity($type: type, $value: value, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active(), !hasManualChanges()) - $lowerRank: TextEntity(intersects($higherRank), type != $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), active(), $lowerRank.getValue().length() > $value.length()) + $higherRank: TextEntity($type: type(), $value: value, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active(), !hasManualChanges()) + $lowerRank: TextEntity(intersects($higherRank), type() != $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), active(), $lowerRank.getValue().length() > $value.length()) then $higherRank.getIntersectingNodes().forEach(node -> update(node)); $higherRank.remove("X.6.1", "remove Entity of higher rank, when intersected by entity of type ENTITY and length of lower rank Entity is bigger than the higher rank Entity"); 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 index 61d808dd..e7e940d5 100644 --- 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 @@ -71,7 +71,7 @@ query "getFileAttributes" rule "CBI.0.0: Redact CBI Authors (non vertebrate Study)" when not FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $entity: TextEntity(type == "CBI_author", dictionaryEntry) + $entity: TextEntity(type() == "CBI_author", dictionaryEntry) then $entity.redact("CBI.0.0", "Author found", "Article 39(e)(3) of Regulation (EC) No 178/2002"); end @@ -83,7 +83,7 @@ rule "CBI.0.0: Redact CBI Authors (non vertebrate Study)" rule "PII.0.0: Redact all PII (non vertebrate study)" when not FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $pii: TextEntity(type == "PII", dictionaryEntry) + $pii: TextEntity(type() == "PII", dictionaryEntry) then $pii.redact("PII.0.0", "Personal Information found", "Article 39(e)(3) of Regulation (EC) No 178/2002"); end @@ -98,7 +98,7 @@ rule "AI.0.0: Add all NER Entities of type CBI_author" nerEntities: NerEntities(hasEntitiesOfType("CBI_author")) then nerEntities.streamEntitiesOfType("CBI_author") - .forEach(nerEntity -> entityCreationService.byNerEntity(nerEntity, EntityType.RECOMMENDATION, document)); + .forEach(nerEntity -> entityCreationService.optionalByNerEntity(nerEntity, EntityType.RECOMMENDATION, document)); end @@ -112,7 +112,7 @@ rule "MAN.0.0: Apply manual resize redaction" not ManualResizeRedaction(annotationId == $id, requestDate.isBefore($requestDate)) $entityToBeResized: TextEntity(matchesAnnotationId($id)) then - manualChangesApplicationService.resizeEntityAndReinsert($entityToBeResized, $resizeRedaction); + manualChangesApplicationService.resize($entityToBeResized, $resizeRedaction); retract($resizeRedaction); update($entityToBeResized); $entityToBeResized.getIntersectingNodes().forEach(node -> update(node)); @@ -190,13 +190,12 @@ rule "MAN.3.0: Apply entity recategorization" when $recategorization: ManualRecategorization($id: annotationId, $type: type, $requestDate: requestDate) not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate)) - $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type != $type) + $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type() != $type) then $entityToBeRecategorized.getIntersectingNodes().forEach(node -> update(node)); - manualChangesApplicationService.recategorize($entityToBeRecategorized, $recategorization); + $entityToBeRecategorized.getManualOverwrite().addChange($recategorization); + update($entityToBeRecategorized); retract($recategorization); - // Entity is copied and inserted, so the old entity needs to be retracted to avoid duplication. - retract($entityToBeRecategorized); end rule "MAN.3.1: Apply entity recategorization of same type" @@ -204,7 +203,7 @@ rule "MAN.3.1: Apply entity recategorization of same type" when $recategorization: ManualRecategorization($id: annotationId, $type: type, $requestDate: requestDate) not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate)) - $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type == $type) + $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type() == $type) then $entityToBeRecategorized.getManualOverwrite().addChange($recategorization); retract($recategorization); @@ -249,8 +248,8 @@ rule "MAN.4.1: Apply legal basis change" rule "X.0.0: Remove Entity contained by Entity of same type" salience 65 when - $larger: TextEntity($type: type, $entityType: entityType, active()) - $contained: TextEntity(containedBy($larger), type == $type, entityType == $entityType, this != $larger, !hasManualChanges(), active()) + $larger: TextEntity($type: type(), $entityType: entityType, active()) + $contained: TextEntity(containedBy($larger), type() == $type, entityType == $entityType, this != $larger, !hasManualChanges(), active()) then $contained.remove("X.0.0", "remove Entity contained by Entity of same type"); retract($contained); @@ -261,8 +260,8 @@ rule "X.0.0: Remove Entity contained by Entity of same type" rule "X.1.0: Merge intersecting Entities of same type" salience 64 when - $first: TextEntity($type: type, $entityType: entityType, !resized(), active()) - $second: TextEntity(intersects($first), type == $type, entityType == $entityType, this != $first, !hasManualChanges(), active()) + $first: TextEntity($type: type(), $entityType: entityType, !resized(), active()) + $second: TextEntity(intersects($first), type() == $type, entityType == $entityType, this != $first, !hasManualChanges(), active()) then TextEntity mergedEntity = entityCreationService.mergeEntitiesOfSameType(List.of($first, $second), $type, $entityType, document); $first.remove("X.1.0", "merge intersecting Entities of same type"); @@ -277,8 +276,8 @@ rule "X.1.0: Merge intersecting Entities of same type" rule "X.2.0: Remove Entity of type ENTITY when contained by FALSE_POSITIVE" salience 64 when - $falsePositive: TextEntity($type: type, entityType == EntityType.FALSE_POSITIVE, active()) - $entity: TextEntity(containedBy($falsePositive), type == $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), !hasManualChanges(), active()) + $falsePositive: TextEntity($type: type(), entityType == EntityType.FALSE_POSITIVE, active()) + $entity: TextEntity(containedBy($falsePositive), type() == $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), !hasManualChanges(), active()) then $entity.getIntersectingNodes().forEach(node -> update(node)); $entity.remove("X.2.0", "remove Entity of type ENTITY when contained by FALSE_POSITIVE"); @@ -290,8 +289,8 @@ rule "X.2.0: Remove Entity of type ENTITY when contained by FALSE_POSITIVE" rule "X.3.0: Remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION" salience 64 when - $falseRecommendation: TextEntity($type: type, entityType == EntityType.FALSE_RECOMMENDATION, active()) - $recommendation: TextEntity(containedBy($falseRecommendation), type == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) + $falseRecommendation: TextEntity($type: type(), entityType == EntityType.FALSE_RECOMMENDATION, active()) + $recommendation: TextEntity(containedBy($falseRecommendation), type() == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) then $recommendation.remove("X.3.0", "remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION"); retract($recommendation); @@ -302,8 +301,8 @@ rule "X.3.0: Remove Entity of type RECOMMENDATION when contained by FALSE_RECOMM rule "X.4.0: Remove Entity of type RECOMMENDATION when text range equals ENTITY with same type" salience 256 when - $entity: TextEntity($type: type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) - $recommendation: TextEntity(getTextRange().equals($entity.getTextRange()), type == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) + $entity: TextEntity($type: type(), (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) + $recommendation: TextEntity(getTextRange().equals($entity.getTextRange()), type() == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) then $entity.addEngines($recommendation.getEngines()); $recommendation.remove("X.4.0", "remove Entity of type RECOMMENDATION when text range equals ENTITY with same type"); @@ -325,8 +324,8 @@ rule "X.5.0: Remove Entity of type RECOMMENDATION when intersected by ENTITY" rule "X.5.1: Remove Entity of type RECOMMENDATION when contained by RECOMMENDATION" salience 256 when - $entity: TextEntity($type: type, entityType == EntityType.RECOMMENDATION, active()) - $recommendation: TextEntity(containedBy($entity), type != $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) + $entity: TextEntity($type: type(), entityType == EntityType.RECOMMENDATION, active()) + $recommendation: TextEntity(containedBy($entity), type() != $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) then $recommendation.remove("X.5.1", "remove Entity of type RECOMMENDATION when contained by RECOMMENDATION"); retract($recommendation); @@ -337,8 +336,8 @@ rule "X.5.1: Remove Entity of type RECOMMENDATION when contained by RECOMMENDATI rule "X.6.0: Remove Entity of lower rank, when contained by by entity of type ENTITY" salience 32 when - $higherRank: TextEntity($type: type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) - $lowerRank: TextEntity(containedBy($higherRank), type != $type, dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), !hasManualChanges(), active()) + $higherRank: TextEntity($type: type(), (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) + $lowerRank: TextEntity(containedBy($higherRank), type() != $type, dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), !hasManualChanges(), active()) then $lowerRank.getIntersectingNodes().forEach(node -> update(node)); $lowerRank.remove("X.6.0", "remove Entity of lower rank, when contained by entity of type ENTITY"); @@ -348,8 +347,8 @@ rule "X.6.0: Remove Entity of lower rank, when contained by by entity of type EN rule "X.6.1: remove Entity of higher rank, when intersected by entity of type ENTITY and length of lower rank Entity is bigger than the higher rank Entity" salience 32 when - $higherRank: TextEntity($type: type, $value: value, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active(), !hasManualChanges()) - $lowerRank: TextEntity(intersects($higherRank), type != $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), active(), $lowerRank.getValue().length() > $value.length()) + $higherRank: TextEntity($type: type(), $value: value, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active(), !hasManualChanges()) + $lowerRank: TextEntity(intersects($higherRank), type() != $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), active(), $lowerRank.getValue().length() > $value.length()) then $higherRank.getIntersectingNodes().forEach(node -> update(node)); $higherRank.remove("X.6.1", "remove Entity of higher rank, when intersected by entity of type ENTITY and length of lower rank Entity is bigger than the higher rank Entity"); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/table_demo.drl b/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/table_demo.drl index 888be2a8..73c345d6 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/table_demo.drl +++ b/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/table_demo.drl @@ -225,7 +225,7 @@ rule "MAN.0.0: Apply manual resize redaction" not ManualResizeRedaction(annotationId == $id, requestDate.isBefore($requestDate)) $entityToBeResized: TextEntity(matchesAnnotationId($id)) then - manualChangesApplicationService.resizeEntityAndReinsert($entityToBeResized, $resizeRedaction); + manualChangesApplicationService.resize($entityToBeResized, $resizeRedaction); retract($resizeRedaction); update($entityToBeResized); $entityToBeResized.getIntersectingNodes().forEach(node -> update(node)); @@ -303,13 +303,12 @@ rule "MAN.3.0: Apply entity recategorization" when $recategorization: ManualRecategorization($id: annotationId, $type: type, $requestDate: requestDate) not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate)) - $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type != $type) + $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type() != $type) then $entityToBeRecategorized.getIntersectingNodes().forEach(node -> update(node)); - manualChangesApplicationService.recategorize($entityToBeRecategorized, $recategorization); + $entityToBeRecategorized.getManualOverwrite().addChange($recategorization); + update($entityToBeRecategorized); retract($recategorization); - // Entity is copied and inserted, so the old entity needs to be retracted to avoid duplication. - retract($entityToBeRecategorized); end rule "MAN.3.1: Apply entity recategorization of same type" @@ -317,7 +316,7 @@ rule "MAN.3.1: Apply entity recategorization of same type" when $recategorization: ManualRecategorization($id: annotationId, $type: type, $requestDate: requestDate) not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate)) - $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type == $type) + $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type() == $type) then $entityToBeRecategorized.getManualOverwrite().addChange($recategorization); retract($recategorization); @@ -375,8 +374,8 @@ rule "MAN.4.1: Apply legal basis change" rule "X.0.0: Remove Entity contained by Entity of same type" salience 65 when - $larger: TextEntity($type: type, $entityType: entityType, active()) - $contained: TextEntity(containedBy($larger), type == $type, entityType == $entityType, this != $larger, !hasManualChanges(), active()) + $larger: TextEntity($type: type(), $entityType: entityType, active()) + $contained: TextEntity(containedBy($larger), type() == $type, entityType == $entityType, this != $larger, !hasManualChanges(), active()) then $contained.remove("X.0.0", "remove Entity contained by Entity of same type"); retract($contained); @@ -387,8 +386,8 @@ rule "X.0.0: Remove Entity contained by Entity of same type" rule "X.2.0: Remove Entity of type ENTITY when contained by FALSE_POSITIVE" salience 64 when - $falsePositive: TextEntity($type: type, entityType == EntityType.FALSE_POSITIVE, active()) - $entity: TextEntity(containedBy($falsePositive), type == $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), !hasManualChanges(), active()) + $falsePositive: TextEntity($type: type(), entityType == EntityType.FALSE_POSITIVE, active()) + $entity: TextEntity(containedBy($falsePositive), type() == $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), !hasManualChanges(), active()) then $entity.getIntersectingNodes().forEach(node -> update(node)); $entity.remove("X.2.0", "remove Entity of type ENTITY when contained by FALSE_POSITIVE"); @@ -400,8 +399,8 @@ rule "X.2.0: Remove Entity of type ENTITY when contained by FALSE_POSITIVE" rule "X.3.0: Remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION" salience 64 when - $falseRecommendation: TextEntity($type: type, entityType == EntityType.FALSE_RECOMMENDATION, active()) - $recommendation: TextEntity(containedBy($falseRecommendation), type == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) + $falseRecommendation: TextEntity($type: type(), entityType == EntityType.FALSE_RECOMMENDATION, active()) + $recommendation: TextEntity(containedBy($falseRecommendation), type() == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) then $recommendation.remove("X.3.0", "remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION"); retract($recommendation); @@ -412,8 +411,8 @@ rule "X.3.0: Remove Entity of type RECOMMENDATION when contained by FALSE_RECOMM rule "X.4.0: Remove Entity of type RECOMMENDATION when text range equals ENTITY with same type" salience 256 when - $entity: TextEntity($type: type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) - $recommendation: TextEntity(getTextRange().equals($entity.getTextRange()), type == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) + $entity: TextEntity($type: type(), (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) + $recommendation: TextEntity(getTextRange().equals($entity.getTextRange()), type() == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) then $entity.addEngines($recommendation.getEngines()); $recommendation.remove("X.4.0", "remove Entity of type RECOMMENDATION when text range equals ENTITY with same type"); @@ -435,8 +434,8 @@ rule "X.5.0: Remove Entity of type RECOMMENDATION when intersected by ENTITY" rule "X.5.1: Remove Entity of type RECOMMENDATION when contained by RECOMMENDATION" salience 256 when - $entity: TextEntity($type: type, entityType == EntityType.RECOMMENDATION, active()) - $recommendation: TextEntity(containedBy($entity), type != $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) + $entity: TextEntity($type: type(), entityType == EntityType.RECOMMENDATION, active()) + $recommendation: TextEntity(containedBy($entity), type() != $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) then $recommendation.remove("X.5.1", "remove Entity of type RECOMMENDATION when contained by RECOMMENDATION"); retract($recommendation); @@ -447,8 +446,8 @@ rule "X.5.1: Remove Entity of type RECOMMENDATION when contained by RECOMMENDATI rule "X.6.0: Remove Entity of lower rank, when contained by by entity of type ENTITY" salience 32 when - $higherRank: TextEntity($type: type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) - $lowerRank: TextEntity(containedBy($higherRank), type != $type, dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), !hasManualChanges(), active()) + $higherRank: TextEntity($type: type(), (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) + $lowerRank: TextEntity(containedBy($higherRank), type() != $type, dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), !hasManualChanges(), active()) then $lowerRank.getIntersectingNodes().forEach(node -> update(node)); $lowerRank.remove("X.6.0", "remove Entity of lower rank, when contained by entity of type ENTITY"); @@ -458,8 +457,8 @@ rule "X.6.0: Remove Entity of lower rank, when contained by by entity of type EN rule "X.6.1: remove Entity of higher rank, when intersected by entity of type ENTITY and length of lower rank Entity is bigger than the higher rank Entity" salience 32 when - $higherRank: TextEntity($type: type, $value: value, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active(), !hasManualChanges()) - $lowerRank: TextEntity(intersects($higherRank), type != $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), active(), $lowerRank.getValue().length() > $value.length()) + $higherRank: TextEntity($type: type(), $value: value, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active(), !hasManualChanges()) + $lowerRank: TextEntity(intersects($higherRank), type() != $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), active(), $lowerRank.getValue().length() > $value.length()) then $higherRank.getIntersectingNodes().forEach(node -> update(node)); $higherRank.remove("X.6.1", "remove Entity of higher rank, when intersected by entity of type ENTITY and length of lower rank Entity is bigger than the higher rank Entity"); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/test_rules.drl b/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/test_rules.drl index 62075dab..ab04310c 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/test_rules.drl +++ b/redaction-service-v1/redaction-service-server-v1/src/test/resources/drools/test_rules.drl @@ -125,7 +125,7 @@ rule "MAN.0.0: Apply manual resize redaction" not ManualResizeRedaction(annotationId == $id, requestDate.isBefore($requestDate)) $entityToBeResized: TextEntity(matchesAnnotationId($id)) then - manualChangesApplicationService.resizeEntityAndReinsert($entityToBeResized, $resizeRedaction); + manualChangesApplicationService.resize($entityToBeResized, $resizeRedaction); retract($resizeRedaction); update($entityToBeResized); $entityToBeResized.getIntersectingNodes().forEach(node -> update(node)); @@ -203,13 +203,12 @@ rule "MAN.3.0: Apply entity recategorization" when $recategorization: ManualRecategorization($id: annotationId, $type: type, $requestDate: requestDate) not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate)) - $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type != $type) + $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type() != $type) then $entityToBeRecategorized.getIntersectingNodes().forEach(node -> update(node)); - manualChangesApplicationService.recategorize($entityToBeRecategorized, $recategorization); + $entityToBeRecategorized.getManualOverwrite().addChange($recategorization); + update($entityToBeRecategorized); retract($recategorization); - // Entity is copied and inserted, so the old entity needs to be retracted to avoid duplication. - retract($entityToBeRecategorized); end rule "MAN.3.1: Apply entity recategorization of same type" @@ -217,7 +216,7 @@ rule "MAN.3.1: Apply entity recategorization of same type" when $recategorization: ManualRecategorization($id: annotationId, $type: type, $requestDate: requestDate) not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate)) - $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type == $type) + $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type() == $type) then $entityToBeRecategorized.getManualOverwrite().addChange($recategorization); retract($recategorization); @@ -275,8 +274,8 @@ rule "MAN.4.1: Apply legal basis change" rule "X.0.0: Remove Entity contained by Entity of same type" salience 65 when - $larger: TextEntity($type: type, $entityType: entityType, active()) - $contained: TextEntity(containedBy($larger), type == $type, entityType == $entityType, this != $larger, !hasManualChanges(), active()) + $larger: TextEntity($type: type(), $entityType: entityType, active()) + $contained: TextEntity(containedBy($larger), type() == $type, entityType == $entityType, this != $larger, !hasManualChanges(), active()) then $contained.remove("X.0.0", "remove Entity contained by Entity of same type"); retract($contained); @@ -287,8 +286,8 @@ rule "X.0.0: Remove Entity contained by Entity of same type" rule "X.1.0: Merge intersecting Entities of same type" salience 64 when - $first: TextEntity($type: type, $entityType: entityType, !resized(), active()) - $second: TextEntity(intersects($first), type == $type, entityType == $entityType, this != $first, !hasManualChanges(), active()) + $first: TextEntity($type: type(), $entityType: entityType, !resized(), active()) + $second: TextEntity(intersects($first), type() == $type, entityType == $entityType, this != $first, !hasManualChanges(), active()) then TextEntity mergedEntity = entityCreationService.mergeEntitiesOfSameType(List.of($first, $second), $type, $entityType, document); $first.remove("X.1.0", "merge intersecting Entities of same type"); @@ -303,8 +302,8 @@ rule "X.1.0: Merge intersecting Entities of same type" rule "X.2.0: Remove Entity of type ENTITY when contained by FALSE_POSITIVE" salience 64 when - $falsePositive: TextEntity($type: type, entityType == EntityType.FALSE_POSITIVE, active()) - $entity: TextEntity(containedBy($falsePositive), type == $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), !hasManualChanges(), active()) + $falsePositive: TextEntity($type: type(), entityType == EntityType.FALSE_POSITIVE, active()) + $entity: TextEntity(containedBy($falsePositive), type() == $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), !hasManualChanges(), active()) then $entity.getIntersectingNodes().forEach(node -> update(node)); $entity.remove("X.2.0", "remove Entity of type ENTITY when contained by FALSE_POSITIVE"); @@ -316,8 +315,8 @@ rule "X.2.0: Remove Entity of type ENTITY when contained by FALSE_POSITIVE" rule "X.3.0: Remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION" salience 64 when - $falseRecommendation: TextEntity($type: type, entityType == EntityType.FALSE_RECOMMENDATION, active()) - $recommendation: TextEntity(containedBy($falseRecommendation), type == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) + $falseRecommendation: TextEntity($type: type(), entityType == EntityType.FALSE_RECOMMENDATION, active()) + $recommendation: TextEntity(containedBy($falseRecommendation), type() == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) then $recommendation.remove("X.3.0", "remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION"); retract($recommendation); @@ -328,8 +327,8 @@ rule "X.3.0: Remove Entity of type RECOMMENDATION when contained by FALSE_RECOMM rule "X.4.0: Remove Entity of type RECOMMENDATION when text range equals ENTITY with same type" salience 256 when - $entity: TextEntity($type: type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) - $recommendation: TextEntity(getTextRange().equals($entity.getTextRange()), type == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) + $entity: TextEntity($type: type(), (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) + $recommendation: TextEntity(getTextRange().equals($entity.getTextRange()), type() == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) then $entity.addEngines($recommendation.getEngines()); $recommendation.remove("X.4.0", "remove Entity of type RECOMMENDATION when text range equals ENTITY with same type"); @@ -351,8 +350,8 @@ rule "X.5.0: Remove Entity of type RECOMMENDATION when intersected by ENTITY" rule "X.5.1: Remove Entity of type RECOMMENDATION when contained by RECOMMENDATION" salience 256 when - $entity: TextEntity($type: type, entityType == EntityType.RECOMMENDATION, active()) - $recommendation: TextEntity(containedBy($entity), type != $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) + $entity: TextEntity($type: type(), entityType == EntityType.RECOMMENDATION, active()) + $recommendation: TextEntity(containedBy($entity), type() != $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) then $recommendation.remove("X.5.1", "remove Entity of type RECOMMENDATION when contained by RECOMMENDATION"); retract($recommendation); @@ -363,8 +362,8 @@ rule "X.5.1: Remove Entity of type RECOMMENDATION when contained by RECOMMENDATI rule "X.6.0: Remove Entity of lower rank, when contained by by entity of type ENTITY" salience 32 when - $higherRank: TextEntity($type: type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) - $lowerRank: TextEntity(containedBy($higherRank), type != $type, dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), !hasManualChanges(), active()) + $higherRank: TextEntity($type: type(), (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) + $lowerRank: TextEntity(containedBy($higherRank), type() != $type, dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), !hasManualChanges(), active()) then $lowerRank.getIntersectingNodes().forEach(node -> update(node)); $lowerRank.remove("X.6.0", "remove Entity of lower rank, when contained by entity of type ENTITY"); @@ -374,8 +373,8 @@ rule "X.6.0: Remove Entity of lower rank, when contained by by entity of type EN rule "X.6.1: remove Entity of higher rank, when intersected by entity of type ENTITY and length of lower rank Entity is bigger than the higher rank Entity" salience 32 when - $higherRank: TextEntity($type: type, $value: value, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active(), !hasManualChanges()) - $lowerRank: TextEntity(intersects($higherRank), type != $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), active(), $lowerRank.getValue().length() > $value.length()) + $higherRank: TextEntity($type: type(), $value: value, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active(), !hasManualChanges()) + $lowerRank: TextEntity(intersects($higherRank), type() != $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), active(), $lowerRank.getValue().length() > $value.length()) then $higherRank.getIntersectingNodes().forEach(node -> update(node)); $higherRank.remove("X.6.1", "remove Entity of higher rank, when intersected by entity of type ENTITY and length of lower rank Entity is bigger than the higher rank Entity"); diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/resources/files/Minimal Examples/Meto1_Page22.pdf b/redaction-service-v1/redaction-service-server-v1/src/test/resources/files/Minimal Examples/Meto1_Page22.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e45c3042c2a6ac0c6f7dba92582f224d5c49b6aa GIT binary patch literal 196360 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 literal 0 HcmV?d00001 diff --git a/redaction-service-v1/redaction-service-server-v1/src/test/resources/performance/dictionaries/EFSA_sanitisation_GFL_v1/rules.drl b/redaction-service-v1/redaction-service-server-v1/src/test/resources/performance/dictionaries/EFSA_sanitisation_GFL_v1/rules.drl index 1d96d377..8b28390c 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/test/resources/performance/dictionaries/EFSA_sanitisation_GFL_v1/rules.drl +++ b/redaction-service-v1/redaction-service-server-v1/src/test/resources/performance/dictionaries/EFSA_sanitisation_GFL_v1/rules.drl @@ -81,7 +81,7 @@ rule "SYN.1.0: Recommend CTL/BL laboratory that start with BL or CTL" rule "CBI.0.0: Redact CBI Authors (Non Vertebrate Study)" when not FileAttribute(label == "Vertebrate Study", value.toLowerCase() == "yes") - $entity: TextEntity(type == "CBI_author", dictionaryEntry) + $entity: TextEntity(type() == "CBI_author", dictionaryEntry) then $entity.apply("CBI.0.0", "Author found", "Article 39(e)(3) of Regulation (EC) No 178/2002"); end @@ -89,7 +89,7 @@ rule "CBI.0.0: Redact CBI Authors (Non Vertebrate Study)" rule "CBI.0.1: Redact CBI Authors (Vertebrate Study)" when FileAttribute(label == "Vertebrate Study", value.toLowerCase() == "yes") - $entity: TextEntity(type == "CBI_author", dictionaryEntry) + $entity: TextEntity(type() == "CBI_author", dictionaryEntry) then $entity.apply("CBI.0.1", "Author found", "Article 39(e)(2) of Regulation (EC) No 178/2002"); end @@ -99,7 +99,7 @@ rule "CBI.0.1: Redact CBI Authors (Vertebrate Study)" rule "CBI.1.0: Don't redact CBI Address (Non Vertebrate Study)" when not FileAttribute(label == "Vertebrate Study", value.toLowerCase() == "yes") - $entity: TextEntity(type == "CBI_address", dictionaryEntry) + $entity: TextEntity(type() == "CBI_address", dictionaryEntry) then $entity.skip("CBI.1.0", "Address found for Non Vertebrate Study"); end @@ -107,7 +107,7 @@ rule "CBI.1.0: Don't redact CBI Address (Non Vertebrate Study)" rule "CBI.1.1: Redact CBI Address (Vertebrate Study)" when FileAttribute(label == "Vertebrate Study", value.toLowerCase() == "yes") - $entity: TextEntity(type == "CBI_address", dictionaryEntry) + $entity: TextEntity(type() == "CBI_address", dictionaryEntry) then $entity.apply("CBI.1.1", "Address found", "Article 39(e)(2) of Regulation (EC) No 178/2002"); end @@ -116,7 +116,7 @@ rule "CBI.1.1: Redact CBI Address (Vertebrate Study)" // Rule unit: CBI.2 rule "CBI.2.0: Don't redact genitive CBI_author" when - $entity: TextEntity(type == "CBI_author", anyMatch(textAfter, "['’’'ʼˈ´`‘′ʻ’']s"), applied()) + $entity: TextEntity(type() == "CBI_author", anyMatch(textAfter, "['’’'ʼˈ´`‘′ʻ’']s"), applied()) then entityCreationService.byTextRange($entity.getTextRange(), "CBI_author", EntityType.FALSE_POSITIVE, document) .ifPresent(falsePositive -> falsePositive.skip("CBI.2.0", "Genitive Author found")); @@ -145,7 +145,7 @@ rule "CBI.7.1: Do not redact Names and Addresses if published information found $table: Table(hasEntitiesOfType("published_information"), hasEntitiesOfType("CBI_author")) $cellsWithPublishedInformation: TableCell() from $table.streamTableCellsWhichContainType("published_information").toList() $tableCell: TableCell(row == $cellsWithPublishedInformation.row) from $table.streamTableCells().toList() - $author: TextEntity(type == "CBI_author", active()) from $tableCell.getEntities() + $author: TextEntity(type() == "CBI_author", active()) from $tableCell.getEntities() then $author.skipWithReferences("CBI.7.1", "Published Information found in row", $table.getEntitiesOfTypeInSameRow("published_information", $author)); end @@ -298,7 +298,7 @@ rule "CBI.20.1: Redact between \"PERFORMING LABORATORY\" and \"LABORATORY PROJEC rule "PII.0.0: Redact all PII (non vertebrate study)" when not FileAttribute(label == "Vertebrate Study", value.toLowerCase() == "yes") - $pii: TextEntity(type == "PII", dictionaryEntry) + $pii: TextEntity(type() == "PII", dictionaryEntry) then $pii.apply("PII.0.0", "Personal Information found", "Article 39(e)(3) of Regulation (EC) No 178/2002"); end @@ -306,7 +306,7 @@ rule "PII.0.0: Redact all PII (non vertebrate study)" rule "PII.0.1: Redact all PII (vertebrate study)" when FileAttribute(label == "Vertebrate Study", value.toLowerCase() == "yes") - $pii: TextEntity(type == "PII", dictionaryEntry) + $pii: TextEntity(type() == "PII", dictionaryEntry) then $pii.apply("PII.0.1", "Personal Information found", "Article 39(e)(2) of Regulation (EC) No 178/2002"); end @@ -452,7 +452,7 @@ rule "ETC.3.1: Redact logos (non vertebrate study)" rule "ETC.5.0: Ignore dossier_redaction entries if confidentiality is not 'confidential'" when not FileAttribute(label == "Confidentiality", value == "confidential") - $dossierRedaction: TextEntity(type == "dossier_redaction") + $dossierRedaction: TextEntity(type() == "dossier_redaction") then $dossierRedaction.ignore("ETC.5.0", "Ignore dossier redactions, when not confidential"); update($dossierRedaction); @@ -469,7 +469,7 @@ rule "AI.0.0: add all NER Entities of type CBI_author" nerEntities: NerEntities(hasEntitiesOfType("CBI_author")) then nerEntities.streamEntitiesOfType("CBI_author") - .forEach(nerEntity -> entityCreationService.byNerEntity(nerEntity, EntityType.RECOMMENDATION, document)); + .forEach(nerEntity -> entityCreationService.optionalByNerEntity(nerEntity, EntityType.RECOMMENDATION, document)); end @@ -494,7 +494,7 @@ rule "MAN.0.0: Apply manual resize redaction" $resizeRedaction: ManualResizeRedaction($id: annotationId) $entityToBeResized: TextEntity(matchesAnnotationId($id)) then - manualChangesApplicationService.resizeEntityAndReinsert($entityToBeResized, $resizeRedaction); + manualChangesApplicationService.resize($entityToBeResized, $resizeRedaction); retract($resizeRedaction); update($entityToBeResized); $entityToBeResized.getIntersectingNodes().forEach(node -> update(node)); @@ -575,8 +575,7 @@ rule "MAN.3.0: Apply entity recategorization" $entityToBeRecategorized.getIntersectingNodes().forEach(node -> update(node)); manualChangesApplicationService.recategorize($entityToBeRecategorized, $recategorization); retract($recategorization); - // Entity is copied and inserted, so the old entity needs to be retracted to avoid duplication. - retract($entityToBeRecategorized); + update($entityToBeRecategorized); end rule "MAN.3.1: Apply image recategorization" @@ -617,8 +616,8 @@ rule "MAN.4.1: Apply legal basis change" rule "X.0.0: remove Entity contained by Entity of same type" salience 65 when - $larger: TextEntity($type: type, $entityType: entityType, active()) - $contained: TextEntity(containedBy($larger), type == $type, entityType == $entityType, this != $larger, !resized(), active()) + $larger: TextEntity($type: type(), $entityType: entityType, active()) + $contained: TextEntity(containedBy($larger), type() == $type, entityType == $entityType, this != $larger, !resized(), active()) then $contained.remove("X.0.0", "remove Entity contained by Entity of same type"); retract($contained); @@ -629,8 +628,8 @@ rule "X.0.0: remove Entity contained by Entity of same type" rule "X.1.0: merge intersecting Entities of same type" salience 64 when - $first: TextEntity($type: type, $entityType: entityType, !resized(), active()) - $second: TextEntity(intersects($first), type == $type, entityType == $entityType, this != $first, !resized(), active()) + $first: TextEntity($type: type(), $entityType: entityType, !resized(), active()) + $second: TextEntity(intersects($first), type() == $type, entityType == $entityType, this != $first, !resized(), active()) then TextEntity mergedEntity = entityCreationService.mergeEntitiesOfSameType(List.of($first, $second), $type, $entityType, document); $first.remove("X.1.0", "merge intersecting Entities of same type"); @@ -645,8 +644,8 @@ rule "X.1.0: merge intersecting Entities of same type" rule "X.2.0: remove Entity of type ENTITY when contained by FALSE_POSITIVE" salience 64 when - $falsePositive: TextEntity($type: type, entityType == EntityType.FALSE_POSITIVE, active()) - $entity: TextEntity(containedBy($falsePositive), type == $type, entityType == EntityType.ENTITY, !resized(), active()) + $falsePositive: TextEntity($type: type(), entityType == EntityType.FALSE_POSITIVE, active()) + $entity: TextEntity(containedBy($falsePositive), type() == $type, entityType == EntityType.ENTITY, !resized(), active()) then $entity.getIntersectingNodes().forEach(node -> update(node)); $entity.remove("X.2.0", "remove Entity of type ENTITY when contained by FALSE_POSITIVE"); @@ -658,8 +657,8 @@ rule "X.2.0: remove Entity of type ENTITY when contained by FALSE_POSITIVE" rule "X.3.0: remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION" salience 64 when - $falseRecommendation: TextEntity($type: type, entityType == EntityType.FALSE_RECOMMENDATION, active()) - $recommendation: TextEntity(containedBy($falseRecommendation), type == $type, entityType == EntityType.RECOMMENDATION, !resized(), active()) + $falseRecommendation: TextEntity($type: type(), entityType == EntityType.FALSE_RECOMMENDATION, active()) + $recommendation: TextEntity(containedBy($falseRecommendation), type() == $type, entityType == EntityType.RECOMMENDATION, !resized(), active()) then $recommendation.remove("X.3.0", "remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION"); retract($recommendation); @@ -670,8 +669,8 @@ rule "X.3.0: remove Entity of type RECOMMENDATION when contained by FALSE_RECOMM rule "X.4.0: remove Entity of type RECOMMENDATION when intersected by ENTITY with same type" salience 256 when - $entity: TextEntity($type: type, entityType == EntityType.ENTITY, active()) - $recommendation: TextEntity(intersects($entity), type == $type, entityType == EntityType.RECOMMENDATION, !resized(), active()) + $entity: TextEntity($type: type(), entityType == EntityType.ENTITY, active()) + $recommendation: TextEntity(intersects($entity), type() == $type, entityType == EntityType.RECOMMENDATION, !resized(), active()) then $entity.addEngines($recommendation.getEngines()); $recommendation.remove("X.4.0", "remove Entity of type RECOMMENDATION when intersected by ENTITY with same type"); @@ -695,8 +694,8 @@ rule "X.5.0: remove Entity of type RECOMMENDATION when contained by ENTITY" rule "X.6.0: remove Entity of lower rank, when intersected by entity of type ENTITY" salience 32 when - $higherRank: TextEntity($type: type, entityType == EntityType.ENTITY, active()) - $lowerRank: TextEntity(intersects($higherRank), type != $type, dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), !resized(), active()) + $higherRank: TextEntity($type: type(), entityType == EntityType.ENTITY, active()) + $lowerRank: TextEntity(intersects($higherRank), type() != $type, dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), !resized(), active()) then $lowerRank.getIntersectingNodes().forEach(node -> update(node)); $lowerRank.remove("X.6.0", "remove Entity of lower rank, when intersected by entity of type ENTITY"); diff --git a/redaction-service-v1/rules-management/src/main/resources/all_redact_manager_rules.drl b/redaction-service-v1/rules-management/src/main/resources/all_redact_manager_rules.drl index c885ae6f..7214d931 100644 --- a/redaction-service-v1/rules-management/src/main/resources/all_redact_manager_rules.drl +++ b/redaction-service-v1/rules-management/src/main/resources/all_redact_manager_rules.drl @@ -97,7 +97,7 @@ rule "SYN.1.0: Recommend CTL/BL laboratory that start with BL or CTL" rule "CBI.0.0: Redact CBI Authors (non vertebrate Study)" when not FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $entity: TextEntity(type == "CBI_author", dictionaryEntry) + $entity: TextEntity(type() == "CBI_author", dictionaryEntry) then $entity.redact("CBI.0.0", "Author found", "Article 39(e)(3) of Regulation (EC) No 178/2002"); end @@ -105,7 +105,7 @@ rule "CBI.0.0: Redact CBI Authors (non vertebrate Study)" rule "CBI.0.1: Redact CBI Authors (vertebrate Study)" when FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $entity: TextEntity(type == "CBI_author", dictionaryEntry) + $entity: TextEntity(type() == "CBI_author", dictionaryEntry) then $entity.redact("CBI.0.1", "Author found", "Article 39(e)(2) of Regulation (EC) No 178/2002"); end @@ -115,7 +115,7 @@ rule "CBI.0.1: Redact CBI Authors (vertebrate Study)" rule "CBI.1.0: Do not redact CBI Address (non vertebrate Study)" when not FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $entity: TextEntity(type == "CBI_address", dictionaryEntry) + $entity: TextEntity(type() == "CBI_address", dictionaryEntry) then $entity.skip("CBI.1.0", "Address found for Non Vertebrate Study"); end @@ -123,7 +123,7 @@ rule "CBI.1.0: Do not redact CBI Address (non vertebrate Study)" rule "CBI.1.1: Redact CBI Address (vertebrate Study)" when FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $entity: TextEntity(type == "CBI_address", dictionaryEntry) + $entity: TextEntity(type() == "CBI_address", dictionaryEntry) then $entity.redact("CBI.1.1", "Address found", "Article 39(e)(2) of Regulation (EC) No 178/2002"); end @@ -132,7 +132,7 @@ rule "CBI.1.1: Redact CBI Address (vertebrate Study)" // Rule unit: CBI.2 rule "CBI.2.0: Do not redact genitive CBI Author" when - $entity: TextEntity(type == "CBI_author", anyMatch(textAfter, "['’’'ʼˈ´`‘′ʻ’']s")) + $entity: TextEntity(type() == "CBI_author", anyMatch(textAfter, "['’’'ʼˈ´`‘′ʻ’']s")) then entityCreationService.byTextRange($entity.getTextRange(), "CBI_author", EntityType.FALSE_POSITIVE, document) .ifPresent(falsePositive -> falsePositive.skip("CBI.2.0", "Genitive Author found")); @@ -331,7 +331,7 @@ rule "CBI.7.1: Do not redact Names and Addresses if published information found $table: Table(hasEntitiesOfType("published_information"), hasEntitiesOfType("CBI_author") || hasEntitiesOfType("CBI_address")) $cellsWithPublishedInformation: TableCell() from $table.streamTableCellsWhichContainType("published_information").toList() $tableCell: TableCell(row == $cellsWithPublishedInformation.row) from $table.streamTableCells().toList() - $authorOrAddress: TextEntity(type == "CBI_author" || type == "CBI_address", active()) from $tableCell.getEntities() + $authorOrAddress: TextEntity(type() == "CBI_author" || type() == "CBI_address", active()) from $tableCell.getEntities() then $authorOrAddress.skipWithReferences("CBI.7.1", "Published Information found in row", $table.getEntitiesOfTypeInSameRow("published_information", $authorOrAddress)); end @@ -493,7 +493,7 @@ rule "CBI.12.2: Skip TableCell with header 'Author' or 'Author(s)' and header 'V rule "CBI.13.0: Ignore CBI Address recommendations" when not FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $entity: TextEntity(type == "CBI_address", entityType == EntityType.RECOMMENDATION) + $entity: TextEntity(type() == "CBI_address", entityType == EntityType.RECOMMENDATION) then $entity.ignore("CBI.13.0", "Ignore CBI Address Recommendations"); retract($entity) @@ -503,7 +503,7 @@ rule "CBI.13.0: Ignore CBI Address recommendations" // Rule unit: CBI.14 rule "CBI.14.0: Redact CBI_sponsor entities if preceded by \"batches produced at\"" when - $sponsorEntity: TextEntity(type == "CBI_sponsor", textBefore.contains("batches produced at")) + $sponsorEntity: TextEntity(type() == "CBI_sponsor", textBefore.contains("batches produced at")) then $sponsorEntity.redact("CBI.14.0", "Redacted because it represents a sponsor company", "Reg (EC) No 1107/2009 Art. 63 (2g)"); end @@ -606,7 +606,7 @@ rule "CBI.17.1: Add recommendation for Addresses in Test Organism sections, with rule "CBI.18.0: Expand CBI_author entities with firstname initials" no-loop true when - $entityToExpand: TextEntity(type == "CBI_author", + $entityToExpand: TextEntity(type() == "CBI_author", value.matches("[^\\s]+"), textAfter.startsWith(" "), anyMatch(textAfter, "(,? [A-Z]\\.?( ?[A-Z]\\.?)?( ?[A-Z]\\.?)?\\b\\.?)") @@ -624,7 +624,7 @@ rule "CBI.18.0: Expand CBI_author entities with firstname initials" // Rule unit: CBI.19 rule "CBI.19.0: Expand CBI_author entities with salutation prefix" when - $entityToExpand: TextEntity(type == "CBI_author", anyMatch(textBefore, "\\b(Mrs?|Ms|Miss|Sir|Madame?|Mme)\\s?\\.?\\s*")) + $entityToExpand: TextEntity(type() == "CBI_author", anyMatch(textBefore, "\\b(Mrs?|Ms|Miss|Sir|Madame?|Mme)\\s?\\.?\\s*")) then entityCreationService.byPrefixExpansionRegex($entityToExpand, "\\b(Mrs?|Ms|Miss|Sir|Madame?|Mme)\\s?\\.?\\s*") .ifPresent(expandedEntity -> { @@ -667,7 +667,7 @@ rule "CBI.21.0: Redact short Authors section (non vertebrate study)" when not FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") $section: Section(containsAnyStringIgnoreCase("author(s)", "author", "authors"), length() < 50, getTreeId().get(0) <= 20) //TODO: evaluate the reason of this rule - not TextEntity(type == "CBI_author", engines contains Engine.NER) from $section.getEntities() + not TextEntity(type() == "CBI_author", engines contains Engine.NER) from $section.getEntities() then entityCreationService.byRegexIgnoreCase("(?<=author\\(?s\\)?\\s\\n?)([\\p{Lu}\\p{L} ]{5,15}(,|\\n)?){1,3}", "CBI_author", EntityType.ENTITY, $section) .forEach(entity -> { @@ -679,7 +679,7 @@ rule "CBI.21.1: Redact short Authors section (vertebrate study)" when FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") $section: Section(containsAnyStringIgnoreCase("author(s)", "author", "authors"), length() < 50, getTreeId().get(0) <= 20) //TODO: evaluate the reason of this rule - not TextEntity(type == "CBI_author", engines contains Engine.NER) from $section.getEntities() + not TextEntity(type() == "CBI_author", engines contains Engine.NER) from $section.getEntities() then entityCreationService.byRegexIgnoreCase("(?<=author\\(?s\\)?\\s\\n?)([\\p{Lu}\\p{L} ]{5,15}(,|\\n)?){1,3}", "CBI_author", EntityType.ENTITY, $section) .forEach(entity -> { @@ -706,7 +706,7 @@ rule "CBI.22.0: Redact Addresses in Reference Tables for vertebrate studies in n rule "PII.0.0: Redact all PII (non vertebrate study)" when not FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $pii: TextEntity(type == "PII", dictionaryEntry) + $pii: TextEntity(type() == "PII", dictionaryEntry) then $pii.redact("PII.0.0", "Personal Information found", "Article 39(e)(3) of Regulation (EC) No 178/2002"); end @@ -714,7 +714,7 @@ rule "PII.0.0: Redact all PII (non vertebrate study)" rule "PII.0.1: Redact all PII (vertebrate study)" when FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $pii: TextEntity(type == "PII", dictionaryEntry) + $pii: TextEntity(type() == "PII", dictionaryEntry) then $pii.redact("PII.0.1", "Personal Information found", "Article 39(e)(2) of Regulation (EC) No 178/2002"); end @@ -1045,7 +1045,7 @@ rule "PII.11.0: Redact On behalf of Sequani Ltd.:" rule "PII.12.0: Expand PII entities with salutation prefix" when not FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $entityToExpand: TextEntity(type == "PII", anyMatch(textBefore, "\\b(Mrs?|Ms|Miss|Sir|Madame?|Mme)\\s?\\.?\\s*")) + $entityToExpand: TextEntity(type() == "PII", anyMatch(textBefore, "\\b(Mrs?|Ms|Miss|Sir|Madame?|Mme)\\s?\\.?\\s*")) then entityCreationService.byPrefixExpansionRegex($entityToExpand, "\\b(Mrs?|Ms|Miss|Sir|Madame?|Mme)\\s?\\.?\\s*") .ifPresent(expandedEntity -> expandedEntity.apply("PII.12.0", "Expanded PII with salutation prefix", "Article 39(e)(3) of Regulation (EC) No 178/2002")); @@ -1056,7 +1056,7 @@ rule "PII.12.0: Expand PII entities with salutation prefix" rule "PII.12.1: Expand PII entities with salutation prefix" when FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $entityToExpand: TextEntity(type == "PII", anyMatch(textBefore, "\\b(Mrs?|Ms|Miss|Sir|Madame?|Mme)\\s?\\.?\\s*")) + $entityToExpand: TextEntity(type() == "PII", anyMatch(textBefore, "\\b(Mrs?|Ms|Miss|Sir|Madame?|Mme)\\s?\\.?\\s*")) then entityCreationService.byPrefixExpansionRegex($entityToExpand, "\\b(Mrs?|Ms|Miss|Sir|Madame?|Mme)\\s?\\.?\\s*") .ifPresent(expandedEntity -> expandedEntity.apply("PII.12.1", "Expanded PII with salutation prefix", "Article 39(e)(2) of Regulation (EC) No 178/2002")); @@ -1142,21 +1142,21 @@ rule "ETC.3.1: Redact logos (vertebrate study)" // Rule unit: ETC.4 rule "ETC.4.0: Redact dossier dictionary entries" when - $dossierRedaction: TextEntity(type == "dossier_redaction") + $dossierRedaction: TextEntity(type() == "dossier_redaction") then $dossierRedaction.redact("ETC.4.0", "Specification of impurity found", "Article 39(e)(3) of Regulation (EC) No 178/2002"); end rule "ETC.4.1: Redact dossier dictionary entries" when - $dossierRedaction: TextEntity(type == "dossier_redaction") + $dossierRedaction: TextEntity(type() == "dossier_redaction") then $dossierRedaction.redact("ETC.4.1", "Dossier Redaction found", "Article 39(1)(2) of Regulation (EC) No 178/2002"); end rule "ETC.4.2: Redact dossier dictionary entries" when - $dossierRedaction: TextEntity(type == "dossier_redaction") + $dossierRedaction: TextEntity(type() == "dossier_redaction") then $dossierRedaction.redact("ETC.4.2", "Dossier redaction found", "Article 63(2)(a) of Regulation (EC) No 1107/2009 (making reference to Article 39 of Regulation EC No 178/2002)"); end @@ -1166,7 +1166,7 @@ rule "ETC.4.2: Redact dossier dictionary entries" rule "ETC.5.0: Ignore dossier_redaction entries if confidentiality is not 'confidential'" when not FileAttribute(label == "Confidentiality", value == "confidential") - $dossierRedaction: TextEntity(type == "dossier_redaction") + $dossierRedaction: TextEntity(type() == "dossier_redaction") then $dossierRedaction.ignore("ETC.5.0", "Ignore dossier redactions, when not confidential"); $dossierRedaction.getIntersectingNodes().forEach(node -> update(node)); @@ -1219,7 +1219,7 @@ rule "ETC.8.1: Redact formulas (non vertebrate study)" rule "ETC.9.0: Redact skipped impurities" when FileAttribute(label == "Redact Skipped Impurities", value soundslike "Yes" || value.toLowerCase() == "y") - $skippedImpurities: TextEntity(type == "skipped_impurities") + $skippedImpurities: TextEntity(type() == "skipped_impurities") then $skippedImpurities.redact("ETC.9.0", "Occasional Impurity found", "Article 63(2)(b) of Regulation (EC) No 1107/2009"); end @@ -1227,7 +1227,7 @@ rule "ETC.9.0: Redact skipped impurities" rule "ETC.9.1: Redact impurities" when FileAttribute(label == "Redact Impurities", value soundslike "Yes" || value.toLowerCase() == "y") - $skippedImpurities: TextEntity(type == "impurities") + $skippedImpurities: TextEntity(type() == "impurities") then $skippedImpurities.redact("ETC.9.1", "Impurity found", "Article 63(2)(b) of Regulation (EC) No 1107/2009"); end @@ -1235,7 +1235,7 @@ rule "ETC.9.1: Redact impurities" // Rule unit: ETC.10 rule "ETC.10.0: Redact Product Composition Information" when - $compositionInformation: TextEntity(type == "product_composition") + $compositionInformation: TextEntity(type() == "product_composition") then $compositionInformation.redact("ETC.10.0", "Product Composition Information found", "Article 63(2)(d) of Regulation (EC) No 1107/2009"); end @@ -1256,7 +1256,7 @@ rule "ETC.11.0: Recommend first line in table cell with name and address of owne rule "ETC.12.0: Redact dossier_redaction (Non vertebrate study)" when not FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $dossierRedaction: TextEntity(type == "dossier_redaction") + $dossierRedaction: TextEntity(type() == "dossier_redaction") then $dossierRedaction.redact("ETC.12.0", "Dossier dictionary entry found", "Article 39(e)(3) of Regulation (EC) No 178/2002"); end @@ -1264,7 +1264,7 @@ rule "ETC.12.0: Redact dossier_redaction (Non vertebrate study)" rule "ETC.12.1: Redact dossier_redaction (Vertebrate study)" when FileAttribute(label == "Vertebrate Study", value soundslike "Yes" || value.toLowerCase() == "y") - $dossierRedaction: TextEntity(type == "dossier_redaction") + $dossierRedaction: TextEntity(type() == "dossier_redaction") then $dossierRedaction.redact("ETC.12.1", "Dossier dictionary entry found", "Article 39(e)(2) of Regulation (EC) No 178/2002"); end @@ -1278,7 +1278,7 @@ rule "AI.0.0: Add all NER Entities of type CBI_author" nerEntities: NerEntities(hasEntitiesOfType("CBI_author")) then nerEntities.streamEntitiesOfType("CBI_author") - .forEach(nerEntity -> entityCreationService.byNerEntity(nerEntity, EntityType.RECOMMENDATION, document)); + .forEach(nerEntity -> entityCreationService.optionalByNerEntity(nerEntity, EntityType.RECOMMENDATION, document)); end @@ -1300,7 +1300,7 @@ rule "AI.2.0: Add all NER Entities of any type except CBI_author" then nerEntities.getNerEntityList().stream() .filter(nerEntity -> !nerEntity.type().equals("CBI_author")) - .forEach(nerEntity -> entityCreationService.byNerEntity(nerEntity, nerEntity.type().toLowerCase(), EntityType.RECOMMENDATION, document)); + .forEach(nerEntity -> entityCreationService.optionalByNerEntity(nerEntity, nerEntity.type().toLowerCase(), EntityType.RECOMMENDATION, document)); end @@ -1311,7 +1311,7 @@ rule "AI.3.0: Recommend authors from AI as PII" nerEntities: NerEntities(hasEntitiesOfType("CBI_author")) then nerEntities.streamEntitiesOfType("CBI_author") - .forEach(nerEntity -> entityCreationService.byNerEntity(nerEntity, "PII", EntityType.RECOMMENDATION, document)); + .forEach(nerEntity -> entityCreationService.optionalByNerEntity(nerEntity, "PII", EntityType.RECOMMENDATION, document)); end //------------------------------------ Manual redaction rules ------------------------------------ @@ -1324,7 +1324,7 @@ rule "MAN.0.0: Apply manual resize redaction" not ManualResizeRedaction(annotationId == $id, requestDate.isBefore($requestDate)) $entityToBeResized: TextEntity(matchesAnnotationId($id)) then - manualChangesApplicationService.resizeEntityAndReinsert($entityToBeResized, $resizeRedaction); + manualChangesApplicationService.resize($entityToBeResized, $resizeRedaction); retract($resizeRedaction); update($entityToBeResized); $entityToBeResized.getIntersectingNodes().forEach(node -> update(node)); @@ -1402,13 +1402,12 @@ rule "MAN.3.0: Apply entity recategorization" when $recategorization: ManualRecategorization($id: annotationId, $type: type, $requestDate: requestDate) not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate)) - $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type != $type) + $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type() != $type) then $entityToBeRecategorized.getIntersectingNodes().forEach(node -> update(node)); - manualChangesApplicationService.recategorize($entityToBeRecategorized, $recategorization); + $entityToBeRecategorized.getManualOverwrite().addChange($recategorization); + update($entityToBeRecategorized); retract($recategorization); - // Entity is copied and inserted, so the old entity needs to be retracted to avoid duplication. - retract($entityToBeRecategorized); end rule "MAN.3.1: Apply entity recategorization of same type" @@ -1416,7 +1415,7 @@ rule "MAN.3.1: Apply entity recategorization of same type" when $recategorization: ManualRecategorization($id: annotationId, $type: type, $requestDate: requestDate) not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate)) - $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type == $type) + $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type() == $type) then $entityToBeRecategorized.getManualOverwrite().addChange($recategorization); retract($recategorization); @@ -1474,8 +1473,8 @@ rule "MAN.4.1: Apply legal basis change" rule "X.0.0: Remove Entity contained by Entity of same type" salience 65 when - $larger: TextEntity($type: type, $entityType: entityType, active()) - $contained: TextEntity(containedBy($larger), type == $type, entityType == $entityType, this != $larger, !hasManualChanges(), active()) + $larger: TextEntity($type: type(), $entityType: entityType, active()) + $contained: TextEntity(containedBy($larger), type() == $type, entityType == $entityType, this != $larger, !hasManualChanges(), active()) then $contained.remove("X.0.0", "remove Entity contained by Entity of same type"); retract($contained); @@ -1486,8 +1485,8 @@ rule "X.0.0: Remove Entity contained by Entity of same type" rule "X.1.0: Merge intersecting Entities of same type" salience 64 when - $first: TextEntity($type: type, $entityType: entityType, !resized(), active()) - $second: TextEntity(intersects($first), type == $type, entityType == $entityType, this != $first, !hasManualChanges(), active()) + $first: TextEntity($type: type(), $entityType: entityType, !resized(), active()) + $second: TextEntity(intersects($first), type() == $type, entityType == $entityType, this != $first, !hasManualChanges(), active()) then TextEntity mergedEntity = entityCreationService.mergeEntitiesOfSameType(List.of($first, $second), $type, $entityType, document); $first.remove("X.1.0", "merge intersecting Entities of same type"); @@ -1502,8 +1501,8 @@ rule "X.1.0: Merge intersecting Entities of same type" rule "X.2.0: Remove Entity of type ENTITY when contained by FALSE_POSITIVE" salience 64 when - $falsePositive: TextEntity($type: type, entityType == EntityType.FALSE_POSITIVE, active()) - $entity: TextEntity(containedBy($falsePositive), type == $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), !hasManualChanges(), active()) + $falsePositive: TextEntity($type: type(), entityType == EntityType.FALSE_POSITIVE, active()) + $entity: TextEntity(containedBy($falsePositive), type() == $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), !hasManualChanges(), active()) then $entity.getIntersectingNodes().forEach(node -> update(node)); $entity.remove("X.2.0", "remove Entity of type ENTITY when contained by FALSE_POSITIVE"); @@ -1515,8 +1514,8 @@ rule "X.2.0: Remove Entity of type ENTITY when contained by FALSE_POSITIVE" rule "X.3.0: Remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION" salience 64 when - $falseRecommendation: TextEntity($type: type, entityType == EntityType.FALSE_RECOMMENDATION, active()) - $recommendation: TextEntity(containedBy($falseRecommendation), type == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) + $falseRecommendation: TextEntity($type: type(), entityType == EntityType.FALSE_RECOMMENDATION, active()) + $recommendation: TextEntity(containedBy($falseRecommendation), type() == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) then $recommendation.remove("X.3.0", "remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION"); retract($recommendation); @@ -1527,8 +1526,8 @@ rule "X.3.0: Remove Entity of type RECOMMENDATION when contained by FALSE_RECOMM rule "X.4.0: Remove Entity of type RECOMMENDATION when text range equals ENTITY with same type" salience 256 when - $entity: TextEntity($type: type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) - $recommendation: TextEntity(getTextRange().equals($entity.getTextRange()), type == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) + $entity: TextEntity($type: type(), (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) + $recommendation: TextEntity(getTextRange().equals($entity.getTextRange()), type() == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) then $entity.addEngines($recommendation.getEngines()); $recommendation.remove("X.4.0", "remove Entity of type RECOMMENDATION when text range equals ENTITY with same type"); @@ -1552,8 +1551,8 @@ rule "X.5.0: Remove Entity of type RECOMMENDATION when intersected by ENTITY" rule "X.5.1: Remove Entity of type RECOMMENDATION when contained by RECOMMENDATION" salience 256 when - $entity: TextEntity($type: type, entityType == EntityType.RECOMMENDATION, active()) - $recommendation: TextEntity(containedBy($entity), type != $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) + $entity: TextEntity($type: type(), entityType == EntityType.RECOMMENDATION, active()) + $recommendation: TextEntity(containedBy($entity), type() != $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) then $recommendation.remove("X.5.1", "remove Entity of type RECOMMENDATION when contained by RECOMMENDATION"); retract($recommendation); @@ -1564,8 +1563,8 @@ rule "X.5.1: Remove Entity of type RECOMMENDATION when contained by RECOMMENDATI rule "X.6.0: Remove Entity of lower rank, when contained by by entity of type ENTITY" salience 32 when - $higherRank: TextEntity($type: type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) - $lowerRank: TextEntity(containedBy($higherRank), type != $type, dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), !hasManualChanges(), active()) + $higherRank: TextEntity($type: type(), (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) + $lowerRank: TextEntity(containedBy($higherRank), type() != $type, dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), !hasManualChanges(), active()) then $lowerRank.getIntersectingNodes().forEach(node -> update(node)); $lowerRank.remove("X.6.0", "remove Entity of lower rank, when contained by entity of type ENTITY"); @@ -1576,8 +1575,8 @@ rule "X.6.0: Remove Entity of lower rank, when contained by by entity of type EN rule "X.6.1: remove Entity of higher rank, when intersected by entity of type ENTITY and length of lower rank Entity is bigger than the higher rank Entity" salience 32 when - $higherRank: TextEntity($type: type, $value: value, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active(), !hasManualChanges()) - $lowerRank: TextEntity(intersects($higherRank), type != $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), active(), $lowerRank.getValue().length() > $value.length()) + $higherRank: TextEntity($type: type(), $value: value, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active(), !hasManualChanges()) + $lowerRank: TextEntity(intersects($higherRank), type() != $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), active(), $lowerRank.getValue().length() > $value.length()) then $higherRank.getIntersectingNodes().forEach(node -> update(node)); $higherRank.remove("X.6.1", "remove Entity of higher rank, when intersected by entity of type ENTITY and length of lower rank Entity is bigger than the higher rank Entity"); diff --git a/redaction-service-v1/rules-management/src/main/resources/all_rules_documine.drl b/redaction-service-v1/rules-management/src/main/resources/all_rules_documine.drl index 33dd8253..a8ac8685 100644 --- a/redaction-service-v1/rules-management/src/main/resources/all_rules_documine.drl +++ b/redaction-service-v1/rules-management/src/main/resources/all_rules_documine.drl @@ -464,7 +464,9 @@ rule "DOC.7.1: Performing Laboratory (Country)" then nerEntities.streamEntitiesOfType("COUNTRY") .filter(nerEntity -> $section.getTextRange().contains(nerEntity.textRange())) - .map(nerEntity -> entityCreationService.byNerEntity(nerEntity, "laboratory_country", EntityType.ENTITY, $section)) + .map(nerEntity -> entityCreationService.optionalByNerEntity(nerEntity, "laboratory_country", EntityType.ENTITY, $section)) + .flatMap(Optional::stream) + .collect(Collectors.toList()) .forEach(entity -> { entity.apply("DOC.7.1", "Performing Laboratory found", "n-a"); }); @@ -473,7 +475,7 @@ rule "DOC.7.1: Performing Laboratory (Country)" rule "DOC.7.2: Performing Laboratory (Country & Name) from dict" when $section: Section(containsString("PERFORMING LABORATORY:") || (containsString("PERFORMING") && containsString("LABORATORY:"))) - $countryOrNameFromDictionary: TextEntity(type == "laboratory_country" || type == "laboratory_name", $type: type, isDictionaryEntry()) from $section.getEntities() + $countryOrNameFromDictionary: TextEntity(type() == "laboratory_country" || type() == "laboratory_name", $type: type, isDictionaryEntry()) from $section.getEntities() then $countryOrNameFromDictionary.apply("DOC.7.2", "Performing " + $type + " dictionary entry found."); end @@ -1308,7 +1310,7 @@ rule "MAN.0.0: Apply manual resize redaction" not ManualResizeRedaction(annotationId == $id, requestDate.isBefore($requestDate)) $entityToBeResized: TextEntity(matchesAnnotationId($id)) then - manualChangesApplicationService.resizeEntityAndReinsert($entityToBeResized, $resizeRedaction); + manualChangesApplicationService.resize($entityToBeResized, $resizeRedaction); retract($resizeRedaction); update($entityToBeResized); $entityToBeResized.getIntersectingNodes().forEach(node -> update(node)); @@ -1386,13 +1388,12 @@ rule "MAN.3.0: Apply entity recategorization" when $recategorization: ManualRecategorization($id: annotationId, $type: type, $requestDate: requestDate) not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate)) - $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type != $type) + $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type() != $type) then $entityToBeRecategorized.getIntersectingNodes().forEach(node -> update(node)); - manualChangesApplicationService.recategorize($entityToBeRecategorized, $recategorization); + $entityToBeRecategorized.getManualOverwrite().addChange($recategorization); + update($entityToBeRecategorized); retract($recategorization); - // Entity is copied and inserted, so the old entity needs to be retracted to avoid duplication. - retract($entityToBeRecategorized); end rule "MAN.3.1: Apply entity recategorization of same type" @@ -1400,7 +1401,7 @@ rule "MAN.3.1: Apply entity recategorization of same type" when $recategorization: ManualRecategorization($id: annotationId, $type: type, $requestDate: requestDate) not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate)) - $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type == $type) + $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type() == $type) then $entityToBeRecategorized.getManualOverwrite().addChange($recategorization); retract($recategorization); @@ -1457,8 +1458,8 @@ rule "MAN.4.1: Apply legal basis change" rule "X.0.0: Remove Entity contained by Entity of same type" salience 65 when - $larger: TextEntity($type: type, $entityType: entityType, active()) - $contained: TextEntity(containedBy($larger), type == $type, entityType == $entityType, this != $larger, !hasManualChanges(), active()) + $larger: TextEntity($type: type(), $entityType: entityType, active()) + $contained: TextEntity(containedBy($larger), type() == $type, entityType == $entityType, this != $larger, !hasManualChanges(), active()) then $contained.remove("X.0.0", "remove Entity contained by Entity of same type"); retract($contained); @@ -1469,8 +1470,8 @@ rule "X.0.0: Remove Entity contained by Entity of same type" rule "X.2.0: Remove Entity of type ENTITY when contained by FALSE_POSITIVE" salience 64 when - $falsePositive: TextEntity($type: type, entityType == EntityType.FALSE_POSITIVE, active()) - $entity: TextEntity(containedBy($falsePositive), type == $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), !hasManualChanges(), active()) + $falsePositive: TextEntity($type: type(), entityType == EntityType.FALSE_POSITIVE, active()) + $entity: TextEntity(containedBy($falsePositive), type() == $type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), !hasManualChanges(), active()) then $entity.getIntersectingNodes().forEach(node -> update(node)); $entity.remove("X.2.0", "remove Entity of type ENTITY when contained by FALSE_POSITIVE"); @@ -1482,8 +1483,8 @@ rule "X.2.0: Remove Entity of type ENTITY when contained by FALSE_POSITIVE" rule "X.3.0: Remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION" salience 64 when - $falseRecommendation: TextEntity($type: type, entityType == EntityType.FALSE_RECOMMENDATION, active()) - $recommendation: TextEntity(containedBy($falseRecommendation), type == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) + $falseRecommendation: TextEntity($type: type(), entityType == EntityType.FALSE_RECOMMENDATION, active()) + $recommendation: TextEntity(containedBy($falseRecommendation), type() == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) then $recommendation.remove("X.3.0", "remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION"); retract($recommendation); @@ -1494,8 +1495,8 @@ rule "X.3.0: Remove Entity of type RECOMMENDATION when contained by FALSE_RECOMM rule "X.4.0: Remove Entity of type RECOMMENDATION when text range equals ENTITY with same type" salience 256 when - $entity: TextEntity($type: type, (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) - $recommendation: TextEntity(getTextRange().equals($entity.getTextRange()), type == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) + $entity: TextEntity($type: type(), (entityType == EntityType.ENTITY || entityType == EntityType.HINT), active()) + $recommendation: TextEntity(getTextRange().equals($entity.getTextRange()), type() == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) then $entity.addEngines($recommendation.getEngines()); $recommendation.remove("X.4.0", "remove Entity of type RECOMMENDATION when text range equals ENTITY with same type"); @@ -1519,8 +1520,8 @@ rule "X.5.0: Remove Entity of type RECOMMENDATION when intersected by ENTITY" rule "X.5.1: Remove Entity of type RECOMMENDATION when contained by RECOMMENDATION" salience 256 when - $entity: TextEntity($type: type, entityType == EntityType.RECOMMENDATION, active()) - $recommendation: TextEntity(containedBy($entity), type != $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) + $entity: TextEntity($type: type(), entityType == EntityType.RECOMMENDATION, active()) + $recommendation: TextEntity(containedBy($entity), type() != $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active()) then $recommendation.remove("X.5.1", "remove Entity of type RECOMMENDATION when contained by RECOMMENDATION"); retract($recommendation); diff --git a/redaction-service-v1/rules-management/src/test/resources/EFSA_sanitisation_GFL_v1/rules.txt b/redaction-service-v1/rules-management/src/test/resources/EFSA_sanitisation_GFL_v1/rules.txt index 92fa5519..da6022cd 100644 --- a/redaction-service-v1/rules-management/src/test/resources/EFSA_sanitisation_GFL_v1/rules.txt +++ b/redaction-service-v1/rules-management/src/test/resources/EFSA_sanitisation_GFL_v1/rules.txt @@ -1 +1 @@ -"package drools\n\nimport static java.lang.String.format;\nimport static com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RedactionSearchUtility.anyMatch;\nimport static com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RedactionSearchUtility.exactMatch;\n\nimport java.util.List;\nimport java.util.LinkedList;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport java.util.Collection;\nimport java.util.stream.Stream;\nimport java.util.Optional;\n\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.*;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.nodes.*;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.nodes.Section;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.nodes.Table;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.nodes.SemanticNode;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.nodes.Document;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.nodes.Paragraph;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.nodes.Image;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.entity.*;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.textblock.*;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.entity.EntityType;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.nodes.ImageType;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.FileAttribute;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Engine;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.services.EntityCreationService;\nimport com.iqser.red.service.redaction.v1.server.redaction.model.dictionary.Dictionary;\nimport com.iqser.red.service.redaction.v1.server.redaction.model.dictionary.DictionaryModel;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualResizeRedaction;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.IdRemoval;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualForceRedaction;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualImageRecategorization;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.AnnotationStatus;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.services.ManualRedactionApplicationService;\nimport com.iqser.red.service.redaction.v1.server.client.model.EntityRecognitionEntity;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.Boundary;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.entity.RedactionEntity;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.Boundary;\nimport com.iqser.red.service.redaction.v1.server.redaction.adapter.NerEntitiesAdapter;\nimport com.iqser.red.service.redaction.v1.server.redaction.adapter.NerEntities;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RedactionSearchUtility;\n\nglobal Document document\nglobal EntityCreationService entityCreationService\nglobal ManualRedactionApplicationService manualRedactionApplicationService\nglobal NerEntitiesAdapter nerEntitiesAdapter\nglobal Dictionary dictionary\n\n//------------------------------------ queries ------------------------------------\n\nquery \"getFileAttributes\"\n $fileAttribute: FileAttribute()\n end\n\n//------------------------------------ Syngenta specific rules ------------------------------------\n\n// Rule unit: SYN.1\nrule \"SYN.1.0: Recommend CTL/BL laboratory that start with BL or CTL\"\n when\n $section: Section(containsString(\"CT\") || containsString(\"BL\"))\n then\n /* Regular expression: ((\\b((([Cc]T(([1ILli\\/])| L|~P))|(BL))[\\. ]?([\\dA-Ziltphz~\\/.:!]| ?[\\(',][Ppi](\\(e)?|([\\(-?']\\/))+( ?[\\(\\/\\dA-Znasieg]+)?)\\b( ?\\/? ?\\d+)?)|(\\bCT[L1i]\\b)) */\n entityCreationService.byRegexIgnoreCase(\"((\\\\b((([Cc]T(([1ILli\\\\/])| L|~P))|(BL))[\\\\. ]?([\\\\dA-Ziltphz~\\\\/.:!]| ?[\\\\(',][Ppi](\\\\(e)?|([\\\\(-?']\\\\/))+( ?[\\\\(\\\\/\\\\dA-Znasieg]+)?)\\\\b( ?\\\\/? ?\\\\d+)?)|(\\\\bCT[L1i]\\\\b))\", \"CBI_address\", EntityType.RECOMMENDATION, $section)\n .forEach(entity -> {\n entity.skip(\"SYN.1.0\", \"\");\n entity.addEngine(Engine.RULE);\n insert(entity);\n });\n end\n\n\n//------------------------------------ CBI rules ------------------------------------\n\n// Rule unit: CBI.0\nrule \"CBI.0.0: Redact CBI Authors (Non Vertebrate Study)\"\n when\n not FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $entity: RedactionEntity(type == \"CBI_author\", dictionaryEntry)\n then\n $entity.apply(\"CBI.0.0\", \"Author found\", \"Article 39(e)(3) of Regulation (EC) No 178/2002\");\n end\n\nrule \"CBI.0.1: Redact CBI Authors (Vertebrate Study)\"\n when\n FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $entity: RedactionEntity(type == \"CBI_author\", dictionaryEntry)\n then\n $entity.apply(\"CBI.0.1\", \"Author found\", \"Article 39(e)(2) of Regulation (EC) No 178/2002\");\n end\n\n\n// Rule unit: CBI.1\nrule \"CBI.1.0: Don't redact CBI Address (Non Vertebrate Study)\"\n when\n not FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $entity: RedactionEntity(type == \"CBI_address\", dictionaryEntry)\n then\n $entity.skip(\"CBI.1.0\", \"Address found for Non Vertebrate Study\");\n end\n\nrule \"CBI.1.1: Redact CBI Address (Vertebrate Study)\"\n when\n FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $entity: RedactionEntity(type == \"CBI_address\", dictionaryEntry)\n then\n $entity.apply(\"CBI.1.1\", \"Address found\", \"Article 39(e)(2) of Regulation (EC) No 178/2002\");\n end\n\n\n// Rule unit: CBI.2\nrule \"CBI.2.0: Don't redact genitive CBI_author\"\n when\n $entity: RedactionEntity(type == \"CBI_author\", anyMatch(textAfter, \"['’’'ʼˈ´`‘′ʻ’']s\"), isApplied())\n then\n RedactionEntity falsePositive = entityCreationService.byBoundary($entity.getBoundary(), \"CBI_author\", EntityType.FALSE_POSITIVE, document);\n falsePositive.skip(\"CBI.2.0\", \"Genitive Author found\");\n insert(falsePositive);\n end\n\n\n// Rule unit: CBI.7\nrule \"CBI.7.0: Do not redact Names and Addresses if published information found in section without tables\"\n when\n $section: Section(!hasTables(),\n hasEntitiesOfType(\"published_information\"),\n (hasEntitiesOfType(\"CBI_author\") || hasEntitiesOfType(\"CBI_address\")))\n then\n $section.getEntitiesOfType(List.of(\"CBI_author\", \"CBI_address\"))\n .forEach(redactionEntity -> {\n redactionEntity.skipWithReferences(\n \"CBI.7.0\",\n \"Published Information found in section\",\n $section.getEntitiesOfType(\"published_information\")\n );\n });\n end\n\nrule \"CBI.7.1: Do not redact Names and Addresses if published information found in same table row\"\n when\n $table: Table(hasEntitiesOfType(\"published_information\"),\n (hasEntitiesOfType(\"CBI_author\") || hasEntitiesOfType(\"CBI_address\")))\n then\n $table.streamEntitiesWhereRowContainsEntitiesOfType(List.of(\"CBI_author\", \"CBI_address\"))\n .forEach(redactionEntity -> {\n redactionEntity.skipWithReferences(\n \"CBI.7.1\",\n \"Published Information found in row\",\n $table.getEntitiesOfTypeInSameRow(\"published_information\", redactionEntity)\n );\n });\n end\n\n\n// Rule unit: CBI.9\nrule \"CBI.9.0: Redact all Cell's with Header Author(s) as CBI_author (non vertebrate study)\"\n agenda-group \"LOCAL_DICTIONARY_ADDS\"\n when\n not FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $table: Table(hasHeader(\"Author(s)\"))\n then\n $table.streamTableCellsWithHeader(\"Author(s)\")\n .map(tableCell -> entityCreationService.bySemanticNode(tableCell, \"CBI_author\", EntityType.ENTITY))\n .filter(Optional::isPresent)\n .map(Optional::get)\n .forEach(redactionEntity -> {\n redactionEntity.apply(\"CBI.9.0\", \"Author(s) found\", \"Article 39(e)(3) of Regulation (EC) No 178/2002\");\n redactionEntity.addEngine(Engine.RULE);\n insert(redactionEntity);\n });\n end\n\nrule \"CBI.9.1: Redact all Cell's with Header Author as CBI_author (non vertebrate study)\"\n agenda-group \"LOCAL_DICTIONARY_ADDS\"\n when\n not FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $table: Table(hasHeader(\"Author\"))\n then\n $table.streamTableCellsWithHeader(\"Author\")\n .map(tableCell -> entityCreationService.bySemanticNode(tableCell, \"CBI_author\", EntityType.ENTITY))\n .filter(Optional::isPresent)\n .map(Optional::get)\n .forEach(redactionEntity -> {\n redactionEntity.apply(\"CBI.9.1\", \"Author found\", \"Article 39(e)(3) of Regulation (EC) No 178/2002\");\n redactionEntity.addEngine(Engine.RULE);\n insert(redactionEntity);\n });\n end\n\n\n// Rule unit: CBI.10\nrule \"CBI.10.0: Redact all Cell's with Header Author(s) as CBI_author (vertebrate study)\"\n agenda-group \"LOCAL_DICTIONARY_ADDS\"\n when\n FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $table: Table(hasHeader(\"Author(s)\"))\n then\n $table.streamTableCellsWithHeader(\"Author(s)\")\n .map(tableCell -> entityCreationService.bySemanticNode(tableCell, \"CBI_author\", EntityType.ENTITY))\n .filter(Optional::isPresent)\n .map(Optional::get)\n .forEach(redactionEntity -> {\n redactionEntity.apply(\"CBI.10.0\", \"Author(s) found\", \"Article 39(e)(2) of Regulation (EC) No 178/2002\");\n redactionEntity.addEngine(Engine.RULE);\n insert(redactionEntity);\n });\n end\n\nrule \"CBI.10.1: Redact all Cell's with Header Author as CBI_author (vertebrate study)\"\n agenda-group \"LOCAL_DICTIONARY_ADDS\"\n when\n FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $table: Table(hasHeader(\"Author\"))\n then\n $table.streamTableCellsWithHeader(\"Author\")\n .map(tableCell -> entityCreationService.bySemanticNode(tableCell, \"CBI_author\", EntityType.ENTITY))\n .filter(Optional::isPresent)\n .map(Optional::get)\n .forEach(redactionEntity -> {\n redactionEntity.apply(\"CBI.10.1\", \"Author found\", \"Article 39(e)(2) of Regulation (EC) No 178/2002\");\n redactionEntity.addEngine(Engine.RULE);\n insert(redactionEntity);\n });\n end\n\n\n// Rule unit: CBI.11\nrule \"CBI.11.0: Recommend all CBI_author entities in Table with Vertebrate Study Y/N Header\"\n agenda-group \"LOCAL_DICTIONARY_ADDS\"\n salience -1\n when\n $table: Table(hasHeader(\"Author(s)\") && hasHeader(\"Vertebrate Study Y/N\"))\n then\n $table.getEntitiesOfType(\"CBI_author\").forEach(entity -> dictionary.addMultipleAuthorsAsRecommendation(entity));\n end\n\n\n// Rule unit: CBI.16\nrule \"CBI.16.0: Add CBI_author with \\\"et al.\\\" Regex (non vertebrate study)\"\n agenda-group \"LOCAL_DICTIONARY_ADDS\"\n when\n not FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $section: Section(containsString(\"et al.\"))\n then\n entityCreationService.byRegex(\"\\\\b([A-ZÄÖÜ][^\\\\s\\\\.,]+( [A-ZÄÖÜ]{1,2}\\\\.?)?( ?[A-ZÄÖÜ]\\\\.?)?) et al\\\\.?\", \"CBI_author\", EntityType.ENTITY, 1, $section)\n .forEach(entity -> {\n entity.apply(\"CBI.16.0\", \"Author found by \\\"et al\\\" regex\", \"Article 39(e)(3) of Regulation (EC) No 178/2002\");\n entity.addEngine(Engine.RULE);\n dictionary.addLocalDictionaryEntry(\"CBI_author\", entity.getValue(), false);\n insert(entity);\n });\n end\n\nrule \"CBI.16.1: Add CBI_author with \\\"et al.\\\" Regex (vertebrate study)\"\n agenda-group \"LOCAL_DICTIONARY_ADDS\"\n when\n FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $section: Section(containsString(\"et al.\"))\n then\n entityCreationService.byRegex(\"\\\\b([A-ZÄÖÜ][^\\\\s\\\\.,]+( [A-ZÄÖÜ]{1,2}\\\\.?)?( ?[A-ZÄÖÜ]\\\\.?)?) et al\\\\.?\", \"CBI_author\", EntityType.ENTITY, 1, $section)\n .forEach(entity -> {\n entity.apply(\"CBI.16.1\", \"Author found by \\\"et al\\\" regex\", \"Article 39(e)(2) of Regulation (EC) No 178/2002\");\n entity.addEngine(Engine.RULE);\n insert(entity);\n dictionary.addLocalDictionaryEntry(\"CBI_author\", entity.getValue(), false);\n });\n end\n\n\n// Rule unit: CBI.17\nrule \"CBI.17.0: Add recommendation for Addresses in Test Organism sections, without colon\"\n when\n $section: Section(!hasTables(), containsString(\"Species\") && containsString(\"Source\") && !containsString(\"Species:\") && !containsString(\"Source:\"))\n then\n entityCreationService.lineAfterString(\"Source\", \"CBI_address\", EntityType.RECOMMENDATION, $section)\n .forEach(entity -> {\n entity.addEngine(Engine.RULE);\n entity.skip(\"CBI.17.0\", \"Line after \\\"Source\\\" in Test Organism Section\");\n insert(entity);\n });\n end\n\nrule \"CBI.17.1: Add recommendation for Addresses in Test Organism sections, with colon\"\n when\n $section: Section(!hasTables(), containsString(\"Species:\"), containsString(\"Source:\"))\n then\n entityCreationService.lineAfterString(\"Source:\", \"CBI_address\", EntityType.RECOMMENDATION, $section)\n .forEach(entity -> {\n entity.addEngine(Engine.RULE);\n entity.skip(\"CBI.17.1\", \"Line after \\\"Source:\\\" in Test Animals Section\");\n insert(entity);\n });\n end\n\n\n// Rule unit: CBI.20\nrule \"CBI.20.0: Redact between \\\"PERFORMING LABORATORY\\\" and \\\"LABORATORY PROJECT ID:\\\" (non vertebrate study)\"\n agenda-group \"LOCAL_DICTIONARY_ADDS\"\n when\n not FileAttribute(label == \"Vertebrate Study\", value == \"Yes\")\n $section: Section(!hasTables(), containsString(\"PERFORMING LABORATORY:\"), containsString(\"LABORATORY PROJECT ID:\"))\n then\n entityCreationService.betweenStrings(\"PERFORMING LABORATORY:\", \"LABORATORY PROJECT ID:\", \"CBI_address\", EntityType.ENTITY, $section)\n .forEach(laboratoryEntity -> {\n laboratoryEntity.skip(\"CBI.20.0\", \"PERFORMING LABORATORY was found for non vertebrate study\");\n laboratoryEntity.addEngine(Engine.RULE);\n dictionary.addLocalDictionaryEntry(laboratoryEntity);\n insert(laboratoryEntity);\n });\n end\n\nrule \"CBI.20.1: Redact between \\\"PERFORMING LABORATORY\\\" and \\\"LABORATORY PROJECT ID:\\\" (vertebrate study)\"\n agenda-group \"LOCAL_DICTIONARY_ADDS\"\n when\n FileAttribute(label == \"Vertebrate Study\", value == \"Yes\")\n $section: Section(!hasTables(), containsString(\"PERFORMING LABORATORY:\"), containsString(\"LABORATORY PROJECT ID:\"))\n then\n entityCreationService.betweenStrings(\"PERFORMING LABORATORY:\", \"LABORATORY PROJECT ID:\", \"CBI_address\", EntityType.ENTITY, $section)\n .forEach(laboratoryEntity -> {\n laboratoryEntity.apply(\"CBI.20.1\", \"PERFORMING LABORATORY was found\", \"Article 39(e)(2) of Regulation (EC) No 178/2002\");\n laboratoryEntity.addEngine(Engine.RULE);\n dictionary.addLocalDictionaryEntry(laboratoryEntity);\n insert(laboratoryEntity);\n });\n end\n\n\n//------------------------------------ PII rules ------------------------------------\n\n// Rule unit: PII.0\nrule \"PII.0.0: Redact all PII (non vertebrate study)\"\n when\n not FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $pii: RedactionEntity(type == \"PII\", dictionaryEntry)\n then\n $pii.apply(\"PII.0.0\", \"Personal Information found\", \"Article 39(e)(3) of Regulation (EC) No 178/2002\");\n end\n\nrule \"PII.0.1: Redact all PII (vertebrate study)\"\n when\n FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $pii: RedactionEntity(type == \"PII\", dictionaryEntry)\n then\n $pii.apply(\"PII.0.1\", \"Personal Information found\", \"Article 39(e)(2) of Regulation (EC) No 178/2002\");\n end\n\n\n// Rule unit: PII.1\nrule \"PII.1.0: Redact Emails by RegEx (Non vertebrate study)\"\n when\n not FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $section: Section(containsString(\"@\"))\n then\n entityCreationService.byRegex(\"\\\\b([A-Za-z0-9._%+\\\\-]+@[A-Za-z0-9.\\\\-]+\\\\.[A-Za-z\\\\-]{1,23}[A-Za-z])\\\\b\", \"PII\", EntityType.ENTITY, 1, $section)\n .forEach(emailEntity -> {\n emailEntity.addEngine(Engine.RULE);\n emailEntity.apply(\"PII.1.0\", \"Found by Email Regex\", \"Article 39(e)(3) of Regulation (EC) No 178/2002\");\n insert(emailEntity);\n });\n end\n\nrule \"PII.1.1: Redact Emails by RegEx (vertebrate study)\"\n when\n FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $section: Section(containsString(\"@\"))\n then\n entityCreationService.byRegex(\"\\\\b([A-Za-z0-9._%+\\\\-]+@[A-Za-z0-9.\\\\-]+\\\\.[A-Za-z\\\\-]{1,23}[A-Za-z])\\\\b\", \"PII\", EntityType.ENTITY, 1, $section)\n .forEach(emailEntity -> {\n emailEntity.addEngine(Engine.RULE);\n emailEntity.apply(\"PII.1.1\", \"Found by Email Regex\", \"Article 39(e)(3) of Regulation (EC) No 178/2002\");\n insert(emailEntity);\n });\n end\n\n\n// Rule unit: PII.2\nrule \"PII.2.0: Redact Phone and Fax by RegEx (non vertebrate study)\"\n when\n not FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $section: Section(containsString(\"Contact\") ||\n containsString(\"Telephone\") ||\n containsString(\"Phone\") ||\n containsString(\"Ph.\") ||\n containsString(\"Fax\") ||\n containsString(\"Tel\") ||\n containsString(\"Ter\") ||\n containsString(\"Mobile\") ||\n containsString(\"Fel\") ||\n containsString(\"Fer\"))\n then\n entityCreationService.byRegexIgnoreCase(\"\\\\b(contact|telephone|phone|ph\\\\.|fax|tel|ter|mobile|fel|fer)[a-zA-Z\\\\s]{0,10}[:.\\\\s]{0,3}([\\\\+\\\\d\\\\(][\\\\s\\\\d\\\\(\\\\)\\\\-\\\\/\\\\.]{4,100}\\\\d)\\\\b\", \"PII\", EntityType.ENTITY, 2, $section)\n .forEach(contactEntity -> {\n contactEntity.addEngine(Engine.RULE);\n contactEntity.apply(\"PII.2.0\", \"Found by Phone and Fax Regex\", \"Article 39(e)(3) of Regulation (EC) No 178/2002\");\n insert(contactEntity);\n });\n end\n\nrule \"PII.2.1: Redact Phone and Fax by RegEx (vertebrate study)\"\n when\n FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $section: Section(containsString(\"Contact\") ||\n containsString(\"Telephone\") ||\n containsString(\"Phone\") ||\n containsString(\"Ph.\") ||\n containsString(\"Fax\") ||\n containsString(\"Tel\") ||\n containsString(\"Ter\") ||\n containsString(\"Mobile\") ||\n containsString(\"Fel\") ||\n containsString(\"Fer\"))\n then\n entityCreationService.byRegexIgnoreCase(\"\\\\b(contact|telephone|phone|ph\\\\.|fax|tel|ter|mobile|fel|fer)[a-zA-Z\\\\s]{0,10}[:.\\\\s]{0,3}([\\\\+\\\\d\\\\(][\\\\s\\\\d\\\\(\\\\)\\\\-\\\\/\\\\.]{4,100}\\\\d)\\\\b\", \"PII\", EntityType.ENTITY, 2, $section)\n .forEach(contactEntity -> {\n contactEntity.addEngine(Engine.RULE);\n contactEntity.apply(\"PII.2.1\", \"Found by Phone and Fax Regex\", \"Article 39(e)(2) of Regulation (EC) No 178/2002\");\n insert(contactEntity);\n });\n end\n\n\n// Rule unit: PII.9\nrule \"PII.9.0: Redact between \\\"AUTHOR(S)\\\" and \\\"COMPLETION DATE\\\" (non vertebrate study)\"\n when\n not FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $section: Section(!hasTables(), containsString(\"AUTHOR(S):\"), containsString(\"COMPLETION DATE:\"), !containsString(\"STUDY COMPLETION DATE:\"))\n then\n entityCreationService.betweenStrings(\"AUTHOR(S):\", \"COMPLETION DATE:\", \"PII\", EntityType.ENTITY, $section)\n .forEach(authorEntity -> {\n authorEntity.apply(\"PII.9.0\", \"AUTHOR(S) was found\", \"Article 39(e)(3) of Regulation (EC) No 178/2002\");\n authorEntity.addEngine(Engine.RULE);\n insert(authorEntity);\n });\n end\n\nrule \"PII.9.1: Redact between \\\"AUTHOR(S)\\\" and \\\"STUDY COMPLETION DATE\\\" (non vertebrate study)\"\n when\n FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $section: Section(!hasTables(), containsString(\"AUTHOR(S):\"), containsString(\"COMPLETION DATE:\"), !containsString(\"STUDY COMPLETION DATE:\"))\n then\n entityCreationService.betweenStrings(\"AUTHOR(S):\", \"COMPLETION DATE:\", \"PII\", EntityType.ENTITY, $section)\n .forEach(authorEntity -> {\n authorEntity.apply(\"PII.9.1\", \"AUTHOR(S) was found\", \"Article 39(e)(2) of Regulation (EC) No 178/2002\");\n authorEntity.addEngine(Engine.RULE);\n insert(authorEntity);\n });\n end\n\nrule \"PII.9.2: Redact between \\\"AUTHOR(S)\\\" and \\\"COMPLETION DATE\\\" (non vertebrate study)\"\n when\n not FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $section: Section(!hasTables(), containsString(\"AUTHOR(S):\"), containsString(\"STUDY COMPLETION DATE:\"))\n then\n entityCreationService.betweenStrings(\"AUTHOR(S):\", \"STUDY COMPLETION DATE:\", \"PII\", EntityType.ENTITY, $section)\n .forEach(authorEntity -> {\n authorEntity.apply(\"PII.9.2\", \"AUTHOR(S) was found\", \"Article 39(e)(3) of Regulation (EC) No 178/2002\");\n authorEntity.addEngine(Engine.RULE);\n insert(authorEntity);\n });\n end\n\nrule \"PII.9.3: Redact between \\\"AUTHOR(S)\\\" and \\\"STUDY COMPLETION DATE\\\" (vertebrate study)\"\n when\n FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $section: Section(!hasTables(), containsString(\"AUTHOR(S):\"), containsString(\"STUDY COMPLETION DATE:\"))\n then\n entityCreationService.betweenStrings(\"AUTHOR(S):\", \"STUDY COMPLETION DATE:\", \"PII\", EntityType.ENTITY, $section)\n .forEach(authorEntity -> {\n authorEntity.apply(\"PII.9.3\", \"AUTHOR(S) was found\", \"Article 39(e)(2) of Regulation (EC) No 178/2002\");\n authorEntity.addEngine(Engine.RULE);\n insert(authorEntity);\n });\n end\n\n\n//------------------------------------ Other rules ------------------------------------\n\n// Rule unit: ETC.0\nrule \"ETC.0.0: Purity Hint\"\n when\n $section: Section(containsStringIgnoreCase(\"purity\"))\n then\n entityCreationService.byRegexIgnoreCase(\"(purity ?( of|\\\\(.{1,20}\\\\))?( ?:)?) .{0,5}[\\\\d\\\\.]+( .{0,4}\\\\.)? ?%\", \"hint_only\", EntityType.ENTITY, 1, $section)\n .forEach(hint -> {\n hint.addEngine(Engine.RULE);\n hint.skip(\"ETC.0.0\", \"\");\n });\n end\n\n\n// Rule unit: ETC.2\nrule \"ETC.2.0: Redact signatures (non vertebrate study)\"\n when\n not FileAttribute(label == \"Vertebrate Study\", value == \"Yes\")\n $signature: Image(imageType == ImageType.SIGNATURE)\n then\n $signature.apply(\"ETC.2.0\", \"Signature Found\", \"Article 39(e)(3) of Regulation (EC) No 178/2002\");\n end\n\nrule \"ETC.2.0: Redact signatures (vertebrate study)\"\n when\n FileAttribute(label == \"Vertebrate Study\", value == \"Yes\")\n $signature: Image(imageType == ImageType.SIGNATURE)\n then\n $signature.apply(\"ETC.2.0\", \"Signature Found\", \"Article 39(e)(2) of Regulation (EC) No 178/2002\");\n end\n\n\n// Rule unit: ETC.3\nrule \"ETC.3.0: Redact logos (vertebrate study)\"\n when\n not FileAttribute(label == \"Vertebrate Study\", value == \"Yes\")\n $logo: Image(imageType == ImageType.LOGO)\n then\n $logo.apply(\"ETC.3.0\", \"Logo Found\", \"Article 39(e)(3) of Regulation (EC) No 178/2002\");\n end\n\nrule \"ETC.3.1: Redact logos (non vertebrate study)\"\n when\n FileAttribute(label == \"Vertebrate Study\", value == \"Yes\")\n $logo: Image(imageType == ImageType.LOGO)\n then\n $logo.apply(\"ETC.3.1\", \"Logo Found\", \"Article 39(e)(2) of Regulation (EC) No 178/2002\");\n end\n\n\n// Rule unit: ETC.5\nrule \"ETC.5.0: Ignore dossier_redaction entries if confidentiality is not 'confidential'\"\n when\n not FileAttribute(label == \"Confidentiality\", value == \"confidential\")\n $dossierRedaction: RedactionEntity(type == \"dossier_redaction\")\n then\n $dossierRedaction.removeFromGraph();\n retract($dossierRedaction);\n end\n\n\n//------------------------------------ AI rules ------------------------------------\n\n// Rule unit: AI.0\nrule \"AI.0.0: add all NER Entities of type CBI_author\"\n salience 999\n when\n nerEntities: NerEntities(hasEntitiesOfType(\"CBI_author\"))\n then\n nerEntities.streamEntitiesOfType(\"CBI_author\")\n .map(nerEntity -> entityCreationService.byNerEntity(nerEntity, EntityType.RECOMMENDATION, document))\n .forEach(entity -> insert(entity));\n end\n\n\n// Rule unit: AI.1\nrule \"AI.1.0: combine and add NER Entities as CBI_address\"\n salience 999\n when\n nerEntities: NerEntities(hasEntitiesOfType(\"ORG\") || hasEntitiesOfType(\"STREET\") || hasEntitiesOfType(\"CITY\"))\n then\n nerEntitiesAdapter.combineNerEntitiesToCbiAddressDefaults(nerEntities)\n .map(boundary -> entityCreationService.byBoundary(boundary, \"CBI_address\", EntityType.RECOMMENDATION, document))\n .forEach(entity -> {\n entity.addEngine(Engine.NER);\n insert(entity);\n });\n end\n\n\n//------------------------------------ Manual redaction rules ------------------------------------\n\n// Rule unit: MAN.0\nrule \"MAN.0.0: Apply manual resize redaction\"\n salience 128\n when\n $resizeRedaction: ManualResizeRedaction($id: annotationId)\n $entityToBeResized: RedactionEntity(matchesAnnotationId($id))\n then\n manualRedactionApplicationService.resizeEntityAndReinsert($entityToBeResized, $resizeRedaction);\n retract($resizeRedaction);\n update($entityToBeResized);\n end\n\n\n// Rule unit: MAN.1\nrule \"MAN.1.0: Apply id removals that are valid and not in forced redactions to Entity\"\n salience 128\n when\n IdRemoval(status == AnnotationStatus.APPROVED, !removeFromDictionary, requestDate != null, $id: annotationId)\n not ManualForceRedaction($id == annotationId, status == AnnotationStatus.APPROVED, requestDate != null)\n $entityToBeRemoved: RedactionEntity(matchesAnnotationId($id))\n then\n $entityToBeRemoved.setIgnored(true);\n end\n\nrule \"MAN.1.1: Apply id removals that are valid and not in forced redactions to Image\"\n salience 128\n when\n IdRemoval(status == AnnotationStatus.APPROVED, !removeFromDictionary, requestDate != null, $id: annotationId)\n not ManualForceRedaction($id == annotationId, status == AnnotationStatus.APPROVED, requestDate != null)\n $imageEntityToBeRemoved: Image($id == id)\n then\n $imageEntityToBeRemoved.setIgnored(true);\n end\n\n\n// Rule unit: MAN.2\nrule \"MAN.2.0: Apply force redaction\"\n salience 128\n when\n ManualForceRedaction($id: annotationId, status == AnnotationStatus.APPROVED, requestDate != null, $legalBasis: legalBasis)\n $entityToForce: RedactionEntity(matchesAnnotationId($id))\n then\n $entityToForce.apply(\"MAN.2.0\", \"Forced redaction\", $legalBasis);\n $entityToForce.setSkipRemoveEntitiesContainedInLarger(true);\n end\n\n\n// Rule unit: MAN.3\nrule \"MAN.3.0: Apply image recategorization\"\n salience 128\n when\n ManualImageRecategorization($id: annotationId, status == AnnotationStatus.APPROVED, $imageType: type)\n $image: Image($id == id)\n then\n $image.setImageType(ImageType.fromString($imageType));\n end\n\n\n//------------------------------------ Entity merging rules ------------------------------------\n\n// Rule unit: X.0\nrule \"X.0.0: remove Entity contained by Entity of same type\"\n salience 65\n when\n $larger: RedactionEntity($type: type, $entityType: entityType)\n $contained: RedactionEntity(containedBy($larger), type == $type, entityType == $entityType, this != $larger, !resized, !skipRemoveEntitiesContainedInLarger)\n then\n $contained.removeFromGraph();\n retract($contained);\n end\n\n\n// Rule unit: X.1\nrule \"X.1.0: merge intersecting Entities of same type\"\n salience 64\n when\n $first: RedactionEntity($type: type, $entityType: entityType, !resized, !skipRemoveEntitiesContainedInLarger)\n $second: RedactionEntity(intersects($first), type == $type, entityType == $entityType, this != $first, !resized, !skipRemoveEntitiesContainedInLarger)\n then\n $first.removeFromGraph();\n $second.removeFromGraph();\n RedactionEntity mergedEntity = entityCreationService.byEntities(List.of($first, $second), $type, $entityType, document);\n retract($first);\n retract($second);\n insert(mergedEntity);\n end\n\n\n// Rule unit: X.2\nrule \"X.2.0: remove Entity of type ENTITY when contained by FALSE_POSITIVE\"\n salience 64\n when\n $falsePositive: RedactionEntity($type: type, entityType == EntityType.FALSE_POSITIVE)\n $entity: RedactionEntity(containedBy($falsePositive), type == $type, entityType == EntityType.ENTITY, !resized, !skipRemoveEntitiesContainedInLarger)\n then\n $entity.removeFromGraph();\n retract($entity)\n end\n\n\n// Rule unit: X.3\nrule \"X.3.0: remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION\"\n salience 64\n when\n $falseRecommendation: RedactionEntity($type: type, entityType == EntityType.FALSE_RECOMMENDATION)\n $recommendation: RedactionEntity(containedBy($falseRecommendation), type == $type, entityType == EntityType.RECOMMENDATION, !resized, !skipRemoveEntitiesContainedInLarger)\n then\n $recommendation.removeFromGraph();\n retract($recommendation);\n end\n\n\n// Rule unit: X.4\nrule \"X.4.0: remove Entity of type RECOMMENDATION when intersected by ENTITY with same type\"\n salience 256\n when\n $entity: RedactionEntity($type: type, entityType == EntityType.ENTITY)\n $recommendation: RedactionEntity(intersects($entity), type == $type, entityType == EntityType.RECOMMENDATION, !resized, !skipRemoveEntitiesContainedInLarger)\n then\n $entity.addEngines($recommendation.getEngines());\n $recommendation.removeFromGraph();\n retract($recommendation);\n end\n\n\n// Rule unit: X.5\nrule \"X.5.0: remove Entity of type RECOMMENDATION when contained by ENTITY\"\n salience 256\n when\n $entity: RedactionEntity(entityType == EntityType.ENTITY)\n $recommendation: RedactionEntity(containedBy($entity), entityType == EntityType.RECOMMENDATION, !resized, !skipRemoveEntitiesContainedInLarger)\n then\n $recommendation.removeFromGraph();\n retract($recommendation);\n end\n\n\n// Rule unit: X.6\nrule \"X.6.0: remove Entity of lower rank, when intersected by entity of type ENTITY\"\n salience 32\n when\n $higherRank: RedactionEntity($type: type, entityType == EntityType.ENTITY)\n $lowerRank: RedactionEntity(intersects($higherRank), type != $type, dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), !resized, !skipRemoveEntitiesContainedInLarger)\n then\n $lowerRank.removeFromGraph();\n retract($lowerRank);\n end\n\n\n//------------------------------------ File attributes rules ------------------------------------\n\n// Rule unit: FA.1\nrule \"FA.1.0: remove duplicate FileAttributes\"\n salience 64\n when\n $fileAttribute: FileAttribute($label: label, $value: value)\n $duplicate: FileAttribute(this != $fileAttribute, label == $label, value == $value)\n then\n retract($duplicate);\n end\n\n\n//------------------------------------ Local dictionary search rules ------------------------------------\n\n// Rule unit: LDS.0\nrule \"LDS.0.0: run local dictionary search\"\n agenda-group \"LOCAL_DICTIONARY_ADDS\"\n salience -999\n when\n DictionaryModel(!localEntries.isEmpty(), $type: type, $searchImplementation: localSearch) from dictionary.getDictionaryModels()\n then\n entityCreationService.bySearchImplementation($searchImplementation, $type, EntityType.RECOMMENDATION, document)\n .forEach(entity -> {\n entity.addEngine(Engine.RULE);\n insert(entity);\n });\n end\n" \ No newline at end of file +"package drools\n\nimport static java.lang.String.format;\nimport static com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RedactionSearchUtility.anyMatch;\nimport static com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RedactionSearchUtility.exactMatch;\n\nimport java.util.List;\nimport java.util.LinkedList;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport java.util.Collection;\nimport java.util.stream.Stream;\nimport java.util.Optional;\n\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.*;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.nodes.*;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.nodes.Section;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.nodes.Table;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.nodes.SemanticNode;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.nodes.Document;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.nodes.Paragraph;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.nodes.Image;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.entity.*;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.textblock.*;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.entity.EntityType;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.nodes.ImageType;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.FileAttribute;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.Engine;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.services.EntityCreationService;\nimport com.iqser.red.service.redaction.v1.server.redaction.model.dictionary.Dictionary;\nimport com.iqser.red.service.redaction.v1.server.redaction.model.dictionary.DictionaryModel;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualResizeRedaction;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.IdRemoval;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualForceRedaction;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualImageRecategorization;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.AnnotationStatus;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.services.ManualRedactionApplicationService;\nimport com.iqser.red.service.redaction.v1.server.client.model.EntityRecognitionEntity;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.Boundary;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.entity.RedactionEntity;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.Boundary;\nimport com.iqser.red.service.redaction.v1.server.redaction.adapter.NerEntitiesAdapter;\nimport com.iqser.red.service.redaction.v1.server.redaction.adapter.NerEntities;\nimport com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RedactionSearchUtility;\n\nglobal Document document\nglobal EntityCreationService entityCreationService\nglobal ManualRedactionApplicationService manualRedactionApplicationService\nglobal NerEntitiesAdapter nerEntitiesAdapter\nglobal Dictionary dictionary\n\n//------------------------------------ queries ------------------------------------\n\nquery \"getFileAttributes\"\n $fileAttribute: FileAttribute()\n end\n\n//------------------------------------ Syngenta specific rules ------------------------------------\n\n// Rule unit: SYN.1\nrule \"SYN.1.0: Recommend CTL/BL laboratory that start with BL or CTL\"\n when\n $section: Section(containsString(\"CT\") || containsString(\"BL\"))\n then\n /* Regular expression: ((\\b((([Cc]T(([1ILli\\/])| L|~P))|(BL))[\\. ]?([\\dA-Ziltphz~\\/.:!]| ?[\\(',][Ppi](\\(e)?|([\\(-?']\\/))+( ?[\\(\\/\\dA-Znasieg]+)?)\\b( ?\\/? ?\\d+)?)|(\\bCT[L1i]\\b)) */\n entityCreationService.byRegexIgnoreCase(\"((\\\\b((([Cc]T(([1ILli\\\\/])| L|~P))|(BL))[\\\\. ]?([\\\\dA-Ziltphz~\\\\/.:!]| ?[\\\\(',][Ppi](\\\\(e)?|([\\\\(-?']\\\\/))+( ?[\\\\(\\\\/\\\\dA-Znasieg]+)?)\\\\b( ?\\\\/? ?\\\\d+)?)|(\\\\bCT[L1i]\\\\b))\", \"CBI_address\", EntityType.RECOMMENDATION, $section)\n .forEach(entity -> {\n entity.skip(\"SYN.1.0\", \"\");\n entity.addEngine(Engine.RULE);\n insert(entity);\n });\n end\n\n\n//------------------------------------ CBI rules ------------------------------------\n\n// Rule unit: CBI.0\nrule \"CBI.0.0: Redact CBI Authors (Non Vertebrate Study)\"\n when\n not FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $entity: RedactionEntity(type == \"CBI_author\", dictionaryEntry)\n then\n $entity.apply(\"CBI.0.0\", \"Author found\", \"Article 39(e)(3) of Regulation (EC) No 178/2002\");\n end\n\nrule \"CBI.0.1: Redact CBI Authors (Vertebrate Study)\"\n when\n FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $entity: RedactionEntity(type == \"CBI_author\", dictionaryEntry)\n then\n $entity.apply(\"CBI.0.1\", \"Author found\", \"Article 39(e)(2) of Regulation (EC) No 178/2002\");\n end\n\n\n// Rule unit: CBI.1\nrule \"CBI.1.0: Don't redact CBI Address (Non Vertebrate Study)\"\n when\n not FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $entity: RedactionEntity(type == \"CBI_address\", dictionaryEntry)\n then\n $entity.skip(\"CBI.1.0\", \"Address found for Non Vertebrate Study\");\n end\n\nrule \"CBI.1.1: Redact CBI Address (Vertebrate Study)\"\n when\n FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $entity: RedactionEntity(type == \"CBI_address\", dictionaryEntry)\n then\n $entity.apply(\"CBI.1.1\", \"Address found\", \"Article 39(e)(2) of Regulation (EC) No 178/2002\");\n end\n\n\n// Rule unit: CBI.2\nrule \"CBI.2.0: Don't redact genitive CBI_author\"\n when\n $entity: RedactionEntity(type == \"CBI_author\", anyMatch(textAfter, \"['’’'ʼˈ´`‘′ʻ’']s\"), isApplied())\n then\n RedactionEntity falsePositive = entityCreationService.byBoundary($entity.getBoundary(), \"CBI_author\", EntityType.FALSE_POSITIVE, document);\n falsePositive.skip(\"CBI.2.0\", \"Genitive Author found\");\n insert(falsePositive);\n end\n\n\n// Rule unit: CBI.7\nrule \"CBI.7.0: Do not redact Names and Addresses if published information found in section without tables\"\n when\n $section: Section(!hasTables(),\n hasEntitiesOfType(\"published_information\"),\n (hasEntitiesOfType(\"CBI_author\") || hasEntitiesOfType(\"CBI_address\")))\n then\n $section.getEntitiesOfType(List.of(\"CBI_author\", \"CBI_address\"))\n .forEach(redactionEntity -> {\n redactionEntity.skipWithReferences(\n \"CBI.7.0\",\n \"Published Information found in section\",\n $section.getEntitiesOfType(\"published_information\")\n );\n });\n end\n\nrule \"CBI.7.1: Do not redact Names and Addresses if published information found in same table row\"\n when\n $table: Table(hasEntitiesOfType(\"published_information\"),\n (hasEntitiesOfType(\"CBI_author\") || hasEntitiesOfType(\"CBI_address\")))\n then\n $table.streamEntitiesWhereRowContainsEntitiesOfType(List.of(\"CBI_author\", \"CBI_address\"))\n .forEach(redactionEntity -> {\n redactionEntity.skipWithReferences(\n \"CBI.7.1\",\n \"Published Information found in row\",\n $table.getEntitiesOfTypeInSameRow(\"published_information\", redactionEntity)\n );\n });\n end\n\n\n// Rule unit: CBI.9\nrule \"CBI.9.0: Redact all Cell's with Header Author(s) as CBI_author (non vertebrate study)\"\n agenda-group \"LOCAL_DICTIONARY_ADDS\"\n when\n not FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $table: Table(hasHeader(\"Author(s)\"))\n then\n $table.streamTableCellsWithHeader(\"Author(s)\")\n .map(tableCell -> entityCreationService.bySemanticNode(tableCell, \"CBI_author\", EntityType.ENTITY))\n .filter(Optional::isPresent)\n .map(Optional::get)\n .forEach(redactionEntity -> {\n redactionEntity.apply(\"CBI.9.0\", \"Author(s) found\", \"Article 39(e)(3) of Regulation (EC) No 178/2002\");\n redactionEntity.addEngine(Engine.RULE);\n insert(redactionEntity);\n });\n end\n\nrule \"CBI.9.1: Redact all Cell's with Header Author as CBI_author (non vertebrate study)\"\n agenda-group \"LOCAL_DICTIONARY_ADDS\"\n when\n not FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $table: Table(hasHeader(\"Author\"))\n then\n $table.streamTableCellsWithHeader(\"Author\")\n .map(tableCell -> entityCreationService.bySemanticNode(tableCell, \"CBI_author\", EntityType.ENTITY))\n .filter(Optional::isPresent)\n .map(Optional::get)\n .forEach(redactionEntity -> {\n redactionEntity.apply(\"CBI.9.1\", \"Author found\", \"Article 39(e)(3) of Regulation (EC) No 178/2002\");\n redactionEntity.addEngine(Engine.RULE);\n insert(redactionEntity);\n });\n end\n\n\n// Rule unit: CBI.10\nrule \"CBI.10.0: Redact all Cell's with Header Author(s) as CBI_author (vertebrate study)\"\n agenda-group \"LOCAL_DICTIONARY_ADDS\"\n when\n FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $table: Table(hasHeader(\"Author(s)\"))\n then\n $table.streamTableCellsWithHeader(\"Author(s)\")\n .map(tableCell -> entityCreationService.bySemanticNode(tableCell, \"CBI_author\", EntityType.ENTITY))\n .filter(Optional::isPresent)\n .map(Optional::get)\n .forEach(redactionEntity -> {\n redactionEntity.apply(\"CBI.10.0\", \"Author(s) found\", \"Article 39(e)(2) of Regulation (EC) No 178/2002\");\n redactionEntity.addEngine(Engine.RULE);\n insert(redactionEntity);\n });\n end\n\nrule \"CBI.10.1: Redact all Cell's with Header Author as CBI_author (vertebrate study)\"\n agenda-group \"LOCAL_DICTIONARY_ADDS\"\n when\n FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $table: Table(hasHeader(\"Author\"))\n then\n $table.streamTableCellsWithHeader(\"Author\")\n .map(tableCell -> entityCreationService.bySemanticNode(tableCell, \"CBI_author\", EntityType.ENTITY))\n .filter(Optional::isPresent)\n .map(Optional::get)\n .forEach(redactionEntity -> {\n redactionEntity.apply(\"CBI.10.1\", \"Author found\", \"Article 39(e)(2) of Regulation (EC) No 178/2002\");\n redactionEntity.addEngine(Engine.RULE);\n insert(redactionEntity);\n });\n end\n\n\n// Rule unit: CBI.11\nrule \"CBI.11.0: Recommend all CBI_author entities in Table with Vertebrate Study Y/N Header\"\n agenda-group \"LOCAL_DICTIONARY_ADDS\"\n salience -1\n when\n $table: Table(hasHeader(\"Author(s)\") && hasHeader(\"Vertebrate Study Y/N\"))\n then\n $table.getEntitiesOfType(\"CBI_author\").forEach(entity -> dictionary.addMultipleAuthorsAsRecommendation(entity));\n end\n\n\n// Rule unit: CBI.16\nrule \"CBI.16.0: Add CBI_author with \\\"et al.\\\" Regex (non vertebrate study)\"\n agenda-group \"LOCAL_DICTIONARY_ADDS\"\n when\n not FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $section: Section(containsString(\"et al.\"))\n then\n entityCreationService.byRegex(\"\\\\b([A-ZÄÖÜ][^\\\\s\\\\.,]+( [A-ZÄÖÜ]{1,2}\\\\.?)?( ?[A-ZÄÖÜ]\\\\.?)?) et al\\\\.?\", \"CBI_author\", EntityType.ENTITY, 1, $section)\n .forEach(entity -> {\n entity.apply(\"CBI.16.0\", \"Author found by \\\"et al\\\" regex\", \"Article 39(e)(3) of Regulation (EC) No 178/2002\");\n entity.addEngine(Engine.RULE);\n dictionary.addLocalDictionaryEntry(\"CBI_author\", entity.getValue(), false);\n insert(entity);\n });\n end\n\nrule \"CBI.16.1: Add CBI_author with \\\"et al.\\\" Regex (vertebrate study)\"\n agenda-group \"LOCAL_DICTIONARY_ADDS\"\n when\n FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $section: Section(containsString(\"et al.\"))\n then\n entityCreationService.byRegex(\"\\\\b([A-ZÄÖÜ][^\\\\s\\\\.,]+( [A-ZÄÖÜ]{1,2}\\\\.?)?( ?[A-ZÄÖÜ]\\\\.?)?) et al\\\\.?\", \"CBI_author\", EntityType.ENTITY, 1, $section)\n .forEach(entity -> {\n entity.apply(\"CBI.16.1\", \"Author found by \\\"et al\\\" regex\", \"Article 39(e)(2) of Regulation (EC) No 178/2002\");\n entity.addEngine(Engine.RULE);\n insert(entity);\n dictionary.addLocalDictionaryEntry(\"CBI_author\", entity.getValue(), false);\n });\n end\n\n\n// Rule unit: CBI.17\nrule \"CBI.17.0: Add recommendation for Addresses in Test Organism sections, without colon\"\n when\n $section: Section(!hasTables(), containsString(\"Species\") && containsString(\"Source\") && !containsString(\"Species:\") && !containsString(\"Source:\"))\n then\n entityCreationService.lineAfterString(\"Source\", \"CBI_address\", EntityType.RECOMMENDATION, $section)\n .forEach(entity -> {\n entity.addEngine(Engine.RULE);\n entity.skip(\"CBI.17.0\", \"Line after \\\"Source\\\" in Test Organism Section\");\n insert(entity);\n });\n end\n\nrule \"CBI.17.1: Add recommendation for Addresses in Test Organism sections, with colon\"\n when\n $section: Section(!hasTables(), containsString(\"Species:\"), containsString(\"Source:\"))\n then\n entityCreationService.lineAfterString(\"Source:\", \"CBI_address\", EntityType.RECOMMENDATION, $section)\n .forEach(entity -> {\n entity.addEngine(Engine.RULE);\n entity.skip(\"CBI.17.1\", \"Line after \\\"Source:\\\" in Test Animals Section\");\n insert(entity);\n });\n end\n\n\n// Rule unit: CBI.20\nrule \"CBI.20.0: Redact between \\\"PERFORMING LABORATORY\\\" and \\\"LABORATORY PROJECT ID:\\\" (non vertebrate study)\"\n agenda-group \"LOCAL_DICTIONARY_ADDS\"\n when\n not FileAttribute(label == \"Vertebrate Study\", value == \"Yes\")\n $section: Section(!hasTables(), containsString(\"PERFORMING LABORATORY:\"), containsString(\"LABORATORY PROJECT ID:\"))\n then\n entityCreationService.betweenStrings(\"PERFORMING LABORATORY:\", \"LABORATORY PROJECT ID:\", \"CBI_address\", EntityType.ENTITY, $section)\n .forEach(laboratoryEntity -> {\n laboratoryEntity.skip(\"CBI.20.0\", \"PERFORMING LABORATORY was found for non vertebrate study\");\n laboratoryEntity.addEngine(Engine.RULE);\n dictionary.addLocalDictionaryEntry(laboratoryEntity);\n insert(laboratoryEntity);\n });\n end\n\nrule \"CBI.20.1: Redact between \\\"PERFORMING LABORATORY\\\" and \\\"LABORATORY PROJECT ID:\\\" (vertebrate study)\"\n agenda-group \"LOCAL_DICTIONARY_ADDS\"\n when\n FileAttribute(label == \"Vertebrate Study\", value == \"Yes\")\n $section: Section(!hasTables(), containsString(\"PERFORMING LABORATORY:\"), containsString(\"LABORATORY PROJECT ID:\"))\n then\n entityCreationService.betweenStrings(\"PERFORMING LABORATORY:\", \"LABORATORY PROJECT ID:\", \"CBI_address\", EntityType.ENTITY, $section)\n .forEach(laboratoryEntity -> {\n laboratoryEntity.apply(\"CBI.20.1\", \"PERFORMING LABORATORY was found\", \"Article 39(e)(2) of Regulation (EC) No 178/2002\");\n laboratoryEntity.addEngine(Engine.RULE);\n dictionary.addLocalDictionaryEntry(laboratoryEntity);\n insert(laboratoryEntity);\n });\n end\n\n\n//------------------------------------ PII rules ------------------------------------\n\n// Rule unit: PII.0\nrule \"PII.0.0: Redact all PII (non vertebrate study)\"\n when\n not FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $pii: RedactionEntity(type == \"PII\", dictionaryEntry)\n then\n $pii.apply(\"PII.0.0\", \"Personal Information found\", \"Article 39(e)(3) of Regulation (EC) No 178/2002\");\n end\n\nrule \"PII.0.1: Redact all PII (vertebrate study)\"\n when\n FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $pii: RedactionEntity(type == \"PII\", dictionaryEntry)\n then\n $pii.apply(\"PII.0.1\", \"Personal Information found\", \"Article 39(e)(2) of Regulation (EC) No 178/2002\");\n end\n\n\n// Rule unit: PII.1\nrule \"PII.1.0: Redact Emails by RegEx (Non vertebrate study)\"\n when\n not FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $section: Section(containsString(\"@\"))\n then\n entityCreationService.byRegex(\"\\\\b([A-Za-z0-9._%+\\\\-]+@[A-Za-z0-9.\\\\-]+\\\\.[A-Za-z\\\\-]{1,23}[A-Za-z])\\\\b\", \"PII\", EntityType.ENTITY, 1, $section)\n .forEach(emailEntity -> {\n emailEntity.addEngine(Engine.RULE);\n emailEntity.apply(\"PII.1.0\", \"Found by Email Regex\", \"Article 39(e)(3) of Regulation (EC) No 178/2002\");\n insert(emailEntity);\n });\n end\n\nrule \"PII.1.1: Redact Emails by RegEx (vertebrate study)\"\n when\n FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $section: Section(containsString(\"@\"))\n then\n entityCreationService.byRegex(\"\\\\b([A-Za-z0-9._%+\\\\-]+@[A-Za-z0-9.\\\\-]+\\\\.[A-Za-z\\\\-]{1,23}[A-Za-z])\\\\b\", \"PII\", EntityType.ENTITY, 1, $section)\n .forEach(emailEntity -> {\n emailEntity.addEngine(Engine.RULE);\n emailEntity.apply(\"PII.1.1\", \"Found by Email Regex\", \"Article 39(e)(3) of Regulation (EC) No 178/2002\");\n insert(emailEntity);\n });\n end\n\n\n// Rule unit: PII.2\nrule \"PII.2.0: Redact Phone and Fax by RegEx (non vertebrate study)\"\n when\n not FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $section: Section(containsString(\"Contact\") ||\n containsString(\"Telephone\") ||\n containsString(\"Phone\") ||\n containsString(\"Ph.\") ||\n containsString(\"Fax\") ||\n containsString(\"Tel\") ||\n containsString(\"Ter\") ||\n containsString(\"Mobile\") ||\n containsString(\"Fel\") ||\n containsString(\"Fer\"))\n then\n entityCreationService.byRegexIgnoreCase(\"\\\\b(contact|telephone|phone|ph\\\\.|fax|tel|ter|mobile|fel|fer)[a-zA-Z\\\\s]{0,10}[:.\\\\s]{0,3}([\\\\+\\\\d\\\\(][\\\\s\\\\d\\\\(\\\\)\\\\-\\\\/\\\\.]{4,100}\\\\d)\\\\b\", \"PII\", EntityType.ENTITY, 2, $section)\n .forEach(contactEntity -> {\n contactEntity.addEngine(Engine.RULE);\n contactEntity.apply(\"PII.2.0\", \"Found by Phone and Fax Regex\", \"Article 39(e)(3) of Regulation (EC) No 178/2002\");\n insert(contactEntity);\n });\n end\n\nrule \"PII.2.1: Redact Phone and Fax by RegEx (vertebrate study)\"\n when\n FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $section: Section(containsString(\"Contact\") ||\n containsString(\"Telephone\") ||\n containsString(\"Phone\") ||\n containsString(\"Ph.\") ||\n containsString(\"Fax\") ||\n containsString(\"Tel\") ||\n containsString(\"Ter\") ||\n containsString(\"Mobile\") ||\n containsString(\"Fel\") ||\n containsString(\"Fer\"))\n then\n entityCreationService.byRegexIgnoreCase(\"\\\\b(contact|telephone|phone|ph\\\\.|fax|tel|ter|mobile|fel|fer)[a-zA-Z\\\\s]{0,10}[:.\\\\s]{0,3}([\\\\+\\\\d\\\\(][\\\\s\\\\d\\\\(\\\\)\\\\-\\\\/\\\\.]{4,100}\\\\d)\\\\b\", \"PII\", EntityType.ENTITY, 2, $section)\n .forEach(contactEntity -> {\n contactEntity.addEngine(Engine.RULE);\n contactEntity.apply(\"PII.2.1\", \"Found by Phone and Fax Regex\", \"Article 39(e)(2) of Regulation (EC) No 178/2002\");\n insert(contactEntity);\n });\n end\n\n\n// Rule unit: PII.9\nrule \"PII.9.0: Redact between \\\"AUTHOR(S)\\\" and \\\"COMPLETION DATE\\\" (non vertebrate study)\"\n when\n not FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $section: Section(!hasTables(), containsString(\"AUTHOR(S):\"), containsString(\"COMPLETION DATE:\"), !containsString(\"STUDY COMPLETION DATE:\"))\n then\n entityCreationService.betweenStrings(\"AUTHOR(S):\", \"COMPLETION DATE:\", \"PII\", EntityType.ENTITY, $section)\n .forEach(authorEntity -> {\n authorEntity.apply(\"PII.9.0\", \"AUTHOR(S) was found\", \"Article 39(e)(3) of Regulation (EC) No 178/2002\");\n authorEntity.addEngine(Engine.RULE);\n insert(authorEntity);\n });\n end\n\nrule \"PII.9.1: Redact between \\\"AUTHOR(S)\\\" and \\\"STUDY COMPLETION DATE\\\" (non vertebrate study)\"\n when\n FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $section: Section(!hasTables(), containsString(\"AUTHOR(S):\"), containsString(\"COMPLETION DATE:\"), !containsString(\"STUDY COMPLETION DATE:\"))\n then\n entityCreationService.betweenStrings(\"AUTHOR(S):\", \"COMPLETION DATE:\", \"PII\", EntityType.ENTITY, $section)\n .forEach(authorEntity -> {\n authorEntity.apply(\"PII.9.1\", \"AUTHOR(S) was found\", \"Article 39(e)(2) of Regulation (EC) No 178/2002\");\n authorEntity.addEngine(Engine.RULE);\n insert(authorEntity);\n });\n end\n\nrule \"PII.9.2: Redact between \\\"AUTHOR(S)\\\" and \\\"COMPLETION DATE\\\" (non vertebrate study)\"\n when\n not FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $section: Section(!hasTables(), containsString(\"AUTHOR(S):\"), containsString(\"STUDY COMPLETION DATE:\"))\n then\n entityCreationService.betweenStrings(\"AUTHOR(S):\", \"STUDY COMPLETION DATE:\", \"PII\", EntityType.ENTITY, $section)\n .forEach(authorEntity -> {\n authorEntity.apply(\"PII.9.2\", \"AUTHOR(S) was found\", \"Article 39(e)(3) of Regulation (EC) No 178/2002\");\n authorEntity.addEngine(Engine.RULE);\n insert(authorEntity);\n });\n end\n\nrule \"PII.9.3: Redact between \\\"AUTHOR(S)\\\" and \\\"STUDY COMPLETION DATE\\\" (vertebrate study)\"\n when\n FileAttribute(label == \"Vertebrate Study\", value.toLowerCase() == \"yes\")\n $section: Section(!hasTables(), containsString(\"AUTHOR(S):\"), containsString(\"STUDY COMPLETION DATE:\"))\n then\n entityCreationService.betweenStrings(\"AUTHOR(S):\", \"STUDY COMPLETION DATE:\", \"PII\", EntityType.ENTITY, $section)\n .forEach(authorEntity -> {\n authorEntity.apply(\"PII.9.3\", \"AUTHOR(S) was found\", \"Article 39(e)(2) of Regulation (EC) No 178/2002\");\n authorEntity.addEngine(Engine.RULE);\n insert(authorEntity);\n });\n end\n\n\n//------------------------------------ Other rules ------------------------------------\n\n// Rule unit: ETC.0\nrule \"ETC.0.0: Purity Hint\"\n when\n $section: Section(containsStringIgnoreCase(\"purity\"))\n then\n entityCreationService.byRegexIgnoreCase(\"(purity ?( of|\\\\(.{1,20}\\\\))?( ?:)?) .{0,5}[\\\\d\\\\.]+( .{0,4}\\\\.)? ?%\", \"hint_only\", EntityType.ENTITY, 1, $section)\n .forEach(hint -> {\n hint.addEngine(Engine.RULE);\n hint.skip(\"ETC.0.0\", \"\");\n });\n end\n\n\n// Rule unit: ETC.2\nrule \"ETC.2.0: Redact signatures (non vertebrate study)\"\n when\n not FileAttribute(label == \"Vertebrate Study\", value == \"Yes\")\n $signature: Image(imageType == ImageType.SIGNATURE)\n then\n $signature.apply(\"ETC.2.0\", \"Signature Found\", \"Article 39(e)(3) of Regulation (EC) No 178/2002\");\n end\n\nrule \"ETC.2.0: Redact signatures (vertebrate study)\"\n when\n FileAttribute(label == \"Vertebrate Study\", value == \"Yes\")\n $signature: Image(imageType == ImageType.SIGNATURE)\n then\n $signature.apply(\"ETC.2.0\", \"Signature Found\", \"Article 39(e)(2) of Regulation (EC) No 178/2002\");\n end\n\n\n// Rule unit: ETC.3\nrule \"ETC.3.0: Redact logos (vertebrate study)\"\n when\n not FileAttribute(label == \"Vertebrate Study\", value == \"Yes\")\n $logo: Image(imageType == ImageType.LOGO)\n then\n $logo.apply(\"ETC.3.0\", \"Logo Found\", \"Article 39(e)(3) of Regulation (EC) No 178/2002\");\n end\n\nrule \"ETC.3.1: Redact logos (non vertebrate study)\"\n when\n FileAttribute(label == \"Vertebrate Study\", value == \"Yes\")\n $logo: Image(imageType == ImageType.LOGO)\n then\n $logo.apply(\"ETC.3.1\", \"Logo Found\", \"Article 39(e)(2) of Regulation (EC) No 178/2002\");\n end\n\n\n// Rule unit: ETC.5\nrule \"ETC.5.0: Ignore dossier_redaction entries if confidentiality is not 'confidential'\"\n when\n not FileAttribute(label == \"Confidentiality\", value == \"confidential\")\n $dossierRedaction: RedactionEntity(type == \"dossier_redaction\")\n then\n $dossierRedaction.removeFromGraph();\n retract($dossierRedaction);\n end\n\n\n//------------------------------------ AI rules ------------------------------------\n\n// Rule unit: AI.0\nrule \"AI.0.0: add all NER Entities of type CBI_author\"\n salience 999\n when\n nerEntities: NerEntities(hasEntitiesOfType(\"CBI_author\"))\n then\n nerEntities.streamEntitiesOfType(\"CBI_author\")\n .map(nerEntity -> entityCreationService.byNerEntity(nerEntity, EntityType.RECOMMENDATION, document))\n .forEach(entity -> insert(entity));\n end\n\n\n// Rule unit: AI.1\nrule \"AI.1.0: combine and add NER Entities as CBI_address\"\n salience 999\n when\n nerEntities: NerEntities(hasEntitiesOfType(\"ORG\") || hasEntitiesOfType(\"STREET\") || hasEntitiesOfType(\"CITY\"))\n then\n nerEntitiesAdapter.combineNerEntitiesToCbiAddressDefaults(nerEntities)\n .map(boundary -> entityCreationService.byBoundary(boundary, \"CBI_address\", EntityType.RECOMMENDATION, document))\n .forEach(entity -> {\n entity.addEngine(Engine.NER);\n insert(entity);\n });\n end\n\n\n//------------------------------------ Manual redaction rules ------------------------------------\n\n// Rule unit: MAN.0\nrule \"MAN.0.0: Apply manual resize redaction\"\n salience 128\n when\n $resizeRedaction: ManualResizeRedaction($id: annotationId)\n $entityToBeResized: RedactionEntity(matchesAnnotationId($id))\n then\n manualRedactionApplicationService.resize($entityToBeResized, $resizeRedaction);\n retract($resizeRedaction);\n update($entityToBeResized);\n end\n\n\n// Rule unit: MAN.1\nrule \"MAN.1.0: Apply id removals that are valid and not in forced redactions to Entity\"\n salience 128\n when\n IdRemoval(status == AnnotationStatus.APPROVED, !removeFromDictionary, requestDate != null, $id: annotationId)\n not ManualForceRedaction($id == annotationId, status == AnnotationStatus.APPROVED, requestDate != null)\n $entityToBeRemoved: RedactionEntity(matchesAnnotationId($id))\n then\n $entityToBeRemoved.setIgnored(true);\n end\n\nrule \"MAN.1.1: Apply id removals that are valid and not in forced redactions to Image\"\n salience 128\n when\n IdRemoval(status == AnnotationStatus.APPROVED, !removeFromDictionary, requestDate != null, $id: annotationId)\n not ManualForceRedaction($id == annotationId, status == AnnotationStatus.APPROVED, requestDate != null)\n $imageEntityToBeRemoved: Image($id == id)\n then\n $imageEntityToBeRemoved.setIgnored(true);\n end\n\n\n// Rule unit: MAN.2\nrule \"MAN.2.0: Apply force redaction\"\n salience 128\n when\n ManualForceRedaction($id: annotationId, status == AnnotationStatus.APPROVED, requestDate != null, $legalBasis: legalBasis)\n $entityToForce: RedactionEntity(matchesAnnotationId($id))\n then\n $entityToForce.apply(\"MAN.2.0\", \"Forced redaction\", $legalBasis);\n $entityToForce.setSkipRemoveEntitiesContainedInLarger(true);\n end\n\n\n// Rule unit: MAN.3\nrule \"MAN.3.0: Apply image recategorization\"\n salience 128\n when\n ManualImageRecategorization($id: annotationId, status == AnnotationStatus.APPROVED, $imageType: type)\n $image: Image($id == id)\n then\n $image.setImageType(ImageType.fromString($imageType));\n end\n\n\n//------------------------------------ Entity merging rules ------------------------------------\n\n// Rule unit: X.0\nrule \"X.0.0: remove Entity contained by Entity of same type\"\n salience 65\n when\n $larger: RedactionEntity($type: type, $entityType: entityType)\n $contained: RedactionEntity(containedBy($larger), type == $type, entityType == $entityType, this != $larger, !resized, !skipRemoveEntitiesContainedInLarger)\n then\n $contained.removeFromGraph();\n retract($contained);\n end\n\n\n// Rule unit: X.1\nrule \"X.1.0: merge intersecting Entities of same type\"\n salience 64\n when\n $first: RedactionEntity($type: type, $entityType: entityType, !resized, !skipRemoveEntitiesContainedInLarger)\n $second: RedactionEntity(intersects($first), type == $type, entityType == $entityType, this != $first, !resized, !skipRemoveEntitiesContainedInLarger)\n then\n $first.removeFromGraph();\n $second.removeFromGraph();\n RedactionEntity mergedEntity = entityCreationService.byEntities(List.of($first, $second), $type, $entityType, document);\n retract($first);\n retract($second);\n insert(mergedEntity);\n end\n\n\n// Rule unit: X.2\nrule \"X.2.0: remove Entity of type ENTITY when contained by FALSE_POSITIVE\"\n salience 64\n when\n $falsePositive: RedactionEntity($type: type, entityType == EntityType.FALSE_POSITIVE)\n $entity: RedactionEntity(containedBy($falsePositive), type == $type, entityType == EntityType.ENTITY, !resized, !skipRemoveEntitiesContainedInLarger)\n then\n $entity.removeFromGraph();\n retract($entity)\n end\n\n\n// Rule unit: X.3\nrule \"X.3.0: remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION\"\n salience 64\n when\n $falseRecommendation: RedactionEntity($type: type, entityType == EntityType.FALSE_RECOMMENDATION)\n $recommendation: RedactionEntity(containedBy($falseRecommendation), type == $type, entityType == EntityType.RECOMMENDATION, !resized, !skipRemoveEntitiesContainedInLarger)\n then\n $recommendation.removeFromGraph();\n retract($recommendation);\n end\n\n\n// Rule unit: X.4\nrule \"X.4.0: remove Entity of type RECOMMENDATION when intersected by ENTITY with same type\"\n salience 256\n when\n $entity: RedactionEntity($type: type, entityType == EntityType.ENTITY)\n $recommendation: RedactionEntity(intersects($entity), type == $type, entityType == EntityType.RECOMMENDATION, !resized, !skipRemoveEntitiesContainedInLarger)\n then\n $entity.addEngines($recommendation.getEngines());\n $recommendation.removeFromGraph();\n retract($recommendation);\n end\n\n\n// Rule unit: X.5\nrule \"X.5.0: remove Entity of type RECOMMENDATION when contained by ENTITY\"\n salience 256\n when\n $entity: RedactionEntity(entityType == EntityType.ENTITY)\n $recommendation: RedactionEntity(containedBy($entity), entityType == EntityType.RECOMMENDATION, !resized, !skipRemoveEntitiesContainedInLarger)\n then\n $recommendation.removeFromGraph();\n retract($recommendation);\n end\n\n\n// Rule unit: X.6\nrule \"X.6.0: remove Entity of lower rank, when intersected by entity of type ENTITY\"\n salience 32\n when\n $higherRank: RedactionEntity($type: type, entityType == EntityType.ENTITY)\n $lowerRank: RedactionEntity(intersects($higherRank), type != $type, dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), !resized, !skipRemoveEntitiesContainedInLarger)\n then\n $lowerRank.removeFromGraph();\n retract($lowerRank);\n end\n\n\n//------------------------------------ File attributes rules ------------------------------------\n\n// Rule unit: FA.1\nrule \"FA.1.0: remove duplicate FileAttributes\"\n salience 64\n when\n $fileAttribute: FileAttribute($label: label, $value: value)\n $duplicate: FileAttribute(this != $fileAttribute, label == $label, value == $value)\n then\n retract($duplicate);\n end\n\n\n//------------------------------------ Local dictionary search rules ------------------------------------\n\n// Rule unit: LDS.0\nrule \"LDS.0.0: run local dictionary search\"\n agenda-group \"LOCAL_DICTIONARY_ADDS\"\n salience -999\n when\n DictionaryModel(!localEntries.isEmpty(), $type: type, $searchImplementation: localSearch) from dictionary.getDictionaryModels()\n then\n entityCreationService.bySearchImplementation($searchImplementation, $type, EntityType.RECOMMENDATION, document)\n .forEach(entity -> {\n entity.addEngine(Engine.RULE);\n insert(entity);\n });\n end\n" \ No newline at end of file diff --git a/redaction-service-v1/rules-management/src/test/resources/dev/Basf-Demo/rules.txt b/redaction-service-v1/rules-management/src/test/resources/dev/Basf-Demo/rules.txt index d0051b4e..2020709c 100644 --- a/redaction-service-v1/rules-management/src/test/resources/dev/Basf-Demo/rules.txt +++ b/redaction-service-v1/rules-management/src/test/resources/dev/Basf-Demo/rules.txt @@ -1 +1 @@ -"package drools\n\nimport static java.lang.String.format;\nimport static com.iqser.red.service.redaction.v1.server.utils.RedactionSearchUtility.anyMatch;\nimport static com.iqser.red.service.redaction.v1.server.utils.RedactionSearchUtility.exactMatch;\n\nimport java.util.List;\nimport java.util.LinkedList;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport java.util.Collection;\nimport java.util.stream.Stream;\nimport java.util.Optional;\n\nimport com.iqser.red.service.redaction.v1.server.model.document.*;\nimport com.iqser.red.service.redaction.v1.server.model.document.TextRange;\nimport com.iqser.red.service.redaction.v1.server.model.document.entity.*;\nimport com.iqser.red.service.redaction.v1.server.model.document.entity.EntityType;\nimport com.iqser.red.service.redaction.v1.server.model.document.entity.MatchedRule;\nimport com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity\nimport com.iqser.red.service.redaction.v1.server.model.document.entity.MatchedRule\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.*;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Section;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Table;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.SemanticNode;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Document;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Paragraph;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Image;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.ImageType;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Page;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Headline;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.SectionIdentifier;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Footer;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Header;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.NodeType;\nimport com.iqser.red.service.redaction.v1.server.model.document.textblock.*;\nimport com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlock;\nimport com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlockCollector;\nimport com.iqser.red.service.redaction.v1.server.model.document.textblock.AtomicTextBlock;\nimport com.iqser.red.service.redaction.v1.server.model.document.textblock.ConcatenatedTextBlock;\nimport com.iqser.red.service.redaction.v1.server.model.NerEntities;\nimport com.iqser.red.service.redaction.v1.server.model.dictionary.Dictionary;\nimport com.iqser.red.service.redaction.v1.server.model.dictionary.DictionaryModel;\nimport com.iqser.red.service.redaction.v1.server.service.document.EntityCreationService;\nimport com.iqser.red.service.redaction.v1.server.service.ManualChangesApplicationService;\nimport com.iqser.red.service.redaction.v1.server.utils.RedactionSearchUtility;\n\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.FileAttribute;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Engine;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualResizeRedaction;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.IdRemoval;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualForceRedaction;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualRecategorization;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualLegalBasisChange;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.AnnotationStatus;\n\nglobal Document document\nglobal EntityCreationService entityCreationService\nglobal ManualChangesApplicationService manualChangesApplicationService\nglobal Dictionary dictionary\n\n//------------------------------------ queries ------------------------------------\n\nquery \"getFileAttributes\"\n $fileAttribute: FileAttribute()\n end\n\n//---------------------------------------------------------------------------\n\n\n\nrule \"H.0.0: retract table of contents page\"\n when\n $page: Page(getMainBodyTextBlock().getSearchText().contains(\"........\") || (getMainBodyTextBlock().getSearchText().contains(\"APPENDICES\") && getMainBodyTextBlock().getSearchText().contains(\"TABLES\")))\n $node: SemanticNode(onPage($page.getNumber()), !onPage($page.getNumber() -1), getType() != NodeType.IMAGE)\n then\n retract($node);\n end\n\n\nrule \"H.0.1: Ignore Table of Contents\"\n salience 10\n when\n $tocHeadline: Headline(containsString(\"CONTENTS\"))\n\n then\n $tocHeadline.getParent().getPages()\n .forEach(page -> page.getMainBody().stream()\n .filter(node -> !node.getType().equals(NodeType.IMAGE))\n .filter(node -> node.getPages().stream().noneMatch(nodePage -> nodePage.getNumber() < page.getNumber()))\n .forEach(node -> retract(node))\n );\n end\n \n\n\n/*\nrule \"H.0.0: Show headlines\"\n when\n $headline: Headline()\n then\n entityCreationService.bySemanticNode($headline, \"headline\", EntityType.RECOMMENDATION);\n end\n*/\n\nrule \"DOC.0.0: Study Type File Attribute\"\n when\n not FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"402\",\"403\",\"404\",\"405\",\"408\",\"414\",\"425\",\"429\",\"436\",\"438\",\"439\",\"471\",\"487\"))\n $section: Section(\n onPage(1)\n ,(containsString(\"OECD\") || containsString(\"EPA\") || containsString(\"OPPTS\"))\n )\n then\n Stream.of(RedactionSearchUtility.findTextRangesByRegexIgnoreCase(\"(?<=OECD)(?:[\\\\w\\\\s,\\\\[\\\\]\\\\(\\\\)\\\\.]{1,10}|(?:.{5,40}(?:Number |Procedure |Guideline )))(4[\\\\d]{2})\", 1, $section.getTextBlock()),\n RedactionSearchUtility.findTextRangesByRegexIgnoreCase(\"(?<=OECD).{5,40}Method (4[\\\\d]{2}).{1,65}(\\\\d{4})\\\\)\", 1, $section.getTextBlock()),\n RedactionSearchUtility.findTextRangesByRegexIgnoreCase(\"(?<=OECD) Guideline (4\\\\d{2})\", 1, $section.getTextBlock()),\n RedactionSearchUtility.findTextRangesByRegexIgnoreCase(\"(?<=OECD) Guideline, Method No. (\\\\d{3})\", 1, $section.getTextBlock()) \n ).flatMap(Collection::stream).findFirst()\n .map(boundary -> $section.getTextBlock().subSequence(boundary).toString())\n .map(value -> FileAttribute.builder().label(\"OECD Number\").value(value).build())\n .ifPresent(fileAttribute -> insert(fileAttribute));\n end\n\n\nrule \"DOC.1.0: Guidelines\"\n when\n $section: Section(\n onPage(1)\n && (\n containsString(\"OECD\")\n || containsString(\"EPA\")\n || containsString(\"OPPTS\")\n )\n )\n then\n entityCreationService.byRegex(\"OECD (No\\\\.? )?\\\\d{3}( \\\\(\\\\d{4}\\\\))?\", \"oecd_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline found\")\n );\n entityCreationService.byRegex(\"OECD[\\\\s,]{1}(?:.{1,40}.(?>Procedure|Method).{1,20}\\\\d{3,4}(?>.{1,100}\\\\d{4}\\\\))?|\\\\[.{1,20}.Skin.{1,20}\\\\]|[\\\\d\\\\s,\\\\(\\\\)]{7,10}|[\\\\w\\\\.\\\\s]{1,15}[\\\\d]{3}\\\\s\\\\(\\\\d{4}\\\\)|.{0,20}[N|n]umber\\\\s\\\\d{3}.{0,1}|Test Guideline \\\\d{3})\", \"oecd_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline found\")\n );\n entityCreationService.byRegex(\"EPA (OPPTS )?\\\\d{3}[. ]\\\\d{4}( \\\\(\\\\d{4}\\\\))?\", \"epa_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"EPA Guideline found\")\n );\n entityCreationService.byRegex(\"EC (Directive )?(No\\\\.? )?\\\\d{3,4}\\\\/\\\\d{3,4}((,? B(\\\\.| )\\\\d{1,2}\\\\.?)? \\\\(\\\\d{4}\\\\))?\", \"ec_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"EC Guideline found\")\n );\n entityCreationService.byRegex(\"Commission Regulation \\\\(EC\\\\) No \\\\d{3}\\\\/\\\\d{4}\", \"ec_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"EC Guideline found\")\n );\n entityCreationService.byRegex(\"OECD Method 4\\\\d{2}.{5,40}\\\\(.{5,40}\\\\d{4}\\\\)\", \"oecd_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline year found\")\n );\n// Examples found in PoC 1\n// entityCreationService.byRegex(\"((OECD Guidelines for Testing of Chemicals, Procedure)|(OECD Guidelines for the Testing of Chemicals No\\\\.)|(OECD Test Guideline)|(OECD \\\\[Test Guideline, Number)) \\\\d{3}( \\\\(\\\\d{4}\\\\))?\", \"oecd_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n// entity.apply(\"DOC.1.0\", \"OECD Guideline year found\")\n// );\n entityCreationService.byRegex(\"OPPTS (Guideline Number )?\\\\d{3}\\\\.\\\\d{4}( \\\\(\\\\d{4}\\\\))?\", \"epa_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"EPA Guideline found\")\n );\n// new approach OECD Guideline\n entityCreationService.byRegex(\"(?<=OECD)(?:[\\\\w\\\\s,\\\\[\\\\]\\\\(\\\\)\\\\.]{1,10}|.{5,40}(?:Number |Procedure |Guideline ))(4[\\\\d]{2})\", \"oecd_guideline_number\", EntityType.ENTITY,1, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline no. found\")\n );\n entityCreationService.byRegex(\"(?<=OECD)(?:[\\\\w\\\\s,\\\\[\\\\]\\\\(\\\\)\\\\.]{1,10}|.{5,40}(?:Number |Procedure |Guideline ))(4[\\\\d]{2}),?\\\\s\\\\(?(\\\\d{4})\\\\)?\", \"oecd_guideline_year\", EntityType.ENTITY,2, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline year found\")\n );\n entityCreationService.byRegex(\"(?<=OECD)[\\\\w\\\\s,\\\\[\\\\]]{1,10}\\\\((\\\\d{4})\\\\)\\\\s(4[\\\\d]{2})\", \"oecd_guideline_year\", EntityType.ENTITY,1, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline year found\")\n );\n entityCreationService.byRegex(\"(?<=OECD).{5,40}Method (4[\\\\d]{2}).{1,65}(\\\\d{4})\\\\)\", \"oecd_guideline_number\", EntityType.ENTITY,1, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline number found\")\n );\n entityCreationService.byRegex(\"(?<=OECD).{5,40}Method (4[\\\\d]{2}).{1,65}(\\\\d{4})\\\\)\", \"oecd_guideline_year\", EntityType.ENTITY,2, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline year found\")\n );\n// missing OECD guideline rules for RFP demo file\n entityCreationService.byRegex(\"(?<=OECD) Guideline (4\\\\d{2})\", \"oecd_guideline_number\", EntityType.ENTITY,1, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline number found\")\n );\n entityCreationService.byRegex(\"OECD Guideline 4\\\\d{2}\", \"oecd_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline found\")\n );\n end\n\n\nrule \"DOC.1.1: Guidelines\"\n when\n $headline: Headline(\n onPage(1),\n containsString(\"OECD\")\n )\n then\n entityCreationService.byRegex(\"(OECD (No\\\\.? )?(\\\\d{3})( \\\\(\\\\d{4}\\\\))?)\", \"oecd_guideline\", EntityType.ENTITY,1, $headline).forEach(entity ->\n entity.apply(\"DOC.1.1\", \"OECD Guideline found\")\n );\n end\n\n\nrule \"DOC.1.2: Guidelines\"\n when\n $section: Section(\n (\n containsString(\"DATA REQUIREMENT\")\n || containsString(\"TEST GUIDELINE\")\n || containsString(\"MÉTODO(S) DE REFERÊNCIA(S):\")\n )\n && (\n containsString(\"OECD\")\n || containsString(\"EPA\")\n || containsString(\"OPPTS\")\n )\n && (\n hasEntitiesOfType(\"oecd_guideline\")\n || hasEntitiesOfType(\"epa_guideline\")\n || hasEntitiesOfType(\"ec_guideline\")\n )\n )\n then\n $section.getEntitiesOfType(List.of(\"oecd_guideline\",\"ec_guideline\", \"epa_guideline\")).forEach(entity -> {\n entity.apply(\"DOC.1.2\", \"OECD guideline found.\");\n });\n end\n\n\nrule \"DOC.1.3: Remove guidelines from irrelevant sections\"\n when\n $section: Section(\n (\n hasEntitiesOfType(\"oecd_guideline\")\n || hasEntitiesOfType(\"epa_guideline\")\n || hasEntitiesOfType(\"ec_guideline\")\n )\n && !(\n (\n containsString(\"DATA REQUIREMENT\")\n || containsString(\"TEST GUIDELINE\")\n || containsString(\"MÉTODO(S) DE REFERÊNCIA(S):\")\n )\n && (\n containsString(\"OECD\")\n || containsString(\"EPA\")\n || containsString(\"OPPTS\")\n )\n )\n )\n then\n $section.getEntitiesOfType(List.of(\"oecd_guideline\", \"ec_guideline\", \"epa_guideline\")).forEach(entity -> {\n entity.remove(\"DOC.1.3\", \"remove guidelines from irrelevant sections\");\n retract(entity);\n });\n end\n\n\nrule \"DOC.3.2: Experimental Completion Date\"\n salience 10\n when \n $section: Section(onPage(1) && (containsString(\"STUDY COMPLETED ON\") || containsString(\"STUDY COMPLETION DATE\") || containsString(\"Report completion date\") || containsString(\"Date of Report\") || containsString(\"AMENDMENT COMPLETION DATE\") || containsString(\"AMENDMENT COMPLETED ON\")))\n \n then\n entityCreationService.byRegex(\"STUDY COMPLETED ON (.{5,20}\\\\d{4})\", \"experimental_end_date\", EntityType.ENTITY, 1, $section).forEach(entity -> {\n entity.apply(\"DOC.3.2\", \"Experimental end date found\");\n });\n entityCreationService.byRegex(\"STUDY COMPLETION DATE (.{5,20}\\\\d{4})\", \"experimental_end_date\", EntityType.ENTITY, 1, $section).forEach(entity -> {\n entity.apply(\"DOC.3.2\", \"Experimental end date found\");\n });\n entityCreationService.byRegex(\"Report completion date (.{5,20}\\\\d{4})\", \"experimental_end_date\", EntityType.ENTITY, 1, $section).forEach(entity -> {\n entity.apply(\"DOC.3.2\", \"Experimental end date found\");\n });\n entityCreationService.byRegex(\"Date of Report (.{5,20}\\\\d{4})\", \"experimental_end_date\", EntityType.ENTITY, 1, $section).forEach(entity -> {\n entity.apply(\"DOC.3.2\", \"Experimental end date found\");\n });\n entityCreationService.byRegex(\"AMENDMENT COMPLETION DATE (.{5,20}\\\\d{4})\", \"experimental_end_date\", EntityType.ENTITY, 1, $section).forEach(entity -> {\n entity.apply(\"DOC.3.2\", \"Experimental end date found\");\n });\n entityCreationService.byRegex(\"AMENDMENT COMPLETED ON (.{5,20}\\\\d{4})\", \"experimental_end_date\", EntityType.ENTITY, 1, $section).forEach(entity -> {\n entity.apply(\"DOC.3.2\", \"Experimental end date found\");\n });\n end\n\n\n\n\n\n // hide all skipped species and strains except in the relevant sections\n rule \"DOC.4.2: Species\"\n salience 1\n when\n $section: Section(\n (hasEntitiesOfType(\"species\") || hasEntitiesOfType(\"strain\"))\n && !(\n anyHeadlineContainsStringIgnoreCase(\"test system\")\n || anyHeadlineContainsStringIgnoreCase(\"Species and strain\")\n || anyHeadlineContainsStringIgnoreCase(\"specification\")\n )\n )\n then\n $section.getEntitiesOfType(List.of(\"species\", \"strain\")).forEach(entity -> {\n entity.remove(\"DOC.4.2\",\"n-a\");\n retract(entity);\n });\n end\n\n\nrule \"DOC.4.3: Species\"\n when\n $section: Section(hasEntitiesOfType(\"species\"))\n then\n $section.getEntitiesOfType(\"species\").forEach(entity -> {\n entity.apply(\"DOC.4.3\", \"Species found.\");\n entity.setValue(entity.getValue().toLowerCase());\n });\n end\n\n\nrule \"DOC.5.0: Strain\"\n when\n $section: Section(\n hasEntitiesOfType(\"species\")\n && hasEntitiesOfType(\"strain\")\n && (\n anyHeadlineContainsStringIgnoreCase(\"test system\")\n || anyHeadlineContainsStringIgnoreCase(\"Species and strain\")\n || anyHeadlineContainsStringIgnoreCase(\"specification\")\n )\n )\n then\n $section.getEntitiesOfType(\"strain\").forEach(entity -> {\n entity.apply(\"DOC.5.0\", \"Strain found.\");\n });\n end\n\n\n\n\nrule \"DOC.35.0: Sex\"\n when\n \n $section: Section(\n (\n anyHeadlineContainsStringIgnoreCase(\"animal\")\n || anyHeadlineContainsStringIgnoreCase(\"Species and strain\")\n || anyHeadlineContainsStringIgnoreCase(\"test system\")\n )\n && !getHeadline().containsStringIgnoreCase(\"selection\")\n && (\n containsStringIgnoreCase(\"sex:\")\n || containsStringIgnoreCase(\"male\")\n || containsStringIgnoreCase(\"female\")\n )\n )\n then\n entityCreationService.byRegexIgnoreCase(\"([S|s]ex:)?[\\\\w\\\\s]{0,10}\\\\b(males?|females?)\\\\b\", \"sex\", EntityType.ENTITY,2, $section).forEach(entity -> {\n entity.apply(\"DOC.35.0\", \"Test animal sex found\");\n });\n end\n\n\n\nrule \"DOC.6.0: Authors\"\n when\n $headline: Headline(onPage(1), containsString(\"AUTHOR\"))\n then\n entityCreationService.bySemanticNodeParagraphsOnly($headline.getParent(), \"author\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.6.0\", \"Author found\"));\n end\n\n\n\nrule \"DOC.6.2: Authors\"\n when\n $page: Page(getNumber() == 1, getMainBodyTextBlock().getSearchText() (contains \"AUTHOR(S)\" || contains \"AUTHORS\" || contains \"Author\"), getMainBodyTextBlock().getSearchText() (contains \"STUDY COMPLETED ON\" || contains \"STUDY COMPLETION DATE\" || contains \"DATE OF INTERIM REPORT\" || contains \"Report completion date\" || contains \"Date of Report\" || contains \"AMENDMENT COMPLETION DATE\"))\n then\n entityCreationService.shortestBetweenAnyString(\n List.of(\"AUTHOR(S)\", \"AUTHORS\", \"Author\"),\n List.of(\"STUDY COMPLETED ON\", \"STUDY COMPLETION DATE\", \"DATE OF INTERIM REPORT\", \"Report completion date\", \"Date of Report\", \"AMENDMENT COMPLETION DATE\"),\n \"author\",\n EntityType.ENTITY,\n document)\n .forEach(entity -> entity.apply(\"DOC.6.2\", \"Author found\"));\n end\n\n\n\n\nrule \"DOC.6.6: laboratory_project_identification\"\n when\n $page: Page(getNumber() == 1, getMainBodyTextBlock().getSearchText() (contains \"LABORATORY PROJECT IDENTIFICATION\" || contains \"TEST FACILITY PROJECT IDENTIFICATION\" || contains \"Laboratory Project Identification\"))\n then\n entityCreationService.shortestBetweenAnyString(\n List.of(\"LABORATORY PROJECT IDENTIFICATION\", \"TEST FACILITY PROJECT IDENTIFICATION\"),\n List.of(\"SPONSOR\", \"VOLUME\", \"This\"),\n \"author\",\n EntityType.ENTITY,\n document)\n .forEach(entity -> entity.apply(\"DOC.6.6\", \"Laboratory Project Identification\"));\n end\n\n\n\nrule \"DOC.7.2: study title by document structure\"\n when\n $page: Page(getNumber() == 1, getMainBodyTextBlock().getSearchText() (contains \"STUDY TITLE\" || contains \"Study Title\" || contains \"STUDYTITLE\" || contains \"Report (Final)\"))\n then\n entityCreationService.shortestBetweenAnyString(\n List.of(\"STUDY TITLE\", \"STUDYTITLE\", \"Report (Final)\"),\n List.of(\"TEST GUIDELINES\", \"TEST GUIDELINE(S)\", \"Guidelines\", \"DATA REQUIREMENT\", \"AUTHOR(S)\", \"AUTHOR\"),\n \"author\",\n EntityType.ENTITY,\n document)\n .forEach(entity -> entity.apply(\"DOC.7.2\", \"Study title found\"));\n end\n\n\n\nrule \"DOC.8.1: Performing Laboratory\"\n when\n $page: Page(getNumber() == 1, getMainBodyTextBlock().getSearchText() (contains \"PERFORMING LABORATORY\" || contains \"TEST FACILITIES\" || contains \"TEST FACILITY\" || contains \"Test Facility\"), getMainBodyTextBlock().getSearchText() (contains \"LABORATORY PROJECT IDENTIFICATION\" || contains \"TEST FACILITY PROJECT IDENTIFICATION\" || contains \"Sponsor\"))\n then\n entityCreationService.shortestBetweenAnyString(\n List.of(\"PERFORMING LABORATORY\", \"TEST FACILITIES\", \"TEST FACILITY\"),\n List.of(\"LABORATORY PROJECT IDENTIFICATION\", \"TEST FACILITY PROJECT IDENTIFICATION\", \"Sponsor\", \"PROJECT IDENTIFICATION\"),\n \"author\",\n EntityType.ENTITY,\n document)\n .forEach(entity -> entity.apply(\"DOC.8.1\", \"Performing Laboratory found\"));\n end\n\n\n\n\nrule \"DOC.8.2: Summary Methods\"\n when\n $headline: Headline(containsString(\"1.1. METHODS\"))\n then\n entityCreationService.bySemanticNodeParagraphsOnly($headline.getParent(), \"summary_methods\", EntityType.ENTITY)\n .filter(e -> !e.getValue().contains(\"Report; Project No\")) \n .filter(e -> !e.getValue().startsWith(\"This document\")) \n .filter(e -> !e.getValue().startsWith(\"Page\"))\n .filter(e -> !e.getValue().startsWith(\"2. INTRODUCTION\"))\n .filter(e -> !e.getValue().startsWith(\"BASF\"))\n .filter(e -> !e.getValue().startsWith(\"The Chemical Company\"))\n .filter(e -> !e.getValue().startsWith(\"We create chemistry\")) \n .forEach(entity -> entity.apply(\"DOC.8.2\", \"Summary Methods found\"));\n end\n\nrule \"DOC.8.3: Summary Observations Laboratory\"\n when\n $headline: Headline(containsString(\"1.2. OBSERVATIONS\"))\n then\n entityCreationService.bySemanticNodeParagraphsOnly($headline.getParent(), \"summary_observations\", EntityType.ENTITY)\n .filter(e -> !e.getValue().contains(\"Report; Project No\"))\n .filter(e -> !e.getValue().startsWith(\"This document\")) \n .filter(e -> !e.getValue().startsWith(\"Page\"))\n .filter(e -> !e.getValue().startsWith(\"2. INTRODUCTION\"))\n .filter(e -> !e.getValue().startsWith(\"BASF\"))\n .filter(e -> !e.getValue().startsWith(\"The Chemical Company\"))\n .filter(e -> !e.getValue().startsWith(\"We create chemistry\")) \n .forEach(entity -> entity.apply(\"DOC.8.3\", \"Summary Observations found\"));\n end\n\n\nrule \"DOC.8.5: Summary Results\"\n when\n Headline((containsStringIgnoreCase(\"1.3. RESULTS\") || containsStringIgnoreCase(\"1.2. RESULTS\")), $sectionIdentifier: getSectionIdentifier())\n $headline: Headline(getSectionIdentifier().isChildOf($sectionIdentifier))\n then\n entityCreationService.bySemanticNodeParagraphsOnly($headline.getParent(), \"summary_results\", EntityType.ENTITY)\n .filter(e -> !e.getValue().contains(\"Report; Project No\"))\n .filter(e -> !e.getValue().startsWith(\"This document\")) \n .filter(e -> !e.getValue().startsWith(\"Page\"))\n .filter(e -> !e.getValue().startsWith(\"2. INTRODUCTION\"))\n .filter(e -> !e.getValue().startsWith(\"BASF\"))\n .filter(e -> !e.getValue().startsWith(\"The Chemical Company\"))\n .filter(e -> !e.getValue().startsWith(\"We create chemistry\")) \n .forEach(entity -> {\n entity.apply(\"DOC.8.5\", \"Summary Results\");\n });\n end\n\nrule \"DOC.8.6: Summary Results 2\"\n when\n $headline: Headline(containsString(\"1.2. RESULTS\"))\n then\n entityCreationService.bySemanticNodeParagraphsOnly($headline.getParent(), \"summary_results\", EntityType.ENTITY)\n .filter(e -> !e.getValue().contains(\"Report; Project No\"))\n .filter(e -> !e.getValue().startsWith(\"This document\")) \n .filter(e -> !e.getValue().startsWith(\"Page\"))\n .filter(e -> !e.getValue().startsWith(\"2. INTRODUCTION\"))\n .filter(e -> !e.getValue().startsWith(\"BASF\"))\n .filter(e -> !e.getValue().startsWith(\"The Chemical Company\")) \n .filter(e -> !e.getValue().startsWith(\"We create chemistry\")) \n .forEach(entity -> entity.apply(\"DOC.8.6\", \"Summary Results\"));\n end\n\n\n\nrule \"DOC.8.4: Summary Conclusion\"\n when\n $headline: Headline(containsString(\"1.4. CONCLUSION\") || containsString(\"1.3. CONCLUSION\"))\n then\n entityCreationService.bySemanticNodeParagraphsOnly($headline.getParent(), \"study_conclusion\", EntityType.ENTITY)\n .filter(e -> !e.getValue().contains(\"Report; Project No\")) \n .filter(e -> !e.getValue().startsWith(\"This document\")) \n .filter(e -> !e.getValue().startsWith(\"Page\"))\n .filter(e -> !e.getValue().startsWith(\"2. INTRODUCTION\"))\n .filter(e -> !e.getValue().startsWith(\"BASF\"))\n .filter(e -> !e.getValue().startsWith(\"The Chemical Company\"))\n .filter(e -> !e.getValue().startsWith(\"We create chemistry\")) \n .forEach(entity -> entity.apply(\"DOC.8.4\", \"Summary Conculsion found\"));\n end\n\n\n\nrule \"DOC.9.0: GLP Study\"\n when\n $headline: Headline(containsString(\"GOOD LABORATORY PRACTICE COMPLIANCE\")\n || containsString(\"GOOD LABORATORY PRACTICE COMPLIANCE STATEMENT\")\n || (containsString(\"DECLARACAO DE CONFORMIDADE\") && containsString(\"PRATICAS DE LABORATORIO\"))\n || containsString(\"GLP Certificate\")\n || containsString(\"GLP Certificates\")\n || containsString(\"GOOD LABORATORY PRACTICE (GLP) CERTIFICATE\")\n || containsString(\"Good Laboratory Practice Certificate\")\n || containsString(\"STATEMENT OF GLP COMPLIANCE AND AUTHENTICATION\")\n || containsString(\"GLP CERTIFICATE (FROM THE COMPETENT AUTHORITY)\")\n || containsString(\"GLP COMPLIANCE STATEMENT\")\n || containsString(\"GLP STATEMENT\") \n )\n then\n entityCreationService.bySemanticNode($headline, \"glp_study\", EntityType.ENTITY).ifPresent(entity -> {\n entity.apply(\"DOC.9.0\", \"GLP Study found\");\n });\n end\n\n\n\nrule \"DOC.9.1: GLP Study\"\n when\n $paragraph: Paragraph(containsString(\"GLP COMPLIANCE STATEMENT\"))\n then\n entityCreationService.byRegex(\"GLP COMPLIANCE STATEMENT\", \"glp_study\", EntityType.ENTITY, $paragraph).forEach(entity -> {\n entity.apply(\"DOC.9.1\", \"GLP Study found\");\n });\n end\n\n\n\n//------------------------------------ Manual redaction rules ------------------------------------\n\n// Rule unit: MAN.0\nrule \"MAN.0.0: Apply manual resize redaction\"\n salience 128\n when\n $resizeRedaction: ManualResizeRedaction($id: annotationId, status == AnnotationStatus.APPROVED, $requestDate: requestDate)\n not ManualResizeRedaction(annotationId == $id, requestDate.isBefore($requestDate))\n $entityToBeResized: TextEntity(matchesAnnotationId($id))\n then\n manualChangesApplicationService.resizeEntityAndReinsert($entityToBeResized, $resizeRedaction);\n retract($resizeRedaction);\n update($entityToBeResized);\n $entityToBeResized.getIntersectingNodes().forEach(node -> update(node));\n end\n\nrule \"MAN.0.1: Apply manual resize redaction\"\n salience 128\n when\n $resizeRedaction: ManualResizeRedaction($id: annotationId, status == AnnotationStatus.APPROVED, $requestDate: requestDate)\n not ManualResizeRedaction(annotationId == $id, requestDate.isBefore($requestDate))\n $imageToBeResized: Image(id == $id)\n then\n manualChangesApplicationService.resizeImage($imageToBeResized, $resizeRedaction);\n retract($resizeRedaction);\n update($imageToBeResized);\n update($imageToBeResized.getParent());\n end\n\n\n// Rule unit: MAN.1\nrule \"MAN.1.0: Apply id removals that are valid and not in forced redactions to Entity\"\n salience 128\n when\n $idRemoval: IdRemoval($id: annotationId, status == AnnotationStatus.APPROVED)\n $entityToBeRemoved: TextEntity(matchesAnnotationId($id))\n then\n $entityToBeRemoved.getManualOverwrite().addChange($idRemoval);\n update($entityToBeRemoved);\n retract($idRemoval);\n $entityToBeRemoved.getIntersectingNodes().forEach(node -> update(node));\n end\n\nrule \"MAN.1.1: Apply id removals that are valid and not in forced redactions to Image\"\n salience 128\n when\n $idRemoval: IdRemoval($id: annotationId, status == AnnotationStatus.APPROVED)\n $imageEntityToBeRemoved: Image($id == id)\n then\n $imageEntityToBeRemoved.getManualOverwrite().addChange($idRemoval);\n update($imageEntityToBeRemoved);\n retract($idRemoval);\n update($imageEntityToBeRemoved.getParent());\n end\n\n\n// Rule unit: MAN.2\nrule \"MAN.2.0: Apply force redaction\"\n salience 128\n when\n $force: ManualForceRedaction($id: annotationId, status == AnnotationStatus.APPROVED)\n $entityToForce: TextEntity(matchesAnnotationId($id))\n then\n $entityToForce.getManualOverwrite().addChange($force);\n update($entityToForce);\n $entityToForce.getIntersectingNodes().forEach(node -> update(node));\n retract($force);\n end\n\nrule \"MAN.2.1: Apply force redaction to images\"\n salience 128\n when\n $force: ManualForceRedaction($id: annotationId, status == AnnotationStatus.APPROVED)\n $imageToForce: Image(id == $id)\n then\n $imageToForce.getManualOverwrite().addChange($force);\n update($imageToForce);\n update($imageToForce.getParent());\n retract($force);\n end\n\n\n// Rule unit: MAN.3\nrule \"MAN.3.0: Apply entity recategorization\"\n salience 128\n when\n $recategorization: ManualRecategorization($id: annotationId, $type: type, status == AnnotationStatus.APPROVED, $requestDate: requestDate)\n not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate))\n $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type != $type)\n then\n $entityToBeRecategorized.getIntersectingNodes().forEach(node -> update(node));\n manualChangesApplicationService.recategorize($entityToBeRecategorized, $recategorization);\n retract($recategorization);\n // Entity is copied and inserted, so the old entity needs to be retracted to avoid duplication.\n retract($entityToBeRecategorized);\n end\n\nrule \"MAN.3.1: Apply entity recategorization of same type\"\n salience 128\n when\n $recategorization: ManualRecategorization($id: annotationId, $type: type, status == AnnotationStatus.APPROVED, $requestDate: requestDate)\n not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate))\n $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type == $type)\n then\n $entityToBeRecategorized.getManualOverwrite().addChange($recategorization);\n retract($recategorization);\n end\n\nrule \"MAN.3.2: Apply image recategorization\"\n salience 128\n when\n $recategorization: ManualRecategorization($id: annotationId, status == AnnotationStatus.APPROVED, $requestDate: requestDate)\n not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate))\n $imageToBeRecategorized: Image($id == id)\n then\n manualChangesApplicationService.recategorize($imageToBeRecategorized, $recategorization);\n update($imageToBeRecategorized);\n update($imageToBeRecategorized.getParent());\n retract($recategorization);\n end\n\n\n// Rule unit: MAN.4\nrule \"MAN.4.0: Apply legal basis change\"\n salience 128\n when\n $legalbasisChange: ManualLegalBasisChange($id: annotationId, status == AnnotationStatus.APPROVED)\n $imageToBeRecategorized: Image($id == id)\n then\n $imageToBeRecategorized.getManualOverwrite().addChange($legalbasisChange);\n end\n\nrule \"MAN.4.1: Apply legal basis change\"\n salience 128\n when\n $legalBasisChange: ManualLegalBasisChange($id: annotationId, status == AnnotationStatus.APPROVED)\n $entityToBeChanged: TextEntity(matchesAnnotationId($id))\n then\n $entityToBeChanged.getManualOverwrite().addChange($legalBasisChange);\n end\n\n\n//------------------------------------ Entity merging rules ------------------------------------\n\n// Rule unit: X.0\nrule \"X.0.0: remove Entity contained by Entity of same type\"\n salience 65\n when\n $larger: TextEntity($type: type, $entityType: entityType, active())\n $contained: TextEntity(containedBy($larger), type == $type, entityType == $entityType, this != $larger, !hasManualChanges(), active())\n then\n $contained.remove(\"X.0.0\", \"remove Entity contained by Entity of same type\");\n retract($contained);\n end\n\n\n// Rule unit: X.1\nrule \"X.1.0: merge intersecting Entities of same type\"\n salience 64\n when\n $first: TextEntity($type: type, $entityType: entityType, !resized(), active())\n $second: TextEntity(intersects($first), type == $type, entityType == $entityType, this != $first, !hasManualChanges(), active())\n then\n TextEntity mergedEntity = entityCreationService.mergeEntitiesOfSameType(List.of($first, $second), $type, $entityType, document);\n $first.remove(\"X.1.0\", \"merge intersecting Entities of same type\");\n $second.remove(\"X.1.0\", \"merge intersecting Entities of same type\");\n retract($first);\n retract($second);\n mergedEntity.getIntersectingNodes().forEach(node -> update(node));\n end\n\n\n// Rule unit: X.2\nrule \"X.2.0: remove Entity of type ENTITY when contained by FALSE_POSITIVE\"\n salience 64\n when\n $falsePositive: TextEntity($type: type, entityType == EntityType.FALSE_POSITIVE, active())\n $entity: TextEntity(containedBy($falsePositive), type == $type, entityType == EntityType.ENTITY, !hasManualChanges(), active())\n then\n $entity.getIntersectingNodes().forEach(node -> update(node));\n $entity.remove(\"X.2.0\", \"remove Entity of type ENTITY when contained by FALSE_POSITIVE\");\n retract($entity)\n end\n\n\n// Rule unit: X.3\nrule \"X.3.0: remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION\"\n salience 64\n when\n $falseRecommendation: TextEntity($type: type, entityType == EntityType.FALSE_RECOMMENDATION, active())\n $recommendation: TextEntity(containedBy($falseRecommendation), type == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active())\n then\n $recommendation.remove(\"X.3.0\", \"remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION\");\n retract($recommendation);\n end\n\n\n// Rule unit: X.4\nrule \"X.4.0: remove Entity of type RECOMMENDATION when intersected by ENTITY with same type\"\n salience 256\n when\n $entity: TextEntity($type: type, entityType == EntityType.ENTITY, active())\n $recommendation: TextEntity(intersects($entity), type == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active())\n then\n $entity.addEngines($recommendation.getEngines());\n $recommendation.remove(\"X.4.0\", \"remove Entity of type RECOMMENDATION when intersected by ENTITY with same type\");\n retract($recommendation);\n end\n\n\n// Rule unit: X.5\nrule \"X.5.0: remove Entity of type RECOMMENDATION when contained by ENTITY\"\n salience 256\n when\n $entity: TextEntity(entityType == EntityType.ENTITY, active())\n $recommendation: TextEntity(containedBy($entity), entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active())\n then\n $recommendation.remove(\"X.5.0\", \"remove Entity of type RECOMMENDATION when contained by ENTITY\");\n retract($recommendation);\n end\n\n\n// Rule unit: X.6\nrule \"X.6.0: remove Entity of lower rank, when intersected by entity of type ENTITY\"\n salience 32\n when\n $higherRank: TextEntity($type: type, entityType == EntityType.ENTITY, active())\n $lowerRank: TextEntity(intersects($higherRank), type != $type, dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), !hasManualChanges(), active())\n then\n $lowerRank.getIntersectingNodes().forEach(node -> update(node));\n $lowerRank.remove(\"X.6.0\", \"remove Entity of lower rank, when intersected by entity of type ENTITY\");\n retract($lowerRank);\n end\n\n\n//------------------------------------ File attributes rules ------------------------------------\n\n// Rule unit: FA.1\nrule \"FA.1.0: remove duplicate FileAttributes\"\n salience 64\n when\n $fileAttribute: FileAttribute($label: label, $value: value)\n $duplicate: FileAttribute(this != $fileAttribute, label == $label, value == $value)\n then\n retract($duplicate);\n end\n\n\n//------------------------------------ Local dictionary search rules ------------------------------------\n\n// Rule unit: LDS.0\nrule \"LDS.0.0: run local dictionary search\"\n agenda-group \"LOCAL_DICTIONARY_ADDS\"\n salience -999\n when\n $dictionaryModel: DictionaryModel(!localEntriesWithMatchedRules.isEmpty()) from dictionary.getDictionaryModels()\n then\n entityCreationService.bySearchImplementation($dictionaryModel.getLocalSearch(), $dictionaryModel.getType(), EntityType.RECOMMENDATION, document)\n .forEach(entity -> {\n Collection matchedRules = $dictionaryModel.getLocalEntriesWithMatchedRules().get(entity.getValue());\n entity.addMatchedRules(matchedRules);\n });\n end\n" \ No newline at end of file +"package drools\n\nimport static java.lang.String.format;\nimport static com.iqser.red.service.redaction.v1.server.utils.RedactionSearchUtility.anyMatch;\nimport static com.iqser.red.service.redaction.v1.server.utils.RedactionSearchUtility.exactMatch;\n\nimport java.util.List;\nimport java.util.LinkedList;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport java.util.Collection;\nimport java.util.stream.Stream;\nimport java.util.Optional;\n\nimport com.iqser.red.service.redaction.v1.server.model.document.*;\nimport com.iqser.red.service.redaction.v1.server.model.document.TextRange;\nimport com.iqser.red.service.redaction.v1.server.model.document.entity.*;\nimport com.iqser.red.service.redaction.v1.server.model.document.entity.EntityType;\nimport com.iqser.red.service.redaction.v1.server.model.document.entity.MatchedRule;\nimport com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity\nimport com.iqser.red.service.redaction.v1.server.model.document.entity.MatchedRule\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.*;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Section;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Table;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.SemanticNode;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Document;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Paragraph;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Image;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.ImageType;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Page;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Headline;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.SectionIdentifier;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Footer;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Header;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.NodeType;\nimport com.iqser.red.service.redaction.v1.server.model.document.textblock.*;\nimport com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlock;\nimport com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlockCollector;\nimport com.iqser.red.service.redaction.v1.server.model.document.textblock.AtomicTextBlock;\nimport com.iqser.red.service.redaction.v1.server.model.document.textblock.ConcatenatedTextBlock;\nimport com.iqser.red.service.redaction.v1.server.model.NerEntities;\nimport com.iqser.red.service.redaction.v1.server.model.dictionary.Dictionary;\nimport com.iqser.red.service.redaction.v1.server.model.dictionary.DictionaryModel;\nimport com.iqser.red.service.redaction.v1.server.service.document.EntityCreationService;\nimport com.iqser.red.service.redaction.v1.server.service.ManualChangesApplicationService;\nimport com.iqser.red.service.redaction.v1.server.utils.RedactionSearchUtility;\n\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.FileAttribute;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Engine;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualResizeRedaction;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.IdRemoval;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualForceRedaction;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualRecategorization;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualLegalBasisChange;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.AnnotationStatus;\n\nglobal Document document\nglobal EntityCreationService entityCreationService\nglobal ManualChangesApplicationService manualChangesApplicationService\nglobal Dictionary dictionary\n\n//------------------------------------ queries ------------------------------------\n\nquery \"getFileAttributes\"\n $fileAttribute: FileAttribute()\n end\n\n//---------------------------------------------------------------------------\n\n\n\nrule \"H.0.0: retract table of contents page\"\n when\n $page: Page(getMainBodyTextBlock().getSearchText().contains(\"........\") || (getMainBodyTextBlock().getSearchText().contains(\"APPENDICES\") && getMainBodyTextBlock().getSearchText().contains(\"TABLES\")))\n $node: SemanticNode(onPage($page.getNumber()), !onPage($page.getNumber() -1), getType() != NodeType.IMAGE)\n then\n retract($node);\n end\n\n\nrule \"H.0.1: Ignore Table of Contents\"\n salience 10\n when\n $tocHeadline: Headline(containsString(\"CONTENTS\"))\n\n then\n $tocHeadline.getParent().getPages()\n .forEach(page -> page.getMainBody().stream()\n .filter(node -> !node.getType().equals(NodeType.IMAGE))\n .filter(node -> node.getPages().stream().noneMatch(nodePage -> nodePage.getNumber() < page.getNumber()))\n .forEach(node -> retract(node))\n );\n end\n \n\n\n/*\nrule \"H.0.0: Show headlines\"\n when\n $headline: Headline()\n then\n entityCreationService.bySemanticNode($headline, \"headline\", EntityType.RECOMMENDATION);\n end\n*/\n\nrule \"DOC.0.0: Study Type File Attribute\"\n when\n not FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"402\",\"403\",\"404\",\"405\",\"408\",\"414\",\"425\",\"429\",\"436\",\"438\",\"439\",\"471\",\"487\"))\n $section: Section(\n onPage(1)\n ,(containsString(\"OECD\") || containsString(\"EPA\") || containsString(\"OPPTS\"))\n )\n then\n Stream.of(RedactionSearchUtility.findTextRangesByRegexIgnoreCase(\"(?<=OECD)(?:[\\\\w\\\\s,\\\\[\\\\]\\\\(\\\\)\\\\.]{1,10}|(?:.{5,40}(?:Number |Procedure |Guideline )))(4[\\\\d]{2})\", 1, $section.getTextBlock()),\n RedactionSearchUtility.findTextRangesByRegexIgnoreCase(\"(?<=OECD).{5,40}Method (4[\\\\d]{2}).{1,65}(\\\\d{4})\\\\)\", 1, $section.getTextBlock()),\n RedactionSearchUtility.findTextRangesByRegexIgnoreCase(\"(?<=OECD) Guideline (4\\\\d{2})\", 1, $section.getTextBlock()),\n RedactionSearchUtility.findTextRangesByRegexIgnoreCase(\"(?<=OECD) Guideline, Method No. (\\\\d{3})\", 1, $section.getTextBlock()) \n ).flatMap(Collection::stream).findFirst()\n .map(boundary -> $section.getTextBlock().subSequence(boundary).toString())\n .map(value -> FileAttribute.builder().label(\"OECD Number\").value(value).build())\n .ifPresent(fileAttribute -> insert(fileAttribute));\n end\n\n\nrule \"DOC.1.0: Guidelines\"\n when\n $section: Section(\n onPage(1)\n && (\n containsString(\"OECD\")\n || containsString(\"EPA\")\n || containsString(\"OPPTS\")\n )\n )\n then\n entityCreationService.byRegex(\"OECD (No\\\\.? )?\\\\d{3}( \\\\(\\\\d{4}\\\\))?\", \"oecd_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline found\")\n );\n entityCreationService.byRegex(\"OECD[\\\\s,]{1}(?:.{1,40}.(?>Procedure|Method).{1,20}\\\\d{3,4}(?>.{1,100}\\\\d{4}\\\\))?|\\\\[.{1,20}.Skin.{1,20}\\\\]|[\\\\d\\\\s,\\\\(\\\\)]{7,10}|[\\\\w\\\\.\\\\s]{1,15}[\\\\d]{3}\\\\s\\\\(\\\\d{4}\\\\)|.{0,20}[N|n]umber\\\\s\\\\d{3}.{0,1}|Test Guideline \\\\d{3})\", \"oecd_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline found\")\n );\n entityCreationService.byRegex(\"EPA (OPPTS )?\\\\d{3}[. ]\\\\d{4}( \\\\(\\\\d{4}\\\\))?\", \"epa_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"EPA Guideline found\")\n );\n entityCreationService.byRegex(\"EC (Directive )?(No\\\\.? )?\\\\d{3,4}\\\\/\\\\d{3,4}((,? B(\\\\.| )\\\\d{1,2}\\\\.?)? \\\\(\\\\d{4}\\\\))?\", \"ec_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"EC Guideline found\")\n );\n entityCreationService.byRegex(\"Commission Regulation \\\\(EC\\\\) No \\\\d{3}\\\\/\\\\d{4}\", \"ec_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"EC Guideline found\")\n );\n entityCreationService.byRegex(\"OECD Method 4\\\\d{2}.{5,40}\\\\(.{5,40}\\\\d{4}\\\\)\", \"oecd_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline year found\")\n );\n// Examples found in PoC 1\n// entityCreationService.byRegex(\"((OECD Guidelines for Testing of Chemicals, Procedure)|(OECD Guidelines for the Testing of Chemicals No\\\\.)|(OECD Test Guideline)|(OECD \\\\[Test Guideline, Number)) \\\\d{3}( \\\\(\\\\d{4}\\\\))?\", \"oecd_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n// entity.apply(\"DOC.1.0\", \"OECD Guideline year found\")\n// );\n entityCreationService.byRegex(\"OPPTS (Guideline Number )?\\\\d{3}\\\\.\\\\d{4}( \\\\(\\\\d{4}\\\\))?\", \"epa_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"EPA Guideline found\")\n );\n// new approach OECD Guideline\n entityCreationService.byRegex(\"(?<=OECD)(?:[\\\\w\\\\s,\\\\[\\\\]\\\\(\\\\)\\\\.]{1,10}|.{5,40}(?:Number |Procedure |Guideline ))(4[\\\\d]{2})\", \"oecd_guideline_number\", EntityType.ENTITY,1, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline no. found\")\n );\n entityCreationService.byRegex(\"(?<=OECD)(?:[\\\\w\\\\s,\\\\[\\\\]\\\\(\\\\)\\\\.]{1,10}|.{5,40}(?:Number |Procedure |Guideline ))(4[\\\\d]{2}),?\\\\s\\\\(?(\\\\d{4})\\\\)?\", \"oecd_guideline_year\", EntityType.ENTITY,2, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline year found\")\n );\n entityCreationService.byRegex(\"(?<=OECD)[\\\\w\\\\s,\\\\[\\\\]]{1,10}\\\\((\\\\d{4})\\\\)\\\\s(4[\\\\d]{2})\", \"oecd_guideline_year\", EntityType.ENTITY,1, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline year found\")\n );\n entityCreationService.byRegex(\"(?<=OECD).{5,40}Method (4[\\\\d]{2}).{1,65}(\\\\d{4})\\\\)\", \"oecd_guideline_number\", EntityType.ENTITY,1, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline number found\")\n );\n entityCreationService.byRegex(\"(?<=OECD).{5,40}Method (4[\\\\d]{2}).{1,65}(\\\\d{4})\\\\)\", \"oecd_guideline_year\", EntityType.ENTITY,2, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline year found\")\n );\n// missing OECD guideline rules for RFP demo file\n entityCreationService.byRegex(\"(?<=OECD) Guideline (4\\\\d{2})\", \"oecd_guideline_number\", EntityType.ENTITY,1, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline number found\")\n );\n entityCreationService.byRegex(\"OECD Guideline 4\\\\d{2}\", \"oecd_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline found\")\n );\n end\n\n\nrule \"DOC.1.1: Guidelines\"\n when\n $headline: Headline(\n onPage(1),\n containsString(\"OECD\")\n )\n then\n entityCreationService.byRegex(\"(OECD (No\\\\.? )?(\\\\d{3})( \\\\(\\\\d{4}\\\\))?)\", \"oecd_guideline\", EntityType.ENTITY,1, $headline).forEach(entity ->\n entity.apply(\"DOC.1.1\", \"OECD Guideline found\")\n );\n end\n\n\nrule \"DOC.1.2: Guidelines\"\n when\n $section: Section(\n (\n containsString(\"DATA REQUIREMENT\")\n || containsString(\"TEST GUIDELINE\")\n || containsString(\"MÉTODO(S) DE REFERÊNCIA(S):\")\n )\n && (\n containsString(\"OECD\")\n || containsString(\"EPA\")\n || containsString(\"OPPTS\")\n )\n && (\n hasEntitiesOfType(\"oecd_guideline\")\n || hasEntitiesOfType(\"epa_guideline\")\n || hasEntitiesOfType(\"ec_guideline\")\n )\n )\n then\n $section.getEntitiesOfType(List.of(\"oecd_guideline\",\"ec_guideline\", \"epa_guideline\")).forEach(entity -> {\n entity.apply(\"DOC.1.2\", \"OECD guideline found.\");\n });\n end\n\n\nrule \"DOC.1.3: Remove guidelines from irrelevant sections\"\n when\n $section: Section(\n (\n hasEntitiesOfType(\"oecd_guideline\")\n || hasEntitiesOfType(\"epa_guideline\")\n || hasEntitiesOfType(\"ec_guideline\")\n )\n && !(\n (\n containsString(\"DATA REQUIREMENT\")\n || containsString(\"TEST GUIDELINE\")\n || containsString(\"MÉTODO(S) DE REFERÊNCIA(S):\")\n )\n && (\n containsString(\"OECD\")\n || containsString(\"EPA\")\n || containsString(\"OPPTS\")\n )\n )\n )\n then\n $section.getEntitiesOfType(List.of(\"oecd_guideline\", \"ec_guideline\", \"epa_guideline\")).forEach(entity -> {\n entity.remove(\"DOC.1.3\", \"remove guidelines from irrelevant sections\");\n retract(entity);\n });\n end\n\n\nrule \"DOC.3.2: Experimental Completion Date\"\n salience 10\n when \n $section: Section(onPage(1) && (containsString(\"STUDY COMPLETED ON\") || containsString(\"STUDY COMPLETION DATE\") || containsString(\"Report completion date\") || containsString(\"Date of Report\") || containsString(\"AMENDMENT COMPLETION DATE\") || containsString(\"AMENDMENT COMPLETED ON\")))\n \n then\n entityCreationService.byRegex(\"STUDY COMPLETED ON (.{5,20}\\\\d{4})\", \"experimental_end_date\", EntityType.ENTITY, 1, $section).forEach(entity -> {\n entity.apply(\"DOC.3.2\", \"Experimental end date found\");\n });\n entityCreationService.byRegex(\"STUDY COMPLETION DATE (.{5,20}\\\\d{4})\", \"experimental_end_date\", EntityType.ENTITY, 1, $section).forEach(entity -> {\n entity.apply(\"DOC.3.2\", \"Experimental end date found\");\n });\n entityCreationService.byRegex(\"Report completion date (.{5,20}\\\\d{4})\", \"experimental_end_date\", EntityType.ENTITY, 1, $section).forEach(entity -> {\n entity.apply(\"DOC.3.2\", \"Experimental end date found\");\n });\n entityCreationService.byRegex(\"Date of Report (.{5,20}\\\\d{4})\", \"experimental_end_date\", EntityType.ENTITY, 1, $section).forEach(entity -> {\n entity.apply(\"DOC.3.2\", \"Experimental end date found\");\n });\n entityCreationService.byRegex(\"AMENDMENT COMPLETION DATE (.{5,20}\\\\d{4})\", \"experimental_end_date\", EntityType.ENTITY, 1, $section).forEach(entity -> {\n entity.apply(\"DOC.3.2\", \"Experimental end date found\");\n });\n entityCreationService.byRegex(\"AMENDMENT COMPLETED ON (.{5,20}\\\\d{4})\", \"experimental_end_date\", EntityType.ENTITY, 1, $section).forEach(entity -> {\n entity.apply(\"DOC.3.2\", \"Experimental end date found\");\n });\n end\n\n\n\n\n\n // hide all skipped species and strains except in the relevant sections\n rule \"DOC.4.2: Species\"\n salience 1\n when\n $section: Section(\n (hasEntitiesOfType(\"species\") || hasEntitiesOfType(\"strain\"))\n && !(\n anyHeadlineContainsStringIgnoreCase(\"test system\")\n || anyHeadlineContainsStringIgnoreCase(\"Species and strain\")\n || anyHeadlineContainsStringIgnoreCase(\"specification\")\n )\n )\n then\n $section.getEntitiesOfType(List.of(\"species\", \"strain\")).forEach(entity -> {\n entity.remove(\"DOC.4.2\",\"n-a\");\n retract(entity);\n });\n end\n\n\nrule \"DOC.4.3: Species\"\n when\n $section: Section(hasEntitiesOfType(\"species\"))\n then\n $section.getEntitiesOfType(\"species\").forEach(entity -> {\n entity.apply(\"DOC.4.3\", \"Species found.\");\n entity.setValue(entity.getValue().toLowerCase());\n });\n end\n\n\nrule \"DOC.5.0: Strain\"\n when\n $section: Section(\n hasEntitiesOfType(\"species\")\n && hasEntitiesOfType(\"strain\")\n && (\n anyHeadlineContainsStringIgnoreCase(\"test system\")\n || anyHeadlineContainsStringIgnoreCase(\"Species and strain\")\n || anyHeadlineContainsStringIgnoreCase(\"specification\")\n )\n )\n then\n $section.getEntitiesOfType(\"strain\").forEach(entity -> {\n entity.apply(\"DOC.5.0\", \"Strain found.\");\n });\n end\n\n\n\n\nrule \"DOC.35.0: Sex\"\n when\n \n $section: Section(\n (\n anyHeadlineContainsStringIgnoreCase(\"animal\")\n || anyHeadlineContainsStringIgnoreCase(\"Species and strain\")\n || anyHeadlineContainsStringIgnoreCase(\"test system\")\n )\n && !getHeadline().containsStringIgnoreCase(\"selection\")\n && (\n containsStringIgnoreCase(\"sex:\")\n || containsStringIgnoreCase(\"male\")\n || containsStringIgnoreCase(\"female\")\n )\n )\n then\n entityCreationService.byRegexIgnoreCase(\"([S|s]ex:)?[\\\\w\\\\s]{0,10}\\\\b(males?|females?)\\\\b\", \"sex\", EntityType.ENTITY,2, $section).forEach(entity -> {\n entity.apply(\"DOC.35.0\", \"Test animal sex found\");\n });\n end\n\n\n\nrule \"DOC.6.0: Authors\"\n when\n $headline: Headline(onPage(1), containsString(\"AUTHOR\"))\n then\n entityCreationService.bySemanticNodeParagraphsOnly($headline.getParent(), \"author\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.6.0\", \"Author found\"));\n end\n\n\n\nrule \"DOC.6.2: Authors\"\n when\n $page: Page(getNumber() == 1, getMainBodyTextBlock().getSearchText() (contains \"AUTHOR(S)\" || contains \"AUTHORS\" || contains \"Author\"), getMainBodyTextBlock().getSearchText() (contains \"STUDY COMPLETED ON\" || contains \"STUDY COMPLETION DATE\" || contains \"DATE OF INTERIM REPORT\" || contains \"Report completion date\" || contains \"Date of Report\" || contains \"AMENDMENT COMPLETION DATE\"))\n then\n entityCreationService.shortestBetweenAnyString(\n List.of(\"AUTHOR(S)\", \"AUTHORS\", \"Author\"),\n List.of(\"STUDY COMPLETED ON\", \"STUDY COMPLETION DATE\", \"DATE OF INTERIM REPORT\", \"Report completion date\", \"Date of Report\", \"AMENDMENT COMPLETION DATE\"),\n \"author\",\n EntityType.ENTITY,\n document)\n .forEach(entity -> entity.apply(\"DOC.6.2\", \"Author found\"));\n end\n\n\n\n\nrule \"DOC.6.6: laboratory_project_identification\"\n when\n $page: Page(getNumber() == 1, getMainBodyTextBlock().getSearchText() (contains \"LABORATORY PROJECT IDENTIFICATION\" || contains \"TEST FACILITY PROJECT IDENTIFICATION\" || contains \"Laboratory Project Identification\"))\n then\n entityCreationService.shortestBetweenAnyString(\n List.of(\"LABORATORY PROJECT IDENTIFICATION\", \"TEST FACILITY PROJECT IDENTIFICATION\"),\n List.of(\"SPONSOR\", \"VOLUME\", \"This\"),\n \"author\",\n EntityType.ENTITY,\n document)\n .forEach(entity -> entity.apply(\"DOC.6.6\", \"Laboratory Project Identification\"));\n end\n\n\n\nrule \"DOC.7.2: study title by document structure\"\n when\n $page: Page(getNumber() == 1, getMainBodyTextBlock().getSearchText() (contains \"STUDY TITLE\" || contains \"Study Title\" || contains \"STUDYTITLE\" || contains \"Report (Final)\"))\n then\n entityCreationService.shortestBetweenAnyString(\n List.of(\"STUDY TITLE\", \"STUDYTITLE\", \"Report (Final)\"),\n List.of(\"TEST GUIDELINES\", \"TEST GUIDELINE(S)\", \"Guidelines\", \"DATA REQUIREMENT\", \"AUTHOR(S)\", \"AUTHOR\"),\n \"author\",\n EntityType.ENTITY,\n document)\n .forEach(entity -> entity.apply(\"DOC.7.2\", \"Study title found\"));\n end\n\n\n\nrule \"DOC.8.1: Performing Laboratory\"\n when\n $page: Page(getNumber() == 1, getMainBodyTextBlock().getSearchText() (contains \"PERFORMING LABORATORY\" || contains \"TEST FACILITIES\" || contains \"TEST FACILITY\" || contains \"Test Facility\"), getMainBodyTextBlock().getSearchText() (contains \"LABORATORY PROJECT IDENTIFICATION\" || contains \"TEST FACILITY PROJECT IDENTIFICATION\" || contains \"Sponsor\"))\n then\n entityCreationService.shortestBetweenAnyString(\n List.of(\"PERFORMING LABORATORY\", \"TEST FACILITIES\", \"TEST FACILITY\"),\n List.of(\"LABORATORY PROJECT IDENTIFICATION\", \"TEST FACILITY PROJECT IDENTIFICATION\", \"Sponsor\", \"PROJECT IDENTIFICATION\"),\n \"author\",\n EntityType.ENTITY,\n document)\n .forEach(entity -> entity.apply(\"DOC.8.1\", \"Performing Laboratory found\"));\n end\n\n\n\n\nrule \"DOC.8.2: Summary Methods\"\n when\n $headline: Headline(containsString(\"1.1. METHODS\"))\n then\n entityCreationService.bySemanticNodeParagraphsOnly($headline.getParent(), \"summary_methods\", EntityType.ENTITY)\n .filter(e -> !e.getValue().contains(\"Report; Project No\")) \n .filter(e -> !e.getValue().startsWith(\"This document\")) \n .filter(e -> !e.getValue().startsWith(\"Page\"))\n .filter(e -> !e.getValue().startsWith(\"2. INTRODUCTION\"))\n .filter(e -> !e.getValue().startsWith(\"BASF\"))\n .filter(e -> !e.getValue().startsWith(\"The Chemical Company\"))\n .filter(e -> !e.getValue().startsWith(\"We create chemistry\")) \n .forEach(entity -> entity.apply(\"DOC.8.2\", \"Summary Methods found\"));\n end\n\nrule \"DOC.8.3: Summary Observations Laboratory\"\n when\n $headline: Headline(containsString(\"1.2. OBSERVATIONS\"))\n then\n entityCreationService.bySemanticNodeParagraphsOnly($headline.getParent(), \"summary_observations\", EntityType.ENTITY)\n .filter(e -> !e.getValue().contains(\"Report; Project No\"))\n .filter(e -> !e.getValue().startsWith(\"This document\")) \n .filter(e -> !e.getValue().startsWith(\"Page\"))\n .filter(e -> !e.getValue().startsWith(\"2. INTRODUCTION\"))\n .filter(e -> !e.getValue().startsWith(\"BASF\"))\n .filter(e -> !e.getValue().startsWith(\"The Chemical Company\"))\n .filter(e -> !e.getValue().startsWith(\"We create chemistry\")) \n .forEach(entity -> entity.apply(\"DOC.8.3\", \"Summary Observations found\"));\n end\n\n\nrule \"DOC.8.5: Summary Results\"\n when\n Headline((containsStringIgnoreCase(\"1.3. RESULTS\") || containsStringIgnoreCase(\"1.2. RESULTS\")), $sectionIdentifier: getSectionIdentifier())\n $headline: Headline(getSectionIdentifier().isChildOf($sectionIdentifier))\n then\n entityCreationService.bySemanticNodeParagraphsOnly($headline.getParent(), \"summary_results\", EntityType.ENTITY)\n .filter(e -> !e.getValue().contains(\"Report; Project No\"))\n .filter(e -> !e.getValue().startsWith(\"This document\")) \n .filter(e -> !e.getValue().startsWith(\"Page\"))\n .filter(e -> !e.getValue().startsWith(\"2. INTRODUCTION\"))\n .filter(e -> !e.getValue().startsWith(\"BASF\"))\n .filter(e -> !e.getValue().startsWith(\"The Chemical Company\"))\n .filter(e -> !e.getValue().startsWith(\"We create chemistry\")) \n .forEach(entity -> {\n entity.apply(\"DOC.8.5\", \"Summary Results\");\n });\n end\n\nrule \"DOC.8.6: Summary Results 2\"\n when\n $headline: Headline(containsString(\"1.2. RESULTS\"))\n then\n entityCreationService.bySemanticNodeParagraphsOnly($headline.getParent(), \"summary_results\", EntityType.ENTITY)\n .filter(e -> !e.getValue().contains(\"Report; Project No\"))\n .filter(e -> !e.getValue().startsWith(\"This document\")) \n .filter(e -> !e.getValue().startsWith(\"Page\"))\n .filter(e -> !e.getValue().startsWith(\"2. INTRODUCTION\"))\n .filter(e -> !e.getValue().startsWith(\"BASF\"))\n .filter(e -> !e.getValue().startsWith(\"The Chemical Company\")) \n .filter(e -> !e.getValue().startsWith(\"We create chemistry\")) \n .forEach(entity -> entity.apply(\"DOC.8.6\", \"Summary Results\"));\n end\n\n\n\nrule \"DOC.8.4: Summary Conclusion\"\n when\n $headline: Headline(containsString(\"1.4. CONCLUSION\") || containsString(\"1.3. CONCLUSION\"))\n then\n entityCreationService.bySemanticNodeParagraphsOnly($headline.getParent(), \"study_conclusion\", EntityType.ENTITY)\n .filter(e -> !e.getValue().contains(\"Report; Project No\")) \n .filter(e -> !e.getValue().startsWith(\"This document\")) \n .filter(e -> !e.getValue().startsWith(\"Page\"))\n .filter(e -> !e.getValue().startsWith(\"2. INTRODUCTION\"))\n .filter(e -> !e.getValue().startsWith(\"BASF\"))\n .filter(e -> !e.getValue().startsWith(\"The Chemical Company\"))\n .filter(e -> !e.getValue().startsWith(\"We create chemistry\")) \n .forEach(entity -> entity.apply(\"DOC.8.4\", \"Summary Conculsion found\"));\n end\n\n\n\nrule \"DOC.9.0: GLP Study\"\n when\n $headline: Headline(containsString(\"GOOD LABORATORY PRACTICE COMPLIANCE\")\n || containsString(\"GOOD LABORATORY PRACTICE COMPLIANCE STATEMENT\")\n || (containsString(\"DECLARACAO DE CONFORMIDADE\") && containsString(\"PRATICAS DE LABORATORIO\"))\n || containsString(\"GLP Certificate\")\n || containsString(\"GLP Certificates\")\n || containsString(\"GOOD LABORATORY PRACTICE (GLP) CERTIFICATE\")\n || containsString(\"Good Laboratory Practice Certificate\")\n || containsString(\"STATEMENT OF GLP COMPLIANCE AND AUTHENTICATION\")\n || containsString(\"GLP CERTIFICATE (FROM THE COMPETENT AUTHORITY)\")\n || containsString(\"GLP COMPLIANCE STATEMENT\")\n || containsString(\"GLP STATEMENT\") \n )\n then\n entityCreationService.bySemanticNode($headline, \"glp_study\", EntityType.ENTITY).ifPresent(entity -> {\n entity.apply(\"DOC.9.0\", \"GLP Study found\");\n });\n end\n\n\n\nrule \"DOC.9.1: GLP Study\"\n when\n $paragraph: Paragraph(containsString(\"GLP COMPLIANCE STATEMENT\"))\n then\n entityCreationService.byRegex(\"GLP COMPLIANCE STATEMENT\", \"glp_study\", EntityType.ENTITY, $paragraph).forEach(entity -> {\n entity.apply(\"DOC.9.1\", \"GLP Study found\");\n });\n end\n\n\n\n//------------------------------------ Manual redaction rules ------------------------------------\n\n// Rule unit: MAN.0\nrule \"MAN.0.0: Apply manual resize redaction\"\n salience 128\n when\n $resizeRedaction: ManualResizeRedaction($id: annotationId, status == AnnotationStatus.APPROVED, $requestDate: requestDate)\n not ManualResizeRedaction(annotationId == $id, requestDate.isBefore($requestDate))\n $entityToBeResized: TextEntity(matchesAnnotationId($id))\n then\n manualChangesApplicationService.resize($entityToBeResized, $resizeRedaction);\n retract($resizeRedaction);\n update($entityToBeResized);\n $entityToBeResized.getIntersectingNodes().forEach(node -> update(node));\n end\n\nrule \"MAN.0.1: Apply manual resize redaction\"\n salience 128\n when\n $resizeRedaction: ManualResizeRedaction($id: annotationId, status == AnnotationStatus.APPROVED, $requestDate: requestDate)\n not ManualResizeRedaction(annotationId == $id, requestDate.isBefore($requestDate))\n $imageToBeResized: Image(id == $id)\n then\n manualChangesApplicationService.resizeImage($imageToBeResized, $resizeRedaction);\n retract($resizeRedaction);\n update($imageToBeResized);\n update($imageToBeResized.getParent());\n end\n\n\n// Rule unit: MAN.1\nrule \"MAN.1.0: Apply id removals that are valid and not in forced redactions to Entity\"\n salience 128\n when\n $idRemoval: IdRemoval($id: annotationId, status == AnnotationStatus.APPROVED)\n $entityToBeRemoved: TextEntity(matchesAnnotationId($id))\n then\n $entityToBeRemoved.getManualOverwrite().addChange($idRemoval);\n update($entityToBeRemoved);\n retract($idRemoval);\n $entityToBeRemoved.getIntersectingNodes().forEach(node -> update(node));\n end\n\nrule \"MAN.1.1: Apply id removals that are valid and not in forced redactions to Image\"\n salience 128\n when\n $idRemoval: IdRemoval($id: annotationId, status == AnnotationStatus.APPROVED)\n $imageEntityToBeRemoved: Image($id == id)\n then\n $imageEntityToBeRemoved.getManualOverwrite().addChange($idRemoval);\n update($imageEntityToBeRemoved);\n retract($idRemoval);\n update($imageEntityToBeRemoved.getParent());\n end\n\n\n// Rule unit: MAN.2\nrule \"MAN.2.0: Apply force redaction\"\n salience 128\n when\n $force: ManualForceRedaction($id: annotationId, status == AnnotationStatus.APPROVED)\n $entityToForce: TextEntity(matchesAnnotationId($id))\n then\n $entityToForce.getManualOverwrite().addChange($force);\n update($entityToForce);\n $entityToForce.getIntersectingNodes().forEach(node -> update(node));\n retract($force);\n end\n\nrule \"MAN.2.1: Apply force redaction to images\"\n salience 128\n when\n $force: ManualForceRedaction($id: annotationId, status == AnnotationStatus.APPROVED)\n $imageToForce: Image(id == $id)\n then\n $imageToForce.getManualOverwrite().addChange($force);\n update($imageToForce);\n update($imageToForce.getParent());\n retract($force);\n end\n\n\n// Rule unit: MAN.3\nrule \"MAN.3.0: Apply entity recategorization\"\n salience 128\n when\n $recategorization: ManualRecategorization($id: annotationId, $type: type, status == AnnotationStatus.APPROVED, $requestDate: requestDate)\n not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate))\n $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type != $type)\n then\n $entityToBeRecategorized.getIntersectingNodes().forEach(node -> update(node));\n manualChangesApplicationService.recategorize($entityToBeRecategorized, $recategorization);\n retract($recategorization);\n // Entity is copied and inserted, so the old entity needs to be retracted to avoid duplication.\n retract($entityToBeRecategorized);\n end\n\nrule \"MAN.3.1: Apply entity recategorization of same type\"\n salience 128\n when\n $recategorization: ManualRecategorization($id: annotationId, $type: type, status == AnnotationStatus.APPROVED, $requestDate: requestDate)\n not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate))\n $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type == $type)\n then\n $entityToBeRecategorized.getManualOverwrite().addChange($recategorization);\n retract($recategorization);\n end\n\nrule \"MAN.3.2: Apply image recategorization\"\n salience 128\n when\n $recategorization: ManualRecategorization($id: annotationId, status == AnnotationStatus.APPROVED, $requestDate: requestDate)\n not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate))\n $imageToBeRecategorized: Image($id == id)\n then\n manualChangesApplicationService.recategorize($imageToBeRecategorized, $recategorization);\n update($imageToBeRecategorized);\n update($imageToBeRecategorized.getParent());\n retract($recategorization);\n end\n\n\n// Rule unit: MAN.4\nrule \"MAN.4.0: Apply legal basis change\"\n salience 128\n when\n $legalbasisChange: ManualLegalBasisChange($id: annotationId, status == AnnotationStatus.APPROVED)\n $imageToBeRecategorized: Image($id == id)\n then\n $imageToBeRecategorized.getManualOverwrite().addChange($legalbasisChange);\n end\n\nrule \"MAN.4.1: Apply legal basis change\"\n salience 128\n when\n $legalBasisChange: ManualLegalBasisChange($id: annotationId, status == AnnotationStatus.APPROVED)\n $entityToBeChanged: TextEntity(matchesAnnotationId($id))\n then\n $entityToBeChanged.getManualOverwrite().addChange($legalBasisChange);\n end\n\n\n//------------------------------------ Entity merging rules ------------------------------------\n\n// Rule unit: X.0\nrule \"X.0.0: remove Entity contained by Entity of same type\"\n salience 65\n when\n $larger: TextEntity($type: type, $entityType: entityType, active())\n $contained: TextEntity(containedBy($larger), type == $type, entityType == $entityType, this != $larger, !hasManualChanges(), active())\n then\n $contained.remove(\"X.0.0\", \"remove Entity contained by Entity of same type\");\n retract($contained);\n end\n\n\n// Rule unit: X.1\nrule \"X.1.0: merge intersecting Entities of same type\"\n salience 64\n when\n $first: TextEntity($type: type, $entityType: entityType, !resized(), active())\n $second: TextEntity(intersects($first), type == $type, entityType == $entityType, this != $first, !hasManualChanges(), active())\n then\n TextEntity mergedEntity = entityCreationService.mergeEntitiesOfSameType(List.of($first, $second), $type, $entityType, document);\n $first.remove(\"X.1.0\", \"merge intersecting Entities of same type\");\n $second.remove(\"X.1.0\", \"merge intersecting Entities of same type\");\n retract($first);\n retract($second);\n mergedEntity.getIntersectingNodes().forEach(node -> update(node));\n end\n\n\n// Rule unit: X.2\nrule \"X.2.0: remove Entity of type ENTITY when contained by FALSE_POSITIVE\"\n salience 64\n when\n $falsePositive: TextEntity($type: type, entityType == EntityType.FALSE_POSITIVE, active())\n $entity: TextEntity(containedBy($falsePositive), type == $type, entityType == EntityType.ENTITY, !hasManualChanges(), active())\n then\n $entity.getIntersectingNodes().forEach(node -> update(node));\n $entity.remove(\"X.2.0\", \"remove Entity of type ENTITY when contained by FALSE_POSITIVE\");\n retract($entity)\n end\n\n\n// Rule unit: X.3\nrule \"X.3.0: remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION\"\n salience 64\n when\n $falseRecommendation: TextEntity($type: type, entityType == EntityType.FALSE_RECOMMENDATION, active())\n $recommendation: TextEntity(containedBy($falseRecommendation), type == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active())\n then\n $recommendation.remove(\"X.3.0\", \"remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION\");\n retract($recommendation);\n end\n\n\n// Rule unit: X.4\nrule \"X.4.0: remove Entity of type RECOMMENDATION when intersected by ENTITY with same type\"\n salience 256\n when\n $entity: TextEntity($type: type, entityType == EntityType.ENTITY, active())\n $recommendation: TextEntity(intersects($entity), type == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active())\n then\n $entity.addEngines($recommendation.getEngines());\n $recommendation.remove(\"X.4.0\", \"remove Entity of type RECOMMENDATION when intersected by ENTITY with same type\");\n retract($recommendation);\n end\n\n\n// Rule unit: X.5\nrule \"X.5.0: remove Entity of type RECOMMENDATION when contained by ENTITY\"\n salience 256\n when\n $entity: TextEntity(entityType == EntityType.ENTITY, active())\n $recommendation: TextEntity(containedBy($entity), entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active())\n then\n $recommendation.remove(\"X.5.0\", \"remove Entity of type RECOMMENDATION when contained by ENTITY\");\n retract($recommendation);\n end\n\n\n// Rule unit: X.6\nrule \"X.6.0: remove Entity of lower rank, when intersected by entity of type ENTITY\"\n salience 32\n when\n $higherRank: TextEntity($type: type, entityType == EntityType.ENTITY, active())\n $lowerRank: TextEntity(intersects($higherRank), type != $type, dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type), !hasManualChanges(), active())\n then\n $lowerRank.getIntersectingNodes().forEach(node -> update(node));\n $lowerRank.remove(\"X.6.0\", \"remove Entity of lower rank, when intersected by entity of type ENTITY\");\n retract($lowerRank);\n end\n\n\n//------------------------------------ File attributes rules ------------------------------------\n\n// Rule unit: FA.1\nrule \"FA.1.0: remove duplicate FileAttributes\"\n salience 64\n when\n $fileAttribute: FileAttribute($label: label, $value: value)\n $duplicate: FileAttribute(this != $fileAttribute, label == $label, value == $value)\n then\n retract($duplicate);\n end\n\n\n//------------------------------------ Local dictionary search rules ------------------------------------\n\n// Rule unit: LDS.0\nrule \"LDS.0.0: run local dictionary search\"\n agenda-group \"LOCAL_DICTIONARY_ADDS\"\n salience -999\n when\n $dictionaryModel: DictionaryModel(!localEntriesWithMatchedRules.isEmpty()) from dictionary.getDictionaryModels()\n then\n entityCreationService.bySearchImplementation($dictionaryModel.getLocalSearch(), $dictionaryModel.getType(), EntityType.RECOMMENDATION, document)\n .forEach(entity -> {\n Collection matchedRules = $dictionaryModel.getLocalEntriesWithMatchedRules().get(entity.getValue());\n entity.addMatchedRules(matchedRules);\n });\n end\n" \ No newline at end of file diff --git a/redaction-service-v1/rules-management/src/test/resources/dev/Flora/rules.txt b/redaction-service-v1/rules-management/src/test/resources/dev/Flora/rules.txt index c1514d61..1739decc 100644 --- a/redaction-service-v1/rules-management/src/test/resources/dev/Flora/rules.txt +++ b/redaction-service-v1/rules-management/src/test/resources/dev/Flora/rules.txt @@ -1 +1 @@ -"package drools\n\nimport static java.lang.String.format;\nimport static com.iqser.red.service.redaction.v1.server.utils.RedactionSearchUtility.anyMatch;\nimport static com.iqser.red.service.redaction.v1.server.utils.RedactionSearchUtility.exactMatch;\n\nimport java.util.List;\nimport java.util.LinkedList;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport java.util.Collection;\nimport java.util.stream.Stream;\nimport java.util.Optional;\n\nimport com.iqser.red.service.redaction.v1.server.model.document.*;\nimport com.iqser.red.service.redaction.v1.server.model.document.TextRange;\nimport com.iqser.red.service.redaction.v1.server.model.document.entity.*;\nimport com.iqser.red.service.redaction.v1.server.model.document.entity.EntityType;\nimport com.iqser.red.service.redaction.v1.server.model.document.entity.MatchedRule;\nimport com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity\nimport com.iqser.red.service.redaction.v1.server.model.document.entity.MatchedRule\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.*;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Section;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Table;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.SemanticNode;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Document;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Paragraph;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Image;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.ImageType;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Page;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Headline;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.SectionIdentifier;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Footer;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Header;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.NodeType;\nimport com.iqser.red.service.redaction.v1.server.model.document.textblock.*;\nimport com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlock;\nimport com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlockCollector;\nimport com.iqser.red.service.redaction.v1.server.model.document.textblock.AtomicTextBlock;\nimport com.iqser.red.service.redaction.v1.server.model.document.textblock.ConcatenatedTextBlock;\nimport com.iqser.red.service.redaction.v1.server.model.NerEntities;\nimport com.iqser.red.service.redaction.v1.server.model.dictionary.Dictionary;\nimport com.iqser.red.service.redaction.v1.server.model.dictionary.DictionaryModel;\nimport com.iqser.red.service.redaction.v1.server.service.document.EntityCreationService;\nimport com.iqser.red.service.redaction.v1.server.service.ManualChangesApplicationService;\nimport com.iqser.red.service.redaction.v1.server.utils.RedactionSearchUtility;\n\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.FileAttribute;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Engine;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualResizeRedaction;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.IdRemoval;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualForceRedaction;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualRecategorization;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualLegalBasisChange;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.AnnotationStatus;\n\nglobal Document document\nglobal EntityCreationService entityCreationService\nglobal ManualChangesApplicationService manualChangesApplicationService\nglobal Dictionary dictionary\n\n//------------------------------------ queries ------------------------------------\n\nquery \"getFileAttributes\"\n $fileAttribute: FileAttribute()\n end\n//---------------------------------------------------------------------------\n\nrule \"H.0.0: retract table of contents page\"\n when\n $page: Page(getMainBodyTextBlock().getSearchText().contains(\"........\") || (getMainBodyTextBlock().getSearchText().contains(\"APPENDICES\") && getMainBodyTextBlock().getSearchText().contains(\"TABLES\")))\n $node: SemanticNode(onPage($page.getNumber()), !onPage($page.getNumber() -1), getType() != NodeType.IMAGE)\n then\n retract($node);\n end\n\n\nrule \"H.1.0: Ignore Table of Contents\"\n salience 10\n when\n $tocHeadline: Headline(containsString(\"CONTENTS\"))\n $page: Page() from $tocHeadline.getParent().getPages()\n $node: SemanticNode(this != $tocHeadline, getType() != NodeType.IMAGE, onPage($page.getNumber()), !onPage($page.getNumber() -1))\n then\n retract($node);\n end\n\n\n// Rule unit: MAN.0\nrule \"H.2.0: Show headlines\"\n when\n $headline: Headline()\n then\n entityCreationService.bySemanticNode($headline, \"headline\", EntityType.ENTITY);\n end\n\n\nrule \"H.3.0: Study Type File Attribute\"\n when\n not FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"402\",\"403\",\"404\",\"405\",\"425\",\"429\",\"436\",\"438\",\"439\",\"471\",\"487\"))\n $section: Section(\n (containsString(\"DATA REQUIREMENT\") || containsString(\"TEST GUIDELINE\") || containsString(\"MÉTODO(S) DE REFERÊNCIA(S):\"))\n ,(containsString(\"OECD\") || containsString(\"EPA\") || containsString(\"OPPTS\"))\n )\n then\n Stream.of(RedactionSearchUtility.findTextRangesByRegexIgnoreCase(\"(?<=OECD)(?:[\\\\w\\\\s,\\\\[\\\\]\\\\(\\\\)\\\\.]{1,10}|(?:.{5,40}(?:Number |Procedure |Guideline )))(4[\\\\d]{2})\", 1, $section.getTextBlock()),\n RedactionSearchUtility.findTextRangesByRegexIgnoreCase(\"(?<=OECD).{5,40}Method (4[\\\\d]{2}).{1,65}(\\\\d{4})\\\\)\", 1, $section.getTextBlock()),\n RedactionSearchUtility.findTextRangesByRegexIgnoreCase(\"(?<=OECD) Guideline (4\\\\d{2})\", 1, $section.getTextBlock())).flatMap(Collection::stream).findFirst()\n .map(textRange -> $section.getTextBlock().subSequence(textRange).toString())\n .map(value -> FileAttribute.builder().label(\"OECD Number\").value(value).build())\n .ifPresent(fileAttribute -> insert(fileAttribute));\n end\n\n\nrule \"DOC.1.0: Guidelines\"\n when\n $section: Section(\n (\n containsString(\"DATA REQUIREMENT\")\n || containsString(\"TEST GUIDELINE\")\n || containsString(\"MÉTODO(S) DE REFERÊNCIA(S):\")\n )\n && (\n containsString(\"OECD\")\n || containsString(\"EPA\")\n || containsString(\"OPPTS\")\n )\n )\n then\n entityCreationService.byRegex(\"OECD (No\\\\.? )?\\\\d{3}( \\\\(\\\\d{4}\\\\))?\", \"oecd_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline found\", \"n-a\")\n );\n entityCreationService.byRegex(\"OECD[\\\\s,]{1}(?:.{1,40}.(?>Procedure|Method).{1,20}\\\\d{3,4}(?>.{1,100}\\\\d{4}\\\\))?|\\\\[.{1,20}.Skin.{1,20}\\\\]|[\\\\d\\\\s,\\\\(\\\\)]{7,10}|[\\\\w\\\\.\\\\s]{1,15}[\\\\d]{3}\\\\s\\\\(\\\\d{4}\\\\)|.{0,20}[N|n]umber\\\\s\\\\d{3}.{0,1}|Test Guideline \\\\d{3})\", \"oecd_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline found\", \"n-a\")\n );\n entityCreationService.byRegex(\"EPA (OPPTS )?\\\\d{3}[. ]\\\\d{4}( \\\\(\\\\d{4}\\\\))?\", \"epa_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"EPA Guideline found\", \"n-a\")\n );\n entityCreationService.byRegex(\"EC (Directive )?(No\\\\.? )?\\\\d{3,4}\\\\/\\\\d{3,4}((,? B(\\\\.| )\\\\d{1,2}\\\\.?)? \\\\(\\\\d{4}\\\\))?\", \"ec_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"EC Guideline found\", \"n-a\")\n );\n entityCreationService.byRegex(\"Commission Regulation \\\\(EC\\\\) No \\\\d{3}\\\\/\\\\d{4}\", \"ec_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"EC Guideline found\", \"n-a\")\n );\n entityCreationService.byRegex(\"OECD Method 4\\\\d{2}.{5,40}\\\\(.{5,40}\\\\d{4}\\\\)\", \"oecd_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline year found\", \"n-a\")\n );\n entityCreationService.byRegex(\"OPPTS (Guideline Number )?\\\\d{3}\\\\.\\\\d{4}( \\\\(\\\\d{4}\\\\))?\", \"epa_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"EPA Guideline found\", \"n-a\")\n );\n entityCreationService.byRegex(\"(?<=OECD)(?:[\\\\w\\\\s,\\\\[\\\\]\\\\(\\\\)\\\\.]{1,10}|.{5,40}(?:Number |Procedure |Guideline ))(4[\\\\d]{2})\", \"oecd_guideline_number\", EntityType.ENTITY,1, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline no. found\", \"n-a\")\n );\n entityCreationService.byRegex(\"(?<=OECD)(?:[\\\\w\\\\s,\\\\[\\\\]\\\\(\\\\)\\\\.]{1,10}|.{5,40}(?:Number |Procedure |Guideline ))(4[\\\\d]{2}),?\\\\s\\\\(?(\\\\d{4})\\\\)?\", \"oecd_guideline_year\", EntityType.ENTITY,2, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline year found\", \"n-a\")\n );\n entityCreationService.byRegex(\"(?<=OECD)[\\\\w\\\\s,\\\\[\\\\]]{1,10}\\\\((\\\\d{4})\\\\)\\\\s(4[\\\\d]{2})\", \"oecd_guideline_year\", EntityType.ENTITY,1, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline year found\", \"n-a\")\n );\n entityCreationService.byRegex(\"(?<=OECD).{5,40}Method (4[\\\\d]{2}).{1,65}(\\\\d{4})\\\\)\", \"oecd_guideline_number\", EntityType.ENTITY,1, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline number found\", \"n-a\")\n );\n entityCreationService.byRegex(\"(?<=OECD).{5,40}Method (4[\\\\d]{2}).{1,65}(\\\\d{4})\\\\)\", \"oecd_guideline_year\", EntityType.ENTITY,2, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline year found\", \"n-a\")\n );\n entityCreationService.byRegex(\"(?<=OECD) Guideline (4\\\\d{2})\", \"oecd_guideline_number\", EntityType.ENTITY,1, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline number found\", \"n-a\")\n );\n entityCreationService.byRegex(\"OECD Guideline 4\\\\d{2}\", \"oecd_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline found\", \"n-a\")\n );\n end\n\n\nrule \"DOC.1.2: Guidelines\"\n when\n $section: Section(\n (\n containsString(\"DATA REQUIREMENT\")\n || containsString(\"TEST GUIDELINE\")\n || containsString(\"MÉTODO(S) DE REFERÊNCIA(S):\")\n )\n && (\n containsString(\"OECD\")\n || containsString(\"EPA\")\n || containsString(\"OPPTS\")\n )\n && (\n hasEntitiesOfType(\"oecd_guideline\")\n || hasEntitiesOfType(\"epa_guideline\")\n || hasEntitiesOfType(\"ec_guideline\")\n )\n )\n then\n $section.getEntitiesOfType(List.of(\"oecd_guideline\",\"ec_guideline\", \"epa_guideline\")).forEach(entity -> {\n entity.apply(\"DOC.1.2\", \"OECD guideline found.\", \"n-a\");\n });\n end\n\n\nrule \"DOC.1.3: Guidelines\"\n when\n $section: Section(\n (\n hasEntitiesOfType(\"oecd_guideline\")\n || hasEntitiesOfType(\"epa_guideline\")\n || hasEntitiesOfType(\"ec_guideline\")\n )\n && !(\n (\n containsString(\"DATA REQUIREMENT\")\n || containsString(\"TEST GUIDELINE\")\n || containsString(\"MÉTODO(S) DE REFERÊNCIA(S):\")\n )\n && (\n containsString(\"OECD\")\n || containsString(\"EPA\")\n || containsString(\"OPPTS\")\n )\n )\n )\n then\n $section.getEntitiesOfType(List.of(\"oecd_guideline\", \"ec_guideline\", \"epa_guideline\")).forEach(entity -> {\n entity.removeFromGraph();\n retract(entity);\n });\n end\n\n\nrule \"DOC.2.0: Report number\"\n when\n $section: Section(containsString(\"LABORATORY PROJECT ID\") , containsString(\"Report Number:\"))\n then\n entityCreationService.lineAfterString(\"Report Number:\", \"report_number\", EntityType.ENTITY, $section).findFirst().ifPresent(entity -> {\n entity.apply(\"DOC.2.0\", \"Report number found\", \"n-a\");\n });\n end\n\n\nrule \"DOC.3.0: Experimental Starting Date\"\n when\n $section: Section(containsString(\"Experimental I. Starting Date:\") || containsString(\"Experimental II. Starting Date:\") || containsStringIgnoreCase(\"experimental start date\") || containsStringIgnoreCase(\"experimental starting date\"))\n then\n entityCreationService.lineAfterStrings(\n List.of(\"Experimental start date\",\n \"Experimental start date:\",\n \"Experimental Starting Date\",\n \"Experimental Starting Date:\",\n \"Experimental starting date\",\n \"Experimental starting date:\",\n \"Experimental Start Date\",\n \"Experimental Start Date:\",\n \"Experimental I. Starting Date:\",\n \"Experimental II. Starting Date:\"), \"experimental_start_date\", EntityType.ENTITY, $section).forEach(entity -> {\n entity.apply(\"DOC.3.0\", \"Experimental start date found\", \"n-a\");\n });\n end\n\n\nrule \"DOC.4.0: Experimental Completion Date\"\n when\n $section: Section(containsStringIgnoreCase(\"experimental termination date\") || containsStringIgnoreCase(\"experimental completion date\"))\n then\n entityCreationService.lineAfterStrings(\n List.of(\"Experimental termination date\",\n \"Experimental termination date:\",\n \"Experimental Completion Date\",\n \"Experimental Completion Date:\",\n \"Experimental completion date\",\n \"Experimental completion date:\",\n \"Experimental Termination Date\",\n \"Experimental Termination Date:\"), \"experimental_end_date\", EntityType.ENTITY, $section).forEach(entity -> {\n entity.apply(\"DOC.4.0\", \"Experimental end date found\", \"n-a\");\n });\n end\n\n\n rule \"DOC.5.0: Ignore species and strain in irrelevant study types\"\n salience 1\n when\n FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"406\",\"428\",\"438\",\"439\",\"471\",\"474\",\"487\"))\n $section: Section(hasEntitiesOfType(\"species\") || hasEntitiesOfType(\"strain\"))\n then\n $section.getEntitiesOfType(List.of(\"species\", \"strain\")).forEach(entity -> {\n entity.removeFromGraph();\n retract(entity);\n });\n end\n\n\n rule \"DOC.5.1: Hide all skipped species and strains except in the relevant sections\"\n salience 1\n when\n FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"402\",\"403\",\"404\",\"405\",\"425\",\"429\",\"436\"))\n $section: Section(\n (hasEntitiesOfType(\"species\") || hasEntitiesOfType(\"strain\"))\n && !(\n anyHeadlineContainsStringIgnoreCase(\"test system\")\n || anyHeadlineContainsStringIgnoreCase(\"animals\")\n || anyHeadlineContainsStringIgnoreCase(\"specification\")\n )\n )\n then\n $section.getEntitiesOfType(List.of(\"species\", \"strain\")).forEach(entity -> {\n entity.removeFromGraph();\n retract(entity);\n });\n end\n\n\nrule \"DOC.5.2: Species\"\n when\n FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"402\",\"403\",\"404\",\"405\",\"425\",\"429\",\"436\"))\n $section: Section(hasEntitiesOfType(\"species\"))\n then\n $section.getEntitiesOfType(\"species\").forEach(entity -> {\n entity.apply(\"DOC.5.2\", \"Species found.\", \"n-a\");\n entity.setValue(entity.getValue().toLowerCase());\n });\n end\n\n\nrule \"DOC.5.3: Strain\"\n when\n FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"402\",\"403\",\"404\",\"405\",\"425\",\"429\",\"436\"))\n $section: Section(\n hasEntitiesOfType(\"species\")\n && hasEntitiesOfType(\"strain\")\n && (\n anyHeadlineContainsStringIgnoreCase(\"test system\")\n || anyHeadlineContainsStringIgnoreCase(\"animals\")\n || anyHeadlineContainsStringIgnoreCase(\"specification\")\n )\n )\n then\n $section.getEntitiesOfType(\"strain\").forEach(entity -> {\n entity.apply(\"DOC.5.3\", \"Strain found.\", \"n-a\");\n });\n end\n\n\nrule \"DOC.6.0: study title by document structure\"\n when\n $table: Table(onPage(1),\n (containsString(\"Final Report\") || containsString(\"SPL\")),\n numberOfRows == 1,\n numberOfCols == 1)\n then\n\n entityCreationService.bySemanticNode($table.getCell(0, 0).streamChildren().toList().get(1), \"title\", EntityType.ENTITY).ifPresent(entity -> {\n entity.apply(\"DOC.6.0\", \"Study title found\", \"n-a\");\n });\n end\n\n\nrule \"DOC.6.1: study title\"\n when\n $table: Table(onPage(1), (containsString(\"Final Report\") || containsString(\"SPL\")))\n then\n entityCreationService.byRegexWithLineBreaksIgnoreCase(\"(?<=\\\\n)[\\\\w\\\\W]{1,300}(?=\\\\nFinal Report)\", \"title\", EntityType.ENTITY, $table).findFirst().ifPresent(entity -> {\n entity.apply(\"DOC.6.1\", \"Title found\", \"n-a\");\n });\n end\n\n\nrule \"DOC.6.2: study title\"\n when\n not Table(onPage(1), (containsString(\"Final Report\") || containsString(\"SPL\")))\n $section: Section(onPage(1), (containsString(\"Final Report\") || containsString(\"SPL\")))\n then\n entityCreationService.byRegexWithLineBreaksIgnoreCase(\"(?<=\\\\n)[\\\\w\\\\W]{1,300}(?=\\\\nFinal Report)\", \"title\", EntityType.ENTITY, $section).findFirst().ifPresent(entity -> {\n entity.apply(\"DOC.6.2\", \"Title found\", \"n-a\");\n });\n end\n\n\n\nrule \"DOC.7.0: Performing Laboratory (Name)\"\n when\n $section: Section(containsString(\"PERFORMING LABORATORY:\"))\n then\n entityCreationService.lineAfterString(\"PERFORMING LABORATORY:\", \"laboratory_name\", EntityType.ENTITY, $section).findFirst().ifPresent(entity -> {\n entity.apply(\"DOC.7.0\", \"Performing Laboratory found\", \"n-a\");\n });\n end\n\n\n rule \"DOC.7.1: Performing Laboratory (Country)\"\n when\n nerEntities: NerEntities(hasEntitiesOfType(\"COUNTRY\"))\n $section: Section(containsString(\"PERFORMING LABORATORY:\"))\n then\n nerEntities.streamEntitiesOfType(\"COUNTRY\")\n .filter(nerEntity -> $section.getTextRange().contains(nerEntity.textRange()))\n .map(nerEntity -> entityCreationService.byNerEntity(nerEntity, \"laboratory_country\", EntityType.ENTITY, $section))\n .forEach(entity -> {\n entity.apply(\"DOC.7.1\", \"Performing Laboratory found\", \"n-a\");\n insert(entity);\n });\n end\n\n\nrule \"DOC.7.2: Performing Laboratory (Country & Name) from dict\"\n when\n $section: Section(\n (hasEntitiesOfType(\"laboratory_country\") || hasEntitiesOfType(\"laboratory_name\"))\n && (containsString(\"PERFORMING LABORATORY:\") || (containsString(\"PERFORMING\") && containsString(\"LABORATORY:\")))\n )\n then\n $section.getEntitiesOfType(\"laboratory_country\").forEach(entity -> {\n entity.apply(\"DOC.7.2\", \"Performing laboratory country dictionary entry found.\", \"n-a\");\n });\n $section.getEntitiesOfType(\"laboratory_name\").forEach(entity -> {\n entity.apply(\"DOC.7.2\", \"Performing laboratory name dictionary entry found.\", \"n-a\");\n });\n end\n\n\nrule \"DOC.7.3: Performing Laboratory (Country) from dict\"\n when\n $section: Section(\n (hasEntitiesOfType(\"laboratory_country\") || hasEntitiesOfType(\"laboratory_name\"))\n && !(containsString(\"PERFORMING LABORATORY:\") || (containsString(\"PERFORMING\") && containsString(\"LABORATORY:\")))\n )\n then\n $section.getEntitiesOfType(List.of(\"laboratory_country\", \"laboratory_name\")).forEach(entity -> {\n entity.removeFromGraph();\n retract(entity);\n });\n end\n\n\nrule \"DOC.8.0: GLP Study\"\n when\n $headline: Headline(containsString(\"GOOD LABORATORY PRACTICE COMPLIANCE\")\n || containsString(\"GOOD LABORATORY PRACTICE COMPLIANCE STATEMENT\")\n || (containsString(\"DECLARACAO DE CONFORMIDADE\") && containsString(\"PRATICAS DE LABORATORIO\"))\n || containsString(\"GLP Certificate\")\n || containsString(\"GLP Certificates\")\n || containsString(\"GOOD LABORATORY PRACTICE (GLP) CERTIFICATE\")\n || containsString(\"Good Laboratory Practice Certificate\")\n || containsString(\"STATEMENT OF GLP COMPLIANCE AND AUTHENTICATION\"))\n then\n entityCreationService.bySemanticNode($headline, \"glp_study\", EntityType.ENTITY).ifPresent(entity -> {\n entity.apply(\"DOC.8.0\", \"GLP Study found\", \"n-a\");\n });\n end\n\n\nrule \"DOC.9.0: Batch number from CoA\"\n when\n $section: Section(\n (\n anyHeadlineContainsString(\"Analytical Report\")\n || anyHeadlineContainsStringIgnoreCase(\"Certificate of Analysis\")\n || containsStringIgnoreCase(\"Certificate of Analysis\")\n )\n && (\n containsStringIgnoreCase(\"batch\")\n || containsStringIgnoreCase(\"bath\")\n || containsStringIgnoreCase(\"barch\")\n || containsStringIgnoreCase(\"bateb\")\n )\n && (\n containsStringIgnoreCase(\"identification\")\n || containsStringIgnoreCase(\"ldentitfication\")\n || containsStringIgnoreCase(\"wentification\")\n || containsStringIgnoreCase(\"mentification\")\n || containsStringIgnoreCase(\"kientification\")\n || containsStringIgnoreCase(\"reference number\")\n || containsStringIgnoreCase(\"test substance\")\n )\n )\n then\n entityCreationService.lineAfterStrings(List.of(\"Batch Identification\",\n \"(Batch Identification):\",\n \"Bateb Identification\",\n \"Batch Wentification\",\n \"Batch Mentification\",\n \"Batch Kientification\",\n \"Barch Identification\",\n \"Bath ldentitfication\",\n \"Batch of test substance :\"), \"batch_number\", EntityType.ENTITY, $section).forEach(entity -> {\n entity.apply(\"DOC.9.0\", \"Batch number found in CoA\", \"n-a\");\n });\n end\n\n\nrule \"DOC.9.1: Batch number\"\n when\n $section: Section(\n (\n anyHeadlineContainsStringIgnoreCase(\"Test Substance\")\n || anyHeadlineContainsStringIgnoreCase(\"Test and Control Substances\")\n || anyHeadlineContainsStringIgnoreCase(\"Test Item\")\n )\n && !(\n anyHeadlineContainsString(\"component\")\n || anyHeadlineContainsString(\"reference\")\n || anyHeadlineContainsString(\"blank\")\n )\n && containsStringIgnoreCase(\"batch\")\n )\n then\n Stream.of(entityCreationService.byRegex(\"Batch ID ([A-Z\\\\d\\\\-]{7,14})\", \"batch_number\", EntityType.ENTITY, 1, $section),\n entityCreationService.lineAfterStrings(List.of(\"Batch Identification\",\n \"Batch number:\",\n \"Batch reference number:\",\n \"Batch:\",\n \"Batch/Lot number:\",\n \"Batch (Lot) Number:\",\n \"Batch Number:\",\n \"Batch Nº:\",\n \"Batch no:\"\n ), \"batch_number\", EntityType.ENTITY, $section)).flatMap(a -> a)\n .forEach(entity -> {\n entity.apply(\"DOC.9.1\", \"Batch number found\", \"n-a\");\n });\n end\n\n\nrule \"DOC.9.2: Batch number\"\n when\n $section: Section(\n (\n anyHeadlineContainsStringIgnoreCase(\"Test Substance\")\n || anyHeadlineContainsStringIgnoreCase(\"Test and Control Substances\")\n || anyHeadlineContainsStringIgnoreCase(\"Test Item\")\n )\n && !(\n anyHeadlineContainsString(\"component\")\n || anyHeadlineContainsString(\"reference\")\n || anyHeadlineContainsString(\"blank\")\n )\n && containsStringIgnoreCase(\"batch\")\n )\n $batchNumber: String() from List.of(\"Batch Identification\",\n \"Batch number:\",\n \"Batch reference number:\",\n \"Batch:\",\n \"Batch/Lot number:\",\n \"Batch (Lot) Number:\",\n \"Batch Number:\",\n \"Batch Nº:\",\n \"Batch no:\")\n $table: Table(containsStringIgnoreCase($batchNumber)) from $section.streamAllSubNodesOfType(NodeType.TABLE).toList()\n then\n entityCreationService.lineAfterStringAcrossColumnsIgnoreCase($batchNumber, \"batch_number\", EntityType.ENTITY, $table).forEach(entity -> {\n entity.apply(\"DOC.9.2\", \"Batch number found\", \"n-a\");\n });\n end\n\n\n\n\nrule \"DOC.10.0: Conclusions - LD50, LC50, Confidence\"\n when\n FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"402\",\"403\",\"425\",\"436\"))\n $section: Section(\n (getHeadline().containsStringIgnoreCase(\"Conclusion\") || anyHeadlineContainsStringIgnoreCase(\"Lethality\"))\n && (containsString(\"LD\") || containsString(\"LC\") || containsString(\"50\") || containsString(\"LD50\") || containsString(\"lethal concentration\") || containsString(\"lethal dose\"))\n && (\n containsString(\"greater than\")\n || containsString(\"higher than\")\n || containsString(\"above\")\n || containsString(\"in excess\")\n || containsString(\"exceeds\")\n || containsString(\"was found to be\")\n || containsString(\"was calculated to be\")\n || containsString(\"estimated to be\")\n )\n )\n then\n entityCreationService.byRegexIgnoreCase(\"(L[D|C]\\\\s?50|lethal concentration|lethal dose).{1,200}(greater than|considered to be above|in excess of|exceeds|higher than)\", \"ld50_greater\", EntityType.ENTITY,2, $section).forEach(entity -> {\n entity.apply(\"DOC.10.0\", \"LD50 greater than found\", \"n-a\");\n });\n entityCreationService.byRegexIgnoreCase(\"\\\\b(?:(?:greater|higher) than|considered to be above|(?:was|is) (?:found|estimated) to be|was calculated to be|in excess of|exceeds|equal to)\\\\s?([\\\\d\\\\.]{1,6})\\\\s?mg\\\\/(?:kg|L)\", \"ld50_value\", EntityType.ENTITY,1, $section).forEach(entity -> {\n entity.apply(\"DOC.10.0\", \"LD50 value found\", \"n-a\");\n });\n entityCreationService.byRegexIgnoreCase(\"confidence interval (?:is )?([\\\\d\\\\.]{2,6}).{0,20} to (?:greater than )?([\\\\d\\\\.]{2,6})\", \"confidence_minimal\", EntityType.ENTITY,1, $section).forEach(entity -> {\n entity.apply(\"DOC.10.0\", \"Minimal Confidence found\", \"n-a\");\n });\n entityCreationService.byRegexIgnoreCase(\"confidence interval (?:is )?([\\\\d\\\\.]{2,6}).{0,20} to (?:greater than )?([\\\\d\\\\.]{2,6})\", \"confidence_maximal\", EntityType.ENTITY,2, $section).forEach(entity -> {\n entity.apply(\"DOC.10.0\", \"Maximal Confidence found\", \"n-a\");\n });\n end\n\n\nrule \"DOC.11.0: Guideline Deviation\"\n when\n FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"402\",\"403\",\"404\",\"405\",\"425\",\"429\",\"436\",\"471\"))\n $section: Section(\n (getHeadline().containsStringIgnoreCase(\"General Information\") || containsString(\"GENERAL INFORMATION\"))\n && (containsStringIgnoreCase(\"from the\") || containsStringIgnoreCase(\"to the\"))\n )\n then\n entityCreationService.betweenRegexes(\"(?:Deviations? from the [G|g]uidelines?)(?: and| or)?( the)?(?: Study Plan)?\", \"(?:(?:Deviations? from the Study Plan)|(?:Performing laboratory test)|(?:Other)|(?:Retention of [S|s]amples)|(?:Amendments? to Final Protocol))\", \"guideline_deviation\", EntityType.ENTITY, $section).forEach(entity -> {\n entity.apply(\"DOC.11.0\", \"Deviation from Guidelines found\", \"n-a\");\n });\n entityCreationService.betweenRegexes(\"(?:Deviations? (?:from|to)(?: the)? [S|s]tudy [P|p]lan)\", \"(?:Regulatory Guidelines)|(?:Other)|(?:Distribution of the report)|(?:Performing laboratory test)|(?:Distribution of the report)|(?:Retention of [S|s]amples)\", \"guideline_deviation\", EntityType.ENTITY, $section).forEach(entity -> {\n entity.apply(\"DOC.11.0\", \"Deviation from Study Plan found\", \"n-a\");\n });\n entityCreationService.betweenStrings(\"Deviations from the study plan\", \"Regulatory Guidelines\", \"guideline_deviation\", EntityType.ENTITY, $section).forEach(entity -> {\n entity.apply(\"DOC.11.0\", \"Deviation from the study plan found\", \"n-a\");\n });\n entityCreationService.byRegexIgnoreCase(\"(?>Study plan adherence)(.{1,20}deviations.{1,20} to the study plan.{0,50}\\\\.)\\\\s\", \"guideline_deviation\", EntityType.ENTITY, 1, $section).forEach(entity -> {\n entity.apply(\"DOC.11.0\", \"Guideline deviation found in text.\", \"n-a\");\n });\n entityCreationService.betweenStringsIncludeEnd(\"Deviations from the study plan\", \"validity of the study.\", \"guideline_deviation\", EntityType.ENTITY, $section).forEach(entity -> {\n entity.apply(\"DOC.11.0\", \"Deviation from the study plan found\", \"n-a\");\n });\n end\n\n\nrule \"DOC.11.1: Guideline Deviation in text\"\n when\n FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"402\",\"403\",\"404\",\"405\",\"425\",\"429\",\"436\",\"471\"))\n $section: Section(\n getHeadline().containsStringIgnoreCase(\"Introduction\")\n && containsStringIgnoreCase(\"deviations from the protocol\")\n )\n then\n entityCreationService.byRegex(\"There were no deviations from the protocol.{1,100}\\\\.\\\\s\", \"guideline_deviation\", EntityType.ENTITY, $section).forEach(entity -> {\n entity.apply(\"DOC.11.1\", \"Guideline deviation found in text.\", \"n-a\");\n });\n end\n\n\nrule \"DOC.12.0: Clinical Signs\"\n when\n FileAttribute(label == \"OECD Number\", value == \"425\")\n $headline: Headline(containsAnyStringIgnoreCase(\"Clinical Signs\", \"Macroscopic Findings\") && !containsString(\"TABLE\") && !getHeadline().containsStringIgnoreCase(\"3 - MACROSCOPIC FINDINGS\"))\n then\n entityCreationService.bySemanticNodeParagraphsOnly($headline.getParent(), \"clinical_signs\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.12.0\", \"Clinical Signs found\", \"n-a\"));\n end\n\n\nrule \"DOC.13.0: Dosages\"\n when\n FileAttribute(label == \"OECD Number\", value == \"425\")\n $section: Section(\n (anyHeadlineContainsStringIgnoreCase(\"Dosages\") || anyHeadlineContainsStringIgnoreCase(\"Study Design\"))\n && !getHeadline().containsString(\"TABLE\")\n )\n then\n entityCreationService.betweenStringsIncludeStartAndEnd(\"The animals were treated\", \".\", \"dosages\", EntityType.ENTITY, $section).forEach(entity -> {\n entity.apply(\"DOC.13.0\", \"Dosage found\", \"n-a\");\n });\n entityCreationService.betweenStringsIncludeStartAndEnd(\"Animals were treated\", \".\", \"dosages\", EntityType.ENTITY, $section).forEach(entity -> {\n entity.apply(\"DOC.13.0\", \"Dosage found\", \"n-a\");\n });\n entityCreationService.byRegexWithLineBreaks(\"(?:\\\\.[\\\\s|\\\\n]|^.{5,20}\\\\n)([^\\\\.]{1,200}(?:animal|given|received)[^\\\\.]{1,200}dose\\\\s(?:levels?\\\\s)?(?:of|at)[^\\\\.]{1,200})(?:\\\\.[\\\\s|\\\\n|$])\", \"dosages\", EntityType.ENTITY,1, $section).forEach(entity -> {\n entity.apply(\"DOC.13.0\", \"Dosage found\", \"n-a\");\n });\n end\n\n\nrule \"DOC.14.0: Mortality\"\n when\n $headline: Headline(containsString(\"Mortality\") && !containsString(\"TABLE\"))\n FileAttribute(label == \"OECD Number\", value == \"425\")\n then\n entityCreationService.bySemanticNodeParagraphsOnly($headline.getParent(), \"mortality\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.14.0\", \"Mortality found\", \"n-a\"));\n end\n\n\nrule \"DOC.15.0: Study Conclusion\"\n when\n FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"402\",\"403\",\"404\",\"405\",\"425\",\"429\",\"436\",\"471\"))\n $section: Section(\n getHeadline().containsStringIgnoreCase(\"Conclusion\")\n )\n then\n entityCreationService.bySemanticNodeParagraphsOnly($section, \"study_conclusion\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.15.0\", \"Study Conclusion found\", \"n-a\"));\n end\n\n\nrule \"DOC.16.0: Weight Behavior Changes\"\n when\n FileAttribute(label == \"OECD Number\", value == \"402\")\n $section: Section(\n getHeadline().containsString(\"Results\")\n && (\n containsString(\"body weight\")\n || containsString(\"body weights\")\n || containsString(\"bodyweight\")\n || containsString(\"bodyweights\")\n )\n )\n then\n entityCreationService.bySemanticNodeParagraphsOnly($section, \"weight_behavior_changes\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.16.0\", \"Weight behavior changes found\", \"n-a\"));\n end\n\n\nrule \"DOC.17.0: Necropsy findings\"\n when\n FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"402\",\"403\",\"436\"))\n $section: Section(\n (\n anyHeadlineContainsStringIgnoreCase(\"Necropsy\")\n || getHeadline().containsStringIgnoreCase(\"Macroscopic Findings\")\n || getHeadline().containsStringIgnoreCase(\"Macroscopic examination\")\n )\n && !getHeadline().containsStringIgnoreCase(\"Table\")\n && !getHeadline().containsStringIgnoreCase(\"Appendix\")\n && !getHeadline().containsStringIgnoreCase(\"3 - MACROSCOPIC FINDINGS\")\n )\n then\n entityCreationService.bySemanticNodeParagraphsOnly($section, \"necropsy_findings\", EntityType.ENTITY)\n .forEach( entity -> entity.apply(\"DOC.17.0\", \"Necropsy section found\", \"n-a\"));\n end\n\n\nrule \"DOC.18.0: Clinical observations\"\n when\n FileAttribute(label == \"OECD Number\", value == \"403\")\n $section: Section(\n (\n anyHeadlineContainsStringIgnoreCase(\"Clinical Observations\")\n || anyHeadlineContainsStringIgnoreCase(\"Clinical observations\")\n || anyHeadlineContainsStringIgnoreCase(\"In-life Observations\")\n || anyHeadlineContainsStringIgnoreCase(\"Postmortem Observations\")\n )\n && !anyHeadlineContainsStringIgnoreCase(\"Appendix\")\n && !anyHeadlineContainsStringIgnoreCase(\"Table\")\n && !anyHeadlineContainsStringIgnoreCase(\"Mortality\")\n )\n then\n entityCreationService.bySemanticNodeParagraphsOnly($section, \"clinical_observations\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.18.0\", \"Clinical observations section found\", \"n-a\"));\n end\n\n\nrule \"DOC.19.0: Bodyweight changes\"\n when\n FileAttribute(label == \"OECD Number\", value == \"403\")\n $headline: Headline(containsAnyStringIgnoreCase(\"Bodyweight\", \"Bodyweights\", \"Body Weights\", \"Body Weight\"), !containsAnyStringIgnoreCase(\"Appendix\", \"TABLE\"))\n then\n entityCreationService.bySemanticNodeParagraphsOnly($headline.getParent(), \"bodyweight_changes\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.19.0\", \"Bodyweight section found\", \"n-a\"));\n end\n\n\nrule \"DOC.20.0: Study Design\"\n when\n FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"402\",\"404\",\"405\",\"406\",\"428\",\"429\",\"438\",\"439\",\"474\",\"487\"))\n $section: Section(\n anyHeadlineContainsStringIgnoreCase(\"study design\")\n && !anyHeadlineContainsString(\"Preliminary screening test\")\n )\n then\n entityCreationService.bySemanticNodeParagraphsOnly($section, \"study_design\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.20.0\", \"Study design section found\", \"n-a\"));\n end\n\n\nrule \"DOC.20.1: Study Design\"\n when\n Headline(containsStringIgnoreCase(\"Study Design\"), $sectionIdentifier: getSectionIdentifier())\n $headline: Headline(getSectionIdentifier().isChildOf($sectionIdentifier))\n then\n entityCreationService.bySemanticNodeParagraphsOnly($headline.getParent(), \"study_design\", EntityType.ENTITY)\n .forEach(entity -> {\n entity.apply(\"DOC.20.1\", \"Study design section found\", \"n-a\");\n });\n end\n\n\nrule \"DOC.21.0: Results and Conclusion (406, 428, 438, 439, 474 & 487)\"\n when\n FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"406\",\"428\",\"438\",\"439\",\"474\",\"487\"))\n $parentHeadline: Headline(\n containsAnyString(\"Results\", \"Conclusion\"),\n !containsAnyString(\"POSITIVE CONTROL\", \"Positive Control\", \"Evaluation\", \"Micronucleus\", \"TABLE\", \"DISCUSSION\", \"CONCLUSIONS\", \"Interpretation\",\"Viability\", \"analysis\"),\n $sectionIdentifier: getSectionIdentifier()\n )\n not Headline(getSectionIdentifier().isChildOf($sectionIdentifier))\n then\n entityCreationService.bySemanticNodeParagraphsOnly($parentHeadline.getParent(), \"results_and_conclusion\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.21.0\", \"Results and Conclusion found\", \"n-a\"));\n end\n\n\nrule \"DOC.21.1: Results and Conclusion (406, 428, 438, 439, 474 & 487)\"\n when\n FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"406\",\"428\",\"438\",\"439\",\"474\",\"487\"))\n Headline(\n containsAnyString(\"Results\", \"Conclusion\"),\n !containsAnyString(\"POSITIVE CONTROL\", \"Positive Control\", \"Evaluation\", \"Micronucleus\", \"TABLE\", \"DISCUSSION\", \"CONCLUSIONS\", \"Interpretation\",\"Viability\", \"analysis\"),\n $sectionIdentifier: getSectionIdentifier()\n )\n $headline: Headline(getSectionIdentifier().isChildOf($sectionIdentifier))\n then\n entityCreationService.bySemanticNodeParagraphsOnly($headline.getParent(), \"results_and_conclusion\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.21.1\", \"Results and Conclusion found\", \"n-a\"));\n end\n\n\nrule \"DOC.22.0: Detailing (404 & 405)\"\n when\n FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"404\",\"405\"))\n $section: Section(\n anyHeadlineContainsStringIgnoreCase(\"Results\")\n && !getHeadline().containsStringIgnoreCase(\"Evaluation\")\n && !getHeadline().containsStringIgnoreCase(\"study\")\n )\n then\n entityCreationService.bySemanticNodeParagraphsOnly($section, \"detailing\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.22.0\", \"Detailing found\", \"n-a\"));\n end\n\n\nrule \"DOC.23.0: Preliminary Test Results (429)\"\n when\n FileAttribute(label == \"OECD Number\", value == \"429\")\n $section: Section(\n ((anyHeadlineContainsString(\"Preliminary Screening Test\") && containsString(\"Clinical observations\"))\n || anyHeadlineContainsString(\"Pre-Experiment\"))\n )\n then\n entityCreationService.bySemanticNodeParagraphsOnly($section, \"preliminary_test_results\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.23.0\", \"Preliminary Test Results found\", \"n-a\"));\n end\n\n\nrule \"DOC.24.0: Test Results (429)\"\n when\n FileAttribute(label == \"OECD Number\", value == \"429\")\n $section: Section((getHeadline().containsString(\"RESULTS AND DISCUSSION\") || getHeadline().containsString(\"Estimation of the proliferative response of lymph node cells\") || getHeadline().containsString(\"Results in the Main Experiment\")))\n then\n entityCreationService.bySemanticNodeParagraphsOnly($section, \"test_results\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.24.0\", \"Test Results found\", \"n-a\"));\n end\n\n\nrule \"DOC.24.1: Test Results (429)\"\n when\n Headline(containsStringIgnoreCase(\"RESULTS AND DISCUSSION\"), $sectionIdentifierResultsAndDiscussion: getSectionIdentifier())\n $headline: Headline(getSectionIdentifier().isChildOf($sectionIdentifierResultsAndDiscussion))\n then\n entityCreationService.bySemanticNodeParagraphsOnly($headline.getParent(), \"test_results\", EntityType.ENTITY)\n .forEach(entity -> {\n entity.apply(\"DOC.24.1\", \"Test Results found\", \"n-a\");\n });\n end\n\n\nrule \"DOC.25.0: Approach used (429)\"\n when\n FileAttribute(label == \"OECD Number\", value == \"429\")\n $section: Section(\n hasEntitiesOfType(\"species\")\n && (containsStringIgnoreCase(\"animals per\") || containsStringIgnoreCase(\"animals /\"))\n )\n then\n entityCreationService.byRegexIgnoreCase(\"\\\\banimals (?:per|\\\\/) .{0,15}(group)\\\\b\", \"approach_used\", EntityType.ENTITY,1, $section).forEach(entity -> {\n entity.apply(\"DOC.25.0\", \"Study animal approach found.\", \"n-a\");\n });\n end\n\n\nrule \"DOC.26.0: Sex\"\n when\n FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"405\",\"429\"))\n $section: Section(\n (\n anyHeadlineContainsStringIgnoreCase(\"animal\")\n || anyHeadlineContainsStringIgnoreCase(\"test system\")\n )\n && !getHeadline().containsStringIgnoreCase(\"selection\")\n && (\n containsStringIgnoreCase(\"sex:\")\n || containsStringIgnoreCase(\"male\")\n || containsStringIgnoreCase(\"female\")\n )\n )\n then\n entityCreationService.byRegexIgnoreCase(\"([S|s]ex:)?[\\\\w\\\\s]{0,10}\\\\b(males?|females?)\\\\b\", \"sex\", EntityType.ENTITY,2, $section).forEach(entity -> {\n entity.apply(\"DOC.26.0\", \"Test animal sex found\", \"n-a\");\n });\n end\n\n\nrule \"DOC.27.0: Animal Number 405\"\n when\n FileAttribute(label == \"OECD Number\", value == \"405\")\n $section: Section(\n (\n anyHeadlineContainsStringIgnoreCase(\"animal\")\n || anyHeadlineContainsStringIgnoreCase(\"test system\")\n || anyHeadlineContainsStringIgnoreCase(\"reaction\")\n )\n && !getHeadline().containsString(\"selection\")\n && (\n containsStringIgnoreCase(\"number of animals\")\n || containsStringIgnoreCase(\"no.\")\n )\n )\n then\n entityCreationService.byRegexIgnoreCase(\"(Number of animals:)[\\\\w\\\\s]{0,10}\\\\b([\\\\d]{1,3})\\\\b\", \"number_of_animals\", EntityType.ENTITY,2, $section).forEach(entity -> {\n entity.apply(\"DOC.27.0\", \"Number of animals found\", \"n-a\");\n });\n entityCreationService.byRegexIgnoreCase(\"(?:.{1,10} No\\\\. )([\\\\d\\\\w\\\\-]{3,8})\", \"animal_numbers\", EntityType.ENTITY,1, $section).forEach(entity -> {\n entity.apply(\"DOC.27.0\", \"Number of animals found\", \"n-a\");\n });\n end\n\n\nrule \"DOC.28.0: Animal Number 429\"\n when\n FileAttribute(label == \"OECD Number\", value == \"429\")\n $section: Section(\n (\n anyHeadlineContainsStringIgnoreCase(\"animal\")\n || anyHeadlineContainsStringIgnoreCase(\"test system\")\n )\n && !getHeadline().containsString(\"selection\")\n && containsStringIgnoreCase(\"number of animals\")\n && (containsStringIgnoreCase(\"per\") || containsString(\"/\"))\n && containsStringIgnoreCase(\"group\")\n )\n then\n entityCreationService.byRegexIgnoreCase(\"(Number of animals per group:)[\\\\w\\\\s]{0,10}\\\\b([\\\\d]{1,3})\\\\b\", \"number_of_animals\", EntityType.ENTITY,2, $section).forEach(entity -> {\n entity.apply(\"DOC.28.0\", \"Number of animals in group found\", \"n-a\");\n });\n entityCreationService.byRegexIgnoreCase(\"(Number of animals per group:).{0,60}\\\\b([\\\\d]{1,3})\\\\sper group\\\\b\", \"number_of_animals\", EntityType.ENTITY,2, $section).forEach(entity -> {\n entity.apply(\"DOC.28.0\", \"Number of animals in group found\", \"n-a\");\n });\n entityCreationService.byRegexIgnoreCase(\"([\\\\d]{1,3})[\\\\w\\\\s\\\\/]{0,20}(?:treatment )?group\\\\b\", \"number_of_animals\", EntityType.ENTITY,1 , $section).forEach(entity -> {\n entity.apply(\"DOC.28.0\", \"Number of animals in group found\", \"n-a\");\n });\n end\n\n\nrule \"DOC.28.1: No. Of animals - Fallback to appendix tables listing all individual animals for 429\"\n when\n $keyword: String() from List.of(\"Animal Number\", \"Animal No.\", \"Animal number\")\n $table: Table(containsString($keyword) && getHeadline().containsString(\"TABLE\") && getHeadline().containsString(\"Individual\"))\n FileAttribute(label == \"OECD Number\", value == \"429\")\n then\n $table.streamTableCellsWithHeader($keyword)\n .map(tableCell -> entityCreationService.bySemanticNode(tableCell, \"animal_numbers\", EntityType.ENTITY))\n .filter(Optional::isPresent)\n .map(Optional::get)\n .forEach(entity -> {\n entity.apply(\"DOC.28.1\", \"Animal number found.\", \"n-a\");\n insert(entity);\n });\n end\n\n\nrule \"DOC.29.0: 4h Exposure\"\n when\n FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"403\",\"436\"))\n $section: Section(\n (containsStringIgnoreCase(\"4 hours\") || containsStringIgnoreCase(\"four hours\"))\n )\n then\n entityCreationService.byRegexIgnoreCase(\"((?<=\\\\.\\\\s\\\\b).{1,100}(4|four) hours.*?\\\\.) \", \"4h_exposure\", EntityType.ENTITY,1, $section).forEach(entity -> {\n entity.apply(\"DOC.29.0\", \"4h exposure sentence found\", \"n-a\");\n });\n end\n\n\nrule \"DOC.30.0: Dilution of the test substance\"\n when\n FileAttribute(label == \"OECD Number\", value == \"404\")\n $section: Section(\n getHeadline().containsString(\"Formulation\")\n && containsString(\"dilution\")\n )\n then\n entityCreationService.bySemanticNodeParagraphsOnly($section, \"dilution\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.30.0\", \"Dilution found.\", \"n-a\"));\n end\n\n\nrule \"DOC.31.0: Positive Control\"\n when\n FileAttribute(label == \"OECD Number\", value == \"429\")\n $section: Section(\n getHeadline().containsStringIgnoreCase(\"Positive Control\")\n && !(getHeadline().containsStringIgnoreCase(\"Appendix\") || getHeadline().containsStringIgnoreCase(\"Table\"))\n )\n then\n entityCreationService.bySemanticNodeParagraphsOnly($section, \"positive_control\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.31.0\", \"Positive control found.\", \"n-a\"));\n end\n\n\nrule \"DOC.32.0: Mortality Statement\"\n when\n FileAttribute(label == \"OECD Number\", value == \"402\")\n $headline: Headline(containsStringIgnoreCase(\"Mortality\") && !containsString(\"TABLE\"))\n then\n entityCreationService.bySemanticNodeParagraphsOnly($headline.getParent(), \"mortality_statement\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.32.0\", \"Mortality Statement found\", \"n-a\"));\n end\n\n\nrule \"DOC.33.0: Dose Mortality\"\n when\n FileAttribute(label == \"OECD Number\", value == \"425\")\n $table: Table(\n (hasHeader(\"Mortality\") || hasHeader(\"Long Term Results\") || hasHeader(\"LongTerm Outcome\") || hasHeader(\"Long Term Outcome\") || hasHeader(\"Comments\") || hasHeader(\"Viability / Mortality\") || hasHeader(\"Viability/Mortality\"))\n &&\n (hasHeader(\"Dose [mg/kg bodyweight]\") || hasHeader(\"Dose [mg/kg body weight]\") ||hasHeader(\"Dose (mg/kg)\") || hasHeader(\"Dose levei (mg/kg)\") || hasHeader(\"Dose Level (mg/kg)\") || hasHeader(\"Dose level (mg/kg)\") || hasHeader(\"Dosage [mg/kg body weight]\"))\n )\n then\n Stream.of($table.streamTableCellsWithHeader(\"Mortality\"),\n $table.streamTableCellsWithHeader(\"Comments\"),\n $table.streamTableCellsWithHeader(\"Long Term Results\"),\n $table.streamTableCellsWithHeader(\"Long Term Outcome\"),\n $table.streamTableCellsWithHeader(\"LongTerm Outcome\"),\n $table.streamTableCellsWithHeader(\"Viability / Mortality\"),\n $table.streamTableCellsWithHeader(\"Viability/Mortality\")\n ).flatMap(a -> a)\n .map(tableCell -> entityCreationService.bySemanticNode(tableCell, \"dose_mortality\", EntityType.ENTITY))\n .filter(Optional::isPresent)\n .map(Optional::get)\n .forEach(entity -> {\n entity.apply(\"DOC.33.0\", \"Dose Mortality Data found.\", \"n-a\");\n insert(entity);\n });\n\n Stream.of($table.streamTableCellsWithHeader(\"Dose [mg/kg bodyweight]\"),\n $table.streamTableCellsWithHeader(\"Dose [mg/kg body weight]\"),\n $table.streamTableCellsWithHeader(\"Dose levei (mg/kg)\"),\n $table.streamTableCellsWithHeader(\"Dose Level (mg/kg)\"),\n $table.streamTableCellsWithHeader(\"Dose level (mg/kg)\"),\n $table.streamTableCellsWithHeader(\"Dose (mg/kg)\"),\n $table.streamTableCellsWithHeader(\"Dosage [mg/kg body weight]\")\n ).flatMap(a -> a)\n .map(tableCell -> entityCreationService.bySemanticNode(tableCell, \"dose_mortality_dose\", EntityType.ENTITY))\n .filter(Optional::isPresent)\n .map(Optional::get)\n .forEach(entity -> {\n entity.apply(\"DOC.33.0\", \"Dose Mortality Data found.\", \"n-a\");\n insert(entity);\n });\n end\n\n\nrule \"DOC.34.0: Results (Main Study)\"\n when\n FileAttribute(label == \"OECD Number\", value == \"429\")\n $section: Section(\n getHeadline().containsString(\"Results\")\n && getHeadline().getTextRange().length() < 20\n && !(getHeadline().containsString(\"Appendix\") || getHeadline().containsString(\"Table\"))\n )\n then\n entityCreationService.bySemanticNodeParagraphsOnly($section, \"results_(main_study)\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.34.0\", \"Results for main study found.\", \"n-a\"));\n end\n\n\nrule \"DOC.35.0: Doses (mg/kg bodyweight)\"\n when\n FileAttribute(label == \"OECD Number\", value == \"402\")\n $section: Section(\n anyHeadlineContainsStringIgnoreCase(\"study design\")\n )\n then\n entityCreationService.bySemanticNodeParagraphsOnly($section, \"doses_(mg_kg_bw)\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.35.0\", \"Doses per bodyweight information found\", \"n-a\"));\n end\n\n//------------------------------------ Manual redaction rules ------------------------------------\n\n// Rule unit: MAN.0\nrule \"MAN.0.0: Apply manual resize redaction\"\n salience 128\n when\n $resizeRedaction: ManualResizeRedaction($id: annotationId, status == AnnotationStatus.APPROVED, $requestDate: requestDate)\n not ManualResizeRedaction(annotationId == $id, requestDate.isBefore($requestDate))\n $entityToBeResized: TextEntity(matchesAnnotationId($id))\n then\n manualChangesApplicationService.resizeEntityAndReinsert($entityToBeResized, $resizeRedaction);\n retract($resizeRedaction);\n update($entityToBeResized);\n $entityToBeResized.getIntersectingNodes().forEach(node -> update(node));\n end\n\nrule \"MAN.0.1: Apply manual resize redaction\"\n salience 128\n when\n $resizeRedaction: ManualResizeRedaction($id: annotationId, status == AnnotationStatus.APPROVED, $requestDate: requestDate)\n not ManualResizeRedaction(annotationId == $id, requestDate.isBefore($requestDate))\n $imageToBeResized: Image(id == $id)\n then\n manualChangesApplicationService.resizeImage($imageToBeResized, $resizeRedaction);\n retract($resizeRedaction);\n update($imageToBeResized);\n update($imageToBeResized.getParent());\n end\n\n\n// Rule unit: MAN.1\nrule \"MAN.1.0: Apply id removals that are valid and not in forced redactions to Entity\"\n salience 128\n when\n $idRemoval: IdRemoval($id: annotationId, status == AnnotationStatus.APPROVED)\n $entityToBeRemoved: TextEntity(matchesAnnotationId($id))\n then\n $entityToBeRemoved.getManualOverwrite().addChange($idRemoval);\n update($entityToBeRemoved);\n retract($idRemoval);\n $entityToBeRemoved.getIntersectingNodes().forEach(node -> update(node));\n end\n\nrule \"MAN.1.1: Apply id removals that are valid and not in forced redactions to Image\"\n salience 128\n when\n $idRemoval: IdRemoval($id: annotationId, status == AnnotationStatus.APPROVED)\n $imageEntityToBeRemoved: Image($id == id)\n then\n $imageEntityToBeRemoved.getManualOverwrite().addChange($idRemoval);\n update($imageEntityToBeRemoved);\n retract($idRemoval);\n update($imageEntityToBeRemoved.getParent());\n end\n\n\n// Rule unit: MAN.2\nrule \"MAN.2.0: Apply force redaction\"\n salience 128\n when\n $force: ManualForceRedaction($id: annotationId, status == AnnotationStatus.APPROVED)\n $entityToForce: TextEntity(matchesAnnotationId($id))\n then\n $entityToForce.getManualOverwrite().addChange($force);\n update($entityToForce);\n $entityToForce.getIntersectingNodes().forEach(node -> update(node));\n retract($force);\n end\n\nrule \"MAN.2.1: Apply force redaction to images\"\n salience 128\n when\n $force: ManualForceRedaction($id: annotationId, status == AnnotationStatus.APPROVED)\n $imageToForce: Image(id == $id)\n then\n $imageToForce.getManualOverwrite().addChange($force);\n update($imageToForce);\n update($imageToForce.getParent());\n retract($force);\n end\n\n\n// Rule unit: MAN.3\nrule \"MAN.3.0: Apply entity recategorization\"\n salience 128\n when\n $recategorization: ManualRecategorization($id: annotationId, $type: type, status == AnnotationStatus.APPROVED, $requestDate: requestDate)\n not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate))\n $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type != $type)\n then\n $entityToBeRecategorized.getIntersectingNodes().forEach(node -> update(node));\n manualChangesApplicationService.recategorize($entityToBeRecategorized, $recategorization);\n retract($recategorization);\n // Entity is copied and inserted, so the old entity needs to be retracted to avoid duplication.\n retract($entityToBeRecategorized);\n end\n\nrule \"MAN.3.1: Apply entity recategorization of same type\"\n salience 128\n when\n $recategorization: ManualRecategorization($id: annotationId, $type: type, status == AnnotationStatus.APPROVED, $requestDate: requestDate)\n not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate))\n $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type == $type)\n then\n $entityToBeRecategorized.getManualOverwrite().addChange($recategorization);\n retract($recategorization);\n end\n\nrule \"MAN.3.2: Apply image recategorization\"\n salience 128\n when\n $recategorization: ManualRecategorization($id: annotationId, status == AnnotationStatus.APPROVED, $requestDate: requestDate)\n not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate))\n $imageToBeRecategorized: Image($id == id)\n then\n manualChangesApplicationService.recategorize($imageToBeRecategorized, $recategorization);\n update($imageToBeRecategorized);\n update($imageToBeRecategorized.getParent());\n retract($recategorization);\n end\n\n\n// Rule unit: MAN.4\nrule \"MAN.4.0: Apply legal basis change\"\n salience 128\n when\n $legalbasisChange: ManualLegalBasisChange($id: annotationId, status == AnnotationStatus.APPROVED)\n $imageToBeRecategorized: Image($id == id)\n then\n $imageToBeRecategorized.getManualOverwrite().addChange($legalbasisChange);\n end\n\nrule \"MAN.4.1: Apply legal basis change\"\n salience 128\n when\n $legalBasisChange: ManualLegalBasisChange($id: annotationId, status == AnnotationStatus.APPROVED)\n $entityToBeChanged: TextEntity(matchesAnnotationId($id))\n then\n $entityToBeChanged.getManualOverwrite().addChange($legalBasisChange);\n end\n\n\n\n//------------------------------------ Entity merging rules ------------------------------------\n\n// Rule unit: X.0\nrule \"X.0.0: remove Entity contained by Entity of same type\"\n salience 65\n when\n $larger: TextEntity($type: type, $entityType: entityType, active())\n $contained: TextEntity(containedBy($larger), type == $type, entityType == $entityType, this != $larger, !hasManualChanges(), active())\n then\n $contained.remove(\"X.0.0\", \"remove Entity contained by Entity of same type\");\n retract($contained);\n end\n\n\n// Rule unit: X.2\nrule \"X.2.0: remove Entity of type ENTITY when contained by FALSE_POSITIVE\"\n salience 64\n when\n $falsePositive: TextEntity($type: type, entityType == EntityType.FALSE_POSITIVE, active())\n $entity: TextEntity(containedBy($falsePositive), type == $type, entityType == EntityType.ENTITY, !hasManualChanges(), active())\n then\n $entity.getIntersectingNodes().forEach(node -> update(node));\n $entity.remove(\"X.2.0\", \"remove Entity of type ENTITY when contained by FALSE_POSITIVE\");\n retract($entity)\n end\n\n\n// Rule unit: X.3\nrule \"X.3.0: remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION\"\n salience 64\n when\n $falseRecommendation: TextEntity($type: type, entityType == EntityType.FALSE_RECOMMENDATION, active())\n $recommendation: TextEntity(containedBy($falseRecommendation), type == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active())\n then\n $recommendation.remove(\"X.3.0\", \"remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION\");\n retract($recommendation);\n end\n\n\n// Rule unit: X.4\nrule \"X.4.0: remove Entity of type RECOMMENDATION when intersected by ENTITY with same type\"\n salience 256\n when\n $entity: TextEntity($type: type, entityType == EntityType.ENTITY, active())\n $recommendation: TextEntity(intersects($entity), type == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active())\n then\n $entity.addEngines($recommendation.getEngines());\n $recommendation.remove(\"X.4.0\", \"remove Entity of type RECOMMENDATION when intersected by ENTITY with same type\");\n retract($recommendation);\n end\n\n\n// Rule unit: X.5\nrule \"X.5.0: remove Entity of type RECOMMENDATION when contained by ENTITY\"\n salience 256\n when\n $entity: TextEntity(entityType == EntityType.ENTITY, active())\n $recommendation: TextEntity(containedBy($entity), entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active())\n then\n $recommendation.remove(\"X.5.0\", \"remove Entity of type RECOMMENDATION when contained by ENTITY\");\n retract($recommendation);\n end\n\n\n// Rule unit: X.7\nrule \"X.7.0: remove all images\"\n salience 512\n when\n $image: Image(imageType != ImageType.OCR, !hasManualChanges())\n then\n $image.remove(\"X.7.0\", \"remove all images\");\n retract($image);\n end\n\n\n//------------------------------------ File attributes rules ------------------------------------\n\n// Rule unit: FA.1\nrule \"FA.1.0: remove duplicate FileAttributes\"\n\n salience 64\n when\n $fileAttribute: FileAttribute($label: label, $value: value)\n $duplicate: FileAttribute(this != $fileAttribute, label == $label, value == $value)\n then\n retract($duplicate);\n end\n\n\n// Rule unit: LDS.0\nrule \"LDS.0.0: run local dictionary search\"\n agenda-group \"LOCAL_DICTIONARY_ADDS\"\n salience -999\n when\n $dictionaryModel: DictionaryModel(!localEntriesWithMatchedRules.isEmpty()) from dictionary.getDictionaryModels()\n then\n entityCreationService.bySearchImplementation($dictionaryModel.getLocalSearch(), $dictionaryModel.getType(), EntityType.RECOMMENDATION, document)\n .forEach(entity -> {\n Collection matchedRules = $dictionaryModel.getLocalEntriesWithMatchedRules().get(entity.getValue());\n entity.addMatchedRules(matchedRules);\n });\n end\n" \ No newline at end of file +"package drools\n\nimport static java.lang.String.format;\nimport static com.iqser.red.service.redaction.v1.server.utils.RedactionSearchUtility.anyMatch;\nimport static com.iqser.red.service.redaction.v1.server.utils.RedactionSearchUtility.exactMatch;\n\nimport java.util.List;\nimport java.util.LinkedList;\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport java.util.Collection;\nimport java.util.stream.Stream;\nimport java.util.Optional;\n\nimport com.iqser.red.service.redaction.v1.server.model.document.*;\nimport com.iqser.red.service.redaction.v1.server.model.document.TextRange;\nimport com.iqser.red.service.redaction.v1.server.model.document.entity.*;\nimport com.iqser.red.service.redaction.v1.server.model.document.entity.EntityType;\nimport com.iqser.red.service.redaction.v1.server.model.document.entity.MatchedRule;\nimport com.iqser.red.service.redaction.v1.server.model.document.entity.TextEntity\nimport com.iqser.red.service.redaction.v1.server.model.document.entity.MatchedRule\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.*;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Section;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Table;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.SemanticNode;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Document;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Paragraph;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Image;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.ImageType;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Page;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Headline;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.SectionIdentifier;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Footer;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.Header;\nimport com.iqser.red.service.redaction.v1.server.model.document.nodes.NodeType;\nimport com.iqser.red.service.redaction.v1.server.model.document.textblock.*;\nimport com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlock;\nimport com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlockCollector;\nimport com.iqser.red.service.redaction.v1.server.model.document.textblock.AtomicTextBlock;\nimport com.iqser.red.service.redaction.v1.server.model.document.textblock.ConcatenatedTextBlock;\nimport com.iqser.red.service.redaction.v1.server.model.NerEntities;\nimport com.iqser.red.service.redaction.v1.server.model.dictionary.Dictionary;\nimport com.iqser.red.service.redaction.v1.server.model.dictionary.DictionaryModel;\nimport com.iqser.red.service.redaction.v1.server.service.document.EntityCreationService;\nimport com.iqser.red.service.redaction.v1.server.service.ManualChangesApplicationService;\nimport com.iqser.red.service.redaction.v1.server.utils.RedactionSearchUtility;\n\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.FileAttribute;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Engine;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualResizeRedaction;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.IdRemoval;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualForceRedaction;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualRecategorization;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualLegalBasisChange;\nimport com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.AnnotationStatus;\n\nglobal Document document\nglobal EntityCreationService entityCreationService\nglobal ManualChangesApplicationService manualChangesApplicationService\nglobal Dictionary dictionary\n\n//------------------------------------ queries ------------------------------------\n\nquery \"getFileAttributes\"\n $fileAttribute: FileAttribute()\n end\n//---------------------------------------------------------------------------\n\nrule \"H.0.0: retract table of contents page\"\n when\n $page: Page(getMainBodyTextBlock().getSearchText().contains(\"........\") || (getMainBodyTextBlock().getSearchText().contains(\"APPENDICES\") && getMainBodyTextBlock().getSearchText().contains(\"TABLES\")))\n $node: SemanticNode(onPage($page.getNumber()), !onPage($page.getNumber() -1), getType() != NodeType.IMAGE)\n then\n retract($node);\n end\n\n\nrule \"H.1.0: Ignore Table of Contents\"\n salience 10\n when\n $tocHeadline: Headline(containsString(\"CONTENTS\"))\n $page: Page() from $tocHeadline.getParent().getPages()\n $node: SemanticNode(this != $tocHeadline, getType() != NodeType.IMAGE, onPage($page.getNumber()), !onPage($page.getNumber() -1))\n then\n retract($node);\n end\n\n\n// Rule unit: MAN.0\nrule \"H.2.0: Show headlines\"\n when\n $headline: Headline()\n then\n entityCreationService.bySemanticNode($headline, \"headline\", EntityType.ENTITY);\n end\n\n\nrule \"H.3.0: Study Type File Attribute\"\n when\n not FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"402\",\"403\",\"404\",\"405\",\"425\",\"429\",\"436\",\"438\",\"439\",\"471\",\"487\"))\n $section: Section(\n (containsString(\"DATA REQUIREMENT\") || containsString(\"TEST GUIDELINE\") || containsString(\"MÉTODO(S) DE REFERÊNCIA(S):\"))\n ,(containsString(\"OECD\") || containsString(\"EPA\") || containsString(\"OPPTS\"))\n )\n then\n Stream.of(RedactionSearchUtility.findTextRangesByRegexIgnoreCase(\"(?<=OECD)(?:[\\\\w\\\\s,\\\\[\\\\]\\\\(\\\\)\\\\.]{1,10}|(?:.{5,40}(?:Number |Procedure |Guideline )))(4[\\\\d]{2})\", 1, $section.getTextBlock()),\n RedactionSearchUtility.findTextRangesByRegexIgnoreCase(\"(?<=OECD).{5,40}Method (4[\\\\d]{2}).{1,65}(\\\\d{4})\\\\)\", 1, $section.getTextBlock()),\n RedactionSearchUtility.findTextRangesByRegexIgnoreCase(\"(?<=OECD) Guideline (4\\\\d{2})\", 1, $section.getTextBlock())).flatMap(Collection::stream).findFirst()\n .map(textRange -> $section.getTextBlock().subSequence(textRange).toString())\n .map(value -> FileAttribute.builder().label(\"OECD Number\").value(value).build())\n .ifPresent(fileAttribute -> insert(fileAttribute));\n end\n\n\nrule \"DOC.1.0: Guidelines\"\n when\n $section: Section(\n (\n containsString(\"DATA REQUIREMENT\")\n || containsString(\"TEST GUIDELINE\")\n || containsString(\"MÉTODO(S) DE REFERÊNCIA(S):\")\n )\n && (\n containsString(\"OECD\")\n || containsString(\"EPA\")\n || containsString(\"OPPTS\")\n )\n )\n then\n entityCreationService.byRegex(\"OECD (No\\\\.? )?\\\\d{3}( \\\\(\\\\d{4}\\\\))?\", \"oecd_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline found\", \"n-a\")\n );\n entityCreationService.byRegex(\"OECD[\\\\s,]{1}(?:.{1,40}.(?>Procedure|Method).{1,20}\\\\d{3,4}(?>.{1,100}\\\\d{4}\\\\))?|\\\\[.{1,20}.Skin.{1,20}\\\\]|[\\\\d\\\\s,\\\\(\\\\)]{7,10}|[\\\\w\\\\.\\\\s]{1,15}[\\\\d]{3}\\\\s\\\\(\\\\d{4}\\\\)|.{0,20}[N|n]umber\\\\s\\\\d{3}.{0,1}|Test Guideline \\\\d{3})\", \"oecd_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline found\", \"n-a\")\n );\n entityCreationService.byRegex(\"EPA (OPPTS )?\\\\d{3}[. ]\\\\d{4}( \\\\(\\\\d{4}\\\\))?\", \"epa_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"EPA Guideline found\", \"n-a\")\n );\n entityCreationService.byRegex(\"EC (Directive )?(No\\\\.? )?\\\\d{3,4}\\\\/\\\\d{3,4}((,? B(\\\\.| )\\\\d{1,2}\\\\.?)? \\\\(\\\\d{4}\\\\))?\", \"ec_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"EC Guideline found\", \"n-a\")\n );\n entityCreationService.byRegex(\"Commission Regulation \\\\(EC\\\\) No \\\\d{3}\\\\/\\\\d{4}\", \"ec_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"EC Guideline found\", \"n-a\")\n );\n entityCreationService.byRegex(\"OECD Method 4\\\\d{2}.{5,40}\\\\(.{5,40}\\\\d{4}\\\\)\", \"oecd_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline year found\", \"n-a\")\n );\n entityCreationService.byRegex(\"OPPTS (Guideline Number )?\\\\d{3}\\\\.\\\\d{4}( \\\\(\\\\d{4}\\\\))?\", \"epa_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"EPA Guideline found\", \"n-a\")\n );\n entityCreationService.byRegex(\"(?<=OECD)(?:[\\\\w\\\\s,\\\\[\\\\]\\\\(\\\\)\\\\.]{1,10}|.{5,40}(?:Number |Procedure |Guideline ))(4[\\\\d]{2})\", \"oecd_guideline_number\", EntityType.ENTITY,1, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline no. found\", \"n-a\")\n );\n entityCreationService.byRegex(\"(?<=OECD)(?:[\\\\w\\\\s,\\\\[\\\\]\\\\(\\\\)\\\\.]{1,10}|.{5,40}(?:Number |Procedure |Guideline ))(4[\\\\d]{2}),?\\\\s\\\\(?(\\\\d{4})\\\\)?\", \"oecd_guideline_year\", EntityType.ENTITY,2, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline year found\", \"n-a\")\n );\n entityCreationService.byRegex(\"(?<=OECD)[\\\\w\\\\s,\\\\[\\\\]]{1,10}\\\\((\\\\d{4})\\\\)\\\\s(4[\\\\d]{2})\", \"oecd_guideline_year\", EntityType.ENTITY,1, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline year found\", \"n-a\")\n );\n entityCreationService.byRegex(\"(?<=OECD).{5,40}Method (4[\\\\d]{2}).{1,65}(\\\\d{4})\\\\)\", \"oecd_guideline_number\", EntityType.ENTITY,1, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline number found\", \"n-a\")\n );\n entityCreationService.byRegex(\"(?<=OECD).{5,40}Method (4[\\\\d]{2}).{1,65}(\\\\d{4})\\\\)\", \"oecd_guideline_year\", EntityType.ENTITY,2, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline year found\", \"n-a\")\n );\n entityCreationService.byRegex(\"(?<=OECD) Guideline (4\\\\d{2})\", \"oecd_guideline_number\", EntityType.ENTITY,1, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline number found\", \"n-a\")\n );\n entityCreationService.byRegex(\"OECD Guideline 4\\\\d{2}\", \"oecd_guideline\", EntityType.ENTITY, $section).forEach(entity ->\n entity.apply(\"DOC.1.0\", \"OECD Guideline found\", \"n-a\")\n );\n end\n\n\nrule \"DOC.1.2: Guidelines\"\n when\n $section: Section(\n (\n containsString(\"DATA REQUIREMENT\")\n || containsString(\"TEST GUIDELINE\")\n || containsString(\"MÉTODO(S) DE REFERÊNCIA(S):\")\n )\n && (\n containsString(\"OECD\")\n || containsString(\"EPA\")\n || containsString(\"OPPTS\")\n )\n && (\n hasEntitiesOfType(\"oecd_guideline\")\n || hasEntitiesOfType(\"epa_guideline\")\n || hasEntitiesOfType(\"ec_guideline\")\n )\n )\n then\n $section.getEntitiesOfType(List.of(\"oecd_guideline\",\"ec_guideline\", \"epa_guideline\")).forEach(entity -> {\n entity.apply(\"DOC.1.2\", \"OECD guideline found.\", \"n-a\");\n });\n end\n\n\nrule \"DOC.1.3: Guidelines\"\n when\n $section: Section(\n (\n hasEntitiesOfType(\"oecd_guideline\")\n || hasEntitiesOfType(\"epa_guideline\")\n || hasEntitiesOfType(\"ec_guideline\")\n )\n && !(\n (\n containsString(\"DATA REQUIREMENT\")\n || containsString(\"TEST GUIDELINE\")\n || containsString(\"MÉTODO(S) DE REFERÊNCIA(S):\")\n )\n && (\n containsString(\"OECD\")\n || containsString(\"EPA\")\n || containsString(\"OPPTS\")\n )\n )\n )\n then\n $section.getEntitiesOfType(List.of(\"oecd_guideline\", \"ec_guideline\", \"epa_guideline\")).forEach(entity -> {\n entity.removeFromGraph();\n retract(entity);\n });\n end\n\n\nrule \"DOC.2.0: Report number\"\n when\n $section: Section(containsString(\"LABORATORY PROJECT ID\") , containsString(\"Report Number:\"))\n then\n entityCreationService.lineAfterString(\"Report Number:\", \"report_number\", EntityType.ENTITY, $section).findFirst().ifPresent(entity -> {\n entity.apply(\"DOC.2.0\", \"Report number found\", \"n-a\");\n });\n end\n\n\nrule \"DOC.3.0: Experimental Starting Date\"\n when\n $section: Section(containsString(\"Experimental I. Starting Date:\") || containsString(\"Experimental II. Starting Date:\") || containsStringIgnoreCase(\"experimental start date\") || containsStringIgnoreCase(\"experimental starting date\"))\n then\n entityCreationService.lineAfterStrings(\n List.of(\"Experimental start date\",\n \"Experimental start date:\",\n \"Experimental Starting Date\",\n \"Experimental Starting Date:\",\n \"Experimental starting date\",\n \"Experimental starting date:\",\n \"Experimental Start Date\",\n \"Experimental Start Date:\",\n \"Experimental I. Starting Date:\",\n \"Experimental II. Starting Date:\"), \"experimental_start_date\", EntityType.ENTITY, $section).forEach(entity -> {\n entity.apply(\"DOC.3.0\", \"Experimental start date found\", \"n-a\");\n });\n end\n\n\nrule \"DOC.4.0: Experimental Completion Date\"\n when\n $section: Section(containsStringIgnoreCase(\"experimental termination date\") || containsStringIgnoreCase(\"experimental completion date\"))\n then\n entityCreationService.lineAfterStrings(\n List.of(\"Experimental termination date\",\n \"Experimental termination date:\",\n \"Experimental Completion Date\",\n \"Experimental Completion Date:\",\n \"Experimental completion date\",\n \"Experimental completion date:\",\n \"Experimental Termination Date\",\n \"Experimental Termination Date:\"), \"experimental_end_date\", EntityType.ENTITY, $section).forEach(entity -> {\n entity.apply(\"DOC.4.0\", \"Experimental end date found\", \"n-a\");\n });\n end\n\n\n rule \"DOC.5.0: Ignore species and strain in irrelevant study types\"\n salience 1\n when\n FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"406\",\"428\",\"438\",\"439\",\"471\",\"474\",\"487\"))\n $section: Section(hasEntitiesOfType(\"species\") || hasEntitiesOfType(\"strain\"))\n then\n $section.getEntitiesOfType(List.of(\"species\", \"strain\")).forEach(entity -> {\n entity.removeFromGraph();\n retract(entity);\n });\n end\n\n\n rule \"DOC.5.1: Hide all skipped species and strains except in the relevant sections\"\n salience 1\n when\n FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"402\",\"403\",\"404\",\"405\",\"425\",\"429\",\"436\"))\n $section: Section(\n (hasEntitiesOfType(\"species\") || hasEntitiesOfType(\"strain\"))\n && !(\n anyHeadlineContainsStringIgnoreCase(\"test system\")\n || anyHeadlineContainsStringIgnoreCase(\"animals\")\n || anyHeadlineContainsStringIgnoreCase(\"specification\")\n )\n )\n then\n $section.getEntitiesOfType(List.of(\"species\", \"strain\")).forEach(entity -> {\n entity.removeFromGraph();\n retract(entity);\n });\n end\n\n\nrule \"DOC.5.2: Species\"\n when\n FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"402\",\"403\",\"404\",\"405\",\"425\",\"429\",\"436\"))\n $section: Section(hasEntitiesOfType(\"species\"))\n then\n $section.getEntitiesOfType(\"species\").forEach(entity -> {\n entity.apply(\"DOC.5.2\", \"Species found.\", \"n-a\");\n entity.setValue(entity.getValue().toLowerCase());\n });\n end\n\n\nrule \"DOC.5.3: Strain\"\n when\n FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"402\",\"403\",\"404\",\"405\",\"425\",\"429\",\"436\"))\n $section: Section(\n hasEntitiesOfType(\"species\")\n && hasEntitiesOfType(\"strain\")\n && (\n anyHeadlineContainsStringIgnoreCase(\"test system\")\n || anyHeadlineContainsStringIgnoreCase(\"animals\")\n || anyHeadlineContainsStringIgnoreCase(\"specification\")\n )\n )\n then\n $section.getEntitiesOfType(\"strain\").forEach(entity -> {\n entity.apply(\"DOC.5.3\", \"Strain found.\", \"n-a\");\n });\n end\n\n\nrule \"DOC.6.0: study title by document structure\"\n when\n $table: Table(onPage(1),\n (containsString(\"Final Report\") || containsString(\"SPL\")),\n numberOfRows == 1,\n numberOfCols == 1)\n then\n\n entityCreationService.bySemanticNode($table.getCell(0, 0).streamChildren().toList().get(1), \"title\", EntityType.ENTITY).ifPresent(entity -> {\n entity.apply(\"DOC.6.0\", \"Study title found\", \"n-a\");\n });\n end\n\n\nrule \"DOC.6.1: study title\"\n when\n $table: Table(onPage(1), (containsString(\"Final Report\") || containsString(\"SPL\")))\n then\n entityCreationService.byRegexWithLineBreaksIgnoreCase(\"(?<=\\\\n)[\\\\w\\\\W]{1,300}(?=\\\\nFinal Report)\", \"title\", EntityType.ENTITY, $table).findFirst().ifPresent(entity -> {\n entity.apply(\"DOC.6.1\", \"Title found\", \"n-a\");\n });\n end\n\n\nrule \"DOC.6.2: study title\"\n when\n not Table(onPage(1), (containsString(\"Final Report\") || containsString(\"SPL\")))\n $section: Section(onPage(1), (containsString(\"Final Report\") || containsString(\"SPL\")))\n then\n entityCreationService.byRegexWithLineBreaksIgnoreCase(\"(?<=\\\\n)[\\\\w\\\\W]{1,300}(?=\\\\nFinal Report)\", \"title\", EntityType.ENTITY, $section).findFirst().ifPresent(entity -> {\n entity.apply(\"DOC.6.2\", \"Title found\", \"n-a\");\n });\n end\n\n\n\nrule \"DOC.7.0: Performing Laboratory (Name)\"\n when\n $section: Section(containsString(\"PERFORMING LABORATORY:\"))\n then\n entityCreationService.lineAfterString(\"PERFORMING LABORATORY:\", \"laboratory_name\", EntityType.ENTITY, $section).findFirst().ifPresent(entity -> {\n entity.apply(\"DOC.7.0\", \"Performing Laboratory found\", \"n-a\");\n });\n end\n\n\n rule \"DOC.7.1: Performing Laboratory (Country)\"\n when\n nerEntities: NerEntities(hasEntitiesOfType(\"COUNTRY\"))\n $section: Section(containsString(\"PERFORMING LABORATORY:\"))\n then\n nerEntities.streamEntitiesOfType(\"COUNTRY\")\n .filter(nerEntity -> $section.getTextRange().contains(nerEntity.textRange()))\n .map(nerEntity -> entityCreationService.byNerEntity(nerEntity, \"laboratory_country\", EntityType.ENTITY, $section))\n .forEach(entity -> {\n entity.apply(\"DOC.7.1\", \"Performing Laboratory found\", \"n-a\");\n insert(entity);\n });\n end\n\n\nrule \"DOC.7.2: Performing Laboratory (Country & Name) from dict\"\n when\n $section: Section(\n (hasEntitiesOfType(\"laboratory_country\") || hasEntitiesOfType(\"laboratory_name\"))\n && (containsString(\"PERFORMING LABORATORY:\") || (containsString(\"PERFORMING\") && containsString(\"LABORATORY:\")))\n )\n then\n $section.getEntitiesOfType(\"laboratory_country\").forEach(entity -> {\n entity.apply(\"DOC.7.2\", \"Performing laboratory country dictionary entry found.\", \"n-a\");\n });\n $section.getEntitiesOfType(\"laboratory_name\").forEach(entity -> {\n entity.apply(\"DOC.7.2\", \"Performing laboratory name dictionary entry found.\", \"n-a\");\n });\n end\n\n\nrule \"DOC.7.3: Performing Laboratory (Country) from dict\"\n when\n $section: Section(\n (hasEntitiesOfType(\"laboratory_country\") || hasEntitiesOfType(\"laboratory_name\"))\n && !(containsString(\"PERFORMING LABORATORY:\") || (containsString(\"PERFORMING\") && containsString(\"LABORATORY:\")))\n )\n then\n $section.getEntitiesOfType(List.of(\"laboratory_country\", \"laboratory_name\")).forEach(entity -> {\n entity.removeFromGraph();\n retract(entity);\n });\n end\n\n\nrule \"DOC.8.0: GLP Study\"\n when\n $headline: Headline(containsString(\"GOOD LABORATORY PRACTICE COMPLIANCE\")\n || containsString(\"GOOD LABORATORY PRACTICE COMPLIANCE STATEMENT\")\n || (containsString(\"DECLARACAO DE CONFORMIDADE\") && containsString(\"PRATICAS DE LABORATORIO\"))\n || containsString(\"GLP Certificate\")\n || containsString(\"GLP Certificates\")\n || containsString(\"GOOD LABORATORY PRACTICE (GLP) CERTIFICATE\")\n || containsString(\"Good Laboratory Practice Certificate\")\n || containsString(\"STATEMENT OF GLP COMPLIANCE AND AUTHENTICATION\"))\n then\n entityCreationService.bySemanticNode($headline, \"glp_study\", EntityType.ENTITY).ifPresent(entity -> {\n entity.apply(\"DOC.8.0\", \"GLP Study found\", \"n-a\");\n });\n end\n\n\nrule \"DOC.9.0: Batch number from CoA\"\n when\n $section: Section(\n (\n anyHeadlineContainsString(\"Analytical Report\")\n || anyHeadlineContainsStringIgnoreCase(\"Certificate of Analysis\")\n || containsStringIgnoreCase(\"Certificate of Analysis\")\n )\n && (\n containsStringIgnoreCase(\"batch\")\n || containsStringIgnoreCase(\"bath\")\n || containsStringIgnoreCase(\"barch\")\n || containsStringIgnoreCase(\"bateb\")\n )\n && (\n containsStringIgnoreCase(\"identification\")\n || containsStringIgnoreCase(\"ldentitfication\")\n || containsStringIgnoreCase(\"wentification\")\n || containsStringIgnoreCase(\"mentification\")\n || containsStringIgnoreCase(\"kientification\")\n || containsStringIgnoreCase(\"reference number\")\n || containsStringIgnoreCase(\"test substance\")\n )\n )\n then\n entityCreationService.lineAfterStrings(List.of(\"Batch Identification\",\n \"(Batch Identification):\",\n \"Bateb Identification\",\n \"Batch Wentification\",\n \"Batch Mentification\",\n \"Batch Kientification\",\n \"Barch Identification\",\n \"Bath ldentitfication\",\n \"Batch of test substance :\"), \"batch_number\", EntityType.ENTITY, $section).forEach(entity -> {\n entity.apply(\"DOC.9.0\", \"Batch number found in CoA\", \"n-a\");\n });\n end\n\n\nrule \"DOC.9.1: Batch number\"\n when\n $section: Section(\n (\n anyHeadlineContainsStringIgnoreCase(\"Test Substance\")\n || anyHeadlineContainsStringIgnoreCase(\"Test and Control Substances\")\n || anyHeadlineContainsStringIgnoreCase(\"Test Item\")\n )\n && !(\n anyHeadlineContainsString(\"component\")\n || anyHeadlineContainsString(\"reference\")\n || anyHeadlineContainsString(\"blank\")\n )\n && containsStringIgnoreCase(\"batch\")\n )\n then\n Stream.of(entityCreationService.byRegex(\"Batch ID ([A-Z\\\\d\\\\-]{7,14})\", \"batch_number\", EntityType.ENTITY, 1, $section),\n entityCreationService.lineAfterStrings(List.of(\"Batch Identification\",\n \"Batch number:\",\n \"Batch reference number:\",\n \"Batch:\",\n \"Batch/Lot number:\",\n \"Batch (Lot) Number:\",\n \"Batch Number:\",\n \"Batch Nº:\",\n \"Batch no:\"\n ), \"batch_number\", EntityType.ENTITY, $section)).flatMap(a -> a)\n .forEach(entity -> {\n entity.apply(\"DOC.9.1\", \"Batch number found\", \"n-a\");\n });\n end\n\n\nrule \"DOC.9.2: Batch number\"\n when\n $section: Section(\n (\n anyHeadlineContainsStringIgnoreCase(\"Test Substance\")\n || anyHeadlineContainsStringIgnoreCase(\"Test and Control Substances\")\n || anyHeadlineContainsStringIgnoreCase(\"Test Item\")\n )\n && !(\n anyHeadlineContainsString(\"component\")\n || anyHeadlineContainsString(\"reference\")\n || anyHeadlineContainsString(\"blank\")\n )\n && containsStringIgnoreCase(\"batch\")\n )\n $batchNumber: String() from List.of(\"Batch Identification\",\n \"Batch number:\",\n \"Batch reference number:\",\n \"Batch:\",\n \"Batch/Lot number:\",\n \"Batch (Lot) Number:\",\n \"Batch Number:\",\n \"Batch Nº:\",\n \"Batch no:\")\n $table: Table(containsStringIgnoreCase($batchNumber)) from $section.streamAllSubNodesOfType(NodeType.TABLE).toList()\n then\n entityCreationService.lineAfterStringAcrossColumnsIgnoreCase($batchNumber, \"batch_number\", EntityType.ENTITY, $table).forEach(entity -> {\n entity.apply(\"DOC.9.2\", \"Batch number found\", \"n-a\");\n });\n end\n\n\n\n\nrule \"DOC.10.0: Conclusions - LD50, LC50, Confidence\"\n when\n FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"402\",\"403\",\"425\",\"436\"))\n $section: Section(\n (getHeadline().containsStringIgnoreCase(\"Conclusion\") || anyHeadlineContainsStringIgnoreCase(\"Lethality\"))\n && (containsString(\"LD\") || containsString(\"LC\") || containsString(\"50\") || containsString(\"LD50\") || containsString(\"lethal concentration\") || containsString(\"lethal dose\"))\n && (\n containsString(\"greater than\")\n || containsString(\"higher than\")\n || containsString(\"above\")\n || containsString(\"in excess\")\n || containsString(\"exceeds\")\n || containsString(\"was found to be\")\n || containsString(\"was calculated to be\")\n || containsString(\"estimated to be\")\n )\n )\n then\n entityCreationService.byRegexIgnoreCase(\"(L[D|C]\\\\s?50|lethal concentration|lethal dose).{1,200}(greater than|considered to be above|in excess of|exceeds|higher than)\", \"ld50_greater\", EntityType.ENTITY,2, $section).forEach(entity -> {\n entity.apply(\"DOC.10.0\", \"LD50 greater than found\", \"n-a\");\n });\n entityCreationService.byRegexIgnoreCase(\"\\\\b(?:(?:greater|higher) than|considered to be above|(?:was|is) (?:found|estimated) to be|was calculated to be|in excess of|exceeds|equal to)\\\\s?([\\\\d\\\\.]{1,6})\\\\s?mg\\\\/(?:kg|L)\", \"ld50_value\", EntityType.ENTITY,1, $section).forEach(entity -> {\n entity.apply(\"DOC.10.0\", \"LD50 value found\", \"n-a\");\n });\n entityCreationService.byRegexIgnoreCase(\"confidence interval (?:is )?([\\\\d\\\\.]{2,6}).{0,20} to (?:greater than )?([\\\\d\\\\.]{2,6})\", \"confidence_minimal\", EntityType.ENTITY,1, $section).forEach(entity -> {\n entity.apply(\"DOC.10.0\", \"Minimal Confidence found\", \"n-a\");\n });\n entityCreationService.byRegexIgnoreCase(\"confidence interval (?:is )?([\\\\d\\\\.]{2,6}).{0,20} to (?:greater than )?([\\\\d\\\\.]{2,6})\", \"confidence_maximal\", EntityType.ENTITY,2, $section).forEach(entity -> {\n entity.apply(\"DOC.10.0\", \"Maximal Confidence found\", \"n-a\");\n });\n end\n\n\nrule \"DOC.11.0: Guideline Deviation\"\n when\n FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"402\",\"403\",\"404\",\"405\",\"425\",\"429\",\"436\",\"471\"))\n $section: Section(\n (getHeadline().containsStringIgnoreCase(\"General Information\") || containsString(\"GENERAL INFORMATION\"))\n && (containsStringIgnoreCase(\"from the\") || containsStringIgnoreCase(\"to the\"))\n )\n then\n entityCreationService.betweenRegexes(\"(?:Deviations? from the [G|g]uidelines?)(?: and| or)?( the)?(?: Study Plan)?\", \"(?:(?:Deviations? from the Study Plan)|(?:Performing laboratory test)|(?:Other)|(?:Retention of [S|s]amples)|(?:Amendments? to Final Protocol))\", \"guideline_deviation\", EntityType.ENTITY, $section).forEach(entity -> {\n entity.apply(\"DOC.11.0\", \"Deviation from Guidelines found\", \"n-a\");\n });\n entityCreationService.betweenRegexes(\"(?:Deviations? (?:from|to)(?: the)? [S|s]tudy [P|p]lan)\", \"(?:Regulatory Guidelines)|(?:Other)|(?:Distribution of the report)|(?:Performing laboratory test)|(?:Distribution of the report)|(?:Retention of [S|s]amples)\", \"guideline_deviation\", EntityType.ENTITY, $section).forEach(entity -> {\n entity.apply(\"DOC.11.0\", \"Deviation from Study Plan found\", \"n-a\");\n });\n entityCreationService.betweenStrings(\"Deviations from the study plan\", \"Regulatory Guidelines\", \"guideline_deviation\", EntityType.ENTITY, $section).forEach(entity -> {\n entity.apply(\"DOC.11.0\", \"Deviation from the study plan found\", \"n-a\");\n });\n entityCreationService.byRegexIgnoreCase(\"(?>Study plan adherence)(.{1,20}deviations.{1,20} to the study plan.{0,50}\\\\.)\\\\s\", \"guideline_deviation\", EntityType.ENTITY, 1, $section).forEach(entity -> {\n entity.apply(\"DOC.11.0\", \"Guideline deviation found in text.\", \"n-a\");\n });\n entityCreationService.betweenStringsIncludeEnd(\"Deviations from the study plan\", \"validity of the study.\", \"guideline_deviation\", EntityType.ENTITY, $section).forEach(entity -> {\n entity.apply(\"DOC.11.0\", \"Deviation from the study plan found\", \"n-a\");\n });\n end\n\n\nrule \"DOC.11.1: Guideline Deviation in text\"\n when\n FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"402\",\"403\",\"404\",\"405\",\"425\",\"429\",\"436\",\"471\"))\n $section: Section(\n getHeadline().containsStringIgnoreCase(\"Introduction\")\n && containsStringIgnoreCase(\"deviations from the protocol\")\n )\n then\n entityCreationService.byRegex(\"There were no deviations from the protocol.{1,100}\\\\.\\\\s\", \"guideline_deviation\", EntityType.ENTITY, $section).forEach(entity -> {\n entity.apply(\"DOC.11.1\", \"Guideline deviation found in text.\", \"n-a\");\n });\n end\n\n\nrule \"DOC.12.0: Clinical Signs\"\n when\n FileAttribute(label == \"OECD Number\", value == \"425\")\n $headline: Headline(containsAnyStringIgnoreCase(\"Clinical Signs\", \"Macroscopic Findings\") && !containsString(\"TABLE\") && !getHeadline().containsStringIgnoreCase(\"3 - MACROSCOPIC FINDINGS\"))\n then\n entityCreationService.bySemanticNodeParagraphsOnly($headline.getParent(), \"clinical_signs\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.12.0\", \"Clinical Signs found\", \"n-a\"));\n end\n\n\nrule \"DOC.13.0: Dosages\"\n when\n FileAttribute(label == \"OECD Number\", value == \"425\")\n $section: Section(\n (anyHeadlineContainsStringIgnoreCase(\"Dosages\") || anyHeadlineContainsStringIgnoreCase(\"Study Design\"))\n && !getHeadline().containsString(\"TABLE\")\n )\n then\n entityCreationService.betweenStringsIncludeStartAndEnd(\"The animals were treated\", \".\", \"dosages\", EntityType.ENTITY, $section).forEach(entity -> {\n entity.apply(\"DOC.13.0\", \"Dosage found\", \"n-a\");\n });\n entityCreationService.betweenStringsIncludeStartAndEnd(\"Animals were treated\", \".\", \"dosages\", EntityType.ENTITY, $section).forEach(entity -> {\n entity.apply(\"DOC.13.0\", \"Dosage found\", \"n-a\");\n });\n entityCreationService.byRegexWithLineBreaks(\"(?:\\\\.[\\\\s|\\\\n]|^.{5,20}\\\\n)([^\\\\.]{1,200}(?:animal|given|received)[^\\\\.]{1,200}dose\\\\s(?:levels?\\\\s)?(?:of|at)[^\\\\.]{1,200})(?:\\\\.[\\\\s|\\\\n|$])\", \"dosages\", EntityType.ENTITY,1, $section).forEach(entity -> {\n entity.apply(\"DOC.13.0\", \"Dosage found\", \"n-a\");\n });\n end\n\n\nrule \"DOC.14.0: Mortality\"\n when\n $headline: Headline(containsString(\"Mortality\") && !containsString(\"TABLE\"))\n FileAttribute(label == \"OECD Number\", value == \"425\")\n then\n entityCreationService.bySemanticNodeParagraphsOnly($headline.getParent(), \"mortality\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.14.0\", \"Mortality found\", \"n-a\"));\n end\n\n\nrule \"DOC.15.0: Study Conclusion\"\n when\n FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"402\",\"403\",\"404\",\"405\",\"425\",\"429\",\"436\",\"471\"))\n $section: Section(\n getHeadline().containsStringIgnoreCase(\"Conclusion\")\n )\n then\n entityCreationService.bySemanticNodeParagraphsOnly($section, \"study_conclusion\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.15.0\", \"Study Conclusion found\", \"n-a\"));\n end\n\n\nrule \"DOC.16.0: Weight Behavior Changes\"\n when\n FileAttribute(label == \"OECD Number\", value == \"402\")\n $section: Section(\n getHeadline().containsString(\"Results\")\n && (\n containsString(\"body weight\")\n || containsString(\"body weights\")\n || containsString(\"bodyweight\")\n || containsString(\"bodyweights\")\n )\n )\n then\n entityCreationService.bySemanticNodeParagraphsOnly($section, \"weight_behavior_changes\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.16.0\", \"Weight behavior changes found\", \"n-a\"));\n end\n\n\nrule \"DOC.17.0: Necropsy findings\"\n when\n FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"402\",\"403\",\"436\"))\n $section: Section(\n (\n anyHeadlineContainsStringIgnoreCase(\"Necropsy\")\n || getHeadline().containsStringIgnoreCase(\"Macroscopic Findings\")\n || getHeadline().containsStringIgnoreCase(\"Macroscopic examination\")\n )\n && !getHeadline().containsStringIgnoreCase(\"Table\")\n && !getHeadline().containsStringIgnoreCase(\"Appendix\")\n && !getHeadline().containsStringIgnoreCase(\"3 - MACROSCOPIC FINDINGS\")\n )\n then\n entityCreationService.bySemanticNodeParagraphsOnly($section, \"necropsy_findings\", EntityType.ENTITY)\n .forEach( entity -> entity.apply(\"DOC.17.0\", \"Necropsy section found\", \"n-a\"));\n end\n\n\nrule \"DOC.18.0: Clinical observations\"\n when\n FileAttribute(label == \"OECD Number\", value == \"403\")\n $section: Section(\n (\n anyHeadlineContainsStringIgnoreCase(\"Clinical Observations\")\n || anyHeadlineContainsStringIgnoreCase(\"Clinical observations\")\n || anyHeadlineContainsStringIgnoreCase(\"In-life Observations\")\n || anyHeadlineContainsStringIgnoreCase(\"Postmortem Observations\")\n )\n && !anyHeadlineContainsStringIgnoreCase(\"Appendix\")\n && !anyHeadlineContainsStringIgnoreCase(\"Table\")\n && !anyHeadlineContainsStringIgnoreCase(\"Mortality\")\n )\n then\n entityCreationService.bySemanticNodeParagraphsOnly($section, \"clinical_observations\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.18.0\", \"Clinical observations section found\", \"n-a\"));\n end\n\n\nrule \"DOC.19.0: Bodyweight changes\"\n when\n FileAttribute(label == \"OECD Number\", value == \"403\")\n $headline: Headline(containsAnyStringIgnoreCase(\"Bodyweight\", \"Bodyweights\", \"Body Weights\", \"Body Weight\"), !containsAnyStringIgnoreCase(\"Appendix\", \"TABLE\"))\n then\n entityCreationService.bySemanticNodeParagraphsOnly($headline.getParent(), \"bodyweight_changes\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.19.0\", \"Bodyweight section found\", \"n-a\"));\n end\n\n\nrule \"DOC.20.0: Study Design\"\n when\n FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"402\",\"404\",\"405\",\"406\",\"428\",\"429\",\"438\",\"439\",\"474\",\"487\"))\n $section: Section(\n anyHeadlineContainsStringIgnoreCase(\"study design\")\n && !anyHeadlineContainsString(\"Preliminary screening test\")\n )\n then\n entityCreationService.bySemanticNodeParagraphsOnly($section, \"study_design\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.20.0\", \"Study design section found\", \"n-a\"));\n end\n\n\nrule \"DOC.20.1: Study Design\"\n when\n Headline(containsStringIgnoreCase(\"Study Design\"), $sectionIdentifier: getSectionIdentifier())\n $headline: Headline(getSectionIdentifier().isChildOf($sectionIdentifier))\n then\n entityCreationService.bySemanticNodeParagraphsOnly($headline.getParent(), \"study_design\", EntityType.ENTITY)\n .forEach(entity -> {\n entity.apply(\"DOC.20.1\", \"Study design section found\", \"n-a\");\n });\n end\n\n\nrule \"DOC.21.0: Results and Conclusion (406, 428, 438, 439, 474 & 487)\"\n when\n FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"406\",\"428\",\"438\",\"439\",\"474\",\"487\"))\n $parentHeadline: Headline(\n containsAnyString(\"Results\", \"Conclusion\"),\n !containsAnyString(\"POSITIVE CONTROL\", \"Positive Control\", \"Evaluation\", \"Micronucleus\", \"TABLE\", \"DISCUSSION\", \"CONCLUSIONS\", \"Interpretation\",\"Viability\", \"analysis\"),\n $sectionIdentifier: getSectionIdentifier()\n )\n not Headline(getSectionIdentifier().isChildOf($sectionIdentifier))\n then\n entityCreationService.bySemanticNodeParagraphsOnly($parentHeadline.getParent(), \"results_and_conclusion\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.21.0\", \"Results and Conclusion found\", \"n-a\"));\n end\n\n\nrule \"DOC.21.1: Results and Conclusion (406, 428, 438, 439, 474 & 487)\"\n when\n FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"406\",\"428\",\"438\",\"439\",\"474\",\"487\"))\n Headline(\n containsAnyString(\"Results\", \"Conclusion\"),\n !containsAnyString(\"POSITIVE CONTROL\", \"Positive Control\", \"Evaluation\", \"Micronucleus\", \"TABLE\", \"DISCUSSION\", \"CONCLUSIONS\", \"Interpretation\",\"Viability\", \"analysis\"),\n $sectionIdentifier: getSectionIdentifier()\n )\n $headline: Headline(getSectionIdentifier().isChildOf($sectionIdentifier))\n then\n entityCreationService.bySemanticNodeParagraphsOnly($headline.getParent(), \"results_and_conclusion\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.21.1\", \"Results and Conclusion found\", \"n-a\"));\n end\n\n\nrule \"DOC.22.0: Detailing (404 & 405)\"\n when\n FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"404\",\"405\"))\n $section: Section(\n anyHeadlineContainsStringIgnoreCase(\"Results\")\n && !getHeadline().containsStringIgnoreCase(\"Evaluation\")\n && !getHeadline().containsStringIgnoreCase(\"study\")\n )\n then\n entityCreationService.bySemanticNodeParagraphsOnly($section, \"detailing\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.22.0\", \"Detailing found\", \"n-a\"));\n end\n\n\nrule \"DOC.23.0: Preliminary Test Results (429)\"\n when\n FileAttribute(label == \"OECD Number\", value == \"429\")\n $section: Section(\n ((anyHeadlineContainsString(\"Preliminary Screening Test\") && containsString(\"Clinical observations\"))\n || anyHeadlineContainsString(\"Pre-Experiment\"))\n )\n then\n entityCreationService.bySemanticNodeParagraphsOnly($section, \"preliminary_test_results\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.23.0\", \"Preliminary Test Results found\", \"n-a\"));\n end\n\n\nrule \"DOC.24.0: Test Results (429)\"\n when\n FileAttribute(label == \"OECD Number\", value == \"429\")\n $section: Section((getHeadline().containsString(\"RESULTS AND DISCUSSION\") || getHeadline().containsString(\"Estimation of the proliferative response of lymph node cells\") || getHeadline().containsString(\"Results in the Main Experiment\")))\n then\n entityCreationService.bySemanticNodeParagraphsOnly($section, \"test_results\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.24.0\", \"Test Results found\", \"n-a\"));\n end\n\n\nrule \"DOC.24.1: Test Results (429)\"\n when\n Headline(containsStringIgnoreCase(\"RESULTS AND DISCUSSION\"), $sectionIdentifierResultsAndDiscussion: getSectionIdentifier())\n $headline: Headline(getSectionIdentifier().isChildOf($sectionIdentifierResultsAndDiscussion))\n then\n entityCreationService.bySemanticNodeParagraphsOnly($headline.getParent(), \"test_results\", EntityType.ENTITY)\n .forEach(entity -> {\n entity.apply(\"DOC.24.1\", \"Test Results found\", \"n-a\");\n });\n end\n\n\nrule \"DOC.25.0: Approach used (429)\"\n when\n FileAttribute(label == \"OECD Number\", value == \"429\")\n $section: Section(\n hasEntitiesOfType(\"species\")\n && (containsStringIgnoreCase(\"animals per\") || containsStringIgnoreCase(\"animals /\"))\n )\n then\n entityCreationService.byRegexIgnoreCase(\"\\\\banimals (?:per|\\\\/) .{0,15}(group)\\\\b\", \"approach_used\", EntityType.ENTITY,1, $section).forEach(entity -> {\n entity.apply(\"DOC.25.0\", \"Study animal approach found.\", \"n-a\");\n });\n end\n\n\nrule \"DOC.26.0: Sex\"\n when\n FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"405\",\"429\"))\n $section: Section(\n (\n anyHeadlineContainsStringIgnoreCase(\"animal\")\n || anyHeadlineContainsStringIgnoreCase(\"test system\")\n )\n && !getHeadline().containsStringIgnoreCase(\"selection\")\n && (\n containsStringIgnoreCase(\"sex:\")\n || containsStringIgnoreCase(\"male\")\n || containsStringIgnoreCase(\"female\")\n )\n )\n then\n entityCreationService.byRegexIgnoreCase(\"([S|s]ex:)?[\\\\w\\\\s]{0,10}\\\\b(males?|females?)\\\\b\", \"sex\", EntityType.ENTITY,2, $section).forEach(entity -> {\n entity.apply(\"DOC.26.0\", \"Test animal sex found\", \"n-a\");\n });\n end\n\n\nrule \"DOC.27.0: Animal Number 405\"\n when\n FileAttribute(label == \"OECD Number\", value == \"405\")\n $section: Section(\n (\n anyHeadlineContainsStringIgnoreCase(\"animal\")\n || anyHeadlineContainsStringIgnoreCase(\"test system\")\n || anyHeadlineContainsStringIgnoreCase(\"reaction\")\n )\n && !getHeadline().containsString(\"selection\")\n && (\n containsStringIgnoreCase(\"number of animals\")\n || containsStringIgnoreCase(\"no.\")\n )\n )\n then\n entityCreationService.byRegexIgnoreCase(\"(Number of animals:)[\\\\w\\\\s]{0,10}\\\\b([\\\\d]{1,3})\\\\b\", \"number_of_animals\", EntityType.ENTITY,2, $section).forEach(entity -> {\n entity.apply(\"DOC.27.0\", \"Number of animals found\", \"n-a\");\n });\n entityCreationService.byRegexIgnoreCase(\"(?:.{1,10} No\\\\. )([\\\\d\\\\w\\\\-]{3,8})\", \"animal_numbers\", EntityType.ENTITY,1, $section).forEach(entity -> {\n entity.apply(\"DOC.27.0\", \"Number of animals found\", \"n-a\");\n });\n end\n\n\nrule \"DOC.28.0: Animal Number 429\"\n when\n FileAttribute(label == \"OECD Number\", value == \"429\")\n $section: Section(\n (\n anyHeadlineContainsStringIgnoreCase(\"animal\")\n || anyHeadlineContainsStringIgnoreCase(\"test system\")\n )\n && !getHeadline().containsString(\"selection\")\n && containsStringIgnoreCase(\"number of animals\")\n && (containsStringIgnoreCase(\"per\") || containsString(\"/\"))\n && containsStringIgnoreCase(\"group\")\n )\n then\n entityCreationService.byRegexIgnoreCase(\"(Number of animals per group:)[\\\\w\\\\s]{0,10}\\\\b([\\\\d]{1,3})\\\\b\", \"number_of_animals\", EntityType.ENTITY,2, $section).forEach(entity -> {\n entity.apply(\"DOC.28.0\", \"Number of animals in group found\", \"n-a\");\n });\n entityCreationService.byRegexIgnoreCase(\"(Number of animals per group:).{0,60}\\\\b([\\\\d]{1,3})\\\\sper group\\\\b\", \"number_of_animals\", EntityType.ENTITY,2, $section).forEach(entity -> {\n entity.apply(\"DOC.28.0\", \"Number of animals in group found\", \"n-a\");\n });\n entityCreationService.byRegexIgnoreCase(\"([\\\\d]{1,3})[\\\\w\\\\s\\\\/]{0,20}(?:treatment )?group\\\\b\", \"number_of_animals\", EntityType.ENTITY,1 , $section).forEach(entity -> {\n entity.apply(\"DOC.28.0\", \"Number of animals in group found\", \"n-a\");\n });\n end\n\n\nrule \"DOC.28.1: No. Of animals - Fallback to appendix tables listing all individual animals for 429\"\n when\n $keyword: String() from List.of(\"Animal Number\", \"Animal No.\", \"Animal number\")\n $table: Table(containsString($keyword) && getHeadline().containsString(\"TABLE\") && getHeadline().containsString(\"Individual\"))\n FileAttribute(label == \"OECD Number\", value == \"429\")\n then\n $table.streamTableCellsWithHeader($keyword)\n .map(tableCell -> entityCreationService.bySemanticNode(tableCell, \"animal_numbers\", EntityType.ENTITY))\n .filter(Optional::isPresent)\n .map(Optional::get)\n .forEach(entity -> {\n entity.apply(\"DOC.28.1\", \"Animal number found.\", \"n-a\");\n insert(entity);\n });\n end\n\n\nrule \"DOC.29.0: 4h Exposure\"\n when\n FileAttribute(label == \"OECD Number\", valueEqualsAnyOf(\"403\",\"436\"))\n $section: Section(\n (containsStringIgnoreCase(\"4 hours\") || containsStringIgnoreCase(\"four hours\"))\n )\n then\n entityCreationService.byRegexIgnoreCase(\"((?<=\\\\.\\\\s\\\\b).{1,100}(4|four) hours.*?\\\\.) \", \"4h_exposure\", EntityType.ENTITY,1, $section).forEach(entity -> {\n entity.apply(\"DOC.29.0\", \"4h exposure sentence found\", \"n-a\");\n });\n end\n\n\nrule \"DOC.30.0: Dilution of the test substance\"\n when\n FileAttribute(label == \"OECD Number\", value == \"404\")\n $section: Section(\n getHeadline().containsString(\"Formulation\")\n && containsString(\"dilution\")\n )\n then\n entityCreationService.bySemanticNodeParagraphsOnly($section, \"dilution\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.30.0\", \"Dilution found.\", \"n-a\"));\n end\n\n\nrule \"DOC.31.0: Positive Control\"\n when\n FileAttribute(label == \"OECD Number\", value == \"429\")\n $section: Section(\n getHeadline().containsStringIgnoreCase(\"Positive Control\")\n && !(getHeadline().containsStringIgnoreCase(\"Appendix\") || getHeadline().containsStringIgnoreCase(\"Table\"))\n )\n then\n entityCreationService.bySemanticNodeParagraphsOnly($section, \"positive_control\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.31.0\", \"Positive control found.\", \"n-a\"));\n end\n\n\nrule \"DOC.32.0: Mortality Statement\"\n when\n FileAttribute(label == \"OECD Number\", value == \"402\")\n $headline: Headline(containsStringIgnoreCase(\"Mortality\") && !containsString(\"TABLE\"))\n then\n entityCreationService.bySemanticNodeParagraphsOnly($headline.getParent(), \"mortality_statement\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.32.0\", \"Mortality Statement found\", \"n-a\"));\n end\n\n\nrule \"DOC.33.0: Dose Mortality\"\n when\n FileAttribute(label == \"OECD Number\", value == \"425\")\n $table: Table(\n (hasHeader(\"Mortality\") || hasHeader(\"Long Term Results\") || hasHeader(\"LongTerm Outcome\") || hasHeader(\"Long Term Outcome\") || hasHeader(\"Comments\") || hasHeader(\"Viability / Mortality\") || hasHeader(\"Viability/Mortality\"))\n &&\n (hasHeader(\"Dose [mg/kg bodyweight]\") || hasHeader(\"Dose [mg/kg body weight]\") ||hasHeader(\"Dose (mg/kg)\") || hasHeader(\"Dose levei (mg/kg)\") || hasHeader(\"Dose Level (mg/kg)\") || hasHeader(\"Dose level (mg/kg)\") || hasHeader(\"Dosage [mg/kg body weight]\"))\n )\n then\n Stream.of($table.streamTableCellsWithHeader(\"Mortality\"),\n $table.streamTableCellsWithHeader(\"Comments\"),\n $table.streamTableCellsWithHeader(\"Long Term Results\"),\n $table.streamTableCellsWithHeader(\"Long Term Outcome\"),\n $table.streamTableCellsWithHeader(\"LongTerm Outcome\"),\n $table.streamTableCellsWithHeader(\"Viability / Mortality\"),\n $table.streamTableCellsWithHeader(\"Viability/Mortality\")\n ).flatMap(a -> a)\n .map(tableCell -> entityCreationService.bySemanticNode(tableCell, \"dose_mortality\", EntityType.ENTITY))\n .filter(Optional::isPresent)\n .map(Optional::get)\n .forEach(entity -> {\n entity.apply(\"DOC.33.0\", \"Dose Mortality Data found.\", \"n-a\");\n insert(entity);\n });\n\n Stream.of($table.streamTableCellsWithHeader(\"Dose [mg/kg bodyweight]\"),\n $table.streamTableCellsWithHeader(\"Dose [mg/kg body weight]\"),\n $table.streamTableCellsWithHeader(\"Dose levei (mg/kg)\"),\n $table.streamTableCellsWithHeader(\"Dose Level (mg/kg)\"),\n $table.streamTableCellsWithHeader(\"Dose level (mg/kg)\"),\n $table.streamTableCellsWithHeader(\"Dose (mg/kg)\"),\n $table.streamTableCellsWithHeader(\"Dosage [mg/kg body weight]\")\n ).flatMap(a -> a)\n .map(tableCell -> entityCreationService.bySemanticNode(tableCell, \"dose_mortality_dose\", EntityType.ENTITY))\n .filter(Optional::isPresent)\n .map(Optional::get)\n .forEach(entity -> {\n entity.apply(\"DOC.33.0\", \"Dose Mortality Data found.\", \"n-a\");\n insert(entity);\n });\n end\n\n\nrule \"DOC.34.0: Results (Main Study)\"\n when\n FileAttribute(label == \"OECD Number\", value == \"429\")\n $section: Section(\n getHeadline().containsString(\"Results\")\n && getHeadline().getTextRange().length() < 20\n && !(getHeadline().containsString(\"Appendix\") || getHeadline().containsString(\"Table\"))\n )\n then\n entityCreationService.bySemanticNodeParagraphsOnly($section, \"results_(main_study)\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.34.0\", \"Results for main study found.\", \"n-a\"));\n end\n\n\nrule \"DOC.35.0: Doses (mg/kg bodyweight)\"\n when\n FileAttribute(label == \"OECD Number\", value == \"402\")\n $section: Section(\n anyHeadlineContainsStringIgnoreCase(\"study design\")\n )\n then\n entityCreationService.bySemanticNodeParagraphsOnly($section, \"doses_(mg_kg_bw)\", EntityType.ENTITY)\n .forEach(entity -> entity.apply(\"DOC.35.0\", \"Doses per bodyweight information found\", \"n-a\"));\n end\n\n//------------------------------------ Manual redaction rules ------------------------------------\n\n// Rule unit: MAN.0\nrule \"MAN.0.0: Apply manual resize redaction\"\n salience 128\n when\n $resizeRedaction: ManualResizeRedaction($id: annotationId, status == AnnotationStatus.APPROVED, $requestDate: requestDate)\n not ManualResizeRedaction(annotationId == $id, requestDate.isBefore($requestDate))\n $entityToBeResized: TextEntity(matchesAnnotationId($id))\n then\n manualChangesApplicationService.resize($entityToBeResized, $resizeRedaction);\n retract($resizeRedaction);\n update($entityToBeResized);\n $entityToBeResized.getIntersectingNodes().forEach(node -> update(node));\n end\n\nrule \"MAN.0.1: Apply manual resize redaction\"\n salience 128\n when\n $resizeRedaction: ManualResizeRedaction($id: annotationId, status == AnnotationStatus.APPROVED, $requestDate: requestDate)\n not ManualResizeRedaction(annotationId == $id, requestDate.isBefore($requestDate))\n $imageToBeResized: Image(id == $id)\n then\n manualChangesApplicationService.resizeImage($imageToBeResized, $resizeRedaction);\n retract($resizeRedaction);\n update($imageToBeResized);\n update($imageToBeResized.getParent());\n end\n\n\n// Rule unit: MAN.1\nrule \"MAN.1.0: Apply id removals that are valid and not in forced redactions to Entity\"\n salience 128\n when\n $idRemoval: IdRemoval($id: annotationId, status == AnnotationStatus.APPROVED)\n $entityToBeRemoved: TextEntity(matchesAnnotationId($id))\n then\n $entityToBeRemoved.getManualOverwrite().addChange($idRemoval);\n update($entityToBeRemoved);\n retract($idRemoval);\n $entityToBeRemoved.getIntersectingNodes().forEach(node -> update(node));\n end\n\nrule \"MAN.1.1: Apply id removals that are valid and not in forced redactions to Image\"\n salience 128\n when\n $idRemoval: IdRemoval($id: annotationId, status == AnnotationStatus.APPROVED)\n $imageEntityToBeRemoved: Image($id == id)\n then\n $imageEntityToBeRemoved.getManualOverwrite().addChange($idRemoval);\n update($imageEntityToBeRemoved);\n retract($idRemoval);\n update($imageEntityToBeRemoved.getParent());\n end\n\n\n// Rule unit: MAN.2\nrule \"MAN.2.0: Apply force redaction\"\n salience 128\n when\n $force: ManualForceRedaction($id: annotationId, status == AnnotationStatus.APPROVED)\n $entityToForce: TextEntity(matchesAnnotationId($id))\n then\n $entityToForce.getManualOverwrite().addChange($force);\n update($entityToForce);\n $entityToForce.getIntersectingNodes().forEach(node -> update(node));\n retract($force);\n end\n\nrule \"MAN.2.1: Apply force redaction to images\"\n salience 128\n when\n $force: ManualForceRedaction($id: annotationId, status == AnnotationStatus.APPROVED)\n $imageToForce: Image(id == $id)\n then\n $imageToForce.getManualOverwrite().addChange($force);\n update($imageToForce);\n update($imageToForce.getParent());\n retract($force);\n end\n\n\n// Rule unit: MAN.3\nrule \"MAN.3.0: Apply entity recategorization\"\n salience 128\n when\n $recategorization: ManualRecategorization($id: annotationId, $type: type, status == AnnotationStatus.APPROVED, $requestDate: requestDate)\n not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate))\n $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type != $type)\n then\n $entityToBeRecategorized.getIntersectingNodes().forEach(node -> update(node));\n manualChangesApplicationService.recategorize($entityToBeRecategorized, $recategorization);\n retract($recategorization);\n // Entity is copied and inserted, so the old entity needs to be retracted to avoid duplication.\n retract($entityToBeRecategorized);\n end\n\nrule \"MAN.3.1: Apply entity recategorization of same type\"\n salience 128\n when\n $recategorization: ManualRecategorization($id: annotationId, $type: type, status == AnnotationStatus.APPROVED, $requestDate: requestDate)\n not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate))\n $entityToBeRecategorized: TextEntity(matchesAnnotationId($id), type == $type)\n then\n $entityToBeRecategorized.getManualOverwrite().addChange($recategorization);\n retract($recategorization);\n end\n\nrule \"MAN.3.2: Apply image recategorization\"\n salience 128\n when\n $recategorization: ManualRecategorization($id: annotationId, status == AnnotationStatus.APPROVED, $requestDate: requestDate)\n not ManualRecategorization($id == annotationId, requestDate.isBefore($requestDate))\n $imageToBeRecategorized: Image($id == id)\n then\n manualChangesApplicationService.recategorize($imageToBeRecategorized, $recategorization);\n update($imageToBeRecategorized);\n update($imageToBeRecategorized.getParent());\n retract($recategorization);\n end\n\n\n// Rule unit: MAN.4\nrule \"MAN.4.0: Apply legal basis change\"\n salience 128\n when\n $legalbasisChange: ManualLegalBasisChange($id: annotationId, status == AnnotationStatus.APPROVED)\n $imageToBeRecategorized: Image($id == id)\n then\n $imageToBeRecategorized.getManualOverwrite().addChange($legalbasisChange);\n end\n\nrule \"MAN.4.1: Apply legal basis change\"\n salience 128\n when\n $legalBasisChange: ManualLegalBasisChange($id: annotationId, status == AnnotationStatus.APPROVED)\n $entityToBeChanged: TextEntity(matchesAnnotationId($id))\n then\n $entityToBeChanged.getManualOverwrite().addChange($legalBasisChange);\n end\n\n\n\n//------------------------------------ Entity merging rules ------------------------------------\n\n// Rule unit: X.0\nrule \"X.0.0: remove Entity contained by Entity of same type\"\n salience 65\n when\n $larger: TextEntity($type: type, $entityType: entityType, active())\n $contained: TextEntity(containedBy($larger), type == $type, entityType == $entityType, this != $larger, !hasManualChanges(), active())\n then\n $contained.remove(\"X.0.0\", \"remove Entity contained by Entity of same type\");\n retract($contained);\n end\n\n\n// Rule unit: X.2\nrule \"X.2.0: remove Entity of type ENTITY when contained by FALSE_POSITIVE\"\n salience 64\n when\n $falsePositive: TextEntity($type: type, entityType == EntityType.FALSE_POSITIVE, active())\n $entity: TextEntity(containedBy($falsePositive), type == $type, entityType == EntityType.ENTITY, !hasManualChanges(), active())\n then\n $entity.getIntersectingNodes().forEach(node -> update(node));\n $entity.remove(\"X.2.0\", \"remove Entity of type ENTITY when contained by FALSE_POSITIVE\");\n retract($entity)\n end\n\n\n// Rule unit: X.3\nrule \"X.3.0: remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION\"\n salience 64\n when\n $falseRecommendation: TextEntity($type: type, entityType == EntityType.FALSE_RECOMMENDATION, active())\n $recommendation: TextEntity(containedBy($falseRecommendation), type == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active())\n then\n $recommendation.remove(\"X.3.0\", \"remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION\");\n retract($recommendation);\n end\n\n\n// Rule unit: X.4\nrule \"X.4.0: remove Entity of type RECOMMENDATION when intersected by ENTITY with same type\"\n salience 256\n when\n $entity: TextEntity($type: type, entityType == EntityType.ENTITY, active())\n $recommendation: TextEntity(intersects($entity), type == $type, entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active())\n then\n $entity.addEngines($recommendation.getEngines());\n $recommendation.remove(\"X.4.0\", \"remove Entity of type RECOMMENDATION when intersected by ENTITY with same type\");\n retract($recommendation);\n end\n\n\n// Rule unit: X.5\nrule \"X.5.0: remove Entity of type RECOMMENDATION when contained by ENTITY\"\n salience 256\n when\n $entity: TextEntity(entityType == EntityType.ENTITY, active())\n $recommendation: TextEntity(containedBy($entity), entityType == EntityType.RECOMMENDATION, !hasManualChanges(), active())\n then\n $recommendation.remove(\"X.5.0\", \"remove Entity of type RECOMMENDATION when contained by ENTITY\");\n retract($recommendation);\n end\n\n\n// Rule unit: X.7\nrule \"X.7.0: remove all images\"\n salience 512\n when\n $image: Image(imageType != ImageType.OCR, !hasManualChanges())\n then\n $image.remove(\"X.7.0\", \"remove all images\");\n retract($image);\n end\n\n\n//------------------------------------ File attributes rules ------------------------------------\n\n// Rule unit: FA.1\nrule \"FA.1.0: remove duplicate FileAttributes\"\n\n salience 64\n when\n $fileAttribute: FileAttribute($label: label, $value: value)\n $duplicate: FileAttribute(this != $fileAttribute, label == $label, value == $value)\n then\n retract($duplicate);\n end\n\n\n// Rule unit: LDS.0\nrule \"LDS.0.0: run local dictionary search\"\n agenda-group \"LOCAL_DICTIONARY_ADDS\"\n salience -999\n when\n $dictionaryModel: DictionaryModel(!localEntriesWithMatchedRules.isEmpty()) from dictionary.getDictionaryModels()\n then\n entityCreationService.bySearchImplementation($dictionaryModel.getLocalSearch(), $dictionaryModel.getType(), EntityType.RECOMMENDATION, document)\n .forEach(entity -> {\n Collection matchedRules = $dictionaryModel.getLocalEntriesWithMatchedRules().get(entity.getValue());\n entity.addMatchedRules(matchedRules);\n });\n end\n" \ No newline at end of file