@ -3,6 +3,7 @@ package drools
import static java.lang.String.format;
import static java.lang.String.format;
import static com.iqser.red.service.redaction.v1.server.document.services.CharSequenceSearchUtils.anyMatch;
import static com.iqser.red.service.redaction.v1.server.document.services.CharSequenceSearchUtils.anyMatch;
import static com.iqser.red.service.redaction.v1.server.document.services.EntityEnrichmentService.setFields;
import static com.iqser.red.service.redaction.v1.server.document.services.EntityEnrichmentService.setFields;
import static com.iqser.red.service.redaction.v1.server.document.mapper.PropertiesMapper.parseImageType;
import java.util.List;
import java.util.List;
@ -21,9 +22,15 @@ import com.iqser.red.service.redaction.v1.server.document.services.EntityCreatio
import com.iqser.red.service.redaction.v1.server.redaction.model.dictionary.Dictionary;
import com.iqser.red.service.redaction.v1.server.redaction.model.dictionary.Dictionary;
import com.iqser.red.service.redaction.v1.server.redaction.model.dictionary.DictionaryModel;
import com.iqser.red.service.redaction.v1.server.redaction.model.dictionary.DictionaryModel;
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualResizeRedaction;
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualResizeRedaction;
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;
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualImageRecategorization;
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.AnnotationStatus;
import com.iqser.red.service.redaction.v1.server.document.services.ManualRedactionApplicationService;
global DocumentGraph document
global DocumentGraph document
global EntityCreationService entityCreationService
global EntityCreationService entityCreationService
global ManualRedactionApplicationService manualRedactionApplicationService
global Dictionary dictionary
global Dictionary dictionary
// --------------------------------------- queries -------------------------------------------------------------------
// --------------------------------------- queries -------------------------------------------------------------------
@ -32,11 +39,64 @@ query "getFileAttributes"
$fileAttribute: FileAttribute()
$fileAttribute: FileAttribute()
end
end
// --------------------------------------- manual redaction rules -------------------------------------------------------------------
rule "Apply manual resize redaction"
salience 200
when
$resizeRedactions: ManualResizeRedaction($id: annotationId)
$entityToBeResized: EntityNode(matchesAnnotationId($id))
then
manualRedactionApplicationService.resizeEntityAndReinsert($entityToBeResized, $resizeRedactions);
retract($resizeRedactions);
update($entityToBeResized);
end
rule "Apply id removals that are valid and not in forced redactions to Entity"
salience 200
when
IdRemoval(status == AnnotationStatus.APPROVED, !removeFromDictionary, requestDate != null, $id: annotationId)
not ManualForceRedaction($id == annotationId, status == AnnotationStatus.APPROVED, requestDate != null)
$entityToBeRemoved: EntityNode(matchesAnnotationId($id))
then
$entityToBeRemoved.removeFromGraph();
retract($entityToBeRemoved);
end
rule "Apply id removals that are valid and not in forced redactions to Image"
salience 200
when
IdRemoval(status == AnnotationStatus.APPROVED, !removeFromDictionary, requestDate != null, $id: annotationId)
not ManualForceRedaction($id == annotationId, status == AnnotationStatus.APPROVED, requestDate != null)
$entityToBeRemoved: ImageNode($id == id)
then
$entityToBeRemoved.setIgnored(true);
end
rule "Apply force redaction"
salience 200
when
ManualForceRedaction($id: annotationId, status == AnnotationStatus.APPROVED, requestDate != null, $legalBasis: legalBasis)
$entityToForce: EntityNode(matchesAnnotationId($id))
then
$entityToForce.setLegalBasis($legalBasis);
$entityToForce.setRedaction(true);
$entityToForce.setSkipRemoveEntitiesContainedInLarger(true);
end
rule "Apply image recategorization"
salience 200
when
ManualImageRecategorization($id: annotationId, status == AnnotationStatus.APPROVED, $imageType: type)
$image: ImageNode($id == id)
then
$image.setImageType(parseImageType($imageType));
end
// --------------------------------------- merging rules -------------------------------------------------------------------
// --------------------------------------- merging rules -------------------------------------------------------------------
rule "merge intersecting Entities of same type"
rule "merge intersecting Entities of same type"
salience 100
salience 100
when
when
$first: EntityNode($type: type, $entityType: entityType, !resized, !skipRemoveEntitiesContainedInLarger)
$first: EntityNode($type: type, $entityType: entityType, !resized, !skipRemoveEntitiesContainedInLarger)
$second: EntityNode(intersects($first), type == $type, entityType == $entityType, this != $first, !resized, !skipRemoveEntitiesContainedInLarger)
$second: EntityNode(intersects($first), type == $type, entityType == $entityType, this != $first, !resized, !skipRemoveEntitiesContainedInLarger)
@ -51,7 +111,6 @@ rule "merge intersecting Entities of same type"
rule "remove Entity of type ENTITY when contained by FALSE_POSITIVE"
rule "remove Entity of type ENTITY when contained by FALSE_POSITIVE"
salience 100
salience 100
when
when
$falsePositive: EntityNode($type: type, entityType == EntityType.FALSE_POSITIVE)
$falsePositive: EntityNode($type: type, entityType == EntityType.FALSE_POSITIVE)
$entity: EntityNode(containedBy($falsePositive), type == $type, entityType == EntityType.ENTITY, !resized, !skipRemoveEntitiesContainedInLarger)
$entity: EntityNode(containedBy($falsePositive), type == $type, entityType == EntityType.ENTITY, !resized, !skipRemoveEntitiesContainedInLarger)
@ -62,7 +121,6 @@ rule "remove Entity of type ENTITY when contained by FALSE_POSITIVE"
rule "remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION"
rule "remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATION"
salience 100
salience 100
when
when
$falseRecommendation: EntityNode($type: type, entityType == EntityType.FALSE_RECOMMENDATION)
$falseRecommendation: EntityNode($type: type, entityType == EntityType.FALSE_RECOMMENDATION)
$recommendation: EntityNode(containedBy($falseRecommendation), type == $type, entityType == EntityType.RECOMMENDATION, !resized, !skipRemoveEntitiesContainedInLarger)
$recommendation: EntityNode(containedBy($falseRecommendation), type == $type, entityType == EntityType.RECOMMENDATION, !resized, !skipRemoveEntitiesContainedInLarger)
@ -73,7 +131,6 @@ rule "remove Entity of type RECOMMENDATION when contained by FALSE_RECOMMENDATIO
rule "remove Entity of type RECOMMENDATION when contained by ENTITY"
rule "remove Entity of type RECOMMENDATION when contained by ENTITY"
salience 100
salience 100
when
when
$entity: EntityNode($type: type, entityType == EntityType.ENTITY)
$entity: EntityNode($type: type, entityType == EntityType.ENTITY)
$recommendation: EntityNode(containedBy($entity), type == $type, entityType == EntityType.RECOMMENDATION, !resized, !skipRemoveEntitiesContainedInLarger)
$recommendation: EntityNode(containedBy($entity), type == $type, entityType == EntityType.RECOMMENDATION, !resized, !skipRemoveEntitiesContainedInLarger)
@ -84,7 +141,6 @@ rule "remove Entity of type RECOMMENDATION when contained by ENTITY"
rule "remove Entity of lower rank, when equal boundaries and entityType"
rule "remove Entity of lower rank, when equal boundaries and entityType"
salience 100
salience 100
when
when
$higherRank: EntityNode($type: type, $entityType: entityType, $boundary: boundary)
$higherRank: EntityNode($type: type, $entityType: entityType, $boundary: boundary)
$lowerRank: EntityNode($boundary == boundary, type != $type, entityType == $entityType, dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type))
$lowerRank: EntityNode($boundary == boundary, type != $type, entityType == $entityType, dictionary.getDictionaryRank(type) < dictionary.getDictionaryRank($type))
@ -98,7 +154,6 @@ rule "remove Entity of lower rank, when equal boundaries and entityType"
rule "run local dictionary search"
rule "run local dictionary search"
agenda-group "LOCAL_DICTIONARY_ADDS"
agenda-group "LOCAL_DICTIONARY_ADDS"
salience -999
salience -999
when
when
DictionaryModel(!localEntries.isEmpty(), $type: type, $searchImplementation: localSearch) from dictionary.getDictionaryModels()
DictionaryModel(!localEntries.isEmpty(), $type: type, $searchImplementation: localSearch) from dictionary.getDictionaryModels()
then
then
@ -107,12 +162,10 @@ rule "run local dictionary search"
entityNodes.forEach(entityNode -> insert(entityNode));
entityNodes.forEach(entityNode -> insert(entityNode));
end
end
// --------------------------------------- CBI rules -------------------------------------------------------------------
// --------------------------------------- CBI rules -------------------------------------------------------------------
rule "1: Redact CBI Authors (Non vertebrate study)"
rule "1: Redact CBI Authors (Non vertebrate study)"
no-loop true
no-loop true
when
when
not FileAttribute(label == "Vertebrate Study" , value.toLowerCase() == "yes")
not FileAttribute(label == "Vertebrate Study" , value.toLowerCase() == "yes")
$entity: EntityNode(type == "CBI_author", entityType == EntityType.ENTITY)
$entity: EntityNode(type == "CBI_author", entityType == EntityType.ENTITY)
@ -124,7 +177,6 @@ rule "1: Redact CBI Authors (Non vertebrate study)"
rule "2: Redact CBI Authors (Vertebrate study)"
rule "2: Redact CBI Authors (Vertebrate study)"
no-loop true
no-loop true
when
when
FileAttribute(label == "Vertebrate Study" , value.toLowerCase() == "yes")
FileAttribute(label == "Vertebrate Study" , value.toLowerCase() == "yes")
$entity: EntityNode(type == "CBI_author", entityType == EntityType.ENTITY)
$entity: EntityNode(type == "CBI_author", entityType == EntityType.ENTITY)
@ -136,7 +188,6 @@ rule "2: Redact CBI Authors (Vertebrate study)"
rule "3: Don't redact CBI Address (Non vertebrate study)"
rule "3: Don't redact CBI Address (Non vertebrate study)"
no-loop true
no-loop true
when
when
not FileAttribute(label == "Vertebrate Study" , value.toLowerCase() == "yes")
not FileAttribute(label == "Vertebrate Study" , value.toLowerCase() == "yes")
$entity: EntityNode(type == "CBI_address", entityType == EntityType.ENTITY)
$entity: EntityNode(type == "CBI_address", entityType == EntityType.ENTITY)
@ -147,7 +198,6 @@ rule "3: Don't redact CBI Address (Non vertebrate study)"
rule "4: Redact CBI Address (Vertebrate study)"
rule "4: Redact CBI Address (Vertebrate study)"
no-loop true
no-loop true
when
when
FileAttribute(label == "Vertebrate Study" , value.toLowerCase() == "yes")
FileAttribute(label == "Vertebrate Study" , value.toLowerCase() == "yes")
$entity: EntityNode(type == "CBI_address", entityType == EntityType.ENTITY)
$entity: EntityNode(type == "CBI_address", entityType == EntityType.ENTITY)
@ -158,7 +208,6 @@ rule "4: Redact CBI Address (Vertebrate study)"
end
end
rule "5: Add FALSE_POSITIVE Entity for genitive CBI_author"
rule "5: Add FALSE_POSITIVE Entity for genitive CBI_author"
when
when
$entity: EntityNode(type == "CBI_author", anyMatch(textAfter, "['’ ’ 'ʼ ˈ ´ `‘ ′ ʻ ’ ']s"), redaction)
$entity: EntityNode(type == "CBI_author", anyMatch(textAfter, "['’ ’ 'ʼ ˈ ´ `‘ ′ ʻ ’ ']s"), redaction)
then
then
@ -167,10 +216,8 @@ rule "5: Add FALSE_POSITIVE Entity for genitive CBI_author"
insert(falsePositive);
insert(falsePositive);
end
end
rule "6: Create Entity from Author(s) cells in Tables with Author(s) header and add Authorname as Recommendation"
rule "6: Create Entity from Author(s) cells in Tables with Author(s) header and add Authorname as Recommendation"
agenda-group "LOCAL_DICTIONARY_ADDS"
agenda-group "LOCAL_DICTIONARY_ADDS"
when
when
authorCell: TableCellNode(!header, (hasHeader("Author(s)") || hasHeader("Author")), hasText())
authorCell: TableCellNode(!header, (hasHeader("Author(s)") || hasHeader("Author")), hasText())
then
then
@ -182,7 +229,6 @@ rule "6: Create Entity from Author(s) cells in Tables with Author(s) header and
rule "7: Add CBI_author with \"et al.\" Regex"
rule "7: Add CBI_author with \"et al.\" Regex"
agenda-group "LOCAL_DICTIONARY_ADDS"
agenda-group "LOCAL_DICTIONARY_ADDS"
when
when
$section: SectionNode(containsString("et al."))
$section: SectionNode(containsString("et al."))
then
then
@ -193,7 +239,6 @@ rule "7: Add CBI_author with \"et al.\" Regex"
end
end
rule "8: Add recommendation for Addresses in Test Organism sections"
rule "8: Add recommendation for Addresses in Test Organism sections"
when
when
//FileAttribute(label == "Vertebrate Study" , value.toLowerCase() == "yes")
//FileAttribute(label == "Vertebrate Study" , value.toLowerCase() == "yes")
$section: SectionNode(containsString("Species") && containsString("Source") && !containsString("Species:") && !containsString("Source:"))
$section: SectionNode(containsString("Species") && containsString("Source") && !containsString("Species:") && !containsString("Source:"))
@ -203,7 +248,6 @@ rule "8: Add recommendation for Addresses in Test Organism sections"
entities.forEach(entity -> insert(entity));
entities.forEach(entity -> insert(entity));
end
end
rule "9: Add recommendation for Addresses in Test Animals sections"
rule "9: Add recommendation for Addresses in Test Animals sections"
when
when
@ -216,7 +260,6 @@ rule "9: Add recommendation for Addresses in Test Animals sections"
end
end
rule "12: Recommend CTL/BL laboratory that start with BL or CTL"
rule "12: Recommend CTL/BL laboratory that start with BL or CTL"
when
when
$section : SectionNode(containsString("BL") || containsString("CT"))
$section : SectionNode(containsString("BL") || containsString("CT"))
then
then
@ -226,13 +269,11 @@ rule "12: Recommend CTL/BL laboratory that start with BL or CTL"
entities.forEach(entity -> dictionary.addLocalDictionaryEntry("PII", entity.getValue(), false));
entities.forEach(entity -> dictionary.addLocalDictionaryEntry("PII", entity.getValue(), false));
end
end
// --------------------------------------- PII rules -------------------------------------------------------------------
// --------------------------------------- PII rules -------------------------------------------------------------------
rule "10: Redacted PII Personal Identification Information (Non vertebrate study)"
rule "10: Redacted PII Personal Identification Information (Non vertebrate study)"
no-loop true
no-loop true
when
when
not FileAttribute(label == "Vertebrate Study" , value.toLowerCase() != "yes")
not FileAttribute(label == "Vertebrate Study" , value.toLowerCase() != "yes")
$entity: EntityNode(type == "PII", entityType == EntityType.ENTITY)
$entity: EntityNode(type == "PII", entityType == EntityType.ENTITY)
@ -242,11 +283,8 @@ rule "10: Redacted PII Personal Identification Information (Non vertebrate study
update($entity)
update($entity)
end
end
rule "11: Redacted PII Personal Identification Information (Vertebrate study)"
rule "11: Redacted PII Personal Identification Information (Vertebrate study)"
no-loop true
no-loop true
when
when
FileAttribute(label == "Vertebrate Study" , value.toLowerCase() == "yes")
FileAttribute(label == "Vertebrate Study" , value.toLowerCase() == "yes")
$entity: EntityNode(type == "PII", entityType == EntityType.ENTITY)
$entity: EntityNode(type == "PII", entityType == EntityType.ENTITY)
@ -257,7 +295,6 @@ rule "11: Redacted PII Personal Identification Information (Vertebrate study)"
end
end
rule "12: Redact Emails by RegEx (Non vertebrate study)"
rule "12: Redact Emails by RegEx (Non vertebrate study)"
when
when
not FileAttribute(label == "Vertebrate Study" , value.toLowerCase() != "yes")
not FileAttribute(label == "Vertebrate Study" , value.toLowerCase() != "yes")
$section: SectionNode(containsString("@"))
$section: SectionNode(containsString("@"))
@ -268,7 +305,6 @@ rule "12: Redact Emails by RegEx (Non vertebrate study)"
end
end
rule "13: Redact Emails by RegEx (Vertebrate study)"
rule "13: Redact Emails by RegEx (Vertebrate study)"
when
when
FileAttribute(label == "Vertebrate Study" , value.toLowerCase() == "yes")
FileAttribute(label == "Vertebrate Study" , value.toLowerCase() == "yes")
$section: SectionNode(containsString("@"))
$section: SectionNode(containsString("@"))
@ -278,10 +314,8 @@ rule "13: Redact Emails by RegEx (Vertebrate study)"
entities.forEach(entity -> insert(entity));
entities.forEach(entity -> insert(entity));
end
end
rule "14: Redact line after contact information (Non vertebrate study)"
rule "14: Redact line after contact information (Non vertebrate study)"
agenda-group "LOCAL_DICTIONARY_ADDS"
agenda-group "LOCAL_DICTIONARY_ADDS"
when
when
not FileAttribute(label == "Vertebrate Study" , value.toLowerCase() == "yes")
not FileAttribute(label == "Vertebrate Study" , value.toLowerCase() == "yes")
$string: String() from List.of("Contact point:",
$string: String() from List.of("Contact point:",
@ -313,10 +347,8 @@ rule "14: Redact line after contact information (Non vertebrate study)"
entities.forEach(entity -> dictionary.addLocalDictionaryEntry("PII", entity.getValue(), false));
entities.forEach(entity -> dictionary.addLocalDictionaryEntry("PII", entity.getValue(), false));
end
end
rule "15: Redact line after contact information (Vertebrate study)"
rule "15: Redact line after contact information (Vertebrate study)"
agenda-group "LOCAL_DICTIONARY_ADDS"
agenda-group "LOCAL_DICTIONARY_ADDS"
when
when
FileAttribute(label == "Vertebrate Study" , value.toLowerCase() == "yes")
FileAttribute(label == "Vertebrate Study" , value.toLowerCase() == "yes")
$string: String() from List.of("Contact point:",
$string: String() from List.of("Contact point:",
@ -346,10 +378,8 @@ rule "15: Redact line after contact information (Vertebrate study)"
entities.forEach(entity -> dictionary.addLocalDictionaryEntry("PII", entity.getValue(), false));
entities.forEach(entity -> dictionary.addLocalDictionaryEntry("PII", entity.getValue(), false));
end
end
rule "16: redact line between contact keywords"
rule "16: redact line between contact keywords"
agenda-group "LOCAL_DICTIONARY_ADDS"
agenda-group "LOCAL_DICTIONARY_ADDS"
when
when
not FileAttribute(label == "Vertebrate Study" , value.toLowerCase() == "yes")
not FileAttribute(label == "Vertebrate Study" , value.toLowerCase() == "yes")
$section: SectionNode((containsString("No:") && containsString("Fax")) || (containsString("Contact:") && containsString("Tel")))
$section: SectionNode((containsString("No:") && containsString("Fax")) || (containsString("Contact:") && containsString("Tel")))
@ -364,7 +394,6 @@ rule "16: redact line between contact keywords"
rule "17: redact line between contact keywords"
rule "17: redact line between contact keywords"
agenda-group "LOCAL_DICTIONARY_ADDS"
agenda-group "LOCAL_DICTIONARY_ADDS"
when
when
FileAttribute(label == "Vertebrate Study" , value.toLowerCase() == "yes")
FileAttribute(label == "Vertebrate Study" , value.toLowerCase() == "yes")
$section: SectionNode((containsString("No:") && containsString("Fax")) || (containsString("Contact:") && containsString("Tel")))
$section: SectionNode((containsString("No:") && containsString("Fax")) || (containsString("Contact:") && containsString("Tel")))
@ -378,7 +407,6 @@ rule "17: redact line between contact keywords"
end
end
rule "18: Redact Phone and Fax by RegEx"
rule "18: Redact Phone and Fax by RegEx"
when
when
$section: SectionNode(containsString("Contact") ||
$section: SectionNode(containsString("Contact") ||
containsString("Telephone") ||
containsString("Telephone") ||