diff --git a/redaction-service-v1/redaction-service-server-v1/build.gradle.kts b/redaction-service-v1/redaction-service-server-v1/build.gradle.kts
index 2f56ffb6..94d35138 100644
--- a/redaction-service-v1/redaction-service-server-v1/build.gradle.kts
+++ b/redaction-service-v1/redaction-service-server-v1/build.gradle.kts
@@ -12,7 +12,7 @@ plugins {
description = "redaction-service-server-v1"
-val layoutParserVersion = "0.161.0"
+val layoutParserVersion = "0.174.0"
val jacksonVersion = "2.15.2"
val droolsVersion = "9.44.0.Final"
val pdfBoxVersion = "3.0.0"
diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/RedactionServiceSettings.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/RedactionServiceSettings.java
index 2b6e4b47..5229dcbf 100644
--- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/RedactionServiceSettings.java
+++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/RedactionServiceSettings.java
@@ -38,6 +38,8 @@ public class RedactionServiceSettings {
private boolean annotationMode;
+ private boolean droolsDebug;
+
public int getDroolsExecutionTimeoutSecs(int numberOfPages) {
diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/logger/ObjectTrackingEventListener.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/logger/ObjectTrackingEventListener.java
new file mode 100644
index 00000000..d52b5e72
--- /dev/null
+++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/logger/ObjectTrackingEventListener.java
@@ -0,0 +1,70 @@
+package com.iqser.red.service.redaction.v1.server.logger;
+
+import org.kie.api.definition.rule.Rule;
+import org.kie.api.event.rule.DefaultRuleRuntimeEventListener;
+import org.kie.api.event.rule.ObjectDeletedEvent;
+import org.kie.api.event.rule.ObjectInsertedEvent;
+import org.kie.api.event.rule.ObjectUpdatedEvent;
+
+import lombok.AllArgsConstructor;
+
+@AllArgsConstructor
+public class ObjectTrackingEventListener extends DefaultRuleRuntimeEventListener {
+
+ RulesLogger logger;
+
+
+ @Override
+ public void objectInserted(ObjectInsertedEvent event) {
+
+ if (!logger.isObjectTrackingActive()) {
+ return;
+ }
+
+ if (event.getRule() == null) {
+ logger.logObjectTracking("ObjectInsertedEvent:{} has been inserted", event.getObject());
+ return;
+ }
+
+ logger.logObjectTracking("ObjectInsertedEvent:{}: {} has been inserted", formatRuleName(event.getRule()), event.getObject());
+ }
+
+
+ @Override
+ public void objectDeleted(ObjectDeletedEvent event) {
+
+ if (!logger.isObjectTrackingActive()) {
+ return;
+ }
+ if (event.getRule() == null) {
+ logger.logObjectTracking("ObjectDeletedEvent: {} has been deleted", event.getOldObject());
+ return;
+ }
+ logger.logObjectTracking("ObjectDeletedEvent: {}: {} has been deleted", formatRuleName(event.getRule()), event.getOldObject());
+ }
+
+
+ @Override
+ public void objectUpdated(ObjectUpdatedEvent event) {
+
+ if (!logger.isObjectTrackingActive()) {
+ return;
+ }
+ if (event.getRule() == null) {
+ logger.logObjectTracking("ObjectUpdatedEvent:{} has been updated", event.getObject());
+ return;
+ }
+ logger.logObjectTracking("ObjectUpdatedEvent:{}: {} has been updated", formatRuleName(event.getRule()), event.getObject());
+ }
+
+
+ public static String formatRuleName(Rule rule) {
+
+ String name = rule.getName();
+ if (name.length() > 20) {
+ return name.substring(0, 20) + "...";
+ }
+ return name;
+ }
+
+}
diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/logger/RulesLogger.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/logger/RulesLogger.java
index 37304f3e..1560d5c9 100644
--- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/logger/RulesLogger.java
+++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/logger/RulesLogger.java
@@ -1,31 +1,40 @@
package com.iqser.red.service.redaction.v1.server.logger;
import java.time.OffsetDateTime;
-import java.util.regex.Pattern;
+
+import org.slf4j.helpers.MessageFormatter;
import com.iqser.red.service.redaction.v1.server.service.websocket.WebSocketService;
+import lombok.Getter;
import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
/**
* This class provides logging functionality specifically for rules execution
* in a Drools context. It is designed to log messages with different log levels
* (INFO, WARN, ERROR) and formats messages using a placeholder-based approach
* similar to popular logging frameworks like SLF4J.
- *
+ *
* Log messages can include placeholders (i.e., `{}`), which will be replaced by
* the corresponding arguments when the message is formatted.
- *
+ *
* Example usage:
*
* logger.info("Message with placeholder {}", object);
*
*/
+@Slf4j
@RequiredArgsConstructor
public class RulesLogger {
private final WebSocketService webSocketService;
private final Context context;
+ @Getter
+ private boolean objectTrackingActive;
+ @Getter
+ private boolean agendaTrackingActive;
+
/**
* Logs a message at the INFO level.
@@ -51,6 +60,75 @@ public class RulesLogger {
}
+ /**
+ * Logs a message at the INFO level, if object tracking has been activated.
+ *
+ * @param message The log message containing optional placeholders (i.e., `{}`).
+ * @param args The arguments to replace the placeholders in the message.
+ */
+ public void logObjectTracking(String message, Object... args) {
+
+ if (objectTrackingActive) {
+ info(message, args);
+ }
+ }
+
+
+ /**
+ * If object tracking is enabled, the RulesLogger will log all inserted/retracted/updated events.
+ * Initial value is disabled.
+ */
+ public void enableObjectTracking() {
+
+ objectTrackingActive = true;
+ }
+
+
+ /**
+ * If object tracking is disabled, the RulesLogger won't log any inserted/retracted/updated events.
+ * Initial value is disabled.
+ */
+ public void disableObjectTracking() {
+
+ objectTrackingActive = false;
+ }
+
+
+ /**
+ * Logs a message at the INFO level, if agenda tracking has been activated.
+ *
+ * @param message The log message containing optional placeholders (i.e., `{}`).
+ * @param args The arguments to replace the placeholders in the message.
+ */
+ public void logAgendaTracking(String message, Object... args) {
+
+ if (agendaTrackingActive) {
+ info(message, args);
+ }
+
+ }
+
+
+ /**
+ * If agenda tracking is enabled, the RulesLogger will log each firing Rule with its name, objects and metadata.
+ * Initial value is disabled.
+ */
+ public void enableAgendaTracking() {
+
+ agendaTrackingActive = true;
+ }
+
+
+ /**
+ * If agenda tracking is disabled, the RulesLogger won't log any rule firings.
+ * Initial value is disabled.
+ */
+ public void disableAgendaTracking() {
+
+ agendaTrackingActive = false;
+ }
+
+
/**
* Logs a message at the ERROR level, including an exception.
*
@@ -67,6 +145,11 @@ public class RulesLogger {
private void log(LogLevel logLevel, String message, Object... args) {
var formattedMessage = formatMessage(message, args);
+ switch (logLevel) {
+ case INFO -> log.info(message, args);
+ case WARN -> log.warn(message, args);
+ case ERROR -> log.error(message, args);
+ }
var ruleLog = RuleLogEvent.builder()
.tenantId(context.getTenantId())
.ruleVersion(context.getRuleVersion())
@@ -85,22 +168,7 @@ public class RulesLogger {
private String formatMessage(String message, Object... args) {
- if (args == null || args.length == 0) {
- return message;
- }
-
- var pattern = Pattern.compile("\\{}");
- var matcher = pattern.matcher(message);
- var sb = new StringBuilder();
- int i = 0;
-
- while (matcher.find() && i < args.length) {
- matcher.appendReplacement(sb, args[i] != null ? args[i].toString() : "null");
- i++;
- }
- matcher.appendTail(sb);
-
- return sb.toString();
+ return MessageFormatter.arrayFormat(message, args).getMessage();
}
}
diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/logger/TrackingAgendaEventListener.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/logger/TrackingAgendaEventListener.java
new file mode 100644
index 00000000..497af362
--- /dev/null
+++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/logger/TrackingAgendaEventListener.java
@@ -0,0 +1,65 @@
+package com.iqser.red.service.redaction.v1.server.logger;
+
+import java.util.Map;
+
+import org.kie.api.definition.rule.Rule;
+import org.kie.api.event.rule.AfterMatchFiredEvent;
+import org.kie.api.event.rule.DefaultAgendaEventListener;
+import org.kie.api.event.rule.MatchCreatedEvent;
+
+import lombok.AllArgsConstructor;
+
+@AllArgsConstructor
+public class TrackingAgendaEventListener extends DefaultAgendaEventListener {
+
+ private RulesLogger logger;
+
+
+ @Override
+ public void matchCreated(MatchCreatedEvent event) {
+
+ if (logger.isAgendaTrackingActive()) {
+ logger.logAgendaTracking(event.toString());
+ }
+ }
+
+
+ @Override
+ public void afterMatchFired(AfterMatchFiredEvent event) {
+
+ if (!logger.isAgendaTrackingActive()) {
+ return;
+ }
+
+ Rule rule = event.getMatch().getRule();
+
+ String ruleName = formatRuleName(rule);
+ Map ruleMetaDataMap = rule.getMetaData();
+
+ StringBuilder sb = new StringBuilder("AfterMatchFiredEvent: " + ruleName);
+
+ if (event.getMatch().getObjects() != null && !event.getMatch().getObjects().isEmpty()) {
+ sb.append(", ").append(event.getMatch().getObjects().size()).append(" objects: ");
+ for (Object object : event.getMatch().getObjects()) {
+ sb.append(object).append(", ");
+ }
+ sb.delete(sb.length() - 2, sb.length());
+ }
+
+ if (!ruleMetaDataMap.isEmpty()) {
+ sb.append("\n With [").append(ruleMetaDataMap.size()).append("] meta-data:");
+ for (String key : ruleMetaDataMap.keySet()) {
+ sb.append("\n key=").append(key).append(", value=").append(ruleMetaDataMap.get(key));
+ }
+ }
+
+ logger.logAgendaTracking(sb.toString());
+ }
+
+
+ public static String formatRuleName(Rule rule) {
+
+ return ObjectTrackingEventListener.formatRuleName(rule);
+ }
+
+}
\ No newline at end of file
diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/PrecursorEntity.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/PrecursorEntity.java
index a3ec0376..e942c582 100644
--- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/PrecursorEntity.java
+++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/PrecursorEntity.java
@@ -180,6 +180,22 @@ public class PrecursorEntity implements IEntity {
}
+ /**
+ * @return true when this entity is of EntityType ENTITY or HINT
+ */
+ public boolean validEntityType() {
+
+ return entityType.equals(EntityType.ENTITY) || entityType.equals(EntityType.HINT);
+ }
+
+
+ @Override
+ public boolean valid() {
+
+ return active() && validEntityType();
+ }
+
+
private static EntityType getEntityType(EntryType entryType) {
switch (entryType) {
diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/dictionary/SearchImplementation.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/dictionary/SearchImplementation.java
index 42862452..0fd97aed 100644
--- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/dictionary/SearchImplementation.java
+++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/dictionary/SearchImplementation.java
@@ -11,6 +11,7 @@ import java.util.stream.Stream;
import org.ahocorasick.trie.Trie;
import com.iqser.red.service.redaction.v1.server.model.document.TextRange;
+import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlock;
import lombok.Data;
@@ -104,6 +105,12 @@ public class SearchImplementation {
}
+ public Stream getBoundaries(TextBlock textBlock) {
+
+ return getBoundaries(textBlock, textBlock.getTextRange());
+ }
+
+
public Stream getBoundaries(CharSequence text, TextRange region) {
if (this.values.isEmpty()) {
diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/entity/IEntity.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/entity/IEntity.java
index ebf5e740..3cc3cb3c 100644
--- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/entity/IEntity.java
+++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/model/document/entity/IEntity.java
@@ -52,6 +52,17 @@ public interface IEntity {
String type();
+ /**
+ * An Entity is valid, when it active and not a false recommendation, a false positive or a dictionary removal.
+ *
+ * @return true, if the entity is valid, false otherwise/
+ */
+ default boolean valid() {
+
+ return active();
+ }
+
+
/**
* Calculates the length of the entity's value.
*
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 a80a00c8..f8334be6 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
@@ -289,6 +289,21 @@ public class TextEntity implements IEntity {
}
+ /**
+ * @return true when this entity is of EntityType ENTITY or HINT
+ */
+ public boolean validEntityType() {
+
+ return entityType.equals(EntityType.ENTITY) || entityType.equals(EntityType.HINT);
+ }
+
+
+ public boolean valid() {
+
+ return active() && validEntityType();
+ }
+
+
@Override
public String value() {
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 0fa334d0..ef0b8ddd 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
@@ -115,7 +115,7 @@ public class Page {
@Override
public String toString() {
- return String.valueOf(number);
+ return "Page: " + number;
}
}
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 884a39f3..5b6aa021 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
@@ -17,6 +17,7 @@ import java.util.stream.Stream;
import com.iqser.red.service.redaction.v1.server.model.document.ConsecutiveTextBlockCollector;
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.IEntity;
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;
@@ -77,6 +78,20 @@ public interface SemanticNode {
Set getEntities();
+ /**
+ * A view of the Entity Set of this SemanticNode including only the active (APPLIED or SKIPPED) Entities which are of a valid type (ENTITY or HINT).
+ * This is used for all functions, which check for the existence of an Entity, such as hasEntityOfType().
+ *
+ * @return Set of valid TextEntities
+ */
+ default Stream streamValidEntities() {
+
+ return getEntities().stream()
+ .filter(IEntity::active)
+ .filter(TextEntity::validEntityType);
+ }
+
+
/**
* Each AtomicTextBlock is assigned a page, so to get the pages this node appears on, it collects the PageNodes from each AtomicTextBlock belonging to this node's TextBlock.
*
@@ -277,9 +292,7 @@ public interface SemanticNode {
*/
default boolean hasEntitiesOfType(String type) {
- return getEntities().stream()
- .filter(TextEntity::active)
- .anyMatch(redactionEntity -> redactionEntity.type().equals(type));
+ return streamValidEntities().anyMatch(redactionEntity -> redactionEntity.type().equals(type));
}
@@ -292,10 +305,8 @@ public interface SemanticNode {
*/
default boolean hasEntitiesOfAnyType(String... types) {
- return getEntities().stream()
- .filter(TextEntity::active)
- .anyMatch(redactionEntity -> Arrays.stream(types)
- .anyMatch(type -> redactionEntity.type().equals(type)));
+ return streamValidEntities().anyMatch(redactionEntity -> Arrays.stream(types)
+ .anyMatch(type -> redactionEntity.type().equals(type)));
}
@@ -308,9 +319,7 @@ public interface SemanticNode {
*/
default boolean hasEntitiesOfAllTypes(String... types) {
- return getEntities().stream()
- .filter(TextEntity::active)
- .map(TextEntity::type)
+ return streamValidEntities().map(TextEntity::type)
.collect(Collectors.toUnmodifiableSet())
.containsAll(Arrays.stream(types)
.toList());
@@ -319,31 +328,28 @@ public interface SemanticNode {
/**
* Returns a List of Entities in this SemanticNode which are of the provided type such as "CBI_author".
- * Ignores Entity with ignored == true or removed == true.
+ * Ignores Entity which are not active or of a removal type ignored == true or removed == true.
*
* @param type string representing the type of entities to return
* @return List of RedactionEntities of any the type
*/
default List getEntitiesOfType(String type) {
- return getEntities().stream()
- .filter(TextEntity::active)
- .filter(redactionEntity -> redactionEntity.type().equals(type))
+ return streamValidEntities().filter(redactionEntity -> redactionEntity.type().equals(type))
.toList();
}
/**
* Returns a List of Entities in this SemanticNode which have any of the provided types such as "CBI_author".
- * Ignores Entity with ignored == true or removed == true.
+ * Ignores Entity that are not valid.
*
* @param types A list of strings representing the types of entities to return
* @return List of RedactionEntities of any provided type
*/
default List getEntitiesOfType(List types) {
- return getEntities().stream()
- .filter(TextEntity::active)
+ return streamValidEntities()//
.filter(redactionEntity -> redactionEntity.isAnyType(types))
.toList();
}
@@ -351,15 +357,14 @@ public interface SemanticNode {
/**
* Returns a List of Entities in this SemanticNode which have any of the provided types.
- * Ignores Entity with the ignored flag set to true or the removed flag set to true.
+ * Ignores Entity that are not valid.
*
* @param types A list of strings representing the types of entities to return
* @return List of RedactionEntities that match any of the provided types
*/
default List getEntitiesOfType(String... types) {
- return getEntities().stream()
- .filter(TextEntity::active)
+ return streamValidEntities()//
.filter(redactionEntity -> redactionEntity.isAnyType(Arrays.stream(types)
.toList()))
.toList();
@@ -463,7 +468,7 @@ public interface SemanticNode {
*/
default boolean containsStringIgnoreCase(String string) {
- return getTextBlock().getSearchText().toLowerCase(Locale.ROOT).contains(string.toLowerCase(Locale.ROOT));
+ return getTextBlock().getSearchTextLowerCase().contains(string.toLowerCase(Locale.ROOT));
}
@@ -774,13 +779,12 @@ public interface SemanticNode {
/**
- * TODO: this produces unwanted results for sections spanning multiple columns.
* Computes the Union of the bounding boxes of all children recursively.
*
* @return The union of the BoundingBoxes of all children
*/
private Map getBBoxFromChildren() {
-
+ //TODO: this produces unwanted results for sections spanning multiple columns.
Map bBoxPerPage = new HashMap<>();
List