From 038dce6e58bafeed7fc88e973611052dbf224eda Mon Sep 17 00:00:00 2001 From: Kilian Schuettler Date: Thu, 4 May 2023 13:08:24 +0200 Subject: [PATCH] RED-6009 - Document Tree Structure * implemented table merging when no headers present --- .../classification/model/table/Cell.java | 2 + .../document/factory/SectionNodeFactory.java | 8 ++- .../document/factory/TableNodeFactory.java | 49 +++++++++++++------ .../services/EntityCreationService.java | 16 +++--- ...Utils.java => RedactionSearchUtility.java} | 5 +- .../document/utils/TableMergingUtility.java | 40 +++++++++++++++ .../drools/manual_redaction_rules.drl | 6 +-- .../src/test/resources/drools/rules.drl | 8 +-- .../src/test/resources/drools/rules_v2.drl | 6 +-- 9 files changed, 104 insertions(+), 36 deletions(-) rename redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/utils/{RedactionSearchUtils.java => RedactionSearchUtility.java} (97%) create mode 100644 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/utils/TableMergingUtility.java diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/model/table/Cell.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/model/table/Cell.java index 8e38323c..f8d66ffe 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/model/table/Cell.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/classification/model/table/Cell.java @@ -27,6 +27,8 @@ public class Cell extends Rectangle { private static final int MIN_SIZE = 1; + private int pageNumber; + public Cell(Point2D topLeft, Point2D bottomRight) { diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/factory/SectionNodeFactory.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/factory/SectionNodeFactory.java index e69c1cfd..407ffc69 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/factory/SectionNodeFactory.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/factory/SectionNodeFactory.java @@ -18,6 +18,7 @@ import com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.no import com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.nodes.PageNode; import com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.nodes.SectionNode; import com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.nodes.SemanticNode; +import com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.TableMergingUtility; import lombok.experimental.UtilityClass; @@ -86,8 +87,11 @@ public class SectionNodeFactory { alreadyMerged.addAll(textBlocks); DocumentGraphFactory.addParagraphOrHeadline(sectionNode, (ClassificationTextBlock) abstractPageBlock, context, textBlocks); } - if (abstractPageBlock instanceof TablePageBlock) { - TableNodeFactory.addTable(sectionNode, (TablePageBlock) abstractPageBlock, context); + if (abstractPageBlock instanceof TablePageBlock tablePageBlock) { + + List tablesToMerge = TableMergingUtility.findConsecutiveTablesWithSameColCountAndSameHeaders(tablePageBlock, pageBlocks); + alreadyMerged.addAll(tablesToMerge); + TableNodeFactory.addTable(sectionNode, tablesToMerge, context); } } } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/factory/TableNodeFactory.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/factory/TableNodeFactory.java index fce245f6..82f673cc 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/factory/TableNodeFactory.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/factory/TableNodeFactory.java @@ -2,7 +2,10 @@ package com.iqser.red.service.redaction.v1.server.layoutparsing.document.factory import static java.util.Collections.emptyList; +import java.util.Collection; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.AbstractPageBlock; import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table.Cell; @@ -23,23 +26,40 @@ public class TableNodeFactory { public static final double TABLE_CELL_MERGE_CONTENTS_SIZE_THRESHOLD = 0.05; - public void addTable(SemanticNode parentNode, TablePageBlock table, DocumentGraphFactory.Context context) { + public void addTable(SemanticNode parentNode, List tablesToMerge, DocumentGraphFactory.Context context) { - PageNode page = context.getPage(table.getPage()); - TableNode tableNode = TableNode.builder().tableOfContents(context.getTableOfContents()).numberOfCols(table.getColCount()).numberOfRows(table.getRowCount()).build(); + setPageNumberInCellTextBlocks(tablesToMerge); + Set pages = tablesToMerge.stream().map(AbstractPageBlock::getPage).map(context::getPage).collect(Collectors.toSet()); + List> mergedRows = tablesToMerge.stream().map(TablePageBlock::getRows).flatMap(Collection::stream).toList(); + TableNode tableNode = TableNode.builder().tableOfContents(context.getTableOfContents()).numberOfCols(mergedRows.get(0).size()).numberOfRows(mergedRows.size()).build(); + + pages.forEach(page -> addTableToPage(page, parentNode, tableNode)); + + List tocId = context.getTableOfContents().createNewChildEntryAndReturnId(parentNode.getTocId(), NodeType.TABLE, tableNode); + tableNode.setTocId(tocId); + addTableCells(mergedRows, tableNode, context); + + IfTableHasNoHeadersAssumeFirstRowAreHeaders(tableNode); + } + + + private static void setPageNumberInCellTextBlocks(List tablesToMerge) { + + tablesToMerge.forEach(table -> table.getRows() + .stream() + .flatMap(Collection::stream) + .peek(cell -> cell.setPageNumber(table.getPage())) + .forEach(cell -> cell.getTextBlocks().stream().filter(tb -> tb.getPage() == 0).forEach(tb -> tb.setPage(table.getPage())))); + } + + + private static void addTableToPage(PageNode page, SemanticNode parentNode, TableNode tableNode) { if (!page.getMainBody().contains(parentNode)) { parentNode.getPages().add(page); } page.getMainBody().add(tableNode); - - List tocId = context.getTableOfContents().createNewChildEntryAndReturnId(parentNode.getTocId(), NodeType.TABLE, tableNode); - tableNode.setTocId(tocId); - - addTableCells(table.getRows(), tableNode, context, table.getPage()); - - IfTableHasNoHeadersAssumeFirstRowAreHeaders(tableNode); } @@ -51,21 +71,20 @@ public class TableNodeFactory { } - private void addTableCells(List> rows, TableNode tableNode, DocumentGraphFactory.Context context, int pageNumber) { + private void addTableCells(List> rows, TableNode tableNode, DocumentGraphFactory.Context context) { for (int rowIndex = 0; rowIndex < rows.size(); rowIndex++) { for (int colIndex = 0; colIndex < rows.get(rowIndex).size(); colIndex++) { - addTableCell(rows.get(rowIndex).get(colIndex), rowIndex, colIndex, tableNode, pageNumber, context); + addTableCell(rows.get(rowIndex).get(colIndex), rowIndex, colIndex, tableNode, context); } } } @SuppressWarnings("PMD.UnusedPrivateMethod") // PMD actually flags this wrong - private void addTableCell(Cell cell, int rowIndex, int colIndex, SemanticNode parentNode, int pageNumber, DocumentGraphFactory.Context context) { + private void addTableCell(Cell cell, int rowIndex, int colIndex, SemanticNode parentNode, DocumentGraphFactory.Context context) { - PageNode page = context.getPage(pageNumber); - cell.getTextBlocks().stream().filter(tb -> tb.getPage() == 0).forEach(tb -> tb.setPage(pageNumber)); + PageNode page = context.getPage(cell.getPageNumber()); TableCellNode tableCellNode = TableCellNode.builder() .tableOfContents(context.getTableOfContents()) diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/services/EntityCreationService.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/services/EntityCreationService.java index 7c76c4fc..7a683305 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/services/EntityCreationService.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/services/EntityCreationService.java @@ -1,7 +1,7 @@ package com.iqser.red.service.redaction.v1.server.layoutparsing.document.services; -import static com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RedactionSearchUtils.getExpandedEndByRegex; -import static com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RedactionSearchUtils.getExpandedStartByRegex; +import static com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RedactionSearchUtility.getExpandedEndByRegex; +import static com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RedactionSearchUtility.getExpandedStartByRegex; import static com.iqser.red.service.redaction.v1.server.redaction.utils.SeparatorUtils.boundaryIsSurroundedBySeparators; import static com.iqser.red.service.redaction.v1.server.redaction.utils.SeparatorUtils.isWhiteSpacesOrSeparatorsOnly; @@ -27,7 +27,7 @@ import com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.no import com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.nodes.SemanticNode; import com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.nodes.TableCellNode; import com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.textblock.TextBlock; -import com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RedactionSearchUtils; +import com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RedactionSearchUtility; import com.iqser.red.service.redaction.v1.server.redaction.adapter.NerEntities; import com.iqser.red.service.redaction.v1.server.redaction.model.EntityType; import com.iqser.red.service.redaction.v1.server.redaction.model.dictionary.SearchImplementation; @@ -47,8 +47,8 @@ public class EntityCreationService { public Stream betweenStrings(String start, String stop, String type, EntityType entityType, SemanticNode node) { TextBlock textBlock = node.buildTextBlock(); - List startBoundaries = RedactionSearchUtils.findBoundariesByString(start, textBlock); - List stopBoundaries = RedactionSearchUtils.findBoundariesByString(stop, textBlock); + List startBoundaries = RedactionSearchUtility.findBoundariesByString(start, textBlock); + List stopBoundaries = RedactionSearchUtility.findBoundariesByString(stop, textBlock); List entityBoundaries = new LinkedList<>(); if (startBoundaries.isEmpty() || stopBoundaries.isEmpty()) { @@ -90,7 +90,7 @@ public class EntityCreationService { public Stream lineAfterString(String string, String type, EntityType entityType, SemanticNode node) { TextBlock textBlock = node.buildTextBlock(); - return RedactionSearchUtils.findBoundariesByString(string, textBlock) + return RedactionSearchUtility.findBoundariesByString(string, textBlock) .stream() .map(boundary -> toLineAfterBoundary(textBlock, boundary)) .filter(boundary -> isValidEntityBoundary(textBlock, boundary)) @@ -100,13 +100,13 @@ public class EntityCreationService { public Stream byRegex(String regexPattern, String type, EntityType entityType, SemanticNode node) { - return RedactionSearchUtils.findBoundariesByRegex(regexPattern, node.buildTextBlock()).stream().map(boundary -> byBoundary(boundary, type, entityType, node)); + return RedactionSearchUtility.findBoundariesByRegex(regexPattern, node.buildTextBlock()).stream().map(boundary -> byBoundary(boundary, type, entityType, node)); } public Stream byString(String keyword, String type, EntityType entityType, SemanticNode node) { - return RedactionSearchUtils.findBoundariesByString(keyword, node.buildTextBlock()).stream().map(boundary -> byBoundary(boundary, type, entityType, node)); + return RedactionSearchUtility.findBoundariesByString(keyword, node.buildTextBlock()).stream().map(boundary -> byBoundary(boundary, type, entityType, node)); } diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/utils/RedactionSearchUtils.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/utils/RedactionSearchUtility.java similarity index 97% rename from redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/utils/RedactionSearchUtils.java rename to redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/utils/RedactionSearchUtility.java index 2037b882..e0bb8d8c 100644 --- a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/utils/RedactionSearchUtils.java +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/utils/RedactionSearchUtility.java @@ -12,7 +12,10 @@ import com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.en import com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.textblock.TextBlock; import com.iqser.red.service.redaction.v1.server.redaction.utils.Patterns; -public class RedactionSearchUtils { +import lombok.experimental.UtilityClass; + +@UtilityClass +public class RedactionSearchUtility { public static boolean anyMatch(CharSequence charSequence, String regexPattern) { diff --git a/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/utils/TableMergingUtility.java b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/utils/TableMergingUtility.java new file mode 100644 index 00000000..f483ad0d --- /dev/null +++ b/redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/layoutparsing/document/utils/TableMergingUtility.java @@ -0,0 +1,40 @@ +package com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils; + +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Stream; + +import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.AbstractPageBlock; +import com.iqser.red.service.redaction.v1.server.layoutparsing.classification.model.table.TablePageBlock; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class TableMergingUtility { + + public List findConsecutiveTablesWithSameColCountAndSameHeaders(TablePageBlock originalTablePageBlock, List pageBlocks) { + + List consecutiveTables = pageBlocks.stream() + .map(abstractPageBlock -> (TablePageBlock) abstractPageBlock) + .filter(tablePageBlock -> !tablePageBlock.equals(originalTablePageBlock)) + .toList(); + assert consecutiveTables.size() == pageBlocks.size() - 1; + + List consecutiveTablesWithSameColCountAndHeaders = new LinkedList<>(); + for (TablePageBlock consecutiveTable : consecutiveTables) { + if (consecutiveTable.getColCount() == originalTablePageBlock.getColCount() && !hasTableHeader(consecutiveTable)) { + consecutiveTablesWithSameColCountAndHeaders.add(consecutiveTable); + } else { + break; + } + } + return Stream.concat(Stream.of(originalTablePageBlock), consecutiveTablesWithSameColCountAndHeaders.stream()).toList(); + } + + + private boolean hasTableHeader(TablePageBlock table) { + + return table.getRows().stream().anyMatch(row -> row.stream().anyMatch(cell -> !cell.isHeaderCell() && !cell.getHeaderCells().isEmpty())); + } + +} 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 2b390d42..d501c8b6 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 @@ -1,8 +1,8 @@ package drools import static java.lang.String.format; -import static com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RedactionSearchUtils.anyMatch; -import static com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RedactionSearchUtils.exactMatch; +import static com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RedactionSearchUtility.anyMatch; +import static com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RedactionSearchUtility.exactMatch; import static com.iqser.red.service.redaction.v1.server.layoutparsing.document.data.mapper.PropertiesMapper.parseImageType; import java.util.List; @@ -37,7 +37,7 @@ import com.iqser.red.service.redaction.v1.server.layoutparsing.document.graph.Bo import java.util.stream.Collectors import java.util.Collection import java.util.stream.Stream -import com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RedactionSearchUtils; +import com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RedactionSearchUtility; global DocumentGraph document global ManualRedactionApplicationService manualRedactionApplicationService 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 07a5de60..5eebff6f 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 @@ -1,8 +1,8 @@ package drools import static java.lang.String.format; -import static com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RedactionSearchUtils.anyMatch; -import static com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RedactionSearchUtils.exactMatch; +import static com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RedactionSearchUtility.anyMatch; +import static com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RedactionSearchUtility.exactMatch; import static com.iqser.red.service.redaction.v1.server.layoutparsing.document.data.mapper.PropertiesMapper.parseImageType; import java.util.List; @@ -37,7 +37,7 @@ import com.iqser.red.service.redaction.v1.server.redaction.adapter.NerEntities; import java.util.stream.Collectors; import java.util.Collection; import java.util.stream.Stream; -import com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RedactionSearchUtils; +import com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RedactionSearchUtility; global DocumentGraph document global EntityCreationService entityCreationService @@ -509,7 +509,7 @@ rule "102: Guidelines FileAttributes" when $section: SectionNode(excludesTables, (containsString("DATA REQUIREMENT(S):") || containsString("TEST GUIDELINE(S):")) && (containsString("OECD") || containsString("EPA") || containsString("OPPTS"))) then - RedactionSearchUtils.findBoundariesByRegex("OECD (No\\.? )?\\d{3}( \\(\\d{4}\\))?", $section.buildTextBlock()).stream() + RedactionSearchUtility.findBoundariesByRegex("OECD (No\\.? )?\\d{3}( \\(\\d{4}\\))?", $section.buildTextBlock()).stream() .map(boundary -> $section.buildTextBlock().subSequence(boundary).toString()) .map(value -> FileAttribute.builder().label("OECD Number").value(value).build()) .forEach(fileAttribute -> insert(fileAttribute)); 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 a6c6d03f..716d28a6 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 @@ -1,8 +1,8 @@ package drools import static java.lang.String.format; -import static com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RedactionSearchUtils.anyMatch; -import static com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RedactionSearchUtils.exactMatch; +import static com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RedactionSearchUtility.anyMatch; +import static com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RedactionSearchUtility.exactMatch; import static com.iqser.red.service.redaction.v1.server.layoutparsing.document.data.mapper.PropertiesMapper.parseImageType; import java.util.List; @@ -37,7 +37,7 @@ import com.iqser.red.service.redaction.v1.server.redaction.adapter.NerEntities; import java.util.stream.Collectors; import java.util.Collection; import java.util.stream.Stream; -import com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RedactionSearchUtils; +import com.iqser.red.service.redaction.v1.server.layoutparsing.document.utils.RedactionSearchUtility; global DocumentGraph document global EntityCreationService entityCreationService