RED-6093: Prototype document structure
*refactored Nodes *added some tests *wip
This commit is contained in:
parent
d0db04f3e9
commit
ba26cfb4ac
@ -1,97 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.document.graph;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.redaction.model.RedRectangle2D;
|
|
||||||
|
|
||||||
import lombok.AccessLevel;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.experimental.FieldDefaults;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@AllArgsConstructor
|
|
||||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
|
||||||
public class AtomicTextBlockEntity implements CharSequence {
|
|
||||||
|
|
||||||
Integer id;
|
|
||||||
//string coords
|
|
||||||
Integer offset;
|
|
||||||
String searchText;
|
|
||||||
List<Integer> lineBreaks;
|
|
||||||
|
|
||||||
//position coords
|
|
||||||
List<Integer> stringIdxToPositionIdx;
|
|
||||||
List<RedRectangle2D> positions;
|
|
||||||
|
|
||||||
Node parent;
|
|
||||||
|
|
||||||
|
|
||||||
public int indexOf(String searchTerm) {
|
|
||||||
|
|
||||||
int pos = searchText.indexOf(searchTerm);
|
|
||||||
return pos == -1 ? -1 : pos + offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public int numberOfLines() {
|
|
||||||
|
|
||||||
return lineBreaks.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public int getNextLinebreak(int startIndex) {
|
|
||||||
|
|
||||||
return lineBreaks.stream()//
|
|
||||||
.filter(linebreak -> linebreak > startIndex) //
|
|
||||||
.findFirst() //
|
|
||||||
.orElse(searchText.length()) + offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public RedRectangle2D getPosition(int stringIdx) {
|
|
||||||
|
|
||||||
return positions.get(stringIdxToPositionIdx.get(stringIdx - offset));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public List<RedRectangle2D> getPositions(int startStringIdx, int endStringIdx) {
|
|
||||||
|
|
||||||
return positions.subList(startStringIdx - offset, endStringIdx - offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int length() {
|
|
||||||
|
|
||||||
return searchText.length();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public char charAt(int index) {
|
|
||||||
|
|
||||||
return searchText.charAt(index - offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CharSequence subSequence(int start, int end) {
|
|
||||||
|
|
||||||
throw new UnsupportedOperationException("AtomicTextBlockEntity can not be sliced!");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public String getFirstLine() {
|
|
||||||
|
|
||||||
return searchText.substring(0, getNextLinebreak(0) - offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return searchText;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -0,0 +1,84 @@
|
|||||||
|
package com.iqser.red.service.redaction.v1.server.document.graph;
|
||||||
|
|
||||||
|
import static java.lang.String.format;
|
||||||
|
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
public class Boundary {
|
||||||
|
|
||||||
|
private int start;
|
||||||
|
private int end;
|
||||||
|
|
||||||
|
|
||||||
|
public Boundary(int start, int end) {
|
||||||
|
|
||||||
|
assert start <= end;
|
||||||
|
this.start = start;
|
||||||
|
this.end = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int length() {
|
||||||
|
return end - start;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public int start() {
|
||||||
|
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public int end() {
|
||||||
|
|
||||||
|
return end;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean contains(Boundary boundary) {
|
||||||
|
|
||||||
|
return start <= boundary.start() && boundary.end() <= end;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean containedBy(Boundary boundary) {
|
||||||
|
|
||||||
|
return boundary.start() <= start && end <= boundary.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean contains(int start, int end) {
|
||||||
|
|
||||||
|
if (start > end) {
|
||||||
|
throw new UnsupportedOperationException("start > end");
|
||||||
|
}
|
||||||
|
return this.start <= start && end <= this.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean containedBy(int start, int end) {
|
||||||
|
|
||||||
|
if (start > end) {
|
||||||
|
throw new UnsupportedOperationException("start > end");
|
||||||
|
}
|
||||||
|
return start <= this.start && this.end <= end;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean contains(int index) {
|
||||||
|
|
||||||
|
return start <= index && index < end;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean intersects(Boundary boundary) {
|
||||||
|
|
||||||
|
return contains(boundary.start()) || contains(boundary.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return format("Range [%d|%d)", start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,49 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.document.graph;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import lombok.AccessLevel;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.experimental.FieldDefaults;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@AllArgsConstructor
|
|
||||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
|
||||||
public class DocumentEntity implements Node {
|
|
||||||
|
|
||||||
List<SectionEntity> sections;
|
|
||||||
List<PageEntity> pages;
|
|
||||||
TableOfContents tableOfContents;
|
|
||||||
Integer numberOfPages;
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TextBlockEntity buildTextBlock() {
|
|
||||||
|
|
||||||
return streamAllNodes()
|
|
||||||
.filter(Node::hasTextBlock)
|
|
||||||
.map(Node::getTextBlock)
|
|
||||||
.collect(TextBlockCollector.collect());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private Stream<Node> streamAllNodes() {
|
|
||||||
|
|
||||||
return Stream.concat(Stream.concat(//
|
|
||||||
tableOfContents.streamEntriesInOrder().map(TableOfContents.Entry::node), //
|
|
||||||
pages.stream().map(PageEntity::getHeader)), //
|
|
||||||
pages.stream().map(PageEntity::getFooter)); //
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
|
|
||||||
return tableOfContents.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -0,0 +1,90 @@
|
|||||||
|
package com.iqser.red.service.redaction.v1.server.document.graph;
|
||||||
|
|
||||||
|
import static com.iqser.red.service.redaction.v1.server.document.services.EntityEnrichmentUtility.enrichEntity;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.DocumentNode;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.EntityNode;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.PageNode;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SectionNode;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.ConcatenatedTextBlock;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlockCollector;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.exception.NotFoundException;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.redaction.model.EntityType;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.experimental.FieldDefaults;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||||
|
public class DocumentGraph {
|
||||||
|
|
||||||
|
List<SectionNode> sections;
|
||||||
|
List<PageNode> pages;
|
||||||
|
TableOfContents tableOfContents;
|
||||||
|
Integer numberOfPages;
|
||||||
|
TextBlock text;
|
||||||
|
|
||||||
|
|
||||||
|
public ConcatenatedTextBlock buildTextBlock() {
|
||||||
|
|
||||||
|
return streamAllNodes().filter(DocumentNode::isTerminal).map(DocumentNode::getAtomicTextBlock).collect(new TextBlockCollector());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public EntityNode createAndAddEntity(Boundary boundary, String type, EntityType entityType) {
|
||||||
|
|
||||||
|
EntityNode entity = EntityNode.initialEntityNode(boundary, type, entityType);
|
||||||
|
addEntityToGraphAndSetFields(entity);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void addEntityToGraphAndSetFields(EntityNode entity) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
boolean inserted = streamAllNodes().anyMatch(node -> node.addEntityAndSetFieldsIfStartIndexContained(entity));
|
||||||
|
} catch (NotFoundException e) {
|
||||||
|
enrichEntity(entity, text);
|
||||||
|
log.warn("Entity \"{}\" with {} is in between two main sections and will be removed!", entity.getValue(), entity.getBoundary());
|
||||||
|
entity.removeFromGraph();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public Set<EntityNode> getEntities() {
|
||||||
|
|
||||||
|
return streamAllNodes().filter(DocumentNode::isTerminal).map(DocumentNode::getEntities).flatMap(List::stream).collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private Stream<DocumentNode> streamAllNodes() {
|
||||||
|
|
||||||
|
return Stream.concat(//
|
||||||
|
tableOfContents.streamEntriesInOrder().map(TableOfContents.Entry::node), //
|
||||||
|
Stream.concat(//
|
||||||
|
pages.stream().map(PageNode::getHeader), //
|
||||||
|
pages.stream().map(PageNode::getFooter))); //
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
|
||||||
|
return text.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,14 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.document.graph;
|
|
||||||
|
|
||||||
import java.awt.geom.Rectangle2D;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class Entity2 {
|
|
||||||
|
|
||||||
String uid;
|
|
||||||
String value;
|
|
||||||
List<Rectangle2D> positions;
|
|
||||||
Boolean redact;
|
|
||||||
|
|
||||||
List<Node> neighbours;
|
|
||||||
}
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.document.graph;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import lombok.AccessLevel;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.experimental.FieldDefaults;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@AllArgsConstructor
|
|
||||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
|
||||||
public class FooterEntity implements Node {
|
|
||||||
|
|
||||||
PageEntity page;
|
|
||||||
TextBlockEntity textBlock;
|
|
||||||
List<Entity2> entities;
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TextBlockEntity buildTextBlock() {
|
|
||||||
|
|
||||||
return getTextBlock();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasTextBlock() {
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TextBlockEntity getTextBlock() {
|
|
||||||
|
|
||||||
return textBlock;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return textBlock.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.document.graph;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import lombok.AccessLevel;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.experimental.FieldDefaults;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@AllArgsConstructor
|
|
||||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
|
||||||
public class HeaderEntity implements Node {
|
|
||||||
|
|
||||||
PageEntity page;
|
|
||||||
TextBlockEntity textBlock;
|
|
||||||
List<Entity2> entities;
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TextBlockEntity buildTextBlock() {
|
|
||||||
|
|
||||||
return getTextBlock();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasTextBlock() {
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TextBlockEntity getTextBlock() {
|
|
||||||
|
|
||||||
return textBlock;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return textBlock.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.document.graph;
|
|
||||||
|
|
||||||
public interface Node {
|
|
||||||
TextBlockEntity buildTextBlock();
|
|
||||||
|
|
||||||
|
|
||||||
default boolean hasTextBlock() {
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
default TextBlockEntity getTextBlock() {
|
|
||||||
|
|
||||||
throw new UnsupportedOperationException("Only terminal Nodes have direct access to TextBlocks");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.document.graph;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import lombok.AccessLevel;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.experimental.FieldDefaults;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@AllArgsConstructor
|
|
||||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
|
||||||
public class PageEntity implements Node {
|
|
||||||
|
|
||||||
Integer number;
|
|
||||||
Integer height;
|
|
||||||
Integer width;
|
|
||||||
List<SectionEntity> sections;
|
|
||||||
List<ParagraphEntity> paragraphs;
|
|
||||||
HeaderEntity header;
|
|
||||||
FooterEntity footer;
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TextBlockEntity buildTextBlock() {
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return number.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.document.graph;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import lombok.AccessLevel;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.experimental.FieldDefaults;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@AllArgsConstructor
|
|
||||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
|
||||||
public class ParagraphEntity implements Node {
|
|
||||||
|
|
||||||
String tocId;
|
|
||||||
Integer id;
|
|
||||||
Integer numberOnPage;
|
|
||||||
Integer numberInSection;
|
|
||||||
|
|
||||||
//foreign key
|
|
||||||
SectionEntity parentSection;
|
|
||||||
PageEntity page;
|
|
||||||
TextBlockEntity textBlock;
|
|
||||||
List<Entity2> entities;
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TextBlockEntity buildTextBlock() {
|
|
||||||
|
|
||||||
return getTextBlock();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasTextBlock() {
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TextBlockEntity getTextBlock() {
|
|
||||||
|
|
||||||
return textBlock;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return textBlock.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.document.graph;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import lombok.AccessLevel;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.experimental.FieldDefaults;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@AllArgsConstructor
|
|
||||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
|
||||||
public class SectionEntity implements Node {
|
|
||||||
|
|
||||||
Integer id;
|
|
||||||
TableOfContents tableOfContents;
|
|
||||||
String tocId;
|
|
||||||
Node parent;
|
|
||||||
//foreign key
|
|
||||||
TextBlockEntity headline;
|
|
||||||
List<SectionEntity> subSections;
|
|
||||||
List<ParagraphEntity> paragraphs;
|
|
||||||
List<Integer> pages;
|
|
||||||
List<Entity2> entities;
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
|
|
||||||
return tocId + ": " + headline.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TextBlockEntity buildTextBlock() {
|
|
||||||
|
|
||||||
return tableOfContents.streamSubEntriesInOrder(tocId).map(TableOfContents.Entry::node).map(Node::getTextBlock).collect(TextBlockCollector.collect());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasTextBlock() {
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TextBlockEntity getTextBlock() {
|
|
||||||
|
|
||||||
return headline;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.document.graph;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import lombok.AccessLevel;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.experimental.FieldDefaults;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@AllArgsConstructor
|
|
||||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
|
||||||
public class TableCellEntity implements Node {
|
|
||||||
|
|
||||||
//foreign key
|
|
||||||
Integer table;
|
|
||||||
TextBlockEntity textBlock;
|
|
||||||
List<Entity2> entities;
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TextBlockEntity buildTextBlock() {
|
|
||||||
|
|
||||||
return textBlock;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasTextBlock() {
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TextBlockEntity getTextBlock() {
|
|
||||||
|
|
||||||
return textBlock;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.document.graph;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import lombok.AccessLevel;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.experimental.FieldDefaults;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@AllArgsConstructor
|
|
||||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
|
||||||
public class TableEntity implements Node {
|
|
||||||
|
|
||||||
Integer id;
|
|
||||||
Integer numberOfRows;
|
|
||||||
Integer numberOfCols;
|
|
||||||
List<List<TableCellEntity>> tableCells;
|
|
||||||
|
|
||||||
// graph connection
|
|
||||||
SectionEntity section;
|
|
||||||
PageEntity page;
|
|
||||||
List<Entity2> entities;
|
|
||||||
|
|
||||||
|
|
||||||
private Stream<TableCellEntity> streamTableCells() {
|
|
||||||
|
|
||||||
return tableCells.stream().flatMap(List::stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private Stream<TableCellEntity> streamTableRow(int row) {
|
|
||||||
|
|
||||||
return tableCells.get(row).stream();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private Stream<TableCellEntity> streamTableCol(int col) {
|
|
||||||
|
|
||||||
return tableCells.stream().map(row -> row.get(col));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TextBlockEntity buildTextBlock() {
|
|
||||||
|
|
||||||
return streamTableCells().map(TableCellEntity::buildTextBlock).collect(TextBlockCollector.collect());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -2,6 +2,7 @@ package com.iqser.red.service.redaction.v1.server.document.graph;
|
|||||||
|
|
||||||
import static java.lang.String.format;
|
import static java.lang.String.format;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -9,6 +10,9 @@ import java.util.stream.Stream;
|
|||||||
|
|
||||||
import javax.management.openmbean.InvalidKeyException;
|
import javax.management.openmbean.InvalidKeyException;
|
||||||
|
|
||||||
|
import com.google.common.hash.Hashing;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.DocumentNode;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@ -28,7 +32,7 @@ public class TableOfContents {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public String createNewEntryAndReturnId(String keyword, String summary, Node node) {
|
public String createNewEntryAndReturnId(String keyword, String summary, DocumentNode node) {
|
||||||
|
|
||||||
String id = String.format("%d", entries.size());
|
String id = String.format("%d", entries.size());
|
||||||
entries.add(new Entry(keyword, id, summary, new LinkedList<>(), node));
|
entries.add(new Entry(keyword, id, summary, new LinkedList<>(), node));
|
||||||
@ -36,7 +40,7 @@ public class TableOfContents {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public String createNewChildEntryAndReturnId(String parentId, String keyword, String summary, Node node) {
|
public String createNewChildEntryAndReturnId(String parentId, String keyword, String summary, DocumentNode node) {
|
||||||
|
|
||||||
Entry parent = getEntryById(parentId);
|
Entry parent = getEntryById(parentId);
|
||||||
String childId = parentId + String.format(".%d", parent.children().size());
|
String childId = parentId + String.format(".%d", parent.children().size());
|
||||||
@ -96,7 +100,7 @@ public class TableOfContents {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public record Entry(String type, String id, String summary, List<Entry> children, Node node) {
|
public record Entry(String type, String id, String summary, List<Entry> children, DocumentNode node) {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
@ -104,6 +108,9 @@ public class TableOfContents {
|
|||||||
return id + ": " + type + ".: " + summary;
|
return id + ": " + type + ".: " + summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Hashing.murmur3_32_fixed().hashString(type + id + summary + children.hashCode(), StandardCharsets.UTF_8).hashCode();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,53 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.document.graph;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.BiConsumer;
|
|
||||||
import java.util.function.BinaryOperator;
|
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
import java.util.stream.Collector;
|
|
||||||
|
|
||||||
public class TextBlockCollector implements Collector<TextBlockEntity, TextBlockEntity, TextBlockEntity> {
|
|
||||||
|
|
||||||
public static Collector<TextBlockEntity, TextBlockEntity, TextBlockEntity> collect() {
|
|
||||||
|
|
||||||
return new TextBlockCollector();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Supplier<TextBlockEntity> supplier() {
|
|
||||||
|
|
||||||
return new TextBlockEntity(Collections.emptyList());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public BiConsumer<TextBlockEntity, TextBlockEntity> accumulator() {
|
|
||||||
|
|
||||||
return TextBlockEntity::concat;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public BinaryOperator<TextBlockEntity> combiner() {
|
|
||||||
|
|
||||||
return TextBlockEntity::concat;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Function<TextBlockEntity, TextBlockEntity> finisher() {
|
|
||||||
|
|
||||||
return Function.identity();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Set<Characteristics> characteristics() {
|
|
||||||
|
|
||||||
return Set.of(Characteristics.IDENTITY_FINISH, Characteristics.CONCURRENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,166 +0,0 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.document.graph;
|
|
||||||
|
|
||||||
import java.awt.geom.Rectangle2D;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.redaction.model.RedRectangle2D;
|
|
||||||
|
|
||||||
import lombok.AccessLevel;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.experimental.FieldDefaults;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
|
||||||
public class TextBlockEntity implements CharSequence, Supplier<TextBlockEntity> {
|
|
||||||
|
|
||||||
List<AtomicTextBlockEntity> atomicTextBlocks;
|
|
||||||
StringBuilder searchTextBuilder;
|
|
||||||
|
|
||||||
//string coords
|
|
||||||
Integer offset;
|
|
||||||
|
|
||||||
|
|
||||||
public TextBlockEntity(List<AtomicTextBlockEntity> atomicTextBlocks) {
|
|
||||||
|
|
||||||
this.atomicTextBlocks = new LinkedList<>();
|
|
||||||
this.searchTextBuilder = new StringBuilder();
|
|
||||||
if (atomicTextBlocks.isEmpty()) {
|
|
||||||
offset = -1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var firstTextBlock = atomicTextBlocks.get(0);
|
|
||||||
this.atomicTextBlocks.add(firstTextBlock);
|
|
||||||
this.offset = firstTextBlock.getOffset();
|
|
||||||
this.searchTextBuilder.append(firstTextBlock.getSearchText());
|
|
||||||
|
|
||||||
atomicTextBlocks.subList(1, atomicTextBlocks.size()).forEach(this::concat);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public TextBlockEntity concat(TextBlockEntity textBlock) {
|
|
||||||
|
|
||||||
if (this.atomicTextBlocks.isEmpty() && this.offset == -1) {
|
|
||||||
this.offset = textBlock.getOffset();
|
|
||||||
} else if (textBlock.getOffset() != offset + length()) {
|
|
||||||
throw new UnsupportedOperationException("Can only concat consecutive TextBlocks");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.searchTextBuilder.append(textBlock.getSearchTextBuilder());
|
|
||||||
this.atomicTextBlocks.addAll(textBlock.getAtomicTextBlocks());
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void concat(AtomicTextBlockEntity atomicTextBlock) {
|
|
||||||
|
|
||||||
if (this.atomicTextBlocks.isEmpty() && this.offset == -1) {
|
|
||||||
this.offset = atomicTextBlock.getOffset();
|
|
||||||
} else if (atomicTextBlock.getOffset() != offset + length()) {
|
|
||||||
throw new UnsupportedOperationException("Can only concat consecutive TextBlocks");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.searchTextBuilder.append(atomicTextBlock.getSearchText());
|
|
||||||
this.atomicTextBlocks.add(atomicTextBlock);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public int indexOf(String searchTerm) {
|
|
||||||
|
|
||||||
int pos = this.searchTextBuilder.indexOf(searchTerm);
|
|
||||||
return pos == -1 ? -1 : pos + offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public int numberOfLines() {
|
|
||||||
|
|
||||||
return atomicTextBlocks.stream().map(AtomicTextBlockEntity::getLineBreaks).mapToInt(List::size).sum();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public int getNextLinebreak(int startIndex) {
|
|
||||||
|
|
||||||
return getAtomicTextBlockByStringIndex(startIndex).getNextLinebreak(startIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public RedRectangle2D getPositions(int stringIdx) {
|
|
||||||
|
|
||||||
return getAtomicTextBlockByStringIndex(stringIdx).getPosition(stringIdx);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public List<RedRectangle2D> getPositions(int startStringIdx, int endStringIdx) {
|
|
||||||
|
|
||||||
List<AtomicTextBlockEntity> textBlocks = getAllAtomicTextBlocksPartiallyInStringIdxRange(startStringIdx, endStringIdx);
|
|
||||||
|
|
||||||
if (textBlocks.size() == 1) {
|
|
||||||
return textBlocks.get(0).getPositions(startStringIdx, endStringIdx);
|
|
||||||
}
|
|
||||||
|
|
||||||
var firstTextBlock = textBlocks.get(0);
|
|
||||||
List<RedRectangle2D> positions = new LinkedList<>(firstTextBlock.getPositions(startStringIdx, firstTextBlock.getOffset() + firstTextBlock.length()));
|
|
||||||
|
|
||||||
for (var textBlock : textBlocks.subList(1, textBlocks.size() - 1)) {
|
|
||||||
positions.addAll(textBlock.getPositions());
|
|
||||||
}
|
|
||||||
|
|
||||||
var lastTextBlock = textBlocks.get(textBlocks.size() - 1);
|
|
||||||
positions.addAll(lastTextBlock.getPositions(lastTextBlock.getOffset(), endStringIdx));
|
|
||||||
|
|
||||||
return positions;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private AtomicTextBlockEntity getAtomicTextBlockByStringIndex(int stringIdx) {
|
|
||||||
|
|
||||||
return atomicTextBlocks.stream().filter(textBlock -> (textBlock.getOffset() + textBlock.length()) > stringIdx).findFirst().orElseThrow(IndexOutOfBoundsException::new);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private List<AtomicTextBlockEntity> getAllAtomicTextBlocksPartiallyInStringIdxRange(int startStringIdx, int endStringIdx) {
|
|
||||||
|
|
||||||
if (offset > endStringIdx || offset + length() <= startStringIdx) {
|
|
||||||
throw new IndexOutOfBoundsException();
|
|
||||||
}
|
|
||||||
|
|
||||||
return atomicTextBlocks.stream().filter(tb -> tb.getOffset() <= endStringIdx && tb.getOffset() + tb.length() > startStringIdx).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public String getSearchText() {
|
|
||||||
|
|
||||||
return searchTextBuilder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int length() {
|
|
||||||
|
|
||||||
return this.searchTextBuilder.length();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public char charAt(int index) {
|
|
||||||
|
|
||||||
return searchTextBuilder.charAt(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CharSequence subSequence(int start, int end) {
|
|
||||||
|
|
||||||
throw new UnsupportedOperationException("AtomicTextBlockEntity can not be sliced!");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TextBlockEntity get() {
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -0,0 +1,202 @@
|
|||||||
|
package com.iqser.red.service.redaction.v1.server.document.graph.nodes;
|
||||||
|
|
||||||
|
import static com.iqser.red.service.redaction.v1.server.document.services.EntityEnrichmentUtility.enrichEntity;
|
||||||
|
import static java.lang.String.format;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.Boundary;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.exception.NotFoundException;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.redaction.model.EntityType;
|
||||||
|
|
||||||
|
public interface DocumentNode {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches all Nodes located underneath this Node in the TableOfContents and concatenates their AtomicTextBlocks into a single TextBlockEntity.
|
||||||
|
* So, for a Section all AtomicTextBlocks of Subsections, Paragraphs, and Tables are concatenated into a single TextBlockEntity
|
||||||
|
*
|
||||||
|
* @return TextBlock containing all AtomicTextBlocks that are located under this Node.
|
||||||
|
*/
|
||||||
|
TextBlock buildTextBlock();
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any Node maintains its own List of Entities.
|
||||||
|
* This List contains all Entities, whose first index is located in any of the AtomicTextBlocks underneath this Node.
|
||||||
|
*
|
||||||
|
* @return List of all Entities associated with this Node
|
||||||
|
*/
|
||||||
|
List<EntityNode> getEntities();
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the PageNode associated with this Node.
|
||||||
|
* If the node has more than one PageNode associated, it returns the PageNode with the lowest number.
|
||||||
|
* For example a section might span multiple pages, it then returns the page where the section starts.
|
||||||
|
*
|
||||||
|
* @return PageNode representing the first page on which the Node is located in the document
|
||||||
|
*/
|
||||||
|
PageNode getPage();
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any Node except the First level of Sections, Header, Footer, and Pages have a direct Parent.
|
||||||
|
* For example a Paragraph has a parent Section, a Table Cell has a parent Table, etc...
|
||||||
|
* hasParent() may be used to check whether a parent is present.
|
||||||
|
*
|
||||||
|
* @return Node that represents the Parent or null, if no parent is present.
|
||||||
|
*/
|
||||||
|
DocumentNode getParent();
|
||||||
|
|
||||||
|
|
||||||
|
Stream<DocumentNode> streamAllSubNodes();
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* By default, a parent is always present, this needs to be overwritten for Headers, Footers, Pages, and Sections.
|
||||||
|
*
|
||||||
|
* @return boolean, indicating whether a parent is present.
|
||||||
|
*/
|
||||||
|
default boolean hasParent() {
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* by default a Node does not have direct access to an AtomicTextBlock
|
||||||
|
*
|
||||||
|
* @return boolean, indicating if a Node has direct access to an AtomicTextBlock
|
||||||
|
*/
|
||||||
|
default boolean isTerminal() {
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* by default a Node does not have direct access to an AtomicTextBlock, this method throws a UnsupportedOperationException if not overridden.
|
||||||
|
*
|
||||||
|
* @return AtomicTextBlock
|
||||||
|
*/
|
||||||
|
default AtomicTextBlock getAtomicTextBlock() {
|
||||||
|
|
||||||
|
throw new UnsupportedOperationException("Only terminal Nodes have access to AtomicTextBlocks!");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* creates an EntityNode with only initial values set, inserts it into the subgraph contained by this node and sets the inferrable fields.
|
||||||
|
* Throws NotFoundException and removes the entity if the provided boundary could not be found in the subgraph.
|
||||||
|
*
|
||||||
|
* @param boundary start and end indices in String coordinates of the entity to be created
|
||||||
|
* @param type type of the entity to be created
|
||||||
|
* @param entityType entityType of the entity to be created
|
||||||
|
* @return the newly created and inserted EntityNode with all fields set.
|
||||||
|
*/
|
||||||
|
default EntityNode createAndAddEntity(Boundary boundary, String type, EntityType entityType) {
|
||||||
|
|
||||||
|
EntityNode entity = EntityNode.initialEntityNode(boundary, type, entityType);
|
||||||
|
addEntityToNodeAndSetFields(entity);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* searches for the first terminal Node containing the start index of the entity to be inserted.
|
||||||
|
* Catches NotFoundException to remove the EntityNode from the graph, then rethrows it
|
||||||
|
*
|
||||||
|
* @param entity newly created EntityNode with only initial values set
|
||||||
|
*/
|
||||||
|
default void addEntityToNodeAndSetFields(EntityNode entity) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
streamAllSubNodes().anyMatch(node -> node.addEntityAndSetFieldsIfStartIndexContained(entity));
|
||||||
|
} catch (NotFoundException e) {
|
||||||
|
entity.removeFromGraph();
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this Node's AtomicTextBlock contains the start index of the entity, the entity's position is read from the AtomicTextBlock.
|
||||||
|
* If the position can not be fully read from the AtomicTextBlock, it recursively looks in the TextBlocks of the parents until all positions are found.
|
||||||
|
* Further, the function throws NotFoundException if no parent contains all positions.
|
||||||
|
* This occurs, when the Entity is in between Nodes that do not share a parent, e.g. main sections.
|
||||||
|
* Finally, the function adds the Entity to its own list of Entities and to every parents' list recursively.
|
||||||
|
*
|
||||||
|
* @param entity The entity to be added to the graph
|
||||||
|
* @return true, if the entity has been added successfully.
|
||||||
|
* false, if the entity's start index is not contained or the Node doesn't have an AtomicTextBlock
|
||||||
|
*/
|
||||||
|
default boolean addEntityAndSetFieldsIfStartIndexContained(EntityNode entity) {
|
||||||
|
|
||||||
|
if (!isTerminal()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AtomicTextBlock atomicTextBlock = getAtomicTextBlock();
|
||||||
|
if (atomicTextBlock.containsIndex(entity.getBoundary().start())) {
|
||||||
|
|
||||||
|
entity.addContainingNode(this);
|
||||||
|
|
||||||
|
getEntities().add(entity);
|
||||||
|
|
||||||
|
addEntityToPage(entity);
|
||||||
|
addEntityToParents(entity);
|
||||||
|
setFields(entity, atomicTextBlock);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void addEntityToPage(EntityNode entity) {
|
||||||
|
|
||||||
|
getPage().getEntities().add(entity);
|
||||||
|
entity.setPage(getPage());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void setFields(EntityNode entity, AtomicTextBlock atomicTextBlock) {
|
||||||
|
|
||||||
|
if (atomicTextBlock.containsRange(entity.getBoundary())) {
|
||||||
|
enrichEntity(entity, atomicTextBlock);
|
||||||
|
} else {
|
||||||
|
this.setFieldsFromParents(this, entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void addEntityToParents(EntityNode entity) {
|
||||||
|
|
||||||
|
DocumentNode node = this;
|
||||||
|
while (node.hasParent()) {
|
||||||
|
node = node.getParent();
|
||||||
|
node.getEntities().add(entity);
|
||||||
|
entity.addContainingNode(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void setFieldsFromParents(DocumentNode node, EntityNode entity) {
|
||||||
|
|
||||||
|
if (node.hasParent()) {
|
||||||
|
DocumentNode parent = node.getParent();
|
||||||
|
TextBlock textBlock = parent.buildTextBlock();
|
||||||
|
if (textBlock.containsRange(entity.getBoundary())) {
|
||||||
|
enrichEntity(entity, textBlock);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
setFieldsFromParents(parent, entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new NotFoundException(format("Position could not be found for Entity %s", entity.toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,84 @@
|
|||||||
|
package com.iqser.red.service.redaction.v1.server.document.graph.nodes;
|
||||||
|
|
||||||
|
import java.awt.geom.Rectangle2D;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import com.google.common.hash.Hashing;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.Boundary;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.redaction.model.EntityType;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.experimental.FieldDefaults;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||||
|
public class EntityNode {
|
||||||
|
|
||||||
|
public static EntityNode initialEntityNode(Boundary boundary, String type, EntityType entityType) {
|
||||||
|
|
||||||
|
return EntityNode.builder().type(type).entityType(entityType).boundary(boundary).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// initial values
|
||||||
|
Boundary boundary;
|
||||||
|
String type;
|
||||||
|
EntityType entityType;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
boolean redact = false;
|
||||||
|
@Builder.Default
|
||||||
|
boolean recommend = false;
|
||||||
|
@Builder.Default
|
||||||
|
boolean removed = false;
|
||||||
|
|
||||||
|
// inferrable from graph
|
||||||
|
int uniqueId;
|
||||||
|
String value;
|
||||||
|
CharSequence textBefore;
|
||||||
|
CharSequence textAfter;
|
||||||
|
PageNode page;
|
||||||
|
List<Rectangle2D> positions;
|
||||||
|
@Builder.Default
|
||||||
|
Set<DocumentNode> containingNodes = new HashSet<>();
|
||||||
|
|
||||||
|
|
||||||
|
public void addContainingNode(DocumentNode containingNode) {
|
||||||
|
|
||||||
|
containingNodes.add(containingNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void removeFromGraph() {
|
||||||
|
|
||||||
|
getContainingNodes().forEach(node -> node.getEntities().remove(this));
|
||||||
|
getPage().getEntities().remove(this);
|
||||||
|
setRemoved(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.append(value);
|
||||||
|
sb.append(boundary.start());
|
||||||
|
sb.append(page.getNumber());
|
||||||
|
positions.forEach(r -> {
|
||||||
|
sb.append(r.getMinX());
|
||||||
|
sb.append(r.getMinY());
|
||||||
|
sb.append(r.getWidth());
|
||||||
|
sb.append(r.getHeight());
|
||||||
|
});
|
||||||
|
return Hashing.murmur3_128().hashString(sb.toString(), StandardCharsets.UTF_8).hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
package com.iqser.red.service.redaction.v1.server.document.graph.nodes;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.experimental.FieldDefaults;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||||
|
public class FooterNode implements DocumentNode {
|
||||||
|
|
||||||
|
@EqualsAndHashCode.Exclude
|
||||||
|
PageNode page;
|
||||||
|
AtomicTextBlock atomicTextBlock;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
@EqualsAndHashCode.Exclude
|
||||||
|
List<EntityNode> entities = new LinkedList<>();
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AtomicTextBlock buildTextBlock() {
|
||||||
|
|
||||||
|
return atomicTextBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DocumentNode getParent() {
|
||||||
|
|
||||||
|
throw new UnsupportedOperationException("Footer has no Parent, use getPage() instead");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Stream<DocumentNode> streamAllSubNodes() {
|
||||||
|
|
||||||
|
return Stream.of(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasParent() {
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isTerminal() {
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
|
||||||
|
return atomicTextBlock.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
package com.iqser.red.service.redaction.v1.server.document.graph.nodes;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.experimental.FieldDefaults;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||||
|
public class HeaderNode implements DocumentNode {
|
||||||
|
|
||||||
|
@EqualsAndHashCode.Exclude
|
||||||
|
PageNode page;
|
||||||
|
AtomicTextBlock atomicTextBlock;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
@EqualsAndHashCode.Exclude
|
||||||
|
List<EntityNode> entities = new LinkedList<>();
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AtomicTextBlock buildTextBlock() {
|
||||||
|
|
||||||
|
return atomicTextBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DocumentNode getParent() {
|
||||||
|
|
||||||
|
throw new UnsupportedOperationException("Header has no Parent, use getPage() instead");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Stream<DocumentNode> streamAllSubNodes() {
|
||||||
|
|
||||||
|
return Stream.of(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasParent() {
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isTerminal() {
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
|
||||||
|
return atomicTextBlock.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
package com.iqser.red.service.redaction.v1.server.document.graph.nodes;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlockCollector;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.ConcatenatedTextBlock;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.experimental.FieldDefaults;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||||
|
public class PageNode implements DocumentNode {
|
||||||
|
|
||||||
|
Integer number;
|
||||||
|
Integer height;
|
||||||
|
Integer width;
|
||||||
|
List<DocumentNode> mainBody;
|
||||||
|
HeaderNode header;
|
||||||
|
FooterNode footer;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
@EqualsAndHashCode.Exclude
|
||||||
|
List<EntityNode> entities = new LinkedList<>();
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ConcatenatedTextBlock buildTextBlock() {
|
||||||
|
|
||||||
|
return mainBody.stream().filter(DocumentNode::isTerminal).map(DocumentNode::getAtomicTextBlock).collect(new TextBlockCollector());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DocumentNode getParent() {
|
||||||
|
|
||||||
|
throw new UnsupportedOperationException("A Page has no parent!");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Stream<DocumentNode> streamAllSubNodes() {
|
||||||
|
|
||||||
|
return Stream.concat(//
|
||||||
|
mainBody.stream(), //
|
||||||
|
Stream.concat(//
|
||||||
|
Stream.of(header), //
|
||||||
|
Stream.of(footer))); //
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageNode getPage() {
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasParent() {
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
|
||||||
|
return number.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
package com.iqser.red.service.redaction.v1.server.document.graph.nodes;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.experimental.FieldDefaults;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||||
|
public class ParagraphNode implements DocumentNode {
|
||||||
|
|
||||||
|
String tocId;
|
||||||
|
Integer id;
|
||||||
|
Integer numberOnPage;
|
||||||
|
Integer numberInSection;
|
||||||
|
|
||||||
|
@EqualsAndHashCode.Exclude
|
||||||
|
SectionNode parentSection;
|
||||||
|
@EqualsAndHashCode.Exclude
|
||||||
|
PageNode page;
|
||||||
|
AtomicTextBlock atomicTextBlock;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
@EqualsAndHashCode.Exclude
|
||||||
|
List<EntityNode> entities = new LinkedList<>();
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AtomicTextBlock buildTextBlock() {
|
||||||
|
|
||||||
|
return atomicTextBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DocumentNode getParent() {
|
||||||
|
|
||||||
|
return parentSection;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isTerminal() {
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
|
||||||
|
return tocId + ": " + atomicTextBlock.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Stream<DocumentNode> streamAllSubNodes() {
|
||||||
|
|
||||||
|
return Stream.of(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,107 @@
|
|||||||
|
package com.iqser.red.service.redaction.v1.server.document.graph.nodes;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.TableOfContents;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.ConcatenatedTextBlock;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlockCollector;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.experimental.FieldDefaults;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||||
|
public class SectionNode implements DocumentNode {
|
||||||
|
|
||||||
|
String tocId;
|
||||||
|
Integer id;
|
||||||
|
@EqualsAndHashCode.Exclude
|
||||||
|
TableOfContents tableOfContents;
|
||||||
|
@EqualsAndHashCode.Exclude
|
||||||
|
DocumentNode parentSection;
|
||||||
|
|
||||||
|
|
||||||
|
AtomicTextBlock headline;
|
||||||
|
|
||||||
|
List<SectionNode> subSections;
|
||||||
|
List<ParagraphNode> paragraphs;
|
||||||
|
List<TableNode> tables;
|
||||||
|
@EqualsAndHashCode.Exclude
|
||||||
|
List<PageNode> pages;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
@EqualsAndHashCode.Exclude
|
||||||
|
List<EntityNode> entities = new LinkedList<>();
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
|
||||||
|
return tocId + ": " + headline.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ConcatenatedTextBlock buildTextBlock() {
|
||||||
|
|
||||||
|
return streamAllSubNodes().map(DocumentNode::getAtomicTextBlock).collect(new TextBlockCollector());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DocumentNode getParent() {
|
||||||
|
|
||||||
|
if (hasParent()) {
|
||||||
|
return parentSection;
|
||||||
|
} else {
|
||||||
|
throw new UnsupportedOperationException("This section has no parent Section!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Stream<DocumentNode> streamAllSubNodes() {
|
||||||
|
|
||||||
|
return tableOfContents.streamSubEntriesInOrder(tocId).map(TableOfContents.Entry::node);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasParent() {
|
||||||
|
|
||||||
|
return parentSection != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isTerminal() {
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AtomicTextBlock getAtomicTextBlock() {
|
||||||
|
|
||||||
|
return headline;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageNode getPage() {
|
||||||
|
|
||||||
|
return pages.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
package com.iqser.red.service.redaction.v1.server.document.graph.nodes;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.experimental.FieldDefaults;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||||
|
public class TableCellNode implements DocumentNode {
|
||||||
|
|
||||||
|
@EqualsAndHashCode.Exclude
|
||||||
|
TableNode parentTable;
|
||||||
|
AtomicTextBlock atomicTextBlock;
|
||||||
|
PageNode page;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
@EqualsAndHashCode.Exclude
|
||||||
|
List<EntityNode> entities = new LinkedList<>();
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AtomicTextBlock buildTextBlock() {
|
||||||
|
|
||||||
|
return atomicTextBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DocumentNode getParent() {
|
||||||
|
|
||||||
|
return parentTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isTerminal() {
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Stream<DocumentNode> streamAllSubNodes() {
|
||||||
|
|
||||||
|
return Stream.of(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
package com.iqser.red.service.redaction.v1.server.document.graph.nodes;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.TableOfContents;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlockCollector;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.ConcatenatedTextBlock;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.experimental.FieldDefaults;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||||
|
public class TableNode implements DocumentNode {
|
||||||
|
|
||||||
|
Integer id;
|
||||||
|
String tocId;
|
||||||
|
Integer numberOfRows;
|
||||||
|
Integer numberOfCols;
|
||||||
|
List<TableCellNode> tableHeaders;
|
||||||
|
List<List<TableCellNode>> tableCells;
|
||||||
|
TableOfContents tableOfContents;
|
||||||
|
|
||||||
|
@EqualsAndHashCode.Exclude
|
||||||
|
SectionNode parentSection;
|
||||||
|
@EqualsAndHashCode.Exclude
|
||||||
|
List<PageNode> pages;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
@EqualsAndHashCode.Exclude
|
||||||
|
List<EntityNode> entities = new LinkedList<>();
|
||||||
|
|
||||||
|
|
||||||
|
private Stream<TableCellNode> streamTableCells() {
|
||||||
|
|
||||||
|
return tableCells.stream().flatMap(List::stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private Stream<TableCellNode> streamTableRow(int row) {
|
||||||
|
|
||||||
|
return tableCells.get(row).stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private Stream<TableCellNode> streamTableCol(int col) {
|
||||||
|
|
||||||
|
return tableCells.stream().map(row -> row.get(col));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ConcatenatedTextBlock buildTextBlock() {
|
||||||
|
|
||||||
|
return streamTableCells().map(TableCellNode::getAtomicTextBlock).collect(new TextBlockCollector());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Stream<DocumentNode> streamAllSubNodes() {
|
||||||
|
|
||||||
|
return streamTableCells().map(Function.identity());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DocumentNode getParent() {
|
||||||
|
|
||||||
|
return parentSection;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageNode getPage() {
|
||||||
|
|
||||||
|
return pages.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,134 @@
|
|||||||
|
package com.iqser.red.service.redaction.v1.server.document.graph.textblock;
|
||||||
|
|
||||||
|
import static java.lang.String.format;
|
||||||
|
|
||||||
|
import java.awt.geom.Rectangle2D;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.Boundary;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.DocumentNode;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.experimental.FieldDefaults;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||||
|
public class AtomicTextBlock implements TextBlock {
|
||||||
|
|
||||||
|
Integer id;
|
||||||
|
|
||||||
|
//string coordinates
|
||||||
|
Boundary boundary;
|
||||||
|
String searchText;
|
||||||
|
List<Integer> lineBreaks;
|
||||||
|
|
||||||
|
//position coordinates
|
||||||
|
List<Integer> stringIdxToPositionIdx;
|
||||||
|
List<Rectangle2D> positions;
|
||||||
|
|
||||||
|
@EqualsAndHashCode.Exclude
|
||||||
|
DocumentNode parent;
|
||||||
|
|
||||||
|
|
||||||
|
public int indexOf(String searchTerm) {
|
||||||
|
|
||||||
|
int pos = searchText.indexOf(searchTerm);
|
||||||
|
return pos == -1 ? -1 : pos + boundary.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public int numberOfLines() {
|
||||||
|
|
||||||
|
return lineBreaks.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AtomicTextBlock> getAtomicTextBlocks() {
|
||||||
|
|
||||||
|
return List.of(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public int getNextLinebreak(int fromIndex) {
|
||||||
|
|
||||||
|
return lineBreaks.stream()//
|
||||||
|
.filter(linebreak -> linebreak > fromIndex) //
|
||||||
|
.findFirst() //
|
||||||
|
.orElse(searchText.length()) + boundary.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public int getPreviousLinebreak(int fromIndex) {
|
||||||
|
|
||||||
|
return lineBreaks.stream()//
|
||||||
|
.filter(linebreak -> linebreak <= fromIndex)//
|
||||||
|
.reduce((a, b) -> b)//
|
||||||
|
.orElse(0) + boundary.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Rectangle2D getPosition(int stringIdx) {
|
||||||
|
|
||||||
|
return positions.get(stringIdxToPositionIdx.get(stringIdx - boundary.start()));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public List<Rectangle2D> getPositions(Boundary range) {
|
||||||
|
|
||||||
|
if (!containsRange(range)) {
|
||||||
|
throw new IndexOutOfBoundsException(format("String index Range [%d|%d) is out of boundary for String index [%d|%d)",
|
||||||
|
range.start(),
|
||||||
|
range.end(),
|
||||||
|
range.start(),
|
||||||
|
range.end()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (range.end() == this.boundary.end()) {
|
||||||
|
return positions.subList(stringIdxToPositionIdx.get(range.start() - this.boundary.start()), positions.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
return positions.subList(stringIdxToPositionIdx.get(range.start() - this.boundary.start()), stringIdxToPositionIdx.get(range.end() - this.boundary.start()));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int length() {
|
||||||
|
|
||||||
|
return searchText.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public char charAt(int index) {
|
||||||
|
|
||||||
|
return searchText.charAt(index - boundary.start());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CharSequence subSequence(int start, int end) {
|
||||||
|
|
||||||
|
return searchText.substring(start - boundary.start(), end - boundary.start());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public String getFirstLine() {
|
||||||
|
|
||||||
|
return searchText.substring(0, getNextLinebreak(0) - boundary.start());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
|
||||||
|
return searchText;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,155 @@
|
|||||||
|
package com.iqser.red.service.redaction.v1.server.document.graph.textblock;
|
||||||
|
|
||||||
|
import static java.lang.String.format;
|
||||||
|
|
||||||
|
import java.awt.geom.Rectangle2D;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.Boundary;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.experimental.FieldDefaults;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||||
|
public class ConcatenatedTextBlock implements TextBlock, Supplier<ConcatenatedTextBlock> {
|
||||||
|
|
||||||
|
List<AtomicTextBlock> atomicTextBlocks;
|
||||||
|
StringBuilder searchText;
|
||||||
|
Boundary boundary;
|
||||||
|
|
||||||
|
|
||||||
|
public ConcatenatedTextBlock(List<AtomicTextBlock> atomicTextBlocks) {
|
||||||
|
|
||||||
|
this.atomicTextBlocks = new LinkedList<>();
|
||||||
|
this.searchText = new StringBuilder();
|
||||||
|
if (atomicTextBlocks.isEmpty()) {
|
||||||
|
boundary = new Boundary(-1, -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var firstTextBlock = atomicTextBlocks.get(0);
|
||||||
|
this.atomicTextBlocks.add(firstTextBlock);
|
||||||
|
this.searchText.append(firstTextBlock.getSearchText());
|
||||||
|
boundary = new Boundary(firstTextBlock.getBoundary().start(), firstTextBlock.getBoundary().end());
|
||||||
|
|
||||||
|
atomicTextBlocks.subList(1, atomicTextBlocks.size()).forEach(this::concat);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public ConcatenatedTextBlock(AtomicTextBlock atomicTextBlocks) {
|
||||||
|
|
||||||
|
new ConcatenatedTextBlock(List.of(atomicTextBlocks));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public ConcatenatedTextBlock concat(TextBlock textBlock) {
|
||||||
|
|
||||||
|
if (this.atomicTextBlocks.isEmpty()) {
|
||||||
|
boundary.setStart(textBlock.getBoundary().start());
|
||||||
|
boundary.setEnd(textBlock.getBoundary().end());
|
||||||
|
} else if (boundary.end() != textBlock.getBoundary().start()) {
|
||||||
|
throw new UnsupportedOperationException(format("Can only concat consecutive TextBlocks, trying to concat %s and %s", textBlock.getBoundary(), boundary));
|
||||||
|
}
|
||||||
|
this.searchText.append(textBlock.getSearchText());
|
||||||
|
this.atomicTextBlocks.addAll(textBlock.getAtomicTextBlocks());
|
||||||
|
boundary.setEnd(textBlock.getBoundary().end());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public int indexOf(String searchTerm) {
|
||||||
|
|
||||||
|
int pos = this.searchText.indexOf(searchTerm);
|
||||||
|
return pos == -1 ? -1 : pos + boundary.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public int numberOfLines() {
|
||||||
|
|
||||||
|
return atomicTextBlocks.stream().map(AtomicTextBlock::getLineBreaks).mapToInt(List::size).sum();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public int getNextLinebreak(int fromIndex) {
|
||||||
|
|
||||||
|
return getAtomicTextBlockByStringIndex(fromIndex).getNextLinebreak(fromIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public int getPreviousLinebreak(int fromIndex) {
|
||||||
|
|
||||||
|
return getAtomicTextBlockByStringIndex(fromIndex).getPreviousLinebreak(fromIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Rectangle2D getPosition(int stringIdx) {
|
||||||
|
|
||||||
|
return getAtomicTextBlockByStringIndex(stringIdx).getPosition(stringIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public List<Rectangle2D> getPositions(Boundary boundary) {
|
||||||
|
|
||||||
|
List<AtomicTextBlock> textBlocks = getAllAtomicTextBlocksPartiallyInStringIdxRange(boundary);
|
||||||
|
|
||||||
|
if (textBlocks.size() == 1) {
|
||||||
|
return textBlocks.get(0).getPositions(boundary);
|
||||||
|
}
|
||||||
|
|
||||||
|
AtomicTextBlock firstTextBlock = textBlocks.get(0);
|
||||||
|
List<Rectangle2D> positions = new LinkedList<>(firstTextBlock.getPositions(new Boundary(boundary.start(), firstTextBlock.getBoundary().end())));
|
||||||
|
|
||||||
|
for (AtomicTextBlock textBlock : textBlocks.subList(1, textBlocks.size() - 1)) {
|
||||||
|
positions.addAll(textBlock.getPositions());
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastTextBlock = textBlocks.get(textBlocks.size() - 1);
|
||||||
|
positions.addAll(lastTextBlock.getPositions(new Boundary(lastTextBlock.getBoundary().start(), boundary.end())));
|
||||||
|
|
||||||
|
return positions;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private AtomicTextBlock getAtomicTextBlockByStringIndex(int stringIdx) {
|
||||||
|
|
||||||
|
return atomicTextBlocks.stream().filter(textBlock -> (textBlock.getBoundary().end()) > stringIdx).findFirst().orElseThrow(IndexOutOfBoundsException::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private List<AtomicTextBlock> getAllAtomicTextBlocksPartiallyInStringIdxRange(Boundary boundary) {
|
||||||
|
|
||||||
|
return atomicTextBlocks.stream().filter(tb -> tb.getBoundary().intersects(boundary)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int length() {
|
||||||
|
|
||||||
|
return this.searchText.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public char charAt(int index) {
|
||||||
|
|
||||||
|
return searchText.charAt(index - boundary.start());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CharSequence subSequence(int start, int end) {
|
||||||
|
|
||||||
|
return searchText.subSequence(start - boundary.start(), end - boundary.start());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ConcatenatedTextBlock get() {
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
package com.iqser.red.service.redaction.v1.server.document.graph.textblock;
|
||||||
|
|
||||||
|
import static java.lang.String.format;
|
||||||
|
|
||||||
|
import java.awt.geom.Rectangle2D;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.Boundary;
|
||||||
|
|
||||||
|
public interface TextBlock extends CharSequence {
|
||||||
|
|
||||||
|
CharSequence getSearchText();
|
||||||
|
|
||||||
|
|
||||||
|
List<AtomicTextBlock> getAtomicTextBlocks();
|
||||||
|
|
||||||
|
|
||||||
|
Boundary getBoundary();
|
||||||
|
|
||||||
|
|
||||||
|
int getNextLinebreak(int fromIndex);
|
||||||
|
|
||||||
|
|
||||||
|
int getPreviousLinebreak(int fromIndex);
|
||||||
|
|
||||||
|
|
||||||
|
Rectangle2D getPosition(int stringIdx);
|
||||||
|
|
||||||
|
|
||||||
|
List<Rectangle2D> getPositions(Boundary range);
|
||||||
|
|
||||||
|
|
||||||
|
int numberOfLines();
|
||||||
|
|
||||||
|
|
||||||
|
int indexOf(String searchTerm);
|
||||||
|
|
||||||
|
|
||||||
|
default boolean containsRange(Boundary range) {
|
||||||
|
|
||||||
|
if (range.end() < range.start()) {
|
||||||
|
throw new IllegalArgumentException(format("Invalid range [%d|%d), StartIndex must be smaller than EndIndex", range.start(), range.end()));
|
||||||
|
}
|
||||||
|
return getBoundary().contains(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
default boolean containsIndex(int stringIndex) {
|
||||||
|
|
||||||
|
return getBoundary().contains(stringIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
default CharSequence subSequence(Boundary boundary) {
|
||||||
|
|
||||||
|
return subSequence(boundary.start(), boundary.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
package com.iqser.red.service.redaction.v1.server.document.graph.textblock;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
import java.util.function.BinaryOperator;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import java.util.stream.Collector;
|
||||||
|
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class TextBlockCollector implements Collector<AtomicTextBlock, ConcatenatedTextBlock, ConcatenatedTextBlock> {
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Supplier<ConcatenatedTextBlock> supplier() {
|
||||||
|
|
||||||
|
return new ConcatenatedTextBlock(Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BiConsumer<ConcatenatedTextBlock, AtomicTextBlock> accumulator() {
|
||||||
|
|
||||||
|
return ConcatenatedTextBlock::concat;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BinaryOperator<ConcatenatedTextBlock> combiner() {
|
||||||
|
|
||||||
|
return ConcatenatedTextBlock::concat;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Function<ConcatenatedTextBlock, ConcatenatedTextBlock> finisher() {
|
||||||
|
|
||||||
|
return Function.identity();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<Characteristics> characteristics() {
|
||||||
|
|
||||||
|
return Set.of(Characteristics.IDENTITY_FINISH, Characteristics.CONCURRENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,11 +1,17 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.document.services;
|
package com.iqser.red.service.redaction.v1.server.document.services;
|
||||||
|
|
||||||
|
import static java.lang.String.format;
|
||||||
|
|
||||||
|
import java.awt.geom.Rectangle2D;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@ -15,19 +21,24 @@ import com.iqser.red.service.redaction.v1.server.classification.model.Header;
|
|||||||
import com.iqser.red.service.redaction.v1.server.classification.model.Page;
|
import com.iqser.red.service.redaction.v1.server.classification.model.Page;
|
||||||
import com.iqser.red.service.redaction.v1.server.classification.model.Section;
|
import com.iqser.red.service.redaction.v1.server.classification.model.Section;
|
||||||
import com.iqser.red.service.redaction.v1.server.classification.model.TextBlock;
|
import com.iqser.red.service.redaction.v1.server.classification.model.TextBlock;
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.AtomicTextBlockEntity;
|
import com.iqser.red.service.redaction.v1.server.document.graph.Boundary;
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.DocumentEntity;
|
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.AtomicTextBlock;
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.FooterEntity;
|
import com.iqser.red.service.redaction.v1.server.document.graph.DocumentGraph;
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.HeaderEntity;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.Node;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.PageEntity;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.ParagraphEntity;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.SectionEntity;
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.TableOfContents;
|
import com.iqser.red.service.redaction.v1.server.document.graph.TableOfContents;
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.TextBlockEntity;
|
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.FooterNode;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.HeaderNode;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.DocumentNode;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.PageNode;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.ParagraphNode;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SectionNode;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.TableNode;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.exception.NotFoundException;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.parsing.model.TextPositionSequence;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.redaction.model.RedRectangle2D;
|
||||||
import com.iqser.red.service.redaction.v1.server.redaction.model.SearchTextWithTextPositionModel;
|
import com.iqser.red.service.redaction.v1.server.redaction.model.SearchTextWithTextPositionModel;
|
||||||
import com.iqser.red.service.redaction.v1.server.redaction.service.SearchTextWithTextPositionFactory;
|
import com.iqser.red.service.redaction.v1.server.redaction.service.SearchTextWithTextPositionFactory;
|
||||||
import com.iqser.red.service.redaction.v1.server.tableextraction.model.AbstractTextContainer;
|
import com.iqser.red.service.redaction.v1.server.tableextraction.model.AbstractTextContainer;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.tableextraction.model.Table;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
@ -37,96 +48,205 @@ public class DocumentGraphFactory {
|
|||||||
|
|
||||||
private final SearchTextWithTextPositionFactory searchTextWithTextPositionFactory;
|
private final SearchTextWithTextPositionFactory searchTextWithTextPositionFactory;
|
||||||
|
|
||||||
private Context context;
|
|
||||||
|
|
||||||
|
public DocumentGraph buildDocumentGraph(Document document) {
|
||||||
|
|
||||||
public DocumentEntity buildEntityFromDocument(Document document) {
|
Context context = new Context(new TableOfContents(),
|
||||||
|
new LinkedList<>(),
|
||||||
context = new Context(new TableOfContents(), new LinkedList<>(), new LinkedList<>(), new LinkedList<>(), new LinkedList<>(), new AtomicInteger(0), new AtomicInteger(0));
|
new LinkedList<>(),
|
||||||
|
new LinkedList<>(),
|
||||||
|
new LinkedList<>(),
|
||||||
|
new AtomicInteger(0),
|
||||||
|
new AtomicInteger(0));
|
||||||
|
|
||||||
context.pages.addAll(document.getPages().stream().map(this::buildPage).toList());
|
context.pages.addAll(document.getPages().stream().map(this::buildPage).toList());
|
||||||
|
|
||||||
// is tracked by Table of Contents
|
// is tracked by Table of Contents
|
||||||
for (int sectionIdx = 0; sectionIdx < document.getSections().size(); sectionIdx++) {
|
addSections(document, context);
|
||||||
addSection(document.getSections().get(sectionIdx), sectionIdx);
|
|
||||||
}
|
|
||||||
|
|
||||||
// not tracked by Table of Contents
|
// not tracked by Table of Contents
|
||||||
document.getHeaders().stream().map(Header::getTextBlocks).flatMap(List::stream).forEach(this::addHeader);
|
addHeaderAndFooterToEachPage(document, context);
|
||||||
document.getFooters().stream().map(Footer::getTextBlocks).flatMap(List::stream).forEach(this::addFooter);
|
DocumentGraph documentGraph = DocumentGraph.builder()
|
||||||
|
.numberOfPages(context.pages.size())
|
||||||
return DocumentEntity.builder().numberOfPages(context.pages.size()).pages(context.pages).sections(context.sections).tableOfContents(context.tableOfContents).build();
|
.pages(context.pages)
|
||||||
|
.sections(context.sections)
|
||||||
|
.tableOfContents(context.tableOfContents)
|
||||||
|
.build();
|
||||||
|
documentGraph.setText(documentGraph.buildTextBlock());
|
||||||
|
return documentGraph;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void addSection(Section section, int sectionIdx) {
|
private void addSections(Document document, Context context) {
|
||||||
|
|
||||||
SectionEntity sectionEntity = SectionEntity.builder()
|
for (int sectionIdx = 0; sectionIdx < document.getSections().size(); sectionIdx++) {
|
||||||
|
addSection(document.getSections().get(sectionIdx), sectionIdx, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void addSection(Section section, int sectionIdx, Context context) {
|
||||||
|
|
||||||
|
SectionNode sectionEntity = SectionNode.builder()
|
||||||
.id(sectionIdx)
|
.id(sectionIdx)
|
||||||
.entities(new LinkedList<>())
|
.entities(new LinkedList<>())
|
||||||
.pages(new LinkedList<>())
|
.pages(new LinkedList<>())
|
||||||
.paragraphs(new LinkedList<>())
|
.paragraphs(new LinkedList<>())
|
||||||
.tableOfContents(context.tableOfContents())
|
.tables(new LinkedList<>())
|
||||||
.subSections(new LinkedList<>())
|
.subSections(new LinkedList<>())
|
||||||
|
.tableOfContents(context.tableOfContents())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
context.sections().add(sectionEntity);
|
context.sections().add(sectionEntity);
|
||||||
if (section.getPageBlocks().get(0) instanceof TextBlock) {
|
List<AbstractTextContainer> pageBlocks = new ArrayList<>(section.getPageBlocks());
|
||||||
sectionEntity.setHeadline(new TextBlockEntity(List.of(buildAtomicTextBlock((TextBlock) section.getPageBlocks().get(0), sectionEntity))));
|
if (pageBlocks.get(0) instanceof TextBlock) {
|
||||||
section.getPageBlocks().remove(0);
|
sectionEntity.setHeadline(buildAtomicTextBlock(((TextBlock) pageBlocks.get(0)).getSequences(), sectionEntity, context));
|
||||||
|
pageBlocks.remove(0);
|
||||||
|
sectionEntity.getPages().add(getPage(section.getPageBlocks().get(0).getPage(), context));
|
||||||
} else {
|
} else {
|
||||||
sectionEntity.setHeadline(emptyTextBlock(sectionEntity));
|
sectionEntity.setHeadline(emptyTextBlock(sectionEntity, context));
|
||||||
}
|
}
|
||||||
|
|
||||||
String sectionId = context.tableOfContents.createNewEntryAndReturnId(TableOfContents.SECTION,
|
String sectionId = context.tableOfContents.createNewEntryAndReturnId(TableOfContents.SECTION, buildSummary(sectionEntity.getHeadline()), sectionEntity);
|
||||||
buildSummary(sectionEntity.getHeadline().getAtomicTextBlocks().get(0)),
|
|
||||||
sectionEntity);
|
|
||||||
sectionEntity.setTocId(sectionId);
|
sectionEntity.setTocId(sectionId);
|
||||||
|
|
||||||
int paragraphIdx = 0;
|
int paragraphIdx = 0;
|
||||||
for (AbstractTextContainer abstractTextContainer : section.getPageBlocks()) {
|
int tableIdx = 0;
|
||||||
|
for (AbstractTextContainer abstractTextContainer : pageBlocks) {
|
||||||
if (abstractTextContainer instanceof TextBlock) {
|
if (abstractTextContainer instanceof TextBlock) {
|
||||||
addParagraph(sectionEntity, (TextBlock) abstractTextContainer, paragraphIdx);
|
addParagraph(sectionEntity, (TextBlock) abstractTextContainer, paragraphIdx, context);
|
||||||
paragraphIdx++;
|
paragraphIdx++;
|
||||||
|
} else if (abstractTextContainer instanceof Table) {
|
||||||
|
addTable(sectionEntity, (Table) abstractTextContainer, tableIdx, context);
|
||||||
|
tableIdx++;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private TextBlockEntity emptyTextBlock(Node parent) {
|
private void addTable(SectionNode sectionEntity, Table table, int tableIdx, Context context) {
|
||||||
|
|
||||||
|
PageNode page = getPage(table.getPage(), context);
|
||||||
|
TableNode tableEntity = TableNode.builder().id(tableIdx).tableOfContents(context.tableOfContents()).pages(new LinkedList<>()).parentSection(sectionEntity).build();
|
||||||
|
sectionEntity.getTables().add(tableEntity);
|
||||||
|
page.getMainBody().add(tableEntity);
|
||||||
|
|
||||||
|
if (!page.getMainBody().contains(sectionEntity)) {
|
||||||
|
sectionEntity.getPages().add(page);
|
||||||
|
page.getMainBody().add(sectionEntity);
|
||||||
|
}
|
||||||
|
|
||||||
return new TextBlockEntity(List.of(AtomicTextBlockEntity.builder()
|
|
||||||
.id(context.textBlockIdx.getAndIncrement())
|
|
||||||
.offset(context.stringOffset.get())
|
|
||||||
.searchText("")
|
|
||||||
.lineBreaks(Collections.emptyList())
|
|
||||||
.stringIdxToPositionIdx(Collections.emptyList())
|
|
||||||
.positions(Collections.emptyList())
|
|
||||||
.parent(parent)
|
|
||||||
.build()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void addParagraph(SectionEntity sectionEntity, TextBlock originalTextBlock, int idx) {
|
private void addParagraph(SectionNode sectionEntity, TextBlock originalTextBlock, int paragraphIdx, Context context) {
|
||||||
|
|
||||||
PageEntity page = getPage(originalTextBlock.getPage());
|
|
||||||
ParagraphEntity paragraph = ParagraphEntity.builder().id(idx).numberOnPage(originalTextBlock.getIndexOnPage()).page(page).parentSection(sectionEntity).build();
|
|
||||||
|
|
||||||
|
PageNode page = getPage(originalTextBlock.getPage(), context);
|
||||||
|
ParagraphNode paragraph = ParagraphNode.builder().id(paragraphIdx).numberOnPage(originalTextBlock.getIndexOnPage()).page(page).parentSection(sectionEntity).build();
|
||||||
sectionEntity.getParagraphs().add(paragraph);
|
sectionEntity.getParagraphs().add(paragraph);
|
||||||
page.getParagraphs().add(paragraph);
|
page.getMainBody().add(paragraph);
|
||||||
|
|
||||||
if (!page.getSections().contains(sectionEntity)) {
|
if (!page.getMainBody().contains(sectionEntity)) {
|
||||||
page.getSections().add(sectionEntity);
|
sectionEntity.getPages().add(page);
|
||||||
|
page.getMainBody().add(sectionEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
var textBlock = buildAtomicTextBlock(originalTextBlock, paragraph);
|
var textBlock = buildAtomicTextBlock(originalTextBlock.getSequences(), paragraph, context);
|
||||||
paragraph.setTextBlock(new TextBlockEntity(List.of(textBlock)));
|
paragraph.setAtomicTextBlock(textBlock);
|
||||||
|
|
||||||
String tocId = context.tableOfContents.createNewChildEntryAndReturnId(sectionEntity.getTocId(), TableOfContents.PARAGRAPH, buildSummary(textBlock), paragraph);
|
String tocId = context.tableOfContents.createNewChildEntryAndReturnId(sectionEntity.getTocId(), TableOfContents.PARAGRAPH, buildSummary(textBlock), paragraph);
|
||||||
paragraph.setTocId(tocId);
|
paragraph.setTocId(tocId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static String buildSummary(AtomicTextBlockEntity textBlock) {
|
private void addHeaderAndFooterToEachPage(Document document, Context context) {
|
||||||
|
|
||||||
|
Map<Integer, List<TextBlock>> headers = document.getHeaders()
|
||||||
|
.stream()
|
||||||
|
.map(Header::getTextBlocks)
|
||||||
|
.flatMap(List::stream)
|
||||||
|
.collect(Collectors.groupingBy(AbstractTextContainer::getPage, Collectors.toList()));
|
||||||
|
|
||||||
|
Map<Integer, List<TextBlock>> footers = document.getFooters()
|
||||||
|
.stream()
|
||||||
|
.map(Footer::getTextBlocks)
|
||||||
|
.flatMap(List::stream)
|
||||||
|
.collect(Collectors.groupingBy(AbstractTextContainer::getPage, Collectors.toList()));
|
||||||
|
|
||||||
|
for (int pageIndex = 1; pageIndex <= document.getPages().size(); pageIndex++) {
|
||||||
|
if (headers.containsKey(pageIndex)) {
|
||||||
|
addHeader(headers.get(pageIndex), context);
|
||||||
|
} else {
|
||||||
|
addEmptyHeader(pageIndex, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int pageIndex = 1; pageIndex <= document.getPages().size(); pageIndex++) {
|
||||||
|
if (footers.containsKey(pageIndex)) {
|
||||||
|
addFooter(footers.get(pageIndex), context);
|
||||||
|
} else {
|
||||||
|
addEmptyFooter(pageIndex, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void addFooter(List<TextBlock> textBlocks, Context context) {
|
||||||
|
|
||||||
|
PageNode page = getPage(textBlocks.get(0).getPage(), context);
|
||||||
|
FooterNode footer = FooterNode.builder().page(page).build();
|
||||||
|
AtomicTextBlock atomicTextBlock = buildAtomicTextBlock(mergeAndSortTextPositionSequences(textBlocks), footer, context);
|
||||||
|
footer.setAtomicTextBlock(atomicTextBlock);
|
||||||
|
page.setFooter(footer);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void addHeader(List<TextBlock> textBlocks, Context context) {
|
||||||
|
|
||||||
|
PageNode page = getPage(textBlocks.get(0).getPage(), context);
|
||||||
|
HeaderNode header = HeaderNode.builder().page(page).build();
|
||||||
|
AtomicTextBlock atomicTextBlock = buildAtomicTextBlock(mergeAndSortTextPositionSequences(textBlocks), header, context);
|
||||||
|
header.setAtomicTextBlock(atomicTextBlock);
|
||||||
|
page.setHeader(header);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void addEmptyFooter(int pageIndex, Context context) {
|
||||||
|
|
||||||
|
PageNode page = getPage(pageIndex, context);
|
||||||
|
FooterNode footer = FooterNode.builder().page(page).build();
|
||||||
|
AtomicTextBlock atomicTextBlock = emptyTextBlock(footer, context);
|
||||||
|
footer.setAtomicTextBlock(atomicTextBlock);
|
||||||
|
page.setFooter(footer);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void addEmptyHeader(int pageIndex, Context context) {
|
||||||
|
|
||||||
|
PageNode page = getPage(pageIndex, context);
|
||||||
|
HeaderNode header = HeaderNode.builder().page(page).build();
|
||||||
|
AtomicTextBlock atomicTextBlock = emptyTextBlock(header, context);
|
||||||
|
header.setAtomicTextBlock(atomicTextBlock);
|
||||||
|
page.setHeader(header);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private AtomicTextBlock emptyTextBlock(DocumentNode parent, Context context) {
|
||||||
|
|
||||||
|
return AtomicTextBlock.builder()
|
||||||
|
.id(context.textBlockIdx.getAndIncrement())
|
||||||
|
.boundary(new Boundary(context.stringOffset.get(), context.stringOffset.get()))
|
||||||
|
.searchText("")
|
||||||
|
.lineBreaks(Collections.emptyList())
|
||||||
|
.stringIdxToPositionIdx(Collections.emptyList())
|
||||||
|
.positions(Collections.emptyList())
|
||||||
|
.parent(parent)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static String buildSummary(AtomicTextBlock textBlock) {
|
||||||
|
|
||||||
if (textBlock == null) {
|
if (textBlock == null) {
|
||||||
return " probably a table";
|
return " probably a table";
|
||||||
@ -140,67 +260,63 @@ public class DocumentGraphFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private PageEntity buildPage(Page p) {
|
private PageNode buildPage(Page p) {
|
||||||
|
|
||||||
return PageEntity.builder()
|
return PageNode.builder().height((int) p.getPageHeight()).width((int) p.getPageWidth()).number(p.getPageNumber()).mainBody(new LinkedList<>()).build();
|
||||||
.height((int) p.getPageHeight())
|
|
||||||
.width((int) p.getPageWidth())
|
|
||||||
.number(p.getPageNumber())
|
|
||||||
.paragraphs(new LinkedList<>())
|
|
||||||
.sections(new LinkedList<>())
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void addFooter(TextBlock textBlock) {
|
private List<TextPositionSequence> mergeAndSortTextPositionSequences(List<TextBlock> textBlocks) {
|
||||||
|
|
||||||
PageEntity page = getPage(textBlock.getPage());
|
Comparator<TextPositionSequence> sortByX = (sequence1, sequence2) -> (int) (sequence1.getTextPositions().get(0).getPosition()[0] - sequence2.getTextPositions()
|
||||||
FooterEntity footer = FooterEntity.builder().page(page).build();
|
.get(0)
|
||||||
AtomicTextBlockEntity textBlockEntity = buildAtomicTextBlock(textBlock, footer);
|
.getPosition()[0]);
|
||||||
footer.setTextBlock(new TextBlockEntity(List.of(textBlockEntity)));
|
Comparator<TextPositionSequence> sortByY = (sequence1, sequence2) -> (int) (sequence1.getTextPositions().get(0).getPosition()[1] - sequence2.getTextPositions()
|
||||||
page.setFooter(footer);
|
.get(0)
|
||||||
|
.getPosition()[1]);
|
||||||
|
|
||||||
|
return textBlocks.stream().map(TextBlock::getSequences).flatMap(List::stream).sorted(sortByX.thenComparing(sortByY)).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void addHeader(TextBlock textBlock) {
|
private AtomicTextBlock buildAtomicTextBlock(List<TextPositionSequence> sequences, DocumentNode parent, Context context) {
|
||||||
|
|
||||||
PageEntity page = getPage(textBlock.getPage());
|
SearchTextWithTextPositionModel searchTextWithTextPositionModel = searchTextWithTextPositionFactory.buildSearchTextToTextPositionModel(sequences);
|
||||||
HeaderEntity header = HeaderEntity.builder().page(page).build();
|
|
||||||
AtomicTextBlockEntity textBlockEntity = buildAtomicTextBlock(textBlock, header);
|
|
||||||
header.setTextBlock(new TextBlockEntity(List.of(textBlockEntity)));
|
|
||||||
page.setHeader(header);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public AtomicTextBlockEntity buildAtomicTextBlock(TextBlock textBlock, Node parent) {
|
|
||||||
|
|
||||||
SearchTextWithTextPositionModel searchTextWithTextPositionModel = searchTextWithTextPositionFactory.buildSearchTextToTextPositionModel(textBlock.getSequences());
|
|
||||||
int offset = context.stringOffset().getAndAdd(searchTextWithTextPositionModel.getSearchText().length());
|
int offset = context.stringOffset().getAndAdd(searchTextWithTextPositionModel.getSearchText().length());
|
||||||
|
|
||||||
return AtomicTextBlockEntity.builder()
|
return AtomicTextBlock.builder()
|
||||||
.id(context.textBlockIdx.getAndIncrement())
|
.id(context.textBlockIdx.getAndIncrement())
|
||||||
.parent(parent)
|
.parent(parent)
|
||||||
.searchText(searchTextWithTextPositionModel.getSearchText())
|
.searchText(searchTextWithTextPositionModel.getSearchText())
|
||||||
.lineBreaks(searchTextWithTextPositionModel.getLineBreaks())
|
.lineBreaks(searchTextWithTextPositionModel.getLineBreaks())
|
||||||
.positions(searchTextWithTextPositionModel.getPositions())
|
.positions(toRectangle2D(searchTextWithTextPositionModel.getPositions()))
|
||||||
.stringIdxToPositionIdx(searchTextWithTextPositionModel.getStringCoordsToPositionCoords())
|
.stringIdxToPositionIdx(searchTextWithTextPositionModel.getStringCoordsToPositionCoords())
|
||||||
.offset(offset)
|
.boundary(new Boundary(offset, offset + searchTextWithTextPositionModel.getSearchText().length()))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public PageEntity getPage(int i) {
|
private List<Rectangle2D> toRectangle2D(List<RedRectangle2D> positions) {
|
||||||
|
|
||||||
return context.pages.stream().filter(page -> page.getNumber() == i).findFirst().orElseThrow(NoSuchFieldError::new);
|
return positions.stream().map(r -> (Rectangle2D) new Rectangle2D.Double(r.getX(), r.getY(), r.getWidth(), r.getHeight())).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private PageNode getPage(int pageIndex, Context context) {
|
||||||
|
|
||||||
|
return context.pages.stream()
|
||||||
|
.filter(page -> page.getNumber() == pageIndex)
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(() -> new NotFoundException(format("Page with number %d not found", pageIndex)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
record Context(
|
record Context(
|
||||||
TableOfContents tableOfContents,
|
TableOfContents tableOfContents,
|
||||||
List<PageEntity> pages,
|
List<PageNode> pages,
|
||||||
List<SectionEntity> sections,
|
List<SectionNode> sections,
|
||||||
List<HeaderEntity> headers,
|
List<HeaderNode> headers,
|
||||||
List<FooterEntity> footers,
|
List<FooterNode> footers,
|
||||||
AtomicInteger stringOffset,
|
AtomicInteger stringOffset,
|
||||||
AtomicInteger textBlockIdx) {
|
AtomicInteger textBlockIdx) {
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,31 @@
|
|||||||
|
package com.iqser.red.service.redaction.v1.server.document.services;
|
||||||
|
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.EntityNode;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock;
|
||||||
|
|
||||||
|
public class EntityEnrichmentUtility {
|
||||||
|
|
||||||
|
public static EntityNode enrichEntity(EntityNode entity, TextBlock textBlock) {
|
||||||
|
|
||||||
|
entity.setPositions(textBlock.getPositions(entity.getBoundary()));
|
||||||
|
entity.setTextAfter(findTextAfter(entity.getBoundary().end(), textBlock));
|
||||||
|
entity.setTextBefore(findTextBefore(entity.getBoundary().start(), textBlock));
|
||||||
|
entity.setValue(textBlock.subSequence(entity.getBoundary()).toString());
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static CharSequence findTextAfter(int index, TextBlock textBlock) {
|
||||||
|
|
||||||
|
int nextLineBreak = textBlock.getNextLinebreak(index);
|
||||||
|
return textBlock.subSequence(index, nextLineBreak);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static CharSequence findTextBefore(int index, TextBlock textBlock) {
|
||||||
|
|
||||||
|
int previousLinebreak = textBlock.getPreviousLinebreak(index);
|
||||||
|
return textBlock.subSequence(previousLinebreak, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
package com.iqser.red.service.redaction.v1.server.document.services;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.Boundary;
|
||||||
|
|
||||||
|
public class RangeComparators {
|
||||||
|
|
||||||
|
public static Comparator<Boundary> contained() {
|
||||||
|
|
||||||
|
return (range1, range2) -> {
|
||||||
|
if (contained(range1, range2)) {
|
||||||
|
return -1;
|
||||||
|
} else if (contained(range2, range1)) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param range1 A Range
|
||||||
|
* @param range2 Also Range
|
||||||
|
* @return true, if range1 contains range2
|
||||||
|
* false, otherwise
|
||||||
|
*/
|
||||||
|
public static boolean contained(Boundary range1, Boundary range2) {
|
||||||
|
|
||||||
|
return range1.start() <= range2.start() && range2.end() <= range1.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean contained(Boundary range, int index) {
|
||||||
|
|
||||||
|
return range.start() <= index && index < range.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
package com.iqser.red.service.redaction.v1.server.document.services;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.Boundary;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.redaction.utils.Patterns;
|
||||||
|
|
||||||
|
public class RegexMatcher {
|
||||||
|
|
||||||
|
public static boolean anyMatch(String regexPattern, CharSequence searchText) {
|
||||||
|
|
||||||
|
var pattern = Patterns.getCompiledPattern(regexPattern, false);
|
||||||
|
return pattern.matcher(searchText).find();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static Boundary findFirstBoundary(String regexPattern, CharSequence searchText) {
|
||||||
|
|
||||||
|
var pattern = Patterns.getCompiledPattern(regexPattern, false);
|
||||||
|
Matcher matcher = pattern.matcher(searchText);
|
||||||
|
return new Boundary(matcher.start(), matcher.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Boundary> findBoundaries(String regexPattern, CharSequence searchText) {
|
||||||
|
|
||||||
|
var pattern = Patterns.getCompiledPattern(regexPattern, false);
|
||||||
|
Matcher matcher = pattern.matcher(searchText);
|
||||||
|
List<Boundary> boundaries = new LinkedList<>();
|
||||||
|
while (matcher.find()) {
|
||||||
|
boundaries.add(new Boundary(matcher.start(), matcher.end()));
|
||||||
|
}
|
||||||
|
return boundaries;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,6 +1,5 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server.redaction.service;
|
package com.iqser.red.service.redaction.v1.server.redaction.service;
|
||||||
|
|
||||||
import java.awt.geom.Rectangle2D;
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@ -33,7 +32,7 @@ public class SearchTextWithTextPositionFactory {
|
|||||||
currentUnicode = word.getTextPositions().get(i).getUnicode();
|
currentUnicode = word.getTextPositions().get(i).getUnicode();
|
||||||
|
|
||||||
if (isLineBreak(currentUnicode)) {
|
if (isLineBreak(currentUnicode)) {
|
||||||
lineBreaksStringIdx.add(stringIdx);
|
lineBreaksStringIdx.add(stringIdx + 1);
|
||||||
} else if (!isRepeatedWhitespace(currentUnicode, previousUnicode) && //
|
} else if (!isRepeatedWhitespace(currentUnicode, previousUnicode) && //
|
||||||
!isHyphenLinebreak(currentUnicode)) {
|
!isHyphenLinebreak(currentUnicode)) {
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,8 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
import org.ahocorasick.trie.Trie;
|
import org.ahocorasick.trie.Trie;
|
||||||
|
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.Boundary;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@ -75,15 +77,30 @@ public class SearchImplementation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<MatchPosition> getMatches(CharSequence text) {
|
|
||||||
|
public List<Boundary> getMatches(CharSequence text) {
|
||||||
|
|
||||||
if (this.values.isEmpty()) {
|
if (this.values.isEmpty()) {
|
||||||
return new ArrayList<>();
|
return new ArrayList<>();
|
||||||
}
|
}
|
||||||
if (this.pattern != null) {
|
if (this.pattern != null) {
|
||||||
|
return this.pattern.matcher(text).results().map(r -> new Boundary(r.start(), r.end())).collect(Collectors.toList());
|
||||||
return this.pattern.matcher(text).results().map(r -> new MatchPosition(r.start(), r.end())).collect(Collectors.toList());
|
|
||||||
} else {
|
} else {
|
||||||
return this.trie.parseText(text).stream().map(r -> new MatchPosition(r.getStart(), r.getEnd() + 1)).collect(Collectors.toList());
|
return this.trie.parseText(text).stream().map(r -> new Boundary(r.getStart(), r.getEnd() + 1)).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public List<Boundary> getMatches(CharSequence text, int startOffset) {
|
||||||
|
|
||||||
|
if (this.values.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
CharSequence subSequence = text.subSequence(startOffset, startOffset + text.length());
|
||||||
|
if (this.pattern != null) {
|
||||||
|
return this.pattern.matcher(subSequence).results().map(r -> new Boundary(r.start() + startOffset, r.end() + startOffset)).collect(Collectors.toList());
|
||||||
|
} else {
|
||||||
|
return this.trie.parseText(subSequence).stream().map(r -> new Boundary(r.getStart() + startOffset, r.getEnd() + startOffset + 1)).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,6 +121,7 @@ public class SearchImplementation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public record MatchPosition(int startIndex, int endIndex) {
|
public record MatchPosition(int startIndex, int endIndex) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,6 @@ package com.iqser.red.service.redaction.v1.server;
|
|||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
@ -27,7 +26,10 @@ import org.kie.api.KieServices;
|
|||||||
import org.kie.api.builder.KieBuilder;
|
import org.kie.api.builder.KieBuilder;
|
||||||
import org.kie.api.builder.KieFileSystem;
|
import org.kie.api.builder.KieFileSystem;
|
||||||
import org.kie.api.builder.KieModule;
|
import org.kie.api.builder.KieModule;
|
||||||
|
import org.kie.api.builder.KieRepository;
|
||||||
|
import org.kie.api.builder.ReleaseId;
|
||||||
import org.kie.api.runtime.KieContainer;
|
import org.kie.api.runtime.KieContainer;
|
||||||
|
import org.kie.internal.io.ResourceFactory;
|
||||||
import org.mockito.stubbing.Answer;
|
import org.mockito.stubbing.Answer;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||||
@ -62,7 +64,9 @@ import lombok.SneakyThrows;
|
|||||||
@Import(AbstractTestWithDictionaries.TestConfiguration.class)
|
@Import(AbstractTestWithDictionaries.TestConfiguration.class)
|
||||||
public class AbstractTestWithDictionaries {
|
public class AbstractTestWithDictionaries {
|
||||||
|
|
||||||
private static final String RULES = loadFromClassPath("drools/rules.drl");
|
protected static final String RULES = loadFromClassPath("drools/rules.drl");
|
||||||
|
protected static final String RULES_PATH = "drools/rules.drl";
|
||||||
|
protected static final String ENTITY_RULES_PATH = "drools/entity_rules.drl";
|
||||||
private static final String VERTEBRATE = "vertebrate";
|
private static final String VERTEBRATE = "vertebrate";
|
||||||
private static final String ADDRESS = "CBI_address";
|
private static final String ADDRESS = "CBI_address";
|
||||||
private static final String AUTHOR = "CBI_author";
|
private static final String AUTHOR = "CBI_author";
|
||||||
@ -499,13 +503,22 @@ public class AbstractTestWithDictionaries {
|
|||||||
KieServices kieServices = KieServices.Factory.get();
|
KieServices kieServices = KieServices.Factory.get();
|
||||||
|
|
||||||
KieFileSystem kieFileSystem = kieServices.newKieFileSystem();
|
KieFileSystem kieFileSystem = kieServices.newKieFileSystem();
|
||||||
InputStream input = new ByteArrayInputStream(RULES.getBytes(StandardCharsets.UTF_8));
|
//kieFileSystem.write(ResourceFactory.newClassPathResource(RULES_PATH, "UTF-8"));
|
||||||
kieFileSystem.write("src/test/resources/drools/rules.drl", kieServices.getResources().newInputStreamResource(input));
|
kieFileSystem.write(ResourceFactory.newClassPathResource(ENTITY_RULES_PATH, "UTF-8"));
|
||||||
KieBuilder kieBuilder = kieServices.newKieBuilder(kieFileSystem);
|
|
||||||
kieBuilder.buildAll();
|
|
||||||
KieModule kieModule = kieBuilder.getKieModule();
|
|
||||||
|
|
||||||
return kieServices.newKieContainer(kieModule.getReleaseId());
|
KieRepository kieRepository = kieServices.getRepository();
|
||||||
|
|
||||||
|
kieRepository.addKieModule(new KieModule() {
|
||||||
|
public ReleaseId getReleaseId() {
|
||||||
|
return kieRepository.getDefaultReleaseId();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
KieBuilder kieBuilder = kieServices
|
||||||
|
.newKieBuilder(kieFileSystem)
|
||||||
|
.buildAll();
|
||||||
|
|
||||||
|
return kieServices.newKieContainer(kieRepository.getDefaultReleaseId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
@ -1,15 +1,36 @@
|
|||||||
package com.iqser.red.service.redaction.v1.server;
|
package com.iqser.red.service.redaction.v1.server;
|
||||||
|
|
||||||
import java.util.List;
|
import static com.iqser.red.service.redaction.v1.server.utils.PdfDraw.drawRectangle2DList;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.Shape;
|
||||||
|
import java.awt.geom.AffineTransform;
|
||||||
|
import java.awt.geom.Rectangle2D;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.kie.api.runtime.KieContainer;
|
||||||
|
import org.kie.api.runtime.KieSession;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.DocumentEntity;
|
import com.iqser.red.service.redaction.v1.model.FileAttribute;
|
||||||
import com.iqser.red.service.redaction.v1.server.document.graph.TextBlockEntity;
|
import com.iqser.red.service.redaction.v1.server.document.graph.DocumentGraph;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.EntityNode;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.PageNode;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.ParagraphNode;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.SectionNode;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.textblock.TextBlock;
|
||||||
import com.iqser.red.service.redaction.v1.server.document.services.DocumentGraphFactory;
|
import com.iqser.red.service.redaction.v1.server.document.services.DocumentGraphFactory;
|
||||||
import com.iqser.red.service.redaction.v1.server.redaction.model.Dictionary;
|
import com.iqser.red.service.redaction.v1.server.redaction.model.Dictionary;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.redaction.model.EntityType;
|
||||||
import com.iqser.red.service.redaction.v1.server.redaction.service.DictionaryService;
|
import com.iqser.red.service.redaction.v1.server.redaction.service.DictionaryService;
|
||||||
import com.iqser.red.service.redaction.v1.server.redaction.utils.SearchImplementation;
|
import com.iqser.red.service.redaction.v1.server.redaction.utils.SearchImplementation;
|
||||||
import com.iqser.red.service.redaction.v1.server.segmentation.PdfSegmentationService;
|
import com.iqser.red.service.redaction.v1.server.segmentation.PdfSegmentationService;
|
||||||
@ -27,10 +48,14 @@ public class DocumentGraphIntegrationTest extends AbstractTestWithDictionaries {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private DictionaryService dictionaryService;
|
private DictionaryService dictionaryService;
|
||||||
|
|
||||||
|
@Qualifier("kieContainer")
|
||||||
|
@Autowired
|
||||||
|
private KieContainer kieContainer;
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
public void testBuildDocumentEntity() {
|
public void testDroolsOnDocumentGraph() {
|
||||||
|
|
||||||
String filename = "files/new/crafted document";
|
String filename = "files/new/crafted document";
|
||||||
|
|
||||||
@ -38,28 +63,189 @@ public class DocumentGraphIntegrationTest extends AbstractTestWithDictionaries {
|
|||||||
ClassPathResource fileResource = new ClassPathResource(filename + ".pdf");
|
ClassPathResource fileResource = new ClassPathResource(filename + ".pdf");
|
||||||
|
|
||||||
var classifiedDoc = segmentationService.parseDocument(TEST_DOSSIER_ID, TEST_FILE_ID, fileResource.getInputStream(), null);
|
var classifiedDoc = segmentationService.parseDocument(TEST_DOSSIER_ID, TEST_FILE_ID, fileResource.getInputStream(), null);
|
||||||
DocumentEntity document = documentGraphFactory.buildEntityFromDocument(classifiedDoc);
|
DocumentGraph document = documentGraphFactory.buildDocumentGraph(classifiedDoc);
|
||||||
|
|
||||||
var start = System.currentTimeMillis();
|
|
||||||
|
|
||||||
TextBlockEntity textBlock = document.buildTextBlock();
|
|
||||||
dictionaryService.updateDictionary(TEST_DOSSIER_TEMPLATE_ID, TEST_DOSSIER_ID);
|
dictionaryService.updateDictionary(TEST_DOSSIER_TEMPLATE_ID, TEST_DOSSIER_ID);
|
||||||
Dictionary dictionary = dictionaryService.getDeepCopyDictionary(TEST_DOSSIER_TEMPLATE_ID, TEST_DOSSIER_ID);
|
Dictionary dictionary = dictionaryService.getDeepCopyDictionary(TEST_DOSSIER_TEMPLATE_ID, TEST_DOSSIER_ID);
|
||||||
|
|
||||||
|
List<EntityNode> foundEntities = new LinkedList<>();
|
||||||
for (var model : dictionary.getDictionaryModels()) {
|
for (var model : dictionary.getDictionaryModels()) {
|
||||||
List<SearchImplementation.MatchPosition> entries = model.getEntriesSearch().getMatches(textBlock);
|
findEntitiesWithSearchImplementationAndAddToGraph(document, model.getEntriesSearch(), EntityType.ENTITY, foundEntities, model.getType());
|
||||||
List<SearchImplementation.MatchPosition> falsePositives = model.getFalsePositiveSearch().getMatches(textBlock);
|
findEntitiesWithSearchImplementationAndAddToGraph(document, model.getFalsePositiveSearch(), EntityType.FALSE_POSITIVE, foundEntities, model.getType());
|
||||||
List<SearchImplementation.MatchPosition> falseRecommendations = model.getFalseRecommendationsSearch().getMatches(textBlock);
|
findEntitiesWithSearchImplementationAndAddToGraph(document, model.getFalseRecommendationsSearch(), EntityType.FALSE_RECOMMENDATION, foundEntities, model.getType());
|
||||||
|
|
||||||
if (!entries.isEmpty()) {
|
|
||||||
var pos = entries.get(0);
|
|
||||||
System.out.println(textBlock.getPositions(pos.startIndex(), pos.endIndex()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
KieSession kieSession = kieContainer.newKieSession();
|
||||||
|
kieSession.setGlobal("document", document);
|
||||||
|
document.getEntities().forEach(kieSession::insert);
|
||||||
|
document.getSections().forEach(kieSession::insert);
|
||||||
|
document.getPages().forEach(kieSession::insert);
|
||||||
|
document.getSections().forEach(sec -> sec.getParagraphs().forEach(kieSession::insert));
|
||||||
|
kieSession.insert(FileAttribute.builder().label("Vertebrate Study").value("Yes").build());
|
||||||
|
kieSession.fireAllRules();
|
||||||
|
|
||||||
|
drawAllEntities(filename, fileResource, document);
|
||||||
}
|
}
|
||||||
|
|
||||||
System.out.printf("Search took %fs", ((float) (System.currentTimeMillis() - start)) / 1000);
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SneakyThrows
|
||||||
|
public void testBuildTextBlockPerformance() {
|
||||||
|
|
||||||
|
int n = 10000;
|
||||||
|
String filename = "files/Metolachlor/S-Metolachlor_RAR_01_Volume_1_2018-09-06";
|
||||||
|
|
||||||
|
prepareStorage(filename + ".pdf");
|
||||||
|
ClassPathResource fileResource = new ClassPathResource(filename + ".pdf");
|
||||||
|
|
||||||
|
var classifiedDoc = segmentationService.parseDocument(TEST_DOSSIER_ID, TEST_FILE_ID, fileResource.getInputStream(), null);
|
||||||
|
DocumentGraph document = documentGraphFactory.buildDocumentGraph(classifiedDoc);
|
||||||
|
var start = System.currentTimeMillis();
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
TextBlock textBlock = document.getText();
|
||||||
|
}
|
||||||
|
float durationMillis = ((float) (System.currentTimeMillis() - start));
|
||||||
|
System.out.printf("%d calls of buildTextBlock() on document took %f s, average is %f ms\n", n, durationMillis / 1000, durationMillis / n);
|
||||||
|
|
||||||
|
SectionNode section = document.getSections().get(8);
|
||||||
|
start = System.currentTimeMillis();
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
section.buildTextBlock();
|
||||||
|
}
|
||||||
|
durationMillis = ((float) (System.currentTimeMillis() - start));
|
||||||
|
System.out.printf("%d calls of buildTextBlock() on section took %f s, average is %f ms\n", n, durationMillis / 1000, durationMillis / n);
|
||||||
|
|
||||||
|
ParagraphNode paragraph = document.getSections().get(8).getParagraphs().get(1);
|
||||||
|
start = System.currentTimeMillis();
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
paragraph.buildTextBlock();
|
||||||
|
}
|
||||||
|
durationMillis = ((float) (System.currentTimeMillis() - start));
|
||||||
|
System.out.printf("%d calls of buildTextBlock() on paragraph took %f s, average is %f ms\n", n, durationMillis / 1000, durationMillis / n);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SneakyThrows
|
||||||
|
public void testDictionarySearchOnDocumentGraph() {
|
||||||
|
|
||||||
|
String filename = "files/Metolachlor/S-Metolachlor_RAR_01_Volume_1_2018-09-06";
|
||||||
|
|
||||||
|
prepareStorage(filename + ".pdf");
|
||||||
|
ClassPathResource fileResource = new ClassPathResource(filename + ".pdf");
|
||||||
|
|
||||||
|
var classifiedDoc = segmentationService.parseDocument(TEST_DOSSIER_ID, TEST_FILE_ID, fileResource.getInputStream(), null);
|
||||||
|
DocumentGraph document = documentGraphFactory.buildDocumentGraph(classifiedDoc);
|
||||||
|
|
||||||
|
dictionaryService.updateDictionary(TEST_DOSSIER_TEMPLATE_ID, TEST_DOSSIER_ID);
|
||||||
|
Dictionary dictionary = dictionaryService.getDeepCopyDictionary(TEST_DOSSIER_TEMPLATE_ID, TEST_DOSSIER_ID);
|
||||||
|
|
||||||
|
int numberOfSearches = 25;
|
||||||
|
float totalTime = 0;
|
||||||
|
float totalGraphTime = 0;
|
||||||
|
List<EntityNode> foundEntities = new LinkedList<>();
|
||||||
|
for (int i = 0; i < numberOfSearches; i++) {
|
||||||
|
|
||||||
|
var graphStart = System.currentTimeMillis();
|
||||||
|
document = documentGraphFactory.buildDocumentGraph(classifiedDoc);
|
||||||
|
var graphTime = ((float) (System.currentTimeMillis() - graphStart)) / 1000;
|
||||||
|
totalGraphTime += graphTime;
|
||||||
|
var start = System.currentTimeMillis();
|
||||||
|
foundEntities = new LinkedList<>();
|
||||||
|
|
||||||
|
for (var model : dictionary.getDictionaryModels()) {
|
||||||
|
findEntitiesWithSearchImplementationAndAddToGraph(document, model.getEntriesSearch(), EntityType.ENTITY, foundEntities, model.getType());
|
||||||
|
findEntitiesWithSearchImplementationAndAddToGraph(document, model.getFalsePositiveSearch(), EntityType.FALSE_POSITIVE, foundEntities, model.getType());
|
||||||
|
findEntitiesWithSearchImplementationAndAddToGraph(document, model.getFalseRecommendationsSearch(), EntityType.FALSE_RECOMMENDATION, foundEntities, model.getType());
|
||||||
|
}
|
||||||
|
var time = ((float) (System.currentTimeMillis() - start)) / 1000;
|
||||||
|
totalTime += time;
|
||||||
|
System.out.printf("%d Search %fs; Graph construction %fs \n", i, time, graphTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.printf("%d Searches took %f s, average %f s\n", numberOfSearches, totalTime, totalTime / numberOfSearches);
|
||||||
|
System.out.printf("%d Graph constructions took %f s, average %f s\n", numberOfSearches, totalGraphTime, totalGraphTime / numberOfSearches);
|
||||||
|
Set<EntityNode> distinctFoundEntities = new HashSet<>(foundEntities);
|
||||||
|
System.out.printf("Found %d entities and saved %d\n", distinctFoundEntities.size(), document.getEntities().size());
|
||||||
|
//assert document.getEntities().size() == distinctFoundEntities.size();
|
||||||
|
|
||||||
|
drawAllEntities(filename, fileResource, document);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static void drawAllEntities(String filename, ClassPathResource fileResource, DocumentGraph document) throws IOException {
|
||||||
|
|
||||||
|
var tmpFileName = "/tmp/" + filename.split("/")[2] + "_ENTITY_BBOX.pdf";
|
||||||
|
try (var fileStream = fileResource.getInputStream()) {
|
||||||
|
PDDocument pdDocument = PDDocument.load(fileStream);
|
||||||
|
|
||||||
|
for (PageNode page : document.getPages()) {
|
||||||
|
AffineTransform mirrorY = new AffineTransform(1, 0, 0, -1, 0, page.getHeight() + 6);
|
||||||
|
List<Rectangle2D> entityPositionsOnPage = page.getEntities()
|
||||||
|
.stream()
|
||||||
|
.filter(EntityNode::isRedact)
|
||||||
|
.map(EntityNode::getPositions)
|
||||||
|
.flatMap(List::stream)
|
||||||
|
.map(mirrorY::createTransformedShape)
|
||||||
|
.map(Shape::getBounds2D)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
drawRectangle2DList(pdDocument, page.getNumber(), entityPositionsOnPage, Color.BLACK);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (PageNode page : document.getPages()) {
|
||||||
|
AffineTransform mirrorY = new AffineTransform(1, 0, 0, -1, 0, page.getHeight() + 6);
|
||||||
|
List<Rectangle2D> entityPositionsOnPage = page.getEntities()
|
||||||
|
.stream()
|
||||||
|
.filter(e -> !e.isRedact())
|
||||||
|
.map(EntityNode::getPositions)
|
||||||
|
.flatMap(List::stream)
|
||||||
|
.map(mirrorY::createTransformedShape)
|
||||||
|
.map(Shape::getBounds2D)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
drawRectangle2DList(pdDocument, page.getNumber(), entityPositionsOnPage, Color.BLUE);
|
||||||
|
}
|
||||||
|
File outputFile = new File(tmpFileName);
|
||||||
|
pdDocument.save(outputFile);
|
||||||
|
pdDocument.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void findEntitiesWithSearchImplementationAndAddToGraph(DocumentGraph documentGraph,
|
||||||
|
SearchImplementation searchImplementation,
|
||||||
|
EntityType entityType,
|
||||||
|
List<EntityNode> foundEntities,
|
||||||
|
String type) {
|
||||||
|
|
||||||
|
documentGraph.getSections().forEach( section -> {
|
||||||
|
TextBlock textBlock = section.buildTextBlock();
|
||||||
|
searchImplementation.getMatches(textBlock, textBlock.getBoundary().start())
|
||||||
|
.stream()
|
||||||
|
.map(bounds -> section.createAndAddEntity(bounds, type, entityType))
|
||||||
|
.forEach(foundEntities::add);
|
||||||
|
});
|
||||||
|
documentGraph.getPages().forEach( page -> {
|
||||||
|
TextBlock textBlock = page.getHeader().buildTextBlock();
|
||||||
|
searchImplementation.getMatches(textBlock, textBlock.getBoundary().start())
|
||||||
|
.stream()
|
||||||
|
.map(bounds -> page.getHeader().createAndAddEntity(bounds, type, entityType))
|
||||||
|
.forEach(foundEntities::add);
|
||||||
|
});
|
||||||
|
documentGraph.getPages().forEach( page -> {
|
||||||
|
TextBlock textBlock = page.getFooter().buildTextBlock();
|
||||||
|
searchImplementation.getMatches(textBlock, textBlock.getBoundary().start())
|
||||||
|
.stream()
|
||||||
|
.map(bounds -> page.getFooter().createAndAddEntity(bounds, type, entityType))
|
||||||
|
.forEach(foundEntities::add);
|
||||||
|
});
|
||||||
|
/*
|
||||||
|
searchImplementation.getMatches(documentGraph.getText())
|
||||||
|
.stream()
|
||||||
|
.map(pos -> documentGraph.createAndAddEntity(new Boundary(pos.startIndex(), pos.endIndex()), type, entityType))
|
||||||
|
.forEach(foundEntities::add);
|
||||||
|
|
||||||
|
*/
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,44 @@
|
|||||||
|
package com.iqser.red.service.redaction.v1.server.document.graph;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class BoundaryTest {
|
||||||
|
|
||||||
|
Boundary startBoundary;
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
startBoundary = new Boundary(10, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testContains() {
|
||||||
|
assertTrue(startBoundary.contains(11));
|
||||||
|
assertTrue(startBoundary.contains(50));
|
||||||
|
assertFalse(startBoundary.contains(9));
|
||||||
|
assertFalse(startBoundary.contains(100));
|
||||||
|
assertFalse(startBoundary.contains(150));
|
||||||
|
assertFalse(startBoundary.contains(-123));
|
||||||
|
assertTrue(startBoundary.contains(new Boundary(11, 99)));
|
||||||
|
assertTrue(startBoundary.contains(new Boundary(10, 100)));
|
||||||
|
assertTrue(startBoundary.contains(new Boundary(11, 11)));
|
||||||
|
assertFalse(startBoundary.contains(9, 100));
|
||||||
|
assertTrue(startBoundary.contains(100, 100));
|
||||||
|
assertFalse(startBoundary.contains(100, 101));
|
||||||
|
assertFalse(startBoundary.contains(150, 151));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testIntersects() {
|
||||||
|
assertTrue(startBoundary.intersects(new Boundary(1, 11)));
|
||||||
|
assertTrue(startBoundary.intersects(new Boundary(11, 12)));
|
||||||
|
assertTrue(startBoundary.intersects(new Boundary(11, 100)));
|
||||||
|
assertFalse(startBoundary.intersects(new Boundary(100, 101)));
|
||||||
|
assertTrue(startBoundary.intersects(new Boundary(99, 101)));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
package com.iqser.red.service.redaction.v1.server.utils;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.geom.Rectangle2D;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
|
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
|
@UtilityClass
|
||||||
|
public class PdfDraw {
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
public static void drawRectangle2DList(PDDocument document, int pageNumber, List<Rectangle2D> rectCollection, Color color) {
|
||||||
|
var pdPage = document.getPage(pageNumber - 1);
|
||||||
|
var contentStream = new PDPageContentStream(document, pdPage, PDPageContentStream.AppendMode.APPEND, true);
|
||||||
|
contentStream.setStrokingColor(color);
|
||||||
|
for (var r : rectCollection) {
|
||||||
|
contentStream.addRect((float) r.getMinX(), (float) r.getMinY(), (float) r.getWidth(), (float) r.getHeight());
|
||||||
|
contentStream.stroke();
|
||||||
|
}
|
||||||
|
contentStream.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
package drools
|
||||||
|
|
||||||
|
import static java.lang.String.format;
|
||||||
|
import static com.iqser.red.service.redaction.v1.server.document.services.RegexMatcher.anyMatch;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.*
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.nodes.*
|
||||||
|
import com.iqser.red.service.redaction.v1.server.redaction.model.EntityType;
|
||||||
|
import com.iqser.red.service.redaction.v1.model.FileAttribute;
|
||||||
|
import com.iqser.red.service.redaction.v1.model.Engine;
|
||||||
|
import com.iqser.red.service.persistence.service.v1.api.model.dossiertemplate.type.DictionaryEntry;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.redaction.utils.EntitySearchUtils;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
global DocumentGraph document
|
||||||
|
|
||||||
|
|
||||||
|
rule "1: Redact CBI_author"
|
||||||
|
|
||||||
|
when
|
||||||
|
FileAttribute(label == "Vertebrate Study" && (value.toLowerCase() == "yes"))
|
||||||
|
entity: EntityNode(type == "CBI_author")
|
||||||
|
then
|
||||||
|
entity.setRedact(true);
|
||||||
|
update(entity)
|
||||||
|
end
|
||||||
|
|
||||||
|
rule "2: do not redact genitive CBI_author"
|
||||||
|
|
||||||
|
when
|
||||||
|
entity: EntityNode(type == "CBI_author", anyMatch("['’’'ʼˈ´`‘′ʻ’']s", textAfter), redact == true)
|
||||||
|
then
|
||||||
|
entity.setRedact(false);
|
||||||
|
entity.setEntityType(EntityType.FALSE_POSITIVE);
|
||||||
|
update(entity)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package drools
|
||||||
|
|
||||||
|
import static java.lang.String.format;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
|
||||||
|
import com.iqser.red.service.redaction.v1.server.document.graph.*
|
||||||
|
import com.iqser.red.service.redaction.v1.server.redaction.model.*;
|
||||||
|
import com.iqser.red.service.redaction.v1.model.FileAttribute;
|
||||||
|
import com.iqser.red.service.redaction.v1.model.Engine;
|
||||||
|
import com.iqser.red.service.persistence.service.v1.api.model.dossiertemplate.type.DictionaryEntry;
|
||||||
|
import com.iqser.red.service.redaction.v1.server.redaction.utils.EntitySearchUtils;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
global DocumentGraph document;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
rule "1: Redact CBI_author"
|
||||||
|
when
|
||||||
|
FileAttribute(label == "Vertebrate Study" && (value == "Yes" || value == "Y" || value == "y"))
|
||||||
|
$entity: Entity2(type == "CBI_author")
|
||||||
|
then
|
||||||
|
modify($entity){
|
||||||
|
$entity.setRedact(true)
|
||||||
|
}
|
||||||
|
end
|
||||||
Loading…
x
Reference in New Issue
Block a user